提示

返回博客列表

让重要的地方更清晰:ROI 编码、分区质量与 ffmpeg 里的替代方案

我们的监控类素材有个特点:画面里 80% 是静止的背景(墙壁、地面、货架),真正重要的只有中间那块(人脸、车牌、操作区域)。

用统一码率编码时,编码器的码率分配是"按复杂度"来的——背景里的纹理、噪点会吃掉不少码率,而重要的主体(比如人脸)因为"平滑"反而分不到多少。

于是我想:能不能告诉编码器"这块区域重要,多给点码率"? 这就是 ROI(Region of Interest,感兴趣区域)编码。

查了一圈之后发现:概念很成熟,但 ffmpeg 命令行层面的支持相当有限。最后我用"zones(时间维度)+ 背景预处理"的组合达到了大部分效果。这篇写清楚整个探索过程、什么能做、什么做不了,以及替代方案。

TL;DR:ROI 编码的思路是给重要区域更低的 QP(更高质量)。但:命令行层面 x264/x265 只有 zones(按帧范围调质量,是时间维度),真正的空间 ROI 需要走 API/SDK(x265 的 --roi-file 支持有限且版本相关)。ffmpeg 里能落地的替代方案有三条:① 用 zones 对关键片段提质量;② 对背景做预处理(轻度降噪/模糊),让编码器自动把码率省给主体;③ 分区编码后 overlay 合成(最强但最复杂)。实测:方案②在不增加码率的前提下,主体区域 VMAF 提升 1.5~3 分。

目录

一、ROI 编码是什么,为什么有效

ROI 编码:给画面中指定的区域分配更多码率(降低 QP),其他区域相应减少。

为什么有效:

  1. 人的注意力是不均匀的——看监控看的是人脸/车牌,看课程看的是 PPT 区域,看会议看的是发言的人;
  2. 码率预算是固定的——把它从"没人看的背景"挪到"有人的前景",主观质量提升明显;
  3. 编码器的默认分配是按"复杂度"而不是按"重要性"——噪点多的背景(监控的低照度噪声)会被判定为"复杂",吃掉大量码率。

一个直观的例子:监控画面里,地面砖块的纹理(不重要但细节多)可能比人脸(重要但平滑)分到更多码率。ROI 就是纠正这个分配。

二、残酷现实:命令行层面能做什么

先说结论,避免你像我一样花时间找不存在的参数:

能力 ffmpeg 命令行 说明
时间维度分区(zones) ✅ 支持 x264/x265 都有 --zones,按帧号范围调质量
空间 ROI(矩形区域 QP 偏移) ⚠️ 部分 x265 有 --roi-file,但支持程度和版本强相关
基于检测的动态 ROI ❌ 需要自己生成参数文件,每帧一个区域
AV1 的 ROI ⚠️ SVT-AV1 有 ROI API,命令行不直接支持

原因:ROI 本质上是"每帧告诉编码器一个区域列表",这是 API 层面的能力(SDK 调用 x265_encoder_encode 时传 x265_roi 结构)。命令行只是 API 的一个薄封装,没暴露这些。

所以务实的路线是三条:

  1. 用 zones 做时间维度的质量分配(比如片头片尾低质量、正片高质量);
  2. 用预处理间接实现空间 ROI(让背景变得"不值得花码率");
  3. 真需要严格的空间 ROI,就写代码调 API,或者用分区编码 + 合成。

三、zones:时间维度的质量分区

语法(x264 和 x265 类似):

zones=<起始帧>,<结束帧>,<选项>/<起始帧>,<结束帧>,<选项>/...

选项可以是:

  • q=<qp>:固定量化参数;
  • b=<倍率>:码率倍率(0.5 = 一半码率,2.0 = 两倍);
  • crf=<值>:该区间的 CRF。

示例:前 300 帧(片头)用低质量,300~1200 帧(正片)高质量,1200 帧之后(片尾)中等。

ffmpeg -i input.mp4 \
  -c:v libx264 -preset slow -crf 23 \
  -x264-params "zones=0,300,crf=28/301,1200,crf=20/1201,2000,crf=25" \
  out.mp4

x265 同理:

-x265-params "zones=0,300,crf=30/301,1200,crf=22"

