拓冰建站拓冰建站
首页 / 资讯中心 / 正文

C#+Halcon工业视觉检测上位机开发实战与避坑指南

做工业视觉检测这些年C# 配上 Halcon 这套组合我用了很久从最初在 Halcon 里调算子、截图看效果到后来把整套检测逻辑封装成独立模块嵌进上位机踩过的坑确实不少。这篇东西不打算讲教科书式的入门而是直接把我平时搭视觉检测程序的完整套路、关键代码和容易翻车的地方都摆出来给正在用 C# 搞 Halcon 的朋友做个参考。先说清楚这套东西能解决什么问题产线上的工件定位、尺寸测量、外观缺陷判断、条码识别基本都离不开视觉检测。Halcon 负责“看”和“算”C# 负责“管”和“用”——管理相机采集、调度检测流程、把结果展示到界面上、跟 PLC 或 MES 通信。适合谁看刚接手视觉项目的上位机工程师或者在实验室里想把 Halcon 算法落成 Windows 软件的人。下面我按实际项目推进的顺序写。1. 方案选型与整体设计思路1.1 为什么选 C# 搭配 Halcon而不是其他组合很多新手会纠结一个问题Halcon 自己有 HDevelop 开发环境也能直接做界面原型为什么还要绕一圈用 C# 来调用我的理解是分工不同HDevelop 适合快速验证算法但它做不了像样的上位机软件没法方便地对接数据库、OPC UA、串口、Socket也做不出现代感的界面。C# 的生态太成熟了WinForms、WPF 随便选第三方库一堆做工业软件的信息化管理很顺手。至于 OpenCV也不是不能做视觉但工业检测讲究“开发快、稳定性高、算子专业”。Halcon 的模板匹配、测量、Blob 分析这些算子都是工业场景打磨过的鲁棒性很强。比如表面划痕检测Halcon 的频域滤波和形态学组合用起来非常顺手OpenCV 你得写一大堆代码还要调参。而 Halcon 直接给你封好了配合 HDevelop 里可视化调试开发效率高一个量级。还有一个现实因素很多非标设备厂的项目周期就那么几周现场调试时间紧张。用 Halcon C#Halcon 算法的稳定性帮你兜底C# 帮你快速堆界面和通信一两天就能把整体流程跑起来。我个人的选型经验是算法复杂度高、检测项多、交付周期紧的项目优先 C# Halcon如果只是做原型验证或者纯算法测试HDevelop 本身就够了。1.2 上位机软件的模块化架构设计拿到一个视觉检测需求别急着写代码。先拆模块我一般是这么分的相机模块负责连接相机、参数设置曝光、增益、触发模式、采集图像。对外只暴露一个采集接口返回 Halcon 的 HObject 图像对象。算法模块封装所有 Halcon 算子逻辑。输入是 HObject 参数输出是检测结果类比如位置坐标、角度、尺寸值、OK/NG 标志。这一层不要让界面代码掺和进来。通信模块走 TCP/IP、Modbus、OPC UA 都行负责跟 PLC、机器人或者 MES 交互。把检测结果打包成协议规定的格式发出去。界面模块负责实时显示图像、叠加检测结果、显示日志、参数配置。界面线程只管刷新不跑算法。数据管理模块保存检测记录、统计良率、导出报表。可以接 SQLite、MySQL 或者本地 CSV。模块化最大的好处一是调试方便相机、算法、通信可以单独测二是换硬件容易比如换相机品牌只需要替换相机模块里的驱动代码算法层根本不动。有人会问是不是每个项目都得这么重不一定。小项目可以适当合并但“算法和界面分离”这条原则我建议无论如何都要守住。原因很简单算法和界面耦合在一起一旦图像处理耗时界面就卡死界面上的参数改动不小心还会污染算法状态现场调参时非常痛苦。2. 开发环境搭建与 Halcon 的 C# 集成2.1 Halcon 安装与 License 配置的几点提醒Halcon 的安装包分 Windows 和 Linux 版本Windows 下直接双击 exe 装就行。需要注意几点版本选择目前市面上常见的有 Halcon 13、17、18、20、21、22、23不同版本对应的 HalconDotNet 程序集版本不一样。你装了 Halcon 23工程里引用的是 23 版的 DLL给别人部署时他也得装 23 的运行时或者你把对应 DLL 一起带上。License 有试用版和正式版加密狗。试用版能跑大部分算子但运行时间有限制而且保存的图像会带水印。正式版分 local license本机文件授权和 license server通过网络授权。我遇到过现场电脑没插加密狗导致程序启动报错的情况所以部署前一定要确认授权方式。提示HALCON 安装路径里bin\dotnet35和bin\dotnet45分别对应不同 .NET Framework 版本的 DLL。用哪个版本取决于你工程的 Target Framework。现在多数上位机用 .NET Framework 4.6.1 或 4.7.2那就用dotnet45目录下的halcondotnet.dll和hdevenginedotnet.dll。2.2 在 Visual Studio 中引用 HalconDotNet 的两种方式第一种直接添加 DLL 引用。在 VS 里右键“引用”→“添加引用”→“浏览”找到 Halcon 安装目录下bin\dotnet45\halcondotnet.dll和hdevenginedotnet.dll添加进来。然后在代码顶部using HalconDotNet;就能用了。第二种如果你不想每台电脑都手动找 DLL可以用 NuGet 包。有些第三方把 HalconDotNet 打包上传了直接搜“HalconDotNet”也能找到。但要注意版本是否匹配我曾经碰到过用 NuGet 包版本和安装的 Halcon 版本不一致某些算子报HALCON error #22011后来还是老老实实改成手动引用本地 DLL。引用之后你还需要把相关 DLL 复制到项目的输出目录。最简单的方法是在工程的生成事件里加一行copy /Y $(HALCONROOT)\bin\dotnet45\*.dll $(TargetDir)或者直接右键引用 DLL把“复制本地”属性设为 True。要不然发布程序后到没装 Halcon 的电脑上跑会报找不到 halcondotnet.dll 或者加载失败。还有一个细节建议在工程里建一个HalconLib文件夹把用到的 Halcon 原生 DLL比如halcon.dll、halconcpp.dll和 .NET DLL 统一放进去引用时指向这个相对路径。这样项目迁移、打包都比较干净不会出现“我这能跑你那不能跑”的灵异问题。2.3 从 HDevelop 脚本到 C# 代码的转换流程HDevelop 写好并调试通过的脚本有两种方式带入 C#第一种是“导出代码”方式。在 HDevelop 的菜单里“文件”→“导出”选择语言C#导出后得到一个.cs文件里面是把 HDevelop 的算子逐行翻译过来的代码。你可以直接把这个 cs 文件加到工程里调用也可以把核心逻辑复制进你自己的方法。第二种是“外部程序调用”方式用HDevEngine在 C# 里加载.hdvp工程文件或者.hdev脚本然后传入参数、执行脚本、读取输出变量。这种方式的优点是不用重新编译 C# 代码就能调整算法特别适合算法还在频繁迭代的阶段。缺点是需要打包 HDevEngine 相关的 DLL 和脚本文件而且性能比直接调用算子略低一点。我实际项目里更常用“导出代码 手工重构”先用 HDevelop 调试算法导出 C# 代码作为参考然后把核心算子重构到 C# 的视觉检测类里参数通过界面配置传入。这样程序运行效率高也方便做单元测试和功能扩展。3. 核心功能实现与关键代码解读3.1 相机图像采集连接相机与实时抓图工业相机的接入方式五花八门GigE Vision 网口相机、USB3.0 相机、Camera Link 相机还有各种厂家的私有 SDK。Halcon 的好处是封装了统一的采集接口只要相机支持 GigE Vision 或 USB3 Vision 协议用 Halcon 的HFramegrabber类就能直接连不需要厂商 SDK。代码很简单HFramegrabber grabber new HFramegrabber(); grabber.OpenFramegrabber( GigEVision2, 0, 0, 0, 0, 0, 0, default, -1, default, -1, false, default, camera_ip, 0, -1); HObject image; grabber.GrabImageAsync(out image, -1);但有些相机比如一些国产 USB 相机协议不规范Halcon 的通用接口连不上这时就得用厂商 SDK 拿到图像数据再转成 Halcon 的HObject。转换方法是这样先用厂商 SDK 拿到 Bitmap 或者像素字节数组然后HOperatorSet.GenImage1生成单通道图像或者用GenImageInterleaved生成彩色图像。// 假设从相机 SDK 拿到 byte[] data, 宽 width, 高 height HObject hoImage; HOperatorSet.GenImage1(out hoImage, byte, width, height, data);这里有个很重要的点Halcon 的 GenImage1 生成的图像是引用外部内存的也就是说它不会拷贝一份数据而是直接用你传进去的指针或数组。因此只要图像对象还在用那份内存就不能被释放。我用过一些相机 SDK它们的回调里会复用缓冲区如果你直接把缓冲区传给 Halcon下一帧回调把缓冲区覆盖了图像就花了。稳妥做法是hoImage hoImage.Clone()强制 Halcon 做一次数据拷贝。3.2 显示控件HWindow 的集成与图像缩放C# 界面要实时显示图像是用 Halcon 自带的HWindowControl控件。它是 WinForms 控件直接拖到窗体上就行。但用途中有个坑HWindowControl 内部管理了一个独立的窗口线程你不能在其他线程里直接调用它的HOperatorSet.DispObj否则会闪退或者界面卡死。我一般的做法是在窗体上放一个 HWindowControl暴露一个SafeDispImage(HObject img)方法这个方法通过Control.BeginInvoke把显示操作调度到 UI 线程执行。算法线程算完结果后把 HObject 和检测结果传给界面让界面去刷新。public void SafeDispImage(HObject img) { if (hWindowControl1.InvokeRequired) { hWindowControl1.BeginInvoke(new ActionHObject(SafeDispImage), img); return; } hWindowControl1.HalconWindow.DispObj(img); }关于图像缩放Halcon 窗口默认是适应控件的但如果你设置了SetPart或者图像分辨率跟窗口大小不同它不会自动缩放只会显示左上角区域。用HOperatorSet.SetPart(hWindow, 0, 0, height - 1, width - 1)把图像完整映射到窗口上。但要注意SetPart 之后图像会被拉伸比例会失真如果你希望保持原始比例就要手动计算窗口里显示区域的位置和大小。另外一个常见需求是鼠标缩放和拖动。HWindowControl 本身没有内置放大镜功能需要自己响应鼠标事件。原理其实是改 SetPart 的窗口范围鼠标滚轮缩小范围就是放大图像然后用鼠标移动来平移。这里就不展开贴代码了思路放在这儿需要的话可以自己实现。3.3 模板匹配从创建模板到定位目标模板匹配是视觉检测里用得最多的功能之一。Halcon 的形状匹配能够抵抗光照变化、旋转和缩放非常稳。核心就是两步创建模板 在线查找。创建模板通常在“离线设置”阶段完成。抓一帧标准图像框选出目标区域比如一个工件的轮廓然后生成模板HObject modelImage, modelRegion, modelShapeModel; HTuple modelID; // 读取标准图像 HOperatorSet.ReadImage(out modelImage, template.png); // 框选区域这里假设已经从交互界面拿到了一个矩形区域 HOperatorSet.GenRectangle1(out modelRegion, row1, col1, row2, col2); // 提取轮廓并创建形状模板 HOperatorSet.ReduceDomain(modelImage, modelRegion, out modelImageReduced); HOperatorSet.CreateShapeModel(modelImageReduced, 6, -30, 60, 0.1, none, use_polarity, 10, 10, out modelID);几个关键参数说一下NumLevels是金字塔层数我常用 4~6AngleStart和AngleExtent是搜索角度范围-30° 到 30°MinContrast是轮廓对比度阈值Optimization可以设 auto。这些参数直接影响匹配速度和稳定性我一般先在 HDevelop 里调好再把参数放进 C# 配置文件中。在线查找时HTuple row, col, angle, score; HObject searchImage; HOperatorSet.ReadImage(out searchImage, search.png); HOperatorSet.FindShapeModel(searchImage, modelID, -30, 60, 0.5, 1, 0.5, least_squares, 0, 0.9, out row, out col, out angle, out score);MinScore是匹配分数阈值MaxOverlap控制多个匹配结果的最大重叠度。FindShapeModel 返回的可能有多组值row、col、angle、score 都是 HTuple 类型相当于一个数组。你要取第一个结果就是row[0].D。这里要特别注意一个容易搞混的点HTuple 可以用方括号下标访问但索引值返回的类型是HOperatorSet.TupleSelect或直接row[0]取出来是 HTuple要拿到 double 用.D拿到 int 用.I拿到 string 用.S。我见过同事直接把row[0]赋给 double 变量编译报错后又不知道怎么回事其实类型转换没做对。3.4 尺寸测量卡尺工具与边缘定位测量功能一般用 Halcon 的一维测量工具metrology它比单纯边缘提取更稳定。核心思路是在指定位置生成一个矩形测量区域然后在区域内沿着垂直方向找边缘点最后拟合出直线、圆或者距离。下面是一段测量两段边缘距离的示例HObject image; HTuple width, height; HOperatorSet.ReadImage(out image, part.png); HOperatorSet.GetImageSize(image, out width, out height); // 创建测量模型 HTuple metrologyHandle; HOperatorSet.CreateMetrologyModel(out metrologyHandle); HOperatorSet.AddMetrologyObjectGeneric(metrologyHandle, line, new HTuple(new double[] { 100, 200 }), new HTuple(new double[] { 500, 600 }), 20, 5, 1, 30, new HTuple(), new HTuple(), out HTuple index); // 设置测量参数 HOperatorSet.SetMetrologyModelParam(metrologyHandle, measure_transition, negative); HOperatorSet.SetMetrologyModelParam(metrologyHandle, num_measures, 20); // 应用测量 HOperatorSet.ApplyMetrologyModel(image, metrologyHandle); // 读取测量结果 HOperatorSet.GetMetrologyObjectResult(metrologyHandle, index, all, result_type, out HTuple result);测量这块常见的坑是“边缘毛刺”导致测量值跳动。Halcon 的卡尺本身可以做边缘筛选比如设置measure_select为 first 或 last或者限制边缘阈值。但更彻底的办法是预处理图像——用高斯滤波或者中值滤波把噪声压下去再测量。我经常用HOperatorSet.GaussFilter搭配HOperatorSet.MedianImage效果比只调卡尺参数好得多。测量结果除了数值还需要叠加到图像上直观显示。可以用HOperatorSet.DispLine、DispCircle这些绘图算子画在 HWindow 上注意画之前要先DispObj显示背景图像否则只在黑屏上画线。3.5 结果输出与界面刷新把检测数据反馈给用户检测完成后C# 这边要处理的事情有两件一是把结果传给界面上显示二是把结果发送给 PLC 或 MES。显示方面我习惯把检测结果封装成一个类public class InspectResult { public bool IsOK { get; set; } public double Score { get; set; } public double Row { get; set; } public double Col { get; set; } public double Angle { get; set; } public double MeasureValue { get; set; } public string Message { get; set; } public HObject ResultImage; // 注意HObject 需要释放 }为了让界面能显示检测框、十字线、测量点我一般会维护一个“叠加层”的 List把要画的图形坐标存起来然后在窗口的 Paint 事件里统一画。不建议在主流程里到处调DispRectangle1容易把代码搅乱。关于 HObject 作为类成员这个问题一定要小心。HObject 底层是非托管资源必须调用Dispose()释放。如果你把它放在类里用完之后没有释放内存会一直涨。正确的做法是使用using模式或者手动Dispose。我习惯是在写结果到界面前先把要显示的图像HOperatorSet.DumpWindowImage保存一份副本原来的图像立刻释放界面显示副本。4. 多线程调度与实时性优化4.1 检测线程与 UI 线程的关系工业视觉检测程序里最忌讳的就是在 UI 线程里跑算法因为相机采集一张图、预处理、模板匹配、测量一顿操作下来可能几百毫秒期间窗口无响应用户体验极差。我的标准做法是开一个独立的检测线程或者用BackgroundWorker。线程模型大致是这样采集线程负责从相机 Grab 图像有图了就丢给检测线程。检测线程做图像处理得出结果通过事件通知 UI 和通信模块。UI 线程只负责响应控件事件和刷新界面。通信线程负责跟 PLC 或 MES 交互防止网络阻塞拖累检测。线程之间用队列传递图像。最简单的方式用ConcurrentQueueHObject但 HObject 不是线程安全的引用类型放进队列前必须 CloneHObject queueImg img.Clone();原始图像马上释放。这样即使队列里等待处理的图像是独立引用也不会因为相机回调覆盖缓冲区而出问题。4.2 图像处理速度优化算子的合理选择Halcon 处理速度虽然快但用不好也能被拖垮。几个实用的提速技巧图像裁剪模板匹配前先用 ROI感兴趣区域裁剪只在小区域内找目标速度能提升好几倍。方法是用HOperatorSet.ChangeDomain配合形状实际处理前ReduceDomain只处理 ROI 里的内容。金字塔层数CreateShapeModel的 NumLevels 越大匹配搜索越快但太大会丢失细节导致找不到目标。一般 4~6 层是甜点区。并行处理Halcon 已经内部做了多线程优化但有些时候你处理多路相机时可以把几路检测分散到多个 Task。注意 Halcon 要求图像对象不能在多个线程同时处理同一个对象但不同图像对象可以并行。避免频繁 SetPart显示窗口时反复设置 SetPart 也会消耗时间尤其是高分辨率图像。如果只是显示不需要交互缩放用HOperatorSet.SetSystem(width, 800)之类的参数让 Halcon 自动缩放显示或者直接缩小图像后再显示。4.3 内存管理的“老大难”问题C# 的垃圾回收能管住托管内存但 Halcon 的 HObject 是非托管资源必须显式释放。很多人写的视觉程序跑一两个小时就卡顿、内存涨到几个 G基本都是 HObject 泄漏。我的铁律是任何一个 HObject 变量用完必须 Dispose或者用 using 包起来。但也要注意别释放太早比如把 HObject 传给了某个异步任务这里就要特别注意生命周期。Halcon 还提供了一个机制叫“图像池”可以避免重复分配大块内存。用HOperatorSet.SetSystem(mmu_enable, true)可以启用内存管理优化但一般默认就是开着的。真正要避免的是频繁创建大图像然后释放每次释放后内存可能不会归还给系统而是放回池里所以任务管理器的内存显示不一定准确这是正常的。5. 常见问题与排查技巧实录5.1 Halcon 版本与 .NET 框架不匹配导致加载失败症状编译通过运行时报BadImageFormatException或者直接提示找不到halcondotnet.dll。原因一般有两个一是引用的 DLL 位数不对。程序集目标平台是 x86但 Halcon 装的是 x64或者反过来。检查办法是看项目的“平台目标”在“项目属性”→“生成”→“平台目标”里改成 x64 或 x86跟 Halcon 安装版本一致。另一个原因是用了 .NET Core 或 .NET 5而 HalconDotNet 老版本是面向 .NET Framework 的混用就可能出问题。解决办法要么把工程改成 .NET Framework要么用支持 .NET Standard/.NET Core 的新版本 Halcon。5.2 连接相机报错 HALCON Error #5300 或 #5301这两个错误通常是设备访问失败。5300 是找不到设备5301 是设备被占用。排查思路先确认相机能枚举出来用 Halcon 自带的hdevelop里InfoFramegrabber看看设备型号和地址。GigE 相机要保证网卡 IP 和设备 IP 在同一网段而且防火墙要放行否则 Halcon 搜不到设备。有一个容易被忽视的点如果相机已经被其他软件打开或者你的程序上一次异常退出没释放HFramegrabber就会报设备被占用。这时候重启软件或者用grabber.Dispose()确保释放干净。5.3 模板匹配分数正常但结果位置不对这通常不是算子的锅而是图像坐标系没搞清楚。Halcon 里的 Row 是从上往下的 y 坐标Col 是从左往右的 x 坐标。HWindow 控件的坐标系也是这样的。但有些相机 SDK 返回的数据是 Bitmap 坐标Bitmap 的原点在左上角跟 Halcon 一样但有些 SDK 是独立坐标系转换处理就得注意。另外如果你在匹配前做了图像旋转或者镜像需要算子HOperatorSet.MirrorImage或RotateImage匹配结果是在变换后的图像坐标系里的。要在原图上叠加显示就得做逆变换。这个很容易忽略现场对不上位置时先查坐标系变换。5.4 程序运行时 Halcon 窗口闪退或者报线程错误原因大多是在非 UI 线程里调用了窗口相关操作比如hWindowControl.HalconWindow.DispObj。解决办法就是我前面说的用BeginInvoke调度到 UI 线程或者直接封装一个SafeDispImage方法。另外如果你的程序还用了多显示器或者缩放比例Halcon 窗口偶尔会显示异常把 HWindowControl 放进一个常规 Panel 里一般能规避。5.5 部署到客户电脑时报缺少 DLL 或者 License 错误这类问题在开发机上不出现一到现场就冒出。处理经验是项目输出目录下要包含 Halcon 的运行时 DLL例如halcon.dll、halconcpp.dll、hdevenginedotnet.dll、halcondotnet.dll还有可能用到hcanvas.dll等。如果用的是加密狗授权客户电脑要装好 Halcon Runtime 和相应驱动。如果是 local license要把 license 文件放到安装目录下或者设置环境变量HALCONLICENSE指向 license 文件位置。记得把bin\x64-win64下的所有 DLL 一起拷贝我见过只拷了 halcondotnet.dll 忘记拷 halcon.dll 的启动时直接崩。6. 现场调试心得与建议再分享一个实用的调试技巧在 HDevelop 里调好算法导出到 C# 后如果现场发现某个工件检测失败不要只让现场工程师截图最好是让程序自动保存检测失败的那一帧原始图像保存为 bmp 或 png。回到电脑上可以直接在 HDevelop 里加载这张图把所有算子和参数回放一遍定位是参数不对还是图像本身质量不好。这比对着黑漆漆的现场屏幕反复试要高效太多了。还有一个建议所有检测参数曝光、模板匹配 MinScore、测量阈值、图像预处理参数都放到配置文件里不要硬编码。现场调参最常做的事情就是改一个分数阈值然后看效果如果能做到界面上直接改、保存、立即生效会省下大量沟通成本。我通常把参数序列化成 JSON 存到本地界面做一个“参数配置”页点保存后算法模块重新加载参数。如果你现在才刚刚起步我的建议是先别追求复杂的架构把 HDevelop 调通作为第一优先级然后用最简单的方式把相机图像显示在 C# 窗口里再跑通一次模板匹配这就已经完成了一个最小可行的视觉检测原型。后面架构可以慢慢演进但那个“一眼看到检测结果”的正反馈非常关键会让你更有动力继续往下做。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门