基于ffmpeg与ffprobe的视频裁剪脚本问题排查与优化需求
视频预处理ETL脚本问题修复与功能扩展
问题1:黑屏裁剪误删后续有效帧修复
原脚本的区间生成逻辑错误,导致仅保留首个有效片段。修复思路是先获取视频总时长,再基于黑屏时间段反向推导所有非黑屏的有效区间:
def get_video_duration(inpath): cmd = f'ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "{inpath}"' return float(subprocess.check_output(shlex.split(cmd)).decode("utf-8").strip()) def get_blackdetect(inpath, invert=False): ffprobe_cmd = f'ffprobe -f lavfi -i "movie={inpath},blackdetect[out0]" -show_entries tags=lavfi.black_start,lavfi.black_end -of default=nw=1 -v quiet' print("ffprobe_cmd:", ffprobe_cmd) lines = ( subprocess.check_output(shlex.split(ffprobe_cmd)) .decode("utf-8") .split("\n") ) times = [ float(x.split("=")[1].strip()) for x in delete_back2back(lines) if x ] # 整理黑屏时间段 black_intervals = [(times[i], times[i+1]) for i in range(0, len(times), 2)] if times else [] total_duration = get_video_duration(inpath) if not invert: # 生成非黑屏的有效区间 timepairs = [] prev_end = 0.0 for start, end in black_intervals: if start > prev_end: timepairs.append((prev_end, start)) prev_end = end # 添加最后一段非黑屏区间 if prev_end < total_duration: timepairs.append((prev_end, total_duration)) # 如果没有黑屏,保留整个视频 if not timepairs: timepairs.append((0.0, total_duration)) else: # invert模式下保留黑屏区间 timepairs = black_intervals or [(0.0, total_duration)] return timepairs
问题2:扩展检测彩色画面与模糊片段
通过组合ffmpeg的colordetect和blurdetect滤镜,实现绿屏、红屏及模糊片段的检测,并合并所有需要移除的时间段:
def get_remove_intervals(inpath): total_duration = get_video_duration(inpath) remove_intervals = [] # 1. 黑屏检测 ffprobe_black = f'ffprobe -f lavfi -i "movie={inpath},blackdetect[out0]" -show_entries tags=lavfi.black_start,lavfi.black_end -of default=nw=1 -v quiet' lines_black = subprocess.check_output(shlex.split(ffprobe_black)).decode("utf-8").split("\n") times_black = [float(x.split("=")[1].strip()) for x in delete_back2back(lines_black) if x] remove_intervals.extend([(times_black[i], times_black[i+1]) for i in range(0, len(times_black), 2)]) # 2. 绿屏检测(替换color=0xff0000可实现红屏检测) ffprobe_green = f'ffprobe -f lavfi -i "movie={inpath},colordetect=color=0x00ff00:threshold=0.1[out0]" -show_entries tags=lavfi.color_start,lavfi.color_end -of default=nw=1 -v quiet' lines_green = subprocess.check_output(shlex.split(ffprobe_green)).decode("utf-8").split("\n") times_green = [float(x.split("=")[1].strip()) for x in delete_back2back(lines_green) if x] remove_intervals.extend([(times_green[i], times_green[i+1]) for i in range(0, len(times_green), 2)]) # 3. 模糊检测(threshold值越小对模糊越敏感) ffprobe_blur = f'ffprobe -f lavfi -i "movie={inpath},blurdetect=threshold=10[out0]" -show_entries tags=lavfi.blur_start,lavfi.blur_end -of default=nw=1 -v quiet' lines_blur = subprocess.check_output(shlex.split(ffprobe_blur)).decode("utf-8").split("\n") times_blur = [float(x.split("=")[1].strip()) for x in delete_back2back(lines_blur) if x] remove_intervals.extend([(times_blur[i], times_blur[i+1]) for i in range(0, len(times_blur), 2)]) # 合并重叠或相邻的移除区间 if not remove_intervals: return [] remove_intervals.sort() merged = [remove_intervals[0]] for current in remove_intervals[1:]: last = merged[-1] if current[0] <= last[1]: merged[-1] = (last[0], max(last[1], current[1])) else: merged.append(current) return merged def get_valid_timepairs(inpath, invert=False): remove_intervals = get_remove_intervals(inpath) total_duration = get_video_duration(inpath) if invert: # invert模式下保留需要移除的区间 valid_pairs = remove_intervals or [(0.0, total_duration)] else: # 正常模式下保留非移除区间 valid_pairs = [] prev_end = 0.0 for start, end in remove_intervals: if start > prev_end: valid_pairs.append((prev_end, start)) prev_end = end if prev_end < total_duration: valid_pairs.append((prev_end, total_duration)) valid_pairs = valid_pairs if valid_pairs else [(0.0, total_duration)] return valid_pairs
问题3:无音频时脚本适配
通过检测视频是否包含音频流,动态调整ffmpeg命令,避免无音频时的错误:
def has_audio_stream(inpath): cmd = f'ffprobe -v error -select_streams a -show_entries stream=codec_type -of default=noprint_wrappers=1:nokey=1 "{inpath}"' result = subprocess.run(shlex.split(cmd), capture_output=True, text=True) return result.stdout.strip() == "audio" def construct_ffmpeg_trim_cmd(timepairs, inpath, outpath): has_audio = has_audio_stream(inpath) cmd = f'ffmpeg -i "{inpath}" -y -r 20 -filter_complex ' cmd += '"' for i, (start, end) in enumerate(timepairs): # 处理视频流 cmd += f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS,format=yuv420p[{i}v]; " # 有音频时才处理音频流 if has_audio: cmd += f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS[{i}a]; " # 拼接流 for i, _ in enumerate(timepairs): cmd += f"[{i}v]" if has_audio: cmd += f"[{i}a]" if has_audio: cmd += f"concat=n={len(timepairs)}:v=1:a=1[outv][outa]" else: cmd += f"concat=n={len(timepairs)}:v=1:a=0[outv]" cmd += '"' # 映射输出流 cmd += f' -map [outv]' if has_audio: cmd += f' -map [outa]' cmd += f' "{outpath}"' return cmd
完整修改后的脚本
import argparse import os import shlex import subprocess parser = argparse.ArgumentParser( __doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument("input", type=str, help="input video file") parser.add_argument( "--invert", action="store_true", help="remove non-black/color/blur instead of removing them", ) args = parser.parse_args() os.chdir(os.path.split(args.input)[0]) args.input = os.path.split(args.input)[1] spl = args.input.split(".") outpath = ( ".".join(spl[:-1]) + "." + ("invert" if args.invert else "") + "out." + spl[-1] ) def delete_back2back(l): from itertools import groupby return [x[0] for x in groupby(l)] def get_video_duration(inpath): cmd = f'ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "{inpath}"' return float(subprocess.check_output(shlex.split(cmd)).decode("utf-8").strip()) def has_audio_stream(inpath): cmd = f'ffprobe -v error -select_streams a -show_entries stream=codec_type -of default=noprint_wrappers=1:nokey=1 "{inpath}"' result = subprocess.run(shlex.split(cmd), capture_output=True, text=True) return result.stdout.strip() == "audio" def get_remove_intervals(inpath): total_duration = get_video_duration(inpath) remove_intervals = [] # 黑屏检测 ffprobe_black = f'ffprobe -f lavfi -i "movie={inpath},blackdetect[out0]" -show_entries tags=lavfi.black_start,lavfi.black_end -of default=nw=1 -v quiet' lines_black = subprocess.check_output(shlex.split(ffprobe_black)).decode("utf-8").split("\n") times_black = [float(x.split("=")[1].strip()) for x in delete_back2back(lines_black) if x] remove_intervals.extend([(times_black[i], times_black[i+1]) for i in range(0, len(times_black), 2)]) # 绿屏检测(可修改color参数为0xff0000实现红屏检测) ffprobe_green = f'ffprobe -f lavfi -i "movie={inpath},colordetect=color=0x00ff00:threshold=0.1[out0]" -show_entries tags=lavfi.color_start,lavfi.color_end -of default=nw=1 -v quiet' lines_green = subprocess.check_output(shlex.split(ffprobe_green)).decode("utf-8").split("\n") times_green = [float(x.split("=")[1].strip()) for x in delete_back2back(lines_green) if x] remove_intervals.extend([(times_green[i], times_green[i+1]) for i in range(0, len(times_green), 2)]) # 模糊检测(threshold可根据需求调整,值越小对模糊越敏感) ffprobe_blur = f'ffprobe -f lavfi -i "movie={inpath},blurdetect=threshold=10[out0]" -show_entries tags=lavfi.blur_start,lavfi.blur_end -of default=nw=1 -v quiet' lines_blur = subprocess.check_output(shlex.split(ffprobe_blur)).decode("utf-8").split("\n") times_blur = [float(x.split("=")[1].strip()) for x in delete_back2back(lines_blur) if x] remove_intervals.extend([(times_blur[i], times_blur[i+1]) for i in range(0, len(times_blur), 2)]) # 合并重叠区间 if not remove_intervals: return [] remove_intervals.sort() merged = [remove_intervals[0]] for current in remove_intervals[1:]: last = merged[-1] if current[0] <= last[1]: merged[-1] = (last[0], max(last[1], current[1])) else: merged.append(current) return merged def get_valid_timepairs(inpath, invert=False): remove_intervals = get_remove_intervals(inpath) total_duration = get_video_duration(inpath) if invert: # invert模式下保留需要移除的区间 valid_pairs = remove_intervals or [(0.0, total_duration)] else: # 正常模式下保留非移除区间 valid_pairs = [] prev_end = 0.0 for start, end in remove_intervals: if start > prev_end: valid_pairs.append((prev_end, start)) prev_end = end if prev_end < total_duration: valid_pairs.append((prev_end, total_duration)) valid_pairs = valid_pairs if valid_pairs else [(0.0, total_duration)] return valid_pairs def construct_ffmpeg_trim_cmd(timepairs, inpath, outpath): has_audio = has_audio_stream(inpath) cmd = f'ffmpeg -i "{inpath}" -y -r 20 -filter_complex ' cmd += '"' for i, (start, end) in enumerate(timepairs): cmd += f"[0:v]trim=start={start}:end={end},setpts=PTS-STARTPTS,format=yuv420p[{i}v]; " if has_audio: cmd += f"[0:a]atrim=start={start}:end={end},asetpts=PTS-STARTPTS[{i}a]; " for i, _ in enumerate(timepairs): cmd += f"[{i}v]" if has_audio: cmd += f"[{i}a]" if has_audio: cmd += f"concat=n={len(timepairs)}:v=1:a=1[outv][outa]" else: cmd += f"concat=n={len(timepairs)}:v=1:a=0[outv]" cmd += '"' cmd += f' -map [outv]' if has_audio: cmd += f' -map [outa]' cmd += f' "{outpath}"' return cmd if __name__ == "__main__": timepairs = get_valid_timepairs(args.input, invert=args.invert) cmd = construct_ffmpeg_trim_cmd(timepairs, args.input, outpath) print(cmd) os.system(cmd)
使用说明
- 彩色画面检测:修改
get_remove_intervals中的colordetect参数,将color=0x00ff00替换为目标颜色的十六进制值(如红屏0xff0000),threshold控制颜色匹配的宽容度 - 模糊检测:调整
blurdetect的threshold值,值越小对模糊的检测越敏感 - 无音频视频:脚本会自动检测并跳过音频处理,无需额外参数
内容的提问来源于stack exchange,提问作者adeshina Ibrahim
相关产品推荐
相关产品推荐

