恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
C#与HALCON视觉检测系统开发实战:从模板匹配到SQLite落库的完整工程
首页
资讯中心
/
C#与HALCON视觉检测系统开发实战:从模板匹配到SQLite落库的完整工程
C#与HALCON视觉检测系统开发实战:从模板匹配到SQLite落库的完整工程
发布时间:2026/10/16 10:37:55
简介一份基于 C# 与 HALCON 开发的完整机器视觉检测软件系统源代码包面向工业视觉应用开发者、机器视觉初学者及需要二次集成的工程技术人员。系统主打工件表面质量检测覆盖图像采集、预处理、特征提取、缺陷判断与结果展示全链路源码结构清晰有助于理解 C# 与 HALCON 算法库如何协同完成实际视觉任务。压缩包共 245 个文件约 45.48MB内含 80 个 C# 源文件、35 幅 BMP 样本图、必要的 DLL 与 EXE 依赖库、项目工程配置及数据库脚本等分类明确便于按模块查阅。已有 7063 人下载学习适合对照代码掌握机器视觉系统搭建流程也可基于现有模块扩展检测功能或整合其他硬件设备。1. 一套能上产线的 C# HALCON 视觉检测系统不是脚本 Demo是能交付的完整工程一条典型的产线视觉检测需求长这样产品流过来相机拍照系统判断尺寸合不合格结果显示在屏幕上数据存进库里。HDevelop 里跑得好好的脚本到了交付阶段往往缺一大块——没有操作界面、没有检测记录、没有参数回退机制。这套源码解决的就是这个断层用 C# 写上位机把 HALCON 图像处理能力嵌进去把相机采集、模板定位、卡尺测量、结果判定、SQLite 存储、用户权限串成一套能直接上线的完整系统。适合两类读者。一类是想用 C# 调 HALCON 做产线视觉项目但不想从空白窗体搭框架的开发者另一类是手里有 HALCON 脚本想知道脚本怎么跟界面、数据库、日志这些工程化部分联动的开发者。这套系统的价值在“完整”——每个环节都是能落地的工程代码不是单点 Demo。2. 系统架构拆解UI、业务、算法、数据四层怎么分工协作2.1 拿到源码先看目录工程分层一看就懂我拆这类工程有个习惯先不开 Visual Studio而是先看目录结构。把整个系统的源码摆在面前时第一件事是确认分层是否清晰。这套源码的目录设计非常规矩大致是下面这个样子VisionSystem/ ├── VisionSystem.sln ├── src/ │ ├── Vision.UI/ # WinForms 界面工程 │ ├── Vision.Core/ # 业务逻辑与流程编排 │ ├── Vision.Algorithm/ # HALCON 算法封装 │ ├── Vision.Data/ # SQLite 数据访问 │ └── Vision.Communication/ # TCP / PLC 通信 ├── config/ │ ├── camera.config │ ├── recipe.config │ └── logging.config └── models/ └── shape_model.shm四个 src 目录正好对应四个职责区域。Vision.UI 只做界面展示和用户交互不写图像处理Vision.Algorithm 是核心负责 HALCON 的算子调用不依赖任何窗体Vision.Core 做流程调度它知道“先拍照、再定位、再测量”的顺序Vision.Data 只管数据库读写把检测结果落库。我拿到源码后最先打开的是 Vision.Algorithm因为它是整个系统的技术核心含金量最高。其次是 Vision.UI主要看数据绑定逻辑确认界面是直接操作控件还是走了 MVVM 之类的模式。这套源码用的是 WinForms控件操作比较直接读起来不费劲。2.2 技术选型为什么是 C# 加 HALCON 而不是纯脚本或 C选型这件事在产线项目里通常不是技术偏好问题而是开发效率问题。HDevelop 做图像算法验证和调试确实快拉个模板、调个阈值几分钟就能看到效果但它做不了友好的工业界面也做不了数据库和通信联动。纯 HDevelop 脚本交付给产线操作员面对的是一个调试窗口这是没法用的。C# 在上位机领域生态成熟WinForms 和 WPF 都够稳定SQLite、日志组件、串口和 Socket 通信库都是现成的。C 性能更强但对团队要求高开发周期长视觉项目的瓶颈多数时候不在算子执行速度上而在业务逻辑和交互流程上。方案优点缺点适合场景HDevelop 脚本调试快、算子验证直观无界面、无数据库、难交付算法验证C# HALCON工程化完善、开发效率高需要维护 .NET 与 HALCON 版本匹配产线交付C HALCON性能上限高开发成本高、联调周期长高速高频检测这套系统的稳定姿态就是技术栈选型里“性价比最高”的那个C# 做壳HALCON 做芯。2.3 一次完整检测的业务流从传感器触发到数据落库我看代码时习惯先梳理业务流再回头细看每个代码块的实现。这套系统的检测流程是典型的上位机视觉检测循环完整链路可以拆成九步外部传感器或 PLC 发出触发信号Vision.Communication 层收到信号UI 把当前状态切换为“运行中”按钮置灰防重复触发CameraService 调用 GrabImage 采集一张 HImage图像预处理包括转灰度、降噪、ROI 裁剪FindShapeModel 做模板匹配找到产品在图像里的位置和角度Metrology 卡尺工具测量关键尺寸和配方里的上下限比较生成 DetectResult 对象结果写入 SQLite同时把 OK/NG 信号通过通信层回报给 PLCUI 用 BeginInvoke 刷新显示。这个流程每一步都不复杂但串起来容易出问题的地方在于两步之间数据怎么传递。这套系统的做法是设计一个统一的结果对象每个模块只管自己那一份数据最后在 Vision.Core 里把所有数据汇总成 DetectResult。这样的好处是逻辑清晰改算法不会牵连界面改界面也不会影响检测逻辑。2.4 线程模型采集和 UI 为什么不许挤在一个线程我看到很多从零搭的视觉项目第一次翻车都翻在线程上。相机的 GrabImage 在 GigE 相机上是阻塞调用如果直接放在按钮点击事件里执行界面会卡死拖动窗口都会一顿一顿。这套系统用的是典型的后台线程加 BeginInvoke 模式private async void BtnStart_Click(object sender, EventArgs e) { _running true; await Task.Run(() DetectionLoop()); } private void DetectionLoop() { while (_running) { _camera.Grab(); var result _detector.Run(_camera.CurrentImage); // 跨线程改控件会抛异常必须通过 BeginInvoke 回 UI 线程 this.BeginInvoke(new Action(() { lblStatus.Text result.IsOk ? OK : NG; lblValue.Text result.MeasuredValue.ToString(F3); })); Thread.Sleep(20); // 给 UI 消息循环喘息时间 } }DetectionLoop 跑在后台线程里采集和检测都在这里执行不阻塞界面。每次循环结束后 Sleep 20 毫秒是为了避免线程空转把 CPU 吃满。BeginInvoke 的语义是“把这段代码丢回 UI 线程执行”这是 WinForms 跨线程更新界的标准解法。这套系统的线程模型没有标新立异但足够可靠产线环境下稳定优先。3. 核心模块实战从相机采集到结果落库的代码全流程3.1 相机采集OpenFramegrabber 参数逐个说清楚相机这块是新手最容易懵的地方因为 OpenFramegrabber 的参数量很大看一眼就劝退。但拆开看其实就三组接口类型、采集配置、连接字符串。这套源码里封装了一个 CameraService核心代码长这样using HalconDotNet; public class CameraService : IDisposable { private HTuple _acqHandle; public bool Open(string cameraName) { try { HOperatorSet.OpenFramegrabber( GigEVision, // 接口类型GigE 相机用 GigEVision 0, 0, 0, 0, 0, 0, // 图像尺寸参数0 表示用相机默认值 default, // Field 场模式 -1, // 位深-1 表示默认 default, // 颜色格式 -1, // 相机类型 false, // 是否使用外部触发 default, // 端口 cameraName, // 相机连接字符串通常是相机 IP 0, -1, out _acqHandle); return true; } catch (HalconException ex) { Log.Error(打开相机失败 ex.Message); return false; } } public bool Grab(out HImage image) { image null; try { HOperatorSet.GrabImage(out image, _acqHandle); return true; } catch (HalconException ex) { Log.Error(采集失败 ex.Message); return false; } } public void Dispose() { if (_acqHandle ! null _acqHandle.Length 0) HOperatorSet.CloseFramegrabber(_acqHandle); } }这里有个关键细节GrabImage 拿到的 HImage 是后续所有处理的数据源它属于托管对象但也包装了非托管内存。Dispose 里关掉采集句柄是必须的否则程序退出时相机会一直处于被占用状态这是产线项目里很典型的问题。我一般不在手写参数上纠结。HALCON 的采集助手可以直接生成开相机的代码把助手生成的参数粘过来替换掉相机 IP 就行。源码里这套参数是能直接用的但如果你换了一台相机建议重新用助手生成而不是手动去猜参数含义省时间也省心。3.2 模板匹配定位FindShapeModel 是整套系统的定海神针视觉检测系统的第一步永远是“先找到产品在哪里”。产品在图像里的位置会偏移、旋转所以需要用模板匹配做定位。这套源码的定位模块封装了 ShapeModel 的读取和匹配public class ProductLocator { private HTuple _modelID; public void LoadModel(string path) { HOperatorSet.ReadShapeModel(path, out HTuple modelID); _modelID modelID; } public Pose Locate(HImage image) { HOperatorSet.FindShapeModel( image, _modelID, -0.39, // 角度搜索下限约 -22 度 0.79, // 角度搜索范围约 45 度 0.7, // 最小匹配分数低于 0.7 视为找不到 1, // 返回的最大匹配个数 0.5, // 最大重叠比例 least_squares, // 亚像素精度least_squares 最准 0, // 金字塔层数0 表示自动 0.7, // 贪婪度越小越慢越准 out HTuple row, out HTuple col, out HTuple angle, out HTuple score); return new Pose { Row row.D, Column col.D, Angle angle.D, Score score.D }; } }定位的输出是一个 Pose包含行、列和角度三个值。这三个值决定了后续测量的参考坐标系不管产品怎么偏移和旋转测量工具都能跟着产品走。这里的角度范围 -0.39 到 0.79 是弧度制换算成角度大约是 -22 度到 45 度覆盖了产线上常见的摆放偏差。MinScore 设为 0.7 是一个比较稳妥的平衡点。太高了光线波动时容易找不到目标太低了会把错误位置当成目标。Greediness 的 0.7 也是类似逻辑追求速度时往 0.9 调追求稳定性时往 0.5 调。这套源码的默认值是产线调试过的结果我建议不轻易动。3.3 卡尺测量Metrology 模型让测量距离精确到亚像素定位之后是测量。这套系统用的是 HALCON 的 Metrology 模型它比手动找边缘再算距离的方式更工程化。测量对象加到模型里之后ApplyMetrologyModel 一次性执行所有测量HOperatorSet.CreateMetrologyModel(out HTuple metrologyHandle); HOperatorSet.SetMetrologyModelImageSize(metrologyHandle, width, height); // 添加一条线性测量对象 // 参数含义依次是模型句柄、类型、位置、测量长度、测量距离、阈值、参数 HOperatorSet.AddMetrologyObjectGeneric( metrologyHandle, line, new HTuple(new double[] { row1, col1, row2, col2 }), new HTuple(25), // 测量长度单位像素 5, // 测量距离卡尺宽度 20, // 边缘阈值 new HTuple(), new HTuple(), out HTuple measureObjectID); HOperatorSet.ApplyMetrologyModel(image, metrologyHandle); HOperatorSet.GetMetrologyObjectResult( metrologyHandle, measureObjectID, all, result_type, out HTuple result);Metrology 的优势在于测量线是依附于定位结果动态生成的。产品偏移了测量线的位置也随之偏移不需要每次重新标定。这套系统把这部分封装在 Vision.Algorithm 里外部调用者只关心最终结果数值。测量长度 25 表示卡尺在边缘两侧各延伸 25 像素测量距离 5 表示每条卡尺线之间的间距。这两个参数直接影响测量稳定性太小容易受到噪声干扰太大又可能跨到别的边缘上。实际调试时可以在 HDevelop 里打开测量助手拖动卡尺看实时测量效果参数确定后再固化到代码里。3.4 检测结果对象数据是怎么在模块之间流转的从定位到测量到判定中间会产生很多中间数据。这套系统没有用全局变量到处传而是定义了一个统一的 DetectResult 对象一次检测的所有输出都汇总到这里public class DetectResult { public string ProductId { get; set; } public bool IsOk { get; set; } public string DefectType { get; set; } public double Value { get; set; } public double UpperLimit { get; set; } public double LowerLimit { get; set; } public string ImagePath { get; set; } public long ElapsedMs { get; set; } public override string ToString() { return ${ProductId} | {(IsOk ? OK : NG)} | 实测 {Value:F3}; } }Value 是测量出来的实际尺寸UpperLimit 和 LowerLimit 是配方里的公差上下限IsOk 就是 Value 和上下限比较的结果。ProductId 是当前产品标识ImagePath 保存本次检测图像的落盘路径方便后续追溯问题批次。ImagePath 这个字段很多新手会忽略但在产线项目里非常重要。产品被判 NG 之后过一会儿产线工人会来问“刚才那个不良品长什么样”如果系统没有保存图像就只能凭口述完全没有说服力。这套源码把每张图像都按时间戳命名存档这是个很成熟的工程习惯。3.5 结果落库SQLite 存储与查询检测结果最终要落到数据库里这套系统用的是 SQLite方便部署不需要单独装数据库服务。写入逻辑被封装在 Data 层public void SaveRecord(DetectResult result) { using (var conn new SQLiteConnection(_connectionString)) { conn.Open(); using (var cmd conn.CreateCommand()) { cmd.CommandText INSERT INTO detect_record (product_id, is_ok, value, upper_limit, lower_limit, image_path, create_time) VALUES (pid, ok, value, upper, lower, img, time); cmd.Parameters.AddWithValue(pid, result.ProductId); cmd.Parameters.AddWithValue(ok, result.IsOk ? 1 : 0); cmd.Parameters.AddWithValue(value, result.Value); cmd.Parameters.AddWithValue(upper, result.UpperLimit); cmd.Parameters.AddWithValue(lower, result.LowerLimit); cmd.Parameters.AddWithValue(img, result.ImagePath); cmd.Parameters.AddWithValue(time, DateTime.Now); cmd.ExecuteNonQuery(); } } }参数化 SQL 是必须的检测结果里如果拼字符串万一产品编码里带了特殊字符SQL 就崩了。SQLite 的连接用完即释放避免长时间占用文件句柄这在产线连续运行几万次检测后差别很明显。4. 从源码到跑通环境配置、编译联调与配置项解读4.1 环境依赖清单源码本身不会凭空运行要把它跑起来需要准备这几样东西。第一个是 HALCON 运行时本机必须安装 HALCON安装时记得勾选 .NET 接口组件。第二个是 Visual Studio版本 2019 或 2022 都行工程目标框架一般是 .NET Framework 4.7.2。第三个是相机驱动但如果暂时没有相机完全可以先用 Demo 模式把整条软件流程跑通源码里保留了无相机模式。没有相机也能跑这件事很重要。我见过太多人在环境搭建阶段就被相机驱动卡住其实视觉系统的软件框架验证根本不需要相机。用 HALCON 安装目录自带的示例图像照样能验证模板匹配、测量、数据库落库这一整条链路。4.2 编译五步走拿到源码后按下面五个步骤操作每一步我都会说明为什么这么做避免重复踩坑。第一步用 Visual Studio 打开解决方案先看工程的 Target Framework 是否和本机安装的 .NET 版本匹配不一致会导致编译错误。第二步检查 HalconDotNet 引用。源码里的引用路径可能指向作者本机的 HALCON 安装目录换机器后这个引用会变成黄色感叹号需要在引用管理里删掉旧的重新从HALCON\bin\dotnet\版本目录添加 halcondotnet.dll。第三步把解决方案平台从 AnyCPU 改成 x64。HALCON 的 DLL 按 64 位和 32 位分开打包如果工程是 AnyCPU运行时在 64 位系统上默认以 64 位进程跑但引用的 DLL 可能是 32 位会出现运行时加载失败。第四步确认 HALCON 的运行时 DLL 能被程序找到最省事的方式是把 HALCON 的 bin 目录加到系统 PATH或者把需要的 DLL 复制到输出目录。第五步F5 启动如果一切正常会先弹出登录窗口登录后进入主界面。编译过程最常见的报错有三个。第一个是找不到 HALCON 的命名空间这是因为引用没加成功。第二个是 BadImageFormatException这是位数不匹配按上面的第三步解决。第三个是缺少 halcon.dll这是运行时 DLL 路径问题按第四步解决。4.3 无相机模式下怎么验证全流程源码里带了一个配置开关可以把系统切换到无相机模式用本地图像模拟采集。这个设计我特别喜欢因为它让开发和调试不依赖硬件public HImage LoadTestImage() { // 从 HALCON 安装目录的示例图像中读取 HOperatorSet.ReadImage(out HImage img, printer_chip/printer_chip_01); return img; }把这条读取逻辑接在 CameraService 的 Grab 方法内部当配置项的 DemoMode 为 true 时不调用相机而是读取本地图片。这样整个流程就可以脱离相机反复跑调试测量参数时特别方便。配上这个模式UI 的“开始检测”按钮仍然有效数据库仍然正常写入记录和真实采集的唯一区别只是图像来源。我在调试视觉项目时都是用这种方式先跑通全流程再去接真实相机。4.4 配置文件逐项解读这套系统的参数没有散落在代码里而是集中在 config 目录下。相机参数和检测配方是分开的这个设计很合理因为两者修改的频率完全不同VisionConfig Camera InterfaceGigEVision Namecam0 ExposureTime5000 / Recipe Currentdefault ModelPathmodels/shape_model.shm/ModelPath Measure UpperLimit25.05 LowerLimit24.95 / /Recipe /VisionConfig配置项作用修改后需要做什么Camera.Interface相机接口类型GigEVision 还是 USB3Vision重新打开程序才生效Camera.ExposureTime曝光时间单位微秒影响图像亮度保存后重新触发采集Recipe.ModelPath模板文件路径换产品就换模板重新加载模板Recipe.Measure公差上下限判定 OK/NG 的依据保存后立即生效这里特别注意测量公差上限下限做成配置之后产线换产品型号时不用重新编译程序只要改配置即可。这套系统把“换型”这个操作简化成了“改配置文件 换模板文件”这是它能直接上产线的重要原因之一。5. HALCON 与 C# 集成避坑指南5 条真实踩坑记录5.1 编译通过运行时“无法加载 halcon.dll”现象工程能编译但一运行就弹窗提示无法加载 halcon.dll 或相关依赖 DLL或者直接抛 BadImageFormatException。原因这个问题九成是位数不匹配。HALCON 安装时可以选择装 32 位还是 64 位工程编译成 x86 却引用了 64 位的 halcondotnet.dll运行时自然找不到对应 DLL。解决把工程平台统一改成 x64并在工程属性里确认 PlatformTarget 为 x64然后删除引用重新添加对应版本的 halcondotnet.dll。从那以后我新建视觉工程第一步就强制设 x64明白了这是视觉项目的硬性门槛。5.2 HDevelop 里能匹配C# 里 FindShapeModel 分数一直为零现象同一个模板同一张图在 HDevelop 里跑 FindShapeModel 输出很漂亮移植到 C# 里跑输出分数永远小于阈值定位失败。原因图像格式不一致。HDevelop 调试时读图默认是灰度而相机采集的彩色图可能直接传给了模板匹配接口模板匹配在内部对图像格式有要求遇到不支持的格式不会报错只会匹配失败。解决采集到图像后先做一次 Rgb1ToGray 转灰度再进入定位模块。模板匹配前统一走一遍预处理不做这个转换后续匹配结果就会写满玄学。5.3 相机采集卡住程序界面毫无响应现象点击开始检测后程序界面卡死窗口无法拖动点关闭也没反应过一会甚至提示“程序未响应”。原因GrabImage 是阻塞调用直接放在了 UI 线程执行。GigE 相机的网络传输存在波动一旦网络拥堵或相机异常阻塞时间会很长UI 线程被卡死整个程序看起来就死了。解决采集逻辑必须放到后台线程界面通过 BeginInvoke 更新。同时给采集过程做超时保护超过设定时间就强制放弃本次采集并重试这个机制在产线网络不稳定的场景里极其重要。5.4 长时间批量检测内存占用一直涨最后卡顿现象系统连续跑几千次检测后内存占用持续攀升从几百兆涨到几个 G界面操作开始明显变慢。原因HImage 包装了非托管内存如果没有正确释放垃圾回收器不会及时回收这部分内存。特别是 HObject 在循环里反复赋值时旧的图像对象如果没有 Dispose内存就会一直累积。解决每次循环里用到的 HImage 和中间 HObject在使用完毕后手动调用 Dispose或者利用 using 语句块包裹。我一般会做一个统一的图像处理上下文类在 finally 块里集中释放避免遗漏。5.5 加载模板文件路径带中文脚本执行失败现象把模型文件放在带中文的路径下程序里加载模板时HDevelop 脚本引擎偶尔执行报错但在英文路径下完全正常。原因HALCON 脚本引擎和底层文件接口对非 ASCII 字符路径的支持存在兼容问题中文路径会传参错误。解决整个工程根目录、模型文件路径、配置路径全部使用英文一条路径都不许出现中文。这个习惯虽然在项目协作时需要多沟通但能省掉大量莫名其妙的文件加载问题。视觉项目里“全英文路径”应该作为一条铁律写进团队规范。6. 二次开发思路把检测参数从“写死”改成可切换配方6.1 扩展点在哪里这套系统最容易改造的地方是测量参数和定位逻辑。Vision.Algorithm 层暴露了 LoadModel 和 Run 两个核心方法只要你新增的算法类实现了同样的接口替换定位器不会动到界面和数据库。这是分层带来的直接好处。如果你想给系统增加“多配方切换”功能重点在 Vision.Core 层加一个 RecipeManager负责按产品型号加载不同的配置和模板。6.2 一个具体改造把公差上下限改成随配方加载原始代码里上下限可能是常量。改造的第一步定义一个 Recipe 类从配置文件读取参数public class Recipe { public string ProductId { get; set; } public string ModelPath { get; set; } public double UpperLimit { get; set; } public double LowerLimit { get; set; } public static Recipe Load(string productId) { // 从 recipe.config 按产品编码读取对应节点 // 找不到时抛出异常防止无配方状态下误判 return new Recipe { ProductId productId, ModelPath default.shm, UpperLimit 25.05, LowerLimit 24.95 }; } }第二步检测流程里不再直接使用硬编码而是注入配方对象。第三步在 UI 上加一个产品型号下拉框切换型号时重新加载 Recipe 和 ShapeModel。验证方法很简单跑之前先用同一张测试图跑三遍把结果写死作为基准改造后再用同一张图跑对比测量值完全一致就说明改造没有引入回归。从那以后我每次接手视觉项目都愿意先跑通无相机模式用同一张图把全流程扛一遍再接真实相机。一套能顺利更换配方的视觉系统才真正具备上线后被长期维护的资格。希望帮到你。本文还有配套的精品资源点击获取