4. OpenCV特征提取与ROI分割:边缘检测、轮廓查找与ROI提取
各位同学,欢迎来到第四章。这一章的内容,我个人认为是整个工业质检项目里最“接地气”的部分。说白了,就是教计算机怎么“看”清楚零件的边界,然后从一堆乱七八糟的背景里,把咱们真正关心的那个区域给抠出来。
你想想看,在产线上,相机拍到的画面里,可能有传送带、有夹具、有反光,甚至还有油污。但咱们真正要检测的,可能只是那个小小的螺丝孔,或者芯片的引脚。怎么把这块区域精确地提取出来?这就是本章要解决的问题。
4.1 Canny边缘检测:工业界的“黄金标准”
Canny边缘检测,在工业视觉领域,地位相当稳固。为什么?因为它抗噪能力强,而且能检测出真正的弱边缘。我记得刚入行时,用过Sobel、Laplacian,但一到产线那种光照忽明忽暗的环境,效果就崩了。后来换成Canny,问题基本解决。
Canny算法内部其实做了好几步:去噪、算梯度、非极大值抑制、双阈值检测。但咱们用OpenCV,一句话就搞定。
import cv2
import numpy as np
# 读取图像
img = cv2.imread('gear.jpg', cv2.IMREAD_GRAYSCALE)
# Canny边缘检测
edges = cv2.Canny(img, threshold1=50, threshold2=150)
# 显示结果
cv2.imshow('Original', img)
cv2.imshow('Canny Edges', edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
这里有个坑,我曾经踩过。在检测精密零件时,边缘总是出现“双线”现象。后来发现是高斯滤波的核大小没调好。Canny内部默认用5x5的高斯核,但如果图像噪声大,我建议先手动做一次高斯模糊,再用Canny。
# 先模糊,再Canny
blurred = cv2.GaussianBlur(img, (7, 7), 1.5)
edges = cv2.Canny(blurred, 30, 100)
4.2 轮廓查找与绘制:从边缘到物体
边缘检测得到的是像素点,还不是物体。怎么把散落的边缘点连成一个个完整的物体边界?这就用到轮廓查找了。
OpenCV里,cv2.findContours() 是核心函数。它有两个输出:轮廓列表和层级关系。层级关系在工业场景里特别有用,比如检测PCB板上的焊盘,内圈和外圈就是父子关系。
# 查找轮廓
contours, hierarchy = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 绘制所有轮廓
img_contour = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cv2.drawContours(img_contour, contours, -1, (0, 255, 0), 2)
cv2.imshow('Contours', img_contour)
cv2.waitKey(0)
轮廓查找出来后,下一步就是筛选。工业场景里,我们通常只关心特定大小、特定形状的轮廓。比如,检测螺丝垫圈,我只找面积在500-2000像素之间的轮廓。
# 筛选轮廓:按面积过滤
min_area = 500
max_area = 2000
filtered_contours = []
for cnt in contours:
area = cv2.contourArea(cnt)
if min_area < area < max_area:
filtered_contours.append(cnt)
print(f"原始轮廓数: {len(contours)}, 筛选后: {len(filtered_contours)}")
除了面积,还可以用轮廓周长、外接矩形宽高比、圆形度等指标。我个人习惯用外接矩形的宽高比来区分横着放的零件和竖着放的零件。
| 筛选指标 | 函数 | 典型应用 |
|---|---|---|
| 面积 | cv2.contourArea() | 过滤小噪点或大背景 |
| 周长 | cv2.arcLength() | 判断轮廓复杂度 |
| 外接矩形 | cv2.boundingRect() | 获取ROI位置和尺寸 |
| 最小外接矩形 | cv2.minAreaRect() | 获取倾斜零件的角度 |
| 圆形度 | 4π·面积/周长² | 区分圆孔和方孔 |
4.3 ROI区域提取:把目标“抠”出来
ROI(Region of Interest),感兴趣区域。说白了,就是把图像里我们关心的那一小块切出来。在工业质检里,这一步几乎是必须的。因为后续的缺陷检测、尺寸测量,都是在ROI上进行的,而不是整张图。
提取ROI最直接的方法,就是用轮廓的外接矩形坐标来裁剪。
# 假设我们已经筛选出一个目标轮廓 cnt
x, y, w, h = cv2.boundingRect(cnt)
# 提取ROI
roi = img[y:y+h, x:x+w]
# 显示ROI
cv2.imshow('ROI', roi)
cv2.waitKey(0)
这里有个细节,我特别想强调。在裁剪ROI时,最好加一点“余量”。比如,外接矩形是(100, 100, 200, 200),我通常会往外扩10个像素,变成(90, 90, 220, 220)。为什么?因为轮廓检测有时会漏掉边缘的一两个像素,扩一点能保证目标完整。
# 带余量的ROI提取
padding = 10
x1 = max(0, x - padding)
y1 = max(0, y - padding)
x2 = min(img.shape[1], x + w + padding)
y2 = min(img.shape[0], y + h + padding)
roi_padded = img[y1:y2, x1:x2]
4.4 模板匹配基础:当你知道目标长什么样
模板匹配,是另一种定位ROI的方法。它不依赖边缘和轮廓,而是直接用一张小图(模板)去大图里“找相似”。在工业场景里,常用于定位印刷字符、Logo、或者标准零件。
OpenCV的 cv2.matchTemplate() 提供了多种匹配方法。我项目中用得最多的是 TM_CCOEFF_NORMED,因为它对光照变化不敏感。
# 读取模板和原图
template = cv2.imread('template.jpg', cv2.IMREAD_GRAYSCALE)
img = cv2.imread('scene.jpg', cv2.IMREAD_GRAYSCALE)
# 模板匹配
result = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED)
# 找到最佳匹配位置
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
# 绘制匹配结果
h, w = template.shape
top_left = max_loc
bottom_right = (top_left[0] + w, top_left[1] + h)
img_color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cv2.rectangle(img_color, top_left, bottom_right, (0, 255, 0), 2)
cv2.imshow('Match Result', img_color)
cv2.waitKey(0)
模板匹配有个明显的局限:它不抗旋转和缩放。如果零件在产线上有角度偏转,模板匹配就会失效。这时候,我一般会用特征点匹配(比如SIFT、ORB)或者形状匹配(cv2.matchShapes)来替代。
4.5 本章小结:一个完整的ROI提取流程
最后,我把本章的知识点串成一个完整的流程。这也是我在实际项目中常用的套路。
- 读图与预处理: 灰度化、高斯滤波去噪。
- Canny边缘检测: 调好双阈值,得到干净的边缘图。
- 轮廓查找: 用RETR_EXTERNAL只找最外层轮廓,减少计算量。
- 轮廓筛选: 按面积、宽高比、圆形度等指标,过滤掉干扰物。
- ROI提取: 用外接矩形坐标,加余量,把目标区域切出来。
- (可选)模板匹配: 如果目标位置有微小偏移,用模板匹配做精确定位。
嗯,这一章的内容就到这里。流程看起来简单,但每个环节都有不少细节。尤其是Canny的阈值和轮廓筛选的条件,需要根据实际产线的图像反复调试。别怕麻烦,调多了就有感觉了。