Qt+VS+Halcon集成:机器视觉二维码识别的工程化实践
2026/9/11 2:27:02 网站建设 项目流程

简介:面向需要在桌面应用或工业上位机中集成二维码识别功能的Qt开发者,这份资源基于Visual Studio与Halcon机器视觉库,完整演示了从摄像头图像采集、灰度与滤波预处理、二维码检测,到最终解码输出与界面展示的实现过程。工程代码组织清晰,涵盖Halcon和Qt的混合编程、按钮触发扫描、结果显示、异常捕获、扫描状态反馈等关键模块,适合具有一定C++基础、准备将商业视觉库融入实际项目的学习者参考。压缩包共227个文件,主要包含C++源文件、头文件、静态库、Qt界面文件、工程配置与资源文件,以及预编译生成的可执行程序、调试符号文件与运行日志,便于边运行边对照源码分析,整体大小约83.58MB。该资源已有114人浏览学习。拿到工程后,既可在Visual Studio中直接编译启动,也可以根据源码快速理解Halcon二次开发的调用流程和参数设置,尤其适合需要快速验证二维码识别效果的实验场景,对毕业设计、课程设计或搭建扫描原型都有直接帮助。

1. 为什么是 QT+VS+Halcon 而不是扫码枪模块

产线上的读码场景比想象中麻烦得多:手机屏幕上的二维码会因为贴膜产生摩尔纹,金属件上的激光打标码在侧光下对比度极低,普通扫码枪遇到这些情况要么反复触发要么直接漏读。我拆过的这套项目不是单纯调库,而是把 Halcon 的机器视觉算子嵌进 Qt 上位机界面里,在 Visual Studio 中编译成独立桌面程序。压缩包内Scan2DCode.cpp是核心逻辑,moc_Scan2DCode.cpp是 Qt 元对象编译产物,qrc_Scan2DCode.cpp管理资源,Scan2DCode.vcxproj保存工程配置。相比接现成扫码枪,QT+VS+Halcon 方案能拿到原始图像,针对反光、模糊、畸变做预处理,也方便把识别过程和结果直接显示在界面上。适合有 Qt 基础、想自研读码模块的机器视觉工程师。

2. VS 项目里把 Halcon 和 Qt 拧在一起的正确姿势

2.1 先确认 Halcon SDK 版本与 license 状态

Halcon 是商业机器视觉库,它的 C++ 接口halconcpp和 Qt 没有官方绑定,但二者在 Windows 下通过 Visual Studio 可以稳定共存。打开这个项目时我第一件事不是看业务代码,而是确认 Halcon 安装版本和 license 授权范围。Halcon 20.11 以上版本的安装目录通常类似C:\Program Files\MVTec\Halcon-20.11-SP1,环境变量HALCONROOT是否指向它,直接影响 VS 里附加目录的解析。如果打开 VS 编译链接时报halcon can not find feature in license,优先检查 license 文件而不是重新安装 SDK。

在开发机上,我习惯用一个批处理固定环境变量,避免 VS 调试器加载到错误版本的 DLL:

set HALCONROOT=C:\Program Files\MVTec\Halcon-20.11-SP1 set HALCONARCH=x64-win64 set PATH=%HALCONROOT%\bin\%HALCONARCH%;%PATH%

说明:HALCONROOT是 Halcon 安装根目录,HALCONARCH必须与 VS 编译目标一致,这里用的x64-win64对应 64 位 Windows 应用。PATH里加上 bin 目录后,调试时才能找到halcon.dllhalconcpp.dll。如果 license 缺少 Data Code 2D 模块,find_data_code_2d不会在创建模型时报错,而是执行到解码时才抛出Feature is not supported。所以这个环境检查必须放在写业务代码之前,否则后面所有识别流程都跑不通。

2.2 属性页三件套:include、lib 与 Platform 匹配