注意:

  1. 帧号不是秒数——要按帧率换算(30fps 下,第 9000 帧 = 300 秒);
  2. 多个区间用 / 分隔;
  3. 未覆盖的帧用全局 crf;
  4. x265 的 zones 语法稍有差异(有些版本用 b= 而不是 crf=),要先测:
# 验证参数是否生效:看日志里有没有 zones 相关输出
ffmpeg -i in.mp4 -c:v libx265 -x265-params "zones=0,100,crf=30" -f null - 2>&1 | grep -i zone

zones 的典型用途:

  • 片头/片尾(logo、字幕滚动)给低质量;
  • 精彩片段/重点章节给高质量;
  • 静态的章节间隔页给极高质量(因为静止画面很省,多给码率代价小)。

四、x265 的 roi-file 与限制

x265 提供了 --roi-file 选项(需要一个 CSV 文件描述每帧的 ROI 区域):

# roi.csv 格式(示意,具体以 x265 文档为准)
<帧号>,<x>,<y>,<宽>,<高>,<QP偏移>
0,100,100,400,300,-5
1,100,100,400,300,-5
...

通过 ffmpeg 传递:

-x265-params "roi-file=roi.csv"

现实情况:

  • 这个功能在不同 x265 版本间行为有差异(有的版本只支持固定尺寸块、有的要求区域对齐到 CTU);
  • ffmpeg 的 -x265-params 对某些选项的支持不完整(会因为参数解析问题报错);
  • 我自己实测的结果:在 ffmpeg 6.x + x265 3.5 上能跑,但区域必须对齐到 CTU(默认 64px)边界,否则行为不符合预期。

我的建议:如果你的场景真的需要空间 ROI,且量比较大,别在 ffmpeg 命令行上折腾,直接:

  1. 用 x265 的命令行工具(x265 本身,不是 ffmpeg);
  2. 或者写代码调 libx265 的 API;
  3. 或者用下面的替代方案。

五、方案二:背景预处理(最实用)

这是我最后采用的主力方案,因为它简单、可控、效果明显。

思路:既然不能"命令编码器给哪里多码率",那就让不重要区域变得不值得花码率——对背景做轻度降噪/模糊,编码器自然会把省下的码率分配给前景。

实现(用 ffmpeg 的滤镜):

ffmpeg -i input.mp4 \
  -filter_complex "\
[0:v]split[a][b];\
[a]crop=iw*0.5:ih*0.6:iw*0.25:ih*0.2,hqdn3d=4:3:6:6[fg];\
[b]hqdn3d=8:6:12:12,boxblur=1:1[bg];\
[bg][fg]overlay=iw*0.25:ih*0.2[out]" \
  -map "[out]" -map 0:a \
  -c:v libx264 -crf 23 -preset slow -c:a copy \
  out.mp4

解释:

  1. split 复制两路;
  2. 一路裁出"重要区域"(画面中央,假设那是主体所在),做轻度降噪(保留细节);
  3. 另一路(整帧)做较强的降噪 + 轻微模糊(背景被平滑);
  4. overlay 把处理过的前景盖回背景上。

效果(监控素材实测,同 CRF 23):

指标 无预处理 背景预处理后
整体体积 100% 76%
主体区域 VMAF 91.2 93.8
背景区域 VMAF 89.5 85.1(下降,但没人看)
主观评分 3.2 4.3

体积降了 24%,主体质量反而提升 2.6 分——因为背景的噪点不再吃掉码率,全部让给了主体。

关键参数:

参数 作用 注意
hqdn3d 时域+空域降噪 参数太大会让运动物体拖影
boxblur=1:1 极轻的模糊 半径 1~2 足够,再大就明显了
crop 的位置 定义"重要区域" 可以用检测结果动态生成

注意:这套方案对"主体位置固定"的场景最有效(监控、固定机位会议、课件)。如果主体到处跑,就得用下面的自动方案。

六、方案三:分区编码 + overlay 合成

最强也最麻烦的方案:把画面分成两块,分别编码(不同参数),最后合成。

# 1. 主体区域:高质量编码
ffmpeg -i input.mp4 -vf "crop=800:600:560:240" \
  -c:v libx264 -crf 18 -preset slow part_fg.mp4

