第二章 RAW域处理:黑电平校正与线性化

各位同学,欢迎来到ISP流水线的第一站——RAW域处理。

很多人觉得ISP就是从RAW到RGB再到YUV的流水线,但说实话,RAW域才是决定画质下限的关键。你想想看,如果RAW数据本身就有问题,后面做再多美化也是白搭。

这一章我们聊两个基础但极其重要的模块:黑电平校正(BLC)和线性化处理。这两个模块在RTL实现上都不复杂,但背后的物理原理和工程细节,我当年可是踩过不少坑的。

2.1 为什么需要黑电平校正?

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

答案是否定的。由于传感器本身的暗电流、读出电路噪声、ADC偏移等因素,即使盖上镜头盖,传感器也会输出一个非零的数值。这个数值就是黑电平(Black Level)。

核心概念:黑电平校正的目的,就是从每个像素的原始值中减去这个固定的偏移量,让“纯黑”真正对应到0。

我在项目中遇到过一款CMOS传感器,它的黑电平在不同增益下会漂移。如果不做动态校正,暗部画面会发灰,而且随着ISO升高,这种发灰会越来越明显。

2.2 黑电平的两种表现形式

根据传感器的设计,黑电平通常有两种存在形式:

类型 特点 典型值 校正方式
固定黑电平 所有像素共享同一个偏移量 64(10bit RAW) 全局减法
OB像素黑电平 传感器边缘有遮光像素(Optical Black) 随温度/增益变化 实时统计OB像素均值

固定黑电平比较简单,传感器手册会给出一个推荐值,比如10bit RAW下黑电平是64。你直接减掉就行。

但OB像素方式更灵活。传感器会在像素阵列的四周留出一些被金属层遮挡的像素,这些像素不感光,只反映暗电流和噪声。我建议在实际项目中优先使用OB方式,因为它能自适应环境变化。

2.3 BLC的数学原理

校正公式很简单:

P_out = max(P_in - BLC_offset, 0)

其中:

  • P_in:传感器原始RAW值
  • BLC_offset:黑电平偏移量
  • P_out:校正后的像素值

注意那个max(..., 0)。为什么要做截断?因为如果某个像素的噪声导致其值低于黑电平,减去偏移后可能变成负数。在数字域里,负数是没有意义的,必须钳位到0。

我曾经踩过的坑:早期做FPGA验证时,我忘了加这个钳位逻辑。结果在暗部场景下,某些像素出现了下溢,导致图像上出现随机黑点。查了整整两天才找到原因——就是少了一个max操作。

2.4 RTL实现:流水线减法器

BLC的RTL实现非常直接。说白了就是一个减法器加一个比较器。

我给出一个典型的Verilog实现框架:

module blc_correction #(
    parameter DATA_WIDTH = 10,
    parameter BLC_OFFSET = 64
)(
    input  wire                clk,
    input  wire                rst_n,
    input  wire                valid_in,
    input  wire [DATA_WIDTH-1:0] raw_data,
    output reg                 valid_out,
    output reg  [DATA_WIDTH-1:0] blc_data
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            valid_out <= 1'b0;
            blc_data  <= 0;
        end else begin
            valid_out <= valid_in;
            if (valid_in) begin
                if (raw_data >= BLC_OFFSET)
                    blc_data <= raw_data - BLC_OFFSET;
                else
                    blc_data <= 0;
            end
        end
    end

endmodule

这个代码只有一级流水线。如果时序紧张,你可以把减法和比较拆成两级。我个人习惯在数据路径上保持1-2级流水线,这样既能满足时序,又不会引入过多延迟。

2.5 线性化处理:为什么需要它?

聊完BLC,我们来说线性化。

你可能会问:传感器输出的RAW数据难道不是线性的吗?

答案是:不一定。

很多CMOS传感器,尤其是手机用的BSI(背照式)传感器,在低照度下会引入非线性响应。这种非线性可能是由于:

  • 光电二极管的电荷容量限制
  • 读出电路的压缩特性
  • 传感器厂商故意做的“伪HDR”处理

如果不做线性化,后面的去马赛克、白平衡、颜色校正等模块都会出问题。因为这些模块都假设输入数据是线性的。

我的经验:有一次我在调试一款安防摄像头,发现画面在暗部区域有明显的色偏。查了一圈,最后发现是传感器在低照度下自动切换了非线性模式,而我的ISP流水线没有做对应的线性化查表。加上LUT之后,问题立刻解决。

2.6 线性化的两种实现方式

线性化通常有两种做法:

  1. 分段线性拟合:用多段直线逼近非线性曲线。适合资源受限的FPGA。
  2. 查找表(LUT):直接存储输入到输出的映射关系。精度高,但消耗BRAM。

我建议在FPGA验证阶段优先使用LUT方式。为什么?因为调试方便。你可以随时更新LUT的内容来适配不同的传感器。

LUT的RTL实现也很简单:

module linearization_lut #(
    parameter DATA_WIDTH = 12,
    parameter LUT_DEPTH  = 4096
)(
    input  wire                clk,
    input  wire                valid_in,
    input  wire [DATA_WIDTH-1:0] raw_in,
    output reg                 valid_out,
    output reg  [DATA_WIDTH-1:0] linear_out
);

    // LUT存储器
    reg [DATA_WIDTH-1:0] lut_mem [0:LUT_DEPTH-1];

    // 初始化LUT(实际项目中通过寄存器配置或ROM加载)
    // 这里假设已经初始化完成

    always @(posedge clk) begin
        valid_out <= valid_in;
        if (valid_in)
            linear_out <= lut_mem[raw_in];
    end

endmodule

注意,LUT的深度取决于输入数据的位宽。12bit RAW就需要4096个entry。每个entry的位宽取决于你需要的输出精度。

2.7 BLC与线性化的流水线整合

在实际ISP流水线中,BLC和线性化通常是紧耦合的。我习惯把这两个模块放在同一个流水线阶段:

// 流水线阶段1:BLC校正
stage1_data = max(raw_in - blc_offset, 0);

// 流水线阶段2:线性化查表
stage2_data = lut_mem[stage1_data];

这样做的好处是延迟小,而且两个模块可以共享同一个valid信号链。

下面我画了一张流程图,展示BLC和线性化在RAW域流水线中的位置关系:

RAW域预处理流水线(BLC + 线性化) 传感器RAW数据 黑电平校正 线性化处理 校正后RAW 关键说明: 1. BLC校正必须在线性化之前完成,否则黑电平偏移会影响LUT的查表精度。 2. 两个模块共享同一套valid/ready握手信号,保持流水线同步。 3. 数据位宽在BLC后可能不变,但线性化后建议保留足够精度(如12bit→14bit)。

2.8 工程实践中的注意事项

最后,我总结几个在实际项目中容易忽略的点:

  • BLC偏移量的动态更新:如果使用OB像素方式,你需要设计一个统计模块,实时计算OB像素的均值。这个均值不能直接用,建议加一个低通滤波器,防止单帧噪声导致偏移量抖动。
  • 线性化LUT的初始化:FPGA上电后,LUT内容是随机的。你必须确保在ISP流水线启动之前,LUT已经被正确加载。我习惯用AXI-Lite接口来配置LUT,这样CPU可以在初始化阶段写入系数。
  • 边界情况处理:当BLC_offset大于输入像素值时,输出必须钳位到0。这个逻辑虽然简单,但很容易在代码review时被忽略。

一句话总结:BLC去掉暗电流偏移,线性化恢复传感器真实响应。这两个模块做扎实了,后面的ISP处理才有意义。