简介:这份基于Halcon与C#开发的视觉检测平台源码,面向工业视觉检测开发者,封装了图像获取、处理、识别、结果可视化等基础流程,适合想要借助C# WinForm落地视觉项目的初中级工程师。压缩包共两千个文件,约200.64MB;其中534个cs主程序文件构成核心逻辑,441个txt可用于说明与数据参考,284个png、68个bmp集中存放界面图样与视觉模板,146个resx承载多语言/控件资源;dll、pdb、config等则清楚标出依赖、调试与运行配置,整体目录结构贴近Visual Studio标准组织。目前已有三千九百四十二人学习下载。平台v0.0.1.2虽属初期版本,但工程可编译通过,内部已包含主程序、图像显示窗口、数据图表、代码编辑控件、自定义按钮及停靠窗体等模块;结合源码可重点理解Halcon形状匹配、OCR识别与C#界面联动的方法,也能在此基础上扩展定位测量、读码等具体检测功能,是一份合适的工程化入门与二次开发参考。
1. 从一次产线停线说起:VisionAndMotionPro 在解决什么问题
年初帮一家汽配件厂调试视觉定位工位,现场反馈“扫码枪偶尔读不到批次号,一停就是半小时”。排查到最后,问题不在扫码枪,而是前面工位的OCR识别结果没有和运动控制联动——图像拿来看了,但判断结果没有真正决定“放行还是拦截”。这不是设备不行,是视觉检测平台没有把图像处理和业务逻辑串成闭环。VisionAndMotionPro 这套基于 Halcon 与 C# 的视觉检测平台源码,版本 v0.0.1.2,正是解决这类问题的典型骨架:Halcon 负责图像算法,C# 负责界面、流程和运动控制接口,源码可编译,模块划分清楚,适合二开也适合当教学案例。它适合三类人:刚接触机器视觉、想在 Windows 上快速搭一套检测界面的上位机工程师;需要把 Halcon 算法集成进 .NET 项目的工控开发者;以及想拆解“图像采集—模板匹配—结果输出”全链路的学生。本文会按工程结构、显示封装、模板匹配、部署排错这条线,把这套源码能复用的部分逐层拆开。
2. 工程骨架与 Halcon/C# 的互操作方式
2.1 源码包的目录结构说明了什么
拿到压缩包先别急着双击 .sln。这套 v0.0.1.2 的源码,第一眼看上去文件很多,但分类有规律。主工程VisionAndMotion是核心,里面按功能拆成了多个 Form 或 UserControl 文件:ImageWindow管图像显示,MyChart管结果曲线,CodeEdit是内嵌代码编辑器,NewButton、OPTLight、DockForm则是控件和布局辅助。.vs文件夹是 Visual Studio 的本地配置缓存,里面藏着.suo文件,记录了最近打开的窗口位置和断点设置,如果开发机和打包机 Visual Studio 版本一致,保留它省不少事;但如果你用的是 VS2022,打开这个 2015/2017 年代建的工程,建议直接删掉.vs和*.suo,让它重新生成,否则容易出现加载缓存冲突。
下表是这套源码里你后续二开最常改的几个文件,建议先在心里建一张地图。
| 文件/模块名 | 职责 | 二开时改哪里 |
|---|---|---|
VisionAndMotion主工程 | 主流程、界面容器、线程调度 | 流程状态机的切换条件 |
ImageWindow | Halcon 图像显示与 ROI 交互 | 显示倍率、ROI 回调 |
MyChart | 检测结果趋势曲线/数据可视化 | 数据源类型、刷新频率 |
CodeEdit | 自定义代码编辑/脚本执行 | 语法高亮、脚本接口 |
OPTLight | 光源控制模拟组件 | 串口或 TCP 指令映射 |
2.2 Halcon 集成进 C# 的两种路线,v0.0.1.2 用的是哪种
Halcon 和 C# 集成有两条主流路线。第一种是 HDevelop 里把算子导出成 C# 代码,然后复制进你的 WinForm 或 WPF 工程,调HOperatorSet的静态方法。这种方式最直观,新手也最容易上手,但每次改算法都要回到 HDevelop 改、再导出、再编译,响应慢,而且算子导出的代码大量使用HTuple,可读性一般。第二种是运行时通过HDevEngine加载.hdev脚本文件,C# 工程只负责传参和取结果,算法修改不用重新编译 C# 工程。VisionAndMotionPro 这版源码的主流写法是第一种,也就是把 Halcon 算子用 C# 直接调用的方式固化在工程里,好处是单 exe 部署相对容易,坏处是算法和界面耦合度偏高,这也是二开时最值得重构的点。
不管选哪条路,第一步都是正确引用 Halcon 的 .NET 接口。安装 Halcon 后,在 Visual Studio 里添加引用,路径一般在C:\Program Files\MVTec\HALCON-xx.x\bin\dotnet35或dotnet4下。以 .NET Framework 4.x 为例,需要添加的依赖核心是halcondotnet.dll。如果你用第二种方式跑脚本,代码骨架长这样:
using HalconDotNet; // 初始化 HDevEngine,加载 hdev 脚本 HDevEngine engine = new HDevEngine(); engine.SetProcedurePath(@"D:\VisionAndMotion\halcon\procedures"); // 加载并执行主脚本 HDevProcedure procedure = new HDevProcedure("find_screw_hole"); HDevProcedureCall call = procedure.CreateCall(); // 传输入参数:图像路径与匹配分数阈值 call.SetInputCTuple("ImagePath", @"D:\images\5.bmp"); call.SetInputCTuple("MinScore", 0.6); // 执行算法 call.Execute(); // 取出结果:抓到的行、列、角度 HTuple row, col, angle; call.GetOutputCTuple("Row", out row); call.GetOutputCTuple("Col", out col); call.GetOutputCTuple("Angle", out angle); Console.WriteLine($"找到 {row.Length} 个目标,首个位置:({row[0].D}, {col[0].D})");这段代码里,HDevEngine的好处是让算法文件独立于主程序,生产现场调参时,你只需要替换脚本目录里的.hdev文件,不用重新编译整个上位机。注意SetInputCTuple里传字符串路径时,Halcon 内部会把字符串当成图像文件名去读取,所以路径里不要带中文,否则在部分语言环境下会吃到编码坑。GetOutputCTuple取出的HTuple如果拿不到值,程序不会立即崩,但后续.D转 double 时会抛HALCONEXCEPTION,调试时看到这个异常,先回去检查脚本里的set_origin或测量结果是否真的写进了输出变量。
2.3 为什么源码里大量使用 HTuple 而不是原生类型
C# 里你习惯用double、int、string,但在 Halcon 的 .NET 接口里,几乎每个算子都绕不开HTuple。它的本质是一个动态数组,可以同时容纳不同类型的数据。在 VisionAndMotionPro 的检测流程里,你经常能看到一个HTuple同时装了十几个点的 XY 坐标,这在 Halcon 里叫“元组”。实操里我用HTuple的习惯是:所有从 Halcon 算子返回的值一律先用HTuple接住,等确定要落库或显示时,再通过.D、.I、.S转成 C# 原生类型。这样虽然多一步转换,但能极大减少类型不匹配导致的偶发异常,尤其是当 Halcon 算子返回空元组时,直接转 double 会崩,而拿到HTuple后可以先判Length == 0。
3. 图像采集线程与 HWindowControl 显示:把检测结果画到界面上
3.1 从文件名解析到图像输入的完整路径
源码包根目录里躺着一批 BMP 测试图:StandardImage1.bmp、StandardImage2.bmp、Pic_2018_07_20_145230_blockId#11411.bmp、Pic_2018_07_20_145658_blockId#15805.bmp。从这些命名能看出这是一套带批次追溯的视觉工位测试数据,blockId后面的数字是批次号,Pic_2018_07_20_145230是日期加毫秒级时间戳。实际调试视觉项目时,这种图片命名习惯非常值得保留,因为现场出了问题你能反查是哪一秒拍的哪一批料。在这些测试图上,你能验证模板匹配在不同光照、不同旋转角度下的响应。
图像输入的常见做法是由采集线程监听相机回调,拿到HObject后交给检测线程,同时用Control.BeginInvoke更新界面显示。VisionAndMotionPro 里显示控件用的是HWindowControl,这是 Halcon 官方提供的 Windows 控件,内部封装了窗口句柄。显示的代码一般长这样:
// 从文件或相机读取图像到 HObject HOperatorSet.ReadImage(out HObject image, @"D:\images\5.bmp"); // 在 HWindowControl 上显示 hWindowControl.HalconWindow.DispObj(image); // 在图像上画十字线标记中心点 HTuple centerRow = 256, centerCol = 256; HOperatorSet.DispCross(hWindowControl.HalconWindow, centerRow, centerCol, 60, 0);DispObj是直接把 HObject 刷到窗口,DispCross是在已有显示层上叠加一个 60 像素长的十字线,0代表十字线的角度方向。这里有个容易踩的坑:HWindowControl.HalconWindow在窗体还没完全显示出来时调用DispObj会抛异常,所以要在Form_Shown事件里做首次显示,而不是Form_Load。
3.2 UI 刷新卡顿的解法:别在采集循环里直接 Invoke
很多 C# 上位机工程师都遇到过这个经典问题:“循环数据采集时 UI 刷新卡顿”。在 VisionAndMotionPro 这类视觉平台里,采集帧率假设是 30fps,如果你每一帧都直接BeginInvoke刷图像,UI 线程会被塞爆,界面拖拽都会掉帧。我的处理方式是加“显示节流器”,只在上一帧显示完成后才请求下一帧,或者干脆定频刷新,比如每秒只刷新 15 帧图像,但检测结果全量记录。
// 节流式刷新:避免 UI 线程堆积 private bool _isDisplayBusy = false; private void OnFrameCaptured(HObject frame) { if (_isDisplayBusy) return; // 上一帧还没显示完,丢弃当前帧 _isDisplayBusy = true; hWindowControl.BeginInvoke(new Action(() => { try { hWindowControl.HalconWindow.DispObj(frame); } finally { _isDisplayBusy = false; } })); }这段代码的关键逻辑是:采集回调进来时先判断_isDisplayBusy,如果 UI 线程还没来得及处理上一帧,这一帧的直接丢弃。视觉检测场景下,算法处理速度决定了节拍,显示慢一拍无伤大雅,但 UI 线程卡死会导致整个上位机假死。BeginInvoke前记得检查hWindowControl.IsHandleCreated,否则窗口销毁后回调线程再往里丢委托会抛ObjectDisposedException。
3.3 Halcon 图像类型与 C# 内存释放
Halcon 的 .NET 接口里,图像对象有几种常见类型,新手容易混淆,列一张对照表:
| 类型 | 含义 | 典型使用 |
|---|---|---|
HObject | 图像数据实体(通道+区域) | 传给算子做处理 |
HImage | 带语义的图像对象 | 保存、显示 |
HTuple | 通用数据元组 | 传参、取结果 |
HRegion | 区域/ROI | 模板匹配的搜索区 |
内存释放这块,C# 里有 GC,但HObject底层封装的是 Halcon 的 C 指针。GC 只回收托管对象,不保证立即释放非托管内存,在长运行的视觉程序里,每帧都 new 一个HObject再丢给 GC,内存占用会一路爬升。推荐用using块或手动Dispose(),尤其在高帧率采集场景,不要等到 GC 来救你。源码里如果看到HOperatorSet.ReadImage之后没有Dispose,二开时建议补上,这属于我拿到源码第一轮就会做的优化。
4. 模板匹配与检测核心:从形状匹配到参数调优
4.1 0.0.1.2 版本能做什么:模板匹配主流程拆解
VisionAndMotionPro 这版虽然版本号是早期阶段,但主流程已经跑通了“加载模板—搜索目标—输出坐标角度”的闭环。核心算法用的是 Halcon 的形状匹配,对应算子主要是create_shape_model和find_shape_model,在 C# 里就是HOperatorSet.CreateShapeModel和HOperatorSet.FindShapeModel。形状匹配比灰度匹配鲁棒得多,它对光照变化、遮挡、噪点都不敏感,适合金属件定位,这也是工业视觉里的标配算法。下面这段代码是从 VisionAndMotionPro 的定位逻辑里抽象出来的核心调用方式:
// 创建形状模板 HOperatorSet.ReadImage(out HObject modelImage, @"D:\images\StandardImage1.bmp"); HOperatorSet.ReduceDomain(modelImage, out HObject modelRegion, out HObject reducedImage); HTuple modelID; HOperatorSet.CreateShapeModel(reducedImage, "auto", -0.39, 0.79, "auto", "auto", "ignore_local_polarity", "auto", out modelID); // 在测试图中查找模板 HOperatorSet.ReadImage(out HObject testImage, @"D:\images\5.bmp"); HTuple row, col, angle, score; HOperatorSet.FindShapeModel(testImage, modelID, -0.39, 0.79, 0.5, 1, 0.5, "least_squares", 0, 0, out row, out col, out angle, out score);CreateShapeModel的第二个参数"auto"让 Halcon 自动决定金字塔层数,-0.39和0.79是起始角和角度范围,单位是弧度,约等于正负 22.5 度。第六个参数"ignore_local_polarity"表示忽略局部极性,适用于目标在暗背景和亮背景里都可能出现的情况。FindShapeModel里的0.5是 MinScore,低于这个分数的匹配结果会被丢弃;0.5是 Greediness,值越大搜索越快但越容易漏检。
4.2 这几个参数决定成败:MinScore 与 Greediness
很多新手拿到find_shape_model的十几个参数直接懵,其实日常调参真正动的就四个:MinScore、Greediness、NumLevels、角度范围。MinScore 默认 0.5,实际项目里我一般从 0.6 起步。如果现场误检多,就往 0.7、0.8 抬;如果漏检多,就降到 0.4,但降太狠,模板只要有一半被遮挡就能匹配上,误检率直线上升。Greediness是贪婪度,0 表示最严格、最慢但最稳;1 表示最快但容易跳过真目标。打磨产线上,我常用 0.7 作为起点,调试时除非帧率不够,否则不建议低于 0.5。
角度范围直接决定搜索耗时。如果工件只在 0 度和 180 度两个装夹方向,就设成大概率的三角展开,别给足 360 度范围,搜索时间会成倍增加。那什么时候用find_scaled_shape_model?当来料尺寸有波动、镜头高度有小幅变化时,用缩放匹配,多出四个缩放范围参数,C# 侧调用它的逻辑和上面这段几乎一样,只是把FindScaledShapeModel替换进去,再额外传入ScaleMin=0.8, ScaleMax=1.2。
4.3 检测结果的数据可视化与导出
源码里的 MyChart 模块就是把每次检测的分数、位置偏差画成折线或柱状图。实际项目中,我建议同时记录三样东西:时间戳、检测分数、XY 偏移量。MyChart 的刷新不需要每次检测都重绘,而是每 100ms 或每 10 次检测批量添加一次数据点,这样既能看到趋势,又不拖累主线程。常见做法是把检测结果写进一个ConcurrentQueue<DetectionResult>,再由定时器批量取出喂给图表组件,这一层的解耦会让整台上位机的稳定性上一个台阶。除了图表,结果也要落盘可追溯,典型代码是把它导出成 CSV。
// 检测结果落盘:CSV 追加写入 using (StreamWriter sw = new StreamWriter(@"D:\VisionAndMotion\results.csv", true)) { sw.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff},{row.D:F3},{col.D:F3},{angle.D:F2},{score.D:F3}"); }这套写法的好处是现场不用装数据库,Excel 或 WPS 就能直接打开分析,班次结束拿 CSV 拉个透视表,哪台设备良率波动一眼就能看出来。row.D和col.D是把 HTuple 转成 double 的写法,注意这里的下标索引问题:FindShapeModel返回的 row、col 是元组,如果你的模板可能找到多个目标,必须用row[0].D拿第一个,直接row.D在元组长度大于 1 时会抛异常。
4.4 定位精度不够时,优先排查的不是算法而是标定
这是我在视觉项目里踩过最大的坑。模板匹配返回的行列坐标是像素坐标,要让机械手去抓,必须转换成机器人坐标系。VisionAndMotionPro 里如果要接运动控制,你需要一个九点标定流程:让机械手走九个已知位置,每个位置拍一张图,记录像素坐标和机器人坐标,然后用 Halcon 的vector_to_hom_mat2d计算仿射变换矩阵。
// 九九点标定:计算像素坐标到机器人坐标的仿射变换 HTuple px = new HTuple(100, 200, 300, 100, 200, 300, 100, 200, 300); HTuple py = new HTuple(100, 100, 100, 200, 200, 200, 300, 300, 300); HTuple rx = new HTuple(10, 20, 30, 10, 20, 30, 10, 20, 30); HTuple ry = new HTuple(10, 10, 10, 20, 20, 20, 30, 30, 30); HOperatorSet.VectorToHomMat2d(px, py, rx, ry, out HTuple homMat2D); // 后续每次匹配得到像素坐标后,转成机器人坐标 HOperatorSet.AffineTransPoint2d(homMat2D, row[0].D, col[0].D, out HTuple robotX, out HTuple robotY);九点标定要覆盖整个视野范围,而且机械手末端走的是方格路线而不是 Z 字,这样能减少机械误差对仿射变换求解的影响。标定做完之后,验证方法很简单:让机械手回到第一个点,看看视觉算出来的坐标和实际坐标差多少,1 毫米以内的误差属于正常,超过就要怀疑是相机安装角度、镜头畸变或标定板贴合度的问题,而不是匹配算法的锅。
5. 部署排错与 ROI 交互的进阶技巧
5.1 换电脑跑不起来,十有八九是运行时和位数不匹配
v0.0.1.2 的源码在开发机上能编译,拷到现场工控机上就报Halcon can not find feature in ...,这是最经典的部署问题。报这个错几乎都是 Halcon 运行时版本和程序引用版本对不上。最简单可靠的检查顺序:先看工程的“目标平台”是 x64 还是 x86,再确认halcondotnet.dll是从哪个目录引用的,最后检查工控机安装了对应版本的 Halcon Runtime。如果机器上没有装完整版 Halcon,需要单独装 Runtime 并设置系统环境变量HALCONROOT,指向安装目录。另一个高频坑是 CPU 指令集,老工控机用老版本 Halcon 通常没问题,但有些新 Halcon 版本要求 SSE4.1 以上,跑匹配算法直接非法指令崩溃。
5.2 交互式 ROI:如何在图像窗口上手动拖出检测区域
VisionAndMotionPro 里有HWindowControl,但 ROI 交互是否能直接用取决于源码有没有封装HWindowControl的HMessage回调。以我这个 Halcon 工程师的习惯,我更倾向于用 Halcon 提供的高级控件HSmartWindowControl来替换,它内部集成了鼠标缩放/拖拽,下面这段是任意手绘 ROI 后获取区域并用于模板匹配的思路:
// 在 HSmartWindowControl 上开启 ROI 绘制模式 hSmart.HalconWindow.SetDraw("margin"); hSmart.HalconWindow.SetColor("red"); // 用户拖拽结束后,得到 HRegion HRegion roi = hSmart.HalconWindow.DrawRegion(); HOperatorSet.ReduceDomain(image, roi, out HObject reduced); // 用 ROI 内的图像去创建模板 HOperatorSet.CreateShapeModel(reduced, "auto", 0, 0, "auto", "auto", "ignore_local_polarity", "auto", out HTuple modelID);生产现场换产时,操作工不需要重新学模板算法——直接在屏幕上拖一个框,点“学习模板”就完成新产品注册。DrawRegion是 Halcon 窗口的交互式方法,会阻塞直到用户双击完成绘制,所以要放在后台任务里调用,不能用它直接卡 UI 线程。线上环境模板管理至少要做三件事:模板文件按产品型号命名、保存对应的 ROI 坐标、存储建模板时用的参数。这样换产时一键加载,不依赖老师傅的记忆。
到这为止,从工程结构、显示刷新、模板匹配到部署排错,已经覆盖了 VisionAndMotionPro 源码里最值得花时间的四个落点。最后一个实操建议:拿到源码后第一件事不是跑通主流程,而是用5.bmp和6.bmp做一次完整的图像差分——把标准图和你现场拍的暗光图放进同一个流程里跑,先量化差异,再动算法参数。这样做能帮你最快摸清这套平台在你自己的工况下极限在哪。
本文还有配套的精品资源,点击获取