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 中值替换法

中值法比均值法更「聪明」一点。它不取平均,而是取邻域内所有像素的中间值。这样做的好处是——对异常点不敏感。

为什么中值更好? 你想想,如果邻域里恰好有一个像素值特别高或特别低,均值会被它带偏。但中值不会,它只取排序后中间那个值。

核心步骤:

  1. 提取坏点邻域内的所有像素值
  2. 将这些值从小到大排序
  3. 取排序后的中间值作为新像素值
// 中值替换法 - 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里用。

重要提醒: 不管用哪种方法,坏点检测一定要准。检测错了,校正就是帮倒忙。我见过一个案例,把正常的暗像素当成坏点去校正,结果整张图出现了一堆亮斑……

坏点校正算法知识体系 坏点校正算法 邻域均值替换法 中值替换法 自适应插值法 3×3窗口 5×5窗口 加权平均 排序取中 快速选择 加权中值 梯度检测 方向插值 动态阈值 选择建议:资源受限→中值法 | 质量优先→自适应法 | 快速原型→均值法 实际工程中常将三种方法组合使用,根据场景动态切换

嗯,三种方法都讲完了。说实话,没有哪一种方法是万能的。我做了这么多年,最后发现最好的策略是——根据场景动态切换。平坦区用均值,边缘区用方向插值,纹理区用中值。这才是工程上的最优解。