ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数字图像处理核心概念:内插、度量、表示与质量详解

数字图像处理核心概念:内插、度量、表示与质量详解

最近在啃数字图像处理的学习笔记,前面的采样与量化章节还算是线性推进,到了第四篇“数字图像的内插、度量、表示与质量”这一块,我明显感觉节奏变了——教材不再只讲传感器怎么把光变成电信号,而是开始真正讨论“一幅离散的像素图像,如何被定量描述、如何组织成数据、如何评估它好不好”。这四个词分开看都是入门概念,合在一起其实是图像处理从“成像”走向“算法落地”的关键过渡,很多工程上的坑也都埋在这里。

这篇笔记我想换一种方式写,不按教材的目录顺序罗列定义,而是从自己动手实现和调参时遇到的问题出发,把内插、度量、表示、质量这四个主题串起来讲。适合正在啃数字图像处理教材的初学者,也适合做图像算法开发、想回头把底层概念再理一遍的工程师。

1. 内插:几何变换绕不开的第一道坎

1.1 为什么输出坐标经常对不上输入坐标

内插本质上是一个映射问题。一幅数字图像是离散的,像素位置只能取整数,比如坐标 (2,3) 有意义,但坐标 (2.37,3.81) 在像素网格上根本不存在。当我们把图像放大、缩小、旋转、做透视矫正时,输出图像上某个整数像素坐标,映射回原图坐标时几乎必然落在一个带小数的位置。这时问题就来了:这个不存在的位置,它的灰度值应该是什么?

采样定理其实已经给出了理想答案:要用一个无限长的 sinc 函数对所有邻域像素做加权卷积,才能无失真地重建连续信号。但现实里我们既没有无限长的数据,也付不起这种计算代价,所以工程上用各种有限邻域的近似核来替代。这个“用有限邻域估计未知位置取值”的过程,就是内插。

这个概念理解了,后面看三种常见方法就通透了:最近邻是复杂度最低的近似,双线性把邻域扩到 2x2,双三次则用到 4x4,本质上都是用更大邻域和更平滑的权函数,去逼近那个理想重建滤波器。

1.2 三种插值方法的原理与观感

最近邻插值不做任何计算,直接把映射坐标四舍五入,取离它最近的原始像素值作为输出。假设原图某点坐标为 (i+a, j+b),其中 a 和 b 是 [0,1) 的小数部分,最近邻的规则就是判断 a、b 是否大于 0.5,然后从四个像素里挑一个。速度极快,但放大后边缘呈台阶状,锯齿感明显。这个方案我在做像素风图标缩放时反而会主动用,因为它能保留硬边,不会像平滑方法那样把笔锋边缘糊掉。

双线性插值稍微认真一点。它在水平方向先做一次线性加权,再在垂直方向做一次线性加权。如果用 (i,j)、(i+1,j)、(i,j+1)、(i+1,j+1) 四个像素做加权,最直观的写法是:

f(x,y) ≈ (1-a)(1-b)f(i,j) + a(1-b)f(i+1,j) + (1-a)b f(i,j+1) + ab f(i+1,j+1)

其中 a、b 同样是坐标小数部分。这个方法代价不高,效果比最近邻平滑很多,是视频缩放、实时图像矫正里最常见的默认选项。它的缺点是相当于做了一次低通滤波,放大后画面会偏软,尤其在高对比度边缘附近,能看到明显的柔化痕迹。

双三次插值再用狠一点,它取映射点周围 4x4 共 16 个像素,用连续的三次多项式核卷积计算。常见核函数可以写成:

h(x)= (a+2)|x|^3 - (a+3)|x|^2 + 1,当 0 ≤ |x| < 1

h(x)= a(|x|^3 - 5|x|^2 + 8|x| - 4),当 1 ≤ |x| < 2

a 通常取 -0.5,此时等价于三次 Hermite 样条,对多数自然图像表现比较稳。双三次能保留更多高频细节,边缘比双线性锐利,但代价也很有意思:它在高反差边缘附近会出现轻微过冲,也就是常说的振铃或光晕。放大的文字截图里,笔画边缘往往带一圈浅浅的白边,这就是过冲。

