我们的监控类素材有个特点:画面里 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 编码是什么,为什么有效
- 二、残酷现实:命令行层面能做什么
- 三、zones:时间维度的质量分区
- 四、x265 的 roi-file 与限制
- 五、方案二:背景预处理(最实用)
- 六、方案三:分区编码 + overlay 合成
- 七、自动 ROI:用检测结果生成参数
- 八、监控/会议/课程三类场景的实测
- 九、什么时候不值得做
- 十、坑清单
一、ROI 编码是什么,为什么有效
ROI 编码:给画面中指定的区域分配更多码率(降低 QP),其他区域相应减少。
为什么有效:
- 人的注意力是不均匀的——看监控看的是人脸/车牌,看课程看的是 PPT 区域,看会议看的是发言的人;
- 码率预算是固定的——把它从"没人看的背景"挪到"有人的前景",主观质量提升明显;
- 编码器的默认分配是按"复杂度"而不是按"重要性"——噪点多的背景(监控的低照度噪声)会被判定为"复杂",吃掉大量码率。
一个直观的例子:监控画面里,地面砖块的纹理(不重要但细节多)可能比人脸(重要但平滑)分到更多码率。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 的一个薄封装,没暴露这些。
所以务实的路线是三条:
- 用
zones做时间维度的质量分配(比如片头片尾低质量、正片高质量); - 用预处理间接实现空间 ROI(让背景变得"不值得花码率");
- 真需要严格的空间 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"
注意:
- 帧号不是秒数——要按帧率换算(30fps 下,第 9000 帧 = 300 秒);
- 多个区间用
/分隔; - 未覆盖的帧用全局 crf;
- 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 命令行上折腾,直接:
- 用 x265 的命令行工具(
x265本身,不是 ffmpeg); - 或者写代码调 libx265 的 API;
- 或者用下面的替代方案。
五、方案二:背景预处理(最实用)
这是我最后采用的主力方案,因为它简单、可控、效果明显。
思路:既然不能"命令编码器给哪里多码率",那就让不重要区域变得不值得花码率——对背景做轻度降噪/模糊,编码器自然会把省下的码率分配给前景。
实现(用 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
解释:
split复制两路;- 一路裁出"重要区域"(画面中央,假设那是主体所在),做轻度降噪(保留细节);
- 另一路(整帧)做较强的降噪 + 轻微模糊(背景被平滑);
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
代价:
- 三次编码(慢);
- 合成时又编码一次(画质二次损失)——除非合成用无损(但那体积巨大);
- 接缝处可能有可见的分界(因为两块的质量差异明显)。
适用场景:对某一小块区域的质量要求极高(比如监控里要能看清车牌,其他都无所谓),且能接受复杂的流水线。
我的实际使用:只在"客户要求能看清某个细节"这类特殊需求下用过两次。日常不用——方案二的性价比高得多。
七、自动 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。否则直接用方案二的固定区域(写死中央区域)就够了。
十、坑清单
- 以为 ffmpeg 有直接的 ROI 参数 → 没有。
-x264-params roi=...会报未知参数。 - zones 的帧号当成了秒 → 要按帧率换算。
- x265 的 zones 语法和 x264 不同 → 先小范围测试确认生效。
- ROI 区域没对齐 CTU 边界 → 行为不符合预期(区域偏移或者不生效)。对齐到 64 的倍数。
- 背景降噪过度 → 运动物体拖影、背景出现"鬼影"。
hqdn3d参数要保守。 - 模糊半径太大 → 一眼看出背景被处理过,很假。1~2 就够。
- 主体检测错了 → 把不重要的区域当主体,重要的反而被降质。要抽查检测结果的截图。
- overlay 的位置算错 → 合成后主体偏移。crop 和 overlay 的坐标要一致。
- 合成时又编码一次 → 二次质量损失。这是方案三的主要缺点。
- 对 PPT/全屏文字用了 ROI → 整屏都重要,ROI 只会让部分变糊。
- 没验证主体区域的实际质量 → 一定要单独裁出主体区域测 VMAF(整体的 VMAF 会被背景稀释,看不出变化)。
- 只测一帧就下结论 → 主体位置会变,要测多个时间点。
- 预处理让背景出现了块效应 → 降噪太强 + 码率太低。降低降噪强度。
- ROI 参数写死在多台机器上不一致 → 编码器版本不同导致行为不同。统一版本。
- 期待 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
# 然后对比这两个