第3章 RAW域预处理:黑电平校正、镜头阴影校正、坏点修复与去噪
好,咱们进入RAW域预处理。这是整个夜景管线的第一道关卡,也是最容易被忽视的环节。我见过不少团队,算法模型调得花里胡哨,结果RAW域预处理没做好,后面再怎么折腾也是白费力气。
说白了,RAW域预处理就干四件事:黑电平校正、镜头阴影校正、坏点修复、去噪。顺序不能乱,每一步都有它的道理。
核心观点:RAW域预处理的质量,直接决定了后续ISP管线能发挥的上限。夜景场景下,信号本来就弱,预处理阶段每多一分噪声,后面就要花十分力气去擦屁股。
3.1 黑电平校正(BLC)
黑电平是什么?传感器在没有光照的情况下,理论上输出应该是0。但实际不是——由于电路暗电流、ADC偏移等因素,传感器会有一个固定的底噪偏移。这个偏移值就是黑电平。
我遇到过最离谱的一次,某款传感器在长曝光下黑电平漂了将近20个DN(数字码值)。如果不校正,整个画面会偏灰,暗部细节全被淹没了。
校正公式很简单:
// 黑电平校正 - 基础实现
// ob_value: 光学黑区统计得到的黑电平值
// raw_data: 输入RAW像素值
// bit_depth: 传感器位深(如10bit、12bit)
uint16_t blc_correction(uint16_t raw_data, uint16_t ob_value, uint8_t bit_depth) {
int16_t corrected = (int16_t)raw_data - (int16_t)ob_value;
// 钳位到有效范围
if (corrected < 0) corrected = 0;
if (corrected > (1 << bit_depth) - 1) corrected = (1 << bit_depth) - 1;
return (uint16_t)corrected;
}
实战技巧:黑电平值不是一成不变的。温度升高、曝光时间变长,黑电平都会漂移。我建议在每帧的消隐区(OB区域)实时统计黑电平,而不是用固定值。夜景长曝光时尤其重要。
3.2 镜头阴影校正(LSC)
镜头阴影,也叫亮度渐晕。说白了就是镜头中心进光多、边缘进光少。你想想看,一个均匀的灰卡,拍出来中心亮、四角暗,这就是镜头阴影。
校正方法是用一个增益矩阵去补偿边缘像素。这个增益矩阵通常在产线标定时生成,存储在OTP(一次性可编程存储器)里。
| 区域 | 相对中心亮度 | 典型增益值 |
|---|---|---|
| 中心(0~20%半径) | 100% | 1.0x |
| 中间(20%~60%半径) | 80%~95% | 1.05x~1.25x |
| 边缘(60%~100%半径) | 50%~75% | 1.33x~2.0x |
注意,增益不能无限放大。边缘增益过大,会把传感器本身的噪声也放大。我见过有人把边缘增益拉到3.0x,结果画面边缘全是噪点,比不校正还难看。
避坑指南:我曾经在某个项目里直接用了标定好的LSC增益表,结果发现夜景模式下边缘偏色严重。后来排查发现,标定时用的是D65光源(色温6500K),而夜景路灯是暖色温(约3000K)。不同色温下镜头阴影特性不一样,最好做色温相关的LSC校正。
3.3 坏点修复(DPC)
传感器上总有那么几个像素是坏的——要么一直亮(白点),要么一直暗(黑点),要么随机闪烁(闪点)。夜景模式下曝光时间长,坏点更容易被看到。
坏点修复分两步:检测和替换。
检测方法有很多,我常用的是基于邻域统计的阈值法:
// 坏点检测 - 邻域比较法
// 以当前像素为中心,取3x3邻域
// 如果当前像素与邻域中值的差异超过阈值,判定为坏点
int is_dead_pixel(uint16_t center, uint16_t neighbors[8], uint16_t threshold) {
// 排序取中值
uint16_t sorted[8];
memcpy(sorted, neighbors, 8 * sizeof(uint16_t));
// 简单冒泡排序(实际用快速选择)
for (int i = 0; i < 7; i++) {
for (int j = 0; j < 7 - i; j++) {
if (sorted[j] > sorted[j+1]) {
uint16_t tmp = sorted[j];
sorted[j] = sorted[j+1];
sorted[j+1] = tmp;
}
}
}
uint16_t median = (sorted[3] + sorted[4]) / 2;
// 判断差异
int diff = abs((int)center - (int)median);
return (diff > threshold) ? 1 : 0;
}
替换策略我推荐用中值替换,而不是均值替换。为什么?因为均值会把邻域的细节模糊掉,而中值能更好地保留边缘。夜景下细节本来就少,经不起均值滤波的折腾。
经验之谈:坏点阈值怎么设?我一般取传感器噪声标准差的3~5倍。太严了会把正常像素误判为坏点,太松了又漏检。夜景下噪声大,阈值可以适当放宽,但别超过8倍标准差,否则坏点就藏不住了。
3.4 去噪(Denoise)
终于到了重头戏。夜景去噪,是所有ISP工程师的噩梦,也是展现功力的地方。
RAW域去噪和RGB域去噪最大的区别是什么?RAW域是Bayer格式,每个像素只有一种颜色分量。你不能直接套用RGB域的滤波算法,否则会引入颜色伪影。
我常用的RAW域去噪方法有几种:
- 双边滤波(Bilateral Filter):保边去噪效果好,但计算量大。适合高端平台。
- 非局部均值(NLM):去噪效果顶级,但速度慢到令人发指。我一般只在拍照模式用。
- 小波阈值去噪:在频域操作,对高斯噪声效果好。夜景下传感器噪声主要是高斯+泊松混合,小波方法挺合适。
- BM3D:目前公认的去噪天花板,但实时性差。移动端基本别想。
对于实时夜景,我推荐改进型双边滤波。为什么?因为它可以在FPGA或DSP上流水线实现,延迟可控。
// 改进型双边滤波 - RAW域实现
// 只对同色通道的像素进行滤波,避免颜色串扰
// sigma_s: 空间域标准差(控制模糊半径)
// sigma_r: 值域标准差(控制边缘保留程度)
void bilateral_filter_raw(uint16_t *raw, uint16_t *output,
int width, int height,
float sigma_s, float sigma_r) {
int radius = (int)(3 * sigma_s + 0.5);
float sigma_s_inv = 1.0f / (2 * sigma_s * sigma_s);
float sigma_r_inv = 1.0f / (2 * sigma_r * sigma_r);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// 只处理当前像素同色的Bayer位置
// 实际实现需要根据Bayer模式(RGGB/GRBG等)做通道分离
float sum_w = 0.0f;
float sum_v = 0.0f;
uint16_t center = raw[y * width + x];
for (int dy = -radius; dy <= radius; dy++) {
for (int dx = -radius; dx <= radius; dx++) {
int nx = x + dx;
int ny = y + dy;
if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue;
// 只对同色通道滤波
if (((x & 1) != (nx & 1)) || ((y & 1) != (ny & 1))) continue;
uint16_t neighbor = raw[ny * width + nx];
float dist_s = dx*dx + dy*dy;
float dist_r = (float)((int)center - (int)neighbor);
dist_r *= dist_r;
float w = expf(-dist_s * sigma_s_inv) * expf(-dist_r * sigma_r_inv);
sum_w += w;
sum_v += w * neighbor;
}
}
output[y * width + x] = (uint16_t)(sum_v / sum_w + 0.5f);
}
}
}
优化建议:上面的代码是教学版,实际工程中千万别这么写。expf()函数太慢了。我一般用LUT(查找表)或者多项式近似来加速。另外,半径可以限制在3~5像素,再大收益有限,计算量翻倍。
3.5 四步串联的工程考量
这四个步骤在硬件流水线上是串行的。但有个细节要注意:坏点修复应该在去噪之前做。为什么?因为坏点如果不先修掉,去噪的时候会把坏点的异常值扩散到邻域,造成更大面积的污染。
另外,黑电平校正和镜头阴影校正的顺序也有讲究。先做BLC再做LSC,因为LSC的增益是基于真实信号值的。如果黑电平没减掉,增益会把黑电平也放大,导致暗部偏色。
| 步骤 | 输入 | 输出 | 关键参数 |
|---|---|---|---|
| BLC | RAW原始值 | 减掉黑电平后的RAW | OB区域统计值 |
| LSC | BLC后的RAW | 亮度均匀的RAW | 增益矩阵(色温相关) |
| DPC | LSC后的RAW | 坏点修复后的RAW | 检测阈值(3~5σ) |
| Denoise | DPC后的RAW | 去噪后的RAW | sigma_s, sigma_r |
重要提醒:夜景模式下,曝光时间变长,传感器温度升高,黑电平会漂移。我建议每帧都重新统计OB区域的黑电平值,而不是用标定值。另外,长曝光下坏点会更明显,检测阈值可以适当降低,宁可误判几个好点,也别漏掉一个坏点。
嗯,RAW域预处理就这些。看起来简单,但每个环节都有坑。我当年第一次做夜景ISP,就是在LSC和DPC的顺序上栽了跟头——先做了DPC再LSC,结果边缘区域的坏点因为增益放大变得特别明显,调试了整整一周才发现问题。
记住一句话:RAW域预处理是ISP管线的地基,地基不稳,楼盖得再高也是危楼。