3. 传统ISP Pipeline:黑电平校正、镜头阴影校正、坏点校正

各位同学,今天我们正式进入ISP Pipeline的第一站。很多人觉得这三步是“预处理中的预处理”,没什么技术含量。但我做了这么多年ISP,可以负责任地告诉你——这三步没做好,后面所有模块都在白费力气。

你想想看,Sensor送出来的原始数据,说白了就是一堆电荷转换成的数字信号。但这堆数字里藏着三个“脏东西”:暗电流偏置、镜头的光学衰减、还有那些死活不听话的坏像素。我们的任务,就是在它们污染整个图像之前,把它们清理掉。

3.1 黑电平校正(Black Level Correction, BLC)

3.1.1 为什么要做黑电平校正?

先问大家一个问题:Sensor在完全无光的情况下,输出应该是0吗?

答案是:不是。

因为Sensor的每个像素都存在暗电流,即使没有光子入射,电荷也会慢慢积累。另外,为了不让ADC在低端出现非线性,厂商会故意加一个固定的偏置电压。这个偏置,就是黑电平(Black Level)。

我记得刚入行时,遇到一个项目,拍出来的夜景图像整体发灰,黑色区域根本压不下去。排查了三天,最后发现是黑电平校正参数没更新,用的还是实验室的默认值。嗯,从那以后,我每次拿到新Sensor,第一件事就是看黑电平的寄存器值。

3.1.2 校正原理与实现

黑电平校正其实很简单:

// 经典的黑电平校正公式
pixel_out = pixel_raw - black_level;

// 如果考虑不同通道的差异(Bayer格式)
pixel_out = pixel_raw - black_level[channel];

但实际项目中,黑电平并不是一个固定值。它会随着温度、增益、曝光时间变化。所以成熟的方案会做动态黑电平校正:

  • OB(Optical Black)像素法:Sensor边缘有一圈被遮挡的像素,用它们的平均值作为当前帧的黑电平。
  • 暗帧法:在完全遮光条件下采集一帧,作为参考。
  • 统计法:从图像直方图的最低端估算黑电平。

关键指标:黑电平校正后,暗区的均值应该稳定在0附近,标准差控制在1个DN以内。如果暗区出现负值,说明校正过头了,需要做截断处理。

3.1.3 避坑指南

我曾经在一个HDR项目里,忽略了不同增益下的黑电平差异。低增益帧和高增益帧用了同一个黑电平值,结果合成后的图像在暗部出现了明显的拼接痕迹。后来我加了一个查表法,根据增益值动态调整黑电平,问题才解决。

3.2 镜头阴影校正(Lens Shading Correction, LSC)

3.2.1 阴影从哪里来?

你拿手机拍一张纯白墙壁,看看四角是不是比中心暗?这就是镜头阴影,也叫亮度衰减(Vignetting)。

原因很简单:镜头是个凸透镜,光线从中心通过时路径最短,从边缘通过时路径更长,而且还要经过更多镜片边缘的遮挡。根据余弦四次方定律,边缘亮度大致按cos⁴θ衰减。

但这还没完。不同颜色的光,衰减程度还不一样。这就导致了色彩阴影(Color Shading)——四角不仅暗,还偏色。

3.2.2 校正策略

镜头阴影校正的核心思路是:给每个像素乘一个增益系数,把衰减补回来。

// 基于网格的LSC实现
// 预先标定一个增益网格(例如17x13个网格点)
// 运行时通过双线性插值计算每个像素的增益

float gain = interpolate_gain(gain_grid, pixel_x, pixel_y);
pixel_out = pixel_raw * gain;

实际项目中,我建议把LSC分成两步:

  1. 亮度阴影校正:只补偿亮度衰减,用单通道增益。
  2. 色彩阴影校正:分别补偿R/G/B通道的衰减差异。
参数 亮度阴影 色彩阴影
校正对象 整体亮度 R/G/B通道比例
网格密度 9x7 或 17x13 17x13 或 33x25
插值方法 双线性 双线性或双三次
标定条件 D65光源,灰卡 多色温光源