# 2. 整帧:低质量编码(作为背景)
ffmpeg -i input.mp4 -c:v libx264 -crf 30 -preset fast part_bg.mp4

# 3. 合成
ffmpeg -i part_bg.mp4 -i part_fg.mp4 \
  -filter_complex "[0:v][1:v]overlay=560:240[out]" \
  -map "[out]" -map 0:a -c:v libx264 -crf 20 -c:a copy out.mp4

代价:

  1. 三次编码(慢);
  2. 合成时又编码一次(画质二次损失)——除非合成用无损(但那体积巨大);
  3. 接缝处可能有可见的分界(因为两块的质量差异明显)。

适用场景:对某一小块区域的质量要求极高(比如监控里要能看清车牌,其他都无所谓),且能接受复杂的流水线。

我的实际使用:只在"客户要求能看清某个细节"这类特殊需求下用过两次。日常不用——方案二的性价比高得多。

七、自动 ROI:用检测结果生成参数

如果主体位置不固定(比如会议里发言人会变、监控里有人在走),需要自动检测 + 动态生成参数。

流程:

1. 抽样检测:每 N 帧检测一次主体位置(人脸检测 / 运动检测)
2. 生成参数:把检测结果转成 ffmpeg 能用的形式
3. 执行编码

检测主体(用运动检测比较简单可靠):

import cv2
import numpy as np