我自己做对比测试时拿一张很低的文字截图放大 4 倍,感受特别直观。最近邻的结果所有笔画边缘都变成方块锯齿,可读性下降明显;双线性平滑不少,但字体的粗细感有点被抹开;双三次最锐,但个别笔画外侧多了一道不易察觉的光晕。没有哪一种绝对最好,关键看你的使用场景。

1.3 选型经验:什么时候用哪种

先给个比较通用的选型参考表:

场景推荐方法原因
像素画、图标放大最近邻保留硬边,不打乱原始配色
自然照片缩放双三次细节保留更好,边缘更实
实时视频处理双线性速度与质量的平衡点
大幅缩小图片低通滤波 + 降采样避免混叠,等效于理想内插
深度学习上采样层初始化双线性权重减少棋盘伪影,训练更稳

这里特别想提醒一个常被忽略的点:缩小图像不是简单的“反向放大”。按采样定理,下采样之前必须做抗混叠滤波,否则高频纹理在缩小的图上会混叠成错误的花纹或摩尔纹。很多图像库的 resize 接口内部已经做了这一步,但如果你自己写代码,直接用隔行抽点的方式缩小,十有八九会出现锯齿和闪烁。

另外,在做算法对比时,不要拿“某库自动抗混叠的 resize 结果”和“自己手写的双线性缩放”直接比较,两者根本不是同一件事。我建议自己实现插值时,先计算目标像素坐标映射到源图坐标系,再分方向加权,最后把结果和库内置接口的差异当作一项单独指标来观察。

2. 图像量度不能只看长宽:分辨率、灰度级与存储三件套

2.1 空间分辨率与灰度级分辨率:一对容易被混为一谈的量

很多初学者把“分辨率”默认成像素宽高,实际上教材里讨论的分辨率有两个维度:空间分辨率描述单位距离能分辨的最小细节,由采样密度决定;灰度级分辨率描述亮度量化等级,由每个像素的比特数决定。这两个维度独立影响图像质量,但视觉表现完全不同。

空间分辨率不足时,细节丢失是“结构性的”:细线消失、边缘变锯齿、高频纹理出现混叠。灰度级分辨率不足时,问题更多出现在平滑渐变区域,表现是假轮廓或带状条纹。最典型的例子是人脸肤色、天空渐变这类区域,量化比特不足时,肉眼能看到一圈一圈的等高线状明暗跳变,而不是自然的连续过渡。

灰度级与比特数的对应关系可以列一张表:

位深灰度级数常见场景
1 bit2二值图、文档扫描
4 bit16早期显示系统
8 bit256常规图像与视频
10 bit1024高清视频、部分相机 RAW
12 bit4096工业相机、摄影 RAW
16 bit65536医学影像、HDR 中间结果

实际做图像处理时,8 bit 和 16 bit 的差异往往在算法链路上才暴露出来。比如对暗部做增益增强时,8 bit 数据在暗区只有很少几个灰度级,增益一拉就出现条带;而 12 bit 或 16 bit 数据暗区信息更丰富,增强后还能保持平滑。这个现象在低光照场景特别明显,也是为什么现代成像链路越来越倾向保留高比特中间结果。

2.2 像素间距、DPI 与真实物理尺寸的换算

“度量”这个词在图像处理里还有另一层含义:像素本身不携带物理尺寸,同一个 1920×1080 矩阵,可以对应手机屏幕上的一小条,也可以对应一块大型广告屏。要把图像和真实世界挂钩,必须知道像素间距,也就是每个像素对应的物理长度。

换算公式很简单:真实长度 = 像素数量 × 像素间距。如果采样设备给出 DPI,则每个像素的物理尺寸为 1/DPI 英寸,换算成毫米要乘以 25.4。在工业测量场景里这是一个极其关键的环节——我用一个常见做法举例:先拍一张已知尺寸的标定板,测出图像中对应边长的像素数量,就能算出这个成像系统的像素间距;之后测任何目标,只需数轮廓跨过多少像素,再乘这个系数就能得到毫米尺寸。如果跳过标定直接按 DPI 估算,误差可能达到几个百分点,对精密测量来说完全不可接受。

