简介:基于Qt与OpenCV的C++视觉检测系统项目包,以米粒为检测对象,实现目标计数与缺陷分类,适合毕业设计、课程设计或工业视觉入门项目的二次开发。系统涵盖图像采集、预处理、目标识别、特征提取与分类等完整流程,通过C++结合OpenCV完成图像读取与算法调用,并在Qt界面上直观展示结果。压缩包共14个文件,约5.32MB,主要包含C++源码(cpp/h)、Qt工程文件(pro/ui/qrc)、readme说明、license以及zbak备份文件,目录结构清晰,便于对照学习。已有86人学习下载。代码经过严格测试,可直接运行参考,既能帮助理解OpenCV中灰度化、二值化、滤波等预处理操作,也能掌握目标计数与缺陷分析函数的设计思路;界面布局和算法参数均可按需调整,适合快速搭建同类视觉检测演示系统。
1. 从米粒计数项目理解Qt与OpenCV的视觉检测分工
在粮油质检环节,数米粒和挑碎粒是重复度最高的动作。一个基于Qt与OpenCV的C++视觉检测系统,能把这两件事合并到一次图像处理流程里:用户打开一张米粒托盘图像,界面直接给出总数、碎粒数和异常形态提示。这类系统在工业上很常见,在毕业设计和课程设计里则是理解计算机视觉落地的理想载体。项目源码按“预处理—分割—计数—缺陷分析—界面显示”组织,后端是C++调用OpenCV算法库,前端用Qt绘制结果,源码包里mainwindow.cpp、mainwindow.h、RiceCounter.pro等文件很容易对照阅读。适合正在选OpenCV毕业设计方向、或者想用Qt封装图像算法的人。
2. 图像预处理与米粒分割:灰度化、二值化与连通域标记
图像采集得到的原图是彩色的,直接做计数会受到背景纹理和阴影干扰。预处理的目标是让米粒变成高对比的连通区域,这样才能用连通域标记统计数量。这里的核心操作是灰度化降维、滤波去噪、二值化分割,最后通过连通域提取每个米粒的位置和面积。
2.1 灰度化与高斯滤波
cv::Mat src = cv::imread("rice.png"); cv::Mat gray, blurred; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::GaussianBlur(gray, blurred, cv::Size(3, 3), 0);cvtColor将BGR三通道转为单通道灰度,像素值范围0到255,后续处理的计算量减少为原来的三分之一。GaussianBlur使用高斯核对图像做卷积,Size(3, 3)是核尺寸,0表示标准差自动从核大小推导。核越大细节丢失越多,对米粒这种边缘圆滑的目标,3x3或5x5已经足够。与中值滤波相比,高斯滤波对高斯噪声的压制更好,且不会过度破坏边缘;如果图像存在明显的椒盐噪声,可以换成cv::medianBlur。灰度化之后,米粒和背景的灰度差异是否足够大,直接决定了二值化能否稳定工作。
2.2 二值化阈值选择与形态学处理
cv::Mat binary; cv::threshold(blurred, binary, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3)); cv::morphologyEx(binary, binary, cv::MORPH_OPEN, kernel);THRESH_OTSU表示让OpenCV自动计算阈值,省去手动调节的麻烦。如果米粒像素比浅色托盘更暗,就用THRESH_BINARY_INV把米粒置为白色前景;如果用THRESH_BINARY,背景会变成白色,前景保持黑色,后续连通域统计就会把背景当成目标。形态学开运算是先腐蚀后膨胀,能够断开米粒之间细小的粘连,同时去掉孤立的噪点。kernel选择3x3矩形结构元素,操作次数少,对面积损失小。经过这一步,粘连不太严重的米粒基本都能被分成独立区域。
| 形态学操作 | 函数 | 作用 |
|---|---|---|
| 腐蚀 | cv::erode | 缩小前景,去除小噪点 |
| 膨胀 | cv::dilate | 扩大前景,填补小孔 |
| 开运算 | morphologyEx(MORPH_OPEN) | 先腐蚀后膨胀,断开粘连 |
| 闭运算 | morphologyEx(MORPH_CLOSE) | 先膨胀后腐蚀,填补空洞 |
2.3 连通域标记提取目标轮廓
cv::Mat labels, stats, centroids; int num = cv::connectedComponentsWithStats(binary, labels, stats, centroids, 8, CV_32S); int riceCount = 0; for (int i = 1; i < num; i++) { int area = stats.at<int>(i, cv::CC_STAT_AREA); if (area > 30) riceCount++; }connectedComponentsWithStats返回的num包含背景标签0,所以循环从1开始遍历。stats每一行依次存放x、y、width、height、area,其中area表示该连通域的像素数量。area小于30的区域大概率是噪声,直接忽略。注意labels必须使用CV_32S类型,这是函数签名要求的固定格式。这一步完成后,每个米粒已经被赋予独立标签,但还没有拿到轮廓坐标。后续做缺陷分析时,需要从labels生成掩膜,或者复制二值图后调用cv::findContours提取轮廓,因为findContours会修改输入图像。
提示:如果米粒之间严重重叠,连通域会把多个米粒当成一个大目标,计数会明显偏少。这种情况下要先考虑分水岭分割,后面会给出一个简单的验证方法。
3. 米粒计数与缺陷分析:从连通域统计到形状特征
计数只是第一步,缺陷分析需要从每个连通域提取形状和灰度特征,然后用规则或分类器判定缺陷类型。此部分对应源码中的目标分析函数,它和计数函数共享同一份预处理结果,但需要额外读取原始灰度图,因为颜色和纹理信息在二值图里已经丢失。
3.1 计数逻辑与面积阈值过滤
int countRice(const cv::Mat& binary) { cv::Mat labels, stats, centroids; int num = cv::connectedComponentsWithStats(binary, labels, stats, centroids, 8, CV_32S); std::vector<int> areas; for (int i = 1; i < num; i++) { areas.push_back(stats.at<int>(i, cv::CC_STAT_AREA)); } int count = 0; for (int a : areas) { if (a > 50) count++; } return count; }面积阈值50是一个示例值,实际要根据相机分辨率和拍摄距离调整。更可靠的做法是统计所有连通域面积的中位数,把小于中位数30%的区域判为碎粒或噪声,这样可以避免固化阈值。如果托盘上存在灰尘或米糠,面积较小的噪点容易被误计,因此面积下限的选取直接影响计数准确率。阈值太小会把噪点算成米粒,阈值太大又会丢掉碎粒,需要结合待分析批次的实际颗粒大小标定。
3.2 缺陷特征:面积、长宽比、圆度与灰度方差
| 特征 | 计算方式 | 典型缺陷 |
|---|---|---|
| 面积 | stats.at (i, CC_STAT_AREA) | 碎粒(面积偏小) |
| 长宽比 | 外接矩形宽高比 max/min | 长形异形粒 |
| 圆度 | 4π * 面积 / 周长² | 半粒、破损粒 |
| 灰度均值 | 掩膜内平均灰度 | 霉变、异色 |
| 灰度方差 | 掩膜内灰度标准差 | 斑点、双色粒 |
double calcRoundness(const std::vector<cv::Point>& contour, int area) { double perimeter = cv::arcLength(contour, true); return 4 * CV_PI * area / (perimeter * perimeter); } void analyzeDefects(const cv::Mat& binary, const cv::Mat& gray, std::vector<std::string>& labels) { cv::Mat labelsMat, stats, centroids; int num = cv::connectedComponentsWithStats(binary, labelsMat, stats, centroids, 8, CV_32S); std::vector<std::vector<cv::Point>> contours; cv::Mat binaryCopy = binary.clone(); cv::findContours(binaryCopy, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); for (size_t i = 0; i < contours.size(); i++) { int area = stats.at<int>(i + 1, cv::CC_STAT_AREA); cv::Rect box = cv::boundingRect(contours[i]); double aspect = std::max(box.width, box.height) * 1.0 / std::min(box.width, box.height); double roundness = calcRoundness(contours[i], area); cv::Mat mask = cv::Mat::zeros(binary.size(), CV_8UC1); cv::drawContours(mask, contours, (int)i, cv::Scalar(255), -1); cv::Mat region; gray.copyTo(region, mask); cv::Scalar mean, stddev; cv::meanStdDev(region, mean, stddev, mask); if (area < 80) labels.push_back("broken"); else if (aspect > 3.0) labels.push_back("long"); else if (roundness < 0.6) labels.push_back("broken"); else if (stddev(0) > 30) labels.push_back("spotted"); else labels.push_back("good"); } }圆度接近1说明轮廓接近圆形,普通米粒呈椭圆,圆度一般在0.8左右;半粒或破损粒轮廓不规则,圆度明显下降。灰度方差对黑点、霉斑敏感,但光照不均会导致误判,所以最好先对灰度图做归一化处理。这里findContours操作的是binary.clone(),因为原函数会破坏输入图像。需要注意的是,contours的索引与stats的标签不一定按同样顺序对齐,严格做法是通过labels掩膜确认每个连通域的编号;毕业设计要求下,可以使用轮廓外接矩形的中心点与centroids做匹配。缺陷判定建议按顺序执行:先查面积,再查长宽比,再查圆度,最后查灰度方差,避免多个阈值之间冲突。
3.3 缺陷分类:规则阈值还是轻量分类器
规则阈值的优点是逻辑直白、答辩好解释,缺点是阈值需要手动调整。如果希望自动分类,可以提取特征向量(area、aspect、roundness、mean、stddev)后训练OpenCV ml模块里的SVM或随机森林模型。对毕业设计来说,规则分类已经足够,但建议把阈值集中放在一个配置结构中,方便在界面上调节。
struct DefectParams { int minArea; double maxAspect; double minRoundness; double maxGrayStddev; }; void initDefectParams(DefectParams& p) { p.minArea = 80; p.maxAspect = 3.0; p.minRoundness = 0.6; p.maxGrayStddev = 30.0; }这样在MainWindow的初始化函数中调用initDefectParams加载一次参数,后续analyzeDefects只读取结构体,不再把魔法数字散落在算法代码里。后续如果要适配不同相机分辨率,只需要修改这一处即可。
4. Qt界面集成OpenCV:从Mat到QImage的显示与交互
算法写完,接下来要把结果显示在界面上。Qt和OpenCV之间的主要障碍是像素格式:OpenCV的Mat默认是BGR,而QImage支持RGB和ARGB。即使一张图在算法里处理得再好,如果格式转换出错,界面显示也会出现颜色异常或图像撕裂。
4.1 在RiceCounter.pro中配置OpenCV
QT += core gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = RiceCounter TEMPLATE = app SOURCES += main.cpp mainwindow.cpp HEADERS += mainwindow.h FORMS += mainwindow.ui INCLUDEPATH += D:/opencv/build/include LIBS += -LD:/opencv/build/x64/mingw/lib \ -lopencv_core410 \ -lopencv_imgproc410 \ -lopencv_imgcodecs410库文件名后面的数字是OpenCV版本号,410对应4.10版,实际使用时必须改成自己安装的版本。编译器也要匹配:MinGW版Qt链接MinGW目录下的lib,MSVC版Qt链接vc15或vc16目录下的lib,混用会报大量未定义的符号。另一个常见坑是程序启动时提示找不到opencv_world410.dll,解决办法是把该dll所在目录加入系统PATH,或者直接把dll复制到exe同级目录。
4.2 Mat转QImage的像素格式
QImage matToQImage(const cv::Mat& mat) { if (mat.type() == CV_8UC3) { cv::Mat rgb; cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); return QImage(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888).copy(); } else if (mat.type() == CV_8UC1) { return QImage(mat.data, mat.cols, mat.rows, mat.step, QImage::Format_Grayscale8).copy(); } return QImage(); }cvtColor把BGR转成RGB,否则显示出来红蓝通道互换。构造函数里的rgb.step非常重要,因为Mat每行末尾可能存在字节对齐,不使用step会导致图像拉伸或错位。最后的copy()必须保留,因为QImage只是保存了data指针,Mat析构后内存被释放,界面绘制时就会读到野指针。实际开发中,把所有可能的Mat类型转换集中到一个函数里是通用做法,这里只列出最常见的CV_8UC3和CV_8UC1。
| Mat类型 | 原始排列 | QImage格式 |
|---|---|---|
| CV_8UC3 | BGR | Format_RGB888(需cvtColor) |
| CV_8UC1 | 灰度 | Format_Grayscale8 |
| CV_8UC4 | BGRA | Format_ARGB32(需cvtColor到RGBA) |
4.3 在按钮槽函数中串联处理流程
void MainWindow::on_openButton_clicked() { QString file = QFileDialog::getOpenFileName( this, "选择图片", "", "Images (*.png *.jpg *.bmp)"); if (file.isEmpty()) return; cv::Mat src = cv::imread(file.toUtf8().toStdString()); if (src.empty()) { QMessageBox::warning(this, "错误", "图片无法读取"); return; } cv::Mat gray, binary; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::GaussianBlur(gray, gray, cv::Size(3, 3), 0); cv::threshold(gray, binary, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_OTSU); int count = countRice(binary); std::vector<std::string> labels; analyzeDefects(binary, gray, labels); ui->label_count->setText(QString("米粒总数: %1").arg(count)); ui->label_defect->setText(QString("缺陷数量: %1").arg(labels.size())); }这里把处理流程直接写在槽函数里,方便阅读,实际项目建议把算法封装成RiceDetector类,槽函数只负责调用和刷新UI。中文路径问题值得注意:cv::imread在Windows下无法正确处理中文路径,toUtf8能缓解一部分问题,最稳妥的方式是用QFile读取字节流后交给cv::imdecode解码。另外countRice和analyzeDefects内部都做了面积过滤,两边的阈值必须保持一致,否则界面上显示的“总数”和“缺陷数”会互相矛盾。
5. 验证计数精度与扩展缺陷分类的实用技巧
5.1 用合成图像验证算法边界
做计数算法时,很难拿到带标准答案的米粒图像。可以用OpenCV自己画一组已知数量的圆形,验证预处理和计数流程是否存在明显错误。
cv::Mat test(512, 512, CV_8UC1, cv::Scalar(255)); for (int i = 0; i < 40; i++) { int x = rand() % 420 + 40; int y = rand() % 420 + 40; cv::circle(test, cv::Point(x, y), 12, cv::Scalar(0), -1); }画40个半径为12像素的圆,跑完整个流程看计数结果是否为40。再把一部分圆两两重叠,找出系统在什么重叠程度下开始失效。这个实验在毕业设计答辩时很有说服力,能够直接展示算法的容错边界,而不是只说“准确率很高”。
5.2 通过标定获得真实尺寸
缺陷阈值以像素为单位时会随拍摄距离变化。可以在米粒旁放一枚硬币,检测硬币外接矩形的宽度,然后计算像素与毫米的换算比例:mm_per_pixel = 已知硬币宽度 / 实际像素宽度。之后把面积阈值从像素值折算成真实平方毫米,这样更换相机距离时不需要重新标定所有参数。
5.3 扩展缺陷类型和界面参数化
源码里的缺陷判定规则目前只有broken、long、spotted三类,扩展空间还很大。比如加入“腹白”检测,可以在掩膜内统计灰度值小于某个阈值的像素比例;加入“异色”检测,可以比较掩膜内的RGB均值与整幅图均值。把DefectParams中的阈值绑定到ui界面的QSpinBox或QSlider上,用户调节时重新调用一次analyzeDefects,缺陷判定结果马上刷新,这样就能在不重新编译代码的情况下适配不同批次的大米。例如把minArea从80下调到40,系统就能识别更小的碎粒,而长宽比阈值从3.0改成2.5,则会把更接近椭圆形态的异形粒也纳入统计。
本文还有配套的精品资源,点击获取