Halcon 的 C++ 接口分两层:旧式算子函数HOperatorSet::FindDataCode2d和基于类的HImage。VS 里要同时使用两者,需要包含目录$(HALCONROOT)\include$(HALCONROOT)\include\halconcpp,链接器库目录使用$(HALCONROOT)\lib\x64-win64,附加依赖项填写halconcpp.lib。最容易踩坑的是平台选择:Halcon 的 x86 和 x64 目录彼此独立,而 Qt 5.15.2 msvc2019_64 只支持 64 位,所以解决方案平台必须选x64,否则链接时会出现LNK2019 无法解析的外部符号,因为导入库架构不匹配。

维护多台开发机时,直接改.vcxproj既容易冲突,也不方便同步。我习惯单独放一个属性表Vision.props,在其他工程项目里点一次<Import>就能复用:

<Project> <PropertyGroup> <HALCONROOT>C:\Program Files\MVTec\Halcon-20.11-SP1</HALCONROOT> </PropertyGroup> <ItemDefinitionGroup> <ClCompile> <AdditionalIncludeDirectories>$(HALCONROOT)\include;$(HALCONROOT)\include\halconcpp;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> </ClCompile> <Link> <AdditionalLibraryDirectories>$(HALCONROOT)\lib\x64-win64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalDependencies>halconcpp.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> </ItemDefinitionGroup> </Project>

说明:AdditionalIncludeDirectories里的两个 include 缺一不可,halconcpp头文件依赖上层 include 的公共头文件;AdditionalDependencies只加halconcpp.lib,不要加halcon.lib,后者是纯 C 接口库,和 halconcpp 混链容易造成符号重复。属性表的好处是支持版本升级,下次把 Halcon 换到 23.05 时,只需要改一处HALCONROOT

2.3 用 ReadImage 与图像尺寸输出验证环境是通的

很多人一上来就写识别,结果环境没通,排查成本极高。稳妥做法是先建一个测试函数,读取本地二维码图片,把长宽和通道数打印出来。如果这一步能通过,说明 include、lib、dll 三者的通路已经打通。

#include "halconcpp/HalconCpp.h" using namespace HalconCpp; void CheckHalconEnv() { HImage testImg; try { testImg.ReadImage("G:/qr_samples/qr_chip.png"); // 替换为你的测试图 int w = testImg.Width(); int h = testImg.Height(); int ch = testImg.CountChannels(); qDebug() << "Image size:" << w << "x" << h << "channels:" << ch; // 如果输出 640x480 3,说明 Halcon 库已被正确加载 } catch (HException &e) { qDebug() << "Halcon error:" << e.ErrorCode() << QString::fromStdString(e.ErrorMessage()); } }

说明:ReadImage会按扩展名自动解析图片格式,返回HImage类;Width()Height()CountChannels()是 halconcpp 的便捷方法,比HOperatorSet::GetImageSize少写两个临时HTuple。这里使用qDebug而不是printf,是为了让输出统一进入 Qt 日志通道,便于后面接入日志窗口。

环境测试通过后可以对照下表排查异常:

现象常见根因排查动作
启动提示halconcpp.dll 缺失运行时 PATH 未包含HALCONROOT\bin\x64-win64在属性表生成前使用批处理设置 PATH
编译通过但链接报 LNK2019Debug 工程链上了 Release lib统一 x64 Debug/Release 配置,建议两种都跑一次
界面启动崩溃并提示qt_qpa_platform_plugin_pathwindeployqt 未生成 platforms 目录开发环境先确认 Qt 插件目录,发布时用 5.1 节命令
运行时报 license feature 不支持授权文件不含 Data Code 2D 模块用官方 license_admin 检查 feature 列表

3. find_data_code_2d 为核心:二维码定位、解码与参数调优

3.1 创建二维码模型:create_data_code_2d_model

Halcon 识别二维码的核心算子组合是create_data_code_2d_modelfind_data_code_2dcreate_data_code_2d_model的第一个参数传"QR Code",它会载入 Halcon 内置的 QR 码训练模型。这个模型不是传统模板匹配,而是基于码的 Finder Pattern 结构完成定位和解码,所以它对旋转、透视畸变的容忍度比 OpenCV 的QRCodeDetector高很多。项目里Scan2DCode.cpp的主要任务就是围绕这两个算子做封装。

