一、概述
1.1 背景介绍:从“看见”到“看懂”
在上一篇文章中,我们成功地为应用程序安装了“眼睛”——集成了OpenCV并实现了图像的加载与显示。现在,我们的程序已经能够“看见”螺丝了。然而,仅仅看见是不够的,机器视觉的核心价值在于能像人一样“看懂”图像,从中提取出有用的信息。
本篇文章的核心任务,就是实现从“看见”到“看懂”的第一次跨越。我们将利用OpenCV强大的图像处理能力,编写第一个真正的视觉算法——自动测量螺丝的尺寸。这是一种经典的、非接触式的测量应用,在工业生产中非常常见。通过这个实战,读者将直观地感受到传统视觉算法是如何通过一系列步骤,从像素中提取出几何信息的。
1.2 学习目标
通过本篇的学习,读者将能够:
- 掌握图像预处理的基本技术,如灰度转换和二值化,这是让计算机能够“理解”图像的关键步骤。
- 学习并实践OpenCV中一个核心的算法——轮廓发现(Contour Finding)。
- 利用发现的轮廓,计算其最小外接矩形(Min Area Rect),从而精确地获得螺丝的长度和宽度。
- 将计算出的尺寸信息和判定结果,通过信号传递回QML界面进行显示。
二、图像预处理:让目标更突出
计算机不像人眼那样智能,一张彩色的原始图像对它来说只是一堆复杂的RGB像素值。为了让计算机能够轻松地识别出我们感兴趣的目标(螺丝),必须先对图像进行预处理,其核心目的就是简化图像,增强目标特征,减弱背景干扰。
【例6-1】 图像灰度化与二值化。
1. 修改Backend (backend.cpp)
我们将继续在Backend::startScan()
函数中进行修改。在加载图像之后,增加灰度转换和二值化的步骤。
// backend.cpp
#include "backend.h"
// ... (之前的include保持不变)
// ... (matToQImage辅助函数保持不变)
Backend::Backend(QObject *parent) : QObject(parent) {}
void Backend::startScan()
{
// ... (加载图像的代码保持不变)
QString imagePath = QDir::currentPath() + "/../../dataset/screw/test/scratch_head/000.png";
cv::Mat sourceMat = cv::imread(imagePath.toStdString());
if (sourceMat.empty()) { /* ... 错误处理 ... */ return; }
emit statusMessageChanged("图像加载成功,开始预处理...");
// --- 1. 灰度转换 ---
// 将BGR彩色图像转换为单通道的灰度图像
cv::Mat grayMat;
cv::cvtColor(sourceMat, grayMat, cv::COLOR_BGR2GRAY);
// --- 2. 二值化 ---
// 将灰度图像转换为只有黑白两种颜色的二值图像
// 此处使用OTSU方法自动寻找最佳阈值
cv::Mat binaryMat;
cv::threshold(grayMat, binaryMat, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU);
// 为了在UI上直观展示处理结果,我们暂时只显示二值化后的图像
QImage imageQ = matToQImage(binaryMat);
if (imageQ.isNull()){ /* ... 错误处理 ... */ return; }
m_imageProvider->updateImage(imageQ);
emit imageReady("screw_processed");
emit statusMessageChanged("图像预处理完成!");
}
2. 运行结果
再次运行程序并点击“开始检测”,现在界面上显示的不再是原始的彩色螺丝图片,而是一张清晰的黑白轮廓图。在这张图中,螺丝主体是白色(像素值为255),背景是黑色(像素值为0),目标物被完美地凸显了出来。
关键代码分析:
(1) cv::cvtColor(...)
: OpenCV中用于色彩空间转换的函数。cv::COLOR_BGR2GRAY
是一个预定义的常量,表示从BGR色彩空间转换到灰度空间。
(2) cv::threshold(...)
: 二值化函数。它将图像中所有像素值大于阈值的像素设为一个值(如255),小于等于阈值的设为另一个值(如0)。
(3) THRESH_BINARY_INV
: 表示反向二值化。因为我们的螺丝比背景暗,普通二值化后螺丝会变黑。使用反向二值化,可以让暗的螺丝变成白色,方便后续处理。
(4) THRESH_OTSU
: 这是一个非常智能的标志。当使用它时,我们传递的阈值参数(这里是0)会被忽略,threshold
函数会自动计算出一个最优的全局阈值来分割前景和背景。这对于光照不均的场景非常有效。
三、轮廓发现与尺寸测量
经过预处理后,图像中的螺丝已经变成了一个清晰的白色区域。现在,我们可以让OpenCV去“寻找”这个白色区域的边界,这个边界就是轮廓。
【例6-2】 寻找轮廓并计算最小外接矩形。
1. 修改Backend (backend.cpp)
在二值化之后,加入轮廓发现和几何计算的逻辑。
// backend.cpp
// ...
#include <vector> // C++标准库,用于存储轮廓
// ... (matToQImage辅助函数)
void Backend::startScan()
{
// ... (加载图像、灰度化、二值化的代码保持不变) ...
cv::Mat sourceMat = cv::imread(...);
cv::Mat binaryMat;
// ... cv::threshold(...) ...
emit statusMessageChanged("预处理完成,开始寻找轮廓...");
// --- 3. 轮廓发现 ---
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binaryMat, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// 假设最大轮廓就是我们的螺丝
if (contours.empty()) {
emit statusMessageChanged("错误:未在图像中找到任何轮廓!");
return;
}
// 寻找面积最大的轮廓
double maxArea = 0;
int maxAreaIdx = -1;
for (int i = 0; i < contours.size(); i++) {
double area = cv::contourArea(contours[i]);
if (area > maxArea) {
maxArea = area;
maxAreaIdx = i;
}
}
if (maxAreaIdx == -1) {
// ... 错误处理
return;
}
// --- 4. 尺寸测量 ---
// 计算最大轮廓的最小外接矩形
cv::RotatedRect rotatedRect = cv::minAreaRect(contours[maxAreaIdx]);
// 获取矩形的尺寸。注意:width和height不一定是物理的长和宽
cv::Size2f rectSize = rotatedRect.size;
float width = std::min(rectSize.width, rectSize.height);
float length = std::max(rectSize.width, rectSize.height);
qDebug() << "Measured dimensions (pixels): Length =" << length << ", Width =" << width;
QString resultMessage = QString("测量结果: 长度= %1 px, 宽度= %2 px").arg(length, 0, 'f', 2).arg(width, 0, 'f', 2);
// --- 5. 结果可视化 ---
// 为了直观展示,我们在原始彩色图上把轮廓和矩形画出来
// 获取矩形的四个顶点
cv::Point2f vertices[4];
rotatedRect.points(vertices);
// 将轮廓和矩形画在sourceMat上
cv::drawContours(sourceMat, contours, maxAreaIdx, cv::Scalar(0, 255, 0), 2); // 绿色轮廓
for (int i = 0; i < 4; i++) {
cv::line(sourceMat, vertices[i], vertices[(i + 1) % 4], cv::Scalar(0, 0, 255), 2); // 红色矩形
}
// 将带有绘制结果的图像发送到UI
QImage imageQ = matToQImage(sourceMat);
m_imageProvider->updateImage(imageQ);
emit imageReady("screw_processed");
emit statusMessageChanged(resultMessage);
}
2. 运行结果
点击“开始检测”后,界面上将显示原始的彩色螺丝图片,但上面已经叠加了绿色的轮廓线和红色的最小外接矩形。同时,状态栏会显示出计算出的像素尺寸。
关键代码分析:
(1) cv::findContours(...)
: OpenCV中用于寻找轮廓的核心函数。
- binaryMat
: 输入必须是二值图像。
- contours
: 输出参数,一个存储向量的向量集(std::vector<std::vector<cv::Point>>
),用于存储所有找到的轮廓。每个轮廓本身是一个由点(cv::Point
)组成的向量。
- cv::RETR_EXTERNAL
: 表示只检测最外层的轮廓,忽略内部的孔洞,这对于我们的需求是最高效的。
- cv::CHAIN_APPROX_SIMPLE
: 一种轮廓点的压缩算法,只保留轮廓的端点,可以节省大量内存。
(2) cv::contourArea(...)
: 计算一个轮廓所包围的面积。我们通过遍历所有轮廓并比较面积,来找到最大的那个,并假定它就是我们的目标螺丝。
(3) cv::minAreaRect(...)
: 计算并返回一个包围轮廓点的、面积最小的旋转矩形(cv::RotatedRect
)。这个矩形能够紧密地贴合倾斜的目标。
(4) rotatedRect.size
: cv::RotatedRect
对象包含中心点、角度和尺寸(cv::Size2f
)信息。size
的width
和height
不保证哪个是长哪个是短,因此我们用std::min
和std::max
来获取物理上的宽度和长度。
(5) cv::drawContours(...)
和 cv::line(...)
: 用于在图像上进行绘制的函数,非常适合在调试和结果展示时,将算法的中间结果可视化。
四、总结与展望
在本篇文章中,我们成功地实现了第一个真正的机器视觉算法。通过图像预处理(灰度、二值化)、核心算法(轮廓发现)和 几何计算(最小外接矩形)这一经典流程,我们让程序从一张普通的图片中精确地提取出了螺丝的像素尺寸。
我们不仅学习了几个关键的OpenCV函数,更重要的是,我们建立了一套解决此类问题的思维框架。然而,读者可能已经发现,这种方法虽然能测量尺寸,但对于识别表面划痕、锈斑等纹理类、无固定形状的瑕疵却无能为力。
这正是传统视觉算法的局限性所在,也为我们引入更强大的AI技术埋下了伏笔。在下一篇文章【《使用Qt Quick从零构建AI螺丝瑕疵检测系统》——7. AI赋能(上):训练你自己的YOLOv8瑕疵检测模型】中,我们将进入深度学习领域,亲手训练一个能够“认识”多种瑕疵的AI模型。