智能摄像头系统 30 天学习计划 Day 08 · FFmpeg:成为你的“视频瑞士军刀”

环境:Raspberry Pi(CSI 摄像头 imx219,Debian 13,aarch64)· FFmpeg 7.1.5 · libcamera v0.7.2

目标:把"会抄命令"变成"看得懂命令,并且能用数据验证"。

先说结论

  1. 能 -c copy 就不要重编码。 remux 速度约 66--72 倍实时,软编 720p30 只有 0.4--1.0 倍。
  2. CRF 每加 5 左右,体积大约减半。 真实摄像头画面比合成画面更敏感,CRF 28 是我这段素材上的"拐点前最优点"。
  3. 推流用 -b:v,不用 CRF。 硬编的码率控制很准(偏差不到 1%),SSIM 均值和同码率的 x264 基本相当。
  4. 我差点冤枉了硬编。 重编后第一帧有明显马赛克,我一路怀疑 I 帧预算、内容难度、码控起步;最后抽图才发现:源文件 test.mp4 的第 1 帧自己就有马赛克,硬编和 x264 重编码一视同仁地继承了它。SSIM 看不出来,因为参照物自带同样的缺陷。
  5. SSIM 只能看趋势,而且参照本身有缺陷时它会"原谅"缺陷。 最后还是要看图。

1. 一条命令的骨架

text 复制代码
ffmpeg [全局参数] [输入参数] -i 输入 [输出参数] 输出

参数跟着它后面 的文件走。内部流程是 demux → decode → filter → encode → mux,-c copy 直接跳过中间三步,所以又快又无损,代价是不能同时改分辨率、帧率、码率。

另外一个常被混淆的点:容器不等于编码。mp4、mkv 只是盒子,H.264、AAC 才是压缩方式。

2. 用 ffprobe 看清素材

先别急着转码,读一读素材本身。我手上有两份:

  • sample.mp4:testsrc2 合成画面,720p30,10 秒(x264 默认参数)。
  • test.mp4:树莓派摄像头硬编直出,1280x720@30,约 4.4 Mbps。

最有信息量的一项是 I 帧间隔:

sample.mp4(x264 默认) 摄像头硬编
I 帧间隔 8.33 s(keyint=250) 2.0 s(每 60 帧)
观众中途加入最坏要等 约 8.3 s 约 2 s

查看 I 帧位置的推荐写法(比解析帧类型列表更省事):

bash 复制代码
ffprobe -v error -select_streams v:0 -skip_frame nokey \
  -show_entries frame=pts_time -of csv=p=0 in.mp4

3. remux 与转码:差了 80 倍

操作 速度
mp4 ↔ mkv(-c copy) 约 66--72x
软编 720p30(x264 medium) 0.8--0.99x

在树莓派上,软编 720p30 已经跑不到实时,更别提同时做别的事。这就是后面"推流链路能不重编码就不重编码"的根据。

顺带几条转码实验的结论(输入 sample.mp4):

  • 对已经压缩过的视频用同样的 CRF 重编码,只小了约 4%,还多损失一代画质。
  • -b:v 1M 能把码率锁在 1040 kb/s,CRF 23 则是 2857 kb/s 且随画面浮动;代价是 1M 版的平均 QP 明显更高(I/P/B 为 21/33/37,对比 16.5/25/30)。
  • 分辨率砍到 1/4 面积,码率降到约 14%;帧率减半,码率只降约 35%,因为帧间差异变大,P/B 帧要多花比特。
  • 降帧率后 GOP 的"秒数"会变:fps=15 之后 keyint=250 等于约 16.7 秒一个 I 帧,-g 要按目标帧率换算。
  • H.265 在 Pi 上不划算:体积小约 33%,但速度只有 0.38x。

4. CRF 到底选多少

我分别用合成画面和真实摄像头画面各做了一组 CRF 18/23/28/35。

合成画面 (体积相对 CRF 23):1.46x / 1.00x / 0.48x / 0.22x,SSIM 从 0.9985 掉到 0.9780。肉眼几乎看不出差别,因为画面只有色块和线条,没有细节可丢。这组素材不适合用来判断画质取舍。

