news 2026/10/18 3:07:31

多相机缺陷检测系统实战:VS2015、Qt5.9与Halcon20组合解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多相机缺陷检测系统实战:VS2015、Qt5.9与Halcon20组合解析

前段时间需要维护一套多相机缺陷检测的系统,打开工程一看,编译环境是VS2015、Qt5.9和Halcon20的组合。放在今天来看,这套搭配多少有点“老”,但完整跑过项目、处理完产线上的各种幺蛾子之后,我反而越来越觉得,这三个版本凑在一起不是偶然,而是产线机器视觉设备里非常务实的一种解法。

这篇文章就围绕这套“VS2015 + Qt5.9 + Halcon20”的多个相机缺陷检测源码来讲。我会把系统架构、算法流程、工程坑点、打包部署整个链路都复盘一遍,适合正在做视觉检测设备、或者拿到一套老代码不知道从哪下手的工程师参考。里面提到的思路和踩坑记录都是实际项目里的真东西,不是demo级别的玩具代码。

1. 这个“奇妙组合”是怎么被逼出来的

1.1 需求端:为什么一台设备要装多个相机

我接触的这套源码,对应的是一台外观缺陷检测设备。产品是一个方形结构件,需要检测上下表面和两个侧面,四个工位各装一个相机。检测目标包括划痕、脏污、压伤、缺料、异物,标准还不完全相同——上下表面用明场看大面积脏污,侧面用低角度光抓划痕,其中一个面还要求检测局部微小凸点。

多相机不是简单的“多买几个摄像头”,它带来三个层面的问题:

  • 多路图像要并发采集,帧率还不能掉得太难看;
  • 每个相机的检测参数、ROI区域、打光条件都不一样,必须能独立配置;
  • 产品在流水线上移动,同一个产品不同工位拍出来的图要能关联到一起,便于判定最终OK/NG。

这些需求如果软件架构设计得不好,后面每个环节都会冒问题。所以先明确一点:多相机检测系统的核心难点不在某个算法有多高级,而在多路图像怎么组织、怎么调度、怎么管理各自的参数。

1.2 版本选型:不是念旧,是“刚好每一步都不得不这么做”

选定VS2015、Qt5.9、Halcon20这个组合,很多人第一反应是“怎么还用这么老的版本”。我在项目里也反复权衡过,最后坚持下来是有实际原因的。

先看VS2015。产线工控机很多时候是客户指定的,系统环境陈旧是常态。有些型号的工控机出厂自带的老版运行库、PLC通信库、IO卡驱动,就是基于v140工具集编译的。我们如果拿了VS2022或者更新工具链,编译出的exe一上工控机就容易缺VCRUNTIME140.dll、MSVCP140.dll。VS2015的v140工具集生成的目标文件在兼容性上非常稳,尤其配合Halcon和相机SDK这种吃C++运行时很敏感的库,少掉很多“运行时冲突”的麻烦。

再看Qt5.9。这个版本是Qt官方的LTS长期支持版本,虽然老,但它在老旧显卡驱动的工控机上显示非常稳定,不依赖太新的OpenGL特性。VS2015对应Qt5.9的MSVC2015_64套件,编出来的程序能直接嵌进VS里调试,不需要像MinGW那样处理链接库格式不一致的问题。如果你用MinGW版Qt去链Halcon的MSVC版lib,一堆无法解析的外部符号,非常折磨人。

最后是Halcon20。与其说是选Halcon20,不如说是选它的多相机接口稳定性和并发支持。比它更老的Halcon 13、17在GigE Vision多路取流上偶尔会掉帧,Halcon20在20.11之后对多线程的算子支持明显改善,而且还能直接兼容VS2015。也就是说,在这个组合里,编译器是旧但能跑,界面库是旧但稳,视觉库是新但兼容,三者正好怼到一块去了。

1.3 Halcon20的安装与授权细节:这一步栽过很多次

关于Halcon20安装,我先给一个比较稳的版本:Halcon 20.11 Progress。安装的时候有几个坑值得记一下。