HImage image; image.ReadImage("G:/qr_samples/qr_rotate.jpg"); HTuple codeHandle, symbolRegions, decodedResults; try { HOperatorSet::CreateDataCode2dModel("QR Code", HTuple(), HTuple(), &codeHandle); HOperatorSet::SetDataCode2dParam(codeHandle, "default_parameters", "enhanced"); HOperatorSet::FindDataCode2d(image, &symbolRegions, codeHandle, HTuple(), HTuple(), &decodedResults); if (decodedResults.Length() > 0) { std::string content = decodedResults[0].S(); qDebug() << "Decoded:" << QString::fromStdString(content); } HOperatorSet::ClearDataCode2dModel(codeHandle); } catch (HException &e) { qDebug() << "Error:" << e.ErrorCode() << QString::fromStdString(e.ErrorMessage()); }

说明:FindDataCode2d参数从左到右依次是输入图像、输出区域、模型句柄、附加参数字符串、附加参数值、输出解码结果。把附加参数留空,所有配置通过SetDataCode2dParam提前写入模型句柄,这样相机循环里无需再传配置,识别效率更高。symbolRegions是找到的码区域轮廓,后续在 Qt 界面上画框、画 ROI 都依赖这个输出。

default_parameters有三个常用档位:standardenhancedmaximum_recognition。区别在模型内部对模糊、反光、遮挡的搜索策略,耗时差异很大,实测参考如下:

参数值适用场景单帧耗时参考
standard打印清晰、光照稳定的纸面标签约 8~15 ms
enhanced手机屏幕码、轻度反光约 20~40 ms
maximum_recognition金属刻印、严重反光、残缺码约 60~150 ms

注意参数值是字符串,必须写成"enhanced"。上线前我通常先用maximum_recognition跑一遍离线图库,确认能识别后,再降回enhanced以保住帧率。

3.2 图像预处理:灰度化、滤波、增强的取舍

Halcon 的find_data_code_2d内部会做归一化处理,但这不代表可以完全不做预处理。彩色相机拍到的通常是 3 通道 RGB,而二维码只关心明暗变化,多通道颜色反而容易干扰对比度计算。常见做法是先判断通道数,转灰度后做中值滤波和直方图均衡化。

HImage colored, gray, filtered, boosted; colored.ReadImage("G:/qr_samples/qr_chip_color.png"); if (colored.CountChannels() == 3) HOperatorSet::Rgb1ToGray(colored, &gray); else gray = colored; filtered = gray.MedianImage("circle", 3, 3); boosted = filtered.EquHistoImage(); HOperatorSet::FindDataCode2d(boosted, &symbolRegions, codeHandle, HTuple(), HTuple(), &decodedResults);

说明:Rgb1ToGray将 RGB 转为灰度图,保留亮度信息,去掉色相干扰。MedianImage使用 3x3 圆形结构元素做中值滤波,可以去掉相机传感器噪点,同时保留二维码边缘。EquHistoImage做直方图均衡化,把低对比度图像的灰度范围拉伸,提升minimum_contrast的命中率。

参数上要注意MedianImage的核半径以像素为单位。当二维码模块宽度小于 3 像素时,滤波核必须小于模块宽度,否则码本身会被当成噪声抹掉。亮度不稳定的场景不要手动二值化,Halcon 的 find 算子内部能处理局部阈值,提前二值化反而会丢掉阴影区域的细节。

3.3 set_data_code_2d_param 调参与识别失败复现

find_data_code_2d返回空字符串或抛异常时,先不要怀疑算子本身,先检查模型参数是否针对场景收敛。SetDataCode2dParam支持很多底层参数,实际项目中最常调的是下面几个:

  • minimum_contrast:默认 10,代表最小的边缘对比度。低对比度场景调到 2~5,反光严重时反而要调高到 15 以上,减少误检。
  • module_width_min/module_width_max:限定二维码模块宽度范围。固定安装距离时给窄范围能大幅提速,例如 4~8 像素。
  • polarity"dark_on_light""light_on_dark",用于深色底上的浅色码。
  • persistence0用于单张拍摄,1用于连续视频流,能复用上一帧位置信息。

