3. 黑电平校正(BLC)——让黑色回归纯粹

黑电平校正,圈里人常叫 BLC(Black Level Correction)。

说白了,就是让传感器在完全无光的时候,输出的像素值应该是 0。但现实世界哪有那么完美?传感器本身有暗电流,读出电路有偏置,你盖上镜头盖拍一张,出来的 raw 图大概率不是全黑,而是带个底数——比如 64、128,甚至更高。

这个底数如果不干掉,后面做自动曝光、白平衡、色彩校正,全都会偏。我见过一个项目,BLC 没调好,暗部发紫,怎么调 AWB 都拉不回来。后来发现是黑电平没扣干净,暗电流补偿系数设错了。

3.1 BLC 原理与必要性

先看一张图,理解 BLC 在整个 ISP 流水线中的位置:

BLC 在 ISP 流水线中的位置 RAW 输入 BLC LSC AWB 后续处理 为什么 BLC 必须放在最前面? • 暗电流和读出偏置是传感器最底层的噪声,必须在任何增益/校正之前扣除。 • 如果先做增益再扣黑电平,暗部噪声会被放大,出现“黑电平漂移”。 • 我习惯在 BLC 之后立即做坏点校正,因为坏点像素的 BLC 值可能异常。

BLC 的必要性,我总结为三点:

  • 消除暗电流偏置——传感器像元在无光照时,由于热激发产生的电子累积,形成底数。
  • 统一黑电平参考——不同增益、不同温度下,暗电流不同。BLC 让黑色参考点归一化。
  • 防止暗部细节丢失——如果黑电平偏高,暗部被抬升,对比度下降,画面发灰。

关键指标:BLC 精度通常要求控制在 ±1 LSB 以内。对于 10-bit RAW,黑电平目标值设为 64,实际扣减后的残差应小于 1 个码值。

3.2 暗电流补偿算法

暗电流补偿,核心就是算一个“扣减值”。但这个值不是固定的,它随温度、增益、曝光时间变化。我见过有人直接写死一个常数,结果夏天和冬天拍出来的黑电平差 10 个码值。

常用的补偿方法有几种:

方法 原理 适用场景 精度
固定值扣除 从校准数据中取一个固定偏移量 低端 sensor,温度稳定 低
OB 行/列统计 利用 sensor 的光学黑区(OB)像素均值 中高端 sensor,实时性好 中
温度查表补偿 预标定不同温度下的暗电流曲线 工业相机,宽温范围 高
自适应滤波 对 OB 像素做低通滤波,动态更新 高动态场景,抗噪性强 高

我个人最常用的是 OB 行统计 + 温度补偿 的组合方案。为什么?因为 OB 行能实时反映当前帧的暗电流状态,而温度补偿能处理 OB 行本身被噪声污染的情况。

算法流程大致如下:

  1. 提取 sensor 输出的 OB 行(通常是前几行或后几行,被金属遮挡的像素)。
  2. 计算 OB 行像素的均值或中位数(中位数抗噪性更好)。
  3. 减去目标黑电平(比如 64),得到当前帧的补偿值 Δ。
  4. 对 Δ 做时间域低通滤波,防止单帧噪声导致闪烁。
  5. 用滤波后的 Δ 对有效像素做减法。

小技巧:OB 行统计时,记得剔除最边缘的 4-8 个像素。这些像素受读出电路边缘效应影响,值往往偏大或偏小。我曾经踩过这个坑,OB 均值偏了 2 个码值,导致整帧画面偏暗。

3.3 FPGA 实现 BLC 的流水线设计

FPGA 上做 BLC,核心是流水线。因为 raw 数据是逐像素流进来的,你不能等一帧收完再算 OB 均值——那样延迟太大了。我们要做的是 帧级统计 + 像素级处理 分离。

看这个流水线结构:

FPGA BLC 流水线设计 阶段1:OB 行提取 行计数器 + 地址判断 阶段2:OB 统计 累加器 + 中位数滤波 阶段3:补偿计算 减法器 + 饱和钳位 阶段4:像素输出 数据对齐 + 时序同步 流水线关键设计要点: 1. OB 行统计与有效像素处理并行——OB 行到来时只做统计,不输出;有效像素到来时用上一帧的补偿值。 2. 补偿值更新时机:每帧的 OB 行结束后,更新寄存器。不要在帧中间更新,否则画面会出现水平分界线。 3. 减法后的饱和处理:如果像素值小于补偿值,直接钳位到 0,不要出现负数。 4. 流水线深度:通常 3-5 级,延迟控制在 10 个时钟周期以内。

Verilog 代码片段(核心减法与钳位逻辑):

// BLC 核心模块
module blc_correction (
    input  wire        clk,
    input  wire        rst_n,
    input  wire [9:0]  raw_data,      // 10-bit RAW 输入
    input  wire        data_valid,
    input  wire [7:0]  blc_offset,    // 来自 OB 统计模块的补偿值
    output reg  [9:0]  corrected_data,
    output reg         data_ready
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            corrected_data <= 10'd0;
            data_ready     <= 1'b0;
        end else if (data_valid) begin
            // 减法 + 饱和钳位
            if (raw_data >= blc_offset)
                corrected_data <= raw_data - blc_offset;
            else
                corrected_data <= 10'd0;  // 钳位到 0
            data_ready <= 1'b1;
        end else begin
            data_ready <= 1'b0;
        end
    end

endmodule

注意:上面的代码是简化版。实际项目中,blc_offset 需要做帧同步——即当前帧的补偿值来自上一帧的 OB 统计。如果 sensor 场景变化剧烈(比如从暗室突然到强光),OB 行统计值可能跳变,建议加一个 IIR 低通滤波器:blc_filt = blc_filt * 0.875 + blc_new * 0.125。

3.4 调试案例:暗部偏紫问题

讲一个我实际遇到的案例。

某款 2MP 传感器,在低照度下(< 10 lux),暗部区域明显偏紫。一开始团队怀疑是 AWB 没调好,折腾了两周无果。后来我让测试组抓了 raw 图,用工具分析暗部 R、G、B 通道的均值。

结果发现:

通道 暗部均值(10-bit) 目标黑电平 偏差
R 72 64 +8
Gr 66 64 +2
Gb 65 64 +1
B 68 64 +4

R 通道的黑电平比其他通道高了 4-7 个码值。这意味着暗部 R 通道没有被充分扣除,导致偏紫。

根因是什么?

  • 该 sensor 的 R 通道暗电流温度系数与其他通道不同。
  • OB 行统计用的是全通道均值,没有分通道处理。
  • 温度升高后,R 通道暗电流增长更快,但补偿值没跟上。

解决方案:

  1. 将 BLC 改为 分通道补偿——R、Gr、Gb、B 各自统计 OB 值。
  2. 增加温度传感器,查表修正各通道的补偿系数。
  3. 在 FPGA 中为每个通道独立设置减法器。

改完后,暗部偏紫问题彻底解决。R 通道黑电平偏差从 +8 降到 +1 以内。

经验总结:BLC 调试不要只看整体均值。分通道看,分区域看。我习惯在调试阶段,让 FPGA 输出每个通道的 OB 统计值到 UART,用串口助手实时观察。这样能快速定位是 sensor 问题还是算法问题。

嗯,BLC 这部分就讲到这里。记住一句话:黑色都搞不定,后面全是白搭。