4. 图像处理基础(二):边缘检测与形态学操作

好,咱们接着聊图像处理。上一节我们讲了滤波和直方图,算是给图像做了个“清洁”和“体检”。这一节,我们要进入更核心的部分——边缘检测形态学操作

说白了,边缘检测就是帮计算机找到图像里“哪里变了”。而形态学操作,则是处理图像形状的“手术刀”。这两个东西,在我做过的几乎所有视觉项目里,都出现过。嗯,一个都跑不掉。

图像处理基础(二)核心知识体系 边缘检测 • Sobel算子:一阶导数,有方向 • Canny算法:多阶段,最稳定 • 应用:车道线、轮廓提取 • 避坑:噪声敏感,先滤波 形态学操作 • 腐蚀:消除边界点,缩小 • 膨胀:扩大边界,填充空洞 • 开运算:先腐蚀后膨胀 • 闭运算:先膨胀后腐蚀 配合使用 核心目标:提取特征 → 简化图像 → 为后续识别做准备

4.1 边缘检测:找到图像中的“突变”

边缘是什么?其实就是图像中像素值发生剧烈变化的地方。比如,白色背景上放一个黑色杯子,杯子的轮廓就是边缘。计算机怎么找?靠计算梯度。

我个人习惯把边缘检测分成两类:一阶导数二阶导数。不过实战中,我们最常用的就是 Sobel 和 Canny。前者是基础,后者是进阶。

4.1.1 Sobel 算子:有方向的边缘检测

Sobel 算子其实就是一个 3x3 的卷积核。它有两个版本:一个检测水平边缘,一个检测垂直边缘。你想想看,图像是二维的,边缘当然也有方向。

Sobel 核心思想:用差分近似代替导数。水平方向看左右像素的差异,垂直方向看上下像素的差异。

import cv2
import numpy as np

# 读取图像,转为灰度
img = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)

# 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_x = cv2.convertScaleAbs(sobel_x)
sobel_y = cv2.convertScaleAbs(sobel_y)

# 合并两个方向
sobel_combined = cv2.addWeighted(sobel_x, 0.5, sobel_y, 0.5, 0)

cv2.imshow('Sobel X', sobel_x)
cv2.imshow('Sobel Y', sobel_y)
cv2.imshow('Sobel Combined', sobel_combined)
cv2.waitKey(0)

小提示:注意这里用了 cv2.CV_64F 数据类型。为什么?因为梯度计算可能出现负值,用 8 位无符号整数会截断数据。我刚开始学的时候吃过这个亏,出来的边缘全是黑的,查了半天才发现是数据类型的问题。

Sobel 的优点是计算快,缺点是对噪声敏感。所以,我建议在使用 Sobel 之前,先做一次高斯滤波。嗯,这是标准流程。

4.1.2 Canny 边缘检测:工业级标准

Canny 算法是我个人最喜欢的边缘检测方法。它不只是一个算子,而是一整套流程。说白了,它做了四件事:去噪、算梯度、非极大值抑制、双阈值检测。

为什么 Canny 这么强?因为它用双阈值解决了“弱边缘”的问题。高阈值确定强边缘,低阈值保留弱边缘,只要弱边缘和强边缘相连,就保留。这招很聪明。

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

# 调整阈值观察效果
edges_low = cv2.Canny(img, 30, 100)   # 低阈值,更多细节
edges_high = cv2.Canny(img, 100, 200) # 高阈值,只保留强边缘

cv2.imshow('Canny Standard', edges)
cv2.imshow('Canny Low', edges_low)
cv2.imshow('Canny High', edges_high)
cv2.waitKey(0)

避坑指南:我曾经在一个项目中,直接用 Canny 处理原始图像,结果边缘断断续续,根本连不起来。后来发现,图像里有大量噪点。加一步高斯滤波后,效果立竿见影。记住:Canny 内部虽然自带滤波,但预处理永远不嫌多

阈值怎么选?我一般用 threshold1:threshold2 = 1:2 或 1:3 的比例。比如 50:150 就是一个不错的起点。你可以用滑动条调试,找到最适合你图像的参数。

