4. OpenCV 基础(三):图像模糊、边缘检测与形态学操作

图像处理里,模糊、边缘检测和形态学操作,可以说是最常用的三板斧。我刚开始接触 OpenCV 时,觉得这些不就是些滤镜嘛,后来做项目才发现,每一步都藏着不少坑。今天咱们就把这三块内容彻底捋清楚。

本章知识体系:图像预处理三剑客 图像预处理 图像模糊 均值模糊:简单平均 高斯模糊:加权平均 中值模糊:中位数替代 边缘检测 Canny 边缘检测 双阈值 + 滞后跟踪 先模糊再检测 形态学操作 腐蚀:消除边界点 膨胀:扩大边界点 开运算 / 闭运算

4.1 图像模糊:降噪的第一步

模糊,说白了就是用邻域像素的某种统计值,去替代中心像素。为什么要这么做?因为真实世界的图像总带着噪声,模糊一下,噪声就被「平均」掉了。我个人习惯在边缘检测之前,一定会先做模糊处理。

4.1.1 均值模糊

最简单的模糊方式。把核覆盖区域的所有像素值加起来,除以像素个数。OpenCV 里用 cv2.blur() 实现。

import cv2
import numpy as np

img = cv2.imread('example.jpg')
# 核大小 5x5
blurred = cv2.blur(img, (5, 5))
cv2.imshow('Mean Blur', blurred)
cv2.waitKey(0)
我的经验:核越大,模糊越强,但图像细节丢失也越严重。做目标检测时,核大小一般不超过 7x7,否则小目标就糊没了。

4.1.2 高斯模糊

均值模糊有个问题——它把邻域里每个像素的权重看成一样的。但实际上,离中心越近的像素,相关性应该越强。高斯模糊就是基于这个想法,用高斯分布给像素分配权重。

# 高斯模糊,核大小 5x5,sigmaX 自动计算
gaussian = cv2.GaussianBlur(img, (5, 5), 0)
cv2.imshow('Gaussian Blur', gaussian)

这里 sigmaX=0 表示让 OpenCV 根据核大小自动计算标准差。我个人习惯用这个参数,省事。

4.1.3 中值模糊

中值模糊不是算平均,而是取邻域内所有像素的中位数。它对椒盐噪声(就是那种黑白点噪声)效果特别好。我在做工业检测项目时,遇到过相机传感器坏点导致的噪声,中值模糊一上,效果立竿见影。

# 中值模糊,核大小必须为奇数
median = cv2.medianBlur(img, 5)
cv2.imshow('Median Blur', median)
注意:中值模糊的核大小必须是奇数。你想想看,如果是偶数,中位数怎么取?OpenCV 会直接报错。

4.2 边缘检测:Canny 算法

边缘检测里,Canny 算法是当之无愧的王者。它不像 Sobel 那样只算梯度,而是有一套完整的流程:去噪、算梯度、非极大值抑制、双阈值检测。我当年第一次用 Canny 时,觉得这东西太神奇了,怎么就能把轮廓提取得这么干净?

4.2.1 Canny 的基本用法

# 先模糊,再 Canny
blurred = cv2.GaussianBlur(img, (5, 5), 0)
edges = cv2.Canny(blurred, 50, 150)
cv2.imshow('Canny Edges', edges)

两个阈值:50 是低阈值,150 是高阈值。梯度值高于高阈值的,一定是边缘;低于低阈值的,一定不是;介于两者之间的,只有跟确定边缘相连的才算。

参数 作用 我的建议值
低阈值 控制弱边缘的保留 30-60
高阈值 控制强边缘的保留 100-200
核大小 高斯模糊的核 5x5

核心原则:高阈值一般是低阈值的 2-3 倍。我曾经调参时把两个阈值设成一样,结果边缘断断续续,根本没法用。

4.2.2 为什么一定要先模糊?

Canny 本身不包含去噪步骤。如果直接对原图做 Canny,噪声会被当成边缘检测出来,结果就是满屏的杂乱线条。我见过不少新手踩这个坑——「为什么我的 Canny 结果全是噪点?」嗯,先模糊一下就好了。

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

形态学操作,说白了就是「让白色区域变大或变小」。它基于图像的形状(形态)来做处理,主要用在二值图像上。我经常在分割之后,用形态学操作来清理分割结果。

4.3.1 腐蚀

腐蚀让白色区域「瘦一圈」。它的原理是:核覆盖的区域里,只要有一个像素是黑色,中心像素就变成黑色。效果就是消除白色的小噪点,断开细小的连接。

# 定义核
kernel = np.ones((5,5), np.uint8)
eroded = cv2.erode(binary_img, kernel, iterations=1)
cv2.imshow('Eroded', eroded)

4.3.2 膨胀

膨胀跟腐蚀相反,让白色区域「胖一圈」。核覆盖的区域里,只要有一个像素是白色,中心像素就变成白色。效果是填充白色区域内的小黑洞,连接断开的白色区域。

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

4.3.3 开运算与闭运算

开运算 = 先腐蚀后膨胀。用来消除小白点,同时保持大区域形状不变。闭运算 = 先膨胀后腐蚀。用来填充小黑洞,同时保持大区域形状不变。

# 开运算
opening = cv2.morphologyEx(binary_img, cv2.MORPH_OPEN, kernel)
# 闭运算
closing = cv2.morphologyEx(binary_img, cv2.MORPH_CLOSE, kernel)
避坑指南:我曾经在 YOLO 检测后处理中,直接用腐蚀去消除误检区域,结果把目标也腐蚀没了。后来改用开运算,效果好了很多。记住:先腐蚀后膨胀,才是开运算。

4.4 综合应用:预处理流水线

在实际项目中,这些操作很少单独使用。我一般会搭一条预处理流水线:

  1. 读图 → 转灰度(如果后续不需要颜色信息)
  2. 高斯模糊 → 降噪,核大小 5x5
  3. Canny 边缘检测 → 提取轮廓
  4. 形态学闭运算 → 连接断开的边缘
  5. 轮廓查找 → 得到目标区域
def preprocess_pipeline(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5,5), 0)
    edges = cv2.Canny(blurred, 50, 150)
    kernel = np.ones((3,3), np.uint8)
    closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
    return closed
重要提醒:预处理不是越复杂越好。你想想看,每多一步操作,就多一分信息损失。我的原则是:够用就行,能少一步就少一步。

好了,这一章的内容就是这些。模糊、边缘检测、形态学操作,这三样东西在 OpenCV 里几乎天天用。多动手试试不同的参数组合,你很快就能找到感觉。