4、OpenCV核心基础(三):图像形态学操作与轮廓查找
各位好,欢迎来到第四章。前两章我们把图像读写、色彩空间和几何变换捋了一遍,算是把OpenCV的基本功打扎实了。这一章咱们要聊两个非常实战的话题:形态学操作和轮廓查找。
说白了,形态学操作就是「用一个小结构去探测图像里的形状」,而轮廓查找则是「把图像里物体的边界给描出来」。这两招在目标检测预处理里几乎天天用——YOLO检测之前,图像质量好不好、目标边界清不清晰,全靠它们撑着。
本章核心脉络:腐蚀/膨胀 → 开运算/闭运算 → 轮廓查找与绘制。每一步都是下一步的基础,别跳着看。
4.1 腐蚀与膨胀:形态学的「原子操作」
腐蚀和膨胀是形态学里最基础的两个操作。你想想看,图像里的白色区域(前景)就像一块橡皮泥——腐蚀是往里削,膨胀是往外推。
4.1.1 腐蚀 (Erosion)
腐蚀的原理很简单:用一个结构元素(比如3×3的矩形)在图像上滑动,只有当结构元素覆盖的区域全部都是白色时,中心点才保留白色,否则变成黑色。
结果就是:白色区域变小了,细小的白色噪点被去掉了。我在做工业检测项目时,经常先用腐蚀去掉传感器噪声,效果比滤波还直接。
import cv2
import numpy as np
# 读取二值图像(或先做阈值处理)
img = cv2.imread('binary.png', 0)
# 定义结构元素:3x3 矩形
kernel = np.ones((3, 3), np.uint8)
# 腐蚀操作
eroded = cv2.erode(img, kernel, iterations=1)
# iterations 控制腐蚀次数,我一般不超过3次,否则目标会消失
我的经验:结构元素不一定是矩形。用椭圆或十字形核可以保留更多细节。比如处理圆形零件时,椭圆核比矩形核好得多。
4.1.2 膨胀 (Dilation)
膨胀和腐蚀正好相反:只要结构元素覆盖的区域里有一个白色像素,中心点就变成白色。结果就是白色区域扩大,小孔被填上。
# 膨胀操作
dilated = cv2.dilate(img, kernel, iterations=1)
膨胀最常用的场景是「连接断开的物体」。我记得有一次做车牌识别,字符笔画因为光照原因断成了几截,膨胀一下立马连上了。
| 操作 | 效果 | 典型应用 |
|---|---|---|
| 腐蚀 | 白色区域缩小,去除小噪点 | 去噪、分离粘连物体 |
| 膨胀 | 白色区域扩大,填充小孔 | 连接断裂区域、增强目标 |
4.2 开运算与闭运算:组合拳才是王道
单独用腐蚀或膨胀,往往副作用太大。比如腐蚀虽然去噪了,但目标也变小了。这时候就需要组合操作。
4.2.1 开运算 (Opening)
开运算 = 先腐蚀,后膨胀。先腐蚀去掉小噪点,再膨胀恢复目标大小。说白了就是「把图像上的小疙瘩磨平」。
# 开运算
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
我处理显微镜细胞图像时,开运算几乎是标配。细胞周围总有细小的杂质,开运算一次就干净了,细胞主体还保持原样。
4.2.2 闭运算 (Closing)
闭运算 = 先膨胀,后腐蚀。先膨胀填上目标内部的小孔,再腐蚀恢复边缘。适合「修补目标内部的裂缝或空洞」。
# 闭运算
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
避坑指南:我曾经在焊缝检测项目里,直接用闭运算去填裂缝,结果把真正的缺陷也填上了。记住:闭运算只适合填小孔,大裂缝别指望它。
| 操作 | 顺序 | 解决什么问题 |
|---|---|---|
| 开运算 | 腐蚀 → 膨胀 | 去除外部小噪点,保留目标大小 |
| 闭运算 | 膨胀 → 腐蚀 | 填充内部小孔,保持边缘平滑 |
4.3 轮廓查找:从像素到形状
形态学操作做完,图像干净了,接下来就是「把目标轮廓找出来」。轮廓查找是目标检测的前置步骤——YOLO虽然直接回归边界框,但很多传统方法还是靠轮廓定位。
4.3.1 findContours 函数
OpenCV的 findContours 是核心函数。它从二值图像里提取所有白色区域的轮廓。
# 注意:输入必须是二值图像,且背景为黑色,前景为白色
contours, hierarchy = cv2.findContours(
binary_img, # 二值图
cv2.RETR_EXTERNAL, # 轮廓检索模式
cv2.CHAIN_APPROX_SIMPLE # 轮廓近似方法
)
这里有两个参数很关键:
- RETR_EXTERNAL:只检测最外层轮廓。我90%的场景都用这个,省事。
- RETR_TREE:检测所有轮廓并建立层级关系。需要区分内外轮廓时用。
- CHAIN_APPROX_SIMPLE:只保存轮廓的端点,节省内存。
- CHAIN_APPROX_NONE:保存所有点,精度高但数据量大。
我个人习惯:先用 RETR_EXTERNAL 试试,如果发现轮廓不完整再换 RETR_TREE。别一上来就开全量模式,数据量大的时候能慢到你怀疑人生。
4.3.2 绘制轮廓
找到轮廓后,用 drawContours 画出来看看效果。
# 在原图上绘制所有轮廓
result_img = img.copy()
cv2.drawContours(
result_img, # 绘制目标图像
contours, # 轮廓列表
-1, # -1 表示绘制所有轮廓
(0, 255, 0), # 绿色
2 # 线条粗细
)
嗯,这里要注意:drawContours 会修改传入的图像,所以最好先 .copy() 一份。我曾经直接在原图上画,结果调试完原图也毁了,还得重新读。
4.3.3 轮廓筛选:别什么都画
实际项目中,图像里往往有很多小轮廓(噪点、无关物体)。我们需要根据面积或周长来筛选。
# 筛选面积大于 500 像素的轮廓
valid_contours = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area > 500:
valid_contours.append(cnt)
print(f"原始轮廓数: {len(contours)}, 筛选后: {len(valid_contours)}")
我做过一个零件分拣项目,图像里有上百个轮廓,但真正要检测的只有五六个。用面积一筛,计算量直接降了一个数量级。
核心思路:形态学操作负责「净化」图像,轮廓查找负责「定位」目标。两者配合,才能给YOLO提供高质量的输入。
4.4 实战小案例:硬币检测
咱们把今天学的串起来,做一个简单的硬币检测流程。
import cv2
import numpy as np
# 1. 读取图像并转灰度
img = cv2.imread('coins.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2. 二值化(阈值处理,下章细讲)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
# 3. 形态学去噪:开运算
kernel = np.ones((5, 5), np.uint8)
clean = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
# 4. 闭运算填充硬币内部反光
clean = cv2.morphologyEx(clean, cv2.MORPH_CLOSE, kernel)
# 5. 查找轮廓
contours, _ = cv2.findContours(clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 6. 筛选并绘制
for cnt in contours:
area = cv2.contourArea(cnt)
if area > 1000: # 排除小噪点
cv2.drawContours(img, [cnt], -1, (0, 255, 0), 2)
# 还可以计算外接矩形或圆形
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(img, (x, y), (x+w, y+h), (255, 0, 0), 2)
cv2.imshow('Result', img)
cv2.waitKey(0)
这个流程虽然简单,但已经包含了形态学+轮廓的完整链路。你把这个跑通了,后面YOLO的预处理部分就轻松多了。
注意:硬币图像的光照不均匀时,二值化效果会变差。我建议先做自适应阈值或高斯滤波,别直接硬阈值。这个坑我踩过好几次。