调试时最有用的操作是把候选区域导出成图片。如果最终识别失败,候选区域叠加在图上,能直观看出是预处理丢了特征,还是搜索范围出了问题:

HTuple candidateRegions; HObject candidateObj; HOperatorSet::GetDataCode2dResults(codeHandle, "candidate_regions", &candidateRegions); HOperatorSet::ConcatObj(symbolRegions, candidateRegions, &candidateObj); WriteImage(candidateObj, "png", 0, "G:/qr_samples/candidates.png");

说明:GetDataCode2dResults返回候选区域句柄数组,ConcatObj把候选区域和最终识别区域拼在一起输出。如果候选区域布满整张图,说明minimum_contrast太低;如果候选区域集中但最终解码为空,说明是解码阶段失败,应该换maximum_recognition或增强打光。

4. Qt 界面显示 Halcon 图像,信号槽与线程模型

4.1 HObject 转 QImage 的像素格式处理

Halcon 内部图像缓冲区布局和 Qt 不同,不能直接交给QLabel显示,必须拿到像素指针并拷贝到QImage。对于 8 位灰度图,使用Format_Grayscale8;RGB 彩色图使用Format_RGB888。我把转换逻辑封装成公共函数,放在Scan2DCode类里作为静态方法:

QImage HObjectToQImage(const HObject &hobj) { HTuple pointer, type, width, height; HOperatorSet::GetImagePointer1(hobj, &pointer, &type, &width, &height); int w = width[0].I(); int h = height[0].I(); uchar *src = (uchar *)pointer[0].L(); QImage img(w, h, QImage::Format_Grayscale8); memcpy(img.bits(), src, static_cast<size_t>(w * h)); return img.copy(); }

这里有个常见坑:GetImagePointer1返回的指针指向 Halcon 对象内部内存,只要hobj还在作用域内就是有效的。如果把HObject析构或放入容器提前释放,指针就失效了。所以用memcpy拷贝到 QImage 必不可少,最后的img.copy()则是为了断开 QImage 可能存在的隐式共享,避免缓冲区被外部修改。

彩色图可以用GetImagePointer3分别取 R、G、B 三个通道,再拼到Format_RGB888QImage中。更简单的做法是先调用ChangeFormatConvertImageType把彩色图统一转成 byte 类型,减少分支判断。

4.2 用 QThread 隔离 Halcon 算子,避免 UI 卡顿

find_data_code_2d在 500 万像素图像上结合enhanced模式,单帧耗时可到 150ms 左右。如果直接写在按钮点击槽里,界面会卡住,鼠标操作失去响应。常见做法是把扫码逻辑放进独立的QObject,用moveToThread放到工作线程执行。

