框架:用Halcon封裝可拖拽工具流)
簡(jiǎn)介面向機(jī)器視覺(jué)開(kāi)發(fā)者的通用圖像處理工具源碼基于Halcon和C# Winform實(shí)現(xiàn)交互方式仿照VisionPro的拖拽設(shè)計(jì)用戶通過(guò)拖拽不同功能模塊即可構(gòu)建流程模塊間自動(dòng)完成參數(shù)與結(jié)果傳遞使JOB運(yùn)行走向直觀可控。工具采用插件體系每個(gè)圖像處理功能獨(dú)立封裝支持動(dòng)態(tài)加載易于擴(kuò)展與二次開(kāi)發(fā)適用于檢測(cè)、測(cè)量、識(shí)別等視覺(jué)場(chǎng)景。資源包共715個(gè)文件約21.94MB主要包含C#源代碼、資源文件、類(lèi)庫(kù)與界面圖標(biāo)其中cs文件398個(gè)、resx文件114個(gè)、dll文件34個(gè)、png圖片70個(gè)并附有可執(zhí)行程序與工程配置方便編譯與學(xué)習(xí)。目前已有237人學(xué)習(xí)下載。通過(guò)該源碼讀者可以掌握插件式圖像處理框架的設(shè)計(jì)方法了解Halcon與Winform的集成技巧、工具間數(shù)據(jù)流的銜接方式以及JOB流程管理思路也可基于現(xiàn)有模塊快速擴(kuò)展新功能縮短項(xiàng)目開(kāi)發(fā)周期。1. 一套仿VisionPro的WinForm通用圖像工具把Halcon引擎裝進(jìn)可拖拉的流程里做機(jī)器視覺(jué)上位機(jī)的人大概率都摸過(guò)VisionPro它那種把圖像源、定位、測(cè)量、通信拖到流程框里、用連線把工具的輸出喂給下一個(gè)工具的設(shè)計(jì)確實(shí)比在代碼里一個(gè)個(gè)調(diào)Halcon算子要直觀得多。這套資源做的就是同一件事基于C# WinForm把Halcon的算子封裝成一個(gè)個(gè)獨(dú)立工具工具之間仿照VisionPro的ToolBlock做值傳遞整套流程按JOB方式組織運(yùn)行。說(shuō)白了就是用Halcon代替VisionPro的底層算法把它的交互體驗(yàn)搬到自家上位機(jī)上。適合正在做視覺(jué)檢測(cè)項(xiàng)目、被Halcon腳本維護(hù)逼瘋、想在WinForm里搭一套可復(fù)用流程框架的C#工程師。底層引擎還是Halcon能干多少活全看你對(duì)算子的熟悉程度這一點(diǎn)先擺清楚。2. 總體架構(gòu)宿主、插件契約、值傳遞與JOB運(yùn)行器怎么拆拿到這套工程壓縮包第一步別急著編譯先把它的分層邏輯看明白。這類(lèi)工具做得好不好不取決于單個(gè)算子用得多花哨而是看宿主程序能不能讓工具之間干凈地協(xié)作。下面把這套資源里最常見(jiàn)的一版架構(gòu)拆開(kāi)講這也是我認(rèn)為它最值得參考的部分。2.1 先分三層宿主界面層、插件契約層、運(yùn)行管理層宿主界面層就是WinForm主窗體負(fù)責(zé)擺放工具列表、畫(huà)布區(qū)域、屬性面板和JOB運(yùn)行按鈕。它不直接引用任何具體工具的代碼只依賴一個(gè)定義好的接口。插件契約層是關(guān)鍵所有工具采集、預(yù)處理、找圓、模板匹配、測(cè)量、OCR之類(lèi)都實(shí)現(xiàn)同一個(gè)接口這樣宿主程序才能在不修改主程序的前提下把新工具dll丟進(jìn)插件目錄就完成加載。運(yùn)行管理層是JOB運(yùn)行器它持有當(dāng)前流程的工具鏈和連線關(guān)系按順序把前一個(gè)工具的輸出值寫(xiě)入后一個(gè)工具的輸入。這套資源里應(yīng)該能看到類(lèi)似的目錄結(jié)構(gòu)主程序工程、工具接口工程、若干獨(dú)立工具工程。工具工程之間互相不認(rèn)識(shí)只認(rèn)得契約工程里定義的接口。這種拆法和VisionPro的ToolBlock里工具是插件的思路是一致的好處是以后加新算法不需要?jiǎng)又鞒绦驂奶幨墙涌诙x得夠不夠穩(wěn)直接決定了后期的開(kāi)發(fā)體驗(yàn)。我在自己項(xiàng)目里一般把這個(gè)契約工程單獨(dú)編譯成一個(gè)dll版本號(hào)管得很?chē)?yán)因?yàn)樗泄ぞ叨家蕾囁坏┙涌诤灻儎?dòng)整個(gè)插件目錄要重新編譯一遍。2.2 值傳遞的核心先定數(shù)據(jù)結(jié)構(gòu)再定接口VisionPro里每個(gè)工具都有輸入和輸出工具A的輸出連到工具B的輸入靠的是一個(gè)通用數(shù)據(jù)類(lèi)型。Halcon世界里這個(gè)通用類(lèi)型天然就是HTuple它既能裝整數(shù)、浮點(diǎn)、字符串也能裝圖像句柄之外的大多數(shù)標(biāo)量數(shù)據(jù)。工具接口里會(huì)定義輸入集合和輸出集合集合里每一項(xiàng)有名字、類(lèi)型、當(dāng)前值運(yùn)行時(shí)按名字從上游工具的輸出集合里取值塞進(jìn)下游工具的輸入集合。這套資源里有一個(gè)細(xì)節(jié)值得關(guān)注就是工具間的值傳遞不是靠強(qiáng)類(lèi)型對(duì)象而是用類(lèi)似鍵值對(duì)的方式。我拆過(guò)不少類(lèi)似工程這里用鍵值對(duì)是務(wù)實(shí)的選擇因?yàn)楣ぞ咛?、?lèi)型各異強(qiáng)類(lèi)型依賴會(huì)把插件體系搞死。HTuple本身是弱類(lèi)型的傳圖像之外的參數(shù)特別方便但如果傳的是HObject圖像就得另開(kāi)一條通道比如在值傳遞包里包一層對(duì)象引用。2.3 JOB運(yùn)行器的走向按連線順序驅(qū)動(dòng)工具鏈JOB在這類(lèi)框架里就是一條完整的檢測(cè)流程。運(yùn)行器拿到當(dāng)前JOB的工具鏈表和連線表從起始工具開(kāi)始逐個(gè)執(zhí)行執(zhí)行完一個(gè)就查它的輸出連接到哪些工具把值推過(guò)去再執(zhí)行下游。這種執(zhí)行模型對(duì)應(yīng)VisionPro里的Job運(yùn)行邏輯也對(duì)應(yīng)Halcon里HDevelop腳本里順序調(diào)用算子的過(guò)程只不過(guò)把腳本文本變成了圖形化連線和參數(shù)面板。運(yùn)行時(shí)要做三件事解析連線表、組裝配料、按順序執(zhí)行。連線表在內(nèi)存里通常就是一個(gè)列表每個(gè)條目記錄上游工具ID、上游輸出名、下游工具ID、下游輸入名。這部分看清了后面寫(xiě)JOB運(yùn)行器就不會(huì)跑偏。3. 核心實(shí)現(xiàn)從ITool接口到插件動(dòng)態(tài)加載再到流程運(yùn)行的落地代碼3.1 先定ITool契約輸入、輸出、執(zhí)行三件事工具接口是整套體系的基石。按VisionPro的思路一個(gè)工具必須能回答三個(gè)問(wèn)題需要什么輸入、產(chǎn)出什么輸出、執(zhí)行時(shí)干些什么。工程里對(duì)應(yīng)的接口大概長(zhǎng)這樣public interface ITool { string Name { get; set; } string ToolType { get; } ListParamItem Inputs { get; } ListParamItem Outputs { get; } bool Execute(JobContext context, out string errorMsg); object GetConfig(); void LoadConfig(string json); ToolStatus Status { get; set; } } public class ParamItem { public string Name { get; set; } public Type DataType { get; set; } public object Value { get; set; } }Execute里的JobContext是運(yùn)行時(shí)上下文它至少應(yīng)該攜帶當(dāng)前幀的HObject圖像、當(dāng)前工具鏈的執(zhí)行日志、以及一個(gè)全局對(duì)象池用來(lái)臨時(shí)存放圖像句柄這些不適合塞進(jìn)HTuple的東西。我在自己的工程里還會(huì)往JobContext里塞一個(gè)CancellationToken配合Halcon算子的SetCheck機(jī)制流程冗長(zhǎng)時(shí)能及時(shí)中斷執(zhí)行。參數(shù)說(shuō)明Inputs和Outputs是運(yùn)行時(shí)值傳遞的懸掛點(diǎn)界面上的拖拉連線本質(zhì)上就是在編輯這兩個(gè)列表中的條目引用。GetConfig和LoadConfig負(fù)責(zé)把工具的參數(shù)序列化成JSON這是后面做JOB保存和回放的基礎(chǔ)建議你在自己的接口設(shè)計(jì)里也預(yù)留這兩個(gè)方法否則后面做項(xiàng)目保存功能時(shí)屁股要擦很久。3.2 插件掃描與動(dòng)態(tài)加載Assembly.LoadFrom的正確姿勢(shì)工具都實(shí)現(xiàn)ITool宿主程序怎么發(fā)現(xiàn)它們常見(jiàn)做法是啟動(dòng)時(shí)掃描指定插件目錄用Assembly.LoadFrom加載所有dll再通過(guò)反射找出ITool的實(shí)現(xiàn)類(lèi)并實(shí)例化。下面是這套資源里插件掃描的典型實(shí)現(xiàn)public ListITool LoadToolsFromDirectory(string pluginDir) { var tools new ListITool(); var files Directory.GetFiles(pluginDir, *.dll); foreach (var file in files) { var assembly Assembly.LoadFrom(file); var types assembly.GetTypes() .Where(t typeof(ITool).IsAssignableFrom(t) !t.IsAbstract); foreach (var type in types) { var instance Activator.CreateInstance(type) as ITool; if (instance ! null) { tools.Add(instance); // 工具自帶描述特性用來(lái)在UI列表里顯示友好名稱 var attr type.GetCustomAttributeToolDescriptionAttribute(); instance.ToolType attr?.DisplayName ?? type.Name; } } } return tools; }拿到實(shí)例之后界面上的工具箱就把它們列出來(lái)用戶拖一個(gè)到畫(huà)布就復(fù)制出一個(gè)工具實(shí)例加入當(dāng)前JOB。這里有兩個(gè)參數(shù)值得注意插件目錄我建議放成獨(dú)立文件夾不要和exe挨在一起因?yàn)檫\(yùn)行時(shí)更新dll時(shí)文件占用會(huì)帶來(lái)麻煩LoadFrom之后的dll會(huì)被鎖定這個(gè)問(wèn)題在避坑清單里會(huì)展開(kāi)講。邏輯說(shuō)明GetTypes()會(huì)觸發(fā)該程序集所有類(lèi)型的加載如果某個(gè)工具dll里混進(jìn)了依賴Halcon運(yùn)行時(shí)但其實(shí)沒(méi)被正確引用的類(lèi)型這里會(huì)拋ReflectionTypeLoadException。項(xiàng)目初期最容易翻車(chē)的點(diǎn)在這常見(jiàn)做法是捕獲異常后把未加載的類(lèi)型列表打印出來(lái)而不是放掉整個(gè)掃描過(guò)程。3.3 JOB運(yùn)行器按連線順序驅(qū)動(dòng)工具鏈JOB運(yùn)行器是整個(gè)框架的發(fā)動(dòng)機(jī)。VisionPro里按連線走向執(zhí)行工具這里用一張鄰接表跑BFS式順序執(zhí)行但視覺(jué)工具的JOB通常不是并發(fā)而是嚴(yán)格的先后依賴所以我一般實(shí)現(xiàn)成帶循環(huán)檢測(cè)的拓?fù)渑判驁?zhí)行。核心代碼框架public void RunJob(Job job, HObject frame, JobContext ctx) { // 先做拓?fù)渑判虼_認(rèn)沒(méi)有環(huán)形連線 var order TopoSort(job.Tools, job.Connections); ctx.Image frame; ctx.Log.Clear(); foreach (var toolId in order) { var tool job.ToolMap[toolId]; // 把上游傳下來(lái)的值灌進(jìn)工具輸入 foreach (var input in tool.Inputs) { if (ctx.RemoteValues.ContainsKey(input.Name)) { input.Value ctx.RemoteValues[input.Name]; } } // 執(zhí)行工具 string err; tool.Execute(ctx, out err); ctx.Log.Add(${tool.Name}: {err}); // 把輸出寫(xiě)入上下文供下游工具拾取 foreach (var output in tool.Outputs) { if (ctx.RemoteValues.ContainsKey(output.Name)) ctx.RemoteValues[output.Name] output.Value; } } }參數(shù)說(shuō)明ctx.RemoteValues是全局值表工具A輸出了一個(gè)叫CenterX的HTuple寫(xiě)入這張表工具B的輸入里也聲明了CenterX運(yùn)行器就用這個(gè)名字去匹配。這里要特別注意命名沖突兩個(gè)工具輸出同名值時(shí)后者會(huì)靜默覆蓋前者踩過(guò)坑的都知道這問(wèn)題在聯(lián)調(diào)時(shí)有多難查。邏輯說(shuō)明TopoSort是這套代碼里最值得優(yōu)先讀的方法工具數(shù)量超過(guò)10個(gè)之后連線會(huì)復(fù)雜到肉眼看不出來(lái)此時(shí)排序結(jié)果里若出現(xiàn)環(huán)運(yùn)行器應(yīng)當(dāng)直接報(bào)錯(cuò)而不是死循環(huán)。我習(xí)慣在排序失敗時(shí)把環(huán)的路徑拼成字符串打到日志里這個(gè)細(xì)節(jié)比工具本身更救命。4. 值傳遞與拖拉交互把ToolBlock的連線體驗(yàn)搬進(jìn)WinForm4.1 輸入輸出怎么映射HTuple與C#類(lèi)型互轉(zhuǎn)VisionPro里連線傳輸?shù)氖欠盒蛿?shù)據(jù)Halcon這邊的HTuple天生就是干這個(gè)的。但團(tuán)隊(duì)里新來(lái)的同事經(jīng)常直接把HTuple到處傳導(dǎo)致工具內(nèi)部全是if (hTuple.Type HTupleType.DOUBLE)這種分支工具多了以后代碼臭得沒(méi)法聞。這套資源里做了專(zhuān)門(mén)的值轉(zhuǎn)換層把HTuple轉(zhuǎn)成C#強(qiáng)類(lèi)型再交給算子使用這一步我認(rèn)為是整個(gè)工程最清醒的設(shè)計(jì)之一。public static double GetDouble(HTuple h, double defaultValue 0.0) { if (h null || h.Length 0) return defaultValue; try { return h.D; } catch { return defaultValue; } } public static Listdouble GetDoubleArray(HTuple h) { var list new Listdouble(); if (h null) return list; for (int i 0; i h.Length; i) list.Add(h.DArr[i]); return list; }邏輯說(shuō)明Halcon的算子返回值經(jīng)常是數(shù)組形式比如FindCircle返回的一組圓心的行列坐標(biāo)必須用h.DArr取完整數(shù)組不能只拿第一個(gè)。我在看這套資源的源碼時(shí)特別注意了它對(duì)空HTuple的處理默認(rèn)值兜底寫(xiě)得很全這直接提升了工具在缺參場(chǎng)景下的健壯度。4.2 拖拉連線的數(shù)據(jù)流UI層如何記錄連接關(guān)系WinForm原生控件里沒(méi)有現(xiàn)成的連線畫(huà)布所以工程里大概率用自定義控件或第三方圖形控件來(lái)實(shí)現(xiàn)拖拉交互。核心不在于畫(huà)線而在于每次拖拽完成時(shí)要記錄一條連接記錄存到Connection對(duì)象里public class ToolConnection { public string OutputToolId { get; set; } public string OutputParamName { get; set; } public string InputToolId { get; set; } public string InputParamName { get; set; } }這套資源的UI交互方式我的理解是工具箱在左側(cè)中間畫(huà)布擺工具圖標(biāo)右側(cè)屬性面板顯示當(dāng)前選中工具的輸入輸出參數(shù)網(wǎng)格。用戶從A工具的輸出行拖動(dòng)到B工具的輸入行界面畫(huà)一條線同時(shí)生成上面的ToolConnection記錄。落庫(kù)后運(yùn)行器就用它做值路由。這也是建議新手先讀的部分因?yàn)樗裊I事件和數(shù)據(jù)流解耦了。如果你正在自己寫(xiě)類(lèi)似的WinForm視覺(jué)框架這個(gè)對(duì)象模型可以直接抄比在控件Paint事件里畫(huà)線然后順手改數(shù)據(jù)要干凈得多。4.3 值傳遞的運(yùn)行時(shí)機(jī)制解析連線、填充輸入、收集輸出有了ToolConnection列表運(yùn)行時(shí)值傳遞的實(shí)現(xiàn)就水到渠成。執(zhí)行到工具A之前先掃描所有連線找出OutputToolId等于當(dāng)前工具ID的記錄從工具的輸出集合里取出對(duì)應(yīng)參數(shù)值寫(xiě)入全局值表。執(zhí)行完工具A后再掃描連線把輸出值推送到目標(biāo)工具的輸入集合。private void ResolveConnections(ToolConnection conn, ITool source, ITool target) { var srcOut source.Outputs.FirstOrDefault(o o.Name conn.OutputParamName); if (srcOut null) { LogError($輸出參數(shù)不存在: {conn.OutputParamName}); return; } var tgtIn target.Inputs.FirstOrDefault(i i.Name conn.InputParamName); if (tgtIn null) { LogError($輸入?yún)?shù)不存在: {conn.InputParamName}); return; } tgtIn.Value srcOut.Value; }參數(shù)說(shuō)明這里有個(gè)坑源工具的輸出類(lèi)型和下游工具的輸入類(lèi)型要保持兼容。比如上游輸出的是HTuple整數(shù)下游聲明的是double賦值時(shí)HTuple的隱式轉(zhuǎn)換沒(méi)問(wèn)題但如果下游聲明的是HObject圖像賦值就會(huì)直接拋異常。所以工具接口里ParamItem.DataType字段在UI上要做類(lèi)型顏色區(qū)分我一般給圖像類(lèi)參數(shù)標(biāo)藍(lán)色、數(shù)值類(lèi)標(biāo)綠色操作者連錯(cuò)線時(shí)能一眼看出來(lái)。邏輯說(shuō)明ResolveConnections的調(diào)用時(shí)機(jī)是執(zhí)行每個(gè)工具前且只解析直接相連的上游連線不跨層傳遞。VisionPro里ToolBlock也是這個(gè)邏輯工具B的輸入不會(huì)自動(dòng)拿到工具A的輸入值除非工具A把它作為輸出暴露出來(lái)。這一點(diǎn)跟新同事講流程走向時(shí)經(jīng)常被誤解建議你在自己的工程里注釋里寫(xiě)清楚。5. 避坑清單Halcon引擎、插件加載與WinForm交互的六條血淚經(jīng)驗(yàn)5.1 現(xiàn)象插件掃描時(shí)拋ReflectionTypeLoadException所有工具都加載不出來(lái)原因插件dll依賴了Halcon的halcondotnet.dll但該dll沒(méi)有復(fù)制到插件目錄Assembly.LoadFrom只會(huì)把待加載程序集所在目錄作為探測(cè)路徑依賴解析失敗。解決把Halcon運(yùn)行相關(guān)的dll統(tǒng)一放到插件根目錄或者宿主程序啟動(dòng)時(shí)提前用Assembly.LoadFrom預(yù)加載halcondotnet.dll。我做這類(lèi)框架時(shí)會(huì)在插件目錄下建一個(gè)runtime子目錄把Halcon runtime、第三方依賴全放進(jìn)去再用AppDomain.CurrentDomain.AssemblyResolve事件兜底手動(dòng)按名解析這樣插件更新時(shí)不會(huì)把主程序拖下水。5.2 現(xiàn)象程序運(yùn)行中替換插件dll提示文件被占用原因Assembly.LoadFrom加載的程序集默認(rèn)不卸載dll文件被進(jìn)程鎖定。在開(kāi)發(fā)環(huán)境里每次編譯插件前都要先關(guān)主程序聯(lián)調(diào)效率極低。解決把LoadFrom改成Assembly.Load(File.ReadAllBytes(file))可以繞開(kāi)文件鎖定但代價(jià)是程序集的依賴上下文脫離后續(xù)更新不愛(ài)生效。更實(shí)用的方案是把插件加載放進(jìn)獨(dú)立AppDomain整個(gè)JOB的加載和卸載都在子域里做。如果項(xiàng)目剛起步建議先接受換插件必須重啟程序的約束優(yōu)先用文件復(fù)制方式隔離插件版本等到插件數(shù)量超過(guò)10個(gè)再折騰獨(dú)立AppDomain不遲。5.3 現(xiàn)象跑的挺好但時(shí)不時(shí)閃退看日志發(fā)現(xiàn)是跨線程更新界面原因Halcon算子通常放在后臺(tái)線程執(zhí)行執(zhí)行結(jié)束后要把圖像和結(jié)果顯示在WinForm控件上。但HWindowControl或其他自定義繪圖控件的句柄只能在UI線程操作跨線程直接調(diào)用會(huì)拋異?;螂S機(jī)崩潰。解決執(zhí)行完成后用BeginInvoke把結(jié)果顯示邏輯切回UI線程。注意Halcon的HWindowControl在后臺(tái)線程里可以調(diào)用算子和HImage.SetWindowAttr但凡是綁定到控件的操作必須在UI線程把控件句柄當(dāng)作普通資源處理就會(huì)翻車(chē)。我在工具執(zhí)行耗時(shí)統(tǒng)計(jì)里專(zhuān)門(mén)加了一項(xiàng)UI刷新耗時(shí)避免把界面卡頓誤判成算法慢。5.4 現(xiàn)象工具輸出的HTuple到下游變成空值定位半天找不到原因原因工具輸出的HTuple是引用類(lèi)型某些算子失敗時(shí)會(huì)把輸出HTuple清空但工具本身沒(méi)有返回錯(cuò)誤狀態(tài)運(yùn)行器照樣把空值寫(xiě)進(jìn)了連線。下游工具拿到空值后算子直接異常。解決在Execute里強(qiáng)制約定算子失敗時(shí)必須返回false并攜帶錯(cuò)誤信息運(yùn)行器檢測(cè)到任一工具執(zhí)行失敗就終止整條JOB而不是繼續(xù)往下跑。這樣做的代價(jià)是調(diào)試時(shí)需要手動(dòng)單步跳過(guò)失敗工具但這個(gè)決策能避免一大半跑著跑著結(jié)果不對(duì)的玄學(xué)問(wèn)題。5.5 現(xiàn)象JOB里放了圖像采集工具但運(yùn)行時(shí)內(nèi)存持續(xù)上漲最后程序被殺掉原因Halcon的HObject是句柄式資源垃圾回收器不會(huì)立即釋放底層內(nèi)存。反復(fù)采集圖像不釋放舊圖像句柄內(nèi)存呈階梯式上漲這就是經(jīng)典的內(nèi)存泄漏。解決工具執(zhí)行完凡是輸出圖像到連線的上游工具要把圖像句柄登記到JobContext的ObjectPool里JOB結(jié)束后統(tǒng)一dispose。HObject實(shí)現(xiàn)了IDisposable但為了在插件場(chǎng)景下不跨dll卡Release我最終改成在JobContext里集中管理所有圖像生命周期比在每個(gè)工具里寫(xiě)using穩(wěn)妥得多。5.6 現(xiàn)象換了一臺(tái)機(jī)器運(yùn)行報(bào)許可證錯(cuò)誤Halcon窗口彈出來(lái)又消失原因Halcon的運(yùn)行時(shí)許可證是按機(jī)器綁定的。開(kāi)發(fā)機(jī)上能用不代表部署機(jī)能用大坑之一就是HALCONROOT和HALCONIMAGES環(huán)境變量沒(méi)配齊或者裝了64位程序但誤用32位license文件。解決部署時(shí)先手動(dòng)運(yùn)行一下Halcon自帶的檢查工具h(yuǎn)develop或license相關(guān)腳本確認(rèn)當(dāng)前機(jī)器能正常打開(kāi)HDevelop再跑主程序。然后把HALCONROOT、HALCONARCH寫(xiě)進(jìn)程序啟動(dòng)配置里而不是依賴系統(tǒng)環(huán)境變量。我在這上面吃過(guò)虧后來(lái)在自己的框架里加了一個(gè)啟動(dòng)自檢界面專(zhuān)門(mén)列出Halcon版本、license路徑、dll加載是否成功聯(lián)調(diào)時(shí)非常頂用。6. 把JOB保存成快照序列化工具鏈配置到JSON的一個(gè)落地習(xí)慣工具鏈的價(jià)值不止在于跑通一次更在于能把一條調(diào)試好的流程存檔下次換工件、換相機(jī)時(shí)只需改參數(shù)不重搭流程。這套資源里如果看了ITool里的GetConfig和LoadConfig你會(huì)發(fā)現(xiàn)幾乎所有工具參數(shù)都是可序列化的我在此基礎(chǔ)上補(bǔ)了一個(gè)JobSerializer用來(lái)把整套JOB配置落地成JSON。public class JobSnapshot { public string JobName { get; set; } public ListToolNode Tools { get; set; } public ListToolConnection Connections { get; set; } public DateTime SavedTime { get; set; } } public class ToolNode { public string Id { get; set; } public string ToolType { get; set; } public string ConfigJson { get; set; } }保存時(shí)遍歷JOB里的工具逐個(gè)調(diào)GetConfig()拿參數(shù)JSON連同連線表一起塞進(jìn)JobSnapshot。加載時(shí)先按ToolType去插件池找類(lèi)型創(chuàng)建實(shí)例再調(diào)LoadConfig()把參數(shù)灌回去最后重建連線關(guān)系。這樣整條流程從能跑變成可復(fù)現(xiàn)換線換產(chǎn)品時(shí)不用重新調(diào)一遍所有閾值這是做視覺(jué)框架后期幸福感提升最明顯的功能。斷點(diǎn)調(diào)試也是這套快照機(jī)制的直接受益者。我給運(yùn)行器加了一個(gè)調(diào)試模式每執(zhí)行完一個(gè)工具自動(dòng)把當(dāng)前JobContext的全局值表和圖像句柄導(dǎo)出到調(diào)試目錄這樣產(chǎn)品出問(wèn)題時(shí)能在事后復(fù)現(xiàn)當(dāng)時(shí)數(shù)據(jù)長(zhǎng)什么樣不用守在產(chǎn)線前干瞪眼。調(diào)試文件按時(shí)間戳命名配合JOB快照能定位到是哪一版參數(shù)跑出了異常結(jié)果。另外提醒一個(gè)實(shí)戰(zhàn)習(xí)慣我每次改完一套視覺(jué)流程都會(huì)強(qiáng)制走一遍保存快照 → 清空J(rèn)OB → 從快照恢復(fù) → 跑一遍的閉環(huán)確認(rèn)序列化沒(méi)有丟參數(shù)才敢把工程交給下一個(gè)人改。這個(gè)習(xí)慣幫我擋掉過(guò)無(wú)數(shù)次你昨天調(diào)的挺好的怎么今天就不行了的尷尬局面。希望這套資源的拆解筆記能幫你在WinForm里搭出一套稱手的Halcon工具鏈拿來(lái)即用也好改著用也好先把拖拉連線跑通后面的擴(kuò)展路就順了。本文還有配套的精品資源點(diǎn)擊獲取