前言:在机器视觉项目中,最怕的不是识别不出来,而是“误判”。特别是在纽扣、螺丝等标准化零件的检测中,两个批次的产品可能只有微小的纹理差异。本文记录了我如何通过“多维度视觉融合”的思路,利用 Qt 和 OpenCV 构建一套抗干扰能力强的检测系统。

1. 痛点:单一维度的局限性

在项目初期,我尝试过简单的 **Template Matching (模板匹配)**,但很快遇到了瓶颈:

  1. 光照敏感:现场灯光稍有波动,像素级匹配的得分就断崖式下跌。
  2. 形变干扰:纽扣在传送带上可能会通过旋转,简单的 matchTemplate 对旋转不具备不变性。
  3. 计算量大:如果对全图进行多角度旋转匹配,CPU 占用率直接飙升,无法满足实时性(<100ms)要求。

因此,我决定采用“漏斗式过滤”架构,从几何、颜色、轮廓、纹理四个维度层层递进,既保证了速度,又提高了精度。

2. 系统核心架构

2.1 硬件与环境

  • 开发环境:Qt 5.14 (MSVC 2017 64bit) + OpenCV 4.5.1
  • 相机设置:固定曝光(关键!)。代码中显式关闭了自动曝光:
    1
    2
    3
    cap.set(cv::CAP_PROP_AUTO_EXPOSURE, 0);
    // 实测设置为 -4 左右适合室内打光环境
    // cap.set(cv::CAP_PROP_EXPOSURE, -4);
  • 数据库:SQLite,用于存储成千上万种纽扣的特征指纹。

2.2 算法流程图

1
2
3
4
5
6
7
8
graph TD
A[摄像头采集] --> B{ROI区域变动?}
B -- 是 --> C[触发检测]
B -- 否 --> A
C --> D[Level 1: 几何/颜色粗筛]
D -- 命中ID列表 --> E[Level 2: 轮廓形状精筛]
E -- Top N 候选 --> F[Level 3: 视觉融合终判]
F --> G[输出最终结果]

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
    19
    void 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 {
    // 其他形状判别逻辑...
    }
    }
  • 实现策略:利用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
double getSSIM(const cv::Mat &img1, const cv::Mat &img2)
{
const double C1 = 6.5025, C2 = 58.5225;

cv::Mat I1, I2;
img1.convertTo(I1, CV_32F);
img2.convertTo(I2, CV_32F);

// 高斯模糊减少噪声干扰
cv::GaussianBlur(I1, mu1, cv::Size(11, 11), 1.5);
cv::GaussianBlur(I2, mu2, cv::Size(11, 11), 1.5);

// 计算 Sigma (方差/协方差)
// t3: 分子, t1: 分母
// ssim_map = t3 / t1;
cv::Scalar mssim = cv::mean(ssim_map);
return mssim.val[0];
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
double orbSimilarity(const cv::Mat &img1, const cv::Mat &img2)
{
cv::Ptr<cv::ORB> orb = cv::ORB::create();
std::vector<cv::KeyPoint> kp1, kp2;
cv::Mat des1, des2;
orb->detectAndCompute(img1, cv::Mat(), kp1, des1);
orb->detectAndCompute(img2, cv::Mat(), kp2, des2);

if (des1.empty() || des2.empty()) return 0.0;

// 使用汉明距离进行特征点匹配
cv::BFMatcher matcher(cv::NORM_HAMMING, true);
std::vector<cv::DMatch> matches;
matcher.match(des1, des2, matches);

int good_matches = 0;
for (auto &m : matches) {
if (m.distance < 80) good_matches++;
}
return (double)good_matches / std::max(kp1.size(), kp2.size());
}

4. 工程化实战细节

4.1 自动 ROI 捕捉

为了解放操作员,我做了一个简单的动态监测:

  • 原理:持续计算画面中心区域的 RGB 均值 cv::mean
  • 触发:当 curr_mean 与背景色的 diff 大于阈值(如10.0)时,认为有物体进入,自动锁定 ROI 区域并开始识别。
    1
    2
    diff = cv::norm(first_color - curr_mean);
    if(diff > 10.0) { /* 触发检测逻辑 */ }

4.2 数据库设计 (button_data_t)

不仅仅存储文本信息,更直接将二进制特征存入 BLOB 字段,方便后续可能的深度学习扩展。

1
2
3
4
5
6
7
8
9
CREATE TABLE button_data_t (
id INTEGER PRIMARY KEY,
shape INTEGER, -- 1:圆, 2:矩形...
diameter REAL, -- 物理直径
point INTEGER, -- 特征点数(如孔数)
outline TEXT, -- 轮廓点序列化字符串
feature BLOB, -- 预留:深度学习特征向量
image_mat BLOB -- 原图压缩数据
);

4.3 颜色提取算法 (K-Means)

在录入新品时,如何自动判断它的“主色”?我没有简单取均值(会被背景干扰),而是使用了 K-Means 聚类

  1. HSV 空间转掩码,提取非背景像素。
  2. 对像素进行 KMeans 聚类 (K=3)。
  3. 取占比最大的聚类中心作为该纽扣的主色。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool extractMainColor(const cv::Mat& srcImg, uchar& b, uchar& g, uchar& r)
{
// ... 1. 提取前景像素到 pixels 向量 ...

// 2. KMeans 聚类
int clusterCount = 3;
cv::Mat samples((int)pixels.size(), 3, CV_32F, pixels.data());
cv::Mat labels, centers;
cv::kmeans(samples, clusterCount, labels,
cv::TermCriteria(cv::TermCriteria::EPS+cv::TermCriteria::COUNT, 10, 1.0),
3, cv::KMEANS_PP_CENTERS, centers);

// 3. 统计最大占比颜色
std::vector<int> counts(clusterCount, 0);
for(int i = 0; i < labels.rows; ++i) counts[labels.at<int>(i)]++;
int maxIdx = std::max_element(counts.begin(), counts.end()) - counts.begin();

b = (uchar)centers.at<float>(maxIdx, 0);
g = (uchar)centers.at<float>(maxIdx, 1);
r = (uchar)centers.at<float>(maxIdx, 2);
return true;
}

5. 总结与展望

这就好比我们在辨认一个人:

  1. Level 1:先看身高体重(几何),排除掉差异巨大的。
  2. Level 2:再看脸型轮廓(Hu矩/轮廓点数)。
  3. Level 3:最后仔细看五官细节(ORB/SSIM)。

通过这套组合拳,系统成功解决了相似纽扣的混淆问题。目前单次检测耗时控制在 200ms 以内(Qt多线程优化后),完全满足产线节拍要求。