第四章 图像特征提取:从像素到几何的跃迁
各位同学,欢迎来到机器视觉最核心的一环——特征提取。
说白了,图像处理的前面几步(滤波、增强)都是在“洗数据”。真正让机器看懂世界的,是特征提取。我个人习惯把这一步称为“从像素到语义的桥梁”。你想想看,一张200万像素的图,直接扔给控制器,它根本不知道哪里是边缘、哪里是角点。特征提取,就是帮它提炼出关键信息。
4.1 边缘检测:找到物体的“骨架”
边缘是什么?是图像中灰度剧烈变化的地方。我刚开始做视觉时,以为边缘检测就是“找黑白交界”,后来踩了坑才明白——噪声、光照不均、纹理干扰,都会让边缘变成一团乱麻。
4.1.1 Sobel算子:简单但够用
Sobel算子是最基础的边缘检测方法。它用两个3x3的卷积核,分别计算水平方向和垂直方向的梯度。
// 水平方向Sobel核
[-1, 0, 1]
[-2, 0, 2]
[-1, 0, 1]
// 垂直方向Sobel核
[-1, -2, -1]
[ 0, 0, 0]
[ 1, 2, 1]
嗯,这里要注意:Sobel对噪声比较敏感。我在项目中遇到过,如果光源有轻微频闪,Sobel会检测出大量伪边缘。我的建议是——先用高斯滤波平滑一下,再做Sobel。
4.1.2 Canny边缘检测:工业界的标配
Canny算法是我用得最多的边缘检测方法。它比Sobel聪明得多,核心步骤是:
- 高斯滤波去噪
- 计算梯度幅值和方向
- 非极大值抑制(只保留局部梯度最大的点)
- 双阈值检测(高阈值确定强边缘,低阈值连接弱边缘)
关键参数:高阈值和低阈值的比例,我一般设为2:1到3:1。比如高阈值100,低阈值50。如果边缘断断续续,说明低阈值设高了;如果伪边缘太多,说明高阈值设低了。
我曾经在一个锂电池极片检测项目中,Canny参数调了整整两天。后来发现,问题不在算法,而在光源——换了同轴光源后,边缘一下就清晰了。所以,别只盯着代码,硬件环境同样重要。
4.2 角点检测:找到图像的“特征点”
角点,说白了就是两条边缘的交点。它在图像匹配、目标跟踪中非常有用。
4.2.1 Harris角点检测
Harris算法的核心思想:用一个窗口在图像上滑动,如果窗口内灰度变化很大,说明这里有角点。
数学上,它计算一个矩阵M的特征值。如果两个特征值都很大,就是角点;一个很大一个很小,是边缘;两个都很小,是平坦区域。
我的经验:Harris对尺度变化比较敏感。如果你要检测的物体大小变化很大,建议用多尺度Harris或者直接上SIFT。
4.2.2 Shi-Tomasi角点检测:Harris的改良版
Shi-Tomasi是Harris的升级版。它直接取M矩阵的最小特征值作为角点响应。说白了,它比Harris更“挑剔”——只保留那些真正稳定的角点。
// OpenCV中的Shi-Tomasi用法
cv::goodFeaturesToTrack(
src, // 输入图像
corners, // 输出的角点坐标
maxCorners, // 最多检测多少个角点
qualityLevel, // 质量等级(0.01-0.1)
minDistance // 角点之间的最小距离
);
我记得有一次做PCB板定位,用Harris检测出了200多个角点,但大部分是焊盘的虚假角点。换成Shi-Tomasi后,只保留最稳定的4个角点,定位精度反而更高了。有时候,少即是多。
4.3 轮廓提取与拟合:从边缘到形状
边缘检测得到的是像素点,轮廓提取则是把这些点连成有意义的形状。
4.3.1 轮廓提取
OpenCV里用findContours函数。这里有个坑——输入图像必须是二值图。我建议用Canny处理后的结果作为输入。
std::vector<std::vector<cv::Point>> contours;
cv::findContours(
binary_img, // 二值图
contours, // 输出的轮廓
RETR_EXTERNAL, // 只提取最外层轮廓
CHAIN_APPROX_SIMPLE // 压缩轮廓点
);
避坑指南:我曾经在提取轮廓时,忘记检查轮廓的面积。结果把噪声点也当成了轮廓,导致后续拟合全部出错。记住:先过滤面积太小的轮廓,再处理。
4.3.2 轮廓拟合
拟合就是把离散的轮廓点,拟合成规则的几何形状。常用的有:
- 直线拟合:用最小二乘法,适合检测边缘直线
- 圆拟合:用RANSAC算法,抗噪声能力强
- 椭圆拟合:适合检测圆形物体的透视变形
我个人习惯用RANSAC做圆拟合。为什么?因为它能自动剔除离群点。我在检测轴承内径时,即使有10%的轮廓点被划痕干扰,RANSAC依然能准确拟合出圆。
4.4 Blob分析:连通域与几何特征
Blob分析,说白了就是“找团块”。它把图像中连通的像素区域找出来,然后计算每个区域的几何特征。
4.4.1 连通域标记
连通域标记有两种方式:4邻域和8邻域。工业上我基本只用8邻域,因为它对斜向连接更敏感。
cv::Mat labels, stats, centroids;
int num_blobs = cv::connectedComponentsWithStats(
binary_img,
labels,
stats,
centroids,
8 // 8邻域
);
4.4.2 几何特征计算
标记完连通域后,我们可以计算每个Blob的:
| 特征 | 含义 | 应用场景 |
|---|---|---|
| 面积 | 像素点个数 | 过滤噪声、筛选目标 |
| 周长 | 边界像素长度 | 判断形状复杂度 |
| 质心 | 几何中心坐标 | 定位、跟踪 |
| 外接矩形 | 最小包围矩形 | 粗略定位、尺寸测量 |
| 圆形度 | 4π×面积/周长² | 判断是否接近圆形 |
| 长宽比 | 外接矩形的长/宽 | 区分细长和扁平物体 |
实战技巧:我在检测药片时,先用面积过滤掉粉尘(面积小于50像素的不要),再用圆形度判断药片是否破损(圆形度小于0.8的剔除)。两步下来,误检率从5%降到了0.3%。
知识体系总览
下面这张图,是我自己总结的特征提取知识体系。你可以把它当作一个“导航图”,遇到具体问题时,先定位到对应模块。
好了,特征提取这部分内容比较多,但都是硬核干货。你可能会问:这么多方法,到底该用哪个?我的建议是——先明确你的目标:是要定位?测量?还是识别?然后从最简单的算法开始试,不行再升级。别一上来就上深度学习,很多时候Sobel+Canny就够用了。