个人经验:标定LSC时,千万别只用D65光源。我习惯至少标定三个色温(D65、A光源、TL84),然后根据当前场景的色温做线性插值。这样色彩阴影的校正精度会高很多。

3.2.3 实现中的坑

LSC看起来简单,但有几个细节不注意,效果会很难看:

  • 增益上限:四角的增益可能达到2倍甚至更高,但增益太大会放大噪声。我一般把增益上限限制在3.0以内,剩下的交给后面的降噪模块。
  • 网格对齐:增益网格必须和Bayer格式对齐。如果网格点落在奇数行,插值时要注意通道匹配。
  • 边界处理:图像边缘的像素可能落在网格范围外,需要做外推或镜像处理。

3.3 坏点校正(Defect Pixel Correction, DPC)

3.3.1 坏点的三种类型

Sensor生产过程中,总会有一些像素不完美。这些坏点如果不处理,会在图像上形成固定的亮点或暗点。

我根据项目经验,把坏点分成三类:

  • 亮点(Hot Pixel):一直输出高值,像一颗星星。
  • 暗点(Dead Pixel):一直输出低值,像一颗黑洞。
  • 闪烁点(Stuck Pixel):输出值固定不变,不随光照变化。

3.3.2 校正方法

坏点校正分为静态校正和动态校正。

静态校正:

在产线上标定坏点位置,生成一张坏点地图(Defect Map)。运行时直接查表替换。

// 静态坏点校正
if (defect_map[x][y] == 1) {
    pixel_out = median_filter(raw_image, x, y, 3x3);
} else {
    pixel_out = raw_image[x][y];
}

动态校正:

实时检测坏点。常用的方法是比较当前像素和周围8个邻域的统计值。

// 动态坏点检测(简化版)
int neighbors[8] = get_neighbors(raw_image, x, y);
int median = get_median(neighbors, 8);
int diff = abs(raw_image[x][y] - median);

if (diff > threshold) {
    pixel_out = median;  // 判定为坏点,用中值替换
} else {
    pixel_out = raw_image[x][y];
}

注意:动态校正的阈值很关键。阈值太小,会把正常细节误判为坏点;阈值太大,又会漏掉坏点。我一般根据ISO值动态调整阈值——ISO越高,阈值越大。

3.3.3 避坑指南

我曾经在一个项目中,只做了静态坏点校正,没做动态校正。结果Sensor用了半年后,新的坏点出现了,产线上标定的坏点地图完全失效。后来我改成“静态+动态”双保险方案:静态坏点地图做第一道防线,动态检测做第二道防线。这样既保证了速度,又覆盖了老化产生的新坏点。

3.4 本章知识体系

下面这张图展示了这三个模块在ISP Pipeline中的位置和关系:

传统ISP Pipeline 前三级 Sensor Raw 数据 黑电平校正 (BLC) 镜头阴影校正 (LSC) 坏点校正 (DPC) 校正后 Raw 数据 暗电流 + ADC偏置 动态/静态校正 亮度阴影 + 色彩阴影 网格标定 + 插值 静态坏点地图 动态中值检测

3.5 本章小结

这三个模块,说白了就是给Raw数据“洗澡”:

  • BLC:洗掉暗电流和偏置的脏东西。
  • LSC:把四角被镜头“吃掉”的光线补回来。
  • DPC:把Sensor上的“坏点痘痘”修掉。

做完这三步,Raw数据才算“干净”,才能交给后面的去噪、去马赛克、白平衡等模块。我见过不少团队,花大把精力调后面的算法,结果前三级没做好,图像质量始终上不去。嗯,基础不牢,地动山摇啊。

个人建议:在实际项目中,把这三级做成一个独立的预处理模块,用单独的硬件加速或DSP核来处理。这样既不影响后面算法的流水线,又能保证实时性。我之前的项目里,这三步加起来只占整个ISP Pipeline的5%左右的计算量,但带来的质量提升,值回票价。