图像预处理:直方图均衡化、高斯滤波、中值滤波、Sobel边缘检测、Canny边缘检测

各位同学,欢迎来到立体匹配实战的第二课。

上一章我们聊了立体匹配的整体框架,说白了就是给左右两张图找对应点。但你想过没有——如果原始图像本身质量就很差,比如太暗、太亮、全是噪点,那匹配结果能好吗?

答案显然是否定的。所以今天这一章,我们专门来啃一啃图像预处理这块硬骨头。

核心观点:预处理做得好,匹配成功一半。这不是夸张,是我踩过无数坑后的血泪总结。

我会带大家逐一过一遍五个经典操作:直方图均衡化、高斯滤波、中值滤波、Sobel边缘检测、Canny边缘检测。每个我都会结合项目经验来讲,保证你听完就能用。

图像预处理 立体匹配前处理 直方图均衡化 增强对比度 高斯滤波 去除高斯噪声 中值滤波 去除椒盐噪声 Sobel边缘检测 一阶梯度 Canny边缘检测 多阶段优化 五大预处理操作 · 各有各的用武之地

1. 直方图均衡化——让暗部亮起来

先说说直方图均衡化。这玩意儿说白了就是把图像的灰度分布「拉平」。如果一张图整体偏暗,像素都挤在低灰度区域,均衡化之后就能把对比度撑开。

我记得有一次做室外场景的立体匹配,下午四点多拍的图,左边树荫底下黑乎乎一片。直接做匹配,那区域全是误匹配。后来我加了一步直方图均衡化,效果立竿见影。

我的经验:均衡化不是万能的。如果图像本身光照均匀,强行均衡反而会引入噪声。我一般先看一眼直方图分布再决定要不要做。

import cv2
import numpy as np

# 读取灰度图
img = cv2.imread('left.png', cv2.IMREAD_GRAYSCALE)

# 全局直方图均衡化
equ = cv2.equalizeHist(img)

# 或者用CLAHE(自适应均衡化,效果更好)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
clahe_img = clahe.apply(img)

cv2.imwrite('equ_left.png', equ)
cv2.imwrite('clahe_left.png', clahe_img)

看到没?CLAHE 比全局均衡化更聪明。它把图像分成小块分别处理,不会让天空过曝。我个人习惯在立体匹配前优先用 CLAHE。

2. 高斯滤波——温柔的降噪

高斯滤波,说白了就是用高斯核跟图像做卷积。每个像素的最终值由它周围像素加权平均得到,权重由高斯分布决定。

为什么要用高斯?因为自然界的噪声大多服从高斯分布。你用高斯滤波去处理高斯噪声,这叫「对症下药」。

注意:高斯滤波的核大小和标准差 σ 是关键参数。核越大,图像越模糊。我一般用 5×5 的核,σ 取 1.0 左右。太大就把边缘也抹掉了,立体匹配反而找不到特征。

# 高斯滤波
gaussian_blur = cv2.GaussianBlur(img, (5, 5), 1.0)

# 也可以指定 σx 和 σy 不同
gaussian_blur2 = cv2.GaussianBlur(img, (5, 5), sigmaX=1.5, sigmaY=1.5)

你想想看,如果图像里全是传感器噪声,不做高斯滤波直接匹配,代价计算会乱成什么样?

3. 中值滤波——专治椒盐噪声

中值滤波的思路很简单:把窗口内的像素排序,取中间值作为输出。这招对付椒盐噪声特别管用。

为什么?因为椒盐噪声是极端的白点或黑点,排序后它们会跑到两端,中间值根本不受影响。

我曾经在一个工厂项目中遇到摄像头有坏点,图像上全是白点。用高斯滤波怎么调都没用,换成中值滤波,一次搞定。

# 中值滤波
median_blur = cv2.medianBlur(img, 5)  # 5 是核大小,必须是奇数

避坑指南:中值滤波的核大小不要太大。3×3 或 5×5 就够了。我曾经试过 7×7,结果图像细节全没了,匹配出来的视差图跟马赛克似的。

4. Sobel边缘检测——梯度就是线索

立体匹配的本质是找对应点。而边缘,就是最明显的特征。Sobel 算子通过计算图像的一阶梯度来检测边缘。

它有两个核:一个检测水平边缘(Gx),一个检测垂直边缘(Gy)。

方向 作用
水平 (Gx) [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]] 检测垂直边缘
垂直 (Gy) [[-1, -2, -1], [0, 0, 0], [1, 2, 1]] 检测水平边缘
# Sobel 边缘检测
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)  # 水平梯度
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)  # 垂直梯度

# 计算梯度幅值
sobel_mag = np.sqrt(sobel_x**2 + sobel_y**2)
sobel_mag = np.uint8(np.clip(sobel_mag, 0, 255))

嗯,这里要注意:Sobel 对噪声敏感。所以我的习惯是——先高斯滤波,再做 Sobel。顺序不能乱。

5. Canny边缘检测——更聪明的选择

Canny 可以说是边缘检测的「天花板」了。它比 Sobel 多做了几件事:

  • 去噪:先用高斯滤波平滑图像
  • 计算梯度:类似 Sobel 算梯度幅值和方向
  • 非极大值抑制:只保留梯度方向上的局部最大值,让边缘变细
  • 双阈值检测:高阈值确定强边缘,低阈值连接弱边缘

说白了,Canny 把 Sobel 的「粗活」精细化了。它输出的边缘是单像素宽的,干净利落。

我的参数建议:低阈值取 50,高阈值取 150。这个比例(1:3)是我试过很多场景后觉得最通用的。如果边缘太多,就提高高阈值;如果边缘太少,就降低低阈值。

# Canny 边缘检测
edges = cv2.Canny(img, threshold1=50, threshold2=150)

# 也可以先高斯滤波再 Canny(Canny 内部自带高斯,但你可以控制参数)
blurred = cv2.GaussianBlur(img, (3, 3), 0.8)
edges_custom = cv2.Canny(blurred, 40, 120)

我记得有一次做室内场景的匹配,纹理太少,Sobel 出来的边缘断断续续。换成 Canny 之后,边缘连续性好多了,匹配成功率直接提升了 15%。

小结一下

这五个操作,各有各的定位:

  • 直方图均衡化——解决光照不均
  • 高斯滤波——去除高斯噪声
  • 中值滤波——去除椒盐噪声
  • Sobel——快速梯度计算
  • Canny——高质量边缘提取

在实际项目中,我通常的流程是:先看直方图决定要不要均衡化,然后根据噪声类型选滤波,最后用 Canny 提取边缘作为匹配特征。当然,具体怎么组合,还得看你的数据长什么样。

好了,这一章的内容就到这里。预处理是立体匹配的「地基」,地基打牢了,后面的代价计算和后处理才能发挥威力。下一章我们进入核心——代价计算,到时候见。


公众号:蓝海资料掘金营,微信deep3321