3. 坏点校正算法基础:三种核心方法
坏点校正,说白了就是要把传感器上那些「不听话」的像素点给拉回来。我做了这么多年ISP,见过各种各样的坏点——有的死黑、有的死白、有的忽明忽暗。处理它们的方法很多,但最基础、最常用的就是这三种:邻域均值替换法、中值替换法、自适应插值法。
嗯,咱们一个一个来看。
3.1 邻域均值替换法
这个方法最直观。你想想看,一个像素坏了,它周围总还有好像素吧?那就用周围的平均值把它替换掉。
核心思路: 检测到坏点后,取其邻域内所有正常像素的均值,赋值给坏点位置。
计算公式:
Pnew = (1/N) × Σ Pi,其中 Pi 为邻域内正常像素,N 为正常像素个数。
邻域选择: 常见的有 3×3、5×5 窗口。我个人习惯用 3×3,计算量小,效果也够用。
| 邻域大小 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 3×3 | 计算快,细节保留好 | 对孤立坏点敏感 | 坏点密度低 |
| 5×5 | 平滑效果好,抗噪强 | 边缘容易模糊 | 坏点密度高 |
// 邻域均值替换法 - C语言伪代码
void mean_correction(uint8_t *image, int width, int height,
int bad_x, int bad_y, int kernel_size) {
int sum = 0, count = 0;
int half = kernel_size / 2;
for (int dy = -half; dy <= half; dy++) {
for (int dx = -half; dx <= half; dx++) {
int x = bad_x + dx;
int y = bad_y + dy;
// 跳过坏点自身
if (dx == 0 && dy == 0) continue;
// 边界检查
if (x >= 0 && x < width && y >= 0 && y < height) {
sum += image[y * width + x];
count++;
}
}
}
image[bad_y * width + bad_x] = sum / count;
}
我的经验: 这个方法在平坦区域效果很好,但到了纹理丰富的地方,容易把细节抹掉。我在做手机摄像头调试时,遇到过一张星空照片,用均值法把星星都给抹没了……后来就改用中值法了。
3.2 中值替换法
中值法比均值法更「聪明」一点。它不取平均,而是取邻域内所有像素的中间值。这样做的好处是——对异常点不敏感。
为什么中值更好? 你想想,如果邻域里恰好有一个像素值特别高或特别低,均值会被它带偏。但中值不会,它只取排序后中间那个值。
核心步骤:
- 提取坏点邻域内的所有像素值
- 将这些值从小到大排序
- 取排序后的中间值作为新像素值
// 中值替换法 - C语言伪代码
uint8_t get_median(uint8_t *arr, int len) {
// 简单冒泡排序,实际工程用快速排序
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
uint8_t temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr[len / 2];
}
void median_correction(uint8_t *image, int width, int height,
int bad_x, int bad_y, int kernel_size) {
uint8_t buffer[25]; // 最大5x5
int count = 0;
int half = kernel_size / 2;
for (int dy = -half; dy <= half; dy++) {
for (int dx = -half; dx <= half; dx++) {
int x = bad_x + dx;
int y = bad_y + dy;
if (dx == 0 && dy == 0) continue;
if (x >= 0 && x < width && y >= 0 && y < height) {
buffer[count++] = image[y * width + x];
}
}
}
image[bad_y * width + bad_x] = get_median(buffer, count);
}
注意: 中值法计算量比均值法大,因为需要排序。在嵌入式平台上,如果坏点很多,可能会拖慢帧率。我曾经在一个低端芯片上吃过这个亏,后来改用快速选择算法才搞定。
3.3 自适应插值法
前面两种方法都是「一刀切」——不管坏点周围是什么情况,都用同一种方式处理。自适应插值法就不一样了,它会根据图像内容动态调整。
核心思想: 先判断坏点所在区域是平坦区、边缘区还是纹理区,然后选择不同的插值策略。
自适应策略:
- 平坦区域: 用均值法,平滑效果好
- 边缘区域: 沿边缘方向插值,保留边缘锐度
- 纹理区域: 用中值法或加权插值,保留细节
怎么判断区域类型?我一般用梯度计算。计算水平、垂直、两个对角线方向的梯度,梯度最小的方向就是边缘方向。
// 自适应插值法 - 方向判断伪代码
typedef enum {
FLAT_AREA,
HORIZONTAL_EDGE,
VERTICAL_EDGE,
DIAGONAL_EDGE,
TEXTURE_AREA
} RegionType;
RegionType detect_region(uint8_t *image, int x, int y, int width) {
// 计算四个方向的梯度
int grad_h = abs(image[y*width + x+1] - image[y*width + x-1]);
int grad_v = abs(image[(y+1)*width + x] - image[(y-1)*width + x]);
int grad_d1 = abs(image[(y+1)*width + x+1] - image[(y-1)*width + x-1]);
int grad_d2 = abs(image[(y+1)*width + x-1] - image[(y-1)*width + x+1]);
int min_grad = min(min(grad_h, grad_v), min(grad_d1, grad_d2));
int max_grad = max(max(grad_h, grad_v), max(grad_d1, grad_d2));
// 阈值判断,实际工程中阈值需要标定
if (max_grad < 10) return FLAT_AREA;
if (min_grad == grad_h && grad_h < 20) return HORIZONTAL_EDGE;
if (min_grad == grad_v && grad_v < 20) return VERTICAL_EDGE;
if (min_grad == grad_d1 || min_grad == grad_d2) return DIAGONAL_EDGE;
return TEXTURE_AREA;
}
避坑指南: 我曾经在一个项目中,自适应插值法的阈值设得太死板,结果在弱光环境下把很多正常像素误判为坏点。后来我改成动态阈值——根据整体图像亮度调整阈值,问题就解决了。
3.4 三种方法对比总结
| 方法 | 计算量 | 边缘保留 | 平坦区域效果 | 抗噪能力 | 实现难度 |
|---|---|---|---|---|---|
| 邻域均值法 | 低 | 差 | 好 | 弱 | 简单 |
| 中值替换法 | 中 | 中 | 好 | 强 | 中等 |
| 自适应插值法 | 高 | 好 | 好 | 强 | 复杂 |
我的建议: 如果项目周期紧、硬件资源有限,先用中值法。如果对图像质量要求高,再上自适应插值法。均值法嘛……我基本只在演示Demo里用。
重要提醒: 不管用哪种方法,坏点检测一定要准。检测错了,校正就是帮倒忙。我见过一个案例,把正常的暗像素当成坏点去校正,结果整张图出现了一堆亮斑……
嗯,三种方法都讲完了。说实话,没有哪一种方法是万能的。我做了这么多年,最后发现最好的策略是——根据场景动态切换。平坦区用均值,边缘区用方向插值,纹理区用中值。这才是工程上的最优解。