4. OpenCV特征提取与ROI分割

各位同学,欢迎来到第四章。这一章的内容,我个人认为是整个工业质检项目里最“接地气”的部分。说白了,就是教计算机怎么“看”清楚零件的边界,然后从一堆乱七八糟的背景里,把咱们真正关心的那个区域给抠出来。

你想想看,在产线上,相机拍到的画面里,可能有传送带、有夹具、有反光,甚至还有油污。但咱们真正要检测的,可能只是那个小小的螺丝孔,或者芯片的引脚。怎么把这块区域精确地提取出来?这就是本章要解决的问题。

本章核心逻辑: 从像素级的边缘检测 → 到物体级的轮廓提取 → 再到区域级的ROI分割。这是一个层层递进、从底层视觉到高层语义的过程。
第4章 知识体系:从边缘到ROI 输入图像 Canny边缘检测 轮廓查找 ROI 模板匹配(备选方案) 说明:边缘是轮廓的基础,轮廓是ROI的边界。模板匹配则是在已知目标形状时,直接定位ROI。 实际项目中,我通常先用Canny找边缘,再用轮廓筛选定位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()
经验之谈: threshold1和threshold2的比例,我一般设为1:2或1:3。比如(50, 150)或(30, 90)。如果边缘断断续续,就降低threshold1;如果噪声太多,就提高threshold2。

这里有个坑,我曾经踩过。在检测精密零件时,边缘总是出现“双线”现象。后来发现是高斯滤波的核大小没调好。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)
注意: cv2.findContours() 在OpenCV 3和4中返回值不同。OpenCV 4返回2个值(contours, hierarchy),OpenCV 3返回3个值(image, contours, hierarchy)。建议统一用OpenCV 4,或者加个版本判断。

轮廓查找出来后,下一步就是筛选。工业场景里,我们通常只关心特定大小、特定形状的轮廓。比如,检测螺丝垫圈,我只找面积在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]
工业场景小贴士: 如果产线上零件位置固定,可以直接用固定坐标提取ROI,省去轮廓查找的步骤。但如果零件位置有偏移,就必须先做轮廓定位,再动态提取ROI。

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)来替代。

避坑指南: 我曾经在一个项目中,用模板匹配检测手机屏幕上的Logo。产线上偶尔会出现Logo印歪的情况,模板匹配死活找不到。后来我改成用轮廓匹配,先提取Logo的外轮廓,再计算轮廓的Hu矩,问题就解决了。所以,模板匹配只适合位置和角度都固定的场景。

4.5 本章小结:一个完整的ROI提取流程

最后,我把本章的知识点串成一个完整的流程。这也是我在实际项目中常用的套路。

  1. 读图与预处理: 灰度化、高斯滤波去噪。
  2. Canny边缘检测: 调好双阈值,得到干净的边缘图。
  3. 轮廓查找: 用RETR_EXTERNAL只找最外层轮廓,减少计算量。
  4. 轮廓筛选: 按面积、宽高比、圆形度等指标,过滤掉干扰物。
  5. ROI提取: 用外接矩形坐标,加余量,把目标区域切出来。
  6. (可选)模板匹配: 如果目标位置有微小偏移,用模板匹配做精确定位。

嗯,这一章的内容就到这里。流程看起来简单,但每个环节都有不少细节。尤其是Canny的阈值和轮廓筛选的条件,需要根据实际产线的图像反复调试。别怕麻烦,调多了就有感觉了。