
简介面向C#与Halcon机器视觉开发者的通用视觉框架完整工程仿EasyVision设计支持多相机多线程调度。各算法工具以独立DLL封装主程序通过公共接口动态加载涵盖图像处理、检测识别、标定、几何工具、几何测量、逻辑工具、通讯工具等常用模块适合学习插件式视觉架构或略作修改直接用于产线项目。压缩包共2000个文件约61.97MB以cs源码、dll插件、resources与resx界面资源为主附有pdb调试符号、cache编译缓存和txt说明目录层级清晰便于定位与对照调试。已有1174人学习下载通过完整工程可掌握多相机并行调度、工具链插件化封装、模块间数据流转等关键实现能够快速替换或扩展算法DLL适配不同视觉检测场景。工程目录按主程序与各工具插件划分配套解决方案文件打开即可编译运行可作为视觉软件框架的二次开发底座。1. 仿easyvision的通用视觉框架这种软件解决的是产线上的什么问题一条视觉检测线最花时间的往往不是那个 find_shape_model 写得怎么样而是把相机、界面、流程、PLC 这些片段粘到一起客户换一个产品型号又要改一轮代码。所谓通用视觉框架就是把这一层粘合做成可视化配置操作员在界面上拖一拖、填几个参数检测流程就换好了C# 在背后调度 Halcon 算子和相机。基于 Winform Halcon C# 做一套仿 easyvision 的通用视觉框架本质上就是把“能跑通的 Demo”升级成“能交给产线操作员的软件”——这也是很多上位机工程师从写脚本走向做产品时最值得投入的一个方向。下面按我的落地路径把整套东西讲清楚。2. 选型逻辑先立住WinformHalconC#这套组合的边界在哪2.1 对比过QtHalcon和C#OpenCV之后为什么还是回到Winform做机器视觉上位机选型来回就那么几个组合C# Winform Halcon、C# WPF Halcon、Qt C Halcon、C# OpenCVSharp。我前前后后都试过最后留在 Winform Halcon 上原因是这套组合在“开发速度”和“现场可维护性”之间最平衡。Winform 的控件模型直白DataGridView、PropertyGrid、TabControl 都能直接绑定做参数配置界面非常顺手。大型视觉框架里最重的部分往往不是算法而是那几十个参数怎么在界面上摆好、存好、改完不翻车——Winform 在这个场景下几乎没有学习成本。WPF 的界面表现力强适合做要对客户演示的炫酷界面但视觉上位机大多数时候是工控机加显示器丑一点没关系可靠不出错更重要。Qt 的话如果团队是 C 背景、要做跨平台设备那确实合理但纯做 Windows 上的视觉工作站C# 的部署和调试效率明显更高。至于 OpenCVSharp算子确实免费但工业场景里模板匹配、测量、标定这些事OpenCV 要用一大段代码才能拼出 Halcon 三行算子的效果。Halcon 的 license 有成本可它换来的是现场调试效率HDevelop 里拖一张图就能试算子参数试好了再导进框架里这个工作流在生产环境下能省大量时间。对比项C# Winform HalconC# WPF HalconQt C HalconC# OpenCVSharp界面开发速度快控件直接绑定中等需要 XAML较慢信号槽要组织取决于 Winform 基础视觉算子丰富度Halcon 领先同左同左需自己拼装现场调试效率高HDevelop 联动同左同左低参数要硬编码授权与部署成本依赖 Halcon license同左同左开源无授权成本谁更合适桌面型视觉工作站重交互高美感界面跨平台设备软件预算敏感或算法研究这里要提醒一句Winform 的边界同样清楚。它不适合做需要复杂动画、多文档界面、跨平台运行的软件。如果你判断未来要往 Linux 设备上走那 Qt 是更远的投资如果只是做产线视觉机Winform 这套就足够。2.2 框架的骨骼界面层、流程层、算子执行层怎么分工不打架通用视觉框架能不能“通用”关键不在用了哪个控件而在分层。我一般把框架拆成四层各管各的互相只通过接口来往界面层只管两件事把参数展示给人看把人改的参数收下来。它不直接调相机也不直接调 Halcon 算子。流程层是框架的核心它维护一个“视觉步骤列表”每个步骤是一个可配置的算子封装这一步的输出交给下一步流程运行时把结果汇总给界面。算子执行层是真正调 Halcon 算子的地方统一收 HObject 进、出 HObject 或 HTuple 结果。通信层单独拉出来放相机、PLC、数据库避免界面和流程里到处散落 Socket 代码。// 算子执行层的一个抽象框架里每个视觉工具都实现这个接口 public interface IVisionStep { string Name { get; } // 步骤名如 模板匹配 bool Run(HObject input, out HObject output, out string result); void LoadParam(string xml); // 从配置还原参数 } // 流程层把步骤串起来执行 public class VisionJob { public ListIVisionStep Steps new ListIVisionStep(); public HObject Execute(HObject source) { HObject current source; foreach (var step in Steps) { // 每一步拿到上一步输出跑完再传给下一步 step.Run(current, out current, out _); } return current; } }代码逻辑说明IVisionStep是整个框架的“插件口”新增一个视觉工具就是实现这个接口。VisionJob是流程容器从相机拿到图像后丢进Execute它会按顺序跑完所有步骤并返回最终结果。参数说明Run里的result我设计成可人工阅读的字符串比如“OK 1.023mm”方便界面直接显示实际项目中还会多加一个Dict类型保存结构化数据方便上位机做数据上报。分层最直接的好处是换相机品牌时只改通信层换检测逻辑时只增删流程里的步骤界面层完全不动。2.3 Halcon窗口控件的选择HWindowControl 和 HSmartWindowControl 在交互上的差别这套框架里所有图像都显示在 Halcon 的窗口控件上而 Halcon .NET 接口提供两种HWindowControl和HSmartWindowControl。很多人第一次做框架时直接拖了 HWindowControl结果现场调试时发现手势缩放、框选放大全都做不了只能写一堆坐标换算代码。我的选择是主显示窗口一律用HSmartWindowControl。它自带鼠标滚轮缩放、按住中键移动、框选放大这些交互在产线调机时就是刚需——操作员要放大看某个缺陷边缘总不能让他回 HDevelop 里看。// 在窗体构造函数里初始化智能窗口 this.smartWindow.HalconWindow.SetPart(0, 0, height - 1, width - 1); this.smartWindow.HalconWindow.SetColored(12);逻辑说明SetPart是设置显示区域的原点和宽高这里把窗口显示范围固定成和图像尺寸一致否则画面会被拉伸变形。SetColored(12)是给 Region 显示随机彩色做缺陷区域叠加时视觉上更直观。参数说明12是彩色模式编号取值范围是 1 到 12现场调试时用大数值更醒目真正投入使用可以改成单一颜色避免误判。HSmartWindowControl 的代价是交互事件多、内部重绘更频繁对性能要求高。如果只是显示一个固化画面、不需要任何手势操作那 HWindowControl 更快。我在框架里把两个控件都封装了默认用智能窗口性能紧张的工位再切回普通窗口。3. 跑通最小视觉框架从新建工程到第一个采集显示完整链路3.1 工程结构搭建与Halcon运行库的引用先从零建一个可运行的骨架工程。我用的是 Visual Studio 2015 或 2019 都可以目标框架建议 .NET Framework 4.6.1 或 4.7.2平台目标选 x64——Halcon 在 64 位下性能更好而且工业相机 SDK 大多是 64 位。新建 Winform 工程后需要引用的 Halcon 核心程序集是这几个文件作用halcondotnet.dllHalcon .NET 接口算子封装在这里面hdevenginedotnet.dllHDevEngine 脚本引擎需要跑 HDevelop 脚本时用hcanvas.dll图像绘制底层库被上面两个依赖引用方式是“浏览”到 Halcon 安装目录下的bin\dotnet文件夹直接把 DLL 拖进引用列表。这里有个常见错误只引用 halcondotnet.dll 不够运行时如果提示找不到 hcanvas.dll就把bin\x64-win64下的所有运行库复制到程序输出目录。Halcon 的运行时 License 也要单独处理好——开发机上装完整版 Halcon产线机器上要装匹配的运行时 License否则程序一启动就提示 license 错误。3.2 相机采集线程与窗口显示的最小代码工业相机取流有两条路一是用 Halcon 自带接口HFramegrabber连接 GigE 或 USB3 相机二是用相机厂商 SDK 取到原图数据再转成 HObject。我的习惯是用厂商 SDK因为自动对焦、曝光控制、硬件触发这些功能在厂商 SDK 里最完整。相机回调拿到的是裸数据往往是 8 位灰度或者 24 位 RGB需要转成 Halcon 的 HObjectpublic static HObject ByteArrayToHObject(byte[] data, int width, int height, bool isColor false) { HObject image; // 把托管数组固定住让 Halcon 直接读内存避免多余拷贝 GCHandle handle GCHandle.Alloc(data, GCHandleType.Pinned); try { IntPtr ptr handle.AddrOfPinnedObject(); if (!isColor) { // 灰度图单通道直接生成 Image HOperatorSet.GenImage1(out image, byte, width, height, ptr); } else { // 彩色图Halcon 默认 RGB 顺序相机 SDK 常给 BGR要注明 byte order HOperatorSet.GenImageInterleaved(out image, ptr, bgr, width, height, -1, bgr, width * 3, 0, 24, 0, 0, 0); } } finally { handle.Free(); } return image; }代码逻辑说明GCHandle.Alloc把 byte[] 固定在托管堆上防止垃圾回收移动内存导致指针失效。GenImage1用这个指针直接创建一张单通道灰度图不需要经过 Bitmap 中转。GenImageInterleaved处理彩色图它会按内存中像素的排列方式生成图像。参数说明byte表示每个像素 8 位width和height是图像宽高彩色模式里width * 3是每行字节数因为每像素 3 字节24是每像素位数。工业检测场景大部分是灰度图跑定位测量也都基于灰度所以这个函数日常用第一个分支就够。有了取图函数接下来是采集循环和显示。注意这里最容易踩的坑就是跨线程操作窗口private void CaptureLoop() { while (_running) { // 相机 SDK 阻塞取帧超时返回 false if (_camera.GetFrame(out byte[] raw, 1000)) { HObject hobj ByteArrayToHObject(raw, _camera.Width, _camera.Height); // 显示必须切回 UI 线程否则 HSmartWindowControl 会闪烁或直接崩掉 _smartWindow.Invoke(new Action(() { HOperatorSet.DispObj(hobj, _smartWindow.HalconWindow); hobj.Dispose(); })); } } }逻辑说明相机取帧是阻塞式的拿到一帧处理一帧没有轮询空转。所有跟窗口有关的操作都用Invoke扔回 UI 线程跨线程直接操作 Halcon 窗口是我见过翻车率最高的写法。参数说明GetFrame的1000是超时毫秒数超过 1 秒取不到帧就返回 false这样相机断开时代码还能继续跑不会卡死整个线程。3.3 把第一步视觉流程封装成可复用方法模板匹配带完整参数采集显示跑通之后框架里第一个要封装的视觉工具就是模板匹配。以找圆形连接器位置为例我在 HDevelop 里建好模型后导出.shm文件然后在框架里封装一个TemplateMatcher类public class TemplateMatcher { private HTuple _modelId; private double _minScore 0.7; public bool LoadModel(string path) { // 从磁盘读取训练好的形状模型 HOperatorSet.ReadShapeModel(path, out _modelId); return true; } public void Match(HObject image, out HTuple row, out HTuple col, out HTuple angle) { HTuple score; HOperatorSet.FindShapeModel(image, _modelId, 0, 6.28, _minScore, 1, 0.5, least_squares, 7, 0.9, out row, out col, out angle, out score); } }代码逻辑说明LoadModel把 HDevelop 里训练好的形状模型读进内存Match在传入图像上搜索该模型返回的是位置行坐标、列坐标和旋转角度。参数说明0是搜索起始角度6.28表示全角度搜索如果工件安装方向固定可以改成0到0.5来大幅提速_minScore是最低匹配分数0.7 意味着模型和目标至少有 70% 相似度太低会找到一堆假目标太高会漏检least_squares是亚像素细化模式测量场景用它最准0.9是 Greediness 贪心系数越大搜索越快但越容易漏现场一般从 0.8 开始试。这套封装做完最小视觉框架已经具备三件事取图、显示、单一视觉工具。下一步就是把多个工具按顺序组合成一条完整流程。4. 从Demo到通用框架把这几个模块补上才算完整4.1 视觉流程抽象采集、定位、测量、判定怎么描述成可配置步骤单工具封装好之后通用框架区别于 Demo 的第一道分水岭是检测流程能不能脱离代码、用配置表达。我一般把一条视觉流程定义成 JSON 数组每个元素是一个步骤每个步骤带类型和参数{ ProductName: A型连接器, Camera: { Index: 0, Exposure: 2000, TriggerSource: Hardware }, Steps: [ { Type: GrabImage, Params: { Camera: 0 } }, { Type: TemplateMatch, Params: { ModelFile: models/connector.shm, MinScore: 0.7 } }, { Type: Measure, Params: { Direction: vertical, Threshold: 128 } }, { Type: Judge, Params: { MinWidth: 2.0, MaxWidth: 2.1 } } ] }执行引擎拿到这份配置逐个创建对应的步骤实例串起来跑public void ExecuteJob(string configPath, HObject source) { // 反序列化产品配置 VisionConfig cfg JsonConvert.DeserializeObjectVisionConfig(File.ReadAllText(configPath)); foreach (var stepCfg in cfg.Steps) { // 按类型创建步骤实例Param 反序列化成该步骤的参数对象 IVisionStep step StepFactory.Create(stepCfg); step.LoadParam(stepCfg.Params.ToString()); step.Run(source, out source, out string stepResult); _log.AppendLine($[{DateTime.Now}] {step.Name}: {stepResult}); } }代码逻辑说明StepFactory是一个简单的工厂类根据Type字符串 new 出对应的步骤实例。这样做最大的好处是新增一种视觉工具时只需要加一个类和一条 Type 映射不用改执行引擎。参数说明configPath指向产品对应的配置文件一套框架管多种产品时就是切不同的配置文件_log.AppendLine把每一步的文本结果追加到日志产线追溯全靠它。注意一个细节JSON 里的Params是动态对象反序列化时我一般转成字典再塞给步骤对象不强行绑成一个强类型这样各步骤参数可以长得不一样框架才“通用”得起来。4.2 多相机与信号交互一台上位机带四路相机的常见做法产线上一台上位机带四个相机非常常见。我的做法是一个相机一条独立采集线程互不阻塞但它们取图后要进入同一个显示区所以取完图用队列汇总由单独的 UI 线程取出来统一显示。多相机同时取流时如果用硬件触发要特别确认每个相机的触发信号是不是分时给的——工业上常用旋转编码器或 PLC 分时触发多台相机避免同时曝光造成光源供电不足。// 用阻塞队列做多相机图像汇合 private BlockingCollection(int camId, HObject image) _frameQueue new BlockingCollection(int, HObject)(capacity: 8); // 每个相机线程取到帧就往队里塞 private void CameraWorker(int camId) { while (_running) { if (_cameras[camId].GetFrame(out byte[] raw, 1000)) { HObject hobj ByteArrayToHObject(raw, _cameras[camId].Width, _cameras[camId].Height); _frameQueue.Add((camId, hobj)); } } } // UI 线程统一拿队列里的帧来显示 private void DisplayWorker() { foreach (var (camId, image) in _frameQueue.GetConsumingEnumerable()) { _smartWindow.HalconWindow.DispObj(image); image.Dispose(); } }代码逻辑说明BlockingCollection自带线程安全capacity 设为 8 是给队列限容如果生产者太快Add会阻塞天然形成背压防止内存被帧堆爆。参数说明(int, HObject)元组里存的是相机编号和图像对象因为四路画面要区分来源GetConsumingEnumerable会一直阻塞等待新帧UI 线程不需要轮询不空转 CPU。信号交互方面PLC 通常通过 Modbus TCP 或 TCP 透传发指令框架里我会单独开一个通信线程用队列把收到的指令转换成框架内部命令比如“切到型号 A”“开始检测”“停止检测”。注意从来不在 PLC 回调里直接操作界面或相机只往命令队列里塞消息。4.3 配置持久化与产品换型一套框架管多个型号框架管理多个产品型号核心就是一个目录约定每个型号一个文件夹里面放视觉配置 JSON、模板文件、参数文件。界面上的按钮“选择型号”其实就是在切换这个文件夹的路径。配置文件读写有一个很值得注意的操作细节不要直接覆盖原文件。我踩过配置写到一半断电、JSON 文件损坏的情况后来改成“先写临时文件再替换”的两步策略public static void SaveConfig(string path, VisionConfig cfg) { // 先序列化到同目录下的临时文件 string tmp path .tmp; File.WriteAllText(tmp, JsonConvert.SerializeObject(cfg, Formatting.Indented)); // 再原子替换原文件 File.Copy(tmp, path, true); File.Delete(tmp); }代码逻辑说明写临时文件是在断电后最多丢一个临时文件原配置文件始终保持完整。参数说明Formatting.Indented让 JSON 带缩进方便现场工程师直接用文本编辑器改动。实际运行中保存前我会先做一次反序列化校验确认代码能正确读回这份 JSON 才执行替换这一步能挡住 90% 的“配置改坏了”现场问题。5. 仿easyvision落地避坑指南这五个问题我调试了最久5.1 现象窗口黑屏或闪烁拖动窗体时画面撕裂原因在采集线程里直接调用了HalconWindow.DispObj跨线程操作 HSmartWindowControl 内部句柄导致绘制冲突。这也是很多新手第一个晚上没法解决的怪问题。解决所有显示操作统一用Control.Invoke或BeginInvoke切回 UI 线程如 3.2 节代码所示。如果在循环里频繁 Invoke性能还不理想可以用生产者消费者队列让 UI 线程按自己的节奏绘图。5.2 现象采集线程跑起来 CPU 占用率接近满核原因用了Thread.Sleep(1)做轮询甚至干脆死循环。Windows 上 Sleep(1) 实际可能高精度定时到 1 毫秒大量线程空转照样吃满 CPU。解决改用阻塞取帧。相机 SDK 大多有阻塞式GetFrame(timeout)线程在等待帧期间是挂起状态不占 CPU。没有阻塞接口的话用AutoResetEvent由相机回调触发信号唤醒取帧逻辑。5.3 现象配置改了参数界面显示还是旧值原因配置对象被缓存了。最常见的写法错误是窗口加载时读了一次配置之后所有控件数据绑定都指向旧对象重新加载时又把新对象赋给了局部变量没替换掉界面绑定的那个引用。解决把配置对象设为窗体成员重新加载后触发一个ConfigChanged事件所有配置面板监听这个事件刷新自身。我一般还会在界面右下角显示“当前配置A型连接器 最后修改 14:32”让操作员一眼看出有没有切错型号。5.4 现象多相机同时打开报“设备地址冲突”或打开失败原因多台 GigE 相机用了相同的 IP 或同一网段没配好Halcon 打开 Grabber 的时候去连接 IP结果连到了错误设备。解决每台相机单独固定 IP并在代码里按唯一标识要么 IP要么相机序列号指定设备。如果走 Halcon 的HFramegrabber需要给每个 Grabber 分别设置device参数如果走厂商 SDK同样要在初始化时传入序列号不能依赖枚举顺序——USB 相机的枚举顺序每次插拔都可能变。5.5 现象测量结果忽大忽小同一张图跑两次结果都不一样原因图像预处理缺失。产线光照变化、工件表面反光、微小的透视旋转都会让边缘测量抖动。另一个常见原因是匹配定位时候的亚像素模式没开粗像素定位直接传给测量步骤误差自然大。解决测量步骤入口统一加预处理光照不均时先做一次illuminate或equ_histo_image再跑边缘提取定位步骤的 SubPixel 明确设成least_squares。定型后把光源亮度范围写在流程配置里每次切换产品时检查当前曝光时间是否在合法区间超范围直接报警不让“玄学”流到测量结果里。6. 框架能跑只是起点建议先做离线回放再加工具条框架做到这里已经能上产线跑但我见过太多项目死在“现场图像和开发时图像不一样”这一步。所以第一个建议是给框架加一个离线回放功能把产线上采集到的原图按时间戳存下来同时存当时的流程配置和结果。回放时用当前框架读取图片、跑配置里的流程把结果显示出来和当时结果对比。这个功能看似简单实际是验证两个东西一是配置迁移到新版本框架后结果是否一致二是排查现场偶发误检时能反复重放同一张图而不是蹲在产线上等下一次坏品出现。第二个值得投入的方向是把视觉工具做成真正的工具条像 easyvision 这类框架那样左侧列出“模板匹配、卡尺测量、Blob 分析、字符识别”拖到流程列表里就能用。做这一步的核心不是界面拖拽而是把每个视觉工具的参数面板标准化无论是什么工具都包含输入图像、参数区、输出结果三块。我在做这套工具条时最大的教训是不要把所有参数都平铺在一个面板里要把“常用参数”和“高级参数”分页操作员只看到曝光时间和最小分数工程师才看金字塔层数。第三个方向是加看门狗机制。相机掉线、PLC 断连、框架卡死都需要自动恢复或至少明确报警。我一般会单独开一个巡检线程每 2 秒检查一次相机连接计数器和最近一帧的时间戳如果超过 5 秒没有新帧且触发信号正常自动尝试重启采集并写错误日志。这个机制看着不上台面但产线上真正让框架“没人敢用”的往往就是死机后必须重启整个电脑而不是软件自己能拉起来。做第一版框架时我最想吃的后悔药就是把视觉流程硬编码在按钮事件里客户提换型需求时只能连夜改代码。后来把流程、参数、型号配置全部外置成 JSON才感觉这套软件终于能交出去了。通用视觉框架的投入产出比不在能跑的那一天而在第一次不用改代码就切换产品型号的那个下午。希望帮到你。本文还有配套的精品资源点击获取