4.2 形态学操作:图像的“整形手术”

形态学操作,听起来很高大上,其实就是处理图像形状的。它基于数学形态学,但实战中我们只需要记住几个基本操作:腐蚀、膨胀、开运算、闭运算。

这些操作通常用于二值图像(黑白图)。你想想看,二值图像里只有 0 和 255,处理起来特别干净。

4.2.1 腐蚀与膨胀:最基础的操作

腐蚀:让白色区域变小。原理是用一个结构元素(比如 3x3 的矩形)在图像上滑动,只有结构元素覆盖的区域全是白色,中心点才保留白色。否则变黑。

膨胀:让白色区域变大。只要结构元素覆盖的区域有一个白色,中心点就变白。

这两个操作是相反的。腐蚀可以消除小白点(噪声),膨胀可以填充小黑洞。

# 创建结构元素
kernel = np.ones((5,5), np.uint8)

# 腐蚀
erosion = cv2.erode(img_binary, kernel, iterations=1)

# 膨胀
dilation = cv2.dilate(img_binary, kernel, iterations=1)

cv2.imshow('Original', img_binary)
cv2.imshow('Erosion', erosion)
cv2.imshow('Dilation', dilation)
cv2.waitKey(0)

我的经验:迭代次数 iterations 不要太大。我见过有人设成 10,结果图像直接消失了。一般 1-3 次就够了。如果效果不够,可以换更大的结构元素,而不是增加迭代次数。

4.2.2 开运算与闭运算:组合拳

开运算 = 先腐蚀后膨胀。作用是去除小白点,同时保持主体形状不变。

闭运算 = 先膨胀后腐蚀。作用是填充小黑洞,同时保持主体形状不变。

你想想看,如果直接腐蚀,白色区域会变小。再膨胀回来,小白点没了,但大块区域恢复了。这就是开运算的精髓。

# 开运算
opening = cv2.morphologyEx(img_binary, cv2.MORPH_OPEN, kernel)

# 闭运算
closing = cv2.morphologyEx(img_binary, cv2.MORPH_CLOSE, kernel)

cv2.imshow('Opening', opening)
cv2.imshow('Closing', closing)
cv2.waitKey(0)

实战场景:我在做细胞计数项目时,图像里有很多微小的杂质。直接用阈值分割,杂质也被算成细胞了。后来用开运算,先腐蚀掉小杂质,再膨胀恢复细胞大小,计数准确率从 70% 提升到了 95%。

4.3 边缘检测 + 形态学:黄金搭档

这两个技术经常一起用。比如,先用 Canny 提取边缘,但边缘可能不连续。这时候用形态学的闭运算,把断开的边缘连起来。或者,先用形态学去噪,再用 Canny 检测,效果更好。

# 完整流程示例
# 1. 读取并灰度化
img = cv2.imread('example.jpg', cv2.IMREAD_GRAYSCALE)

# 2. 高斯滤波去噪
blurred = cv2.GaussianBlur(img, (5,5), 1.5)

# 3. Canny 边缘检测
edges = cv2.Canny(blurred, 50, 150)

# 4. 形态学闭运算连接断开的边缘
kernel = np.ones((3,3), np.uint8)
edges_closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)

cv2.imshow('Edges', edges)
cv2.imshow('Edges Closed', edges_closed)
cv2.waitKey(0)

这个流程,我在做车道线检测时用过。车道线有时因为光照原因断断续续,闭运算一处理,线条就完整了。嗯,效果立竿见影。

4.4 本章小结

这一节我们讲了两个核心工具:边缘检测和形态学操作。Sobel 适合快速检测,Canny 适合高质量检测。腐蚀膨胀是基础,开闭运算是进阶。

我个人建议,初学者先把 Canny 和开闭运算练熟。这两个组合能解决 80% 的常见问题。剩下的 20%,等你遇到具体项目时,再针对性学习。

记住一句话:图像处理没有银弹。每个项目都要调参数、试流程。多动手,多踩坑,经验就来了。


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