3、RAW预处理:黑电平校正、坏点校正、镜头阴影校正(LSC)

好,咱们进入RAW域处理的第一道关卡。说白了,传感器拿到的原始数据是不能直接用的。为什么?因为硬件有物理偏差,有制造缺陷,还有光学系统的天然瑕疵。这三步预处理,就是把这些“脏东西”先清理干净。

我个人习惯把这三大校正称为“RAW的三件套”。顺序也很讲究:先黑电平,再坏点,最后LSC。调换顺序?嗯,我试过,效果会打折扣,后面细说。

3.1 黑电平校正(Black Level Correction)

先问个问题:传感器在完全无光的情况下,输出应该是0吗?

答案是否定的。因为传感器电路本身有暗电流,像素在无光照时也会积累少量电荷。这个底噪值,就是黑电平。如果不减掉,整张画面会偏灰,暗部细节全被埋没。

3.1.1 原理与实现

黑电平校正的公式很简单:

I_corrected = I_raw - black_level

但black_level怎么取?不同传感器策略不同。常见两种:

方式 说明 我遇到的坑
固定值 传感器厂商给一个固定偏移量,比如64 温度一变,固定值就不准了
OB像素 传感器边缘有被遮光的像素,实时读取均值 OB行偶尔有坏点,得先剔除再平均
我的习惯:优先用OB像素方式。如果必须用固定值,建议在产线标定环节做温度补偿查表。

3.1.2 代码示例

def black_level_correction(raw_img, ob_pixels=None, fixed_bl=None):
    if ob_pixels is not None:
        # 剔除OB行中的异常值(比如上下5%分位)
        ob_clean = np.percentile(ob_pixels, [5, 95])
        bl = np.mean(ob_clean)
    else:
        bl = fixed_bl
    
    corrected = np.maximum(raw_img.astype(np.int16) - bl, 0)
    return corrected.astype(np.uint16)
注意:减法后一定要做clamp到0,否则负值在后续处理中会引发灾难。我曾经见过一个项目,忘了这步,结果坏点校正直接把负值当亮点处理了……

3.2 坏点校正(Defect Pixel Correction)

传感器制造不可能完美。总有一些像素对光不敏感(死点),或者一直亮着(亮点),或者响应异常。这些坏点如果不处理,会在最终图像上形成刺眼的“星星”或“黑洞”。

3.2.1 坏点检测策略

我把它分成两类:

  • 静态坏点:出厂就有的,通过标定生成坏点map
  • 动态坏点:使用中出现的,靠算法实时检测

实际项目中,我建议两者结合。静态map保底,动态算法兜底。

3.2.2 动态坏点检测算法

最常用的方法是“邻域比较法”。看当前像素和周围8个邻居的差异:

def detect_defect_pixel(img, x, y, threshold=40):
    center = img[x, y]
    neighbors = get_8_neighbors(img, x, y)
    median_val = np.median(neighbors)
    
    if abs(int(center) - int(median_val)) > threshold:
        return True
    return False

为什么用中位数而不是均值?因为均值容易被其他坏点带偏。中位数更鲁棒。

关键参数:threshold的选取。太严会误杀正常像素,太松会漏掉坏点。我一般先做一张暗场图(盖上镜头盖拍),统计正常像素的噪声标准差,threshold取3~5倍sigma。

3.2.3 坏点校正方法

检测到坏点后,怎么修?

方法 原理 适用场景
中值替换 用邻域中位数替代 孤立坏点,效果最好
均值替换 用邻域均值替代 坏点簇,但会模糊细节
插值法 沿边缘方向插值 坏点在纹理区域
避坑指南:我曾经在Bayer域直接做坏点校正,没考虑颜色通道。结果绿色通道的坏点用红蓝邻居去修,颜色全乱了。记住:坏点校正必须在同色通道内进行。

3.3 镜头阴影校正(LSC)

这是光学系统的“原罪”。镜头中心光线直射传感器,边缘光线斜射,导致边缘亮度下降。再加上镜头本身的透光率不均匀,就形成了“暗角”。

3.3.1 校正原理

说白了就是给每个像素乘一个增益系数。中心区域增益接近1,边缘增益大于1。

I_corrected(x, y) = I_raw(x, y) * gain(x, y)

gain(x, y)怎么来?两种主流方式:

  • 多项式拟合:用二维多项式拟合阴影曲面
  • 查表法:预先标定每个像素的增益,存成LUT

3.3.2 标定流程

我自己在项目中用的标定步骤:

  1. 拍摄一张均匀白板(DNP或X-Rite白卡)
  2. 取图像中心区域均值作为参考亮度
  3. 计算每个像素的增益 = 参考亮度 / 该像素亮度
  4. 对增益图做平滑(防止噪声放大)
  5. 存储为LUT或拟合系数
重要:标定用的白板必须均匀且中性。我见过有人用A4纸,结果纸本身纹理和偏色全被当成镜头阴影校正进去了,出图反而更糟。

3.3.3 代码实现

def apply_lsc(raw_img, gain_lut):
    # gain_lut 与 raw_img 同尺寸
    corrected = raw_img * gain_lut
    # 注意:增益后可能溢出,需要做饱和处理
    corrected = np.clip(corrected, 0, 2**12 - 1)  # 假设12bit RAW
    return corrected.astype(np.uint16)

3.3.4 分通道处理

不同波长的光折射率不同,所以R、G、B通道的阴影曲线不一样。必须分通道标定和校正。

通道 阴影特点 校正策略
R 边缘衰减最严重 增益系数最大
G 居中 标准增益
B 衰减相对较小 增益系数最小
性能优化:如果平台算力有限,可以降采样计算增益,然后双线性插值回原分辨率。我做过测试,8x8降采样对画质影响几乎不可见,但速度能快10倍。

3.4 三大校正的流程关系

最后,我用一张图把这三步串起来。顺序不能乱:黑电平校正必须在最前面,因为它影响所有像素的基准。坏点校正紧随其后,因为坏点如果不修,LSC会把坏点增益放大。LSC放在最后,因为它依赖前两步的干净数据。

RAW预处理三大校正流程 RAW原始数据 步骤1:黑电平校正 减去暗电流偏移,恢复真实黑位 步骤2:坏点校正 检测并修复死点/亮点/异常点 步骤3:镜头阴影校正 分通道增益补偿,消除暗角 干净的RAW数据 ⚠ 顺序不可调换 黑电平→坏点→LSC 调换会导致误差放大 或校正失效

嗯,这三步做完,RAW数据才算“干净”了。后面才能放心地做去马赛克、白平衡、色调映射。每一步都有坑,但踩过一次就记住了。希望你们能一次跳过这些坑。