3. 图像预处理实战:高斯模糊、中值滤波、直方图均衡化、形态学操作、边缘检测

图像预处理,说白了就是给原始图像“洗把脸”。

摄像头直接拍出来的画面,往往带着噪声、光照不均、背景杂乱等问题。你要是直接把这种图扔给 YOLO,检测效果大概率会打折扣。我刚开始做项目时,就吃过这个亏——模型在实验室跑得好好的,一到现场就各种漏检。后来才发现,是预处理没跟上。

这一章,咱们就把 OpenCV 里最常用的几个预处理手段过一遍。它们分别是:高斯模糊、中值滤波、直方图均衡化、形态学操作(腐蚀与膨胀),以及Canny 边缘检测。

核心思路:预处理不是越复杂越好,而是要根据实际场景,选对工具、调好参数。目标只有一个——让后续的检测更稳、更快。

图像预处理知识体系 原始图像 去噪 光照校正 结构提取 高斯模糊 / 中值滤波 直方图均衡化 腐蚀 / 膨胀 / Canny 预处理后图像

3.1 去噪:高斯模糊与中值滤波

噪声是图像处理里最常见的“敌人”。传感器热噪声、光线不足导致的颗粒感、传输过程中的干扰……都会让画面变脏。

3.1.1 高斯模糊

高斯模糊的原理,是用一个符合高斯分布的卷积核,对图像做加权平均。说白了,就是让每个像素的值,变成它周围像素的“加权平均值”。

这样做的好处是:平滑噪声,同时保留边缘的相对强度。但代价是图像会变模糊,细节会丢失。

我的经验:高斯模糊的核大小,一般取奇数。3x3 或 5x5 最常用。核越大,模糊越强,但计算量也越大。我一般从 5x5 开始试,效果不够再加。

import cv2

img = cv2.imread('frame.jpg')
# 高斯模糊,核大小 5x5,标准差自动计算
blurred = cv2.GaussianBlur(img, (5, 5), 0)
cv2.imshow('Gaussian Blur', blurred)

3.1.2 中值滤波

中值滤波,顾名思义,是用邻域内所有像素的中位数来替换中心像素的值。

它对付“椒盐噪声”(黑白点噪声)特别有效。为什么?因为中位数不受极端值影响。你想想看,一个白点周围全是黑点,取中位数后,白点就被“拉回”正常值了。

注意:中值滤波的核大小也必须为奇数。而且它比高斯模糊慢一些,尤其是核大了之后。实时处理时,建议用 3x3 或 5x5。

# 中值滤波,核大小 5
median = cv2.medianBlur(img, 5)
cv2.imshow('Median Filter', median)
方法 适用噪声类型 边缘保留能力 速度
高斯模糊 高斯噪声、传感器噪声 中等 快
中值滤波 椒盐噪声、脉冲噪声 较好 中等

3.2 光照校正:直方图均衡化

你有没有遇到过这种情况?摄像头对着窗户拍,人脸黑成一团,背景却亮得刺眼。这就是光照不均。

直方图均衡化,就是用来解决这个问题的。它通过拉伸图像的灰度分布,让暗的地方变亮,亮的地方变暗,从而让细节更清晰。

3.2.1 全局直方图均衡化

OpenCV 里直接用 equalizeHist 就行。但注意,它只接受单通道灰度图。

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
equ = cv2.equalizeHist(gray)
cv2.imshow('Equalized', equ)

3.2.2 CLAHE(自适应直方图均衡化)

全局均衡化有个问题:它会放大噪声,而且会让图像看起来不自然。我个人更推荐用 CLAHE。

CLAHE 把图像分成一个个小块,在每个块内做均衡化,然后用插值消除块之间的边界。效果更细腻,噪声控制也更好。

# 创建 CLAHE 对象,对比度限制 2.0,块大小 8x8
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
clahe_img = clahe.apply(gray)
cv2.imshow('CLAHE', clahe_img)

