3、Bayer域处理:黑电平校正、镜头阴影校正、坏点校正的硬件实现
Bayer域是ISP Pipeline的起点。说白了,传感器刚把光信号转成电信号,数据还是最原始的状态。这时候有三个问题必须马上处理:黑电平不准、镜头带来的亮度衰减、以及传感器本身的坏点。这三个模块我习惯放在一起讲,因为它们在硬件上经常共用一些计算资源。
3.1 黑电平校正(BLC)
黑电平校正,名字听着挺唬人,其实道理很简单。传感器在完全无光的情况下,理论上应该输出0。但实际不是这样——由于暗电流、电路噪声等因素,它会输出一个非零的基底值。这个基底值就是黑电平。
校正公式就一行:
P_out = max(0, P_in - BLC_offset)
硬件实现上,我一般用减法器加饱和钳位电路。注意这里有个坑——BLC_offset不是全局统一的。不同颜色通道的暗电流特性不一样,所以R、Gr、Gb、B四个通道要分别配置。
关键参数:BLC_offset通常为64~256(12bit RAW),具体值取决于传感器型号和环境温度。温度每升高10°C,暗电流大约翻一倍。
我个人的经验:在项目中,我遇到过客户抱怨暗部偏色。排查下来,问题出在BLC_offset只用了全局值,没做通道分离。改成四通道独立配置后,问题立刻解决。
3.2 镜头阴影校正(LSC)
镜头阴影,也叫亮度衰减。你想想看,镜头中心到边缘的光路长度不一样,边缘收到的光自然少。再加上镜头本身的cos⁴定律,边缘亮度可能只有中心的60%~70%。
校正方法是用一个增益矩阵去乘每个像素。增益值预先标定好,存在查找表里。
P_out = P_in × gain_table[x][y]
硬件实现时,我推荐用双线性插值。为什么?因为全分辨率增益表太占存储了。比如一个200万像素的传感器,如果每个像素存一个16bit增益值,那就是4MB。这在ASIC里是很大的开销。
实际做法是:
- 存储一个稀疏的增益网格(比如32×32或64×64)
- 运行时用双线性插值算出每个像素的增益
- 乘法器做一次乘法和一次移位
我曾经踩过的坑:增益值不能太大。如果边缘增益超过4倍,噪声会被严重放大。我见过一个方案,为了追求亮度均匀,把边缘增益设到6倍,结果暗部噪声直接炸了。后来我们加了增益上限钳位,并配合去噪模块一起调。
LSC的另一个难点是温度漂移。镜头阴影曲线会随温度变化,尤其是塑料镜头。我建议在标定时至少采集三个温度点(-20°C、25°C、60°C),然后做线性插值。
3.3 坏点校正(DPC)
传感器生产过程中,难免会有几个像素不听话。坏点分两种:
| 类型 | 表现 | 成因 |
|---|---|---|
| 亮点 | 始终输出高值(接近满阱) | 像素短路、漏电 |
| 暗点 | 始终输出低值(接近0) | 像素断路、光敏层损坏 |
校正策略有两种:
- 静态校正:出厂时标定坏点坐标,存到OTP里。运行时直接替换。
- 动态校正:实时检测坏点,用周围像素插值替换。
我个人的建议是两者结合。静态校正处理已知的固定坏点,动态校正处理使用过程中新出现的坏点。
动态检测的经典算法是:
if |P_center - median(neighbors)| > threshold:
P_center = median(neighbors)
硬件实现时,需要开一个3×3或5×5的窗口。每个时钟周期读入一行数据,用Line Buffer缓存。我常用的窗口是5×5,因为3×3在斜线边缘容易误判。
注意:阈值不能一刀切。亮区和暗区的噪声水平不同,我习惯用自适应阈值——根据局部均值动态调整threshold。这样在暗部不会误把噪声当坏点,在亮部也不会漏检。
3.4 三模块的硬件融合
这三个模块在硬件上可以串行处理,也可以融合。我画一张图来说明典型的流水线结构:
这张图展示的是串行流水线。每个模块处理完数据后直接传给下一个。硬件上,每个模块之间只需要一个FIFO做数据缓冲。
我见过有些设计把BLC和LSC合并成一个模块,因为两者都是乘加运算。但我不建议这么做——分开调试更方便,而且BLC的减法器比乘法器省面积,硬凑在一起反而浪费。
3.5 硬件资源估算
以1080p@30fps为例,我列一个典型的资源消耗:
| 模块 | Line Buffer | 乘法器 | 加法器 | 查找表 |
|---|---|---|---|---|
| BLC | 0 | 0 | 1 | 4×16bit |
| LSC | 2行 | 2 | 3 | 32×32×16bit |
| DPC | 4行 | 0 | 8 | 1×16bit(阈值) |
注意:Line Buffer是FPGA里最贵的资源。DPC用5×5窗口需要4行缓存,每行1920像素×12bit,合计约92Kbit。如果芯片面积紧张,可以考虑用3×3窗口,但校正效果会差一些。
3.6 调试与验证
这三个模块调试时,我习惯按这个顺序来:
- 第一步:先关掉LSC和DPC,单独调BLC。拍一张全黑图,看输出是否接近0。
- 第二步:打开LSC,拍一张均匀光照的灰卡图。看四角亮度是否和中心一致。
- 第三步:打开DPC,拍一张平场图。看有没有残留的亮点或暗点。
我曾经在一个项目里,LSC和DPC一起开,结果怎么调都调不好。后来发现是LSC的增益放大了坏点的差异,导致DPC误判。解决办法是先做LSC,再做DPC,顺序不能乱。
嗯,Bayer域这三个模块,说起来都不复杂,但细节很多。硬件实现时,面积、功耗、精度三者要权衡。我一般先按最保守的方案做,然后根据时序和面积报告逐步优化。