七种缩放算法,PSNR 冠军是最粗糙的 neighbor。 这件事说明:PSNR 不是观感的裁判。
前六节都在定「缩到多大」,这一节换成「怎么缩」。 算法选错,尺寸再对画面也难看。
一、缩放要猜中间像素
缩小时要扔掉像素,放大时要造出新像素——两件事都得「猜」。 猜法不同,就是算法不同。
scale=80:45:flags=lanczos
不给 flags 就用默认的 bicubic。
要指定就加在尺寸后面,用冒号接。
二、七种算法,一次比完
比较的办法是「缩到 80×45 再放回 160×90」,与原图逐像素对比。 PSNR 越大越接近原图;锐度用拉普拉斯响应的方差,越大边缘越硬。
算法 PSNR 锐度
fast_bilinear 15.05 dB 2567 bilinear 14.04 dB 968 bicubic 16.93 dB 3493 lanczos 17.07 dB 4118 neighbor 18.32 dB 31586 area 16.16 dB 2680 spline 16.95 dB 3680
原图的锐度是 35759,可以拿它当参照。 neighbor 的 31586 已经很接近原图,看起来「几乎没损失」。
而 bilinear 的锐度只有 968——只剩原图的百分之三。 它把棋盘格彻底糊平了。
三、PSNR 冠军是最粗糙的那个
把三项排个名:
项 得主 数值
PSNR 最高 neighbor 18.32 dB 锐度最高 neighbor 31586 PSNR 最低 bilinear 14.04 dB
neighbor 是最近邻——最简单、最粗糙的那种。 它居然同时拿下两项第一。这跟我们平时的印象完全相反。
为什么?因为这段素材左上角是 4 像素的棋盘格。 缩到一半之后每格还剩 2 像素,黑白交替的结构一点没丢; 放大回 4 像素时位置又正好对齐——所以逐像素差得最少。
而 lanczos 会先做低通滤波,把棋盘格糊成一片灰, 再放大也回不来,于是「更不像原图」。
换句话说:neighbor 保住了「结构」,lanczos 保住了「平滑」。 PSNR 只能看见前者。
四、那到底该选哪个
按场合选,别按分数选:
场合 建议 理由
缩到一半、求速度 fast_bilinear 最快,质量够用 默认、通用 bicubic 不指定就是这个 缩略图、要清晰 lanczos 锐度高,代价是慢 像素画、二维码 neighbor 绝不模糊,块就是块 大比例缩小 area 按面积平均,最稳
neighbor 唯一的正道用途是「不能糊」的场合:二维码、像素画、UI 截图。 用它缩照片,出来是马赛克——再高的 PSNR 也救不了观感。
area 是大比例缩小的最优解:它真的把每个源区域的像素平均了一下。
缩到四分之一以下时,它通常比 lanczos 更稳。
五、这件事的普适教训
PSNR 衡量的是「与原图逐像素差多少」,不是「人看着舒不舒服」。 遇到规则图案、细线条、文字,PSNR 甚至会反过来咬你一口。
所以调参时别只盯一个数字:先看指标,再亲眼看一帧。 指标负责排除明显错误,眼睛负责判断观感。两件事都得做。
这一条在视频编码里同样成立: CRF 与 PSNR 都是手段,最终标准永远是「这段片子能不能看」。
六、动手 Demo:七种算法,两条指标,一个反直觉的结论
下面这段代码把七种算法各跑一遍:先缩到一半,再放回原尺寸。 然后跟原图比 PSNR,再算锐度。
import av
import numpy as np
from fractions import Fraction
TB = Fraction(1, 25)
c0 = av.open(u"s7.mp4")
fr = next(c0.decode(video=0))
src = fr.to_ndarray(format=u"rgb24")
base = fr.to_ndarray()
c0.close()
def psnr(a, b):
m = np.mean((a.astype(np.float64) - b.astype(np.float64)) ** 2)
return 99.0 if m <= 0 else 10.0 * np.log10(255.0 * 255.0 / m)
def sharp(a):
g = a.astype(np.float64).mean(axis=2)
lap = (-4 * g[1:-1, 1:-1] + g[:-2, 1:-1] + g[2:, 1:-1]
+ g[1:-1, :-2] + g[1:-1, 2:])
return float(lap.var())
def once(flag):
g = av.filter.Graph()
p = g.add_buffer(width=160, height=90, format=u"yuv420p", time_base=TB)
dn = g.add(u"scale", u"80:45:flags=" + flag)
sk1 = g.add(u"buffersink")
p.link_to(dn)
dn.link_to(sk1)
g.configure()
vf = av.VideoFrame.from_ndarray(base, format=u"yuv420p")
vf.pts = 0
vf.time_base = TB
p.push(vf)
small = sk1.pull().to_ndarray(format=u"rgb24")
g2 = av.filter.Graph()
q = g2.add_buffer(width=80, height=45, format=u"rgb24", time_base=TB)
up = g2.add(u"scale", u"160:90:flags=" + flag)
sk2 = g2.add(u"buffersink")
q.link_to(up)
up.link_to(sk2)
g2.configure()
vf2 = av.VideoFrame.from_ndarray(small, format=u"rgb24")
vf2.pts = 0
vf2.time_base = TB
q.push(vf2)
return sk2.pull().to_ndarray(format=u"rgb24")
print(u"原图锐度 %.0f" % sharp(src))
for flag in (u"fast_bilinear", u"bilinear", u"bicubic", u"lanczos",
u"neighbor", u"area", u"spline"):
back = once(flag)
print(u"%-14s PSNR %5.2f dB 锐度 %6.0f"
% (flag, psnr(src, back), sharp(back)))
实跑输出:
--------------------------------------------------------------------
7.2.7 使用 scale 的指定算法进行缩放
--------------------------------------------------------------------
**① 缩放要「猜」中间像素,怎么猜就是算法。**
**scale 用 `flags=` 指定算法,不给就用默认的 bicubic。**
`scale=80:45:flags=lanczos`
**② 七种算法:先缩一半,再放大回来,与原图比**
**比较的办法是「缩到 80×45 再放回 160×90」,跟原图逐像素对比。**
**PSNR 越大越接近原图;锐度用拉普拉斯响应的方差,越大边缘越硬。**
算法 PSNR 锐度
----------------------------------------
fast_bilinear 15.05 dB 2567
bilinear 14.04 dB 968
bicubic 16.93 dB 3493
lanczos 17.07 dB 4118
neighbor 18.32 dB 31586
area 16.16 dB 2680
spline 16.95 dB 3680
**原图的锐度是 35759,可以拿它当参照。**
**③ 这张表最反直觉的地方**
项 得主 数值
---------------------------------------
PSNR 最高 neighbor 18.32 dB
锐度最高 neighbor 31586
PSNR 最低 bilinear 14.04 dB
**PSNR 冠军是 neighbor,而 neighbor 是最近邻——最粗糙的那个。**
**锐度它也拿第一,31586 已经接近原图的 35759。**
**④ 为什么会这样**
**这段素材左上角是 4 像素的棋盘格。缩到一半之后,每格还剩 2 像素,**
**黑白交替的结构一点没丢;放大回 4 像素时位置又正好对齐——所以逐像素差得最少。**
**而 lanczos 会先做低通滤波,把棋盘格糊成一片灰,**
**再放大也回不来,于是「更不像原图」。**
**⑤ 那到底该选哪个**
场合 建议 理由
-------------------------------------------------------------
缩到一半、求速度 fast_bilinear 最快,质量够用
默认、通用 bicubic 不指定就是这个
缩略图、要清晰 lanczos 锐度高,代价是慢
像素画、二维码 neighbor 绝不模糊,块就是块
大比例缩小 area 按面积平均,最稳
**neighbor 唯一的正道用途是「不能糊」的场合:二维码、像素画、UI 截图。**
**用它缩照片,出来是马赛克——再高的 PSNR 也救不了观感。**
**⑥ 这件事的普适教训**
**PSNR 衡量的是「与原图逐像素差多少」,不是「人看着舒不舒服」。**
**遇到规则图案、细线条、文字,PSNR 甚至会反过来咬你一口。**
**所以调参时别只盯一个数字:先看指标,再亲眼看一帧。**
七行跑完,排名与本节表格一致:neighbor 两项都第一,bilinear 垫底。
这段代码分成两段建图:第一段缩到 rgb24 的 80×45,第二段再放大回 160×90。 中间用 rgb24 走,是为了让两次缩放都在同一个色彩空间里,避免 yuv 往返干扰。
如果拿这段素材直接看画面,neighbor 的结果是一块一块的马赛克, lanczos 的结果是平滑的、边缘稍软——但 PSNR 会告诉你反过来。 这就是本节要传达的那件事:先信指标,再信眼睛,最后信场合。
30 秒实验
- 换素材:拿一张实拍照片(不是合成图)跑同一段,看排名变不变。
- 只缩不放:去掉第二段,看七种算法缩出来的锐度差多少。
- 加一档:试
flags=neighbor缩到四分之一,看马赛克有多明显。
常见误区
- 按 PSNR 选算法。规则图案会让最近邻占便宜,结论会反过来。
- 以为默认算法最差。默认 bicubic 在中段,多数场合够用。
- 用 neighbor 缩照片。出来是马赛克,PSNR 再高也没法看。
自测三问(点开对答案)
1. 为什么 neighbor 的 PSNR 最高? —— 素材是 4 像素棋盘格,缩一半再放回时结构正好对齐,逐像素差得最少。 2. 大比例缩小该用哪个算法? —— area,它按面积平均,比插值类更稳。 3. PSNR 适合用来判断观感吗? —— 不完全适合,它只量逐像素差,不量人眼感受。- ☐ (本节完,下一节 7.2.8 scale 参数说明)