第二章 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 线性化的两种实现方式
线性化通常有两种做法:
- 分段线性拟合:用多段直线逼近非线性曲线。适合资源受限的FPGA。
- 查找表(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域流水线中的位置关系:
2.8 工程实践中的注意事项
最后,我总结几个在实际项目中容易忽略的点:
- BLC偏移量的动态更新:如果使用OB像素方式,你需要设计一个统计模块,实时计算OB像素的均值。这个均值不能直接用,建议加一个低通滤波器,防止单帧噪声导致偏移量抖动。
- 线性化LUT的初始化:FPGA上电后,LUT内容是随机的。你必须确保在ISP流水线启动之前,LUT已经被正确加载。我习惯用AXI-Lite接口来配置LUT,这样CPU可以在初始化阶段写入系数。
- 边界情况处理:当BLC_offset大于输入像素值时,输出必须钳位到0。这个逻辑虽然简单,但很容易在代码review时被忽略。
一句话总结:BLC去掉暗电流偏移,线性化恢复传感器真实响应。这两个模块做扎实了,后面的ISP处理才有意义。