简介面向希望在Visual Studio 2019中结合Qt与OpenCV完成图像显示和处理的开发者这份实例以可运行的Qt Widgets工程演示了环境配置、图像读取、Mat转QImage、灰度转换与平滑滤波等关键环节人工智能与计算机视觉方向的学习者可直接将其作为入门模板。压缩包共46个文件其中cpp/h源代码、ui界面文件与sln/vcxproj工程文件构成完整可编译项目exe文件便于直接运行查看效果jpg/png图片作为测试素材tlog、obj、pdb、ilk等对应编译链接与调试中间产物整个资源包约12.34MB目录结构紧凑适合对照修改调试。已有867人浏览学习说明这套VS2019QtOpenCV的组合配置方式得到过实际验证。参考代码后读者既能掌握第三方库引用、界面布局与图像数据转换的配合写法也可在现有基础上继续扩展边缘检测、直方图等算法免去从零搭建环境的重复工作。1. 在VS2019里把Qt和OpenCV凑到一起为什么这步最劝退在VS2019里做Qt和OpenCV的显示图像与处理图像测试很多人第一步不是写代码而是被环境配置卡住。明明代码只有几十行双击运行却提示找不到DLL或者界面弹出来就崩溃最后只能对着黑匣子一样的编译输出发呆。这篇笔记从零开始搭一个能跑的实例用VS2019创建Qt Widgets项目在QLabel上显示OpenCV读进来的图像再用阈值和Canny做两个图像处理测试。适合刚装好VS2019、想在Windows下把Qt界面和OpenCV算法串起来的入门工程师。我会把版本搭配、配置路径和几个最容易翻车的坑一次性讲透。2. 搭建VS2019QtOpenCV环境版本搭配、安装顺序和项目配置环境配置不是玄学但确实有讲究。VS2019自带的是MSVC v142编译器Qt必须选对应的msvc2019_64预编译包OpenCV的Windows版库文件则按VS版本分成vc14/vc15两套。这三者的版本和位数一旦错位后面每一步都是无头苍蝇。下面先讲版本搭配再给安装顺序最后落到VS2019项目里的具体配置。2.1 版本搭配先定死MSVC v142、Qt msvc2019_64、OpenCV vc15以最稳妥的搭配为例VS2019 CommunityQt 5.15.2OpenCV 4.5.5。Qt 5.15.2 是官方提供的长期支持版本安装器里能直接选到“MSVC 2019 64-bit”组件OpenCV 4.5.5 的Windows压缩包解压后会看到build\x64\vc15目录里面的库文件是给VS2017/2019用的。这里有个容易混淆的点vc15对应的是Visual Studio 2017的工具集但VS2019的MSVC v142工具集与vc15构建的库在ABI上是兼容的所以OpenCV官方才一直沿用了这套命名。千万别选成mingw版本的OpenCV那是给Qt套件里MinGW编译器用的跟MSVC是两个编译器体系库文件格式都不一样。组件推荐版本说明Visual Studio2019 Community必须装“使用C的桌面开发”工作负载Qt5.15.2在线安装器勾选 MSVC 2019 64-bitOpenCV4.5.5 Windows版解压后路径不含中文和空格使用 x64\vc15 库目标平台x64所有配置必须统一为64位位数这点必须强调Qt、OpenCV、VS工程三者的位数要全部是x64。如果Qt选了32位msvc2019OpenCV用的是x64链接时会直接报LNK1112: 模块计算机类型“x86”与目标计算机类型“x64”冲突。这个报错出现后不用怀疑代码配置错了。2.2 安装顺序与路径先装VS2019再装QtOpenCV用解压版顺序建议是先装VS2019再装Qt最后解压OpenCV。VS2019先装好Qt在线安装器在检测到MSVC编译器后会给你列出可用的Qt版本勾选组件时就更直观。Qt安装器里如果没勾选“MSVC 2019 64-bit”后面在VS里创建Qt项目时会找不到Kit。我的习惯是Qt安装路径只保留版本号和编译器名例如D:\Qt\5.15.2\msvc2019_64方便配置环境变量。OpenCV不需要安装程序解压到固定位置即可。我用的是D:\lib\opencv解压后目录结构是D:\lib\opencv\build\include和D:\lib\opencv\build\x64\vc15\bin。路径里不要有中文和空格否则后面链接阶段可能出现各种奇怪的路径解析问题。接下来配置环境变量把Qt和OpenCV的bin目录都加到PATH里。可以在Windows PowerShell里执行# 追加Qt和OpenCV的bin目录到用户PATH # 注意不要用setxsetx会把超长PATH截断造成系统命令找不到 $qtBin D:\Qt\5.15.2\msvc2019_64\bin $cvBin D:\lib\opencv\build\x64\vc15\bin $env:PATH $qtBin;$cvBin;$env:PATH [Environment]::SetEnvironmentVariable(PATH, $env:PATH, User)逻辑说明$env:PATH修改的是当前进程的环境变量SetEnvironmentVariable把新PATH写入用户环境变量。这个操作做完要关闭所有VS和cmd窗口再重新打开环境变量才会生效。如果环境变量配好了在cmd里执行where opencv_world455.dll能看到DLL路径这一步验证比打开VS强得多。提示如果DLL路径没找到优先检查用户PATH是否真的写进去了可以用reg query HKCU\Environment查看。别急着重装OpenCV。2.3 创建Qt Widgets项目并配置include/libVS2019本身不带Qt项目模板需要先安装扩展“Qt Visual Studio Tools”。在VS2019的菜单栏依次选择“扩展” - “管理扩展” - “联机”搜索“Qt Visual Studio Tools”下载后重启VS。安装成功后会多出一个“Qt VS Tools”菜单。创建项目时文件 - 新建 - 项目搜索“Qt”选择“Qt Widgets Application”。如果没有这个模板检查扩展是否装好或者VS是不是2019版本——扩展对VS版本的匹配很敏感。项目创建后第一件事是把平台切到x64。“解决方案平台”下拉框里默认可能是x86如果没有x64就点“配置管理器”添加。接下来配置OpenCV的目录。右键项目 - 属性注意左上角配置和平台要选对“Debug”和“Release”要分别配置。我习惯在“VC目录”里设置包含目录和库目录这比“附加包含目录”更直观// 这是VC目录里的设置不是代码 // 包含目录添加 D:\lib\opencv\build\include D:\lib\opencv\build\include\opencv2 // 库目录添加 D:\lib\opencv\build\x64\vc15\lib这里可能出现的问题是Qt VS Tools扩展已经自动给项目添加了Qt的include和lib手动加OpenCV后不要覆盖Qt的路径。然后在“链接器 - 输入 - 附加依赖项”里填库文件名。Debug填opencv_world455d.libRelease填opencv_world455.lib。带不带d是Debug和Release的约定后面第5章会专门说混用导致的崩溃。到这里环境就算齐了。一个比较容易忽略的点配置完成后重新编译一下空项目确认Qt的moc和OpenCV的库引用都正常。如果编译链接通过但运行闪退回过去看2.2的环境变量。我在这一步踩过一次配好了所有目录但忘了把OpenCV的bin加到PATH一运行就报DLL缺失排查时先入为主以为是项目配置白白浪费了一个小时。3. 在Qt界面上显示图像从cv::Mat到QImage再到QLabel显示图像是整个实例的地基。OpenCV读进来的是cv::Mat界面控件能认的是QImage/QPixmap中间这层转换做扎实了后面图像处理的结果才能正确展示。这章的代码量不大但坑特别密集通道顺序、内存生命周期、缩放模式、中文路径一个不注意就花屏或崩溃。3.1 最小显示代码imread、cvtColor、QImage、setPixmap先把最常用的一段代码贴出来。“打开图像”按钮的槽函数里写// MainWindow.cpp #include opencv2/imgcodecs.hpp #include opencv2/imgproc.hpp void MainWindow::on_openButton_clicked() { QString fileName QFileDialog::getOpenFileName(this, 打开图片, , Images (*.jpg *.png *.bmp *.jpeg)); if (fileName.isEmpty()) return; // imread在Windows下对中文路径支持不好这里先用toStdString第5章再讲替代方案 cv::Mat src cv::imread(fileName.toStdString(), cv::IMREAD_COLOR); if (src.empty()) { QMessageBox::warning(this, 警告, 图像读取失败路径是否存在或格式不符); return; } // OpenCV读进来的颜色顺序是BGRQt的QImage默认按RGB解释必须转换 cv::Mat rgb; cv::cvtColor(src, rgb, cv::COLOR_BGR2RGB); this-srcMat rgb.clone(); // 后面处理测试时还要用原图 // 用Mat的data直接构造QImage注意bytesPerLine传mat.step而不是width*3 QImage img(rgb.data, rgb.cols, rgb.rows, int(rgb.step), QImage::Format_RGB888); ui-imageLabel-setPixmap(QPixmap::fromImage(img)); }逻辑说明imread把图片文件读成BGR排列的MatcvtColor转成RGB后QImage构造函数的第五个参数告诉Qt每一行像素有多少字节。mat.step是Mat在内存中的实际行跨度它往往大于width * channels因为OpenCV有内存对齐。如果这里图省事填width * 3图像宽度不是4的倍数时显示出来就是斜向撕裂的花屏。另外QImage只是浅拷贝了rgb.data的指针rgb和src的生命周期必须覆盖QImage的使用所以我在代码里用this-srcMat rgb.clone()把原图留存下来避免后面处理时Mat被释放。为了方便后面多个按钮复用把Mat转QImage的代码封装成一个独立函数注意返回前要copy一份// utils.h QImage matToQImage(const cv::Mat mat) { if (mat.type() CV_8UC3) { cv::Mat rgb; cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); // 必须copy否则函数返回后rgb析构留下悬垂指针 return QImage(rgb.data, rgb.cols, rgb.rows, int(rgb.step), QImage::Format_RGB888).copy(); } if (mat.type() CV_8UC1) { return QImage(mat.data, mat.cols, mat.rows, int(mat.step), QImage::Format_Grayscale8).copy(); } return QImage(); }这段封装把“BGR转RGB”和“单通道灰度图”两种情况都处理了。第4章的处理结果会用到这里避免重复踩通道顺序的坑。3.2 图像缩放与自适应scaled的三种模式怎么选用上面的代码显示一张1200万像素的照片QLabel会被撑出界面只有左上角一小块可见。我做这个实例时习惯让图像自动适应label的尺寸。QLabel本身有个setScaledContents(bool)属性设置为true后图片会填满整个label但会拉伸变形不适合做测试。更好的方式是自己处理QPixmap的缩放// 更新label显示带自适应缩放 void MainWindow::updateLabel(const QImage img) { QPixmap pix QPixmap::fromImage(img); QSize labelSize ui-imageLabel-size(); if (labelSize.isEmpty()) return; // 窗口还没显示时label尺寸是0 QPixmap scaled pix.scaled(labelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); ui-imageLabel-setPixmap(scaled); }参数说明Qt::KeepAspectRatio保证缩放后不变形空白部分用label背景色填充Qt::IgnoreAspectRatio会把图拉伸填满label观察效果时会误导Qt::KeepAspectRatioByExpanding会按比例放大并裁剪适合做封面。缩放质量这里选了Qt::SmoothTransformation它使用双线性插值缩放大图时效果明显好于Qt::FastTransformation代价是CPU占用高一些。测试机性能一般的话可以改回FastTransformation不影响处理结果。这里有一个隐藏问题如果imageLabel尺寸当前还没布局完成调用scaled会返回一个空QPixmap。我一般先qDebug() labelSize确认不是0或者在窗口resize事件里重新调用一次updateLabel。简单做法是让label的sizePolicy保持默认然后把这个自适应逻辑放进resizeEvent里窗口大小改变时重算。3.3 保存显示结果QPixmap与本地文件显示和处理做完下一步往往是想把结果存下来。常见的错误是直接调cv::imwrite处理中文路径又踩回第5章要讲的坑。我推荐在Qt侧用QImage::save保存因为QImage对QString路径的处理比OpenCV友好得多// 保存label当前显示的内容 void MainWindow::on_saveButton_clicked() { if (ui-imageLabel-pixmap().isNull()) return; QString savePath QFileDialog::getSaveFileName(this, 保存图像, , PNG图像 (*.png);;JPEG图像 (*.jpg)); if (savePath.isEmpty()) return; QPixmap pix ui-imageLabel-pixmap(); bool ok pix.save(savePath); if (!ok) { QMessageBox::warning(this, 错误, 保存失败检查路径权限); } }逻辑说明从label取出的是经过缩放的pixmap尺寸可能小于原图。如果测试时要保留完整分辨率应该在处理函数里保存未被缩放的QImage而不是从label取。另外注意JPEG保存会有损压缩PNG无损。保存的目录如果不存在会返回false所以我在弹窗里给出提示。这个保存函数可以复用后面给处理结果加一个“另存为”按钮就是复制这段代码。4. 图像处理测试灰度化、二值化和Canny边缘检测的调参实例环境通了图像也显示出来了现在到“处理图像”这个核心诉求。这一章做一个最简单的处理面板三个按钮分别触发灰度化、二值化和Canny边缘检测两个QSlider实时调整threshold和Canny的阈值处理结果立刻回显到label上。整个过程不追求算法深度重点是让你看清OpenCV参数变化对结果的影响。4.1 处理流程封装输入Mat、处理类型、可调参数不把处理逻辑散落在按钮槽函数里而是封装成一个独立的函数。这样三种处理共用同一套“取原图 - 处理 - 转QImage - 显示”的流程后面加新算法也方便。封装函数如下// ImageProcessor.h enum ProcType { PROC_GRAY 0, PROC_BINARY 1, PROC_CANNY 2 }; cv::Mat processImage(const cv::Mat src, int type, int param1, int param2);// ImageProcessor.cpp cv::Mat processImage(const cv::Mat src, int type, int param1, int param2) { cv::Mat dst; switch (type) { case PROC_GRAY: { cv::cvtColor(src, dst, cv::COLOR_BGR2GRAY); break; } case PROC_BINARY: { cv::Mat gray; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::threshold(gray, dst, param1, 255, cv::THRESH_BINARY); break; } case PROC_CANNY: { cv::Mat gray; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); // 边缘检测对噪声敏感先做一次3x3高斯模糊 cv::GaussianBlur(gray, gray, cv::Size(3, 3), 0); cv::Canny(gray, dst, param1, param2); break; } default: src.copyTo(dst); } return dst; }逻辑说明函数参数中src是原始BGR彩色图type决定走哪条处理链路param1和param2是阈值参数。统一在函数内部做灰度化避免外面把灰度图再当彩色图传给Canny那种错误很常见。返回的dst可能是单通道灰度图也可能是二值图后续的matToQImage会自动识别通道数。这个函数的粒度是“同步”如果你的图像可以一次性处理完它已经够了如果要做滑动条实时调节直接调用也存在UI卡顿风险这个问题在第5章和第6章展开。4.2 灰度化与二值化threshold参数的实际含义灰度化本身没有参数cvtColor的转换码是COLOR_BGR2GRAY。二值化里threshold的行为值得细说param1是阈值param2是最大值。当某个像素值大于等于阈值输出param2通常255否则输出0。我一开始把两个参数都暴露给用户param2也放了个滑块结果谁拖乱谁。后来只保留阈值一个滑块最大值固定255。// 二值化阈值滑块变化 void MainWindow::on_thresholdSlider_valueChanged(int value) { ui-thresholdSpinBox-setValue(value); doProcess(PROC_BINARY, value, 255); } void MainWindow::doProcess(int type, int p1, int p2) { if (srcMat.empty()) return; cv::Mat result processImage(srcMat, type, p1, p2); QImage img matToQImage(result); updateLabel(img); }参数说明threshold的类型除了THRESH_BINARY还有THRESH_BINARY_INV反色、THRESH_TRUNC超过阈值的截断和THRESH_TOZERO低于阈值的置零。做物体检测预处理时THRESH_BINARY_INV配合白色前景比较常用。滑块范围建议0到255初始值128。滑块调整时注意每次都从srcMat重新做灰度化和二值化而不是在上一次结果上继续二值化否则阈值叠加后结果会越变越离谱。4.3 Canny边缘检测两个阈值和Sobel核大小的配合Canny是两个阈值做滞后处理低阈值param1和高阈值param2。像素梯度大于高阈值判为强边缘小于低阈值丢弃中间像素如果与强边缘连通则保留。所以低阈值和高阈值的比值通常取1:2到1:3。例如低阈值50高阈值150。我的界面上放两个滑块为了方便先固定比例为1:2但实际上两个独立滑块能让你更直观体会到滞后阈值的意义。// Canny低阈值滑块 void MainWindow::on_cannyLowSlider_valueChanged(int value) { ui-cannyLowSpin-setValue(value); int high ui-cannyHighSlider-value(); doProcess(PROC_CANNY, value, high); }除了两个阈值Canny函数还有第三个隐藏参数apertureSize默认是3表示Sobel算子的核大小。核越大对噪声越平滑但边缘定位也越模糊。我的习惯是保持默认3只有图像噪声特别大时才调到5。还有最后一个参数L2gradient默认false表示用L1范数近似true则用更精确的L2范数计算梯度幅值边缘细节会多一些但耗时也更高。在测试时可以先true和false各跑一遍看哪套配置更符合你的视觉预期。5. Qt与OpenCV联调排查崩溃、花屏、中文路径和线程卡顿到这里环境、显示、处理都跑通了但真正让人掉头发的往往是联调阶段的几个典型问题。下面几节都是我在本机上一一复现过的现象按“现象 - 原因 - 解决”的结构写你能直接对照自己的报错去查。5.1 启动报“找不到opencv_world455.dll”根本原因不在代码现象编译链接都通过双击exe后立刻弹窗“由于找不到opencv_world455.dll无法继续执行代码”。更诡异的是有时候从开始菜单运行不行从VS里按F5却能跑有时候反过来。原因exe运行时需要加载OpenCV的DLL系统在当前目录和PATH里搜不到它。从VS里F5能跑是因为VS把OpenCV的bin目录临时塞进了调试环境变量但从桌面双击exe时用的是用户PATH。所以这不是代码问题是部署问题。解决先把D:\lib\opencv\build\x64\vc15\bin确认加到用户PATH重启VS。如果仍然报“0xc000007b”那多半是DLL位数和exe位数不一致检查解决方案平台是不是x64。临时方案是把三个opencv_world4*.dll复制到exe的生成目录下但这只适合测试不适合日后维护。用命令验证一下where opencv_world455.dll结果能打印出完整路径说明系统能找到。找不到的话第2章的环境变量配置要重做。注意OpenCV 4.x还有一个opencv_videoio_ffmpeg455_64.dll之类的可选DLL如果只在显示图像时用不到但读取视频时需要一起分发。5.2 图像花屏或颜色不对BGR与RGB的转换顺序错了现象显示出来的图像红色和蓝色完全互换人脸像科幻片或者整体图像撕裂斜向错位。原因通道顺序和行跨度两个问题。OpenCV默认BGRQImage按RGB解释不转的话红蓝错位是必然的。撕裂问题则是我第3章强调的bytesPerLine必须用mat.step。有同行为了省事在构造QImage后用QImage::rgbSwapped()来做反过来转换结果颜色对了却开始花屏就是因为没处理行字节对齐。解决统一用matToQImage内部先转RGB再构造QImage。如果mat本身是灰度图或二值图就不要转RGB直接用QImage::Format_Grayscale8构造并确保数据位数匹配。判断方法很简单mat.channels()等于1就选灰度格式等于3就选RGB格式。把这段封装成matToQImage以后所有显示都走这个函数能彻底杜绝该问题。5.3 Debug和Release混用链接库程序运行随机崩溃现象在Debug配置下编译通过运行后一调用cvtColor就崩溃报“0xC0000005 访问冲突”Release配置反而正常或者反过来。原因Debug配置链接了Release版OpenCV库opencv_world455.lib而Qt本身是Debug库。OpenCV在Debug下会开启内存检查如果库内部分配的内存用库外释放或者反过来堆管理器直接中断。症状不是必现的所以特别坑。解决Debug项目的“附加依赖项”填opencv_world455d.libRelease填opencv_world455.lib。为了防止以后忘了改可以在公共头文件里用预处理宏强制// 写在Qt项目的主头文件或预编译头里 #ifdef _DEBUG #pragma comment(lib, opencv_world455d.lib) #else #pragma comment(lib, opencv_world455.lib) #endif加了这个后项目属性的附加依赖项就不用再手动维护了。这个习惯我沿用到现在不仅OpenCV其他第三方库也按这个模式处理能少踩一半链接期崩溃的坑。5.4 中文路径读取失败imread的字符编码坑现象用QFileDialog选择一个路径带中文的图片src.empty()为真路径全英文就正常。用调试器看fileName.toStdString()的值像乱码。原因std::string在MSVC下按本地代码页GBK解释而Qt的toStdString()输出的是UTF-8编码。imread在Windows上接受的是本地代码页路径UTF-8字节流被误读成GBK文件自然找不到。解决推荐用QFile读文件字节再imdecode彻底绕过路径编码QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { qWarning() 打开文件失败: fileName; return; } QByteArray bytes file.readAll(); std::vectorchar buf(bytes.begin(), bytes.end()); cv::Mat src cv::imdecode(buf, cv::IMREAD_COLOR);imdecode从内存缓冲区解码图片和imread的后续行为一致但文件名的编码问题被消解了。注意务必检查返回Mat是否为空因为如果文件不是有效图片imdecode返回空Mat代码要继续向下走就会在cvtColor处崩溃。5.5 大图处理卡死界面先确认是否阻塞了UI线程现象点击“Canny”按钮后窗口标题栏出现“无响应”拖不动过几秒恢复。图像越大卡顿越明显。原因processImage在UI线程里执行算法运行期间Qt事件循环被阻塞对鼠标键盘的各种消息没法响应。这是典型的线程饥饿问题跟OpenCV本身没有直接关系换成任何耗时算法都会卡。解决先确认耗时位置。可以在doProcess前后加QElapsedTimer打点如果单次超过100ms就需要把处理挪到后台线程。最简单的验证是把图像缩小后再点一次如果明显变快说明确实是大图处理的时间问题。完整方案下一章给。这一节先判断别急着改如果你只是偶尔点一次按钮等待是能接受的想做得流畅必须上线程。6. 进阶把图像处理放到QThread后台避免界面卡死QThread的完整用法能写一篇长文但这里只给一个能用的最小方案。目标是把processImage扔到子线程执行执行完通过信号把QImage传回主线程更新界面。// ProcessThread.h #include QThread #include opencv2/core.hpp class ProcessThread : public QThread { Q_OBJECT public: void setInput(const cv::Mat m) { src m; } void setArgs(int t, int p1, int p2) { type t; param1 p1; param2 p2; } signals: void resultReady(const QImage img); protected: void run() override { cv::Mat dst processImage(src, type, param1, param2); QImage img matToQImage(dst); // 用第3章的转换函数 emit resultReady(img); } private: cv::Mat src; int type, param1, param2; };在窗口里这样使用ProcessThread* thread new ProcessThread(this); connect(thread, ProcessThread::resultReady, this, [](const QImage img) { updateLabel(img); thread-deleteLater(); }); thread-setInput(srcMat); thread-setArgs(PROC_CANNY, low, high); thread-start();验证线程是否生效在run里打印线程IDqDebug() worker thread: QThread::currentThreadId();和主线程的ID对比不同就说明确实在后台执行。注意子线程里不能直接操作ui控件所有结果都通过信号槽传回主线程。我最初偷懒在run里直接改label界面倒是没卡但程序随机闪退后来才明白UI必须在主线程更新。自从在这个小测试实例里养成了“处理超过50毫秒就开线程”的习惯我的Qt界面很少再出现无响应。这个线程类虽然简单但配合第2章的配置、第3章的显示和第4章的处理封装已经是一套能跑通的VS2019QtOpenCV图像处理测试骨架。你接下来可以加高斯模糊、形态学处理甚至换成YOLO的推理流程——只要把处理函数里的内容换掉即可。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?