
简介面向需要在VS2019中集成Qt与OpenCV开展图像处理的开发者这套工程示例完整演示了从环境配置、图像读取到界面显示、灰度转换、平滑滤波等基础操作代码注释与项目预设齐全适合计算机视觉初学者快速上手也可作为中高级开发者封装图像处理模块的参考起点。压缩包共46个文件包含4个cpp源码、2个h头文件、1个ui界面文件以及vcxproj、sln等工程配置文件、4张jpg测试图片和编译生成的exe、obj、pdb产物整体12.34MB工程目录结构清晰便于直接打开对比运行结果。目前已有867人学习下载。除了基础显示功能工程还展示了OpenCV Mat与Qt QImage之间的格式转换思路并提供cvtColor、blur等处理函数的调用示例可在此基础上继续扩展边缘检测、人脸识别等视觉算法是理解两个库协同工作方式的不错素材。1. VS2019、Qt 和 OpenCV 的 helloworld 工程先让图像显示出来做 Windows 桌面图像工具时最怕的就是环境都装好了结果一个 helloworld 都跑不起来。这篇笔记要拆的这份资源就是一套现成的 VS2019 Qt OpenCV 实例工程里有 helloworld.sln、mainwindow 源码和 x64 编译输出目录核心干两件事——用 OpenCV 读取图像再把 OpenCV 的 Mat 转成 Qt 的 QImage 放到 QLabel 上显示。它适合刚打通环境、想确认工具链没问题的开发者也适合需要一份「能跑起来改改就能用」的模板去套后续图像处理逻辑的人。下文从原理讲到配置再到代码逐段说明和避坑照着走一遍基本能复现。2. 为什么 Mat 和 QImage 能互相转先搞清楚 BGR/RGB 与内存布局2.1 QImage 与 Mat 的格式差异很多人在这一步翻车cv::imread读进来的图像显示出来颜色不对或者干脆花屏。根子在cv::Mat默认是 BGR 三通道排列而 Qt 的QImage::Format_RGB888按 RGB 排列。两者都是连续内存里的像素裸数据矩阵的data指针可以直接给 QImage 用但通道顺序不换Qt 就会把 B 当 R 显示于是整张图偏蓝或者看起来脏兮兮的。另一个差异是步长stride。OpenCV 的Mat为了对齐一行数据的字节数不一定等于cols * channels所以转换时不能只传data、width、height要把step也就是mat.step也交给 QImage否则图像会斜着切或者错位。2.2 为什么选 msvc2019_64 vc16 这套组合Qt 官方对 Windows 提供两类预编译包MinGW 版和 MSVC 版。VS2019 的编译器是 MSVC v142所以必须选名字里带 msvc2019_64 的 Qt 包比如qt-opensource-windows-x86-64-5.15.2.exe安装时勾选的msvc2019_64组件。选 MinGW 包的话VS2019 链接时对不上 C 运行时库会报一堆unresolved external symbol。OpenCV 同理。官方 Windows 预编译包里的库目录是x64/vc15和x64/vc16vc16 对应 VS2019。很多教程说把opencv_world455.lib加进附加依赖项就行但没强调要在x64/vc16/lib里拿如果误用了 vc15 的库编译能过运行时会因为 MSVC 运行时版本不一致直接崩。组件推荐版本说明VSVS2019MSVC v142使用 C 的桌面开发工作负载Qt5.15.2 msvc2019_64和 VS2019 编译器对应OpenCV4.xx64/vc16Debug 用 *d.libRelease 用不带 d 的 lib2.3 转换时的固定套路cvtColor 加上深拷贝我一般把 Mat 到 QImage 的转换收敛成一个独立函数每次调用都走「BGR 转 RGB → 包 QImage → 深拷贝」三步。深拷贝不是可有可无的QImage的构造函数只保存data指针不接管cv::Mat的内存释放。如果 Mat 是函数的局部变量函数返回后 QImage 还在引用已释放的堆内存界面一刷新就踩悬空指针。加上copy()之后QImage 持有独立的内存生命周期和 Mat 脱钩这是能稳定显示的关键一步。QImage MatToQImage(const cv::Mat mat) { // 只处理三通道 8 位图其他格式先转成 CV_8UC3 cv::Mat rgb; if (mat.type() ! CV_8UC3) { cv::cvtColor(mat, rgb, cv::COLOR_GRAY2BGR); } else { cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); // 通道顺序修正 } QImage img((const uchar*)rgb.data, rgb.cols, rgb.rows, (int)rgb.step, QImage::Format_RGB888); return img.copy(); // 深拷贝防止野指针 }这里COLOR_BGR2RGB是 OpenCV 4.x 里推荐的写法老代码里的CV_BGR2RGB也能编译但会有一条 deprecated 警告。rgb.step作为 QImage 的bytesPerLine参数必须显式传入尤其是图像宽度不是 4 的倍数时。这段逻辑在后面完整工程里会复用先在这里立住后面看代码就不会觉得晕。3. 环境配置把这个工程喂给 VS2019 的完整步骤3.1 解压后的目录结构下到的资源解压后大概是这样的helloworld/ ├── helloworld.sln ├── helloworld/ │ ├── helloworld.vcxproj │ ├── helloworld.vcxproj.filters │ ├── main.cpp │ ├── mainwindow.cpp │ ├── mainwindow.h │ ├── mainwindow.ui │ └── x64/ └── .vs/helloworld.sln是 VS2019 的解决方案文件直接双击就能加载。.vs目录是 VS 的本地缓存里面会有Browse.VC.db这类文件如果加载工程后智能感知一直抽风可以先删掉.vs再重新打开。x64目录是编译输出我之前拿到类似工程时习惯先看它里面的 exe 能不能直接跑能跑说明拿到的是编译过的完整包不能跑就按下面的步骤重新配。打开 sln 后第一件事是确认平台是 x64 而不是 Win32。Qt 的 msvc2019_64 和 OpenCV 的 vc16 都只提供了 64 位库用 Win32 平台编译会在链接阶段直接报“找不到 .lib”文件。视图菜单里打开“配置管理器”把活动解决方案平台切到 x64顺手把 Debug 切一次、Release 切一次。3.2 Qt VS Tools 与 Qt 版本关联VS2019 默认不认 .ui 文件和 Qt 的 moc 机制必须装扩展菜单“扩展 → 管理扩展”联机搜索 Qt Visual Studio Tools安装后重启 VS。这个扩展负责把 .ui 编译成 ui_mainwindow.h并自动调用 moc 处理带 Q_OBJECT 的类。如果没装扩展打开 sln 时 VS 会把一堆 Qt 头文件当普通文本编译时每个带 Q_OBJECT 的文件都会报“无法打开 ui_mainwindow.h”或 moc 相关错误。装完扩展后在 VS 菜单栏会多一项“Qt VS Tools”进去选“Qt Versions”点“Add”填你本机 Qt 的 msvc2019_64 路径比如D:\Qt\5.15.2\msvc2019_64。这一步经常被忽略扩展装了但没告诉 VS 用哪个 Qt 版本结果工程属性里的$(QTDIR)是空值所有#include QtWidgets/QMainWindow都找不到路径。提示工程能编译但一运行就提示找不到 Qt5Widgets.dll通常是没把D:\Qt\5.15.2\msvc2019_64\bin加进系统 PATH或者 Qt Versions 里配的是 MinGW 版路径。3.3 包含目录、库目录、附加依赖项右键 helloworld 工程 → 属性在“VC 目录”里配置包含目录和库目录。常见做法是直接写绝对路径简单直观缺点是换机器要改但对 helloworld 这类工程够用。配置项值包含目录D:\Qt\5.15.2\msvc2019_64\include、C:\opencv\build\include库目录D:\Qt\5.15.2\msvc2019_64\lib、C:\opencv\build\x64\vc16\lib附加依赖项DebugQt5Widgetsd.lib;Qt5Guid.lib;Qt5Cored.lib;opencv_world455d.lib附加依赖项ReleaseQt5Widgets.lib;Qt5Gui.lib;Qt5Core.lib;opencv_world455.lib附加依赖项在“链接器 → 输入”里填。注意两个坑第一Debug 配置必须用带d后缀的库opencv_world455d.lib对应 Debugopencv_world455.lib对应 Release混用会报“无法解析的外部符号”第二不要把两类配置一起编辑VS 的“所有配置”会同时改 Debug 和 Release但 lib 文件名不同要分开填。环境变量方面推荐把下面这几个路径加进系统 PATH或者至少在 VS 的“调试 → 环境”里写一行PATH$(QTDIR)\bin;C:\opencv\build\x64\vc16\bin;$(PATH)$(QTDIR)是 Qt VS Tools 注入的宏前提是 3.2 节里 Qt Versions 配好了。OpenCV 的 bin 必须进 PATH否则运行时找不到opencv_world455.dll程序会在 main 还没执行到时弹一个黑窗口然后退出。4. 核心代码解析helloworld 到底在做什么4.1 入口和主窗口结构main.cpp是标准 Qt 入口创建一个MainWindow实例并进入事件循环#include mainwindow.h #include QtWidgets/QApplication int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; w.show(); return app.exec(); }mainwindow.h里声明一个成员变量保存当前 Mat以及一个按钮槽函数#pragma once #include QtWidgets/QMainWindow #include opencv2/opencv.hpp #include ui_mainwindow.h class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent nullptr); ~MainWindow(); private slots: void on_btnGray_clicked(); // 转灰度 void on_btnBlur_clicked(); // 高斯模糊 private: QImage MatToQImage(const cv::Mat mat); Ui::MainWindowClass ui; cv::Mat currentImage_; // 一直持有当前图像数据 };这里Q_OBJECT宏必须有否则 Qt 的 moc 不生成元对象代码槽函数不会触发。ui_mainwindow.h是 Qt VS Tools 从.ui文件生成的不要在工程里手动找它编译时会自动生成在中间目录。4.2 构造函数里完成首次加载构造函数读取一张图片转成 QImage 后交给界面上一个 QLabel 显示MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { ui.setupUi(this); setWindowTitle(Qt OpenCV helloworld); cv::Mat image cv::imread(D:/images/test.jpg); if (image.empty()) { // 打不开时给个提示避免程序静默退出 ui.imageLabel-setText(无法加载图片请检查 imread 路径); return; } currentImage_ image.clone(); // Mat 深拷贝 ui.imageLabel-setPixmap(QPixmap::fromImage(MatToQImage(currentImage_))); }imread的路径是 Windows 风格正斜杠反斜杠在 C 字符串里要转义写D:\\images\\test.jpg容易漏不如直接正斜杠。image.clone()这步很关键currentImage_后续要长期保存不能只是浅拷贝。如果只写currentImage_ image;引用计数会共享底层数据原变量析构时 Mat 数据不一定会释放逻辑上能用但容易在后续赋值时出现莫名其妙的尺寸变化。4.3 转灰度与高斯模糊处理后更新显示两个按钮的核心处理思路完全一致先对currentImage_做 OpenCV 处理再把结果深拷贝回成员变量最后刷新 QLabelvoid MainWindow::on_btnGray_clicked() { if (currentImage_.empty()) return; cv::Mat gray, grayBgr; cv::cvtColor(currentImage_, gray, cv::COLOR_BGR2GRAY); // 转回三通道保持显示链路统一省得 MatToQImage 里判断格式 cv::cvtColor(gray, grayBgr, cv::COLOR_GRAY2BGR); currentImage_ grayBgr.clone(); ui.imageLabel-setPixmap(QPixmap::fromImage(MatToQImage(currentImage_))); } void MainWindow::on_btnBlur_clicked() { if (currentImage_.empty()) return; cv::Mat blurred; cv::GaussianBlur(currentImage_, blurred, cv::Size(5, 5), 0.0); currentImage_ blurred.clone(); ui.imageLabel-setPixmap(QPixmap::fromImage(MatToQImage(currentImage_))); }GaussianBlur的第三个参数是内核尺寸cv::Size(5, 5)表示 5x5 的高斯核。内核越大图像越糊但计算越慢0.0是标准差参数传 0 表示由内核尺寸自动推导。转灰度时我故意做了COLOR_GRAY2BGR原因在 2.3 节MatToQImage 只处理三通道输入灰度图要么扩展回三通道要么在转换函数里多写一个分支。helloworld 里用前者代码路径更短真正做性能优化时可以改成在转换函数里判断mat.channels() 1再直接用Format_Grayscale8。4.4 QLabel 的尺寸策略.ui文件里放一个 QLabel再套一个垂直布局。默认情况下 QLabel 不会自动缩放图片图大就裁掉图小就空白。加上setScaledContents(true)可以用但会拉伸变形图片不是正方形时形状会变。我一般这样处理ui.imageLabel-setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); ui.imageLabel-setScaledContents(true);Ignored策略允许布局随意拉伸控件配合setScaledContents(true)后图片会跟随控件缩放。代价是高宽比不保持图像会变形。如果对比例敏感可以自己在resizeEvent里按currentImage_的宽高比重新设置 QLabel 的固定尺寸但这就超出了 helloworld 的范畴先记下这个取舍即可。5. 避坑指南编译报错、花屏和闪退的常见原因5.1 环境报错dependent 路径、Qt 版本和 DLL 缺失现象一编译时输出:-1: error: dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\QtWidgets does not exist。原因这是 Qt VS Tools 在解析工程时找不到 Qt 头文件目录。常见于两类情况一是 Qt Versions 里配的路径不对装的是msvc2017_64却填成了msvc2019_64的工程二是.vcxproj里写死了$(QTDIR)而当前 VS 会话里这个宏没被赋值。解决打开 Qt VS Tools → Qt Versions确认路径指向真实存在的目录如果用了相对路径检查..\..\..\是否真的指到qt\5.15.2\msvc2019_64。我一般直接删掉 Qt Versions 里的记录重新添加一次让 VS 重新解析宏这种玄学问题多半能消掉。现象二编译链接都正常运行时报由于找不到 opencv_world455.dll无法继续执行代码。原因OpenCV 的 bin 目录不在 DLL 搜索路径里。VS 调试时会把工程目录和 PATH 作为搜索路径但系统 PATH 没加 OpenCV 的 bin 就会漏。解决要么把C:\opencv\build\x64\vc16\bin加进系统 PATH 后重启 VS要么打开工程属性 → 调试 → 环境写入PATHC:\opencv\build\x64\vc16\bin;$(PATH)。在 Debug 模式下用第二种方式更省事不用重启机器。现象三Qt 菜单、图标、窗口标题都正常但一调用ui.setupUi就崩溃或报moc相关错误。原因工程的.ui文件没有被 Qt VS Tools 正确参与编译生成ui_mainwindow.h的步骤没执行。解决右键工程 → Qt → Build或者在工程属性里确认“自定义生成步骤”里有 Qt 的条目。更快的办法是把.ui文件打开一下再关掉触发 VS 重新跑一次 Qt 的构建规则。5.2 编码踩坑花屏、悬空指针和链接顺序现象四图像能显示但颜色偏蓝、偏紫或者像从负片里翻出来的。原因OpenCV 读进来的 BGR 数据被直接当成 RGB 传给了 QImage红蓝通道互换。解决回到 2.3 节的MatToQImage确认先调cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB)再构造 QImage。这一步是必须的没有捷径。如果发现颜色还是怪检查原始 Mat 是不是已经是三通道以外的格式比如从摄像头拿到的可能是CV_8UC3但也有例外。现象五程序运行中切换窗口、最小化或连续点按钮偶发性崩溃崩溃堆栈指向QImage::pixel或QPixmap。原因QImage 持有的是悬空指针。QImage 构造函数并没有复制像素数据只记下指针。如果 Mat 是函数局部变量函数返回后 Mat 析构、像素内存释放QImage 还在界面上被绘制。解决MatToQImage里返回值用img.copy()或者先把currentImage_设成成员变量并在赋值时用.clone()。这两个深拷贝至少保留一个不然就是靠运气在跑。现象六链接阶段报LNK2019: 无法解析的外部符号符号名带 Qt 或 OpenCV 字样。原因一是 Debug/Release 的 lib 文件混用比如 Debug 配置里填了不带 d 的opencv_world455.lib二是附加依赖项顺序不对OpenCV 的静态依赖和 Qt 库之间互相引用要按照「后依赖的先链接」的规则排。解决Debug 填opencv_world455d.lib和Qt5Widgetsd.libRelease 填不带 d 的版本。链接顺序上我习惯把 Qt 的 Widgets、Gui、Core 放在 OpenCV 库前面这个顺序从 Qt4 时代就有效很少出问题。注意如果你拿到的是别人工程里的opencv_world460.lib而自己装的是 4.5.5不要硬改依赖项文件名直接重装对应版本或调整自己 OpenCV 的版本让两者匹配。OpenCV 4.6 之后部分 API 有变化混版本编译可能踩到函数签名不一致的坑。6. 进阶从静态图片到摄像头实时处理helloworld 跑通后最值得加的扩展是从imread静态图切到VideoCapture实时视频帧。核心思路是用一个QTimer每 33 毫秒触发一次读取和刷新模拟 30 帧每秒的显示效果。#include QTimer #include opencv2/videoio.hpp // 成员变量 // cv::VideoCapture m_cap; // QTimer m_timer; void MainWindow::startCamera() { if (!m_cap.open(0)) return; // 0 是默认摄像头 connect(m_timer, QTimer::timeout, this, MainWindow::grabFrame); m_timer.start(33); // 约 30 FPS } void MainWindow::grabFrame() { cv::Mat frame; m_cap.read(frame); if (frame.empty()) return; currentImage_ frame.clone(); ui.imageLabel-setPixmap(QPixmap::fromImage(MatToQImage(currentImage_))); }VideoCapture::open(0)打开默认摄像头read是阻塞式的USB 摄像头一般没问题如果帧率波动大可以改成轮询m_cap.grab()和m_cap.retrieve()拆开读。实时视频的处理逻辑和静态图完全一样灰度按钮、模糊按钮依然只作用于currentImage_只是它的来源从 imread 变成了摄像头。发布部署是最后一个环节。把 exe 放到新目录后用 Qt 自带的工具收集依赖。在“VS2019 开发者命令提示符”里执行windeployqt.exe /dir F:\release_demo\helloworld.exe这条命令会把 Qt5Widgets.dll、Qt5Gui.dll、platforms 插件等复制到 exe 旁边。OpenCV 的 DLL 不会自动处理手动把opencv_world455.dll以及 Debug 版带 d 的那份复制到 exe 同目录。如果客户机器没有 VC 运行库把 VS2019 安装目录里 Redist 的 vc_redist.x64.exe 一并拷过去装上。回看整个工程真正让我学到东西的不是那几百行代码而是 Mat 和 QImage 之间那层看不见的数据引用关系——所有花屏、闪退、偶发性崩溃根源几乎都在内存所有权上。从那以后我每次写显示逻辑都强制走「Mat clone → cvtColor 转 RGB → QImage copy」这条固定链路在新机器上拿到别人的 Qt/OpenCV 工程也是先跑 helloworld 再往上叠功能。这套做法能避开大部分环境坑和指针坑希望帮到你。本文还有配套的精品资源点击获取