第一,位数必须对齐。小学三年级都知道,但实际项目里总有同事在这个上翻车。相机SDK是64位的,那Halcon就必须装64位,VS工程也必须选x64平台。Halcon安装目录下会分x64-win64和x86-win32子目录,装完以后别装错。三处都是64位,才不会出现链接阶段signature不匹配的问题。

第二,环境变量。正常安装时,安装程序会自动把HALCONROOT指向安装目录,同时把C:\Program Files\MVTec\HALCON-20.11\bin\x64-win64写进PATH。但很多工控机是用的镜像部署系统,环境变量被精简掉,Halcon的DLL找不到,程序一启动就报错。我建议在开发机上专门打开环境变量确认一下:

HALCONROOT=C:\Program Files\MVTec\HALCON-20.11 HALCONARCH=x64-win64 PATH=...\bin\x64-win64;...\bin\x64-win64\dotnet

第三,license。开发调试阶段用试用license或正常采购的开发授权,正式部署时用runtime license。常见问题是把开发版license直接丢到产线电脑上,结果到期后相机全锁,很难堪。还有一个小细节,如果同一台机器装过多个Halcon版本,秋水一样的PATH变量会把版本搞混,插件装完还是提示找不到hdevengine.dll。

装完之后,建议先用Halcon自带的例程验证相机是否都能正常取流。examples\cpp下有现成的open_framegrabber例子,直接改成对应相机IP地址,跑通了再往Qt工程里集成。这个步骤能帮你排除相机、网卡、IP设置、防火墙等底层因素,不然到最后你会发现Qt工程里报错,本质上跟Qt一点关系都没有。

2. 多相机采集与任务流水线:不是“多开几个线程”那么简单

2.1 采集层设计:用Halcon还是相机SDK

多相机采集,第一件事是决定用Halcon的接口直接取图,还是用相机厂商SDK。我在这套源码里两条路都走过,给你说下对比。

如果相机支持GigE Vision或者USB3 Vision标准协议,直接用Halcon的GrabImageAsync是最省力的。好处很明显:

  • 抓出来的图像直接是HImage对象,后续threshold、connection这些算子直接调用,不需要做内存拷贝;
  • Halcon内部对多路相机采集有独立buffer管理,一台相机一路handle,不容易相互干扰;
  • 代码量小,不用维护每家厂商不同的SDK回调。

但某些场景下必须用厂商SDK:比如相机私有的触发模式、多相机硬同步、GPIO控制光源、某些特定型号的相机没有走标准协议。这种情况就老老实实拿SDK回调里的图像buffer,用GenBuffer或者GenImage1包成HObject再进处理管线。

我这套系统里主流相机走的是Halcon GigEVision接口,只有带硬触发联动的关键工位加了一个厂商SDK辅助控制。整体逻辑就是:能用标准协议就用Halcon,必须私有控制才引SDK。

2.2 一个相机一个采集线程 + 检测线程池

多相机采集最容易踩的坑是“一锅炖”——在UI线程里循环反复抓四个相机的图像,然后发现界面卡顿、图像掉帧、CPU占用还爆满。正确的做法是把采集和界面彻底分离。

我给这套源码设计的是这样一套结构:

  • 每个相机一个独立的采集线程(std::thread),内部循环调GrabImageAsync;
  • 抓到图像后塞进一个线程安全的任务队列(带容量上限,避免内存爆掉);
  • 检测线程从队列里取图,跑Halcon算法,得出OK/NG结果和缺陷坐标;
  • 检测结果通过信号槽机制发回主界面,主界面只负责显示和操作交互。

代码骨架大致长这样:

