3. 去马赛克算法:从Bayer到全彩

各位好,欢迎来到第三章。今天聊去马赛克——说白了,就是把Bayer格式的RAW图,变成一张完整的RGB彩色图像。

我刚开始做ISP时,觉得这步不就是插值嘛,随便填填就行。后来被一个边缘伪彩问题折磨了两周,才明白这里面的门道有多深。嗯,咱们一步步来。

3.1 先搞懂Bayer格式

大部分图像传感器,每个像素只能感知光的强度,分不清颜色。那怎么得到彩色?答案是在像素表面覆盖一层滤色阵列——最常见的就是Bayer Pattern。

Bayer阵列的排列规则很简单:

  • 50% 绿色(G)——人眼对绿光最敏感
  • 25% 红色(R)
  • 25% 蓝色(B)

常见的排列是 RGGB 或 BGGR。我习惯用RGGB,因为大部分sensor厂商默认就是这个顺序。

关键理解:每个像素只记录一个颜色通道的值,另外两个通道是缺失的。去马赛克的任务,就是根据周围像素,把缺失的两个通道“猜”出来。

举个例子,一个2x2的RGGB块:

位置 像素值 实际颜色
(0,0) 200 R
(0,1) 180 G
(1,0) 190 G
(1,1) 150 B

对于位置(0,0)的R像素,它缺少G和B值。我们需要用周围的G和B像素来估算。

3.2 双线性插值——最朴素的方法

双线性插值,说白了就是“取平均”。对每个缺失的通道,直接用周围同色像素的平均值来填充。

举个例子: 在RGGB阵列中,要插值位置(0,0)的G值:

// 伪代码:双线性插值G通道
// 对于R像素位置 (i,j)
G(i,j) = ( G(i-1,j) + G(i+1,j) + G(i,j-1) + G(i,j+1) ) / 4

这个方法的好处是简单、快。我在做低功耗嵌入式项目时,如果算力有限,偶尔会用这个方案。但代价也很明显——边缘模糊。

我曾经踩过的坑: 在一个1080P的监控项目里,用了双线性插值。结果画面里的文字边缘全是锯齿和伪彩。客户直接说“这画面像油画”。从那以后,我对双线性插值就只敢用在低分辨率预览流上了。

3.3 边缘导向插值——聪明一点的做法

双线性插值的问题在于,它不管图像内容,一律平均。但真实图像里,边缘方向很重要。你想想看,沿着边缘方向插值,颜色过渡自然;跨过边缘插值,就会产生伪彩。

边缘导向插值的核心思路:先判断边缘方向,再选择插值方向。

具体做法:

  1. 计算水平方向和垂直方向的梯度
  2. 比较梯度大小,判断边缘方向
  3. 沿着梯度小的方向(即边缘方向)进行插值

举个例子,插值G通道时:

// 计算水平和垂直梯度
delta_h = |G(i,j-1) - G(i,j+1)|
delta_v = |G(i-1,j) - G(i+1,j)|

if (delta_h < delta_v) {
    // 水平方向更平滑,用水平邻居插值
    G(i,j) = (G(i,j-1) + G(i,j+1)) / 2
} else if (delta_v < delta_h) {
    // 垂直方向更平滑,用垂直邻居插值
    G(i,j) = (G(i-1,j) + G(i+1,j)) / 2
} else {
    // 无法判断,用四个方向平均
    G(i,j) = (G(i-1,j) + G(i+1,j) + G(i,j-1) + G(i,j+1)) / 4
}

这个方法比双线性好很多。边缘清晰度明显提升,伪彩也减少了。但有个问题——在纹理密集区域,梯度判断容易出错。

3.4 自适应插值——更精细的调优

自适应插值,说白了就是“看菜下饭”。它不只是判断一个简单的水平/垂直方向,而是根据局部区域的纹理复杂度,动态调整插值权重。

我常用的自适应插值流程:

  1. 计算局部梯度矩阵:在5x5或7x7窗口内,计算多个方向的梯度
  2. 分析纹理方向:不只是水平和垂直,还包括45°、135°等对角方向
  3. 生成插值权重:根据梯度大小,给每个方向分配权重
  4. 加权插值:用权重对周围像素做加权平均

我的个人习惯: 在自适应插值中,我会给G通道更高的权重。因为G通道占Bayer阵列的一半,而且人眼对绿色最敏感。G通道插值好了,整体画质就稳了八成。

下面是一个简化的自适应插值代码示例:

// 自适应插值G通道(简化版)
float weights[4] = {0};
float gradients[4] = {0};

// 计算四个方向的梯度
gradients[0] = abs(G(i,j-1) - G(i,j+1));  // 水平
gradients[1] = abs(G(i-1,j) - G(i+1,j));  // 垂直
gradients[2] = abs(G(i-1,j-1) - G(i+1,j+1));  // 45°
gradients[3] = abs(G(i-1,j+1) - G(i+1,j-1));  // 135°

// 根据梯度计算权重(梯度越小,权重越大)
float total_weight = 0;
for (int k = 0; k < 4; k++) {
    weights[k] = 1.0f / (gradients[k] + 0.001f);  // 加小常数防除零
    total_weight += weights[k];
}

// 加权插值
float G_interp = 0;
G_interp += weights[0] * (G(i,j-1) + G(i,j+1)) / 2;
G_interp += weights[1] * (G(i-1,j) + G(i+1,j)) / 2;
G_interp += weights[2] * (G(i-1,j-1) + G(i+1,j+1)) / 2;
G_interp += weights[3] * (G(i-1,j+1) + G(i+1,j-1)) / 2;
G_interp /= total_weight;

3.5 三种方法对比

我把这三种方法放在一起对比,方便你选型:

方法 计算量 边缘质量 伪彩控制 适用场景
双线性插值 低 差(模糊) 差 低功耗、预览流
边缘导向插值 中 中(边缘清晰,纹理区易错) 中 通用场景、中等算力
自适应插值 高 好(纹理区也稳定) 好 高质量拍照、旗舰机

3.6 去马赛克流程总览

下面这张图,是我自己画的一个去马赛克整体流程。你可以看到,从Bayer输入到全彩输出,中间经历了哪些关键步骤。

去马赛克算法流程总览 Bayer RAW 输入 选择插值策略 双线性插值 边缘导向插值 自适应插值 全彩 RGB 输出 实际项目中,自适应插值最常用,但双线性插值在低功耗场景仍有价值

3.7 实际项目中的选择建议

说了这么多,到底该用哪种?我根据项目经验,给你几个参考:

  • 低功耗IoT摄像头(比如门铃、电池相机):用双线性插值,配合简单的后处理去伪彩。省电第一。
  • 安防监控(日夜全彩):用边缘导向插值。算力够用,画质也能接受。
  • 手机拍照/高端相机:必须上自适应插值。我见过一个旗舰机项目,光去马赛克模块就迭代了四版,最后才把边缘伪彩压下去。

一句话总结: 去马赛克没有银弹。选哪种方法,取决于你的算力预算、画质要求和功耗限制。我个人建议,如果条件允许,优先上自适应插值——省下来的后期调优时间,远比多花的算力值钱。