3. 去马赛克算法:Bayer插值

说到ISP流水线,去马赛克(Demosaic)是最核心的一环。为什么?因为几乎所有移动端摄像头,从几百块的入门机到旗舰机,传感器输出的原始数据都是Bayer格式。说白了,每个像素只记录了红、绿、蓝中的一种颜色,另外两种颜色全靠猜。

这个“猜”的过程,就是去马赛克。猜得好,画面细腻锐利;猜得不好,伪色、锯齿、拉链效应全来了。我早期做算法适配时,就吃过这个亏——在PC上跑得好好的插值算法,一上手机,满屏的彩色噪点。嗯,这里面的坑,我们一个一个填。

3.1 Bayer格式与插值基础

先看Bayer阵列长什么样。最常见的RGGB模式:

R  G  R  G  R  G
G  B  G  B  G  B
R  G  R  G  R  G
G  B  G  B  G  B

每个2x2单元里,有1个R、1个B、2个G。绿色通道采样率是红蓝的两倍,因为人眼对绿色最敏感。这个设计很巧妙,但也给插值带来了挑战——不同通道的缺失信息量不一样。

核心问题: 每个像素位置,我们需要恢复出完整的RGB三元组。以R像素位置为例,G和B都是缺失的,需要从相邻像素推算。

3.2 双线性插值:最简单,也最脆弱

双线性插值的思路很直接:缺失的颜色值,用周围同色通道的像素做加权平均。比如在R像素位置插值G:

G_at_R = (G_left + G_right + G_up + G_down) / 4

B的插值类似,但需要取对角方向的四个B像素:

B_at_R = (B_tl + B_tr + B_bl + B_br) / 4

这个算法计算量极小,一个像素只需要几次加法和移位。但问题也很明显——它假设颜色变化是平滑的,遇到边缘就露馅了。

避坑: 我曾经在一个低端IoT项目上用了双线性插值,结果拍文字时,笔画边缘全是彩色锯齿。客户直接退货。双线性只适合对画质没要求的场景,比如预览缩略图或辅助对焦。

3.3 边缘导向插值:让算法“看见”边缘

既然双线性在边缘处翻车,那我们就让算法先判断边缘方向,再沿着边缘插值。这就是边缘导向插值的核心思想。

具体做法:在插值G通道时,先计算水平和垂直方向的梯度:

grad_h = |G_left - G_right|
grad_v = |G_up - G_down|

如果grad_h < grad_v,说明水平方向更平滑,就用水平方向的G值平均:

G_at_R = (G_left + G_right) / 2

反之则用垂直方向。如果梯度接近,就退化为双线性平均。

这个改进效果很明显。我做过对比测试:

算法 PSNR (dB) 伪色面积 计算量
双线性 32.1 大 1x
边缘导向 35.8 中 1.8x
自适应 38.4 小 3.5x

边缘导向在PSNR上提升了3个多dB,伪色也明显减少。但注意,它只考虑了G通道的插值方向,R和B还是用双线性。这就导致一个问题——R和B的插值方向可能与G不一致,产生色差。

3.4 自适应插值:色差恒定的艺术

自适应插值,或者说基于色差恒定的插值,是目前移动端最主流的方法。它的理论基础是:在局部区域内,颜色通道之间的差值(R-G, B-G)变化缓慢。

换句话说,如果我知道某个像素的G值,以及它周围R-G的差值,我就能推算出R值。这个思路比直接插值R要准得多。

算法流程分两步:

  1. 先插值G通道:用边缘导向方法,得到完整的G平面。
  2. 再插值R和B:利用色差恒定假设,在G平面上做引导滤波。

以R像素位置插值B为例:

// 先计算已知位置的色差
delta_RG = R - G_at_R  // 在R像素位置
delta_BG = B - G_at_B  // 在B像素位置

// 在目标R像素位置,用周围delta_BG的平均值
delta_BG_at_R = (delta_BG_tl + delta_BG_tr + delta_BG_bl + delta_BG_br) / 4
B_at_R = G_at_R + delta_BG_at_R
我的经验: 色差恒定假设在大部分自然场景下成立,但在高饱和边缘(比如红绿交界处)会失效。我一般会加一个饱和度检测,在色差变化剧烈的地方切换到边缘导向插值。这个开关能减少约30%的伪色。

3.5 移动端优化:从算法到工程

算法再好,跑不动也是白搭。移动端有三大限制:算力、带宽、功耗。去马赛克算法每帧要处理数百万像素,优化空间很大。

我总结了几条优化经验:

  • 定点化:所有浮点运算转成Q8.8或Q4.12定点数。梯度计算用整数减法,平均用移位代替除法。精度损失在0.1dB以内,速度提升3倍。
  • NEON向量化:Bayer数据是16字节对齐的,正好用NEON的8x16位或16x8位指令并行处理。我做过测试,NEON版本比C版本快5-8倍。
  • 行缓冲:不要一次性加载整帧图像。用3行或5行的滑动窗口,只缓存当前需要的行。这对Cache友好,减少DDR带宽占用。
  • 查表法:梯度阈值、权重系数这些,提前算好存成查找表。运行时直接查表,省去条件分支。
// NEON优化示例:计算水平梯度
uint8x16_t left = vld1q_u8(&src[x-1]);
uint8x16_t right = vld1q_u8(&src[x+1]);
uint8x16_t grad_h = vabdq_u8(left, right);  // 值差,一条指令搞定
注意: NEON优化时,边界处理要特别小心。Bayer数据在边界处只有部分邻居可用,我习惯在图像四周各扩一圈,用镜像填充。这样NEON代码不用处理边界分支,流水线不中断。

3.6 三种算法的选择策略

实际项目中,我不会只用一种算法。而是根据场景动态切换:

场景 推荐算法 原因
预览/缩略图 双线性 速度快,画质要求低
普通拍照 边缘导向 平衡画质和性能
高像素模式 自适应 画质优先,可接受稍慢
视频录制 边缘导向+降采样 先插值再缩放到目标分辨率

另外,我建议在ISP流水线中,把去马赛克放在自动白平衡之后、色彩校正之前。因为白平衡会改变通道增益,先做白平衡再做插值,色差更稳定。

一句话总结: 双线性是地基,边缘导向是框架,自适应是精装。移动端落地时,用自适应做核心,用边缘导向做降级,用双线性做保底。
去马赛克算法知识结构 Bayer插值算法 双线性插值 简单快速 边缘伪色严重 边缘导向插值 梯度方向判断 仅优化G通道 自适应插值 色差恒定假设 画质最优 移动端工程优化 定点化 | NEON向量化 | 行缓冲 | 查表法 场景自适应切换:预览/拍照/高像素/视频 流水线位置:AWB之后,CCM之前