You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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)

使用说明

  1. 彩色画面检测:修改get_remove_intervals中的colordetect参数,将color=0x00ff00替换为目标颜色的十六进制值(如红屏0xff0000),threshold控制颜色匹配的宽容度
  2. 模糊检测:调整blurdetect的threshold值,值越小对模糊的检测越敏感
  3. 无音频视频:脚本会自动检测并跳过音频处理,无需额外参数

内容的提问来源于stack exchange,提问作者adeshina Ibrahim

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 23:36:19