真实画面 (test.mp4,原片 5227 KiB):

CRF 码率 占原片体积 SSIM 编码速度 主观画质
18 3545 kb/s 81% 0.9951 0.37x 与 CRF 28 接近
23 1583 kb/s 36% 0.9924 0.50x 未单独评
28 593 kb/s 13% 0.9890 0.69x 与 CRF 18 接近
35 187 kb/s 4% 0.9826 0.90x 明显较差

几个值得记住的点:

  • 真实画面对 CRF 更敏感:CRF 18 与 35 的体积差 18.9 倍(合成画面是 6.8 倍),因为低 CRF 会连传感器噪点一起保留。
  • 摄像头默认的 4.4 Mbps 很浪费:同一素材 CRF 28 软编只要 593 kb/s,我在 Mac 上对比,和 CRF 18 看不出区别。
  • 软编耗时随 CRF 明显变化(10.8--26.3 s),真实 720p30 画面上常用 CRF 范围内全都跑不到实时。合成画面看不出这点(耗时几乎不变)。
  • SSIM 不能单独拍板:CRF 18→28 的 SSIM 下降(0.0061)和 28→35(0.0064)几乎一样,但前者肉眼看不出,后者明显变差。

5. 硬编:码率很准,压缩效率和 x264 相当

树莓派的硬件编码器是 h264_v4l2m2m(驱动 bcm2835-codec)。同样是 test.mp4,我分别编成 600k / 1000k / 2000k:

目标码率 实际码率 SSIM(全片均值) 对照
600k 约 604 kb/s 0.9882 x264 CRF 28:593 kb/s,0.9890
1000k 约 1004 kb/s 0.9906 ---
2000k 约 2011 kb/s 0.9935 x264 CRF 23:1583 kb/s,0.9924

码率控制偏差不到 1%,SSIM 均值也和同码率的 x264 差不多(600k 对 CRF 28 只差 0.0008)。

不过我在 Mac 上播放时,觉得 600k / 1000k 的第一帧有明显的马赛克 ,当时还以为是低码率下硬编的毛病。下一节讲这个"案子",剧透一下:冤枉了硬编。

6. 破案记:第一帧的马赛克,其实不是硬编的错

剧透:马赛克在源文件里,不是编码器引入的。下面是我怎么一步步走到这里的,包括走错的三步。

现象

用 h264_v4l2m2m 把 test.mp4 重编成 600k / 1000k,播放时第一帧有明显的马赛克;2000k 看起来没问题。全片 SSIM 均值却和同码率的 x264 差不多。

弯路一:I 帧预算不足,周期性变糊?------排除

最直觉的猜测是"低码率下 I 帧分到的比特太少,所以每隔一段时间变糊一下"。我做了两个检验。

检验一:逐帧大小。

bash 复制代码
for f in hw_600k hw_1000k hw_2000k; do
  echo $f
  ffprobe -v error -select_streams v:0 -show_entries frame=pict_type,pkt_size \
    -of csv=p=0 $f.mp4 \
  | awk -F, '{n[$2]++; s[$2]+=$1} END{for(k in n) printf "%s: %d 帧,平均 %.0f B\n",k,n[k],s[k]/n[k]}'
done

(小坑:ffprobe 的 csv 列顺序是它的内部顺序,这里是 pkt_size 在前、pict_type 在后,和 -show_entries 里写的顺序无关。)

目标码率 I 帧平均 P 帧平均 I / P
600k 6819 B 2108 B 3.2x
1000k 9958 B 3637 B 2.7x
2000k 16315 B 7628 B 2.1x

I 帧不但不小,反而码率越低,I 帧相对 P 帧分到的越多。(这组文件用的是 ffmpeg 默认的 -g 12,有 25 个 I 帧,和摄像头直出的每 60 帧一个不一致,后面重测时已经改成 -g 60。)

检验二:逐帧 SSIM。

