简介:一套基于C#与HALCON的模板匹配视觉框架,专为工业场景中的物品定位与匹配效率优化而设计,适合机器视觉工程师、上位机开发人员及HALCON初学者参考。工程以窗体程序展示完整交互流程:先选取搜索局域与模板局域,若物品超出搜索局域会导致匹配失败;再通过涂抹屏蔽多余线条,可减少干扰并提升匹配速度;随后设置匹配参数,角度保持默认,搜索个数默认为一,重叠个数可使用默认,计算精度按需调整,金字塔级数设置为七到三时匹配时间更短;所有参数在创建模板时一并持久化保存。实测在五百万像素迈德威视相机上匹配耗时约两到三毫秒。压缩包共三十八个文件,以九个C#源码为核心,辅以可执行程序、DLL动态库、配置与资源文件等,整体约二十五点七五兆字节,目录结构直观,可直接运行与二次开发。目前已有三千六百九十八人学习下载,对于希望理解HALCON模板匹配中区域选择、干扰屏蔽与参数持久化设计的人来说,是一份可直接落地的参考。 做机器视觉上位机这几年,被问得最多的一个组合就是 C# 加 HALCON。2021 年前后那阵子,很多项目从原来的纯 HDevelop 脚本转成 C# 联合开发,标题里那句“模板匹配,读取,涂抹,设置匹配参数,持久化保存参数”其实不是几件事,而是一条完整的工业视觉定位流水线。读图、把特征区域涂出来、生成模板、调好匹配参数、最后把整套配置落盘,下次开机直接加载,省掉重复调试的时间。这篇文章就把这条流水线拆开讲清楚,适合正在学 C# 上位机、或者刚拿到 HALCON 授权准备做视觉项目的朋友直接参考。
1. 项目全貌:C#+HALCON模板匹配这条流水线
1.1 拆解标题里的五个动作
先把这个标题翻译成人话。
“读取”指的是拿到图像来源,本地文件、相机采集、网络图片都算;“涂抹”不是美工修图,而是在图上手动圈出你关心的区域,也就是 ROI,告诉算法“我只认这个区域里的特征”;“模板匹配”就是让程序记住这个区域的特征,之后在每张新图像里找相似的目标;设置匹配参数则是在找目标时控制“找多快、找多准、允许旋转多少度”;“持久化保存参数”是把刚才调好的模型和参数写进磁盘,避免每次开机重来。
这五个动作串起来就是典型的视觉定位方案。比如机器人抓取,先拍一张标准工件,涂抹出抓取点特征,生成模板,保存参数,产线上每来一个工件就匹配一次,输出坐标和角度给机器人。我在实际项目里,这套流程应付过零件定位、标签检测、二维码辅助定位,基本逻辑都一样。
1.2 这组技术选型能帮你解决什么问题
C# 加 HALCON 的组合在工业视觉里太常见了。HALCON 的图像算法沉淀多年,模板匹配的稳定性和速度都很能打;C# 的优势则是开发效率高,界面、数据库、Socket 通信、PLC 交互这些周边功能写起来方便。你需要的是一个能快速集成 HALCON 算法,又方便做业务逻辑的上位机语言,选 C# 属于省力且稳妥的方案。
当然,这套组合也有学习门槛。HALCON 海量的算子容易让人眼花缭乱,C# 这边还要管内存释放和界面线程,如果不注意细节,就会出现模板丢失、UI 卡顿、程序退出异常。这些问题后面我会一个个说。
2. 环境准备:先把HalconDotNet引到C#工程里
2.1 版本、授权与安装细节
2021 年前后常用的 HALCON 版本已经到 20.11,现在 21 甚至更高版本也很成熟。安装时要注意勾选 .NET 组件,默认安装会带上 HalconDotNet.dll,位置通常在安装目录下的 bin 文件夹里。如果只装了运行时没有开发组件,C# 里是找不到这个 DLL 的。
License 是我见过新手踩坑最多的地方。模板匹配属于标准模块,一般来说基础授权都包含,但如果你用的是试用版或按模块授权的加密狗,就一定要确认创建模板和匹配的算子没有被额外限制。还有一点,HALCON 授权有时会跟系统时间绑定,时间改错了会导致 License 失效,弹出莫名其妙的授权错误,排错时先检查系统时间。
2.2 添加DLL引用和平台配置
新建一个 WinForms 工程,我习惯用 .NET Framework 4.7.2,稳定而且很多工控电脑预装运行库。在解决方案里右键引用,选择“添加引用”,浏览到 HalconDotNet.dll,然后把这个 DLL 的“复制本地”属性设为 true,避免发布时少了依赖。
另一个特别容易忽略的是平台目标。工业相机驱动和 HALCON 很多是 x64 的,所以在项目属性的“生成”里,把平台目标改成 x64,不要留在“Any CPU”。否则运行时加载 DLL 失败,报 BadImageFormatException,你可能会误以为是 HALCON 没装好,其实是位号不一致。
代码开头先引入命名空间。
using HalconDotNet;2.3 一句代码验证读取图像
环境配好之后,最快验证方式就是读一张图,显示到 HWindowControl 控件上。拖一个 HWindowControl 到窗体,再放一个按钮,按钮事件里写这么几行:
private void BtnLoad_Click(object sender, EventArgs e) { HImage image = new HImage(); HOperatorSet.ReadImage(out image, @"D:\Work\Images\sample.png"); hWindowControl1.HalconWindow.DispObj(image); }这里要特别注意路径里的反斜杠,C# 字符串里要转义,或者直接用 @ 前缀。如果能弹出图像,说明 HalconDotNet 引用和环境完全没问题,可以进入下一步。
3. 制作模板:读取图像和“涂抹”ROI
3.1 读图不只是读图片:图像数据类型要统一
工业现场很少直接给你一张 PNG,相机 SDK 拿到的往往是一段灰度字节流,或者已经封装成 HObject 的帧。如果拿到的是 byte 数组,需要先用 gen_image1 或 gen_image3 转成 HImage,这个过程要注意宽度、高度、像素格式是否匹配,否则显示出来是花的。
模板匹配对颜色不敏感,三通道彩色图反而会拖慢速度。我习惯先把图像转成 byte 灰度,再往下走。
HImage gray = image.ConvertImageType("byte");如果是彩色图要转灰度,可以先用image.Rgb1ToGray(),或者用ConvertImageType统一像素类型。代码写到这里,你手里的图像就是干净的灰度图了。
3.2 “涂抹”ROI的两种实现方式
所谓涂抹,意思是告诉 HALCON:模板特征在这个区域里,其他位置都不重要。实现方式有两种。
第一种是调用 HALCON 自带的交互绘制算子。HDevelop 里常用的 draw_region、draw_rectangle1 在 C# 里也能调用,但它们是阻塞式的,会一直等待鼠标操作,在 WinForms 界面里直接用很容易造成窗体假死。除非你有十足把握,我建议用第二种。
第二种是自己用鼠标事件在 HWindowControl 上画矩形。流程是:MouseDown 记录起点,MouseMove 画一个临时矩形,MouseUp 记录终点并生成 HRegion。核心代码大致如下:
private double startRow, startCol; private void hWindowControl1_MouseDown(object sender, MouseEventArgs e) { startRow = e.Y; startCol = e.X; } private void hWindowControl1_MouseUp(object sender, MouseEventArgs e) { HOperatorSet.DispObj(image, hWindowControl1.HalconWindow); HOperatorSet.DispRect1(hWindowControl1.HalconWindow, Math.Min(startRow, e.Y), Math.Min(startCol, e.X), Math.Max(startRow, e.Y), Math.Max(startCol, e.X)); HRegion roi = new HRegion(); roi.GenRectangle1( Math.Min(startRow, e.Y), Math.Min(startCol, e.X), Math.Max(startRow, e.Y), Math.Max(startCol, e.X)); }这样就能在图像上框出目标区域。如果工件形状不规则,也可以用类似思路支持多点围成多边形,本质就是鼠标点集生成 Region。
3.3 创建形状模板的底层逻辑
ROI 出来之后,下一步不是直接创建模板,而是先做 reduce_domain。简单理解就是裁剪出那一小块图像,让算法只关心 ROI 内部的特征,避免背景干扰。
HImage imageReduced = image.ReduceDomain(roi); HTuple modelID; HOperatorSet.CreateShapeModel(imageReduced, 6, 0, 360, "auto", "auto", 20, 5, out modelID);CreateShapeModel 的参数解释一下:
- 6 是金字塔层数,层数越多匹配越快,但模板特征太少时容易失败
- 0 和 360 表示允许匹配时在 0 到 360 度范围内旋转
- "auto" 让 HALCON 自动决定角度步长和优化方式
- 20 是对比度阈值,小于这个灰度差异的特征会被忽略
- 5 是最小对比度,直接影响匹配时抗干扰能力
创建成功后,modelID 就是一个可以被匹配的句柄。如果想把模板存成文件,HALCON 有现成算子:
HOperatorSet.WriteShapeModel(modelID, @"D:\Work\Models\demo.shm");这一步之后,“涂抹 ROI + 生成模板 + 保存模板文件”的链路就闭环了。
4. 设置匹配参数:让模板在不同工况下都能找到目标
4.1 四个核心匹配参数怎么调
模板建好了,FindShapeModel 成了主力,但参数不对,结果就是找不着或者乱找。我用一个表格把最关键的几个参数列出来。
| 参数名 | 推荐范围 | 作用与经验 |
|---|---|---|
| AngleStart / AngleExtent | 实际旋转范围 | 如果工件姿态任意摆放,设 -180 到 360;如果只有轻微旋转,设 -10 到 20,能省不少时间 |
| MinScore | 0.5 到 0.8 | 分数阈值越低越容易误检,越高越容易漏检,现场一般从 0.6 起调 |
| NumLevels | 4 到 8 | 匹配时使用的金字塔层数,大图可以设为 6 以上,但要确保模板纹理够丰富 |
| Greediness | 0.5 到 0.9 | 越大匹配越快,但可能跳过一些候选位置,第一次调参先给 0.8,找不到就降 |
| SubPixel | least_squares | 亚像素精度,工业定位建议至少用 least_squares,输出坐标更平滑 |
一次典型的匹配调用是这样的:
HTuple row, col, angle, score; HOperatorSet.FindShapeModel(image, modelID, -10, 20, 0.65, 1, 0.5, "least_squares", 6, 0.8, out row, out col, out angle, out score);这段代码的意思是:在当前图像里找 1 个目标,角度范围从 -10 度到 20 度,最小分数 0.65,亚像素精度 least_squares,金字塔层数 6,贪婪度 0.8。匹配成功的话,row、col 是目标中心坐标,angle 是旋转角度,score 是匹配质量。
4.2 动态调整参数并实时预览
调参这件事不能每次都改代码重编译。我习惯在界面上放一排 NumericUpDown,分别绑定 MinScore、NumLevels、Greediness 等参数,旁边放一个“单次匹配”按钮,点一下就跑一次 FindShapeModel,再在图像上画结果。
这样调参效率会高很多。你可以在现场看着工件实时调,调到一个稳定的阈值区间后,再把参数固化到配置文件里。我踩过一个坑,只看一次匹配正确就保存参数,结果换了光照之后出现大量误检,后来都是连续测几十张图才敢锁参数。
4.3 匹配结果怎么用到实际项目
模板匹配的最终目的往往是输出坐标给机器人,或者做尺寸测量。HALCON 返回的 row、col 是像素坐标,angle 是弧度,如果要引导机器人抓取,还要做像素坐标到世界坐标的标定。
如果只是做相对偏移,可以在建模板时记录一个参考点,比如模板中心在基准图中的位置,然后每帧匹配得到的坐标减去参考点,就是当前工件的偏移量。这个思路简单,在很多定位项目里足够用。真正涉及旋转中心和相机标定时,再上齐 vector_to_hom_mat2d 这套变换,本文先不展开。
5. 持久化保存参数:让配置可以“重启不变”
5.1 三类数据要分开存
很多新手以为持久化就是 SaveFileDialog 保存个文件,其实工程化要考虑三点:模板模型、ROI 区域、匹配参数。
HALCON 的模型文件只保存模板特征,不关心你界面上把 MinScore 设成了多少;ROI 区域如果变了,模板强度也会变;匹配参数则是经验值,需要能快速调整。所以我习惯分成两到三个文件:
- .shm 文件,存模板模型,由 HALCON 的 WriteShapeModel 生成
- 配置文件,存 JSON 或 XML,包含图像路径、ROI 坐标、匹配参数、显示设置
- 可选:现场截图、参数备份目录
这样分工的好处是,一个模板文件可以配合多套参数,比如不同光照下的不同 MinScore,只需要切换配置文件,不用反复重建模板。
5.2 用JSON把MatchConfig保存到磁盘
C# 里序列化配置最简单的方式是 Newtonsoft.Json,或者 .NET Core 时代用 System.Text.Json 也行。先定义一个配置类:
public class MatchConfig { public string TemplatePath { get; set; } public double MinScore { get; set; } public int NumLevels { get; set; } public double AngleStart { get; set; } public double AngleExtent { get; set; } public double Greediness { get; set; } }保存时序列化成 JSON:
MatchConfig cfg = new MatchConfig { TemplatePath = @"D:\Work\Models\demo.shm", MinScore = 0.65, NumLevels = 6, AngleStart = -10, AngleExtent = 20, Greediness = 0.8 }; File.WriteAllText(@"D:\Work\Config\match.json", JsonConvert.SerializeObject(cfg, Formatting.Indented));读取同样简单:
MatchConfig cfg = JsonConvert.DeserializeObject<MatchConfig>( File.ReadAllText(@"D:\Work\Config\match.json"));考虑到现场调试人员不一定能看懂 JSON,你也可以再做个“导出调试报告”功能,把配置、参数截图、匹配结果图打包到一个文件夹,方便事后追溯。
5.3 启动时一键恢复模板和参数
程序启动时自动加载配置,是最能体现持久化价值的场景。代码套路不长:
private MatchConfig config; private void Form_Load(object sender, EventArgs e) { if (File.Exists(configPath)) { config = JsonConvert.DeserializeObject<MatchConfig>( File.ReadAllText(configPath)); if (File.Exists(config.TemplatePath)) { HOperatorSet.ReadShapeModel(config.TemplatePath, out modelID); } // 把参数回填到界面上 numMinScore.Value = (decimal)config.MinScore; numNumLevels.Value = config.NumLevels; numAngleStart.Value = (decimal)config.AngleStart; numAngleExtent.Value = (decimal)config.AngleExtent; numGreediness.Value = (decimal)config.Greediness; } }这里有个细节,ReadShapeModel 出来的 modelID 是非托管句柄,每次重新加载前要记得释放旧的,否则长时间运行会句柄泄漏。可以用HOperatorSet.ClearShapeModel(modelID),或者在使用完的模型对象上调用 Dispose。程序退出时也建议统一释放所有 HALCON 资源。
6. 常见问题与排查技巧实录
6.1 “Can not find feature”和HALCON报错怎么定位
HALCON 的错误提示有时比较抽象,比如 "Can not find feature in the model",看到这行字先别慌,按以下顺序排查:
第一,检查模板区域和 reduce_domain。很多人直接在整张原图上创建模板,没有做 reduce,导致模板里包含大量背景,匹配时特征不稳定。
第二,降低金字塔层数。模板区域小、纹理少时,层数太高会丢失细节,从 6 降到 4 或 2 往往能解决。
第三,调低 minContrast。如果图像对比度不够,模板特征提取不出来,把最小对比度降到 10 以下再试试。
第四,确认匹配图像和模板图像分辨率一致。模板是在 500 万相机下建的,现场换成了 200 万相机,不重新建模板的话很容易找不到。
6.2 License和运行时授权那些事
HALCON 在开发机上跑得好好的,部署到工控机上突然报授权错误,最常见的原因是运行时 License 没配置好。正版授权一般会提供一个 License 文件,放到对应目录,程序每次启动会自动读取。试用授权通常有有效期,时间一到就会报错,项目上线前一定要检查授权有效期,别等客户产线停线了才发现。
另外要确认授权模块包含你用的算子。模板匹配里面 CreateShapeModel、FindShapeModel 属于标准模块,但有些高精度或 3D 相关的算子需要额外授权,否则调用时会报 "feature not supported"。这种问题查得越早越好。
6.3 C#循环采集、扫码枪触发和UI卡顿
很多上位机项目会碰到“循环采集图像,界面卡成 PPT”的情况。绝大多数原因是把算法、采集、显示全塞在 UI 线程里跑。FindShapeModel 再快,一帧几毫秒到几十毫秒,如果主线程每毫秒都在刷新图像,窗体响应必然会迟钝。
解决思路是分线程。采集和匹配放到后台线程,等结果出来再用 Control.BeginInvoke 把坐标、截图更新到 UI。扫码头也是类似,扫码枪通过串口或网口发来一个条码,这不是定时循环,而是事件触发,不要在串口 DataReceived 事件里直接做耗时操作,把条码丢进队列,由匹配线程取出来执行即可。
private void OnScanCodeReceived(string code) { Task.Run(() => { HTuple row, col, angle, score; HOperatorSet.FindShapeModel(currentImage, modelID, ...); // 处理结果,转回 UI 线程显示 this.BeginInvoke(new Action(() => { lblCode.Text = code; lblPos.Text = $"X={row.D}, Y={col.D}"; })); }); }这样做的好处是扫码、采图、匹配各自不阻塞,界面始终能拖动,参数调整也有实时反馈。
6.4 我做这类项目保留的几个习惯
最后说几个我自己的固定操作。
第一,所有配置文件和模板路径用相对路径,发布时把 Work 目录拷到客户电脑,程序启动时基于 AppDomain.CurrentDomain.BaseDirectory 拼接路径,避免抱着一堆绝对路径到处改。
第二,每调一次参数,用 HALCON 的 DumpWindowImage 或 C# 截屏把当前匹配结果存下来,命名带时间戳。这样如果后面发现误检,能回看是哪一次调参导致的,排查效率翻倍。
第三,模板文件和配置文件定期备份,同时保存一份参数说明 txt,把现场光照、相机高度、工件材质写清楚。三个月后客户说“程序怎么不准了”,你能最快判断是环境变了还是参数漂了。
第四,凡是涉及非托管资源,牢记三条规矩:用完释放、重载前释放、程序退出前释放。C# 有垃圾回收不假,但 HALCON 句柄很多不在托管堆里,等 GC 来收就晚了。
这套 C# 加 HALCON 的模板匹配流程,代码量不大,真正的坑集中在环境、参数和运维习惯上。照着上面的步骤走一遍,从读图到保存参数一条链跑通,后面再扩展相机采集、多模板切换、通信对接,都会顺很多。
本文还有配套的精品资源,点击获取