顺便提一下像素间的距离度量。图像处理里除了欧氏距离,还常用 D4 和 D8:

D4(p,q) = |xp - xq| + |yp - yq|

D8(p,q) = max(|xp - xq|, |yp - yq|)

D4 也叫城市街区距离,D8 也叫棋盘距离。形态学膨胀腐蚀、连通性分析、距离变换这些算法,运行结果直接依赖你选哪个度量。选错了,结构元素形状和距离场都会呈现出不一样的扩散方向。这个属于“度量的选择”问题,教材里容易一带而过,但做形态学相关的工程时真的要当心。

2.3 位深、存储字节数与带宽成本

存储需求是一个特别容易被低估的度量指标。一幅图像的字节数由宽、高、位深共同决定,公式是:

字节数 = 宽 × 高 × 位深 / 8

如果每个像素 8 bit,一张 1920×1080 的灰度图约 2.07 MB;换成 RGB 三通道 24 bit 就是约 6.22 MB;如果上到 16 bit 单通道,字节数又要翻倍。表面上看只是数据类型换了一下,实际在高帧率处理链路上,内存带宽和缓存命中率的差异立刻就会显现。

不同图像类型的存储成本更直观:

图像类型每像素比特典型大小举例(1920×1080)
二值图1 bit约 0.26 MB
8 bit 灰度8 bit约 2.07 MB
16 bit 灰度16 bit约 4.15 MB
RGB88824 bit约 6.22 MB
RGBA888832 bit约 8.29 MB
12 bit Bayer 原始数据12 bit约 3.11 MB

工业相机里常说的 12 bit RAW,实际每像素只占 12 bit 而不是 16 bit,但很多设备为了对齐内存会按 16 bit 打包,结果数据量立刻上了三分之一。做系统设计时要问清楚是紧凑打包还是对齐打包,这个决定直接关系到带宽预算和 FPGA/DSP 侧的处理方案。我自己就吃过这个亏,一开始按 16 bit 估算缓存,后来确认设备输出紧凑 12 bit,缓存设计多出不少余量,白白浪费了资源。

3. 表示:同一个矩阵的三种组织方式

3.1 从连续光强度到数值数组:坐标系、矩阵与邻域

数字图像的本质是一个二维离散函数 f(x,y),x 和 y 是采样点坐标,函数值是被量化后的光强度。工程实现时,它就是一个二维数组,行列号对应坐标。这里有一个约定俗称的坐标系:原点在左上角,x 轴向右,y 轴向下。这个方向和中学数学里的坐标系是反的,很多运算上的“怪问题”都源于这个细节。

举个例子,假设我要实现一个 3x3 均值滤波,卷积核通常写成:

kernel = [[1, 1, 1], [1, 1, 1], [1, 1, 1]] / 9

如果坐标对应关系搞反,卷积核在图像上旋转了 180 度,对称核没影响,但换成 Sobel 这类方向核,上下边缘的方向判断就全错了。我见过不止一个人把 Sobel 的 y 方向核写反,导致边缘检测结果上下颠倒。解决方式很朴素:所有涉及核运算的函数,先把坐标系约定写进注释,再统一用一个工具函数来核翻转,不要靠脑子记。

图像表示里另一个基础概念是邻域。4 邻域包含上下左右四个像素,8 邻域再加上四个对角像素。滤波、形态学、连通域分析几乎都要先定义邻域关系。很多算法文档默认 8 邻域,但实现时没有显式确认,结果用 4 邻域也能跑通,只是结果在少量像素上不一致。这个差异在调试时很难发现,但对比标准答案时会成为无解的谜题。

3.2 灰度、索引、彩色与多光谱:组织方式的取舍

按数据组织方式,数字图像可以分成几类,每一类的存储结构和适用场景都不一样。

