027、去马赛克算法演进:从双线性插值到方向自适应,再到深度学习驱动的色彩重建
一个让我失眠三天的调试问题
2018年,我在调试一款48M sensor的安防IPC时,遇到了一个诡异的色彩伪影问题。画面中有一片深绿色的树叶,边缘处出现了明显的紫色和青色条纹,像是给树叶镶了一圈彩虹边。当时我以为是镜头色差,换了三颗镜头,问题依旧。后来发现,同样的raw图在竞品平台上跑出来是干净的。问题出在我们的ISP去马赛克模块上。
这个案例让我深刻意识到:去马赛克算法不是简单的“填色游戏”,它直接决定了图像分辨率的真实感和色彩保真度。今天这篇笔记,我就把这些年踩过的坑、调过的参数、看过的论文,掰开了揉碎了讲清楚。
为什么需要去马赛克?Bayer pattern的宿命
先说说最基础的。CMOS sensor上每个像素只能感光一种颜色,最常见的排列是Bayer pattern——RGGB。一个像素要么是R,要么是G,要么是B。要得到全彩图像,必须把缺失的两个颜色猜出来。
这个“猜”的过程,就是去马赛克(Demosaicing)。听起来简单,但实际做起来,你会发现它是个典型的病态问题——信息不完整,任何插值都会引入误差。误差的表现形式就是伪色(false color)和锯齿(zipper effect)。
第一代:双线性插值——简单但粗暴
最早期的方案就是双线性插值。比如要恢复一个R像素位置的G值,就取周围四个G像素的平均。代码写起来很简单:
// 双线性插值恢复G通道// 注意:这里假设输入是Bayer raw,已经分离出各通道uint16_tdemosaic_bilinear_g(uint16_t*bayer,intx,inty,intwidth){// 踩坑记录:边界处理一定要做,否则直接数组越界if(x<1||x>=width-1||y<1||y>=height-1){returnbayer[y*width+x];// 边界直接返回原始值,别问我为什么}// 取上下左右四个G像素的平均uint16_tg_up=bayer[(y-1)*width+x];uint16_tg_down=bayer[(y+1)*width+x];uint16_tg_left=bayer[y*width+(x-1)];uint16_tg_right=bayer[y*width+(x+1)];return(g_up+g_down+g_left+g_right)>>2;}这个方案的问题很明显:它假设图像在局部是平滑的,但实际图像边缘处色彩变化剧烈。结果就是边缘处出现明显的伪色——绿色树叶边缘出现紫色,红色花朵边缘出现青色。在安防场景下,监控画面里的车牌边缘全是彩虹,根本没法看。
第二代:边缘导向插值——方向自适应
为了解决边缘伪色问题,业界提出了边缘导向插值。核心思想:先判断当前像素处于哪个方向(水平、垂直、对角),然后沿着边缘方向插值,避免跨边缘插值。
我当年在调试车载摄像头时,用的是一个经典的Hamilton-Adams算法。它先计算水平和垂直方向的梯度,选择梯度小的方向进行插值。
// 边缘导向的G通道插值// 别这样写:直接硬编码阈值判断方向,不同场景下阈值要动态调整uint16_tdemosaic_edge_guided_g(uint16_t*bayer,intx,inty,intwidth){// 计算水平梯度intgrad_h=abs(bayer[y*width+(x-1)]-bayer[y*width+(x+1)]);// 计算垂直梯度intgrad_v=abs(bayer[(y-1)*width+x]-bayer[(y+1)*width+x]);// 这里踩过坑:梯度阈值不能固定,要根据噪声水平调整// 低光场景下梯度会被噪声淹没,需要先做降噪if(grad_h<grad_v){// 水平方向更平滑,沿水平插值return(bayer[y*width+(x-1)]+bayer[y*width+(x+1)])>>1;}elseif(grad_v<grad_h){// 垂直方向更平滑return(bayer[(y-1)*width+x]+bayer[(y+1)*width+x])>>1;}else{// 两个方向差不多,用双线性return(bayer[(y-1)*width+x]+bayer[(y+1)*width+x]+bayer[y*width+(x-1)]+bayer[y*width+(x+1)])>>2;}}这个方案比双线性好很多,但问题在于:梯度计算本身受噪声影响很大。在低光场景下,梯度方向判断经常出错,导致插值方向选反,伪色反而更严重。我在车载夜视场景下吃过这个亏——晚上路灯下的车牌,边缘全是锯齿。
第三代:基于残差插值——从“猜颜色”到“猜差异”
2014年左右,一篇ICIP论文提出了残差插值(Residual Interpolation)的概念。核心思想:不直接插值目标颜色,而是先插值一个参考通道(通常是G通道),然后插值目标通道与参考通道的差值。
这个思路很巧妙。因为不同颜色通道在局部区域的相关性很高,它们的差值变化比单个通道更平滑。插值差值比插值原始值更准确。
// 残差插值恢复R通道// 注意:这里假设G通道已经完整恢复uint16_tdemosaic_residual_r(uint16_t*bayer_r,uint16_t*g_full,intx,inty,intwidth){// 先计算已知R位置的残差:R - G// 这里踩过坑:残差计算前要确保G值已经去马赛克完成,否则残差不准intresidual=bayer_r[y*width+x]-g_full[y*width+x];// 对残差进行插值(假设用双线性)intresidual_interp=bilinear_interp(residual,x,y,width);// 最终R值 = 插值后的残差 + G值returnresidual_interp+g_full[y*width+x];}这个方案在纹理丰富的区域表现很好,但在纯色区域反而会引入噪声。因为纯色区域的残差应该是常数,但插值过程会引入高频噪声。后来我在调试时加了一个自适应开关:根据局部方差判断是纹理区还是平坦区,平坦区用传统插值,纹理区用残差插值。
第四代:基于稀疏表示和字典学习
2010年代,压缩感知理论被引入去马赛克领域。核心思想:自然图像在某个变换域(如DCT、小波)是稀疏的,可以利用这个先验来重建缺失的颜色。
具体做法是:先训练一个过完备字典,字典中的每个原子代表一种图像块模式。去马赛克时,将已知像素投影到字典上,找到最稀疏的表示,然后重建缺失像素。
这个方案理论很美,但实际落地时有两个痛点:一是字典训练需要大量高质量图像,不同场景(室内、室外、夜景)需要不同字典;二是计算量巨大,在嵌入式平台上跑不动。我在安防IPC上试过,一帧1080p图像要处理3秒,直接被产品经理骂了。
第五代:深度学习驱动的色彩重建
2017年,CVPR上出现了第一篇用CNN做去马赛克的论文。从此,这个领域被深度学习彻底颠覆。
深度学习的核心优势:端到端学习,不需要手工设计特征。网络可以自动学习到复杂的色彩相关性、纹理模式、边缘方向。
我实际部署过的一个轻量级网络结构(适合移动端):
输入:Bayer raw (4通道,RGGB分离) ↓ 卷积层1: 3x3, 32通道, ReLU ↓ 残差块 x 4: 每个残差块包含两个3x3卷积,跳跃连接 ↓ 卷积层2: 3x3, 12通道 (输出RGB三个通道,每个通道4x超分) ↓ PixelShuffle: 将12通道重排为3通道HxW ↓ 输出:RGB全彩图像这个网络在DIV2K数据集上训练后,PSNR比传统方法高3-5dB。但落地时遇到两个坑:
第一个坑是训练数据。公开数据集大多是高质量DSLR拍摄的,和手机/安防sensor的噪声特性完全不同。直接用公开模型,在真实sensor数据上效果很差。后来我用自己的sensor采集了上万张raw图,配合ISP pipeline做数据增强(加噪声、调增益),才把模型训好。
第二个坑是计算量。全卷积网络在1080p图像上推理一次需要几百毫秒,在安防IPC的ARM芯片上根本跑不动。后来用了模型剪枝和量化,把模型压缩到原来的1/5,精度只掉了0.3dB,推理时间降到30ms。
当前工业界的实际选择
说了这么多代算法,现在工业界到底用哪个?我的经验是:
- 高端手机(旗舰机):深度学习方案。有专门的NPU或DSP加速,模型可以做到10ms以内。但要注意,不同sensor要单独训练模型,否则效果打折扣。
- 中端手机/车载:残差插值+边缘导向的混合方案。计算量适中,效果可接受。关键是要做好自适应切换——平坦区用简单插值,纹理区用复杂算法。
- 安防IPC/低端设备:边缘导向插值就够了。但一定要配合降噪,否则低光场景下伪色严重。我一般会在去马赛克前先做一次双边滤波。
个人经验性建议
不要迷信PSNR。PSNR高的算法,主观效果不一定好。我见过PSNR高0.5dB但伪色更明显的算法。一定要做主观评测,特别是边缘和纹理区域。
去马赛克和降噪要联合设计。先降噪再去马赛克,会损失细节;先去马赛克再降噪,伪色会被放大。我现在的做法是:在去马赛克过程中嵌入一个轻量级降噪模块,对梯度计算和残差计算做噪声抑制。
Bayer pattern的选择影响算法设计。RGGB、RCCB、RYYCy等不同pattern,色彩相关性不同,去马赛克算法要针对性调整。特别是RCCB(安防常用),因为C通道(Clear)没有颜色信息,传统算法直接套用会出问题。
调试时准备一张标准色卡和一张纹理卡。色卡看伪色,纹理卡看锯齿和细节损失。我每次调完去马赛克参数,都会用这两张卡做回归测试。
深度学习不是万能药。在极端低光(<1 lux)场景下,信噪比太低,网络也会“瞎猜”。这时候传统方法的先验知识(如平滑性假设)反而更可靠。我现在的做法是:根据ISO值动态切换算法,ISO高时用传统方法,ISO低时用深度学习。
最后说一句:去马赛克算法发展了三十年,从简单的插值到复杂的深度学习,本质都是在解决“信息缺失”的问题。但无论算法多先进,物理规律摆在那里——缺失的信息永远无法完美恢复。我们的目标不是“完美”,而是“看起来舒服”。这个认知,是我调试了上百个Camera项目后最深刻的体会。