bash 复制代码
ffmpeg -hide_banner -loglevel error -i hw_600k.mp4 -i test.mp4 \
  -lavfi ssim=stats_file=ssim_600k.log -f null -
awk 'NR==1||NR==2||NR==3||NR==61||NR==62||NR==63' ssim_600k.log | cut -d' ' -f1,5

第 1 帧(I)0.9788,第 2、3 帧§0.9757、0.9751;第 61 帧(I)0.9788,第 62、63 帧§0.9765、0.9772。I 帧并不比相邻 P 帧差,没有"周期性变糊"。

弯路二:开头的内容本身难编?------排除

用 -g 60 重编 600k 和 2000k,再加一个同码率的 x264 CRF 28(约 593 kb/s)做对照,看按秒平均 SSIM:

bash 复制代码
awk '{gsub("All:","",$5); s+=$5; if(NR%30==0){printf "第%d秒 %.4f\n",NR/30,s/30; s=0}}' ssim60_600k.log
秒 硬编 600k 硬编 2000k x264 CRF 28
1 0.9855 0.9943 0.9904
2 0.9883 0.9930 0.9878
3 0.9880 0.9932 0.9890
4 0.9869 0.9928 0.9880
5 0.9894 0.9933 0.9886
6 0.9911 0.9944 0.9897
7 0.9899 0.9940 0.9896
8 0.9909 0.9943 0.9896
9 0.9905 0.9939 0.9892

x264 的曲线是平的,第 1 秒反而最高,所以开头的画面并不比后面难编。硬编 600k 的第 1 秒比 x264 低约 0.005,第 5 秒起追平并略高。-g 60 也确认生效(两个文件都是 5 个 I 帧)。

弯路三:硬件码控起步,第一个 I 帧给得太少?------只是相关,不是原因

再看 hw60_600k 和 x264_crf28 前 30 帧的字节数:

硬编 600k x264 CRF 28
第 1 帧(I) 973 B 4451 B
第 1--10 帧平均 1193 B 3066 B
第 11--20 帧平均 1594 B 1703 B
第 21--30 帧平均 2493 B 1822 B
前 30 帧合计 约 52.8 KB(≈ 422 kb/s) 约 65.9 KB(≈ 527 kb/s)

第一个 I 帧只有 973 字节,不到 x264 的 1/4。我当时把它当成了头号嫌疑:硬件码控起步时给第一帧的比特极少,所以马赛克。听起来很顺,但还没看图。

破案:抽图

对四个文件各抽第 1 帧和第 61 帧:

bash 复制代码
for f in test hw60_600k hw60_2000k x264_crf28; do
  ffmpeg -hide_banner -loglevel error -y -i $f.mp4 -vf "select='eq(n,0)+eq(n,60)'" -fps_mode vfr ${f}_f%d.png
done

拷到 Mac 上并排看:

文件 第 1 帧 第 61 帧
test.mp4(源文件) 马赛克明显 较好
hw60_600k 马赛克明显 较好
hw60_2000k 马赛克明显 较好
x264_crf28 马赛克明显 较好

四个文件的第 1 帧全都有马赛克,包括源文件和软编的 x264;第 61 帧全都正常。 马赛克在 test.mp4 的第 1 帧里就有,重编码(硬编、软编、600k 还是 2000k)只是忠实地继承了它。

为什么 SSIM 一直没发现

我计算 SSIM 时,拿 test.mp4 当参照。参照物的第 1 帧本身就带着马赛克,编码器把它复现得越忠实,得分反而越高。所以不管是全片均值、按秒平均,还是第 1 帧自己的 SSIM(0.9788,并不低),都看不出问题。指标只能衡量"和参照有多像",参照本身有缺陷时,它会连缺陷一起原谅。

