027、去马赛克算法演进:从双线性插值到方向自适应,再到深度学习驱动的色彩重建
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(x1||xwidth-1||y1||yheight-1){returnbayer[y*widthx];// 边界直接返回原始值别问我为什么}// 取上下左右四个G像素的平均uint16_tg_upbayer[(y-1)*widthx];uint16_tg_downbayer[(y1)*widthx];uint16_tg_leftbayer[y*width(x-1)];uint16_tg_rightbayer[y*width(x1)];return(g_upg_downg_leftg_right)2;}这个方案的问题很明显它假设图像在局部是平滑的但实际图像边缘处色彩变化剧烈。结果就是边缘处出现明显的伪色——绿色树叶边缘出现紫色红色花朵边缘出现青色。在安防场景下监控画面里的车牌边缘全是彩虹根本没法看。第二代边缘导向插值——方向自适应为了解决边缘伪色问题业界提出了边缘导向插值。核心思想先判断当前像素处于哪个方向水平、垂直、对角然后沿着边缘方向插值避免跨边缘插值。我当年在调试车载摄像头时用的是一个经典的Hamilton-Adams算法。它先计算水平和垂直方向的梯度选择梯度小的方向进行插值。// 边缘导向的G通道插值// 别这样写直接硬编码阈值判断方向不同场景下阈值要动态调整uint16_tdemosaic_edge_guided_g(uint16_t*bayer,intx,inty,intwidth){// 计算水平梯度intgrad_habs(bayer[y*width(x-1)]-bayer[y*width(x1)]);// 计算垂直梯度intgrad_vabs(bayer[(y-1)*widthx]-bayer[(y1)*widthx]);// 这里踩过坑梯度阈值不能固定要根据噪声水平调整// 低光场景下梯度会被噪声淹没需要先做降噪if(grad_hgrad_v){// 水平方向更平滑沿水平插值return(bayer[y*width(x-1)]bayer[y*width(x1)])1;}elseif(grad_vgrad_h){// 垂直方向更平滑return(bayer[(y-1)*widthx]bayer[(y1)*widthx])1;}else{// 两个方向差不多用双线性return(bayer[(y-1)*widthx]bayer[(y1)*widthx]bayer[y*width(x-1)]bayer[y*width(x1)])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值已经去马赛克完成否则残差不准intresidualbayer_r[y*widthx]-g_full[y*widthx];// 对残差进行插值假设用双线性intresidual_interpbilinear_interp(residual,x,y,width);// 最终R值 插值后的残差 G值returnresidual_interpg_full[y*widthx];}这个方案在纹理丰富的区域表现很好但在纯色区域反而会引入噪声。因为纯色区域的残差应该是常数但插值过程会引入高频噪声。后来我在调试时加了一个自适应开关根据局部方差判断是纹理区还是平坦区平坦区用传统插值纹理区用残差插值。第四代基于稀疏表示和字典学习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项目后最深刻的体会。