4、图像预处理:灰度化、直方图均衡化、高斯滤波

好,咱们进入实战前的第一道工序——图像预处理。

你想想看,相机拍出来的原始棋盘格照片,直接拿去提取角点,十有八九会出问题。光照不均匀、传感器噪声、色彩干扰……这些因素都会让角点检测算法"找不着北"。所以,在调用 cv2.findChessboardCorners 之前,我们必须先把图像"收拾干净"。

我个人习惯把预处理分成三个步骤:灰度化 → 直方图均衡化 → 高斯滤波。每一步都有它的道理,咱们一个一个说。

核心观点:预处理不是"可有可无"的步骤,而是决定标定成败的关键。一张处理得当的图像,能让角点提取的鲁棒性提升一个量级。

图像预处理流程 原始图像 灰度化 直方图均衡化 高斯滤波 预处理后的图像 → 角点提取 减少噪声干扰,增强对比度,提高角点检测的准确率

4.1 灰度化:去掉颜色,保留结构

棋盘格标定板本身就是黑白相间的图案。颜色信息对我们来说,其实是多余的。RGB 三个通道不仅增加了计算量,还会引入光照色温的干扰。

灰度化的本质,就是把每个像素从三维颜色空间映射到一维亮度空间。OpenCV 里最常用的方法是 cv2.cvtColor,用 COLOR_BGR2GRAY 标志。

import cv2

# 读取彩色图像
img_bgr = cv2.imread('chessboard.jpg')

# 灰度化
img_gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)

这里有个细节要注意:OpenCV 默认读取的图像是 BGR 顺序,不是 RGB。如果你用 cv2.imread 读图,千万别写成 COLOR_RGB2GRAY,否则颜色通道会乱掉。嗯,这个坑我踩过不止一次。

小技巧:如果你用的是彩色相机拍的棋盘格,灰度化后可以用 cv2.imshow 看一眼。如果棋盘格黑白边界清晰可见,说明灰度化没问题。如果边界模糊,可能是光照问题,需要调整拍摄环境。

4.2 直方图均衡化:让暗部亮起来

灰度化之后,图像可能还存在一个问题:对比度不足。比如棋盘格放在阴影里,或者光照从一侧打过来,导致部分区域太暗、部分区域太亮。

直方图均衡化就是用来解决这个问题的。它通过重新分布像素的灰度值,让图像的直方图尽可能均匀地铺满整个灰度范围(0-255)。说白了,就是把暗的地方拉亮,亮的地方稍微压暗,让细节都显现出来。

# 直方图均衡化
img_eq = cv2.equalizeHist(img_gray)

就这么一行代码。但效果非常明显。

我记得有一次在工厂车间里做标定,现场灯光条件很差,棋盘格有一半都在阴影里。直接灰度化后提取角点,成功率不到 30%。加上直方图均衡化之后,成功率直接飙到 95% 以上。从那以后,直方图均衡化就成了我预处理流程里的"标配"。

注意:直方图均衡化只适用于单通道灰度图。如果你对彩色图直接调用 equalizeHist,OpenCV 会报错。另外,如果图像本身对比度已经很好,均衡化反而可能引入噪声或过度增强。建议先试跑一次,看看效果再决定是否加入流程。

4.3 高斯滤波:温柔地去除噪声

图像里总有一些"不速之客"——传感器噪声、灰尘颗粒、反光斑点。这些噪声在角点检测时会被误认为是棋盘格的角点,导致检测结果出现大量假阳性。

高斯滤波是一种线性平滑滤波器。它用高斯核与图像做卷积,每个像素的值被替换为邻域内像素的加权平均。权重由高斯分布决定:离中心越近的像素权重越大,越远的权重越小。这种"温柔"的平滑方式,能在去除噪声的同时,较好地保留边缘信息。

# 高斯滤波,核大小 5x5,标准差 1.0
img_blur = cv2.GaussianBlur(img_eq, (5, 5), 1.0)

参数 (5, 5) 是高斯核的大小,必须是奇数。1.0 是标准差 σ。这两个参数怎么选?我一般遵循一个经验法则:

核大小 σ 值 适用场景
3×3 0.5 - 0.8 噪声较少,图像清晰
5×5 1.0 - 1.5 中等噪声,通用场景
7×7 1.5 - 2.0 噪声较多,图像较粗糙

核越大、σ 越大,平滑效果越强,但边缘也会越模糊。对于棋盘格标定,我建议从 (5, 5) 和 σ=1.0 开始试。如果角点检测结果中有很多"假角点",可以适当增大核尺寸;如果角点位置偏移明显,说明平滑过度了,需要减小核尺寸。

经验之谈:高斯滤波的核大小不要超过棋盘格单个方格尺寸的 1/3。比如你的棋盘格每个方格是 30×30 像素,那核大小最大用 9×9。再大就会把棋盘格的边缘也平滑掉,角点位置会偏移。

4.4 三步合一的完整代码

把上面三步串起来,就是咱们预处理的标准流程:

import cv2

def preprocess_image(image_path):
    # 1. 读取图像
    img = cv2.imread(image_path)
    if img is None:
        print("图像读取失败,请检查路径")
        return None

    # 2. 灰度化
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 3. 直方图均衡化
    eq = cv2.equalizeHist(gray)

    # 4. 高斯滤波
    blurred = cv2.GaussianBlur(eq, (5, 5), 1.0)

    return blurred

# 使用示例
processed_img = preprocess_image('chessboard.jpg')
if processed_img is not None:
    cv2.imwrite('processed_chessboard.jpg', processed_img)
    print("预处理完成,结果已保存")

这段代码可以直接复制到你的项目里用。我建议你把预处理封装成函数,这样后面标定多张图像时,只需要循环调用就行。

4.5 避坑指南

最后,分享几个我在项目中踩过的坑:

  • 顺序不能乱。一定是先灰度化,再直方图均衡化,最后高斯滤波。如果把滤波放在均衡化之前,噪声会被均衡化放大,效果反而更差。
  • 不要过度滤波。高斯滤波的目的是"去噪保边",不是"磨皮"。滤波太强,棋盘格的角点会变得圆润,亚像素精度会下降。
  • 直方图均衡化不是万能的。如果图像本身光照极不均匀(比如一半亮一半暗),建议改用 CLAHE(对比度受限自适应直方图均衡化)。这个后面章节会讲到。
  • 保存中间结果。调试阶段,建议把灰度化、均衡化、滤波后的图像都保存一份。这样出了问题,可以快速定位是哪一步导致的。

我的习惯:在预处理函数里加一个 debug=True 参数。开启后,会显示每一步的中间结果图像。调试完再关掉,不影响最终性能。

好了,图像预处理这块就讲到这里。记住这三个步骤,它们是后续角点提取的"地基"。地基打牢了,后面的工作才能稳。