// 采集线程内 HTuple hv_AcqHandle; OpenFramegrabber("GigEVision", 0, 0, 0, 0, 0, 0, "default", -1, "default", -1, "false", "default", cameraIP, 0, -1, &hv_AcqHandle); GrabImageStart(hv_AcqHandle, -1); while (bRunning && !bStopRequested) { HImage image; image.GrabImageAsync(&hv_AcqHandle, -1); if (queue.size() < MAX_QUEUE_LENGTH) { queue.push(std::move(image)); } QThread::msleep(2); } CloseFramegrabber(hv_AcqHandle);
// 检测线程内 try { HImage image; if (queue.try_pop(image, 200)) { DetectorResult result = detector.run(image); emit resultReady(cameraId, result); } } catch (HException& e) { emit errorOccurred(cameraId, QString(e.ErrorMessage().Text())); }

这里有个细节:队列长度一定要设上限。如果检测速度跟不上采集速度,队列无限增长,程序会在一分钟之内吃光所有内存。我一般设100到200帧上限,满了就丢新帧,宁可少检一帧不可让程序崩掉。

2.3 多相机参数与工件ID联动管理

多个相机的检测参数绝不能写死在代码里。原因很简单:产品换型、光源调整、相机角度微调,这些在产线现场时刻都在发生,总不能为了改一个曝光值重新编译一次程序。

我的做法是每个相机一个配置段,全部丢到一个JSON文件里统一管理。内容包含:

  • 相机IP、曝光时间、增益、触发模式;
  • ROI区域坐标;
  • 每一类缺陷算法的使能开关和阈值参数;
  • 检测结果的保存路径、是否存NG图。

换产品的时候界面上下拉选择产品型号,程序自动把所有相机参数加载进来。另外,多相机检测一定要解决“同一产品多工位结果关联”的问题。我这边用条码或者产品流水号做主键,每个相机检测完成后把结果回写到产品记录里,等所有工位都检测完,汇总规则再触发最终判断。这样才不会有“一个面NG了但另外几个面OK,设备却不报警”的低级问题。

3. Halcon缺陷检测三板斧:打光、分割、特征筛选

3.1 打光先行:图像质量决定了算法的天花板

说句得罪人的话,很多团队做缺陷检测遇到算法不稳定,第一反应是疯狂调Halcon算子参数,但真正的问题往往是打光就不对。

我举几个实际例子。检测金属表面划痕,如果用普通的环形LED照明,划痕方向不固定,反光时有时无。后面改成多角度低角度光源,让结构光从侧面掠过表面,划痕散射光明显增强,黑白对比一下就出来了。检测凸点或者颗粒,用暗场照明,背景压暗,颗粒边缘散射光变成亮点,阈值分割极其轻松。

所以在这套源码里,我特别强调一个原则:**算法的任务是稳定提取特征,不是替光源擦屁股。**拿到图像先不要急着写代码,先调光源角度、亮度、相机曝光,让缺陷和非缺陷区域的灰度差值保持在比较明显的水平上。图像质量不好,后面一切算子都是白搭。

3.2 缺陷分割:不同缺陷对应不同的算子套路

在Halcon里做缺陷检测,总的核心思想是“把缺陷区域从背景中剥离出来”。针对不同缺陷类型,我总结了四种最实用的套路:

划痕与浅色脏污,适合用局部动态阈值dyn_threshold。先对原图做均值滤波或高斯平滑,得到一张背景估计图,再用原图和背景图做差,超过动态阈值的像素就是疑似缺陷。

HObject smoothed, diffRegion; MeanImage(image, &smoothed, 21, 21); DynThreshold(image, smoothed, &diffRegion, 5, "dark");

凸点凹点,用中值滤波差分。中值滤波对边缘保持比较好,原图减掉中值结果后,非边缘区域的微小灰度突变会被放大,凸点凹点直接暴露。

规则纹理表面的异常,频域滤波更有效。先做FFT,用滤波器把周期性纹理的频段抑制掉,再反变换回来,纹理变成均匀背景,local defect才显露出来。这个比空域卷积更高效。

大面积脏污或整体灰阶不均,先做背景校正。可以用一个大尺度的高斯滤波作为背景估计,然后原图像素灰度除以背景灰度再乘一个标准值,把光照不均的影响抹平,最后阈值分割。

分割完了,通常还要做一轮形态学清理。connection把连通域打散,opening_circle去掉细小的毛刺噪声,closing_circle填补缺陷内部的小孔洞。

