最近在啃数字图像处理的学习笔记,前面的采样与量化章节还算是线性推进,到了第四篇“数字图像的内插、度量、表示与质量”这一块,我明显感觉节奏变了——教材不再只讲传感器怎么把光变成电信号,而是开始真正讨论“一幅离散的像素图像,如何被定量描述、如何组织成数据、如何评估它好不好”。这四个词分开看都是入门概念,合在一起其实是图像处理从“成像”走向“算法落地”的关键过渡,很多工程上的坑也都埋在这里。
这篇笔记我想换一种方式写,不按教材的目录顺序罗列定义,而是从自己动手实现和调参时遇到的问题出发,把内插、度量、表示、质量这四个主题串起来讲。适合正在啃数字图像处理教材的初学者,也适合做图像算法开发、想回头把底层概念再理一遍的工程师。
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 bit | 2 | 二值图、文档扫描 |
| 4 bit | 16 | 早期显示系统 |
| 8 bit | 256 | 常规图像与视频 |
| 10 bit | 1024 | 高清视频、部分相机 RAW |
| 12 bit | 4096 | 工业相机、摄影 RAW |
| 16 bit | 65536 | 医学影像、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 |
| RGB888 | 24 bit | 约 6.22 MB |
| RGBA8888 | 32 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。这个过程只有几百行代码,但你能亲眼看到“平滑”和“锐化”的代价,也能理解为什么没有免费的午餐。
对准备做工程的读者多说一句:图像表示里的坐标系统、通道顺序、数据类型、内存布局,这些细节看起来最无聊,却是实际项目里最容易出事故的地方。我见过太多花几天调试算法,最后发现只是某个模块把图片当成了另一种宽度读取的案例。先花半小时把输入输出的表示层搞清楚,后面省下的时间远不止半小时。第四篇笔记就写到这里,后面继续写邻域滤波和形态学相关的内容时,再和大家继续聊。