2. RAW域预处理
各位同学,欢迎来到RAW域预处理这一章。说实话,这是整个ISP链路里最「脏」最「累」的活,但也是决定画质下限的关键。你想想看,传感器拿到的原始数据,就像一块刚挖出来的玉石——满是裂痕和杂质。我们预处理要做的,就是把这些瑕疵先处理掉,后面的算法才能好好干活。
我个人习惯把预处理分成四个模块:坏点校正(DPC)、黑电平校正(BLC)、镜头阴影校正(LSC)、绿色平衡(GB)。它们之间没有严格的先后顺序,但通常BLC要最先做,因为后面的算法都依赖正确的黑电平基准。
1. 黑电平校正(BLC)—— 先把基准找对
黑电平校正,说白了就是告诉芯片:「0」到底代表多少。传感器在完全无光的情况下,输出并不是0,而是一个偏置值,通常在几十到几百个DN(数字数)之间。这个偏置就是黑电平。
为什么要做BLC?因为后面的所有处理——坏点检测、去噪、白平衡——都假设黑电平是0。如果黑电平没校正好,暗部细节会偏色,甚至出现「黑不黑」的问题。
BLC的典型做法有两种:
- 全局BLC:整帧减去一个固定值。简单,但无法应对传感器不均匀性。
- 逐行/逐列BLC:每行或每列单独统计暗像素均值。效果好,但需要OB(光学黑)区域支持。
代码实现其实不复杂:
// 全局BLC示例
void black_level_correction(uint16_t *raw, int width, int height, int blc) {
for (int i = 0; i < width * height; i++) {
if (raw[i] > blc)
raw[i] -= blc;
else
raw[i] = 0; // 防止下溢
}
}
2. 坏点校正(DPC)—— 把「坏像素」揪出来
传感器生产过程中,难免会有一些像素点响应异常。有的是死点(一直亮或一直暗),有的是热点(噪声特别大)。这些坏点如果不处理,会在图像上形成固定的亮点或暗点,非常影响观感。
坏点校正分两步:检测和替换。
| 坏点类型 | 表现 | 检测方法 | 替换策略 |
|---|---|---|---|
| 死点(暗) | 输出始终接近0 | 与邻域均值差异过大 | 中值滤波或邻域插值 |
| 死点(亮) | 输出始终接近饱和值 | 与邻域均值差异过大 | 中值滤波或邻域插值 |
| 热点 | 噪声方差明显偏高 | 多帧统计方差 | 中值滤波或双边滤波 |
检测算法我常用的是「邻域比较法」。以当前像素为中心,取一个3x3或5x5的窗口,计算邻域像素的均值或中值。如果当前像素值与邻域中值的差超过一个阈值,就判定为坏点。
替换策略上,我个人偏好中值滤波。为什么?因为均值滤波会把坏点的「坏」扩散到邻域,而中值滤波能更好地保留边缘。当然,如果坏点恰好位于边缘上,中值也可能出错——这时候可以用方向性插值,沿着边缘方向做替换。
// 简单的坏点检测与替换(3x3窗口)
void dpc_process(uint16_t *raw, int width, int height, int threshold) {
for (int y = 1; y < height - 1; y++) {
for (int x = 1; x < width - 1; x++) {
int idx = y * width + x;
// 取3x3邻域中值
uint16_t neighbors[9];
int n = 0;
for (int dy = -1; dy <= 1; dy++)
for (int dx = -1; dx <= 1; dx++)
neighbors[n++] = raw[(y+dy)*width + (x+dx)];
// 排序取中值(这里省略排序实现)
uint16_t median = quick_select(neighbors, 9, 4);
// 判断是否为坏点
if (abs(raw[idx] - median) > threshold) {
raw[idx] = median; // 替换为中值
}
}
}
}
3. 镜头阴影校正(LSC)—— 把「暗角」补回来
镜头阴影,也叫暗角或亮度衰减。任何镜头都有这个问题——中心亮、四周暗。原因很简单:镜头的光学路径长度不同,边缘光线到达传感器的角度更大,能量衰减更明显。
LSC的原理就是给每个像素乘上一个增益系数,让四周的亮度补到和中心一致。这个增益系数通常通过标定得到:拍一张均匀白板,然后计算每个像素需要乘多少才能达到中心亮度。
增益系数的存储是个问题。如果每个像素存一个系数,一张1200万像素的图就需要1200万个系数,太占内存。实际工程中,我们通常用网格方式存储:
- 在图像上均匀取NxN个网格点(比如17x17或33x33)
- 每个网格点存一个增益值
- 其他像素的增益通过双线性插值得到
| 网格密度 | 内存占用 | 精度 | 适用场景 |
|---|---|---|---|
| 9x9 | 81个系数 | 较低 | 低端摄像头、快速原型 |
| 17x17 | 289个系数 | 中等 | 大多数手机摄像头 |
| 33x33 | 1089个系数 | 较高 | 高端相机、广角镜头 |
另外,LSC还要考虑色差。不同波长的光衰减程度不同,所以R、G、B通道的增益系数应该分开标定。否则会出现「中心白、四周偏色」的问题——这个我踩过坑,教训深刻。
4. 绿色平衡(GB)—— 让G通道「自洽」
绿色平衡是Bayer RAW特有的一个步骤。Bayer格式中,绿色像素的数量是红色和蓝色的两倍,而且它们分布在不同的行上(RGr和Gb)。理论上,Gr和Gb的响应应该一致,但实际上由于制造工艺的微小差异,Gr和Gb之间会有偏差。
这种偏差如果不校正,会在图像上产生「棋盘格」纹理,尤其是在均匀区域。你想想看,本来应该是纯色的天空,结果出现了周期性的明暗变化,多难受。
绿色平衡的做法很简单:统计Gr和Gb的均值,然后调整其中一个通道,使两者均值相等。
// 绿色平衡示例(全局统计法)
void green_balance(uint16_t *raw, int width, int height) {
long sum_gr = 0, sum_gb = 0;
int cnt_gr = 0, cnt_gb = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if ((y % 2 == 0) && (x % 2 == 1)) { // Gr位置
sum_gr += raw[y*width + x];
cnt_gr++;
} else if ((y % 2 == 1) && (x % 2 == 0)) { // Gb位置
sum_gb += raw[y*width + x];
cnt_gb++;
}
}
}
float mean_gr = (float)sum_gr / cnt_gr;
float mean_gb = (float)sum_gb / cnt_gb;
float gain = mean_gr / mean_gb; // 以Gr为基准
// 调整Gb通道
for (int y = 1; y < height; y += 2) {
for (int x = 0; x < width; x += 2) {
int idx = y * width + x;
raw[idx] = (uint16_t)(raw[idx] * gain + 0.5f);
}
}
}
还有一种更精细的做法——局部绿色平衡。把图像分成多个小块,每个块独立计算Gr/Gb比例。这样能应对传感器非均匀性,但计算量会大一些。在移动平台上,我通常用全局法加一个简单的低通滤波,效果已经足够。
嗯,这一章的内容就到这里。这些预处理算法看起来简单,但实际工程中坑很多。温度漂移、传感器非均匀性、镜头一致性……每一个都够你调上几天。希望我的经验能帮你少走一些弯路。