3.3 特征筛选:压误检的关键一步

分割出来的区域并不全是缺陷。金属反光、灰尘、正常结构边缘、螺纹轮廓,这些都有可能被分割出来。这时候就要用select_shape、select_gray这类特征筛选算子。

我个人经验里,最有效的几个特征组合是:

  • 面积area:太小的面积基本是噪点,可以过滤;
  • 宽高比width/height:真正的划痕长宽比很高,灰尘接近圆形;
  • 圆形度circularity:污点通常较圆,划痕通常细长;
  • 灰度均值和标准差:区域亮度与原图对应位置的灰阶差异,可以区分真实缺陷和反光假点。

举个例子,一段疑似划痕区域,面积只有5个像素,宽高比却很大,这时候不能直接判NG,否则正常的微小氧化点都会报警。我会再加一条约束:区域的灰度均值要比周围背景暗多少以上,或者划痕的长度必须超过某个像素值。把这些规则组合起来,误检率能压下来一个数量级。

HObject selected; SelectShape(connected, &selected, "area", "and", 20, 99999); SelectShape(selected, &selected, "width", "and", 3, 99999); SelectShape(selected, &selected, "height", "and", 3, 99999); SelectGray(selected, &selected, image, "mean", "and", 0, 128);

调参这件事,我不建议对着实时视频盲调,而是先收集一批有代表性的样本图,比如100张正常品和50张已知NG品,离线跑一遍算法,把所有候选区域的面积、宽高比、灰度均值统计成一张表,再根据分布去定阈值。这样做出来的参数可解释、可复现,而不是“差不多调到不误报为止”。

4. VS2015与Qt5.9工程里的坑:从环境变量到内存释放

4.1 环境变量与“HALCONROOT找不到”的排查链路

这套源码交付后,最容易出现的第一类报错是启动时弹窗:找不到hdevengine.dll,或找不到halcon.dll,或者“无法定位程序输入点”。

排查顺序不要乱。先确认开发机能正常跑,再拿到工控机或者同事的机器上。常见原因:

  • 没有安装Halcon runtime,或者PATH里没有bin目录;
  • halconcpp.dll存在但依赖的hdevengine.dll不在同目录;
  • 系统里同时装了新老Halcon,PATH顺序被改坏,程序加载到另一个版本的DLL。

解决方式:把Halcon的bin目录显式加到PATH,并且确认HALCONROOT指向正确版本。这类问题在视觉项目里出现频率非常高,我甚至见过有同事把整个Halcon的lib文件夹拷到exe目录,以此解决DLL依赖,虽然丑,但当年确实能救急。反倒是VS2015安装包丢失或者损坏这类问题,多数情况下是Windows Installer缓存(C:\ProgramData\Package Cache)被清理过导致的,碰到就建议直接修复安装VS2015,并保证磁盘空间充足,离线ISO用官方镜像。

4.2 Qt VS Tools 与 moc、uic 生成链问题

VS2015下用Qt开发,需要装Qt VS Tools插件。这个插件负责调用moc(元对象编译器)、uic(界面编译器)、rcc(资源编译器)。实际使用中最常见的坑是:

  • 新增了一个类,里面写了Q_OBJECT,但moc没有自动重新生成,编译报“未定义的虚表”或者“无法解析的外部符号”;
  • 改了UI文件,界面不生效,因为uic没有重新运行;
  • Debug和Release混用,链接报一堆LNK2038之类。

我的经验是,每次从Git上拉完代码先做一次“全部清理”,然后在Qt VS Tools菜单里强制重新运行moc。另外一个被忽视的点是字符集:Halcon20的HTuple默认宽字符处理,Qt默认UTF-8,如果项目设置了多字节字符集,混合使用时容易在字符串传递上出乱码和截断,建议在工程属性里统一使用Unicode字符集。

4.3 图像对象、内存与相机释放的顺序敏感

多相机程序的崩溃重灾区,十个里面有八个跟释放顺序有关。

