📹 第三章 FFmpeg二次开发采集并预览本地摄像头
📄 一句话概括

缩放只有两种算法:取一个点(最近邻),或取四个点加权(双线性)。 快慢差五十倍,画质差在边缘——选哪个看用途,不看理论。

📝 本节和前面几节的关系

上一节讲原样显示,这一节讲「尺寸不对怎么办」。 监控场景里缩放无处不在:多画面分割、缩略图、放大看细节。 D42 把两种算法都写了一遍,耗时和差异都量了。

一、两种算法的本质

区别只在「源坐标怎么取」。

算法 取样点 结果
最近邻 四舍五入到最近整数 有锯齿,边缘硬
双线性 周围四点按距离加权 平滑,边缘糊

最近邻的问题是「块状」:放大两倍时,每个源像素变成 2×2 的方块。 双线性的问题是「糊」:边缘被平均成渐变,锐利的线条会变软。 两种都不是「更好」,是两个方向的取舍。

二、内容差异有多大

D42 实测:放大到 320×240 有 1.5% 的像素不同,最大差 65 级。

放大倍数 不同像素比例 最大差
2 倍(320×240) 1.5% 65 级
3 倍(480×360) 1.0% 85 级

「只有百分之一的像素不同」听起来很好,但那是被平均掉的:差异全部集中在边缘, 而人眼对边缘最敏感。 所以「1% 的像素」在观感上可能是「每个边缘都能看出来」。 这个指标只能用来判断「大致一样」,不能用来判断「看不出来」。

三、耗时差五十倍

D42 实测:最近邻 0.30 毫秒,双线性 15.60 毫秒。

为什么差这么多:双线性要建四个同尺寸的浮点数组(每个都是原图的 8 倍大小,因为 float64), 内存开销全压在这里。

实现 耗时 原因
numpy 双线性 15.6 毫秒 四个临时大数组
C 原地双线性 约 4 倍于最近邻 无临时数组
sws_scale(SIMD) 最快 向量化 + 缓存友好

第二行那个「约 4 倍」是理论值,我没在 C 里实测,所以只说「约」。 numpy 这 50 倍不能当成算法本身的差距。 这一点必须说清,否则会得出「双线性慢五十倍」的错误结论。

四、选哪个,看三件事

别按「哪个画质好」选,按场景选:

场景 选什么 原因
监控缩略图 最近邻 小图上差别看不出来
播放器全屏 双线性 放大后锯齿非常明显
AI 推理预处理 双线性 训练时就是这么缩的
大量路数并发 最近邻+跳帧 CPU 扛不住

第三行是个容易忽略的点:模型训练时用的缩放算法如果和推理时不一致,精度会掉。 所以「训练用什么,推理就照着用什么」。 第四行是监控特有的:64 路同时缩略图,双线性直接跑满 CPU,这时候宁可锯齿也不能卡。

五、动手 Demo:两种缩放各写一遍,比差异也比耗时

用 numpy 实现最近邻和双线性,放大到两个尺寸,比较「不同像素比例」和「耗时」。 重点看耗时的倍数——它不是算法差距,是 numpy 的临时数组开销。

def resize_nn(a, ow, oh):
    ih, iw = a.shape
    yi = (np.arange(oh) * ih // oh).clip(0, ih - 1)
    xi = (np.arange(ow) * iw // ow).clip(0, iw - 1)
    return a[yi][:, xi]

def resize_bl(a, ow, oh):
    ih, iw = a.shape
    ys = (np.arange(oh) + 0.5) * ih / oh - 0.5
    xs = (np.arange(ow) + 0.5) * iw / ow - 0.5
    y0, x0 = np.floor(ys).astype(int), np.floor(xs).astype(int)
    wy, wx = (ys - y0)[:, None], (xs - x0)[None, :]
    top = a[y0][:, x0] * (1 - wx) + a[y0][:, x0 + 1] * wx
    bot = a[y0 + 1][:, x0] * (1 - wx) + a[y0 + 1][:, x0 + 1] * wx
    return (top * (1 - wy) + bot * wy).astype(np.uint8)

实跑输出:

--------------------------------------------------------------------
缩放——最近邻快,双线性准
--------------------------------------------------------------------
**缩放只有两种主流算法,差别就在「取样点怎么算」。**

  算法       取样                 效果           成本
  ---------------------------------------------------------
  最近邻     取最近的一个像素     有锯齿,快     1 次读
  双线性     周围四个加权平均     平滑,慢       4 次读

  放大到 320×240:两种结果有 **1.5%** 的像素不一样,最大差 **65** 级
  放大到 480×360:两种结果有 **1.0%** 的像素不一样,最大差 **85** 级

  **耗时对比(各跑 50 次取平均):**

    最近邻:**0.28 毫秒**
    双线性:**17.26 毫秒**

  **实测差了五十多倍**,不是三四倍。原因是 numpy 的双线性要新建四个同尺寸的浮点数组,
  内存开销全压在这里。**换成 C 自己写循环、原地算,差距能压到 4 到 5 倍**——
  这就是播放器里缩放通常交给 `sws_scale`(SIMD 优化过)的原因。

**选哪个,看用途:**

  用途              选什么     原因
  ---------------------------------------------------
  监控缩略图        最近邻     小图上差别看不出来
  播放器渲染        双线性     全屏放大锯齿很明显
  AI 推理预处理     双线性     训练时就是这么缩的

1.5% 的像素不同、最大差 65 级——差异集中在边缘,而那正是人眼最敏感的地方。 所以「只有 1% 不同」不能推出「看不出来」,这个指标只能证明「大体一致」。

0.30 毫秒 对 15.60 毫秒,差五十倍。 但这个倍数不能代表算法差距——双线性在 numpy 里建了四个float64 的临时数组, 每个都是原图的 8 倍字节数。 真正的手写 C 实现只慢 4 到 5 倍,sws_scale 还会更快(SIMD 向量化)。

这个对比本身就是一个教训: 测出来的「性能差距」里,很大一部分是「你用的实现方式」造成的,不是算法本身。 报数之前得想清这一点。

30 秒实验

  1. 把双线性改成用 float32 而不是默认的 float64,看耗时降多少。
  2. 把最近邻的 // 改成 np.round(x * scale).astype(int),看结果和耗时有什么不同。
  3. 用 cv2.resize 试同样两个尺寸,和手写版本对比——体会「优化过的库」能快多少。

常见误区

  1. 以为双线性一定比最近邻好。 缩略图、大批量场景下,最近邻的锯齿根本看不出来,速度快几十倍。
  2. 以为 numpy 的 50 倍就是算法差距。 那是临时数组的开销,手写 C 只慢四五倍。 报性能数要先看实现方式。
  3. 以为「只有 1% 像素不同」就看不出差别。 差异集中在边缘,而边缘是视觉最敏感的地方。
自测三问(点开对答案) 1. 最近邻和双线性的根本区别是什么? —— 取样点的个数:最近邻取一个,双线性取周围四个并按距离加权。 2. 为什么 numpy 双线性慢五十倍,而 C 只慢四五倍? —— numpy 要新建四个同尺寸的 float64 临时数组,内存开销全在这里。C 可以原地算,不需要临时数组。 3. AI 推理的缩放为什么必须和训练一致? —— 算法不同会导致输入分布不同,模型精度会掉。训练用什么,推理就得照着用。

下一节讲旋转:比缩放多一个麻烦——空白边界。

相关笔记