2. RAW域预处理

各位同学,欢迎来到RAW域预处理这一章。说实话,这是整个ISP链路里最「脏」最「累」的活,但也是决定画质下限的关键。你想想看,传感器拿到的原始数据,就像一块刚挖出来的玉石——满是裂痕和杂质。我们预处理要做的,就是把这些瑕疵先处理掉,后面的算法才能好好干活。

我个人习惯把预处理分成四个模块:坏点校正(DPC)、黑电平校正(BLC)、镜头阴影校正(LSC)、绿色平衡(GB)。它们之间没有严格的先后顺序,但通常BLC要最先做,因为后面的算法都依赖正确的黑电平基准。

核心观点:预处理做得好不好,直接决定后续去噪、插值、白平衡的效果。我见过太多项目,后面算法调得再好,预处理没做好,画质就是上不去。
RAW域预处理知识体系 RAW域预处理 坏点校正 (DPC) 黑电平校正 (BLC) 镜头阴影校正 (LSC) 绿色平衡 (GB) 关键依赖关系: • BLC 通常最先执行,为后续模块提供正确的黑电平基准 • DPC 可在 BLC 之前或之后,但建议在 BLC 之后做,避免黑电平偏移影响坏点检测 • LSC 和 GB 通常放在 BLC 和 DPC 之后,顺序可互换

1. 黑电平校正(BLC)—— 先把基准找对

黑电平校正,说白了就是告诉芯片:「0」到底代表多少。传感器在完全无光的情况下,输出并不是0,而是一个偏置值,通常在几十到几百个DN(数字数)之间。这个偏置就是黑电平。

为什么要做BLC?因为后面的所有处理——坏点检测、去噪、白平衡——都假设黑电平是0。如果黑电平没校正好,暗部细节会偏色,甚至出现「黑不黑」的问题。

我的经验:有一次项目里,暗部总是偏绿。查了三天,最后发现是BLC参数没跟上温度变化。黑电平会随温度漂移,每升高10°C,黑电平可能增加1-2个DN。所以,我建议用动态BLC,实时统计暗像素的均值来更新。

BLC的典型做法有两种:

  • 全局BLC:整帧减去一个固定值。简单,但无法应对传感器不均匀性。
  • 逐行/逐列BLC:每行或每列单独统计暗像素均值。效果好,但需要OB(光学黑)区域支持。

代码实现其实不复杂:

// 全局BLC示例
void black_level_correction(uint16_t *raw, int width, int height, int blc) {
    for (int i = 0; i < width * height; i++) {
        if (raw[i] > blc)
            raw[i] -= blc;
        else
            raw[i] = 0;  // 防止下溢
    }
}
注意:BLC之后一定要做饱和处理。如果减完变成负数,要截断到0。否则后面算法会出奇怪的问题——我曾经因为这个bug,调试了整整一个通宵。

2. 坏点校正(DPC)—— 把「坏像素」揪出来

传感器生产过程中,难免会有一些像素点响应异常。有的是死点(一直亮或一直暗),有的是热点(噪声特别大)。这些坏点如果不处理,会在图像上形成固定的亮点或暗点,非常影响观感。

坏点校正分两步:检测和替换。

坏点类型 表现 检测方法 替换策略
死点(暗) 输出始终接近0 与邻域均值差异过大 中值滤波或邻域插值
死点(亮) 输出始终接近饱和值 与邻域均值差异过大 中值滤波或邻域插值
热点 噪声方差明显偏高 多帧统计方差 中值滤波或双边滤波

检测算法我常用的是「邻域比较法」。以当前像素为中心,取一个3x3或5x5的窗口,计算邻域像素的均值或中值。如果当前像素值与邻域中值的差超过一个阈值,就判定为坏点。

阈值怎么设?我一般用动态阈值——根据当前区域的亮度水平自适应调整。暗区阈值小一些,亮区阈值大一些。固定阈值在暗区容易漏检,在亮区又容易误检。

替换策略上,我个人偏好中值滤波。为什么?因为均值滤波会把坏点的「坏」扩散到邻域,而中值滤波能更好地保留边缘。当然,如果坏点恰好位于边缘上,中值也可能出错——这时候可以用方向性插值,沿着边缘方向做替换。

// 简单的坏点检测与替换(3x3窗口)
void dpc_process(uint16_t *raw, int width, int height, int threshold) {
    for (int y = 1; y < height - 1; y++) {
        for (int x = 1; x < width - 1; x++) {
            int idx = y * width + x;
            // 取3x3邻域中值
            uint16_t neighbors[9];
            int n = 0;
            for (int dy = -1; dy <= 1; dy++)
                for (int dx = -1; dx <= 1; dx++)
                    neighbors[n++] = raw[(y+dy)*width + (x+dx)];
            // 排序取中值(这里省略排序实现)
            uint16_t median = quick_select(neighbors, 9, 4);
            // 判断是否为坏点
            if (abs(raw[idx] - median) > threshold) {
                raw[idx] = median;  // 替换为中值
            }
        }
    }
}
避坑指南:我曾经在项目中遇到一个问题——坏点校正后,图像边缘出现了「振铃」现象。后来发现是阈值设得太低,把正常边缘误判为坏点了。解决办法是:在检测时加入边缘强度判断,如果当前像素位于强边缘上,就放宽阈值。

