前言:在机器视觉项目中,最怕的不是识别不出来,而是“误判”。特别是在纽扣、螺丝等标准化零件的检测中,两个批次的产品可能只有微小的纹理差异。本文记录了我如何通过“多维度视觉融合”的思路,利用 Qt 和 OpenCV 构建一套抗干扰能力强的检测系统。
1. 痛点:单一维度的局限性
在项目初期,我尝试过简单的 **Template Matching (模板匹配)**,但很快遇到了瓶颈:
- 光照敏感:现场灯光稍有波动,像素级匹配的得分就断崖式下跌。
- 形变干扰:纽扣在传送带上可能会通过旋转,简单的
matchTemplate对旋转不具备不变性。 - 计算量大:如果对全图进行多角度旋转匹配,CPU 占用率直接飙升,无法满足实时性(<100ms)要求。
因此,我决定采用“漏斗式过滤”架构,从几何、颜色、轮廓、纹理四个维度层层递进,既保证了速度,又提高了精度。
2. 系统核心架构
2.1 硬件与环境
- 开发环境:Qt 5.14 (MSVC 2017 64bit) + OpenCV 4.5.1
- 相机设置:固定曝光(关键!)。代码中显式关闭了自动曝光:
1
2
3cap.set(cv::CAP_PROP_AUTO_EXPOSURE, 0);
// 实测设置为 -4 左右适合室内打光环境
// cap.set(cv::CAP_PROP_EXPOSURE, -4); - 数据库:SQLite,用于存储成千上万种纽扣的特征指纹。
2.2 算法流程图
1 | graph TD |
3. 深入解析:漏斗式过滤算法
3.1 Level 1:极速粗筛 (SQL级)
在这一层,我们甚至不需要进行复杂的图像处理。所有的标准件在录入时,已经计算好了它的物理属性并存入 SQLite。
特征提取:
- **形状 (Shape)**:通过
approxPolyDP多边形逼近,判断是圆形(1)、矩形(2)、三角形(3)还是异形(4)。如果是圆形,利用minEnclosingCircle计算直径;如果是多边形,计算最长边或外接矩形。 - **颜色 (Color)**:提取 ROI 区域的 RGB 均值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void MainWindow::shape_detect(const std::vector<cv::Point>& contour, Detect_button& Dbtn) {
std::vector<cv::Point> approx;
double peri = cv::arcLength(contour, true);
cv::approxPolyDP(contour, approx, 0.02 * peri, true);
// 圆度计算公式:4 * pi * Area / Perimeter^2
double area = cv::contourArea(contour);
double perimeter = cv::arcLength(contour, true);
double circularity = 4 * CV_PI * area / (perimeter * perimeter);
if (circularity > 0.85) {
Dbtn.shape = 1; // 圆形
cv::Point2f center; float radius;
cv::minEnclosingCircle(contour, center, radius);
Dbtn.diameter = 2 * radius;
} else {
// 其他形状判别逻辑...
}
}- **形状 (Shape)**:通过
实现策略:利用 SQLite 的
WHERE子句直接过滤,速度是毫秒级的。1
2
3
4
5
6
7// 允许直径误差 ±10px,颜色误差 ±30
QList<int> filterByShapeDiameterColor(...) {
// ... SQL Logic ...
if (!inRange(map["diameter"].toDouble(), diameter, diameterTolerance)) continue;
if (!inRange(map["color_r"].toInt(), color_r, colorTolerance)) continue;
// ...
}
3.2 Level 2:形状指纹 (Hu矩)
通过 Level 1 后,可能剩下几十个颜色大小相近的纽扣(比如都是白色圆形)。这时需要对比微观轮廓(例如边缘有没有波浪纹)。
核心算法:
cv::matchShapes。原理:基于 **Hu Moments (Hu矩)**。Hu矩具有平移、旋转、缩放不变性,非常适合工业零件。
代码细节:
1
2
3
4
5
6
7
8
9
10// 1. 提取轮廓
cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// 2. 匹配度计算(越小越相似)
// 0.0 表示不使用特定参数
double score = cv::matchShapes(cnt1, cnt2, cv::CONTOURS_MATCH_I1, 0.0);
struct MatchScore { int id; double score; };
std::vector<MatchScore> scores;
// 遍历所有候选ID,计算相似度并排序
3.3 Level 3:视觉融合终判 (核心)
这是本系统的杀手锏。当候选者只剩下 3-5 个时,我们对图像进行像素级的深度比对。我设计了一个加权评分公式:
$$ Score = 0.2 \cdot S*{SSIM} + 0.3 \cdot S*{Hist} + 0.5 \cdot S_{ORB} $$
A. 结构相似度 (SSIM)
传统的 MSE (均方误差) 容易受整体光照影响。SSIM 分离了亮度、对比度、结构三个分量,更接近人眼的主观感受。
- 优化:对图像进行高斯模糊预处理,减少高频噪点对结构判断的干扰。
1 | double getSSIM(const cv::Mat &img1, const cv::Mat &img2) |
B. 旋转不变的模板匹配
纽扣可能是旋转的。我实现了一个三分法 (Ternary Search) 角度搜索算法,比暴力的 360 度循环快得多。
- 粗搜:每隔 60 度算一次。
- 精搜:确定大致范围后,利用三分法逼近最优旋转角。
1
2
3
4
5
6// 三分法找最佳旋转角
while (right - left > eps) {
double mid1 = left + (right - left) / 3.0;
double mid2 = right - (right - left) / 3.0;
// ... rotate and matchTemplate ...
}
C. ORB 特征点匹配
对于有花纹的纽扣,轮廓和颜色都一样,唯独中间的花纹不同。这时候 ORB (Oriented FAST and Rotated BRIEF) 就派上用场了。
- 阈值设定:使用 Hamming 距离 < 80 作为“好匹配点”的判定标准。
- 相似度计算:
GoodMatches / Min(KeyPoints1, KeyPoints2)。
1 | double orbSimilarity(const cv::Mat &img1, const cv::Mat &img2) |
4. 工程化实战细节
4.1 自动 ROI 捕捉
为了解放操作员,我做了一个简单的动态监测:
- 原理:持续计算画面中心区域的 RGB 均值
cv::mean。 - 触发:当
curr_mean与背景色的diff大于阈值(如10.0)时,认为有物体进入,自动锁定 ROI 区域并开始识别。1
2diff = cv::norm(first_color - curr_mean);
if(diff > 10.0) { /* 触发检测逻辑 */ }
4.2 数据库设计 (button_data_t)
不仅仅存储文本信息,更直接将二进制特征存入 BLOB 字段,方便后续可能的深度学习扩展。
1 | CREATE TABLE button_data_t ( |
4.3 颜色提取算法 (K-Means)
在录入新品时,如何自动判断它的“主色”?我没有简单取均值(会被背景干扰),而是使用了 K-Means 聚类。
- HSV 空间转掩码,提取非背景像素。
- 对像素进行 KMeans 聚类 (K=3)。
- 取占比最大的聚类中心作为该纽扣的主色。
1 | bool extractMainColor(const cv::Mat& srcImg, uchar& b, uchar& g, uchar& r) |
5. 总结与展望
这就好比我们在辨认一个人:
- Level 1:先看身高体重(几何),排除掉差异巨大的。
- Level 2:再看脸型轮廓(Hu矩/轮廓点数)。
- Level 3:最后仔细看五官细节(ORB/SSIM)。
通过这套组合拳,系统成功解决了相似纽扣的混淆问题。目前单次检测耗时控制在 200ms 以内(Qt多线程优化后),完全满足产线节拍要求。