缩放只有两种算法:取一个点(最近邻),或取四个点加权(双线性)。 快慢差五十倍,画质差在边缘——选哪个看用途,不看理论。
上一节讲原样显示,这一节讲「尺寸不对怎么办」。 监控场景里缩放无处不在:多画面分割、缩略图、放大看细节。 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 秒实验
- 把双线性改成用
float32而不是默认的float64,看耗时降多少。 - 把最近邻的
//改成np.round(x * scale).astype(int),看结果和耗时有什么不同。 - 用
cv2.resize试同样两个尺寸,和手写版本对比——体会「优化过的库」能快多少。
常见误区
- 以为双线性一定比最近邻好。 缩略图、大批量场景下,最近邻的锯齿根本看不出来,速度快几十倍。
- 以为 numpy 的 50 倍就是算法差距。 那是临时数组的开销,手写 C 只慢四五倍。 报性能数要先看实现方式。
- 以为「只有 1% 像素不同」就看不出差别。 差异集中在边缘,而边缘是视觉最敏感的地方。
自测三问(点开对答案)
1. 最近邻和双线性的根本区别是什么? —— 取样点的个数:最近邻取一个,双线性取周围四个并按距离加权。 2. 为什么 numpy 双线性慢五十倍,而 C 只慢四五倍? —— numpy 要新建四个同尺寸的 float64 临时数组,内存开销全在这里。C 可以原地算,不需要临时数组。 3. AI 推理的缩放为什么必须和训练一致? —— 算法不同会导致输入分布不同,模型精度会掉。训练用什么,推理就得照着用。下一节讲旋转:比缩放多一个麻烦——空白边界。
- ☐ (本节完,下一节 03.04.10 Qt旋转图像)