尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

手写Halcon版VisionPro:插件化视觉框架的设计与落地

手写Halcon版VisionPro:插件化视觉框架的设计与落地 简介机器视觉工程中流程编排与工具复用是提升现场调试效率的关键。传统配置驱动方式难以应对产品换型时流程级变化而商用软件VisionPro凭借ToolBlock工具块交互模式成为行业标杆但其授权成本与封闭性也让许多团队转向Halcon。Halcon算子丰富、算法迭代快然而DLL集成模式以大量HOperatorSet调用为主检测流程一旦复杂便难以维护。本文基于C# WinForm与Halcon算子库讲解如何构建一个仿VisionPro的插件化视觉框架通过定义纯净接口契约、引脚化数据映射、运行时拓扑排序执行引擎将算法封装为可拖拽、连线的独立工具实现JOB配置化与现场可视化调试。内容涵盖插件动态加载、图像传递生命周期、UI线程安全等工程实践帮助开发者降低视觉项目的二次开发成本为工业检测场景提供灵活可扩展的解决方案。 做机器视觉上位机这行的人几乎都绕不开一个场景项目交付后现场换产品型号工程师把参数拍下来发给你你在办公室里改坐标、调阈值、重新编译再打包发过去。一次两次能忍时间长了谁都受不了。这也是为什么我一直想做一个“Halcon版VisionPro”——用WinForm做宿主程序把检测流程拆成带输入输出引脚的插件化工具让现场人员像操作VisionPro的ToolBlock一样拖工具、连线、配参数、跑JOB底层算法全部走Halcon。这个项目我断断续续做了一年多中间踩了不少坑今天把整个框架的设计思路和落地细节完整写出来希望能给同样在搞视觉框架、想做工具化封装、或者正在纠结“怎么把Halcon算法变成可复用工具”的朋友一些参考。1. 为什么我要做一个“Halcon版VisionPro”1.1 现场调试的痛点每一次换型都要改代码先说说这个项目的起因。我之前做过一个3C产品的外观检测项目检测流程不复杂定位、测量、判断OK/NG但是产品型号有十几种。每种型号的模板不一样、测量位置不一样、公差不一样。代码层面我一开始用的是“配置文件驱动”的方式把参数全部放到ini和xml里自认为做得够好了。可实际上呢现场换型的时候工程师需要打开配置文件找到“模板匹配”那一节的十几个参数挨个调。调完一个型号下一个型号又要调一遍。而且最难受的是有些检测流程不是简单的串行比如产品A需要先找定位再做轮廓测量产品B需要先做OCR再根据字符内容决定测量区域。这种流程级的变化配置文件根本表达不了最后还是得改代码、重新编译、重新部署。VisionPro之所以在工业现场那么受欢迎核心不是它的算法有多强而是它的ToolBlock交互模型把工具拖到流程区用连线把上一个工具的某个输出接到下一个工具的某个输入双击工具打开配置面板设参数跑完一个JOB每个节点都有状态颜色。这种用鼠标搭流程的方式几乎不写代码现场人员也能快速上手。1.2 VisionPro与Halcon的取舍VisionPro的优势很清晰但它的授权成本不低而且对于已经习惯了Halcon算子库的团队来说很多定制算法在VisionPro里实现起来很别扭。Halcon最大的优势是算子全、算法迭代快尤其是一些偏门算子比如特定场景下的3D处理、畸变校正、深度学习工具HDevelop里验证好之后一导出很快就能集成到自己的程序里。但Halcon有一个明显短版它的DLL集成模式太“裸”了。HDevelop导出的C#代码本质上是一大串HOperatorSet.*调用一旦检测流程复杂了代码里全是流程逻辑非常难维护。更关键的是流程一旦写死换产品型号或者调整检测步骤的时候就得动代码。所以当时我就在想能不能把VisionPro的交互框架搬过来把Halcon当成底层的算法引擎工具做成插件开发新算法的时候只写工具不动主程序流程通过拖拽和连线来组织存成JOB文件现场调试时改流程就像搭积木一样。1.3 这个项目到底做了什么这个项目叫“通用图像处理工具”定位是模仿VisionPro的交互方式做一个可扩展的视觉检测框架。具体功能包括工具面板左侧列出所有已加载的工具插件运行时把工具拖到流程画布。流程画布工具节点可拖动、可连线节点颜色实时反映执行状态。工具配置区选中工具后右侧加载对应的配置面板双击工具也可以打开。JOB运行控制支持单步、连续、暂停、停止执行顺序由数据依赖关系自动决定。结果输出每个工具的输出引脚能在“结果面板”里查看也可以连接到输出工具推送至UI或MES。这套框架做下来最大的好处是新项目来了先写几个工具插件然后在界面上搭流程不再需要改主程序。我后面会分模块讲清楚插件契约怎么定、值传递怎么做、JOB执行引擎怎么写、Halcon集成有哪些坑。你会看到这本质上不是一个“算法项目”而是一个“软件框架项目”算法只是被插件包装起来的零件。2. 插件契约整个框架的地基2.1 接口程序集要足够“干净”插件化设计一开始就要想清楚一点主程序和工具插件之间靠什么“对话”。最简单的做法是定义一组接口工具插件实现接口主程序反射加载并调用。但接口程序集里放什么类型是个很关键的决定。我当时踩过一个坑最早把接口和Halcon类型绑在一起直接在接口方法里传HObject。结果某个插件用了新版本Halcon的halcondotnet.dll主程序用的是旧版本一加载就报类型不匹配或者Could not load file。后来我把接口程序集中的Halcon依赖全部去掉只用基础类型加自定义DTO问题才彻底解决。接口程序集我建议保持最小依赖只包含工具接口IJobFlowTool引脚定义ToolPin、PinDirection运行上下文JobContext宿主回调IToolHost少量公共结果类型不引用WinForm、不引用Halcon、不引用任何第三方库。这样设计有个附带好处将来如果想把主程序换成WPF版本或者做无界面的命令行执行器这套接口可以原封不动地复用。2.2 工具接口与引脚声明的设计我定义的IJobFlowTool接口大致长这样public interface IJobFlowTool { string ToolName { get; } Guid ToolGuid { get; } bool Enabled { get; set; } IListToolPin InputPins { get; } IListToolPin OutputPins { get; } void LoadConfig(string json); string SaveConfig(); bool Execute(JobContext context); void Dispose(); }引脚的定义是这套框架的神经末梢public class ToolPin { public string PinName { get; set; } public string Description { get; set; } public Type ValueType { get; set; } public PinDirection Direction { get; set; } } public enum PinDirection { Input, Output }为什么引脚的数据类型用Type而不是直接用强类型因为当主程序加载完插件后它根本不知道每个工具“细节上”输入输出是什么类型只有在用户连线的那个瞬间它才能通过反射检查两个引脚是否兼容。比如上游“模板匹配工具”输出CenterX是double下游“测量工具”输入InitX也是double那就匹配如果上游输出HObject、下游输入double那就得提示“类型不兼容”。接口里的LoadConfig和SaveConfig是处理参数持久化的。每个工具把自己的参数序列化成JSON字符串主程序统一存到JOB文件里。这样JOB文件就是一个自包含的流程描述换一台电脑拷过去照样能跑。2.3 算法工具与UI工具为什么会分离VisionPro里每个工具都有两套东西一套是做检测的算法逻辑一套是配置界面。我一开始想省事直接把算法类和配置界面放在同一个类里后来发现出了大问题工具可能在没有界面的环境下运行比如工控机上只跑JOB不调试算法类加载了完全用不到的WinForm控件加载失败直接导致工具不可用。所以我把工具拆成两层算法层实现IJobFlowTool纯C#逻辑不依赖UI负责真正执行图像处理。界面层实现IConfigurableToolUI返回一个UserControl负责参数配置和结果显示可选实现。public interface IConfigurableToolUI { UserControl CreateControl(); void LoadTool(IJobFlowTool tool); }主程序加载工具时先创建算法实例然后用反射去找同一个插件程序集里有没有对应的UI类。我约定了一个命名规则工具类叫PMAlignTool界面类就叫PMAlignToolUI。如果找不到UI类也没关系算法照常运行只是不能配置参数而已。这个约定让工具作者有选择余地想快速开发一个控制台型工具可以不做UI想做得精致就做一个UI。3. 工具间值传递把“连线”搬进WinForm3.1 VisionPro连线机制的本质VisionPro的连线表面上是一根从A工具拖到B工具的线本质上是一个数据映射把A工具的某个输出项OutputItem映射到B工具的某个输入项InputItem。比如你要把“模板匹配”输出的中心点坐标传给“卡尺测量”作为ROI的初值那你就在“卡尺测量”的输入引脚列表里选择“连接自PMAlignTool.CenterX”。这套机制的精髓我认为是“运行时拉取”而不是“上游推送”。也就是说B工具在执行前会主动去解析自己每一个输入引脚的数据来源从上游工具的Outputs缓存里取数再通过反射或字典赋值给自己。这样做的好处是工具的依赖关系只体现在“我运行前要拿到数据”不要求上游工具提前把数据“塞”给某个全局变量流程的扩展性会好很多。3.2 用引脚声明和运行时拉取实现参数串联我在框架里用一张JobConnection表来存储所有连线关系public class JobConnection { public string SourceToolId { get; set; } public string SourcePinName { get; set; } public string TargetToolId { get; set; } public string TargetPinName { get; set; } }当JOB运行时执行引擎在对某个工具执行前会调用一个绑定方法private void BindInputs(JobContext context, IJobFlowTool tool) { foreach (var pin in tool.InputPins) { var conn FindConnection(tool.ToolGuid.ToString(), pin.PinName); if (conn null) continue; var sourceTool GetToolById(conn.SourceToolId); var sourceValue context.GetOutput(sourceTool.ToolGuid, conn.SourcePinName); // 类型兼容检查 if (sourceValue ! null pin.ValueType.IsInstanceOfType(sourceValue)) { context.SetInput(tool.ToolGuid, pin.PinName, sourceValue); } else { throw new InvalidOperationException( $连线类型不匹配{conn.SourceToolId}.{conn.SourcePinName} - {tool.ToolName}.{pin.PinName}); } } }工具执行时通过JobContext来取输入、写输出public bool Execute(JobContext context) { var image context.GetInputHObject(ToolGuid, Image); var initX context.GetInputdouble(ToolGuid, InitX); var initY context.GetInputdouble(ToolGuid, InitY); // 执行Halcon算子 // ... context.SetOutput(ToolGuid, DistancePx, distancePx); context.SetOutput(ToolGuid, DistanceMm, distanceMm); return true; }这种值传递方式不光是传图像坐标、角度、字符串、区域、轮廓都能传。只要引脚声明正确、类型匹配数据就可以在任意工具之间流动。3.3 类型兼容与图像共享的安全边界连线视图里用户能看到的只是一根线但框架内部必须处理“类型兼容”和“数据安全”两个问题。类型兼容相对好处理画布上从一个输出引脚拖动到另一个工具的输入引脚时先判断两边ValueType是否一致或者是否可以隐式转换。不一致就给出红色提示且不允许连接。数据安全则要谨慎很多。Halcon的HObject在C#里是个“轻量句柄”它不像普通的托管对象那样有强类型约束。如果两个工具共享同一个HObject变量下游把图像裁切、转换或改了Region上游工具下次执行时可能就拿到一个被污染的对象。所以我做了一个约定所有工具输入的图像默认只读需要修改图像时必须通过GenCopyImage或者CropDomain生成新对象绝对不允许直接修改输入图像的内容。同时框架在连线层提供一个“值拷贝”选项用户可以选择数据传递是“引用传递”还是“拷贝传递”。图像默认引用传递遇到特殊需求比如下游会改图就改成拷贝传递。这一章的结论是工具间值传递不复杂但要保持清晰的“谁创建、谁释放、谁只读、谁可改”边界否则项目跑到一半内存爆了或者结果诡异你会非常痛苦。4. JOB执行引擎拓扑排序与运行状态机4.1 什么是JOB在VisionPro里JOB是一个完整的工作流一组工具、工具之间的连线、全局输入输出保存为一个.vpp文件。我在框架里把JOB定义为一个JSON文件包含三部分工具列表每个工具的实例ID、插件类型、显示名称、参数配置。连线表所有工具之间的数据依赖。全局输入输出JOB对外的接口比如“输入图像”“输出OK/NG结果”。一个典型JOB文件长这样{ jobName: 定位测量JOB, tools: [ { id: img-001, type: ImageSourceTool, name: 图像源, config: {\path\:\D:/images/1.png\} }, { id: pm-001, type: PMAlignTool, name: 模板匹配, config: {\modelFile\:\model.shm\} }, { id: cl-001, type: CaliperTool, name: 卡尺测量, config: {\roi\:[100,200,50]} }, { id: jd-001, type: JudgeTool, name: 判断, config: {\tolerance\:0.02} } ], connections: [ { sourceTool: img-001, sourcePin: Image, targetTool: pm-001, targetPin: Image }, { sourceTool: pm-001, sourcePin: CenterX, targetTool: cl-001, targetPin: InitX }, { sourceTool: pm-001, sourcePin: CenterY, targetTool: cl-001, targetPin: InitY }, { sourceTool: cl-001, sourcePin: DistanceMm, targetTool: jd-001, targetPin: Distance } ] }4.2 从连线表到执行顺序JOB里面的工具不是一个一个直接排好的因为用户可以拖出任意连接关系甚至可能出现循环依赖。如果工具A依赖B、B依赖CC又依赖A那肯定跑不了。所以执行引擎的第一步是拓扑排序。我用的是最经典的Kahn算法public static ListIJobFlowTool TopologicalSort( ListIJobFlowTool tools, ListJobConnection connections) { var inDegree tools.ToDictionary(t t.ToolGuid, _ 0); var adjacency tools.ToDictionary(t t.ToolGuid, _ new ListGuid()); foreach (var conn in connections) { var source tools.FirstOrDefault(t t.ToolGuid.ToString() conn.SourceToolId); var target tools.FirstOrDefault(t t.ToolGuid.ToString() conn.TargetToolId); if (source null || target null) continue; inDegree[target.ToolGuid]; adjacency[source.ToolGuid].Add(target.ToolGuid); } var queue new QueueIJobFlowTool(tools.Where(t inDegree[t.ToolGuid] 0)); var result new ListIJobFlowTool(); while (queue.Count 0) { var current queue.Dequeue(); result.Add(current); foreach (var nextGuid in adjacency[current.ToolGuid]) { inDegree[nextGuid]--; if (inDegree[nextGuid] 0) { queue.Enqueue(tools.First(t t.ToolGuid nextGuid)); } } } if (result.Count ! tools.Count) { throw new InvalidOperationException(JOB存在循环依赖无法执行); } return result; }注意有些工具没有任何输入引脚比如图像源工具它们天然就是拓扑排序的起点。如果工具没有连线关系它也会被排进去只是执行顺序相对自由。4.3 单步、断点与异常兜底JOB执行引擎对外提供Start、Stop、Pause、StepOnce四个操作。StepOnce是调试模式下的利器每执行完一个工具就停下来让你在结果面板里查看所有工具的Outputs值确认没问题再继续。每个工具在执行前引擎设置状态为Running执行后根据返回值设置Success或Failed如果工具抛出了未捕获异常引擎捕获后标记Failed停止整个JOB并把异常堆栈写入日志。工具内部最好自己try/catch返回false来表示业务异常比如模板匹配不到目标这样JOB可以继续运行其他分支工具。状态机的实现是典型的枚举加事件通知public enum ToolRunState { NotRun, Running, Success, Failed, Disabled }每个工具执行结束引擎触发一个ToolExecuted事件UI层收到后更新画布上节点的颜色。运行中还能实时看到流程在哪里卡住了这在现场调试时非常重要。5. 插件动态加载Assembly.LoadFrom之外的几道坎5.1 加载一个工具DLL并不难难在卸载和多版本插件加载的核心代码其实就三行var asm Assembly.LoadFrom(dllPath); var types asm.GetTypes() .Where(t typeof(IJobFlowTool).IsAssignableFrom(t) !t.IsAbstract);但实际项目里这只解决了“加载”这一个动作。加载完之后你会碰到一堆问题重复加载同一路径的DLL第二次Assembly.LoadFrom会复用第一次加载的程序集如果你希望加载一份新编译的DLL开发时经常改只能重启程序或者用独立的AssemblyLoadContext。依赖冲突插件A引用了Newtonsoft.Json 9插件B引用了Newtonsoft.Json 12如果都复制到主目录运行时后加载的会覆盖先加载的导致插件B调用某些API直接抛错。现在.NET Core/.NET 5可以用AssemblyLoadContext做隔离但.NET Framework下的AppDomain隔离非常麻烦我最后用的是“约定优先”所有插件只能依赖主程序提供的公共DLL集合第三方库全部由主程序统一管理不许插件自带。卸载如果要做热更新必须在.NET Core下用Collectible AssemblyLoadContext同时要求插件代码不泄漏内部不要开线程、不要挂静态事件。坦白讲工业视觉软件对热更新的需求不强我的建议是主程序启动时加载插件运行中不支持卸载产品更新时整包替换。5.2 WinForm画布上的工具节点仿照VisionPro的画布交互在WinForm里需要自己画节点和连线。节点本身我定义成继承自Control的自绘控件绘制圆角矩形、图标、名称、状态色块。拖动节点时更新控件位置连线时用贝塞尔曲线从输出引脚画到输入引脚。要让画布在几十个节点下依然流畅关键点有两个双缓冲绘图整个画布区域先画到一张内存Bitmap上再一次性贴回屏幕避免每个节点单独Invalidate导致闪烁。局部刷新节点状态变化时只Invalidate该节点的矩形区域不要整屏重画。命中和连线检测也值得说一下节点较少时直接判断鼠标是否落在节点矩形区域内节点很多时可以做一个简单的四叉树或者网格索引但一般项目不需要。连线命中更特殊因为贝塞尔曲线不是矩形建议把曲线转成一系列小线段再对线段做距离判断否则用户很难选中已有连线去删除。5.3 插件更新与版本管理插件多了之后版本管理必须跟上。我在每个插件程序集里加了一个自定义Attribute记录插件版本、作者、支持的最小主程序版本[AttributeUsage(AttributeTargets.Class)] public class ToolPluginAttribute : Attribute { public string Name { get; set; } public string Version { get; set; } public string Author { get; set; } }主程序加载插件时把这个Attribute读出来显示在工具列表的“关于”里。如果插件依赖的接口版本比主程序新启动时直接提示“插件版本过高请升级主程序”避免运行到一半出现MethodMissingException。另外插件DLL的构建输出目录要规范主程序目录/plugins/工具类目/xxx.dll。工具类目可以是“定位”“测量”“识别”“图像处理”等主程序启动时递归扫描plugins目录。这样管理插件会很有条理也方便发布时按类目打zip包。6. Halcon集成HObject的生命周期与显示线程6.1 HObject/HTuple不是纯托管对象这是很多人用Halcon写C#最容易踩的坑。Halcon的C#接口里HObject、HTuple看起来是C#类但内部包含非托管句柄它们实现了IDisposable但Dispose并不会像using语句那样自动执行。如果你写工具时new了一堆HObject用完之后不释放时间长了内存占用会一直涨直到程序崩溃。我统计过一个定位工具跑一次平均会创建大概10~20个HObject临时对象。如果现场产线一分钟跑5次一天就是7万个对象。不释放的话内存增长是不可逆的。所以我在工具开发规范里强制要求函数内部创建的临时HObject一律用using或try/finally包裹using (HObject imageReduced image.ReduceDomain(region)) using (HObject edges imageReduced.EdgesSubPix(canny, 1, 20, 40)) { // 处理edges }如果工具的Execute方法输出HObject给下游那释放责任就属于“创建者”即输出这个对象的工具自己或者由JobContext统一管理。简单来说谁创建谁释放谁输出谁负责生命周期。6.2 跨工具传图像时要注意什么图像在工具之间传递千万不要觉得“我传了一个HObject变量就是传了引用”。Halcon的HObject在C#里是一个句柄结构复制它只是多了一个引用计数底层图像数据会被共享。大多数算子比如CropDomain、Threshold、ReduceDomain都会创建一个新的HObject而不是修改原图所以传递过程中很少出现“下游把上游的图像改了”的情况。但靠“大多数情况”做框架是不行的必须做明确约定输入图像对所有工具是只读的。需要修改图像才能做处理的时候工具内部必须自己创建新对象。如果某个工具要输出“修改后的图像”给下游它应该在输出前做一次深拷贝确保上游图像不受影响。界面显示的时候显示控件应该拿到图像的副本不要在窗口DispObj的同时算法线程又把这个HObject释放了。否则会有“HALCON error #4241: Object is invalid”之类的错误。6.3 HWindow显示与UI线程WinForm下显示Halcon图像大家通常直接用Halcon提供的HWindowControl。这个东西在工具箱里拖进来很方便但它有几个“坑”HWindowControl依赖窗口句柄创建后不能随便跨线程调用。在非UI线程里直接调用HalconWindow.DispObj轻则图像闪烁、刷新异常重则直接抛错。如果工具运行在工作线程而工作线程里要显示图像最稳妥的做法是先在工作线程里把要显示的HObject复制一份再通过Control.BeginInvoke切回UI线程显示。我在框架里提供了一套统一的“显示服务”工具想往界面推送任何图像或结果都调用IToolHost的ShowImage、ShowMessage方法由主程序内部处理线程切换。这样工具作者就完全不用关心自己在哪个线程上。Halcon的运行时还有一个不受人控制的问题许可证。开发时用完整的开发版License到了客户现场要么装Runtime要么用加密狗。而且Halcon版本一旦升级有些算子的默认参数行为会变所以建议项目定了就不轻易升级Halcon版本否则你可能要重新验证所有检测流程。7. 一个从零跑通的DEMO定位测量判OK/NG7.1 定义四个插件工具理论讲再多不如一个完整DEMO直观。假设我们要做一个最常见的检测流程先模板匹配定位再卡尺测量间距最后判断是否在公差内。我定义了四个工具工具名类名输入引脚输出引脚作用图像源ImageSourceTool无Image (HObject)读取本地图像模板匹配PMAlignToolImage (HObject)CenterX, CenterY, Angle, Score使用形状匹配定位卡尺测量CaliperToolImage, InitX, InitYDistancePx, DistanceMm沿指定方向找两条边缘并测距OK判断JudgeToolDistanceMmResultOK, Message比较公差输出OK/NGPMAlignTool的引脚声明前面已经写过这里补充CaliperTool的关键点public class CaliperTool : IJobFlowTool { public string ToolName 卡尺测量; public Guid ToolGuid { get; set; } Guid.NewGuid(); public bool Enabled { get; set; } true; public IListToolPin InputPins { get; } new ListToolPin { new ToolPin { PinName Image, ValueType typeof(HObject), Direction PinDirection.Input }, new ToolPin { PinName InitX, ValueType typeof(double), Direction PinDirection.Input }, new ToolPin { PinName InitY, ValueType typeof(double), Direction PinDirection.Input } }; public IListToolPin OutputPins { get; } new ListToolPin { new ToolPin { PinName DistancePx, ValueType typeof(double), Direction PinDirection.Output }, new ToolPin { PinName DistanceMm, ValueType typeof(double), Direction PinDirection.Output } }; public bool Execute(JobContext context) { var image context.GetInputHObject(ToolGuid, Image); var initX context.GetInputdouble(ToolGuid, InitX); var initY context.GetInputdouble(ToolGuid, InitY); // 构造卡尺ROI找边缘像素计算间距 // ... 这里就是你的Halcon算法代码 context.SetOutput(ToolGuid, DistancePx, distancePx); context.SetOutput(ToolGuid, DistanceMm, distancePx / scale); return true; } }JudgingTool的逻辑更简单拿下拉公差和上公差做一次判断输出bool和字符串。这四个工具放在四个独立插件DLL里。注意它们都引用同一个“接口程序集”但互相不引用彼此独立。7.2 连线表和JOB运行在界面上我从左侧工具列表拖出四个工具到画布然后连线ImageSourceTool.Image - PMAlignTool.ImagePMAlignTool.CenterX - CaliperTool.InitXPMAlignTool.CenterY - CaliperTool.InitYCaliperTool.DistanceMm - JudgeTool.Distance点击“运行”按钮后JOB引擎先做拓扑排序。四个工具的输入依赖很明确排序结果是ImageSourceTool - PMAlignTool - CaliperTool - JudgeTool。引擎会依次执行图像源工具从磁盘读出图像写入Outputs[Image]。模板匹配工具从Inputs拿到Image调用Halcon做形状匹配写入Outputs[CenterX]、[CenterY]、[Angle]、[Score]。卡尺测量工具从Inputs拿到Image、InitX、InitY在指定位置创建测量ROI找两条边缘计算像素间距再通过标定换算成毫米写入Outputs[DistanceMm]。OK判断工具拿到DistanceMm与公差比较输出ResultOK和Message。每个工具运行完画布上节点颜色从灰色变为绿色如果模板匹配分数太低PMAlignTool内部判断后返回false节点变为红色JOB停止结果面板里能看到“匹配失败”的日志。7.3 运行效果与调试方式这个DEMO跑起来之后最直观的效果是我想调整某个型号的匹配模板只需要双击PMAlignTool节点重新加载新的模板文件想改公差双击JudgeTool节点修改上下限想换测量位置调整CaliperTool里的ROI参数。这种体验和改代码完全不一样。调试时还可以开“单步”模式每跑一个工具停一下在“变量查看器”里看每个工具当前有哪些输出值。有一次现场说匹配位置总是偏我单步跑到模板匹配发现Score只有0.3再查看图像原来产品表面多了一个高光区域干扰了模板匹配这种情况如果不开单步根本不知道是哪个环节出了问题。8. 实际用下来这些坑最值得说8.1 工作线程与UI刷新最早写这个框架的时候我大意了直接在工具Execute方法里访问UI控件结果JOB跑在工作线程上每次执行到那个工具界面就卡顿甚至白屏。后来我把所有跨UI操作封装成一个方法public static void PostToUi(Action action) { if (SynchronizationContext.Current _uiSyncContext) { action(); } else { _uiSyncContext.Post(_ action(), null); } }在主程序启动时记录SynchronizationContext.Current所有工具要更新UI必须走这个封装。别小看这个设计工具数量一多线程问题就是最大的故障来源。8.2 画布性能从卡顿到流畅工具节点少的时候画布重绘无所谓。但我测试过当JOB里拖了50个工具节点、100条连线之后拖动一个节点能感到明显的掉帧。原因很简单每个节点的OnPaint都重新绘制圆角矩形、阴影、文字重绘区域又放得很大。我的优化方案是分层缓存节点层每个节点自绘到一个小的Bitmap上拖动时直接BitBlt贴图。连线层所有连线提前画到一张跟画布等大的Bitmap上节点状态变化时才更新连线层。交互层鼠标拖拽的动态效果比如正在拖的线单独画在最上层。优化之后100个节点的JOB拖动依然流畅。如果你也想做类似的画布工具建议一开始就把重绘策略设计好不要等卡了再改。8.3 Halcon版本、许可与对方机器Halcon的许可分开发版和运行时版。开发版可以是试用License但到了客户现场要么买Runtime要么带加密狗。这套框架本身不改变这些条件但你要注意开发机和工控机的Halcon版本尽量保持一致否则在你的电脑上调好的模板文件.shm等在工控机上可能加载失败。分发的时候除了halcondotnet.dll还要带上halcon.dll、hcanvas.dll、hdevengine.dll等原生运行库。漏掉其中一个运行时就会报“Failed to load HALCON library”。Halcon的深度学习模块比传统的shape model更依赖运行时组件如果工具里用了OCR、分类这些深度学习方法务必在目标机器上先跑一遍注册测试。8.4 给想自研视觉框架的人三个建议第一想清楚边界。这个框架解决的是“流程编排”问题不是“算法万能”问题。工具插件里的算法才真正决定检测效果框架能帮你做的是参数可保存、流程可复制、现场可调试。第二不要一上来就规划一个庞大的插件生态。先做两个最常用的工具图像源、模板匹配跑通端到端的流程再慢慢把测量、OCR、读码这些工具补进去。插件数量一开始就铺太多接口设计没经过实际验证后面重构成本很高。第三插件SDK必须配备文档和示例。我自己写工具很顺手但团队里其他同事接入时如果只有接口没有示例根本用不起来。我给插件开发者写了一份简短的README附上一个最简工具的完整代码新人照着改半小时就能出一个新工具。这套框架做到今天最让我满意的一点是新项目基本不需要动主程序工作重心变成了“根据需求开发新工具插件”和“在现场搭建JOB流程”。如果你也想做类似的东西不用一步到位先从一个最小可运行的版本开始把VisionPro里最核心的“拖工具、连线、跑JOB”三个交互做出来算法插件顺手加一两个你就知道这个东西能给你省多少事了。本文还有配套的精品资源点击获取
返回列表