资讯动态

Halcon与Qt集成:实现可缩放拖拽的图像显示窗口及灰度值提取

发布时间:2026/9/16 16:03:12 来源:尧图企业网站定制
简介面向Qt与Halcon开发者的C图像处理示例工程演示结合Halcon与Qt框架完成图片加载、缩放、拖拽以及灰度值实时显示的核心流程适合机器视觉界面开发入门或需要快速搭建图像交互工具的中高级开发者参考。压缩包共17个文件、仅18KB包含3个cpp源文件、2个ui界面文件、2个h头文件、pro工程文件、qrc资源文件及6张png图标/示例图片结构精简便于直接导入Qt Creator查看工程配置与实现。目前已有924人学习使用被用作图像显示模块的入门样例。工程代码覆盖QGraphicsView缩放拖拽、QLabel显示图像、鼠标移动时读取灰度值等关键片段同时预留Halcon图像处理接口可据此扩展灰度变换、模板匹配等功能是一份轻量可跑的实践参考。1. 一个能显示灰度值又能缩放拖拽的Halcon图像窗口值得拆开看拿到这个项目的第一感觉是它把 Halcon 和 Qt 接起来的思路很朴素但恰恰是很多机器视觉上位机都会遇到的基本功——图像能显示在界面上能滚轮缩放、能按住拖动鼠标划过时能看到对应像素的灰度值。压缩包里除了MyQtHalcon.pro、qhalconwind.cpp、mainwindow.cpp这些常规文件还带了Circle.png、Rect2.png、reset.png、open.png这类资源图明显是把图像显示交互和 ROI 形态串在了一起。机器视觉开发里Halcon 算得快但界面弱Qt 界面强但图像算法弱这两者的桥接从来不是把图像转成 QImage 那么简单。你会遇到图像数据在 HObject 里的存储格式问题会遇到 QGraphicsView 缩放时默认锚点在左上角的别扭感还会遇到鼠标拖拽和灰度值获取相互打架的情况。这个 demo 最有价值的地方在于它把整条链路完整走了一遍适合两类人一是刚把 Halcon 接进 Qt 界面的新手二是想彻底搞清 QGraphicsView 交互边界的熟手。2. 让Halcon跑进Qt工程pro配置与运行时DLL的坑2.1 先从依赖讲清楚 Halcon 和 Qt 的关系Halcon 提供的是 C 接口HalconCpp它自身不依赖 Qt只负责图像算法和结果计算。Qt 负责的是窗口、事件循环、鼠标交互和像素级显示。两者唯一的连接点是Halcon 把图像数据从文件或相机读进HObjectQt 把HObject转成QImage后显示。所以工程配置要解决三件事头文件路径、库文件路径、运行时 DLL 路径。在 Windows 上我一般用 MSVC 2019 64 位编译链配 Qt 5.15.2因为 Halcon 官方发布的库是 MSVC 编译的如果用 MinGW 去链接最容易遇到符号不匹配的问题。下面这份MyQtHalcon.pro是整理后的版本直接可以套用QT core gui widgets TARGET MyQtHalcon TEMPLATE app HALCON_ROOT C:/Program Files/MVTec/HALCON-20.11 INCLUDEPATH $$HALCON_ROOT/include/halconcpp \ $$HALCON_ROOT/include LIBS -L$$HALCON_ROOT/lib/x64-win64 \ -lhalconcpp \ -lhalcon SOURCES main.cpp \ mainwindow.cpp \ qhalconwind.cpp HEADERS mainwindow.h \ qhalconwind.h FORMS mainwindow.ui \ qhalconwind.ui RESOURCES image.qrcHALCON_ROOT换成你自己安装的版本目录LIBS里-lhalconcpp是 C 接口库-lhalcon是核心算法库这两个缺一不可。注意INCLUDEPATH里include/halconcpp要在前面因为HalconCpp.h内部会用相对路径引用include下的公共头文件顺序反了会报找不到头文件错误。2.2 运行时 DLL 和 license 问题比编译更常踩编译过了只是第一步运行时报错才是最让人头疼的。最常见的是启动时提示找不到halcon.dll或halconcpp.dll解决办法是把这两个 DLL 从HALCON_ROOT/bin/x64-win64拷贝到 exe 目录或者把该目录加进系统PATH。这里有个坑用windeployqt打包时Qt 会把自己的 DLL 全部带上但不会主动拷贝 Halcon 的运行时库所以手动拷贝这一步必须做否则换一台电脑运行就崩。另一个运行期典型报错是halcon can not find feature in这类提示通常不是代码逻辑错误而是 Halcon 运行时找不到 license 文件或者HALCONROOT环境变量没有指向安装目录。可以在系统环境变量里检查一下HALCONROOT是否指向C:/Program Files/MVTec/HALCON-20.11同时确认%HALCONROOT%/license目录下有有效的 license 文件。此时还要检查程序有没有以管理员权限运行因为某些 Halcon 版本在读写 license 时有目录权限要求。qt_qpa_platform_plugin_pathd:\qt\5.15.2\msvc2019_64这类报错是平台插件加载失败问题出在QT_QPA_PLATFORM_PLUGIN_PATH环境变量被人为设置了错误的路径。正常情况 Qt 会从 exe 相邻的platforms目录加载qwindows.dll不需要手动指定这个环境变量。如果调试时确实设置了把它删掉再确认platforms目录存在即可。配置项作用常见错误INCLUDEPATH指定 Halcon 头文件目录顺序颠倒导致相对引用失败LIBS指定 halconcpp 和 halcon 库缺少-lhalcon导致链接阶段符号未定义HALCONROOT运行时定位 license 和模块未设置导致can not find feature inDLL 拷贝保证 exe 独立运行windeployqt不打包 Halcon 运行时3. 图像加载链路从HObject到QImage的桥接实现3.1 为什么不能直接把 HObject 显示出来Halcon 的HObject内部是一个图像数据容器它管理的是 Halcon 自己的内存池结构体定义对外部完全透明。Qt 的QImage则是把像素数据按行排列的 RGB 或灰度缓冲区。两者之间没有一个万能转换函数必须自己写桥接代码。Halcon 官方其实有例程核心思路是用GetImagePointer1拿到图像数据的首地址和宽高再把这些数据按 Qt 能识别的格式封装成QImage。这个转换最关键的一点是GetImagePointer1返回的是 Halcon 内部缓冲区的指针不是拷贝。如果直接拿这个地址去构造QImage等 Halcon 对象析构或者下一次读取图像后这个指针就失效了界面上会出现黑图或者花屏。所以转换函数里必须做一次深拷贝让QImage持有自己的数据副本。3.2 灰度图和彩色图的两条转换路径下面这段是我在实际项目中整理的转换函数处理了单通道灰度图和多通道彩色图两种情况同时对 16 位图做了降位处理QImage HObjectToQImage(const HObject hobj) { HString cType; HTuple width, height; HImage himage(hobj); void* ptr nullptr; himage.GetImagePointer1(ptr, cType, width, height); int w width[0].I(); int h height[0].I(); if (cType byte ptr ! nullptr) { // 单通道 8 位灰度图 QImage img((const uchar*)ptr, w, h, w, QImage::Format_Indexed8); QVectorQRgb colorTable(256); for (int i 0; i 256; i) colorTable[i] qRgb(i, i, i); img.setColorTable(colorTable); return img.copy(); // 深拷贝脱离 Halcon 内存管理 } if (cType int2 || cType uint2) { // 16 位深度图先线性拉伸到 8 位再显示 HImage scaled; HImage himg16(hobj); ScaleImageMax(himg16, scaled); HImage byteImg; ConvertImageType(scaled, byteImg, byte); return HObjectToQImage(HObject(byteImg)); } // 到此说明不是单通道灰度图按 RGB 三通道处理 void* r nullptr, *g nullptr, *b nullptr; HImage himg3(hobj); himg3.GetImagePointer3(r, g, b, cType, width, height); if (cType ! byte || r nullptr || g nullptr || b nullptr) return QImage(); QImage rgbImg(w, h, QImage::Format_RGB888); for (int row 0; row h; row) { const uchar* pr (const uchar*)r row * w; const uchar* pg (const uchar*)g row * w; const uchar* pb (const uchar*)b row * w; uchar* dst rgbImg.scanLine(row); for (int col 0; col w; col) { dst[col * 3 0] pr[col]; dst[col * 3 1] pg[col]; dst[col * 3 2] pb[col]; } } return rgbImg; }第一段代码里Format_Indexed8配合颜色表是显示灰度图最快的路径img.copy()保证了QImage拥有一份独立内存Halcon 对象释放后界面不会花掉。第二段处理int2/uint2深度图时ScaleImageMax会把灰度范围线性拉伸到 0255ConvertImageType再转成 byte这样 16 位图也能正常显示在普通屏幕上。第三段GetImagePointer3返回的是三个独立通道的指针按行scanLine写入Format_RGB888注意 Halcon 的通道顺序是 R、G、B和 QImage 的 RGB888 一致不需要交换通道。3.3 转完之后挂到 QGraphicsScene 上显示图像转成QImage之后显示建议用QGraphicsView QGraphicsScene而不是QLabel。因为后者没有内建的缩放和平移能力所有交互都要自己写。QGraphicsScene负责管理图像项QGraphicsView负责视口渲染和与用户的交互void QHalconWind::LoadImage(const QString filePath) { HObject img; ReadImage(img, filePath.toStdString().c_str()); m_image HObjectToQImage(img); m_halconImage img; // 保存副本后续取灰度值用 m_scene-clear(); m_item m_scene-addPixmap(QPixmap::fromImage(m_image)); m_scene-setSceneRect(m_item-boundingRect()); ui-graphicsView-resetTransform(); ui-graphicsView-centerOn(m_item); }m_scene-clear()是清掉上一次的 pixmap 项避免重复加载时图像叠加。setSceneRect把场景边界锁定到图像区域这样视图滚动时不会留出一大片空白。centerOn让图像第一次加载时就居中显示这是 QLabel 方案做不到的体验。这里保存了一个m_halconImage副本后面第 5 章取灰度值会用到避免每次鼠标移动都重新读文件。4. 缩放和拖拽AnchorUnderMouse与ScrollHandDrag的真正边界4.1 缩放前必须理解 QGraphicsView 的三层坐标系QGraphicsView内部有三套坐标视图坐标viewport 上的像素位置、场景坐标scene 中的逻辑位置、项目坐标pixmap 项内的位置。滚轮缩放本质是给 view 的 transform 施加 scale 变换而不是修改图像本身。setTransformationAnchor(QGraphicsView::AnchorUnderMouse)的含义是缩放之后鼠标下方的那个场景点仍然保持在鼠标正下方。如果不设置这个属性默认锚点是AnchorViewCenter你会看到图像朝视口中央缩放鼠标滚到哪里都没用图像总是跑偏。AnchorViewCenter和AnchorUnderMouse的差异在标注测量时尤为明显。做视觉定位项目时用户通常会把鼠标移到要放大的缺陷附近此时必须用AnchorUnderMouse。AnchorNoAnchor则完全不调整滚动条适合写程序化缩放时用。这三者的适用场景差异不小我简单列个表方便对照锚点模式缩放中心适用场景AnchorViewCenter视口中心程序自动缩放、适合全图预览AnchorUnderMouse鼠标所在场景点人工查看图像细节、缺陷定位AnchorNoAnchor不调整滚动条多视图联动、自定义动画缩放4.2 滚轮缩放与手动拖拽的完整实现关于拖拽网上教程普遍推荐setDragMode(QGraphicsView::ScrollHandDrag)这个模式确实能快速实现手型拖拽。但是当你需要同时在鼠标移动时取灰度值、画 ROI 或者显示坐标提示时ScrollHandDrag会吞掉一部分鼠标事件导致你的mouseMoveEvent收不到实时坐标两个功能互相打架。我一般会放弃内置拖拽自己重写鼠标事件来控制滚动条void QHalconWind::wheelEvent(QWheelEvent* event) { qreal factor 1.15; if (event-angleDelta().y() 0) factor 1.0 / factor; ui-graphicsView-scale(factor, factor); event-accept(); }wheelEvent里factor取 1.15 是比较舒服的步进一次滚轮约放大 15%。向下滚动时angleDelta().y()为负factor变成约 0.87 实现缩小。这里不用自己计算锚点位置因为构造函数里已经设置过AnchorUnderMouse视图会自动保持鼠标下的场景点不动。void QHalconWind::mousePressEvent(QMouseEvent* event) { if (event-button() Qt::LeftButton) { m_lastPos event-pos(); ui-graphicsView-setCursor(Qt::ClosedHandCursor); event-accept(); } } void QHalconWind::mouseMoveEvent(QMouseEvent* event) { if (event-buttons() Qt::LeftButton) { QPoint delta event-pos() - m_lastPos; m_lastPos event-pos(); QScrollBar* hBar ui-graphicsView-horizontalScrollBar(); QScrollBar* vBar ui-graphicsView-verticalScrollBar(); hBar-setValue(hBar-value() - delta.x()); vBar-setValue(vBar-value() - delta.y()); } ShowGrayValue(event-pos()); // 灰度值函数见第 5 章 } void QHalconWind::mouseReleaseEvent(QMouseEvent* event) { ui-graphicsView-setCursor(Qt::CrossCursor); event-accept(); }拖拽的数学原理很简单鼠标向右移动时图像也想向右跟随那滚动条的值必须减小。所以delta.x()是负号接入setValue。m_lastPos用来计算两次事件之间的相对位移而不是鼠标在图像上的绝对坐标否则拖一屏图片会非常卡。mouseReleaseEvent里把光标改回十字形这是因为图片查看器通常要提示用户「当前不是拖拽状态」。这个方案和ScrollHandDrag的区别在于按下左键拖的是滚动条松开后立即恢复鼠标事件mouseMoveEvent始终能拿到坐标为灰度值显示留出了干净的通道。4.3 大图缩放时的绘制性能优化图像文件动辄几千乘几千像素QGraphicsView默认每帧全量重绘会明显掉帧。一般会打开这三个开关ui-graphicsView-setRenderHint(QPainter::SmoothPixmapTransform, true); ui-graphicsView-setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); ui-graphicsView-setOptimizationFlag(QGraphicsView::DontAdjustForAntialiasing, true);SmoothPixmapTransform让缩放后的图像边缘不出现锯齿视觉上更平滑。BoundingRectViewportUpdate只在鼠标拖动或缩放导致的矩形区域变化时重绘避免整帧刷新。DontAdjustForAntialiasing减少抗锯齿对绘制区域的额外膨胀缩放时画面更跟手。这三个参数是 QGraphicsView 性能调优里最常用的组合实际项目里基本不会再加更多因为再往下的瓶颈就是 Halcon 的图像处理了。5. 鼠标下的灰度值GetGrayval与QImage像素读取谁更合适5.1 两条技术路线的选择逻辑显示灰度值这件事从 Qt 侧也能做把QImage转成Format_Grayscale8然后用QImage::pixelColor()逐点取灰度。这条路的问题是mouseMoveEvent每秒触发几十次每次调用pixelColor都要走一遍颜色空间转换性能开销比想象中大。更关键的是很多项目里你已经持有 Halcon 的HImage对象这时用GetGrayval直接拿灰度值省掉一次 Qt 侧的转换逻辑上也更统一。Halcon 的GetGrayval(Image, Row, Column, Grayval)是专门为单像素灰度提取设计的第一个参数传HImage第二、第三个参数是行和列注意顺序是「行在前、列在后」很多时候写成GetGrayval(img, x, y)的人都会在这里翻车出来的灰度值完全是乱的。返回值是HTuple里面存放的是实际灰度数值。下表是两种方式的适用边界方案性能适用场景QImage::pixelColor()慢每次都要颜色转换纯 Qt 项目、不引入 Halcon 算法GetGrayval()快直接读内存与 Halcon 图像处理共存的视觉项目5.2 在鼠标事件里取灰度值的实现既然第 4 章已经绕开了ScrollHandDragmouseMoveEvent就可以放心做两件事拖动画布和读坐标。下面这段是和 4.2 配套的灰度值显示代码void QHalconWind::ShowGrayValue(const QPoint viewPos) { if (m_halconImage.IsInitialized() false) return; QPointF scenePos ui-graphicsView-mapToScene(viewPos); int col qRound(scenePos.x()); int row qRound(scenePos.y()); if (col 0 || row 0 || col m_halconImage.Width() || row m_halconImage.Height()) { ui-statusBar-showMessage(QString(Gray: --)); return; } HImage himg(m_halconImage); HTuple grayValue; himg.GetGrayval(row, col, grayValue); // 注意先传行再传列 int gray grayValue[0].I(); ui-statusBar-showMessage( QString(X: %1, Y: %2, Gray: %3).arg(col).arg(row).arg(gray)); }mapToScene把视口坐标转成场景坐标这个场景坐标就是图像本身的像素坐标因为 pixmap item 的坐标系和像素坐标一一对应。qRound做四舍五入避免取到 3.7 这种非整数像素位置。边界判断非常重要否则鼠标移到图像外面时GetGrayval会抛异常。himg.GetGrayval(row, col, grayValue)中row对应 y 轴、col对应 x 轴这个顺序和图像数组的行列语义一致和 Qt 的x, y参数顺序正好相反是这章最容易写错的地方。如果加载的是彩色图直接调GetGrayval会返回 RGB 三通道的 tuple取灰度值前应先转一次HImage himg(m_halconImage); HImage grayImg; rgb1_to_gray(himg, grayImg); HTuple grayValue; grayImg.GetGrayval(row, col, grayValue);rgb1_to_gray使用标准的0.299R 0.587G 0.114B权重把彩色图转成灰度图。这个转换只影响灰度值的计算不会改变界面上显示的原图属于典型的「算法侧处理、显示侧不变」的模式。5.3 灰度值类型和位深带来的显示细节Halcon 的GetGrayval返回的灰度值类型跟随图像类型。8 位 byte 图返回 0255 的整数16 位图返回可能到 65535。直接把这些数值显示在状态栏没问题但如果要叠加到图像上用QPainter绘制文字或伪彩色就得先做归一化否则 16 位图全屏都是黑的——因为 8 位显示上限 255 容纳不下 16 位的数据范围。此时可以用ScaleImageMax拉伸后再提取灰度值保证视觉上的映射关系是符合直觉的。还有一个工程细节如果图像是 HALCON 从相机实时采集的HObject的内存会被采集线程反复覆盖此时mouseMoveEvent里直接GetGrayval可能读到半帧数据。稳妥做法是在采集回调里用CopyObj复制一份图像到 UI 线程。这个 demo 是静态读图不涉及线程竞争但做在线检测项目时这行代码能省掉很多难以复现的崩溃。6. 用图像金字塔验证缩放重采样对灰度值的影响6.1 显示缩放和图像重采样是两码事很多人会误以为第 4 章的滚轮缩放改变了图像本身其实 QGraphicsView 只是在显示层做变换图像像素没动过。真正改变像素的是 Halcon 侧的图像缩放比如ZoomImageFactor。搞清楚这个差别对灰度值验证很有意义我可以用小倍率缩放前后对比灰度差异来判断一个图像处理流程引入的误差有多大。6.2 ZoomImageFactor 做金字塔重采样与灰度对比写一个函数把原图缩小一半再放大两倍然后对比同一坐标点的灰度值变化void CompareZoomGray(const HImage src, int row, int col) { HImage small, restored; ZoomImageFactor(src, small, 0.5, 0.5, constant); ZoomImageFactor(small, restored, 2.0, 2.0, constant); HTuple srcGray, resGray; HImage s(src); s.GetGrayval(row, col, srcGray); restored.GetGrayval(row, col, resGray); int diff qAbs(srcGray[0].I() - resGray[0].I()); qDebug() Gray diff: diff; }ZoomImageFactor第四、第五个参数是宽高缩放系数最后一个参数是插值方式。constant是最近邻插值速度快但会丢失细节bilinear是双线性插值平滑效果好bicubic是双三次插值适合精度要求高的测量场景。这个对比结果能直观告诉你如果是做像素级灰度测量缩放后直接取灰度值是不可靠的应该在处理链路里保持原始分辨率。6.3 这个技巧在现实项目里的判断标准拿缩放重采样带来的灰度偏差和GetGrayval的接口配合起来可以形成一个简单的图像质量评估偏差小于 2 个灰度级说明插值方式对本场景不敏感可以直接用偏差超过 10 个灰度级就要格外小心后续的阈值分割和边缘提取结果都可能被重采样污染。在 Halcon 的测量应用里推荐用ZoomImageFactor的bicubic配合GaussianFilter预处理能有效抑制缩放产生的振铃效应。如果你的项目只是做界面展示和人工观察constant足够因为显示缩放本身不修改图像数据灰度值依然来自原始图像。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价