二值图像每个像素只占 1 bit,非黑即白。早期文档处理、条码识别里大量使用。灰度图像是单通道多级亮度,8 bit 满级 256 级,是图像算法最常用的中间格式。索引图像比较特殊,它保存的不是颜色本身,而是一个索引号,真正颜色存在调色板表里。比如某像素值为 5,就表示颜色查表第 5 项。这种表示在色块面积很大的图表、图标场景下很省空间,而且更换调色板能瞬间改变整幅图配色。

RGB 彩色图像是目前最常见的表示,三个通道分别存储红、绿、蓝强度。再往上是多光谱图像,通道数可以到几十甚至上百,遥感图像、医学荧光成像、部分质检设备都会用到。通道越多,信息越丰富,但存储、传输、特征提取的复杂度也大幅上升。选哪种表示方式不是看哪个高级,而是看任务需要什么信息。

工程里最常见的表示错误,是把彩色图转成灰度图后忘记调整通道数。比如一个模型输入通道是 3,某次预处理把图像转成了单通道灰度,结果形状不匹配直接报错。这类问题好解决,难的是另一种情况:代码把彩色图当成灰度读进来,只取其中一个通道值,然后全部失败,错误信息很隐蔽。我现在的习惯是,不论数据从哪来,第一步先打印 shape、dtype、通道顺序、取值范围,确认“表示”正确了再做处理。

3.3 数据类型的精确表示与内存布局:性能与兼容的双重考验

同样是灰度值,用 uint8、uint16、float32 表示,背后的语义差异很大。uint8 的 255 是饱和亮度,uint16 的 65535 也是饱和亮度,而 float 有时范围是 [0,1],有时是 [0,255],还有时是 [0,65535]。

这个范围约定是最容易引发事故的地方。举例来说,两个模块处理同一张图,一个按 [0,255] 的 float 传递,另一个按 [0,1] 的 float 传递,互相对接时如果不做归一化检查,模型输出可能整体偏移。这个偏移在暗部几乎看不出来,但高光区域会被明显抬高,最终导致评测指标和肉眼观感同时异常。排查方式很简单:任何输入数据,先统计 min 和 max,再决定是否归一化。

内存布局则是“表示”问题里更底层的环节。常见的布局有两种:平面布局 CHW 和交错布局 HWC。RGB 交错布局里,内存顺序是 R,G,B,R,G,B;平面布局里,所有 R 值连着存,再存所有 G 值,最后存所有 B 值。批处理时扩展成 NCHW 或 NHWC。不同的算子对布局的偏好不同,卷积类运算在 NCHW 下访存更规整,一些图像处理算子反而在 NHWC 下简单。

写高性能算子时,布局决定 cache 命中率,决定能不能做向量化读取。我做过一个很小的通道分离实验,同一张三通道图,变成 CHW 平面布局后遍历每个通道,比 HWC 交错布局快了不少,原因就是连续访存。如果要用加速指令集或者手写 SIMD,不理解布局简直寸步难行。这里给一个实操建议:自定义算子之前,先写一个布局转换小工具,并打印一段内存字节,肉眼确认排列顺序,再开始写逻辑。

还有一个非常经典的表示坑:不同图像库对通道顺序约定不一样。有的库原生 BGR,有的库原生 RGB,混用时最简单的表现是整幅图色偏,红色和蓝色互换。这种错误几乎不报错,只在视觉检查时发现。我的固定处理是:在数据进入算法模块前统一转成 RGB 平面布局,并加注释说明当前数据的通道顺序,凡是跨库调用都要重新检查这一项。

4. 图像质量:主观观感与客观指标的使用边界

4.1 采样不足与量化不足:两种典型退化

讨论图像质量,先要明白“质量问题”到底从哪来。空间采样不足会引起混叠,典型观感是锯齿、摩尔纹、远处密集纹理出现错误的花纹。你把一张棋盘格照片等比缩小,如果没做抗混叠,缩小后的图像里会出现奇怪的灰条纹和斜向纹理,这不是传感器坏了,是高频信号折叠到了低频区间。

