恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
WPF与Halcon实现二维码识别:从机器视觉到上位机开发全攻略
首页
资讯中心
/
WPF与Halcon实现二维码识别:从机器视觉到上位机开发全攻略
WPF与Halcon实现二维码识别:从机器视觉到上位机开发全攻略
发布时间:2026/10/12 13:49:45
简介这是一份面向WPF桌面应用开发者和机器视觉初学者的完整示例项目演示如何将Halcon视觉库集成到WPF界面中实现二维码图片上传、识别、结果展示及异常提示。包内包含可直接运行的exe、csproj工程文件、XAML界面与C#逻辑代码以及Halcon识别相关的DLL和HDevelop引用配置适合用来理解WPF与Halcon混合开发的整体流程。压缩包共233个文件主要由png图片、cs源码、seq图像序列、xaml界面定义、config配置及少量dll/exe组成整体约35.34MB文件结构清晰便于按功能模块查找与复用。已有161人学习下载。通过阅读工程源码与界面设计可以掌握二维码定位、图像预处理、解码调用和结果回显的关键写法也能借鉴其容错处理与批量识别思路为后续开发更完善的识别工具提供可直接参考的起点。1. 为什么用 WPF Halcon 而不是一把扫码枪如果你的产线还靠人手一把扫码枪去刷工件上的二维码那你大概率遇到过这几个场面读码员姿势稍微歪一点就滴滴报警、反光工件要来回找角度、数据进不了 MES 还得靠人工敲键盘。我手头这个“WPF Halcon 实现二维码识别”的资源就是专门把这类场景拍平用的——用相机抓图、用 Halcon 在图像里找码并解码、再用 WPF 把结果实时扔到上位机界面。它解决的不是“能不能读出来”的问题而是“能不能稳定、自动、批量地读出来”的问题。适合正在做上位机开发、机器视觉选型、或者被扫码枪和读码器价格劝退的工程师参考尤其是想在 Windows 桌面上自己搭一套视觉识别框架的那批人。2. 工程搭建把 Halcon 的 .NET 引用接进 WPF 项目2.1 引用配置与平台位数最常见的翻车点Halcon 和 WPF 本身都支持 32 位和 64 位但这俩凑到一起时平台位数不一致会直接让程序在运行时崩掉报错通常是“未能加载 DLL 或程序集”或者“入口点错误”。常见做法是先确认你装的 Halcon 运行时是 x64 还是 x86再去 Visual Studio 里把 WPF 项目的平台目标改成一致。我一般会把整个解决方案统一成 x64因为现在工业相机 SDK 和 Halcon 新版基本都是 x64 优先。具体操作分三步第一步在项目里添加对halcondotnet.dll的引用这个 DLL 在 Halcon 安装目录的bin/dotnet文件夹下第二步右键项目选“属性”把“平台目标”改成 x64别用“Any CPU”第三步把 Halcon 安装目录下的bin/x64-win64或者对应位数目录里的 DLL 全部复制到 WPF 项目的输出目录或者直接把该目录加到系统 PATH。如果不做第三步程序在别人电脑上部署时会报找不到运行时组件别问我怎么知道的。# 将 Halcon 运行时 DLL 复制到项目输出目录示例路径 copy C:\Program Files\HALCON-XX\bin\x64-win64\*.dll D:\WpfQrProject\bin\x64\Debug\提示Halcon 的授权文件license.dat也要一起带上部署到没有安装 Halcon 的机器时需要设置环境变量HALCONROOT指向运行时所在目录否则 License 校验会失败。2.2 相机采集接入用 Halcon 自带的图像采集接口Halcon 最大的优势之一是它对市面主流工业相机、GigE Vision、USB3 Vision 协议做了统一封装你不需要为每家的 SDK 单独写对接代码。通过OpenFramegrabber算子打开设备GrabImage抓图整个流程不依赖相机厂家的 DLL。这里我用的是 GigE Vision 接口一个很典型的打开设备的代码先贴在下面。using HalconDotNet; public class CameraService { private HTuple _acqHandle; public void OpenCamera(string cameraName) { // 打开 GigE Vision 相机注意参数顺序固定 HOperatorSet.OpenFramegrabber( GigEVision, 0, 0, 0, 0, 0, 0, default, -1, default, -1, false, default, cameraName, 0, -1, out _acqHandle); } public HObject GrabSingle() { HObject image; HOperatorSet.GrabImage(out image, _acqHandle); return image; } public void Close() { if (_acqHandle ! null) { HOperatorSet.CloseFramegrabber(_acqHandle); } } }这段代码里OpenFramegrabber有 16 个参数最容易写错的是第 12 个参数false它表示设备是否自动同步单相机一般配 false第 13 个参数default是相机参数设置第 14 个参数cameraName是设备的唯一标识名可以通过InformationFramegrabber枚举出来。如果你的相机是 USB3 Vision 接口把第一个参数改成USB3Vision即可后面参数基本通用。2.3 实时取流先起线程再谈界面WPF 界面是 UI 线程采集图像不能直接跑在 UI 线程里——否则相机一卡界面直接“白屏未响应”。我通常用BackgroundWorker或者Task.Run包一个循环把抓到的图像交给识别逻辑识别完再通过事件回调回 UI 线程。注意HObject是继承自IDisposable的资源对象每帧用完要释放否则内存涨得比股票还快。private void StartGrabbing() { _cancellationToken new CancellationTokenSource(); Task.Run(() { while (!_cancellationToken.IsCancellationRequested) { HObject frame _cameraService.GrabSingle(); // 丢给识别处理这里只触发事件 OnFrameCaptured?.Invoke(frame); // 必须释放HObject 在底层有非托管内存 frame.Dispose(); } }, _cancellationToken.Token); }这段代码的要点在于frame.Dispose()的位置——必须放在事件触发之后。如果你把识别和释放放在同一帧里而识别是耗时操作就会造成下一帧抓图被阻塞实际采集帧率会下降得很厉害。我的习惯是抓图和识别分离抓图线程只负责采集与暴露事件识别线程在事件里另行排队处理。3. 核心识别流程从采集图像到拿到解码字符串3.1 选对模型QRCode 还是 DataMatrix二维码识别在 Halcon 里的核心是一系列DataCode2d算子。创建模型先用CreateDataCode2dModel第一个参数要传入码制类型常见的包括QRCode、DataMatrix、PDF417等。很多人以为“二维码就等于 QRCode”其实工业级工件上大量用的是 DataMatrix 码因为它的容错率更高且占用面积小。我做这个项目前有个认知误区默认创建了 QRCode 模型结果试产时样品上的 DM 码一个都识别不出来反直觉的地方就在这里先看清楚你现场用的到底是什么码制再去调识别参数。这部分资源里配套了一个模型选型说明但我建议自己的代码里写一个简单工厂避免复制粘贴太多重复参数public class QrModelFactory { public static HTuple CreateModel(string codeType) { HTuple modelHandle; HTuple emptyTuple new HTuple(); // 创建 DataCode2d 模型 HOperatorSet.CreateDataCode2dModel(codeType, emptyTuple, emptyTuple, out modelHandle); return modelHandle; } }emptyTuple的作用是把默认参数的选择权交给 Halcon如果你对特定场景有偏好可以用 key-value 数组直接传进去。比如在强反光表面我会传入contrast_min和contrast_max来限制对比度范围减少反光造成的高亮误判。3.2 识别参数哪些值是值得动手调的识别模型的参数不是越多越好实际只影响结果的主要是这几个contrast_min控制最小对比度值太低会把背景噪声当码module_width是码模块的像素宽度范围用来约束二维码在图像中的大小symbol_size限制二维码的尺寸范围polarity控制码的颜色极性默认是dark_on_light如果工件上印的是白码黑底要改成light_on_dark。我一般先跑一个默认参数看find_data_code_2d返回的status是什么——如果状态是no_code_found再逐项调整这些参数。public HTuple Recognize(HObject image, HTuple modelHandle) { HTuple symbolRegions; HTuple resultTexts; HTuple status; // 第一次识别 HOperatorSet.FindDataCode2d( image, modelHandle, new HTuple(), new HTuple(), out symbolRegions, out resultTexts); // 从结果中取状态 HOperatorSet.GetDataCode2dResults( modelHandle, all, status, out status); if (status.S no_code_found) { // 降低最小对比度重新识别 HOperatorSet.SetDataCode2dParam(modelHandle, contrast_min, 20); HOperatorSet.FindDataCode2d( image, modelHandle, new HTuple(), new HTuple(), out symbolRegions, out resultTexts); } return resultTexts; }这段逻辑用的是“二次尝试”策略。第一次用默认参数跑如果没找到码把contrast_min从默认值 30 降到 20 再跑一次。这个策略在光照不太稳定的产线里很管用但也别降太低降到 20 以下时误检率会明显增加——我在后面避坑章节里会详细讲这个现象。3.3 结果解析一串结果里可能不止一个码FindDataCode2d返回的resultTexts是一个 HTuple可能包含多个字符串因为一张图里可能同时出现多个二维码。用get_data_code_2d_results配合decoded_string才能拿到最终解码字符串。资源里还写了一个自定义类把“码位置”、“码内容”、“识别耗时”打包成一个结果对象方便 WPF 的 ListView 直接绑定。这个设计省了我很多事因为上线之后车间主任会反复要求“把每个工件的读码记录导出来”有结构化对象做导出会容易很多。public class QrResult { public string Code { get; set; } public double Row { get; set; } public double Column { get; set; } public double TimeMs { get; set; } }这里我把Row和Column定义为码中心点的图像坐标方便后面做基于位置的扫码逻辑。一个实用技巧是如果同一张图上同时有多个码你可以用symbolRegions的凸包中心点排序把码按从左到右、从上到下的顺序排列这样输出的字符串顺序和工件装配顺序一致下游系统不用再做矫正。4. WPF 界面的实时刷新跨线程与图像转换4.1 HObject 到 BitmapSource先转换再显示把 Halcon 的 HObject 显示到 WPF 的 Image 控件上不能直接赋值。HObject 是 Halcon 自己的图像对象WPF 能认的是BitmapSource比如BitmapImage。转换思路是先从 HObject 里拿到像素指针再封装成BitmapSource。GigE 相机出来的普遍是黑白图8 位灰度如果是彩色图要先把 Bayer 格式转换成 RGB 再给 WPF否则界面上的颜色会像调色盘被打翻。private BitmapSource HObjectToBitmapSource(HObject hObject) { HTuple hvPointer, hvType, hvWidth, hvHeight; HOperatorSet.GetImagePointer1(hObject, out hvPointer, out hvType, out hvWidth, out hvHeight); // 仅处理 8 位灰度图 if (hvType.S byte) { int width hvWidth.I; int height hvHeight.I; // 从非托管指针拷贝到托管字节数组 byte[] buffer new byte[width * height]; System.Runtime.InteropServices.Marshal.Copy(hvPointer, buffer, 0, buffer.Length); var bitmap System.Windows.Media.Imaging.BitmapSource.Create( width, height, 96, 96, System.Windows.Media.PixelFormats.Gray8, null, buffer, width); return bitmap; } return null; }这里有个小陷阱Marshal.Copy复制的是原始内存但BitmapSource.Create的stride参数一定要等于width因为 8 位灰度每个像素占一个字节。如果你的图像来自 HALCON 的zoom_image缩放或者裁剪过 ROIwidth 会变化stride 也要跟着变。很多人在显示时出现“图像斜了”或者“颜色花掉”八成就是 stride 写错了。4.2 用 Dispatcher 把识别结果扔回 UI 线程WPF 的控件只能在 UI 线程更新。识别线程跑完不能直接写TextBlock.Text否则会抛InvalidOperationException异常——这也是新手最容易踩的坑。我通常用Dispatcher.BeginInvoke把结果封装成委托丢给 UI 线程执行这样界面刷新不会阻塞识别循环。private void OnRecognized(QrResult result) { // 从后台线程回到 UI 线程 Application.Current.Dispatcher.BeginInvoke(new Action(() { TxtCode.Text result.Code; TxtTime.Text result.TimeMs.ToString(F1) ms; })); }BeginInvoke是异步的识别线程不会等 UI 刷完才去抓下一帧Invoke则是同步的会卡住后台线程。识别频率要求高的场景下尽量用BeginInvoke。不过要控制 UI 更新的频率如果每秒钟抓 30 帧每帧都去刷新 TextBox界面反而会闪烁甚至丢帧我习惯加一个节流开关只把每秒最后一帧的结果刷新到界面。4.3 识别结果展示和状态码加一个“失败原因”面板真正的好界面不是只显示“读成功/读失败”而是显示失败原因。Halcon 的get_data_code_2d_results除了status还能拿到num_found找到的码数量以及每个候选码的quality分数。我把质量分绑定成一个进度条方便车间看是“完全没读到”还是“读到了但质量差”。这个设计在调参时特别有用比闷头调contrast_min直观得多。// 获取质量分示例 HTuple quality; HOperatorSet.GetDataCode2dResults(modelHandle, all, quality, out quality); foreach (var q in quality) { // 0~1 之间的浮点数小于 0.4 的可以当成低质量码 }我在这个资源包里看到过类似的封装把状态码、质量分、解码字符串打包成QrResultDetail类配了 WPF 的模板直接绑定就能用。省了很多人第一次接触 Halcon 时摸不清结果对象里到底有什么字段的功夫。5. 避坑实录识别率低、崩线程、License 报错的五个案例5.1 现象打开相机就报“#8212: No frame grabber available”原因Halcon 的采集接口不认这个相机通常是 GigE Vision 驱动没装或者 Halcon 版本比相机协议新不兼容旧设备的 GenICam 配置文件。解决设备管理器里看相机是否有感叹号用 Halcon 自带的hdevelop打开图像采集助手测试一下能不能抓到图。我这里当时就是没有安装相机 SDK 自带的 GigE Vision Filter Driver装了之后立即可用。5.2 现象识别率忽高忽低同一个二维码一会读出一会读不出原因光照不稳定Halcon 的contrast_min设成固定值光照强时码和背景对比度高弱时对比度不足被忽略。解决不要只用固定参数识别增加一个“动态阈值”策略每帧先统计图像的灰度直方图用min_max_gray算出目标区域的最大最小灰度差再把这个差值折算进contrast_min。我试过用 50 帧图像做统计把contrast_min从固定 30 改成动态 2535 之间浮动后识别率从 91% 提到了 99%。// 动态调整对比度的思路 HOperatorSet.MinMaxGray(image, modelHandle, absolute, 0, out minGray, out maxGray, out range); double contrastMin Math.Max(20, range * 0.3); HOperatorSet.SetDataCode2dParam(modelHandle, contrast_min, contrastMin);5.3 现象识别线程跑一段时间后内存一直涨最后程序假死原因HObject 没有释放。Halcon 的 HObject 底层是非托管内存GC 不主动回收它每帧都 new 一个 HObject 而不 dispose内存必然爆。解决所有GrabImage出来的 HObject 只要用完立刻Dispose()另外用HOpeartorSet的临时对象比如symbolRegions、resultTexts也要释放。我自己的规矩是谁创建、谁释放识别方法内部创建的所有 HTuple 和 HObject 在方法出口统一清理。public QrResult RecognizeAndRelease(HObject image, HTuple modelHandle) { HTuple symbolRegions; HTuple resultTexts; QrResult result null; try { HOperatorSet.FindDataCode2d( image, modelHandle, new HTuple(), new HTuple(), out symbolRegions, out resultTexts); result new QrResult { Code resultTexts.S }; } finally { symbolRegions.Dispose(); resultTexts.Dispose(); } return result; }5.4 现象换了一台工控机程序启动时 License 报错但开发机好好的原因Halcon 的授权文件是按机器绑定或按 dongle 绑定的直接拷贝license.dat到新机器经常会失效。解决先确认新机器的时间是否准确——Halcon 授权有时间校验系统时间差了几天都会报错然后确认环境变量HALCONROOT和HALCONARCH是否指向正确目录特别是HALCONARCH如果默认成x86-sse2-win32而程序是 x64 的License 也会校验失败。提示运行时 DLL 和环境变量都要一起检查Halcon 的报错提示有时候很模糊先查%HALCONROOT%存不存在再查架构变量。5.5 现象WPF 界面有时候突然卡住鼠标转圈关闭相机再打开又好了原因在 UI 线程里调用了GrabImage或者FindDataCode2d。这类算子内部有阻塞等待一旦相机出图不稳定UI 线程就会被堵死。解决所有 Halcon 算子一律放后台线程WPF 只通过Dispatcher接收结果。给相机抓图加超时重试逻辑连续 5 次抓不到图就自动断开重连别让程序傻等。6. 进阶批量多码识别与性能踩点6.1 单图多码把“一次识别”拆成“一次检测多次读取”在生产场景里同一张图上往往有多个二维码——比如一版药板上有 10 个码或者一个托盘上有多个整箱码。Halcon 的FindDataCode2d默认会找出图像里所有符合模型参数的码每个码都输出一个symbolRegions但解码字符串的每个元素对应顺序不一定符合实际阅读顺序。我一般先用sort_region按码区域的中心点排序再做遍历解码保证输出的字符串顺序是可控的。// 多码识别按中心行坐标排序 HTuple area, row, col; HOperatorSet.AreaCenter(symbols, out area, out row, out col); HOperatorSet.SortRegion(symbols, sortedSymbols, first_point, row, true);这段代码里SortRegion按“first_point”先取每个码区域的锚点按行坐标从上到下排。对于流水线固定方向走的情况我一般按 row 排序如果是称重台上随便摆放就要按 col 和 row 的组合排序否则读出的码顺序在相邻两板之间会乱。我的经验是排序逻辑下游一般都要不如把排序结果直接揉进QrResult对象里作为Index字段。6.2 性能优化先降分辨率再找码二维码识别的耗时跟图像分辨率成正比但产线上其实不需要用 500 万像素全图去找码。我一般把图像先降采样到原来的 1/2 或者 1/4如果码尺寸像素足够识别速度能提升 3 到 5 倍。这个方法在这个资源包里也写了做法是先用zoom_image_factor把图缩小一半跑FindDataCode2d定位码区域再用原图在码区域附近做一次精读。这种做法要注意的是module_width参数要跟着缩放比例一起调。原图 4 像素一个模块缩小到 2 像素后如果还设 4FindDataCode2d会认为图里没有符合尺寸的码。所以我的写法是把module_width也除以缩放系数。6.3 性能踩点每帧识别耗时怎么看不要在界面上拿秒表掐时间。Halcon 自带get_timing算子可以在FindDataCode2d前后分别取系统时间戳做差分。我在资源里看到它用了count_seconds比Stopwatch更贴底层。不管哪种关键是分开统计“采集耗时”和“识别耗时”因为两者瓶颈不同采集耗时高说明相机参数或传输链路有问题识别耗时高说明图像预处理代码写得重了。double t1, t2; HOperatorSet.CountSeconds(out t1); HOperatorSet.FindDataCode2d(image, modelHandle, new HTuple(), new HTuple(), out symbols, out resultTexts); HOperatorSet.CountSeconds(out t2); double findTimeMs (t2 - t1) * 1000.0;识别耗时超过 100ms 时不要急着换 CPU先检查图像是不是太大、有没有做过不必要的median_image或binocular处理。我在另一个项目里见过有人为了“清晰”在识别前加了个 5x5 的中值滤波结果每帧识别从 40ms 涨到 120ms——中值滤波是排序算法非常吃资源而二维码识别本身对噪点并不敏感。从那以后我每次调识别前都强制走一遍“先用原图跑再逐项加预处理”的流程加一项就测一次耗时绝不带着一堆无用算子堆上去。这个习惯帮我挡掉了很多不必要的系统瓶颈每次改完参数就会跑一遍固定测试图集把识别率和耗时记下来对比。希望这份 WPF Halcon 二维码识别的拆解能让你少走几趟弯路直接把精力花在读码率提升而不是环境配置上。本文还有配套的精品资源点击获取