软件校正算法:全局BLC、局部BLC、基于统计的BLC、自适应BLC
好,我们接着聊黑电平校正的软件实现。说实话,硬件搞定了,软件才是真正见功夫的地方。你想想看,同一个传感器,不同人写的BLC算法,出来的画质能差一大截。我这些年踩过的坑,多半都在这块。
软件校正,说白了就是「算」出来的。我们手里拿到的Raw图,每个像素都带着一个暗电流偏置。我们要做的,就是把这个偏置减掉。但问题来了——怎么减?减多少?
嗯,这里我把常见的四种软件BLC算法拆开讲。每种都有它的脾气,选对了事半功倍。
全局BLC算法
这是最朴素的做法。我刚开始做ISP时,第一个版本用的就是它。
核心思想:整张图用一个固定的黑电平值去减。这个值怎么来?通常是取OB区域(光学黑区)的平均值。
数学表达:
I_corrected(x, y) = I_raw(x, y) - BLC_global
其中 BLC_global = mean(OB_region)
代码实现也很直接:
// 全局BLC示例(C风格伪代码)
uint16_t blc_global = 0;
uint32_t sum = 0;
int count = 0;
// 1. 统计OB区域均值
for (int y = OB_START_Y; y < OB_END_Y; y++) {
for (int x = OB_START_X; x < OB_END_X; x++) {
sum += raw_image[y][x];
count++;
}
}
blc_global = sum / count;
// 2. 整图减去该值
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
int corrected = raw_image[y][x] - blc_global;
output[y][x] = (corrected < 0) ? 0 : corrected;
}
}
我的经验:全局BLC适合传感器一致性好的场景。我在一个工业相机项目里用过,传感器温度稳定,OB区也干净,效果还不错。但一旦温度变化大,或者传感器有行噪声,全局BLC就露怯了。
注意:减法后可能出现负值,一定要做截断处理。我见过有人忘了这步,结果暗部出现诡异的条纹——嗯,那是负数被当作大正数读出来了。
局部BLC算法
全局BLC搞不定怎么办?那就分区域处理。局部BLC,说白了就是把图像切成若干块,每块单独算一个黑电平。
为什么需要局部? 因为传感器不同区域的暗电流可能不一样。尤其是大靶面传感器,边缘和中心的温度都有差异,暗电流自然不同。
实现思路:
- 将图像划分为 M×N 个网格
- 每个网格内,取OB区域或暗像素计算局部BLC值
- 对每个像素,用其所在网格的BLC值进行校正
- 网格之间做双线性插值,避免出现块状效应
// 局部BLC插值示例
// 假设已经计算出每个网格的BLC值,存在blc_grid[grid_h][grid_w]中
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
// 计算当前像素在网格中的位置
float grid_x = (float)x / WIDTH * (grid_w - 1);
float grid_y = (float)y / HEIGHT * (grid_h - 1);
// 双线性插值得到该点的BLC值
float blc_local = bilinear_interp(blc_grid, grid_x, grid_y);
int corrected = raw_image[y][x] - (int)blc_local;
output[y][x] = (corrected < 0) ? 0 : corrected;
}
}
避坑指南:我曾经在一个项目中把网格切得太细(64×64),结果每个网格内像素太少,BLC值受噪声影响很大,校正后反而出现了网格状噪声。后来我改成16×16,再配合插值,效果就好多了。
基于统计的BLC算法
这个思路有点意思。它不依赖OB区域,而是从图像本身「猜」出黑电平。
原理:假设图像中总有一些真正的暗像素(比如镜头盖遮挡区域、或者场景中的纯黑物体)。这些像素的亮度应该接近0,但实际读出来有个偏置。我们通过统计直方图,找到这个偏置。
常用方法:
- 直方图最小值法:统计整图直方图,取最低非零bin对应的灰度值作为BLC
- 百分比截断法:取直方图前0.1%像素的平均值
- 暗通道先验法:借鉴去雾算法中的暗通道概念,取局部最小值
// 直方图最小值法示例
uint32_t histogram[4096] = {0}; // 假设12bit Raw图
// 1. 统计直方图
for (int y = 0; y < HEIGHT; y++) {
for (int x = 0; x < WIDTH; x++) {
histogram[raw_image[y][x]]++;
}
}
// 2. 找到第一个非零bin
uint16_t blc_stat = 0;
for (int i = 0; i < 4096; i++) {
if (histogram[i] > 0) {
blc_stat = i;
break;
}
}
// 3. 用这个值做全局校正
// ... 同全局BLC
我的建议:基于统计的方法在暗光场景下特别好用。因为暗光下OB区域的信噪比很低,直接用OB均值反而不准。但要注意——如果场景本身就很亮,没有真正的暗像素,这个方法会误判。我一般把它作为全局BLC的补充,两者取加权平均。
自适应BLC算法
这是目前高端ISP里用的方案。说白了,就是让算法自己判断该用哪种策略,或者动态调整参数。
自适应体现在哪里?
- 根据增益自适应:模拟增益越大,暗电流噪声越明显,BLC需要更精细
- 根据温度自适应:温度升高,暗电流呈指数增长,BLC值要跟着调
- 根据场景自适应:暗场景用统计法,亮场景用OB法
一个简单的自适应策略:
// 自适应BLC伪代码
float temperature_coeff = get_sensor_temperature(); // 从传感器读取温度
float gain = get_analog_gain(); // 当前模拟增益
// 根据温度和增益调整BLC权重
float weight_ob = 0.5;
float weight_stat = 0.5;
if (temperature_coeff > 50.0) { // 高温
weight_ob = 0.8; // 更依赖OB区域
weight_stat = 0.2;
}
if (gain > 10.0) { // 高增益
weight_ob = 0.3; // OB区域噪声大,更多依赖统计
weight_stat = 0.7;
}
// 融合两种BLC结果
uint16_t blc_final = (uint16_t)(blc_ob * weight_ob + blc_stat * weight_stat);
核心要点:自适应BLC不是一种固定的算法,而是一种策略框架。你可以在里面塞各种子算法,让它们根据条件自动切换。我做过一个版本,甚至根据图像的行号做渐变——因为卷帘快门下,不同行的曝光时间有微小差异,暗电流也不一样。
四种算法对比
| 算法 | 复杂度 | 精度 | 适用场景 | 典型缺陷 |
|---|---|---|---|---|
| 全局BLC | 低 | 低 | 传感器一致性好的场景 | 无法处理局部差异 |
| 局部BLC | 中 | 中 | 大靶面、温度梯度明显的场景 | 网格参数需要调优 |
| 基于统计的BLC | 中 | 中高 | 暗光、无OB区域的场景 | 亮场景可能误判 |
| 自适应BLC | 高 | 高 | 多场景、多增益、宽温范围 | 实现复杂,需要标定 |
嗯,总结一下。软件BLC没有银弹。我个人的习惯是:先上全局BLC保底,再根据实际效果逐步升级。别一上来就搞自适应,调试起来能让你怀疑人生。先把基础打牢,再谈优化。
下一章我们聊聊BLC的标定流程——这个才是真正决定校正效果的关键步骤。