Skip to content

Commit e80a6e4

Browse files
committed
add 2026-02-03-ffmpeg-select-frames
Signed-off-by: code4lala <fenglala@outlook.com>
1 parent e71fae3 commit e80a6e4

File tree

1 file changed

+206
-0
lines changed

1 file changed

+206
-0
lines changed
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
---
2+
layout: post
3+
comments: true
4+
title: "使用ffmpeg裁剪想要的帧"
5+
date: 2026-02-03 21:33:26 +0800
6+
tags: ffmpeg
7+
lang: zh
8+
---
9+
10+
`原创`
11+
12+
看到 https://www.youtube.com/watch?v=zzHeG7l6FT8 《【東方ボーカルMV】東方言えるかな》里有非常多漂亮的角色(我全都要),觉得很适合拿来做壁纸用,但是又有一些没有画面的场景,可以剪掉来提升观感。
13+
14+
成品链接:
15+
https://steamcommunity.com/sharedfiles/filedetails/?id=3659280460
16+
17+
在线观看:
18+
https://www.bilibili.com/video/BV1HGFLz8E1H
19+
20+
### 步骤:
21+
22+
1. 从youtube上下载视频和音频
23+
2. 使用ffmpeg合并视频和音频
24+
3. 使用ffmpeg导出所有帧为图片
25+
4. 人工过一遍所有图片,把没有人物的场景删掉
26+
5. 整理出来筛选后的图片帧序号并使用ffmpeg裁剪视频
27+
28+
### 从youtube上下载视频和音频
29+
30+
第一步这里使用
31+
https://snapany.com/zh/youtube-1
32+
直接解析youtube地址拿到googlevideo.com域名的链接来下载。
33+
这里下载到的视频和音频是两个独立的文件。
34+
35+
### 使用ffmpeg合并视频和音频
36+
37+
让AI生成一个ffmpeg命令
38+
39+
```sh
40+
ffmpeg -i videoplayback.mp4 -i videoplayback.m4a -c copy output.mp4
41+
```
42+
43+
### 使用ffmpeg导出所有帧为图片
44+
45+
让AI生成一个ffmpeg命令
46+
47+
```sh
48+
ffmpeg -i input.mp4 frame_%05d.png
49+
```
50+
51+
这里我导出了之后复制一份,一份是原始的,一份用来人工筛选后删除不需要的,方便后续剪视频。
52+
53+
### 让AI整理出来筛选后的图片帧序号并使用ffmpeg裁剪视频
54+
55+
直接让AI写个python脚本
56+
57+
```python
58+
import os
59+
import re
60+
import subprocess
61+
62+
def find_deleted_ranges(images_dir, images2_dir):
63+
all_frames = []
64+
for f in os.listdir(images2_dir):
65+
m = re.match(r"frame_(\d+)\.png", f)
66+
if m:
67+
all_frames.append(int(m.group(1)))
68+
all_frames.sort()
69+
70+
keep_frames = []
71+
for f in os.listdir(images_dir):
72+
m = re.match(r"frame_(\d+)\.png", f)
73+
if m:
74+
keep_frames.append(int(m.group(1)))
75+
keep_frames.sort()
76+
77+
keep_set = set(keep_frames)
78+
deleted = [n for n in all_frames if n not in keep_set]
79+
80+
print("\n===== 帧统计 =====")
81+
print(f"原视频总帧数: {len(all_frames)}")
82+
print(f"保留帧数: {len(keep_frames)}")
83+
print(f"删除帧数: {len(deleted)}")
84+
85+
if not deleted:
86+
return [], all_frames, keep_frames, deleted
87+
88+
ranges = []
89+
start = deleted[0]
90+
prev = deleted[0]
91+
92+
for n in deleted[1:]:
93+
if n == prev + 1:
94+
prev = n
95+
else:
96+
ranges.append((start, prev))
97+
start = n
98+
prev = n
99+
ranges.append((start, prev))
100+
101+
print("\n===== 删除的连续帧区间 =====")
102+
for s, e in ranges:
103+
print(f" - 从 {s}{e} (共 {e - s + 1} 帧)")
104+
105+
return ranges, all_frames, keep_frames, deleted
106+
107+
108+
def build_video_select(ranges):
109+
parts = []
110+
for s, e in ranges:
111+
parts.append(f"between(n,{s-1},{e-1})")
112+
return "not(" + "+".join(parts) + ")"
113+
114+
115+
def build_audio_select(ranges, fps):
116+
parts = []
117+
for s, e in ranges:
118+
t1 = (s - 1) / fps
119+
t2 = (e - 1) / fps
120+
parts.append(f"between(t,{t1},{t2})")
121+
return "not(" + "+".join(parts) + ")"
122+
123+
124+
def get_fps(ffmpeg, input_video):
125+
cmd = [ffmpeg, "-i", input_video]
126+
p = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
127+
out, err = p.communicate()
128+
fps_match = re.search(r"(\d+(?:\.\d+)?) fps", err)
129+
fps = float(fps_match.group(1)) if fps_match else 30.0
130+
print("\n原视频帧率 =", fps)
131+
return fps
132+
133+
134+
def get_frame_count(ffprobe, video_path):
135+
cmd = [
136+
ffprobe,
137+
"-v", "error",
138+
"-select_streams", "v:0",
139+
"-count_packets",
140+
"-show_entries", "stream=nb_read_packets",
141+
"-of", "csv=p=0",
142+
video_path
143+
]
144+
result = subprocess.run(cmd, capture_output=True, text=True)
145+
try:
146+
return int(result.stdout.strip())
147+
except:
148+
return None
149+
150+
151+
def main():
152+
ffmpeg = r".\ffmpeg-2026-01-29-git-c898ddb8fe-full_build\bin\ffmpeg.exe"
153+
ffprobe = r".\ffmpeg-2026-01-29-git-c898ddb8fe-full_build\bin\ffprobe.exe"
154+
155+
input_video = "videoplayback.mp4"
156+
output_video = "video_cut.mp4"
157+
158+
images_dir = "images" # 这个是删过帧只保留了人物画面的文件夹
159+
images2_dir = "images2" # 这个是原始的所有帧的文件夹
160+
161+
ranges, all_frames, keep_frames, deleted = find_deleted_ranges(images_dir, images2_dir)
162+
if not ranges:
163+
return
164+
165+
fps = get_fps(ffmpeg, input_video)
166+
167+
video_select = build_video_select(ranges)
168+
audio_select = build_audio_select(ranges, fps)
169+
170+
print("\n===== 视频过滤表达式 =====")
171+
print(video_select)
172+
173+
print("\n===== 音频过滤表达式(按时间) =====")
174+
print(audio_select)
175+
176+
cmd = [
177+
ffmpeg,
178+
"-i", input_video,
179+
"-vf", f"select='{video_select}',setpts=N/({fps}*TB)",
180+
"-af", f"aselect='{audio_select}',asetpts=N/SR/TB",
181+
"-c:v", "libx264",
182+
"-c:a", "aac",
183+
"-pix_fmt", "yuv420p",
184+
"-y",
185+
output_video
186+
]
187+
188+
print("\n===== 开始裁剪视频(含音频) =====")
189+
subprocess.run(cmd)
190+
print("\n完成!输出文件:", output_video)
191+
192+
actual_frames = get_frame_count(ffprobe, output_video)
193+
194+
print("\n===== 裁剪后帧数验证 =====")
195+
print(f"预期保留帧数: {len(keep_frames)}")
196+
print(f"实际视频帧数: {actual_frames}")
197+
198+
if actual_frames == len(keep_frames):
199+
print("✔ 帧数完全一致,音视频同步裁剪成功")
200+
else:
201+
print("✘ 帧数不一致,请检查源数据")
202+
203+
204+
if __name__ == "__main__":
205+
main()
206+
```

0 commit comments

Comments
 (0)