3. 镜头阴影校正(LSC)—— 把「暗角」补回来

镜头阴影,也叫暗角或亮度衰减。任何镜头都有这个问题——中心亮、四周暗。原因很简单:镜头的光学路径长度不同,边缘光线到达传感器的角度更大,能量衰减更明显。

LSC的原理就是给每个像素乘上一个增益系数,让四周的亮度补到和中心一致。这个增益系数通常通过标定得到:拍一张均匀白板,然后计算每个像素需要乘多少才能达到中心亮度。

增益系数的存储是个问题。如果每个像素存一个系数,一张1200万像素的图就需要1200万个系数,太占内存。实际工程中,我们通常用网格方式存储:

  • 在图像上均匀取NxN个网格点(比如17x17或33x33)
  • 每个网格点存一个增益值
  • 其他像素的增益通过双线性插值得到
网格密度 内存占用 精度 适用场景
9x9 81个系数 较低 低端摄像头、快速原型
17x17 289个系数 中等 大多数手机摄像头
33x33 1089个系数 较高 高端相机、广角镜头
注意:LSC增益不能太大。如果边缘增益超过2.0,意味着信号被放大了2倍,噪声也会被放大2倍。这时候边缘的信噪比会明显下降。我建议增益上限控制在1.5-2.0之间,如果暗角太严重,应该从镜头设计上解决,而不是全靠算法。

另外,LSC还要考虑色差。不同波长的光衰减程度不同,所以R、G、B通道的增益系数应该分开标定。否则会出现「中心白、四周偏色」的问题——这个我踩过坑,教训深刻。

4. 绿色平衡(GB)—— 让G通道「自洽」

绿色平衡是Bayer RAW特有的一个步骤。Bayer格式中,绿色像素的数量是红色和蓝色的两倍,而且它们分布在不同的行上(RGr和Gb)。理论上,Gr和Gb的响应应该一致,但实际上由于制造工艺的微小差异,Gr和Gb之间会有偏差。

这种偏差如果不校正,会在图像上产生「棋盘格」纹理,尤其是在均匀区域。你想想看,本来应该是纯色的天空,结果出现了周期性的明暗变化,多难受。

绿色平衡的做法很简单:统计Gr和Gb的均值,然后调整其中一个通道,使两者均值相等。

// 绿色平衡示例(全局统计法)
void green_balance(uint16_t *raw, int width, int height) {
    long sum_gr = 0, sum_gb = 0;
    int cnt_gr = 0, cnt_gb = 0;
    
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            if ((y % 2 == 0) && (x % 2 == 1)) {  // Gr位置
                sum_gr += raw[y*width + x];
                cnt_gr++;
            } else if ((y % 2 == 1) && (x % 2 == 0)) {  // Gb位置
                sum_gb += raw[y*width + x];
                cnt_gb++;
            }
        }
    }
    
    float mean_gr = (float)sum_gr / cnt_gr;
    float mean_gb = (float)sum_gb / cnt_gb;
    float gain = mean_gr / mean_gb;  // 以Gr为基准
    
    // 调整Gb通道
    for (int y = 1; y < height; y += 2) {
        for (int x = 0; x < width; x += 2) {
            int idx = y * width + x;
            raw[idx] = (uint16_t)(raw[idx] * gain + 0.5f);
        }
    }
}
我的建议:全局统计法在均匀场景下效果不错,但如果场景中有大面积纹理,Gr和Gb的统计均值可能受场景内容影响。更稳健的做法是:只在图像的平滑区域做统计,避开纹理和边缘。可以用局部方差来判断是否为平滑区域。

还有一种更精细的做法——局部绿色平衡。把图像分成多个小块,每个块独立计算Gr/Gb比例。这样能应对传感器非均匀性,但计算量会大一些。在移动平台上,我通常用全局法加一个简单的低通滤波,效果已经足够。

总结一下:预处理四个模块,BLC是基础,DPC是去瑕疵,LSC是补亮度,GB是调均匀。它们各自解决不同的问题,但目标一致——为后续的ISP处理提供干净、准确的RAW数据。

嗯,这一章的内容就到这里。这些预处理算法看起来简单,但实际工程中坑很多。温度漂移、传感器非均匀性、镜头一致性……每一个都够你调上几天。希望我的经验能帮你少走一些弯路。