3、去马赛克算法与硬件加速

去马赛克,说白了就是把Bayer格式的RAW图恢复成完整的RGB彩色图像。这是ISP流水线里最基础也最关键的环节之一。我刚开始做ISP时,总觉得这步不就是插值嘛,随便算算就行。后来踩了不少坑才明白——去马赛克做不好,后面的白平衡、降噪全是白搭。

3.1 为什么需要去马赛克?

CMOS传感器每个像素只能感知一种颜色。常见的Bayer模式是RGGB排列:一半绿色,四分之一红色,四分之一蓝色。每个像素只有单通道数据,要得到全彩图,就得靠周围像素把缺失的两个通道“猜”出来。

核心矛盾:传感器面积有限,每个像素只能放一个滤色片。去马赛克就是解决“单像素单通道 → 全像素三通道”的数学问题。

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

双线性插值是最简单的做法。拿当前像素周围同颜色的像素做加权平均。比如R像素位置缺G和B,那就取上下左右四个G像素的平均值作为G值,取四个角上B像素的平均值作为B值。

// 以R像素位置插值G为例
G_at_R = (G_left + G_right + G_up + G_down) / 4

这个方法计算量极小,但问题也很明显。我在一个低照度项目里试过,边缘全是彩色锯齿,尤其是文字边缘,红绿蓝像彩虹一样散开。说白了,双线性插值没考虑图像内容,遇到边缘就露馅。

方法 计算量 边缘质量 硬件开销
双线性插值 极低 差(伪彩严重) 极小
边缘导向插值 中等 较好 中等
自适应插值 较高 优秀 较大

3.3 边缘导向插值——加一点方向判断

边缘导向插值的思路很简单:先判断当前像素处于水平边缘还是垂直边缘,然后只沿着边缘方向做插值,不跨边缘。

具体做法是计算水平和垂直方向的梯度:

// 计算水平梯度
grad_h = |G_left - G_right|

// 计算垂直梯度
grad_v = |G_up - G_down|

// 梯度小的方向就是边缘方向
if (grad_h < grad_v) {
    // 水平边缘,用左右像素插值
    G = (G_left + G_right) / 2
} else {
    // 垂直边缘,用上下像素插值
    G = (G_up + G_down) / 2
}

我在一个1080p@60fps的项目里用过这个方案。效果比双线性好很多,彩色锯齿基本消失。但遇到45度斜边或者纹理密集区域,还是会出问题。为什么?因为只考虑了水平和垂直两个方向,太死板了。

我的经验:边缘导向插值适合资源受限的嵌入式场景。如果芯片面积和功耗有严格限制,这个方案是性价比最高的选择。

3.4 自适应插值——更聪明的做法

自适应插值把方向判断扩展到多个方向,甚至结合色差恒定性原理。色差恒定性是说:在局部区域内,R-G和B-G的差值变化很缓慢。利用这个特性,可以先插值G通道,再用G通道辅助插值R和B。

典型的自适应插值流程:

  1. 插值G通道:在多个方向(水平、垂直、对角线)上计算梯度,选择梯度最小的方向做插值
  2. 计算色差:在已知R/G/B位置计算R-G和B-G的色差值
  3. 插值色差:对色差图做双线性插值,得到缺失位置的色差值
  4. 恢复R/B:用插值后的色差加上G值,得到R和B
// 自适应插值核心步骤(简化)
// Step 1: 插值G
grad_h = |G_left - G_right| + |R - 2*R_center + R_right|
grad_v = |G_up - G_down| + |R - 2*R_center + R_down|
// 选择梯度小的方向插值G

// Step 2: 计算色差
delta_RG = R - G_at_R

// Step 3: 插值色差
delta_RG_interp = bilinear_interp(delta_RG)

// Step 4: 恢复R
R_recovered = G + delta_RG_interp

这个方案效果最好,但计算量也最大。我记得在一个4K@30fps的项目里,纯软件跑自适应插值,CPU占用率直接飙到80%以上。没办法,只能上硬件加速。

注意:自适应插值在纹理极其密集的区域(比如草地、布料)仍然可能出现伪彩。这时候需要结合后面的伪彩抑制模块一起处理。

3.5 硬件流水线设计——Line Buffer与Window Buffer

去马赛克算法需要访问邻域像素。在硬件里,你不能像软件那样随便读内存。必须用Line Buffer和Window Buffer来构建数据窗口。

Line Buffer:说白了就是一行一行的数据缓存。比如你要做5x5的窗口,就需要缓存4行数据(加上当前行共5行)。每来一个新像素,Line Buffer就滚动更新。

Window Buffer:从Line Buffer里取出一个窗口大小的数据块。比如5x5窗口,就是25个像素同时送到计算单元。

去马赛克硬件流水线架构 Line Buffer 阵列(4行缓存,每行1920像素@1080p) Window Buffer(5×5窗口) 25个像素同时输出 并行计算单元(梯度计算 + 插值) RGB像素输出 数据复用 每个像素被 25次复用

我建议Line Buffer的深度至少覆盖算法需要的窗口大小。比如5x5窗口,至少4行Line Buffer。如果芯片面积允许,多缓存一两行可以降低控制逻辑的复杂度。

3.6 加速策略——并行度与数据复用

去马赛克的硬件加速核心就两个词:并行和复用。

并行度:窗口里的25个像素可以同时做梯度计算。比如计算水平梯度,左右两个像素可以并行读取。我见过一个设计,一个时钟周期内同时计算8个方向的梯度,然后选最小的那个。

数据复用:每个像素会被多个窗口用到。比如5x5窗口,中心像素会被周围24个窗口复用。Line Buffer就是做这个的——把数据留在片上,避免重复从DDR读。

加速策略 具体做法 效果
像素级并行 窗口内所有像素同时读取 单周期完成梯度计算
行级流水 Line Buffer滚动更新,与计算单元并行 无等待周期
数据复用 片上缓存复用,减少DDR访问 带宽降低5~10倍
多窗口并行 同时处理多个相邻像素的窗口 吞吐量翻倍

我的建议:如果做1080p@60fps,4行Line Buffer + 5x5窗口 + 单像素流水就够了。如果做4K@60fps,建议把并行度提到同时处理2~4个像素,否则时钟频率会飙到很高。

我曾经在一个4K项目里踩过坑:Line Buffer只做了3行,结果窗口数据总是差一行,控制逻辑变得极其复杂。后来老老实实加到5行,问题迎刃而解。嗯,硬件设计有时候就是这样,省那一点面积,换来一堆麻烦。

3.7 三种算法的硬件开销对比

指标 双线性插值 边缘导向插值 自适应插值
Line Buffer行数 2行 3行 5行
窗口大小 3×3 3×3 5×5
乘法器数量 4个 8个 20~30个
逻辑门数(估算) ~5K gates ~15K gates ~50K gates
PSNR提升(相对双线性) 基准 +2~3dB +5~8dB

选哪种方案,取决于你的项目需求。低功耗IoT设备,双线性就够了。消费级摄像头,边缘导向是主流。高端安防或手机,必须上自适应插值。

总结一下:去马赛克不是越复杂越好。硬件架构师要做的,是在图像质量、芯片面积、功耗之间找到平衡点。我个人的习惯是:先评估目标PSNR,再反推算法复杂度,最后定硬件方案。千万别一上来就追求最牛的算法——你想想看,芯片面积翻倍,成本也翻倍,客户不一定买单。