ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Qt5与OpenCV的图像视频处理工具开发实战

基于Qt5与OpenCV的图像视频处理工具开发实战 简介基于QT5与OpenCV实现的图像处理及视频处理软件大作业项目面向正在学习Qt图形界面编程和OpenCV视觉应用的在校学生或开发者可作为课程设计、毕业设计或入门实践参考。功能全面覆盖图像处理的灰度化、均值滤波、边缘检测、伽马变换、旋转镜像、亮度调节、二值化、对比度与饱和度调节、色彩调整、水印及图片保存以及视频处理的二值化、边缘检测、灰度化、平滑、局部马赛克与缩放并额外加入界面美化和中英翻译支持。资源包共69个文件、约11.57MB包含cpp/h源码、ui界面文件、qrc资源、qss样式、png/jpg图像素材、qm/ts翻译文件、dll运行库、gif动图及flv视频素材便于对照界面与代码进行学习。已有11113人浏览学习源码基于QT 5.12.2与OpenCV 4.0.1编写目录结构清晰可直接编译运行帮助快速理解图像视频处理算法与Qt项目开发流程。1. 先解决环境问题这套项目最容易跪在“版本组合”上很多人做这类图像处理桌面软件第一反应是赶紧把代码跑起来结果卡在环境上三天出不了门。QT5加OpenCV的组合网上教程不少但真正能让你少走弯路的版本搭配其实就那么几种。根据我这些年做课程设计、帮同事救火的经历最稳妥的方案是QT 5.9.9 或 5.12.0 配 OpenCV 4.5.5编译器选 MSVC 2017 64位。这套组合我前后在不同电脑上装过七八次基本一次过不会出现什么奇怪的链接报错。为什么强调JDK版本这种细节因为OpenCV 4系列对C11标准支持非常完整QT 5.12及以上版本对高DPI屏幕的适配也好很多你在2K、4K屏幕上做界面设计控件不会糊成一团。有些教程推荐QT 5.6配OpenCV 3.x那套实在太老了且不说OpenCV 3的一些API写法跟现代代码差异很大单是摄像头读取那块的兼容性就够你折腾一阵子。环境配置的核心就两件事一是路径二是环境变量。下载OpenCV后解压到一个纯英文路径比如D:\Libs\opencv\。路径里千万别出现中文也别出现空格这是无数人踩过的坑。配置环境变量Path把D:\Libs\opencv\build\x64\vc15\bin加进去。注意用的是vc15对应VS2017别选成了vc14或vc16那个是VS2015/2019的具体看你用的编译器。在QT项目的.pro文件里添加下面这几行INCLUDEPATH D:/Libs/opencv/build/include LIBS D:/Libs/opencv/build/x64/vc15/lib/opencv_world455.lib Debug模式需要额外加一个 LIBS D:/Libs/opencv/build/x64/vc15/lib/opencv_world455d.lib这里有一个非常重要的细节Debug和Release版本链接的lib库后缀不同。带d的是Debug库不带的是Release库。如果你用Debug模式编译却链接了Release库编译时可能不报错但运行时会出现各种莫名其妙的内存错误反过来也一样。所以最省事的做法是在.pro文件里就用条件判断区分开来CONFIG(debug, debug|release) { LIBS D:/Libs/opencv/build/x64/vc15/lib/opencv_world455d.lib } else { LIBS D:/Libs/opencv/build/x64/vc15/lib/opencv_world455.lib }还有一个特别容易忽略的点运行时DLL的位置。就算你环境变量配好了有些电脑上QT Creator启动程序时还是可能提示找不到opencv_world455.dll。这时候不用怀疑配置有问题直接把这个dll文件复制到C:\Windows\System32里或者复制到你的exe同级目录下问题就消失了。我个人更推荐复制到exe目录因为这样程序发给别人用的时候只要带上这个dll就能跑。2. 主界面框架设计菜单栏、工具栏、显示区的分工逻辑这类图像视频处理软件界面说复杂也复杂说简单其实核心就几个区域一个放原始图像/视频的显示区、一个放处理结果的显示区、一个控制面板区。我建议用QMainWindow作为主窗口不要直接在QWidget里堆控件因为菜单栏、状态栏这些MainWindow自带的框架到了后期想加功能时省太多事了。菜单栏这边我一般设计成文件、图像处理、视频处理、帮助四个一级菜单。文件菜单放打开图像、打开视频、保存结果图像处理菜单放灰度化、二值化、边缘检测、膨胀腐蚀这些算法动作视频处理菜单放打开摄像头、开始/暂停、帧差法这些功能。工具栏就放几个最常用的快捷按钮比如打开图片、灰度化、保存结果这几个。显示区我用两个QLabel控件左右排列左边显示原图右边显示处理后的结果。为什么不用QGraphicsView因为对于课程设计级别的软件QLabel加setPixmap的读写已经足够简单QGraphicsView虽然功能强大但牵扯到场景、视图、图元那一套概念学习成本不低。当然如果你后续想做缩放拖拽、画矩形框这些交互操作那就得换成QGraphicsView自己重写mousePressEvent了那是后话。控制面板区放在窗口右侧用QDockWidget挂载。面板里放几个滑块QSlider和数值显示QLabel用来调节亮度、对比度、阈值等参数。每个滑块旁边标注清楚含义这些小细节能让界面看起来专业很多而不是控件随便往上一堆就算完事。布局方面注意给出合理的最小尺寸限制窗口大小固定在一个值是比较省心的做法。因为一旦用户把窗口拉伸得过大两个QLabel撑满整个界面图片放上去会变形。可以在QLabel上设置setAlignment(Qt::AlignCenter)配合setScaledContents(false)再加上setMinimumSize(1,1)这样图片会按比例居中显示不会拉伸变糊。这一部分我的经验是先把界面搭扎实再写算法逻辑。因为界面是别人看你项目的第一印象而且界面框架一旦定型后面改写成本很高。很多新手喜欢先憋算法再拼界面最后程序跑起来了界面却杂乱无章答辩时印象分打了不少折扣。3. Mat与QImage之间的转换整个项目最容易出bug的一环做图像处理的人都知道OpenCV读进来是cv::MatQT显示要用QPixmap或QImage中间的转换环节要是写不好轻则图像色彩怪怪的重则直接内存崩溃。这个转换问题说大不大说小不小但十个人里至少有七个人在这上面出过错。核心转换代码如下// cv::Mat 转 QImage QImage matToQImage(const cv::Mat mat) { switch (mat.type()) { case CV_8UC3: // 三通道彩色图像 { QImage image(mat.data, mat.cols, mat.rows, mat.step, QImage::Format_RGB888); return image.rgbSwapped(); // BGR转RGB这步是关键 } case CV_8UC1: // 单通道灰度图像 { QImage image(mat.data, mat.cols, mat.rows, mat.step, QImage::Format_Grayscale8); return image; } default: qWarning() 不支持的Mat类型; return QImage(); } }rgbSwapped()这行必须要有。OpenCV里的默认彩色通道顺序是BGR蓝绿红而QT是RGB红绿蓝如果你不转换出来的图像红色和蓝色是反的——人脸看起来跟阿凡达似的。这个函数就干一件事把BGR通道顺序调换成RGB一行代码解决但漏掉它的人真不少。还有一个容易被忽略的隐患cv::Mat和QImage的内存共享问题。上面这段代码里QImage直接用了mat.data的指针也就是说QImage和Mat共享同一块内存。如果Mat是函数的局部变量函数结束后Mat被销毁QImage就成了野指针运行时大概率崩溃。解决办法是在返回QImage之前调用.copy()QImage image(mat.data, mat.cols, mat.rows, mat.step, QImage::Format_RGB888); return image.rgbSwapped().copy(); // 深拷贝切断与Mat的生命周期关联反过来QImage转Mat也是同样的道理cv::Mat qImageToMat(const QImage image) { if (image.isNull()) { return cv::Mat(); } if (image.format() QImage::Format_RGB32) { cv::Mat mat(image.height(), image.width(), CV_8UC4, (void*)image.bits(), image.bytesPerLine()); return mat.clone(); } // 其他格式需要先转换 QImage fixed image.convertToFormat(QImage::Format_RGB888); cv::Mat mat(fixed.height(), fixed.width(), CV_8UC3, (void*)fixed.bits(), fixed.bytesPerLine()); return mat.clone(); }这里clone()的原因和上面一样保证返回值拥有独立的内存空间不依赖原QImage的存活时间。我的建议是把这两个转换函数封装成一个独立的工具类或者工具头文件不要在业务代码里到处重复写。因为一旦需要修改你只需要改一个地方。我见过太多人的代码里同一个转换逻辑在五六个文件里各写一遍有的加了rgbSwapped有的没加最后调试起来非常痛苦。另外QT的QPixmap有一个特点它只能从QImage构造所以流程是cv::Mat-QImage-QPixmap然后在QLabel上显示。这一步的显示性能也不用太担心对于单张图像或者25帧左右的视频流QLabel的刷新速度足够用。4. 图像处理功能落地灰度化、二值化、边缘检测、膨胀腐蚀图像处理这部分是整个软件的核心算法区。设计上我建议做一个ImageProcessor类的思路把所有图像处理算法封装进去界面层只负责调用这样代码结构清晰后面扩展功能也好操作。4.1 灰度化与二值化灰度化最简单OpenCV一行搞定cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);二值化需要的参数是阈值。这里我用一个QSlider让用户动态调节阈值范围0到255滑块一动图像实时刷新。这种交互式调参的体验非常加分答辩时演示效果好得多。代码逻辑int thresholdValue ui-thresholdSlider-value(); cv::Mat gray, binary; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::threshold(gray, binary, thresholdValue, 255, cv::THRESH_BINARY);这里要注意threshold函数默认的阈值类型是THRESH_BINARY表示大于阈值的设为255白色小于阈值的设为0黑色。如果你做的是黑白文档扫描之类的功能可能还需要THRESH_BINARY_INV反相得到黑底白字的效果。到底用哪种看你实际需求。4.2 边缘检测边缘检测是图像处理里出场率最高的算法之一。最常用的是Canny算子cv::Mat gray, edges; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::GaussianBlur(gray, gray, cv::Size(3,3), 0); // 先降噪将Canny对噪声敏感度降低 cv::Canny(gray, edges, lowThreshold, lowThreshold * ratio, 3);ratio一般取2或3意思是高阈值是低阈值的2到3倍。这里有一个常规教程不会细说但很重要的点Canny里面那两个阈值的大小直接决定了边缘检测的效果。阈值设得小边缘细节多但噪声也多阈值设得大边缘干净但很多弱边缘会丢。所以我在界面上放了两个滑块分别控制低阈值和高阈值让用户可以自己试出最优参数。默认值我给的是lowThreshold50, highThreshold150大多数场景下这个组合表现不错。4.3 膨胀与腐蚀膨胀和腐蚀是形态学处理的基础操作应用场景非常广比如去噪、连接断裂的边缘、提取轮廓前预处理等。OpenCV的调用方式cv::Mat kernel cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3,3)); cv::dilate(src, dst, kernel); // 膨胀 cv::erode(src, dst, kernel); // 腐蚀核的大小和形状对处理结果影响很大。我在界面上加了一个核大小的滑块范围1到21只取奇数因为OpenCV要求核的中心点必须存在偶数核会偏移。滑块一动内核重建并重新处理。这种交互式的体验感比固定写死一个3×3核要直观得多。关于膨胀腐蚀对图像的影响用一句话理解就行膨胀让亮区域扩大高亮会“长胖”腐蚀让亮区域缩小高亮会“瘦身”。如果先腐蚀再膨胀就是开运算用来去小白点先膨胀再腐蚀就是闭运算用来填补小洞。这些组合操作在OpenCV都有现成函数morphologyEx后续做分割任务时非常有用。5. 视频处理功能落地打开播放、帧提取、实时处理视频处理比静态图像多了一个维度时间。也就意味着你要处理“逐帧”的概念。我的做法是用QTimer驱动逐帧读取而不是用while循环一帧一帧地读。因为while循环会阻塞QT的事件循环导致界面卡死无法响应。核心流程是这样的用cv::VideoCapture打开视频文件或摄像头设置一个QTimer每隔固定时间触发一次在回调里读取一帧、处理、显示视频帧率一般25fps或30fps所以定时器间隔设置在30~40ms之间比较合理。// 打开视频文件 cv::VideoCapture capture; capture.open(filePath); if (!capture.isOpened()) { QMessageBox::warning(this, 错误, 视频文件无法打开请检查文件格式或路径); return; } // 获取视频帧率 double fps capture.get(cv::CAP_PROP_FPS); double interval 1000.0 / fps; // 单位毫秒 // QTimer定时器开始驱动 QTimer *timer new QTimer(this); connect(timer, QTimer::timeout, this, []() { cv::Mat frame; capture frame; if (frame.empty()) { timer-stop(); // 视频放完自动停止 return; } cv::Mat processed; // 这里调用图像处理函数比如灰度化、边缘检测 processFrame(frame, processed); // 转QImage并显示 QImage qimg matToQImage(processed); ui-resultLabel-setPixmap(QPixmap::fromImage(qimg)); }); timer-start(interval);这个方案有一个天然的好处暂停和继续都好实现。暂停就是timer-stop()继续就是timer-start(interval)不用去维护复杂的线程状态。而且处理过程中界面不会卡死因为每一帧的处理耗时都很短QTimer回调执行完就立刻回到事件循环用户依然可以拖动滑块、点击按钮。获取视频帧率这一步很重要。如果视频的原始帧率是60fps你还在用40ms间隔去读帧虽然也能播但画面节奏明显不对。而读取CAP_PROP_FPS得到真实帧率后动态计算定时器间隔就能保证播放速度接近实际视频速度。还有一个新手的常见困惑OpenCV能不能直接读取RTMP或网络视频流可以的VideoCapture::open(rtmp://...)这种用法在支持FFmpeg的OpenCV构建版本里是可以的但网络流不稳定时容易卡顿或直接失败。课程设计级别一般不会碰这个但如果要做建议先本地验证网络环境别在答辩现场开网络流演示。6. findContours轮廓检测与fillPoly填充的实战用法在热搜词里findContours和fillPoly出现频率很高这块单独拿出来聊一下。轮廓检测在智能车赛道识别、工件定位、二维码识别等场景中非常常用。6.1 轮廓检测的完整流程标准的轮廓检测流程是先做预处理灰度-二值化-取反再找轮廓然后筛选目标轮廓。实战代码std::vectorstd::vectorcv::Point contours; std::vectorcv::Vec4i hierarchy; cv::Mat gray, binary, inverted; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::threshold(gray, binary, 100, 255, cv::THRESH_BINARY); cv::bitwise_not(binary, inverted); // 取反让前景变白色 cv::findContours(inverted, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);这里有两个参数值得说清楚。RETR_EXTERNAL表示只提取最外层轮廓内部嵌套的不管如果你只需要图像中最大的物体轮廓用这个就够了。RETR_TREE则会返回所有层级的轮廓树适合有嵌套结构的场景比如表示很多个字母的轮廓。CHAIN_APPROX_SIMPLE表示压缩轮廓点只保留端点能省不少内存如果是精度要求高的场景或者你要逐段描绘轮廓就选CHAIN_APPROX_NONE保留所有点。找到轮廓之后通常会根据面积或周长做筛选排除掉噪点形成的无关轮廓std::vectorstd::vectorcv::Point validContours; for (const auto c : contours) { double area cv::contourArea(c); if (area 1000) { // 面积小于1000的轮廓视为噪点 validContours.push_back(c); } }contourArea是轮廓闭合区域的面积单位是像素的平方。阈值设多少要看你的实际图像分辨率800×600的图像和4000×3000的图像同一物体占的面积差别太大了。所以这个值最好是可调的放在界面上让用户根据需要调节。6.2 fillPoly的两种用途fillPoly这个函数简单来说就是把多边形轮廓内部填充成指定颜色。它在实操中有两类常用场景第一类是生成掩膜Mask。比如你有一个不规则的区域想只在区域内做处理就可以用fillPoly把这个区域填充成白色生成一张掩膜图然后用bitwise_and提取区域内容cv::Mat mask cv::Mat::zeros(src.size(), CV_8UC1); std::vectorcv::Point poly {cv::Point(100,100), cv::Point(400,100), cv::Point(250,400)}; cv::fillPoly(mask, std::vectorstd::vectorcv::Point{poly}, cv::Scalar(255)); cv::Mat result; cv::bitwise_and(src, src, result, mask); // 只保留三角形区域内的内容这种用法在做感兴趣区域ROI提取时特别方便比用矩形ROI提取更灵活想抠什么形状都行。第二类是在图像处理结果中可视化标记。智能车赛道识别里识别到左右边界后把边界之间的区域用半透明的颜色填充然后在界面上叠加上去一眼就能看出程序“看懂”了赛道没。填充的时候配合cv::addWeighted做透明叠加效果会好很多。6.3 drawContours和fillPoly的区别drawContours只绘制轮廓边界线线条宽度可以设置但默认不会填充内部。fillPoly则是把多边形内部完全填充成一种颜色。做形状识别统计时经常是先用findContours找轮廓然后drawContours把边界画出来给人看或者fillPoly把区域填出来做掩膜。两者配合使用非常多。7. 项目结构的演进思路一个小项目如果想从“能跑”变得“漂亮”最简单的改进就是把代码按职责拆分成几个文件。类似下面的结构main.cpp程序入口MainWindow.h / MainWindow.cpp主窗口界面和槽函数ImageProcessor.h / ImageProcessor.cpp图像处理算法封装VideoProcessor.h / VideoProcessor.cpp视频处理逻辑封装ImageConverter.h / ImageConverter.cppMat与QImage互转工具这样拆分之后MainWindow只关心界面逻辑和用户交互不操心算法细节ImageProcessor只处理图像不碰QT控件ImageConverter是纯工具函数不依赖任何业务。就算算法代码再多也不会出现上万个代码堆在一个文件里的悲剧。拆完代码之后有条件的话可以给几个核心函数写一下注释和简单的单元测试。比如matToQImage转换后可以断言“转换后的QImage尺寸与原Mat相同”这种基础检查。虽然这种项目大概率不会让你做完整测试但养成“写完核心函数顺手验证一下”的习惯往后的项目受益无穷。8. 几个人会问到的关键细节提醒不要直接改Mat里的像素值来调节亮度对比度。正确做法是用convertTo函数配合查表法LUT效率高出好几个数量级。界面滑块调亮度时用alpha和beta两个参数配合convertTo就可以了600×600的图像处理一次也就几毫秒。注意VideoCapture的释放时机。切换视频文件或关闭窗口时先release()再重新open()新的文件否则在极端情况下会死锁或者卡死几秒。我在窗口关闭事件closeEvent里会主动做一次释放void MainWindow::closeEvent(QCloseEvent *event) { if (timer timer-isActive()) { timer-stop(); } if (capture.isOpened()) { capture.release(); } event-accept(); }这个小细节不会给你加什么功能但能避免程序退出时“窗口关了进程还在”的诡异问题。图像处理函数不要用std::thread直接开线程改UI。QT的UI只能在主线程里操作子线程访问控件轻则警告重则崩溃。如果未来处理的图像很大想要流畅体验请去查QRunnable和QThreadPool的用法把算法部分丢到工作线程处理完通过信号signal把结果传回主线程。最后说一句掏心窝的话这类图像视频处理软件技术难度其实不在“写算法”而在把零散的图像处理函数组织成一个有逻辑、可扩展、界面好用的小工具。你不要想着把OpenCV里的几百个函数都塞进去能把灰度化、二值化、边缘检测、形态学处理、轮廓检测这五类基本功能做得顺手、界面做得清晰、代码结构做得干净这门课的水平已经超过九成的人了。后面再往里加功能就像是往搭好的架子上放砖每放一块都不费力。本文还有配套的精品资源点击获取
返回列表