需要收回的说法

  • "600k / 1000k 有马赛克,2000k 没有":逐帧抽图不成立,2000k 的第 1 帧同样有。我之前的印象来自播放,可能是马赛克持续的时间或严重程度不同,但我没有逐帧证据。
  • "硬件码控起步给第一个 I 帧极少":不再是主要解释。973 字节与"源头是一幅块状的画面(块状画面好压)"也相容,我没有进一步区分。
  • "码率不要低于 1 Mbps,否则第一帧有马赛克":这个理由不成立。600k 的第 61 帧画质较好。(但我只抽了两帧,运动剧烈的画面还没验证。)

还没解释的

  1. 源头为什么有 :test.mp4 是 rpicam-vid 录的,默认码率约 4.4 Mbps,在这个码率下第 1 帧仍有马赛克,说明不只是"码率太低"。候选原因:相机刚启动(前 5 帧已经丢掉预热,但曝光/白平衡可能还没完全稳定),或者硬件编码器起步时第一个 I 帧偏粗。
  2. 硬编 600k 第 1 秒 SSIM 偏低(0.9855)而 2000k 不低:这个小差距是否另有原因,我没查清。
观察到 说明
2 Mbps 和 8 Mbps 的第 1 帧都有马赛克 与码率无关,更像相机刚启动或编码器起步
8 Mbps 干净、2 Mbps 有 起步时给的比特太少,提高码率有用
pipe_test_f1.png 也有 这条链路每次都会这样,不是偶然
马赛克只在第 0、1 帧,第 5 帧已经干净 只影响第一个 I 帧附近的极少几帧

实际影响

  • 重编码救不了 :坏的参考帧会被一直继承,想要干净的开头,只能丢掉开头(比如用 -c copy 从第二个 I 帧开始截,注意第 7 节裁剪的起点近似问题;这个做法我还没验证)。
  • 推流影响有限:观众中途加入拿到的是后续的 I 帧;受影响的主要是录制文件的开头和起播的第一秒。
  • 720p 主码流仍然沿用 2 Mbps (Day 07 的选择),但这次的马赛克不能再作为"1 Mbps 以下不可用"的证据。
  • 方法上的教训:对比画质要看"独立于参照"的东西------抽图、看实际画面,而不是只信指标。

7. 裁剪、分段与 I 帧:同一个根源

-c copy 只能从 I 帧开始拷贝,所以 I 帧间隔会直接影响裁剪和分段:

  • 裁剪 (-ss 3 -t 4):-c copy 得到 122 帧 / 4.1 s,文件 2681 KiB,速度 359x;重编码是 120 帧 / 4.0 s,1432 KiB,速度 0.89x。前者的预卷数据仍写在文件里,靠 mp4 编辑列表在播放时跳过,起点近似准确,但文件约 1.9 倍大。我一开始还以为"多出了 3 秒内容",用 ffprobe -count_frames 实测才发现是编辑列表隐藏了预卷帧。
  • 分段 :sample.mp4 的 I 帧只有 0 s / 8.33 s,segment_time 3 只得到 2 个文件。摄像头素材每 2 秒一个 I 帧,segment_time 4 却切出 6.0 s + 3.7 s,原因是第 2 个 I 帧的时间戳是 3.99996,差 0.00004 秒,没被算作到达 4 秒。加上 -segment_time_delta 0.05 就正常了。

8. 在树莓派上录制

  • 不指定分辨率,rpicam-vid 默认录出 640x480 ,务必显式写 --width/--height/--framerate,录完用 ffprobe 核对。
  • 文件方式和管道方式都得到 292 帧、9.733 s、1280x720,全程约 30 fps、无丢帧(前 5 帧是相机预热,不录制)。
  • 未指定 --bitrate 时码率在 4.4--5.3 Mbps,偏高;指定 --bitrate 2000000 后 CPU 约 10%(单次采样),软编则是 342%。
  • 管道直转 mp4 的写法:rpicam-vid ... -o - | ffmpeg -f h264 -framerate 30 -i - -c copy out.mp4。

9. 我踩过的坑