我整理过一条明确的释放原则:

  1. 先停采集线程,等待线程join退出;
  2. 再释放任务队列里所有未处理的HImage;
  3. 最后再调用CloseFramegrabber关闭相机。

如果先关了相机句柄,采集线程还在下一次GrabImageAsync里,程序就直接访问冲突。如果是用了厂商SDK,顺序变成:先停止采集回调,再释放图像buffer,再卸载SDK库,最后关UI窗口。

另外,Halcon的HObject是引用计数类型,多线程共享同一个HObject时如果一边读取一边析构,会触发计数错乱。所以我在源码里有一条硬性规定:检测线程拿到的是图像的一个独立拷贝,或者检测完成后立刻把对象回收,绝不跨线程共享同一个HObject实例。

还有一个小坑,某些工控机显卡驱动对Qt5.9的渲染支持不完整,图像显示控件频繁刷新会闪烁,这个不是Halcon的问题,解决办法是改用QPixmap离线绘制后一次性贴图,不要在paintEvent里实时做格式转换。

5. 性能优化与产线稳定性:断线重连和误检闭环

5.1 相机断线重连:产线稳定性最容易翻车的点

一套外观检测设备,算法再准确,相机只要掉线一次,整个工位就停摆。GigE相机最怕网线松动、工控机网卡休眠、供电波动。为此我在源码里加了三重保障:

  • 采集线程内对每次抓图设置超时,超过5秒没有新帧就判定断线;
  • 断线后自动进入重连流程,重新OpenFramegrabber,最多重试10次,每次间隔1秒;
  • 界面状态栏实时显示相机连接状态,断开标红,恢复标绿。

重连时有个细节,有些相机在重连前必须调用一下OpenFramegrabber中的reset参数,或者先释放旧句柄再重新获取,直接复用句柄多半会一直失败。为了保证UI在重连期间不卡死,重连逻辑放在独立线程里跑,弹窗提示放主界面。这套机制上线之后,现场报障率明显下降。

5.2 误检统计与调参闭环:用数据代替感觉

我在做这套源码时,给程序内置了一个“离线回放模式”。现场采集的图像全部按日期、相机ID、产品型号分类保存,然后可以在调试界面上回放任意一批图,边调整算法参数边看效果。

这里面最核心的方法是“NG留样”。每张判NG的图强制保存原图、预处理结果图、缺陷分割图,这样积累一段时间后,可以回放分析有哪些NG是真正的缺陷,有哪些是误检。我经常对同事说:误检不是靠智商消灭的,是靠样本量消灭的。你手里的NG样本越多,参数就能调得越干净。

同时,OK品也要抽一部分保存,专门用来验证“调完参数后是否把正常件误杀了”。这两类样本要分开目录管理,每次算法改动都跑一遍全量回放,对比误检率和漏检率,全部通过才更新到产线版本。

5.3 性能分析:先量化,再谈优化

多相机系统经常出现“CPU占用高但不知道卡在哪”的情况。我习惯用QElapsedTimer给每一段处理计时:

  • 采集耗时(GrabImageAsync返回时间);
  • 算法耗时(从HImage传入到缺陷结果输出);
  • 结果存储耗时(写文件、写数据库);
  • UI刷新耗时。

把四个数据打到同一个日志文件里,一跑便知瓶颈在哪。

如果算法耗时长,优先看是否用多线程并行算子,Halcon里可以通过set_system("thread_num", n)开启部分算子的内置并行,也可以把多相机检测任务分到线程池,每个相机一个检测线程,充分利用多核。

我有个很深的体会:真正拖慢系统的往往是存储环节。如果每张图都同步写高清原图到机械硬盘,一个产品多几个面,整条线速度就被IO拖死了。正确做法是图像保存全部走异步队列,NG图必存、OK图抽存,检测线程绝不等磁盘写完成再继续。

6. 部署收尾:从开发机到工控机,打包清单与运行环境

6.1 Qt打包与Halcon运行时合并

到了交付阶段,最怕的就是开发机上能跑,拷到工控机上各种缺DLL。Qt这边有现成工具:

windeployqt --release --compiler-runtime app.exe

这条命令会把Qt5.9运行库、VS2015的C++运行库都收集到exe同目录。注意一定要加--compiler-runtime,不然目标机器上没装VS2015 Redistributable的话,照样报缺少VCRUNTIME140.dll。

Halcon这边则要手动拷贝runtime,最低限度包括:

  • halcon.dll
  • halconcpp.dll
  • hdevengine.dll
  • 对应的runtime license文件

如果还用了Halcon的扩展或第三方库,一并拷入。不要为了省事把整个Halcon安装目录复制过去,授权容易出问题,文件又多又杂。另外,license文件要放到Halcon的license目录下或者在代码里显式指定路径,这个不配好,工控机上打开相机就会报license错误。

之前有同事从WinForm项目转过来,问Qt怎么打包安装程序。其实思路是一样的,WinForm可以用安装项目或InstallShield,Qt照样可以用Inno Setup写脚本,把整个exe目录打成Setup包。差别只是Qt多了一步windeployqt,其他没有本质区别。

6.2 工控机运行环境标准化

产线工控机的环境一定要标准化,否则同一个exe在这个工厂能跑,到另一个工厂就瘫痪。我在交付时固定了一份运行环境清单:

  • 操作系统:Win10企业版LTSC(或者Win7 SP1,前提是相机SDK还支持);
  • 关闭Windows自动更新,防止半夜自动重启把产线搞停;
  • 电源设置为高性能模式,关闭屏幕睡眠和硬盘休眠;
  • 防火墙对相机网卡网段放行,或直接关闭公网连接;
  • 相机网卡禁用节能模式,这个我踩过,网卡一节能,GigE相机掉帧掉得怀疑人生。

这些步骤看着基础,但每一项都是在产线上真金白银换来的经验。机器视觉设备是7x24小时跑的,任何“意外”其实都是必然,提前在环境层面堵死比事后救火省心得多。

6.3 源码与参数管理的一点体会

最后说回标题里的“多个相机缺陷检测源码”本身。这套源码我维护了大半年,最大的感触是:多相机系统的代码本身不复杂,复杂的是参数、配置和依赖的版本管理。

我的做法是:

  • 所有相机参数、检测参数、产品型号配置全部放JSON独立文件,绝不混进二进制;
  • 每个版本的Halcon runtime、相机SDK、Qt库版本记录在README里,锁死依赖;
  • Git分支按产品型号区分,改参数直接改配置文件,不重新编译exe。

这套实践下来,现场调试的效率高了不少。很多时候客户反馈问题,远程要一份配置文件就能复现,不用反复传exe。

如果你也要在产线上搭一套多相机缺陷检测系统,我的建议是别纠结版本新旧,先把环境变量、相机释放顺序、打包依赖这三件事用纸列清楚,能少熬很多个夜。VS2015、Qt5.9和Halcon20这个组合,在把多个相机缺陷检测源码从开发机迁到工控机这件事上,确实是我试过的搭配里最省心的一版。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 10:58:35

基于PJ85718DM与STM32F401RB的远程温度监测方案设计

1. 项目缘起与整体设计思路温度监测这件事&#xff0c;听起来像是电子入门的第一课——读个传感器&#xff0c;串口打印一下&#xff0c;完事。但真正落到工业现场、HVAC&#xff08;供暖、通风与空调&#xff09;机组、嵌入式设备机柜里&#xff0c;事情就完全不是那么回事了。…

作者头像 李华
网站建设 2026/10/17 17:46:34

多租户自动化测试进阶:从租户池到并发隔离的工程实践

做过多租户系统自动化测试的朋友应该都有同感&#xff1a;单租户的用例写得再漂亮&#xff0c;一到多租户环境就容易翻车。接口单独调都是通的&#xff0c;全量回归一跑就出现各种诡异问题——A租户的数据出现在B租户的报表里&#xff0c;某个租户的配置被另一个租户的用例偷偷…

作者头像 李华