量化不足则表现为伪轮廓和条带。8 bit 数据在暗部只有很少的灰度级可用,如果把暗部像素做线性增强,原本平滑的渐变会变成明显的阶梯。10 bit 素材转 8 bit 后如果没有加抖动处理,夜空中经常能看到一圈一圈的带状条纹,这就是量化噪声的直观体现。

判断图像质量问题,要先观察退化属于哪一类。带锯齿、细节消失为主的,优先考虑提高采样密度或降低高频;出现渐变阶梯、带状跳变的,优先提高量化比特或增加抖动。很多时候我们把“画质不好”简单归因于“分辨率太低”,实际上可能是量化环节出问题了。这两类退化也可能同时存在,别急着下结论。

4.2 客观评价指标的适用领域与盲区

客观指标是工程里偷懒的工具,但每个指标都有明显盲区。均方误差 MSE 和峰值信噪比 PSNR 是历史最悠久的两个指标。MSE 把所有像素误差同等对待,对轻微的几何偏移极其敏感。把一张图整体平移 1 个像素,MSE 会变得很大,可人眼几乎看不出差别。这导致只比较 PSNR 时,有时会得出“A 算法远好于 B 算法”的结论,但实际主观观感完全相反。

PSNR 公式是:

PSNR = 10 * log10(MAX^2 / MSE)

MAX 一般取 255。但这个指标对均值偏移比较敏感,对结构失真不敏感。做图像压缩算法评测时,它仍然作为基础指标在用,但单独用一定不够。SSIM 的出发点更接近人眼感知,它从亮度、对比度、结构三个因子来计算相似度。对模糊比较敏感,对轻度对比度变化相对宽容。这使得 SSIM 很适合评价风格相似、结构保留程度高的算法,但也存在误判:一张亮度整体偏低的图,和一张亮度正常的图,SSIM 可能给出较高得分,人眼却会明显觉得暗部信息不对。

实际评测时,我通常会把 PSNR、SSIM 以及无参考指标结合起来看,但最终决策一定有人眼主观盲评参与。主观评测的做法也挺朴素:把待比较的图像顺序打乱,让评测者两两对比选优,记录得分。找十个人左右,结果就比单跑一个指标靠谱得多。

4.3 从“重建质量”回到“采集质量”:另一个维度

这里还应该区分一下讨论的是哪种质量。前面说的都可以算作“处理后图像与参考图的相似度”,但图像质量还有一个更靠上游的维度,指的是采集系统本身的成像质量,包括噪声水平、动态范围、坏点数量、信噪比、色彩还原准确度等。如果做的是相机调试或 ISP 算法相关方向,关注点就不是 PSNR,而是 raw 域噪声、暗电流、光响应非均匀性这些指标。

举个例子,调试低光照降噪算法时,不同增益下信噪比曲线有没有收敛,比单帧图像的 PSNR 更有意义。ISP 参数调整,最终也还是需要人在标准显示器前做主观检查,观察肤色是否自然、暗部是否脏、细节是否保留。这种评价很难完全自动化,所以我才特别强调“指标只是辅助,感知才是终点”的项目方法论。

5. 给一起啃教材的朋友几句实在话

如果只看教材,这一章的概念都很容易“背下来”,但真正建立直觉,还是建议动手实现一次。我的建议是:自己把最近邻、双线性、双三次插值写一遍,用同一张低分辨率图像放大 4 倍,把三种结果并排看起来,再算一下各自的 PSNR 和 SSIM。这个过程只有几百行代码,但你能亲眼看到“平滑”和“锐化”的代价,也能理解为什么没有免费的午餐。

对准备做工程的读者多说一句:图像表示里的坐标系统、通道顺序、数据类型、内存布局,这些细节看起来最无聊,却是实际项目里最容易出事故的地方。我见过太多花几天调试算法,最后发现只是某个模块把图片当成了另一种宽度读取的案例。先花半小时把输入输出的表示层搞清楚,后面省下的时间远不止半小时。第四篇笔记就写到这里,后面继续写邻域滤波和形态学相关的内容时,再和大家继续聊。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进