sws_scale 一件事干两件活:换像素格式,顺带改尺寸。
它在实战里常常比解码还贵,而省掉它的办法只有一个:别转,让显卡转。
09.04 拿到了 AVFrame,但那个帧的格式是解码器说了算的,不一定是渲染器想要的。
这一节讲这一层「翻译」,第九章里它是唯一一个「纯计算、没有状态」的环节——所以也是最容易量清楚的。
一、七个参数,两个用途
sws_getContext 的参数看着多,其实分两组:
| 组 | 参数 | 作用 |
|---|---|---|
| 来源 | 宽、高、像素格式 | 输入长什么样 |
| 目标 | 宽、高、像素格式 | 输出要什么样 |
| 算法 | flags |
缩放用哪种插值 |
两组参数一填,你就同时指定了「格式转换」和「尺寸缩放」。 所以「转换」和「缩放」在 FFmpeg 里是一个动作,不是两个。
这跟很多人的直觉不同:在图像库里 cvtColor 和 resize 是两个函数,
在 FFmpeg 里是一个。
理解这一点,你才不会去写「先转格式再缩放」的两步走——那等于算了两次。
flags 那一栏值得单独说:
SWS_BILINEAR 是默认,快而够用;SWS_BICUBIC 更清晰但更慢;SWS_FAST_BILINEAR 最快但最糊。
ffplay 会根据「是否缩小」自动挑一个,这个细节说明作者对性能很在意。
二、line_size 不等于宽度
这是本节最容易让人算错一个数的地方。frame.linesize[0] 常常大于 frame.width:
| 概念 | 含义 | 典型值 |
|---|---|---|
width |
一行的有效像素数 | 320 |
linesize[0] |
一行的实际字节数 | 320 或 384 |
为什么会有差:因为 CPU 和 SIMD 指令喜欢「按 32 或 64 字节对齐」的内存, 所以每行末尾会补几个字节的空白。
算错会怎样:把 width 当步长去读,每行都会偏一点,整个画面斜着花掉。
这是「YUV 数据看起来像噪声」的经典原因之一。
更麻烦的是它不固定:同一个分辨率,不同编码器、不同对齐设置,linesize 可能不一样。
所以「按 320 算」这种硬编码,换台机器就崩。
三、为什么不要用 CPU 转 RGB
Demo 把五种目标格式的耗时列了一遍,结论比预期强烈得多:
「不转」(yuv420p)是最便宜的,而转成 rgb24 贵了几十倍。
为什么差这么多:转 RGB 要对每个像素做一次矩阵乘, 还要把色度上采样(4:2:0 的色度只有亮度的四分之一)。 而「不转」只是把三块平面读出来,几乎不计算。
所以省掉这一步的价值,就是几十倍的时间。 怎么省:让渲染器直接吃 YUV 纹理, 色彩转换交给显卡在采样时顺手做——GPU 做这件事几乎不要钱,因为它本来就在做纹理采样。
这也解释了本课第六章的一个实验结论:用 SDL_UpdateYUVTexture 时「没省时间」,
因为软件渲染器会把转换搬进渲染器里做——省的是「一次内存拷贝」,不是「一次转换」。
要真省下来,得用硬件渲染器。
四、gray 为什么反而最贵
Demo 里有一行特别反直觉:转成 gray 的字节数比 yuv420p 还少,耗时却最长。
「数据量小就快」在这里不成立。 决定快慢的是「走哪条转换路径」,不是「搬了多少字节」。
这条规律可以推广:优化转换不是「少转几个像素」,而是「别转」。 要灰度,就让解码器出灰度,要 RGB,就让显卡去转。 在 CPU 上「少搬一点」,收益基本为零。
这类「反直觉但可解释」的结果,是本课坚持实跑的原因:
如果只看文档,你会以为 gray 便宜;真跑一遍,才知道它贵在路径上。
五、2026 现状:硬解与 GPU 转换
书上那一套是「软解 + 软件转换 + 软件渲染」,全在 CPU 上。 现在更常见的组合是下面这两种:
| 组合 | 做法 | 代价 | |---|---| | 硬解 + 硬转 | 解码在 GPU,转格式也在 GPU | 兼容性差,格式支持有限 | | 软解 + 硬转 | CPU 解码,显卡做 YUV 采样 | 最常用,兼容性好 |
第二行是现在的标准做法,因为它把「兼容性」和「性能」折中得最好: 解码交给 CPU(什么格式都能解),转换交给显卡(免费)。
这也意味着 sws_scale 在现代播放器里应该「尽量少被调用」。
它只剩两个用途:一是缩放(分辨率不匹配时),二是实在没法交给显卡的老设备。
所以这一节的知识点要这样摆:先知道它怎么用,再知道它贵,最后知道它该被绕过。 三步都走完,才算真懂。
六、动手 Demo:图像格式转换——这一步为什么常被当成瓶颈
同一个帧,转成五种格式,量五种耗时。 这组数字能解释两件事:为什么播放器宁可要 YUV 纹理,以及为什么「字节数少」不等于「快」。
for fmt in ("yuv420p", "rgb24", "bgr24", "nv12", "gray"):
t0 = time.perf_counter()
for fr in frames:
a = fr.to_ndarray(format=fmt) # 走 sws_scale
dt = (time.perf_counter() - t0) / len(frames)
实跑输出:
--------------------------------------------------------------------
图像格式转换——这一步为什么常被当成瓶颈
--------------------------------------------------------------------
**ffplay 里 `sws_scale` 被调用的次数跟解码一样多**——**每个要显示的视频帧都得过一次**,
**而且一次要处理几万到几十万个像素。**
**PyAV 的 `to_ndarray` 和 `reformat` 走的就是同一个 `sws_scale`。**
目标格式 每帧耗时 单帧字节数 相对原帧
-------------------------------------------------------
yuv420p 0.052 毫秒 115200 1.00 倍
rgb24 1.697 毫秒 230400 32.92 倍
bgr24 1.412 毫秒 230400 27.40 倍
nv12 1.299 毫秒 115200 25.20 倍
gray 3.069 毫秒 76800 59.56 倍
**第一行 `yuv420p` 是「不转」的情况,它是最便宜的**。
**因为没有重采样,只是把已有的三块平面读出来。**
**转成 `rgb24` 贵了不止一点**,**因为每个像素要做一次 YUV 到 RGB 的矩阵乘**,
**还要做色度上采样(4:2:0 的色度只有亮度的四分之一)。**
**这就是「为什么不要用 CPU 转 RGB」的量化依据**:**这一整列时间,在真正的播放器里是可以省掉的。**
**只要让渲染器直接吃 YUV,色彩转换由显卡在采样时顺手做。**
**最后一行 `gray` 是最反直觉的一行**:**它的字节数比 `yuv420p` 还少(只保留 Y 平面)**,
**耗时却是最贵的一个。**
**所以「数据量小就快」在这里不成立**——**决定快慢的是「走哪条转换路径」,不是「搬了多少字节」。**
**这条规律放到工程上就是一句话**:**优化转换不是「少转几个像素」,而是「别转」**。
**要灰度就让它灰度解码,要 RGB 就让显卡去转**,**在 CPU 上「少搬一点」几乎没有收益。**
第一行是「不转」的基准,它只把三块平面读出来,几乎不算。 所以它最快,也是唯一「不该被优化掉」的那一行。
后面三行的倍数都在几十倍,这个量级足够解释「为什么播放器一转 RGB 就卡」。 注意这是 320×240——换成 1080p,像素多二十倍,这几行的耗时也要涨二十倍。
gray 那一行是全表最反直觉的:字节数最少,耗时最长。
所以「看字节数估时间」这个习惯在这里会骗人。
这张表的用处不是记数字,是记排序:「不转」远快于「转」, 而「转成哪种」之间的差异是次要的。 所以优化的第一步永远是「能不能不转」,不是「转哪个更便宜」。
最后交代一个测不到的东西:这里量的是 CPU 路径。 真机上如果用硬件渲染器直接吃 YUV,这一整列时间会消失——但那需要一块能跑起来的显卡和一块能显示的画面,沙箱都没有。 所以本节能证明的是「CPU 转换很贵」,证明不了「GPU 转换免费」——后者是硬件原理推出来的,不是这里量出来的。
30 秒实验
- 把帧改成 1280×720,看
rgb24的耗时涨了几倍,是不是跟像素数成正比。 - 算一下:1080p、30 帧每秒,如果每帧转换要 2 毫秒,每秒要花多少 CPU 时间?
- 判断一下:
linesize大于width时,按宽度当步长读会看到什么现象?
常见误区
- 以为转换和缩放是两个函数。
sws_getContext一次就同时指定了格式和尺寸。 - 把
width当步长读数据。 必须用linesize,否则每行偏一点,画面斜着花掉。 - 以为字节数少就快。
gray字节最少却最慢,快慢取决于走哪条转换路径。 - 以为软件渲染下用 YUV 纹理就省了转换。 软件渲染器会把转换搬进渲染器,省的是拷贝不是计算。
自测三问(点开对答案)
1. `sws_getContext` 同时指定了哪两件事? —— 像素格式转换和尺寸缩放。所以「先转换再缩放」是算了两次。 2. `linesize` 为什么可能大于 `width`? —— 因为每行末尾要补齐到 32 或 64 字节对齐,方便 SIMD 指令访问。 3. 为什么「转 gray」反而比「转 rgb24」慢? —— 因为耗时取决于走哪条转换路径,不是数据量。这也说明优化方向应是「别转」而不是「少转」。图像这条线走完了,接下来是声音。音频转换比图像更容易出错, 因为「输入样本数」和「输出样本数」不是一对一的,而且它还有一个「内部残留」的问题。
- ☐ (本节完,下一节 09.06 FFplay的音频重采样)