☰
WPF与Halcon实现二维码识别:从机器视觉到上位机开发全攻略
2026/10/12 13:49:39 网站建设 项目流程

简介:这是一份面向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缩放,或者裁剪过 ROI,width 会变化,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 改成动态 25~35 之间浮动后,识别率从 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 像素后如果还设 4,FindDataCode2d会认为图里没有符合尺寸的码。所以我的写法是把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 二维码识别的拆解,能让你少走几趟弯路,直接把精力花在读码率提升而不是环境配置上。

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

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

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

立即咨询