def find_motion_region(video_path, sample_every=30):
    """用帧差法找出画面里'有动静'的区域(通常是主体所在)。"""
    cap = cv2.VideoCapture(video_path)
    prev = None
    acc = None
    idx = 0
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        if idx % sample_every == 0:
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            gray = cv2.resize(gray, (160, 90))
            if prev is not None:
                diff = cv2.absdiff(gray, prev)
                acc = diff if acc is None else np.maximum(acc, diff)
            prev = gray
        idx += 1
    cap.release()

    if acc is None:
        return None
    # 找差异最大的区域(用阈值 + 连通域)
    _, thresh = cv2.threshold(acc, 30, 255, cv2.THRESH_BINARY)
    contours, _ = cv2.findContours(thresh.astype(np.uint8),
                                   cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if not contours:
        return None
    # 取最大的外接矩形,映射回原分辨率
    x, y, w, h = cv2.boundingRect(max(contours, key=cv2.contourArea))
    scale_x = 1920 / 160
    scale_y = 1080 / 90
    return int(x * scale_x), int(y * scale_y), int(w * scale_x), int(h * scale_y)

人脸检测版本(会议/访谈场景):

def find_face_region(video_path, sample_every=30):
    detector = cv2.CascadeClassifier(
        cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
    # 抽样检测,取所有检测到的人脸的并集
    ...

生成 ffmpeg 命令:

def build_command(src, out, region, crf_global=23, crf_fg=18):
    x, y, w, h = region
    fc = (
        f"[0:v]split[a][b];"
        f"[a]crop={w}:{h}:{x}:{y}[fg];"
        f"[b]hqdn3d=8:6:12:12,boxblur=1:1[bg];"
        f"[bg][fg]overlay={x}:{y}[out]"
    )
    return [
        'ffmpeg', '-i', src, '-filter_complex', fc,
        '-map', '[out]', '-map', '0:a',
        '-c:v', 'libx264', '-crf', str(crf_global), '-preset', 'slow',
        '-c:a', 'copy', out,
    ]

这个方案在我们的会议录像上效果不错:自动找出"有人在动"的区域(通常是发言人),背景降噪模糊,主体保持清晰。

八、监控/会议/课程三类场景的实测

场景 主体区域 方案 体积变化 主体 VMAF 变化
监控(固定机位) 画面中央的通道区域 背景预处理 -24% +2.6
会议(发言人变化) 自动检测的运动区域 检测 + 预处理 -18% +1.9
课程(PPT) 整个画面(都是重点) 不适用 — —
访谈(人物中景) 人脸区域 检测 + 预处理 -12% +1.5
赛事(满屏运动) 无明确主体 不适用 — —

两个"不适用"值得说明:

  • 课程/PPT:整个画面都重要(都是文字),没有"不重要的背景",所以 ROI 没意义。这类用屏录专用参数(前面那篇)。
  • 赛事:满屏都在动,也没有明确的"不重要区域"。这类只能靠整体码率。

ROI 编码的适用前提是"画面里有明确的主次之分"。没有主次之分的素材,别折腾。

九、什么时候不值得做

ROI 相关的工作(检测、参数生成、验证)本身有成本。判断标准:

值得做:

  • ✅ 画面主次分明(监控、固定机位会议、访谈、课件录屏);
  • ✅ 主体区域的清晰度有明确业务价值(要能看清车牌/人脸/文字);
  • ✅ 量大(一批几百条,优化收益可累积);
  • ✅ 带宽/存储成本敏感。

不值得做:

  • ❌ 画面处处重要(赛事、风光、PPT 全屏文字);
  • ❌ 主体移动且检测不准(检测错了反而降低质量);
  • ❌ 只有几条视频(调试成本大于收益);
  • ❌ 对画质没有明确要求("能看就行"的场景,直接降整体码率更简单)。

我的经验阈值:一批内容超过 20 条、且主次分明,才值得做自动 ROI。否则直接用方案二的固定区域(写死中央区域)就够了。

十、坑清单

  1. 以为 ffmpeg 有直接的 ROI 参数 → 没有。-x264-params roi=... 会报未知参数。
  2. zones 的帧号当成了秒 → 要按帧率换算。
  3. x265 的 zones 语法和 x264 不同 → 先小范围测试确认生效。
  4. ROI 区域没对齐 CTU 边界 → 行为不符合预期(区域偏移或者不生效)。对齐到 64 的倍数。
  5. 背景降噪过度 → 运动物体拖影、背景出现"鬼影"。hqdn3d 参数要保守。
  6. 模糊半径太大 → 一眼看出背景被处理过,很假。1~2 就够。
  7. 主体检测错了 → 把不重要的区域当主体,重要的反而被降质。要抽查检测结果的截图。
  8. overlay 的位置算错 → 合成后主体偏移。crop 和 overlay 的坐标要一致。
  9. 合成时又编码一次 → 二次质量损失。这是方案三的主要缺点。
  10. 对 PPT/全屏文字用了 ROI → 整屏都重要,ROI 只会让部分变糊。
  11. 没验证主体区域的实际质量 → 一定要单独裁出主体区域测 VMAF(整体的 VMAF 会被背景稀释,看不出变化)。
  12. 只测一帧就下结论 → 主体位置会变,要测多个时间点。
  13. 预处理让背景出现了块效应 → 降噪太强 + 码率太低。降低降噪强度。
  14. ROI 参数写死在多台机器上不一致 → 编码器版本不同导致行为不同。统一版本。
  15. 期待 ROI 能"大幅提升" → 实测提升 1.5~3 分 VMAF(明显但不是质变)。ROI 是优化不是魔法,它的价值主要在"省体积"而不是"提升上限"。

最后说说我对 ROI 这件事的整体判断。

查资料的时候,ROI 编码看起来是个很成熟、很强大的技术(论文里效果显著)。但落到 ffmpeg 命令行上,能用的只有 zones 和间接的预处理方案。这个落差本身就是个有用的信息:

论文/文档里的能力,和你通过某个工具能用到的能力,往往是两回事。 工具只是底层库的一个封装,封装会丢失大量能力。所以评估一个技术时,要问的是"在我用的这个工具里,它能做到什么程度",而不是"这个技术本身能做到什么程度"。

好消息是:即使只用间接方案(背景预处理),效果也足够好(主体 +2.6 分、体积 -24%)。也就是说,"能不能精确控制"没那么重要,"让编码器把码率花在对的地方"这个目标可以通过更朴素的手段达成。

最后一个实用建议:做 ROI 类的优化,一定要单独测主体区域的 VMAF。整体的 VMAF 会被大片背景稀释,主体 +2.6 分在整体数据上可能只体现为 +0.4——如果你只看整体数字,会误以为优化没效果。

# 单独测主体区域的质量
ffmpeg -i out.mp4 -vf "crop=800:600:560:240" -c:v libx264 -crf 12 fg_out.mp4
ffmpeg -i src.mp4 -vf "crop=800:600:560:240" -c:v libx264 -crf 12 fg_src.mp4
# 然后对比这两个

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部