《使用Qt Quick从零构建AI螺丝瑕疵检测系统》——6. 传统算法实战:用OpenCV测量螺丝尺寸
目录
一、概述
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 // 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::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模型。