class ScanWorker : public QObject { Q_OBJECT public slots: void grabAndDecode() { HImage frame; // 从相机采集接口得到 frame,此处省略具体采集代码 HTuple codeHandle, result, symbolRegions; HOperatorSet::CreateDataCode2dModel("QR Code", HTuple(), HTuple(), &codeHandle); HOperatorSet::SetDataCode2dParam(codeHandle, "default_parameters", "enhanced"); HOperatorSet::FindDataCode2d(frame, &symbolRegions, codeHandle, HTuple(), HTuple(), &result); QString text = result.Length() > 0 ? QString::fromStdString(result[0].S()) : QString(); emit decodeFinished(text); HOperatorSet::ClearDataCode2dModel(codeHandle); } signals: void decodeFinished(const QString &text); };

线程启动的惯用写法是:

QThread *thread = new QThread(this); ScanWorker *worker = new ScanWorker; worker->moveToThread(thread); connect(thread, &QThread::finished, worker, &QObject::deleteLater); connect(button, &QPushButton::clicked, worker, &ScanWorker::grabAndDecode, Qt::QueuedConnection); connect(worker, &ScanWorker::decodeFinished, label, &QLabel::setText, Qt::QueuedConnection); thread->start();

说明:moveToThread后,worker 的槽会在thread事件循环里执行。按钮的clicked信号跨线程连接到 worker,必须显式用Qt::QueuedConnection,否则默认直连仍会阻塞 UI 线程。同理,decodeFinished返回 UI 线程也需要 QueuedConnection,才能保证QLabel::setText在主线程安全执行。

线程职责禁止事项
UI 线程显示图像、更新状态、接收结果信号执行 Halcon 长耗时算子
工作线程相机抓帧、预处理、find_data_code_2d直接访问 QWidget 对象
相机线程若使用 GigE 相机,可单独建线程采集与 UI 共用同一图像缓冲

4.3 动态 ROI 与扫描指示器

固定视野下二维码往往只占一小块区域,全图识别既慢又容易误检。项目里我用一个QRubberBand让用户拖拽 ROI,框选后把矩形坐标换算到 Halcon 图像坐标,再用ReduceDomain裁剪出有效区域:

// roiRect 是 QRubberBand 映射到图像尺寸后的矩形 int col1 = roiRect.left(), row1 = roiRect.top(); int col2 = roiRect.right(), row2 = roiRect.bottom(); HObject roiImage, roiRegion; HOperatorSet::GenRectangle1(&roiRegion, row1, col1, row2, col2); HOperatorSet::ReduceDomain(image, roiRegion, &roiImage);

说明:GenRectangle1参数顺序是 row1、col1、row2、col2,先 y 后 x,与 QRect 的 x/y 顺序相反,很多人在这一步写反。ROI 越小,find 算子搜索范围越小,增强模式耗时能从 150ms 降到 30ms。扫描指示器可以是一个QLabel加样式表,在decodeFinished信号里切换绿色和红色,并把解码内容显示出来,用户看到颜色变化就能获知扫码状态。

5. windeployqt 打包 Halcon 运行时与三类典型报错

5.1 部署命令与目录结构

发布到客户机前,先使用windeployqt自动拷贝 Qt 依赖,再手动补齐 Halcon 运行时。在 VS 的开发者命令行里执行:

windeployqt --release --compiler-runtime --dir deploy Scan2DCode.exe xcopy "C:\Program Files\MVTec\Halcon-20.11-SP1\bin\x64-win64\halcon.dll" deploy\ xcopy "C:\Program Files\MVTec\Halcon-20.11-SP1\bin\x64-win64\halconcpp.dll" deploy\ xcopy "C:\Program Files\MVTec\Halcon-20.11-SP1\bin\x64-win64\hdevengine.dll" deploy\ xcopy "C:\Program Files\MVTec\Halcon-20.11-SP1\license" deploy\license\ /E /I

说明:windeployqt会生成platforms\qwindows.dll和 Qt 各模块 DLL,--compiler-runtime会带上 VC++ Redistributable。Halcon 这边必须手动复制halcon.dllhalconcpp.dllhdevengine.dll,license 目录必须搬到部署目录下,否则客户机上会报CAN NOT FIND FEATURE IN LICENSE

5.2 客户机三类典型报错排查

第一类,Halcon license 报错。最常出现在授权文件缺少Data Code 2D模块。可以在开发机上用license_admin.exe检查 feature 列表,确认包含data_code_2d后再分发。

第二类,Qt 平台插件缺失。报错文字里有qt_qpa_platform_plugin_path,说明windeployqt未生成platforms目录。解决方法是重新执行windeployqt,并确认deploy\platforms\qwindows.dll存在。

第三类,相机在客户机上打不开。Halcon 连接工业相机依赖 GenICam runtime,客户机需要安装对应厂商的 GenICam 过滤器和 GigE Vision 驱动,同时设置网卡巨型帧和包大小为 9000,这不是 Halcon 本身的问题。

发布前最终检查依赖,在开发命令行执行:

dumpbin /dependents Scan2DCode.exe | findstr /i "halcon"

如果输出包含halcon.dllhalconcpp.dll,且deploy\platforms\qwindows.dll存在,把一张二维码测试图放到 deploy 目录下运行程序,即可完成冒烟验证。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询