现象 原因 / 解决
`ffprobe ... grep "^I"` 没有输出
SSH 下 rpicam-vid 报 Failed to create egl/drm preview 没有显示环境,加 -n
-vsync is deprecated 改用 -fps_mode vfr
硬编文件有 25 个 I 帧,预期 5 个 h264_v4l2m2m 默认 -g 12,显式写 -g 60
重编码后第一帧有马赛克,指标却看不出 源文件第 1 帧本身就有,参照物自带缺陷;对比时要抽图看,别只信 SSIM

10. 速查表

bash 复制代码
# 看信息
ffprobe -hide_banner in.mp4
ffprobe -v error -select_streams v:0 -skip_frame nokey -show_entries frame=pts_time -of csv=p=0 in.mp4   # I 帧位置

# 转换
ffmpeg -i in.mkv -c copy out.mp4                                                    # 转封装(无损)
ffmpeg -framerate 30 -i in.h264 -c copy out.mp4                                     # 裸流转 mp4
ffmpeg -i in.mp4 -c:v libx264 -preset medium -crf 23 -c:a aac out.mp4               # CRF 压缩
ffmpeg -i in.mp4 -c:v libx264 -b:v 1M -maxrate 1M -bufsize 2M out.mp4               # 指定码率
ffmpeg -i in.mp4 -c:v h264_v4l2m2m -b:v 2M -g 60 out.mp4                            # Pi 硬编(务必写 -g)

# 截图与裁剪
ffmpeg -ss 5 -i in.mp4 -frames:v 1 -q:v 2 shot.jpg                                  # 截一帧
ffmpeg -ss 00:00:03 -i in.mp4 -t 4 -c copy clip.mp4                                 # 快速裁剪(起点近似)
ffmpeg -ss 00:00:03 -i in.mp4 -t 4 -c:v libx264 -crf 23 -c:a aac clip.mp4           # 精确裁剪

# 摄像头录制
rpicam-vid -n -v 0 -t 10000 --width 1280 --height 720 --framerate 30 --bitrate 2000000 --codec h264 --inline -o cam.h264

# 分段录制
ffmpeg ... -c copy -f segment -segment_time 60 -segment_time_delta 0.05 -reset_timestamps 1 -strftime 1 "rec_%Y%m%d_%H%M%S.mp4"

小结

今天最大的收获不是某条命令,而是一个习惯:每一个结论都要有数据,而且数据要选对粒度。

这次的马赛克就是反面教材:全片 SSIM、按秒 SSIM、逐帧大小我都看了,还顺着数据编出了"硬件码控起步"这样顺口的解释;而真正的答案,一张 PNG 就够了------马赛克在源文件里,硬编是被冤枉的。

更要紧的是,指标是拿参照物来比的,参照物自己有缺陷时,指标会一起原谅它。

如果你也在树莓派上折腾视频,三条可以直接带走:能 -c copy 就别重编码;硬编一定显式写 -g;评价画质别只看一个数字,先抽几张图看看。

相关推荐
zyseo81 小时前
谷歌SEO 排名因素权重地图:把精力花在刀刃上
1024程序员节
basketball6161 小时前
动态规划第一章:引论
算法·动态规划·1024程序员节
CRMEB系统商城1 小时前
家电零售的供应商协同与物流提效
java·开发语言·人工智能·小程序·1024程序员节
xiaoniu1428571 小时前
命题的映射化
数学·1024程序员节·高中数学·文化课
jason.zeng@15022071 小时前
Nginx 配置详解(静态 HTML、下载目录、API 反向代理三套场景)
java·vue.js·1024程序员节
鬼手点金1 小时前
从 API 消息到模型 Token:Chat Template
java·前端·javascript·数据库·人工智能·深度学习·1024程序员节
kaixin_learn_qt_ing1 小时前
插件式框架学习(1)
1024程序员节·插件式框架
在所不辞兄1 小时前
【零基础学智能仿真-61】DFT数值收敛与计算预算
人工智能·深度学习·神经网络·机器学习·1024程序员节
Shadow(⊙o⊙)1 小时前
LangChain 自定义工具全解析:从 @tool 到 bind_tools 的完整调用链路
java·服务器·前端·1024程序员节