4. 图像处理基础(二):边缘检测与形态学操作
好,咱们接着聊图像处理。上一节我们讲了滤波和直方图,算是给图像做了个“清洁”和“体检”。这一节,我们要进入更核心的部分——边缘检测和形态学操作。
说白了,边缘检测就是帮计算机找到图像里“哪里变了”。而形态学操作,则是处理图像形状的“手术刀”。这两个东西,在我做过的几乎所有视觉项目里,都出现过。嗯,一个都跑不掉。
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