避坑指南:我曾经在一个夜间监控项目里,直接用全局均衡化,结果把背景噪声放大了好几倍,目标反而看不清。换成 CLAHE 后,效果立竿见影。所以,优先用 CLAHE,除非你确定场景光照很均匀。

3.3 形态学操作:腐蚀与膨胀

形态学操作,听起来高大上,其实就是对图像的形状做“腐蚀”或“膨胀”。

它们通常用于二值图像(黑白图),但也可以用在灰度图上。核心思想是用一个结构元素(比如 3x3 的矩形)在图像上滑动,做逻辑运算。

3.3.1 腐蚀

腐蚀,就是让白色区域“瘦一圈”。它会把物体边界上的像素“吃掉”。

有什么用?去除小的白色噪点,或者分离连在一起的物体。

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
eroded = cv2.erode(binary_img, kernel, iterations=1)

3.3.2 膨胀

膨胀,正好相反,让白色区域“胖一圈”。

它用来填补物体内部的空洞,或者连接断裂的轮廓。

dilated = cv2.dilate(binary_img, kernel, iterations=1)

3.3.3 开运算与闭运算

实际项目中,我们很少单独用腐蚀或膨胀。更常用的是它们的组合:

  • 开运算:先腐蚀,再膨胀。用来去除小噪点,同时保持物体大小不变。
  • 闭运算:先膨胀,再腐蚀。用来填补小空洞,同时保持物体大小不变。
# 开运算
opening = cv2.morphologyEx(binary_img, cv2.MORPH_OPEN, kernel)
# 闭运算
closing = cv2.morphologyEx(binary_img, cv2.MORPH_CLOSE, kernel)

我的习惯:在做目标检测前,如果背景有大量小噪点,我会先做一次开运算。如果目标内部有空洞,我会做一次闭运算。这两个操作,几乎成了我预处理流程里的“标配”。

3.4 边缘检测:Canny

边缘检测,是计算机视觉里最经典的任务之一。Canny 算法,又是其中最常用的。

它为什么好?因为它不是简单地找梯度大的点,而是经过了一系列精心设计的步骤:

  1. 高斯滤波:先平滑,减少噪声干扰。
  2. 计算梯度:用 Sobel 算子算出每个像素的梯度强度和方向。
  3. 非极大值抑制:只保留梯度方向上的局部最大值,让边缘变细。
  4. 双阈值检测:用高阈值和低阈值,区分强边缘、弱边缘和非边缘。
  5. 边缘连接:只保留与强边缘相连的弱边缘,断开孤立的弱边缘。

嗯,这里要注意:双阈值的选择,是 Canny 效果好坏的关键。

# Canny 边缘检测,低阈值 50,高阈值 150
edges = cv2.Canny(gray, 50, 150)
cv2.imshow('Canny Edges', edges)

避坑指南:高阈值设得太高,会丢失真实边缘;设得太低,会保留大量噪声。我一般从 (50, 150) 开始调,然后根据效果微调。如果边缘太碎,就降低低阈值;如果噪声太多,就提高高阈值。

参数 作用 常见范围
低阈值 控制弱边缘的保留 30 - 80
高阈值 控制强边缘的保留 100 - 200

3.5 综合实战:一个完整的预处理流程

说了这么多,咱们来串一下。假设你拿到一张监控画面,光照不均,还有噪声。你会怎么做?

import cv2

img = cv2.imread('monitor.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 1. 去噪:中值滤波,去掉椒盐噪声
denoised = cv2.medianBlur(gray, 5)

# 2. 光照校正:CLAHE
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
enhanced = clahe.apply(denoised)

# 3. 二值化(假设我们要做轮廓检测)
_, binary = cv2.threshold(enhanced, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# 4. 形态学:开运算去噪点
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)

# 5. 边缘检测
edges = cv2.Canny(cleaned, 50, 150)

cv2.imshow('Final Result', edges)

总结一下:预处理没有固定公式。但你可以记住这个通用思路:去噪 → 光照校正 → 二值化 → 形态学清理 → 边缘检测。每一步的参数,都要根据你的实际场景去调。多试几次,你就能找到最适合自己项目的“配方”。