ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

VS2019下Qt与OpenCV环境配置及图像处理实战

VS2019下Qt与OpenCV环境配置及图像处理实战 简介面向Visual Studio 2019环境下整合Qt与OpenCV进行图像显示与处理的开发者这套示例工程完整演示了从新建Qt Widgets项目、配置OpenCV头文件与库路径到利用imread读取图像、将Mat转换为QImage并在QLabel上呈现以及通过cvtColor灰度化和blur滤波等操作更新界面的全过程。压缩包共46个文件大小12.34MB以cpp/h源码、sln/vcxproj工程配置、tlog/obj编译中间文件为主另含ui界面文件、jpg/png示例图像及可直接运行的exe程序便于对照学习或二次修改。已有867人学习下载工程结构清晰适合计算机视觉入门者快速上手也可供有经验开发者参考VS2019中Qt与OpenCV的联合调试与界面集成思路。1. 在VS2019里把Qt和OpenCV接起来这组组合到底能干什么如果你在VS2019里手动配过Qt和OpenCV大概率经历过这么一幕Qt的安装包给了mingw和msvc两套编译器OpenCV又分vc14/vc15/vc16几个预编译目录稍微选错一个编译期就甩你一堆LNK2019。这份实例工程helloworld解决的就是这个事——在VS2019里把Qt的图像显示能力和OpenCV的图像处理能力串成一条完整链路imread读图 → Mat转QImage → QLabel显示 → cvtColor灰度化 → blur滤波 → 刷新界面。适两类人刚装完环境不知道怎么下手的初学者以及想拿一个最小可编译工程当骨架、往里面填自己算法的从业者。工程本身不大但环境配置的坑一个不少下面按我实际踩过的顺序讲。2. 环境与工程配置VS2019、Qt插件和OpenCV的版本匹配与路径设置这一章解决“能不能跑起来”的问题。大多数人在代码层面纠结半天最后发现卡在环境上——Qt的预编译库和VS工具集对不上或者OpenCV的lib链接了却没把dll放进PATH。我见过太多人把代码改来改去最后发现是环境变量的问题。2.1 版本匹配msvc2019_64、vc16和工具集v142Qt从5.9开始官方提供msvc2017_64和msvc2019_64两种预编译包5.15之后基本只剩msvc2019_64。VS2019默认平台工具集是v142对应OpenCV里的vc16目录VS2017是v141、vc15。这里有个匹配原则Qt和OpenCV的预编译库必须与VS工具集对应混用就会在后面第5章里看到那堆链接错误。VS版本平台工具集Qt预编译包OpenCV目录VS2017v141msvc2017_64vc15VS2019v142msvc2019_64vc16VS2022v143msvc2022_64vc16兼容如果你下载的Qt只有msvc2017_64在VS2019里也能编译但必须把工程的平台工具集改成v141。这个改动在工程属性 → 常规 → 平台工具集。别小看这一步我第一次就是在VS2019里直接用msvc2017_64的Qt库什么都不改就编译结果Qt自带的moc生成文件全部报错。然后是Qt VS Tools插件。打开VS2019的“扩展”菜单 → “管理扩展” → 在线搜索Qt Visual Studio Tools装完重启VS。这个插件的作用是把Qt的include、lib路径自动写进.vcxproj工程文件手动配容易漏。插件装好后菜单栏会多出一项“Qt VS Tools”点进去 → Qt Versions把Qt的安装路径加进去比如D:\Qt\Qt5.15.2\5.15.2\msvc2019_64。之后在工程上右键 → “Qt Project Settings”确认Qt Installation里选的就是刚才那个版本。2.2 属性页配置附加包含目录、库目录和附加依赖项工程解压后右键helloworld工程 → 属性需要改三个地方。注意区分“VC目录”和“C/C → 常规 → 附加包含目录”这两套入口它们效果等价但很多人改了后者忘了前者导致头文件找不到。我习惯统一用VC目录因为直观。VC目录 → 包含目录追加$(QTDIR)\include D:\opencv\build\include D:\opencv\build\include\opencv2这里的$(QTDIR)是Qt VS Tools自动注入的宏指向2.1里配置的Qt路径。OpenCV的包含目录按你自己的实际安装位置改。VC目录 → 库目录追加$(QTDIR)\lib D:\opencv\build\x64\vc16\lib然后链接器 → 输入 → 附加依赖项这里有个必须遵守的规则Debug和Release分别配不同的lib。配置附加依赖项Debugx64opencv_world460d.lib; Qt5Cored.lib; Qt5Guid.lib; Qt5Widgetsd.libReleasex64opencv_world460.lib; Qt5Core.lib; Qt5Gui.lib; Qt5Widgets.lib注意Qt的debug库带d后缀OpenCV的带d后缀的是opencv_world460d.lib。如果debug配置里链接了不带d的Qt5Core.lib链接器大概率报Qt库不匹配或者LNK2038。最后配置环境变量。我的做法是新建系统变量OPENCV_DIR指向D:\opencv\build再把D:\opencv\build\x64\vc16\bin和D:\Qt\Qt5.15.2\5.15.2\msvc2019_64\bin追加到PATH。这一步不配编译能过一运行就弹“找不到opencv_world460.dll”。2.3 从helloworld.sln出发跑通第一个空白窗口解压后目录结构是这样的helloworld/ .vs/ x64/ helloworld.sln helloworld/用VS2019打开helloworld.sln如果系统提示“需要安装C工作负载”先回Visual Studio Installer把“使用C的桌面开发”勾上。打开后先别急着编译做三件事第一把解决方案配置切成Debug x64第二按2.2把属性页里的OpenCV路径改成你机器上的实际路径第三确认工程名上右键能看到Qt Project Settings菜单且里面选中的Qt版本存在。然后按F5。如果一切正常会弹出一个空白窗口桌面任务栏能看到Qt的程序图标。这里要说一下helloworld这个工程的最小验证目标不是显示图片而是证明Qt的moc、uic和OpenCV的库文件已经全部接入VS2019的编译链路。我第一次配的时候窗口没弹出来反而在输出窗口看到“无法启动程序找不到Qt5Cored.dll”那就是PATH没配好。3. 从Mat到QImage图像显示链路与核心代码拆解空白窗口跑通后下一步就是往窗口里塞一张图。整个链路的核心是OpenCV读进来的是MatQt界面显示的是QImage两者之间必须做一次格式转换。这一步坑最多我不是说函数名记不住而是数据类型匹配和内存生命周期的问题。3.1 imread读图路径、通道顺序和Mat的常见误区先看读取端的代码cv::Mat src cv::imread(D:/images/test.jpg); if (src.empty()) { QMessageBox::critical(this, 错误, 图片读取失败请检查路径); return; }imread读图失败时不会抛异常而是返回一个空的Mat所以必须用empty()判断。我见过新手直接不判断就往下走结果后面的cvtColor直接抛OpenCV(4.5.5) Error: Assertion failed。路径这里有个小细节Windows下写D:\\images\\test.jpg或者D:/images/test.jpg都行但千万别写单反斜杠D:\images\test.jpg\t会被当成制表符转义。还有一个中文路径的问题。OpenCV在Windows下用窄字符读中文路径时偶尔会读到空Mat这不是代码问题而是OpenCV内部用的是ANSI编码。我的处理习惯是测试阶段把图片统一放英文路径等程序稳定后再考虑用imdecode配合宽字符处理。另外讲清楚通道顺序OpenCV默认的Mat颜色通道是BGR不是RGB。这个顺序问题会直接影响下一节QImage的转换很多人转出来的图红蓝互换就是在这埋下的。3.2 Mat转QImageFormat_RGB888的隐性前提这是整个实例里最需要讲透的一段。直接看转换函数QImage matToQImage(const cv::Mat mat) { // 如果Mat是单通道灰度图转成Format_Grayscale8 if (mat.type() CV_8UC1) { QImage qimg(mat.data, mat.cols, mat.rows, static_castint(mat.step), QImage::Format_Grayscale8); return qimg.copy(); // 拷贝数据避免悬空指针 } // 如果Mat是3通道BGR先转成RGB再显示 cv::Mat rgb; cv::cvtColor(mat, rgb, cv::COLOR_BGR2RGB); QImage qimg(rgb.data, rgb.cols, rgb.rows, static_castint(rgb.step), QImage::Format_RGB888); return qimg.copy(); }这段代码里有三个关键点。第一个是通道顺序OpenCV的Mat是BGR而QImage::Format_RGB888期望的数据排列是RGB。如果直接拿src.data去构造QImage出来的图红色和蓝色对调——人脸变蓝脸。所以必须先cvtColor(mat, rgb, cv::COLOR_BGR2RGB)。第二个是bytesPerLine参数。很多人写QImage(image.data, image.cols, image.rows, QImage::Format_RGB888)只传四个参数这是教科书式的写法但有个隐性前提Mat内存必须是连续的且每行没有填充字节。对于imread读出来的完整图像这个前提成立但如果是Mat的ROI子区域或者经过某些操作后的Matstep会大于cols * channels这时候四参数构造出来的QImage显示出来就是斜的、撕裂的。第五个参数rgb.step就是每行真实占用的字节数把它传进去就永远不会花。我这里显式用了static_castint(rgb.step)因为QImage构造函数的bytesPerLine是int类型Mat的step是size_t在x64下直接传会有警告。第三个是返回值里的.copy()。QImage构造函数的浅拷贝特性它只保存指针不复制像素数据。如果函数返回的QImage生命周期结束了但Mat数据被释放比如函数外重新给Mat赋值QImage就变成了野指针程序表现是时好时坏偶尔闪退。加.copy()让QImage拥有一份独立数据成本是一次内存拷贝在这个场景下完全值得。3.3 QLabel挂载图片布局、缩放和内存释放转换完成后把QImage放到界面上。工程里用QLabel做显示容器两种常见做法一种是在设计师界面里拖一个QLabel另一种是代码动态创建。这个实例用的是动态创建再放进布局QLabel *label new QLabel(this); label-setPixmap(QPixmap::fromImage(qimg)); ui-verticalLayout-addWidget(label);ui-verticalLayout是主窗口布局管理器addWidget会把QLabel追加到布局底部。这里有个细节如果多次刷新比如每点一次按钮处理一遍图像每次new QLabel都会叠一个新控件在上面旧的不释放内存越涨越高。我的习惯是先在设计师界面里放一个固定的QLabel命名labelImage处理完只更新这个label的Pixmap不重复创建。设置缩放模式也是一个必要步骤。原始图片可能很大不缩放会撑爆窗口QPixmap pixmap QPixmap::fromImage(qimg); QSize labelSize ui-labelImage-size(); ui-labelImage-setPixmap(pixmap.scaled(labelSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));Qt::KeepAspectRatio保持宽高比Qt::SmoothTransformation用平滑算法缩放比快速缩放质量好。注意scaled返回新QPixmap不会修改原图所以处理链路里可以放心用原始Mat继续算。到这里图像显示链路就跑通了读图 → 转格式 → 显示。4. 图像处理参数实操灰度、滤波与边缘检测的调参思路链路通了接下来就是OpenCV的主场——图像处理。这个实例里做的三个操作用来演示“处理-显示-刷新”的完整闭环。我会把每个操作的参数含义和调参边界讲清楚因为实际工程中坑不在函数调用而在参数变化对结果的影响怎么预判。4.1 cvtColor转灰度为什么彩色图处理前先转GRAY先看灰度转换cv::Mat gray; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);这个函数把3通道的BGR彩色图转成单通道灰度图。灰度图的数据量是彩色的三分之一后续的滤波、边缘检测、二值化这些算法在单通道上计算速度明显更快。Canny边缘检测的输入就要求单通道灰度图直接传彩色图会抛异常。注意区分COLOR_BGR2GRAY和COLOR_RGB2GRAY如果你的Mat来源是imread通道顺序是BGR就用BGR2GRAY如果前面3.2节已经转成了RGB那就用RGB2GRAY。用错了不会报错只是灰度权重略有差异因为OpenCV对这两组转换用了不同的加权系数。转换前还有个判断要做如果原图本身就是灰度图cvtColor会报assertion failed。稳妥做法是先判断src.channels()三通道才转单通道直接用cv::Mat gray; if (src.channels() 3) cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); else gray src.clone();这个看似多余的分支在实际项目中能省很多事。图像源可能是相机、视频帧、别人传进来的Mat通道数在调用前并不确定。4.2 blur与GaussianBlur核大小和sigma的取舍滤波是降噪的手段直接决定后面边缘检测的质量。均值滤波和高斯滤波是这个场景最常用的两个cv::Mat blurred; cv::blur(gray, blurred, cv::Size(5, 5)); // 均值滤波 cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0); // 高斯滤波blur的第三个参数是核大小Size(w, h)表示每个输出像素取周围w x h窗口内像素的平均值。核越大图像越平滑但边缘也越模糊。Size(5, 5)是最常用的起步值效果是让噪点明显减少但不会把线条糊掉。GaussianBlur和blur的区别在于加权方式均值滤波窗口内每个像素权重相等高斯滤波按高斯分布给中心像素更高权重边缘保留能力更好。第四个参数sigmaX传0时OpenCV会根据核大小自动计算sigma值这个自动值在大多数场景下都是合理选择。如果你在图像上看到滤波结果是“馒头状”的白斑那是sigma设大了核的尺寸和sigma不匹配。这里说一个参数调整的通用逻辑不是核越大越好而是“刚好滤掉噪声、不损失特征”的临界点最好。在helloworld这个例子里如果源图是干净的截图其实blur都可以不调如果是从相机采的图一般先用GaussianBlur(Size(3, 3), 0)效果不够再往上加。4.3 Canny边缘检测双阈值的比例关系边缘检测是这个实例的视觉高潮代码很短参数却很讲究cv::Mat edges; cv::Canny(blurred, edges, 50, 150);Canny的双阈值机制高于高阈值150的像素点被当作强边缘低于低阈值50的像素点被排除介于两者之间的只在与强边缘相连的情况下保留。这个机制决定了高低阈值的比例比绝对值更重要经典的经验比例是1:2到1:3。把参数改成Canny(blurred, edges, 100, 200)边缘数量会减少但更干净改成Canny(blurred, edges, 20, 60)边缘数量增多但容易出现碎边。实际调参时如果检测到的边缘太多太碎我的顺序是先提高低阈值把弱边缘滤掉如果边缘断断续续不连续再降低高阈值多保留一些强边缘。调Canny前先固定前面的滤波参数不要两个环节一起调不然你根本不知道是哪个改动起的作用。4.4 把“处理-刷新”封装成同一个函数处理完之后要刷新界面。如果每次处理都写一遍转换和显示代码代码会很快失控。我一般会写一个统一的刷新函数void MainWindow::updateDisplay(const cv::Mat mat) { QImage qimg matToQImage(mat); ui-labelImage-setPixmap( QPixmap::fromImage(qimg).scaled( ui-labelImage-size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); }然后加一个“开始处理”按钮槽函数里依次调用void MainWindow::on_processBtn_clicked() { cv::Mat gray, blurred, edges; cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0); cv::Canny(blurred, edges, 50, 150); updateDisplay(edges); // 显示处理结果 }这个封装带来的好处是每次处理完只需要调用updateDisplay(处理结果Mat)界面刷新逻辑不受处理算法影响。如果想对比灰度、滤波、Canny三个中间结果只需要分三次调用updateDisplay或者加一个下拉框让用户选择显哪一层——工程里后续拓展都从这儿切进去。这套“处理函数与显示函数分离”的结构就是从helloworld这个最小工程开始养成的习惯。5. 避坑排查VS2019QtOpenCV高频报错5连这一章我整理了在VS2019里搭这套环境最常碰到的5个报错全部是实际操作中反复出现的。每个报错按“现象 → 原因 → 解决”来描述这些坑我基本都踩过不止一遍。5.1 LNK2038/LNK2019Debug和Release的lib混用现象C编译阶段一切正常链接阶段报一堆LNK2038: mismatch detected for RuntimeLibrary或LNK2019: unresolved external symbol而且报错的函数大多是OpenCV或Qt的内部符号。原因绝大多数情况是Debug工程链接了release版本的lib。以OpenCV为例debug库是opencv_world460d.librelease库是opencv_world460.lib两个库的运行时库定义不同/MDd 与 /MD混链必然触发LNK2038。Qt的库同理Qt5Cored.lib和Qt5Core.lib不能混用。解决回到2.2节的表格把附加依赖项按当前配置重新核对一遍。我通常的做法是Debug配置下只写带d的库Release配置下只写不带d的库并且在属性页的“配置”下拉框里分别选择“Debug”和“Release”各配一次不要在“所有配置”里配一次就完事——你需要能区分两份依赖表。5.2 fatal: cannot mix incompatible Qt library (version ex50601) with this library现象程序编译运行都没问题但启动瞬间弹出一个Qt错误对话框英文提示类似fatal: cannot mix incompatible Qt library (version ex50601) with this library后面还带版本号。原因这个ex50601是Qt编码过的版本号对应某个具体Qt版本。这个报错的本质是程序编译时用的Qt头文件版本和程序运行时加载到的Qt5Core.dll版本不一致。最常见的场景是PATH环境变量里排在前面的bin目录属于另一个Qt版本程序启动时加载了错误的dll。比如你机器上装了Qt 5.12和Qt 5.15工程用5.15编译但PATH里5.12的bin在前面。解决先把PATH环境变量里所有Qt相关的路径列出来确认工程用的那个Qt版本的bin目录是否排在前面。我一般直接写一个bat脚本启动程序前临时把Qt的bin目录插到PATH最前面或者在系统环境变量里把不需要的Qt版本bin路径删掉。这个报错偶尔也出现在Qt版本相同、但编译器不同的情况下msvc2017和msvc2019混用处理方法是保持工程和Qt库同为msvc2019_64。5.3 qt.qpa.plugin: could not find the Qt platform plugin windows现象程序启动直接崩控制台输出qt.qpa.plugin: could not find the Qt platform plugin windows in ...退出代码不是0有时候是-10737418190xC0000005访问冲突。原因Qt程序启动时需要加载平台插件Windows下是platforms/qwindows.dll这个插件默认位于Qt安装目录的plugins\platforms下。VS里直接F5运行时如果Qt的bin目录不在PATH里或者QT_QPA_PLATFORM_PLUGIN_PATH环境变量没设置Qt就找不到这个dll。很多人在发布时只拷了exe和几个Qt dll忘了把plugins目录一并拷走运行时就报这个错。解决三种方式任选。第一种开发阶段设置环境变量QT_QPA_PLATFORM_PLUGIN_PATH指向D:\Qt\Qt5.15.2\5.15.2\msvc2019_64\plugins第二种把整个plugins目录拷贝到exe所在目录第三种最简单确保Qt的bin目录在PATH里然后VS里把“调试 → 环境”设置为PATH$(QTDIR)\bin;$(PATH)。这个问题在Linux下也常见只是平台插件名变成了linuxfb或xcb报错格式一模一样。5.4 Qt输出中文乱码从源文件编码到显示编码——vs2019这套组合逃不掉的一课现象界面上按钮文字显示成“锟斤拷”“烫烫烫”或者qDebug()输出的中文全是乱码。原因VS2019在简体中文系统下默认把无BOM的UTF-8源文件按GBK代码页936解析。Qt的QString内部是UTF-16源码里写的字符串字面量先被编译器按GBK读成字节流再被Qt按UTF-8/本地编码转成QString中间编码错位显示出来就是乱码。解决分两步。第一步在用到中文字符串的.cpp文件开头加一行#pragma execution_character_set(utf-8)强制MSVC把源码里的窄字符按UTF-8处理。第二步把源文件另存为”UTF-8带签名“即带BOM的格式VS2019里选“文件 → 另存为 → 编码保存 → Unicode (UTF-8 with signature)”。至于qDebug()输出乱码那是控制台代码页的问题可以在main函数开头加system(chcp 65001)或者在项目属性里把控制台代码页切到UTF-8。相比之下用QString::fromUtf8(中文)包裹字符串字面量是最省事的做法但治标不治本后续每个文件都得记得包。5.5 C2065“contourArea”: 未定义标识符现象代码里调用了cv::findContours没报错但调用contourArea时报C2065: contourArea: 未定义标识符后面还跟着C3861: 找不到标识符。原因这个报错有两种可能。一种是你写了contourArea但没加cv::前缀同时文件里又没有using namespace cv;编译器压根不知道这个函数。另一种更隐蔽你只包含了opencv2/core.hpp和opencv2/highgui.hpp但contourArea和findContours声明在opencv2/imgproc.hpp里头文件没包含全编译器同样报未定义。解决先确认包含#include opencv2/imgproc.hpp再给函数加cv::前缀写成cv::contourArea(contour)。这个坑在OpenCV 4.x里尤其常见因为OpenCV 3.x有些例程靠highgui.hpp间接引入了imgproc4.x里模块边界更严格必须显式包含。代码里如果同时用了findContours和contourArea检查一下是否一个写前缀另一个没写——我在真实项目里就见过这种一半一半的写法编译报错指向后一个函数排查半天。6. 把处理流程封装成可复用模块一个二值化轮廓检测的最小验证6.1 封装独立处理函数前面4.4节把显示刷新封装了这一节把算法处理进一步抽成独立模块。以“二值化 轮廓检测”为例这是视觉项目里最常用的组合也是helloworld这类实例往后走必然要碰的方向。我一般会写一个不依赖界面类、只接收Mat并返回Mat的纯函数这样脱离Qt也能单测cv::Mat processImage(const cv::Mat src, double lowThresh, double highThresh) { cv::Mat gray, blurred, binary, edges; if (src.channels() 3) cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); else gray src.clone(); cv::GaussianBlur(gray, blurred, cv::Size(3, 3), 0); cv::Canny(blurred, edges, lowThresh, highThresh); cv::Mat kernel cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3)); cv::morphologyEx(edges, edges, cv::MORPH_CLOSE, kernel); std::vectorstd::vectorcv::Point contours; cv::findContours(edges, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); cv::Mat result src.clone(); cv::drawContours(result, contours, -1, cv::Scalar(0, 0, 255), 2); return result; }两个参数lowThresh/highThresh暴露给界面便于实时调Canny阈值。函数内部用MORPH_CLOSE闭运算把断裂的边缘连成闭合轮廓再drawContours画到原图上。这样的封装主界面调起来只是一行updateDisplay(processImage(src, ui-lowSpin-value(), ui-highSpin-value()))。6.2 用一张简单图验证全流程验证方式建议这样找一张背景简单、目标物体轮廓清晰的图比如白色背景上一个深色瓶子跑上面的函数。肉眼判断三个指标轮廓是否闭合、边缘是否有明显断裂、原图上是否有误检的碎片轮廓。如果断裂多把闭运算核从Size(3, 3)加到Size(5, 5)但注意核太大会把两个临近物体糊成一条轮廓。如果误检多提高低阈值或者加一个面积过滤遍历contourscv::contourArea(contour) 100就跳过去不画。这套验证流程的好处是把“处理效果好坏”从主观感受变成了三个可检查的客观标准。我在C视觉项目里搭界面的时候也习惯先用一个独立的process函数把算法边界定死再回头做界面交互。那次改了几版之后再没出现过“界面调好了算法重写、又得从头测”的反复。从那以后我每次拿到一个新下载的QtOpenCV工程都强制自己走一遍固定顺序先确认版本匹配、再跑通空白窗口、然后逐层验证显示链路和处理链路。helloworld这个工程虽然是最小实例但这条链路走通了后面的算法开发和界面扩展都是在同一张骨架上生长的。希望帮到你。本文还有配套的精品资源点击获取
返回列表