VpToolSupport.cs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Runtime.Serialization;
  7. using System.Threading;
  8. using System.Windows.Forms;
  9. using Cognex.VisionPro;
  10. using PropertyGridLib.Attributes;
  11. using PropertyGridLib.Controls;
  12. using System.ComponentModel;
  13. using TeamAAS.Camera.Images;
  14. using TeamAAS.FlowEditor.Execution;
  15. using TeamAAS.FlowEditor.Models;
  16. using TeamAAS.FlowEditor.Plugins;
  17. using TeamAAS.FlowEngine.FormulaData;
  18. using Plugins.Vpp.Converters;
  19. namespace Plugins.Vpp
  20. {
  21. /// <summary>
  22. /// VisionPro 原生工具反射支撑(算子级节点的基础设施)。
  23. /// 设计:不依赖具体工具程序集(CogBlob/PMAlign/Caliper…),按工具类名在已加载程序集中
  24. /// 反射创建/运行/取结果 —— 新增"某个 VP 工具"的算子节点时零引用成本;
  25. /// 工具实例挂在模型上(object 承载),配置随流程文件(BinaryFormatter)整体持久化,
  26. /// 与 VppToolBlockModel.ToolBlock 同一持久化模式。
  27. /// </summary>
  28. internal static class VpToolSupport
  29. {
  30. /// <summary>
  31. /// VisionPro 工具/显示程序集清单(简单名)。这些程序集是懒加载的:应用没显式用到之前
  32. /// 不在 AppDomain 里,反射扫描会"找不到 CogBlobTool"。按类型名查工具前先主动加载一轮,
  33. /// 加载失败(本机未装 VisionPro)静默跳过。
  34. /// </summary>
  35. private static readonly string[] CognexToolAssemblies =
  36. {
  37. "Cognex.VisionPro.ToolGroup",
  38. "Cognex.VisionPro.Blob",
  39. "Cognex.VisionPro.Blob.Controls",
  40. "Cognex.VisionPro.PMAlign",
  41. "Cognex.VisionPro.PMAlign.Controls",
  42. "Cognex.VisionPro.Caliper",
  43. "Cognex.VisionPro.Caliper.Controls",
  44. "Cognex.VisionPro.ID",
  45. "Cognex.VisionPro.Display.Controls",
  46. "Cognex.VisionPro.Controls",
  47. "Cognex.VisionPro.ToolGroup.Controls",
  48. };
  49. private static void EnsureCognexAssembliesLoaded()
  50. {
  51. foreach (var name in CognexToolAssemblies)
  52. {
  53. try { Assembly.Load(name); } catch { /* 未安装/缺失 → 跳过 */ }
  54. }
  55. }
  56. /// <summary>按简单类名(如 "CogBlobTool")在已加载程序集中查找 ICogTool 实现。
  57. /// 找不到时先主动加载 VisionPro 工具程序集再重扫一次(懒加载问题)。</summary>
  58. public static Type FindToolType(string simpleName)
  59. {
  60. var found = FindToolTypeInLoaded(simpleName);
  61. if (found != null) return found;
  62. EnsureCognexAssembliesLoaded();
  63. return FindToolTypeInLoaded(simpleName);
  64. }
  65. private static Type FindToolTypeInLoaded(string simpleName)
  66. {
  67. Type found = null;
  68. foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
  69. {
  70. try
  71. {
  72. if (asm.IsDynamic) continue;
  73. foreach (var t in asm.GetTypes())
  74. {
  75. if (t.Name == simpleName && typeof(ICogTool).IsAssignableFrom(t) && !t.IsAbstract)
  76. return t;
  77. if (t.Name == simpleName) found = t;
  78. }
  79. }
  80. catch { }
  81. }
  82. return found;
  83. }
  84. /// <summary>创建默认工具实例。失败抛异常(节点报错并提示)。</summary>
  85. public static object CreateTool(string simpleName)
  86. {
  87. var type = FindToolType(simpleName)
  88. ?? throw new InvalidOperationException($"未找到 VisionPro 工具类型 {simpleName}(检查 VPP 运行库是否已加载)");
  89. return Activator.CreateInstance(type);
  90. }
  91. /// <summary>
  92. /// 设置输入图像(GenImage → ICogImage 自动转换)。
  93. /// 兼容 ICogRecord(自动抽取其中的 ICogImage,如绑定到工具块的「输出图层N」);
  94. /// 类型不符时抛出带实际类型信息的异常(而不是反射的"Object 无法转换 ICogImage")。
  95. /// </summary>
  96. public static void SetInputImage(object tool, object image)
  97. {
  98. if (tool == null || image == null) return;
  99. object cog;
  100. if (image is GenImage gi)
  101. cog = GenImageConverter.ToCogImage(gi);
  102. else if (image is ICogImage ci)
  103. cog = ci;
  104. else if (image is ICogRecord rec)
  105. {
  106. cog = FindRecordImage(rec) ?? throw new InvalidOperationException(
  107. $"输入图像绑定到了记录({rec.GetType().Name}),但其中没有可用的 ICogImage(检查「输入图像」应绑定图像输出而非记录/图层)");
  108. }
  109. else
  110. cog = image;
  111. var prop = tool.GetType().GetProperty("InputImage");
  112. var expect = prop?.PropertyType;
  113. if (cog != null && expect != null && !expect.IsInstanceOfType(cog))
  114. throw new InvalidOperationException(
  115. $"输入图像类型不符:实际 {cog.GetType().Name},需要 {expect.Name}(检查「输入图像」绑定来源是否为图像输出)");
  116. prop?.SetValue(tool, cog);
  117. }
  118. /// <summary>从工具运行记录里递归找第一张 ICogImage(记录树:Content 或 SubRecords)。</summary>
  119. private static ICogImage FindRecordImage(ICogRecord record)
  120. {
  121. if (record == null) return null;
  122. try
  123. {
  124. if (record.Content is ICogImage img) return img;
  125. var subs = record.SubRecords;
  126. if (subs != null)
  127. {
  128. foreach (ICogRecord sub in subs)
  129. {
  130. var found = FindRecordImage(sub);
  131. if (found != null) return found;
  132. }
  133. }
  134. }
  135. catch { }
  136. return null;
  137. }
  138. /// <summary>运行工具(ICogTool.Run)。</summary>
  139. public static void Run(object tool) => ((ICogTool)tool).Run();
  140. /// <summary>
  141. /// 按属性路径取值,支持 索引 与 0参方法:如 "Results.Count"、"Results[0].Score"、
  142. /// "Results[0].GetPose.TranslationX"(GetPose 为 0 参方法时自动调用)。任一环节失败返回 null。
  143. /// </summary>
  144. public static object GetPath(object root, string path)
  145. {
  146. try
  147. {
  148. object cur = root;
  149. foreach (var seg in path.Split('.'))
  150. {
  151. if (cur == null) return null;
  152. var m = System.Text.RegularExpressions.Regex.Match(seg, @"^(?<name>[^\[]+)(\[(?<idx>\d+)\])?$");
  153. // 方法段带 "()"(如 GetBlobs())——成员查找用裸名(GetBlobs)
  154. var name = m.Groups["name"].Value.Replace("()", "");
  155. cur = GetMember(cur, name);
  156. if (cur == null) return null;
  157. if (m.Groups["idx"].Success && int.TryParse(m.Groups["idx"].Value, out var idx))
  158. cur = GetIndexed(cur, idx);
  159. }
  160. return cur;
  161. }
  162. catch { return null; }
  163. }
  164. private static object GetMember(object obj, string name)
  165. {
  166. if (obj == null || string.IsNullOrEmpty(name)) return null;
  167. var type = obj.GetType();
  168. var prop = type.GetProperty(name);
  169. if (prop != null) return prop.GetValue(obj);
  170. var field = type.GetField(name);
  171. if (field != null) return field.GetValue(obj);
  172. // GetXxx 属性不存在时尝试同名 GetXxx() 方法(如 Blob:GetBlobs/GetBlobMeasure)
  173. var method = type.GetMethod(name, Type.EmptyTypes)
  174. ?? (name.StartsWith("Get") ? null : type.GetMethod("Get" + name, Type.EmptyTypes));
  175. if (method != null)
  176. {
  177. try { return method.Invoke(obj, null); } catch { return null; }
  178. }
  179. // 集合默认项:名字为空/First 时取第 0 项
  180. if (name == "First") return GetIndexed(obj, 0);
  181. return null;
  182. }
  183. private static object GetIndexed(object obj, int index)
  184. {
  185. try
  186. {
  187. if (obj is System.Collections.IList list) return index < list.Count ? list[index] : null;
  188. var indexer = obj?.GetType().GetProperties()
  189. .FirstOrDefault(p => p.GetIndexParameters().Length == 1 && p.GetIndexParameters()[0].ParameterType == typeof(int));
  190. if (indexer != null && CountOf(obj) > index) return indexer.GetValue(obj, new object[] { index });
  191. }
  192. catch { }
  193. return null;
  194. }
  195. private static int CountOf(object obj)
  196. {
  197. try
  198. {
  199. var count = obj?.GetType().GetProperty("Count")?.GetValue(obj);
  200. return count is int i ? i : 0;
  201. }
  202. catch { return 0; }
  203. }
  204. /// <summary>
  205. /// 打开工具的原生编辑器(VisionPro 惯例:CogXxxTool ↔ CogXxxEditV2 控件,Subject 属性挂工具)。
  206. /// 编辑控件可能与工具不在同一程序集(如 CogBlobTool 的 EditV2 在 ToolGroup.Controls),
  207. /// 因此在全部已加载程序集中按类名查找。找不到返回 false(节点日志提示改用属性面板)。
  208. /// </summary>
  209. public static bool OpenNativeEditor(object tool, string title)
  210. {
  211. if (tool == null) return false;
  212. var type = tool.GetType();
  213. var baseName = type.Name; // 如 CogBlobTool
  214. var shortName = baseName.Replace("Tool", ""); // VisionPro 编辑控件命名惯例去掉 Tool:CogBlobEditV2
  215. foreach (var suffix in new[] { "EditV2", "Edit", "EditControl" })
  216. {
  217. var ctlType = FindControlType(baseName + suffix)
  218. ?? FindControlType(shortName + suffix);
  219. if (ctlType == null) continue;
  220. try
  221. {
  222. var ctl = (Control)Activator.CreateInstance(ctlType);
  223. ctlType.GetProperty("Subject")?.SetValue(ctl, tool);
  224. using (var form = new Form { Text = title ?? type.Name, Width = 1280, Height = 860, StartPosition = FormStartPosition.CenterScreen })
  225. {
  226. ctl.Dock = DockStyle.Fill;
  227. form.Controls.Add(ctl);
  228. Views.CogGdiRender.Apply(form); // 编辑器内部显示控件统一切 GDI 渲染(远程环境白屏修复)
  229. form.ShowDialog();
  230. }
  231. return true;
  232. }
  233. catch { }
  234. }
  235. return false;
  236. }
  237. /// <summary>在全部已加载程序集中按简单类名查找 WinForms 控件类型。
  238. /// 找不到时先主动加载 VisionPro 编辑控件程序集再重扫一次(懒加载问题)。</summary>
  239. private static Type FindControlType(string simpleName)
  240. {
  241. var found = FindControlTypeInLoaded(simpleName);
  242. if (found != null) return found;
  243. EnsureCognexAssembliesLoaded();
  244. return FindControlTypeInLoaded(simpleName);
  245. }
  246. private static Type FindControlTypeInLoaded(string simpleName)
  247. {
  248. foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
  249. {
  250. try
  251. {
  252. if (asm.IsDynamic) continue;
  253. foreach (var t in asm.GetTypes())
  254. {
  255. if (t.Name == simpleName && typeof(Control).IsAssignableFrom(t) && !t.IsAbstract)
  256. return t;
  257. }
  258. }
  259. catch { }
  260. }
  261. return null;
  262. }
  263. /// <summary>
  264. /// 学习工具属性树的可读标量路径(迁移 VisionPro「添加终端」的选树体验):
  265. /// 递归展开公共属性与字段(深度≤5、总量封顶 300),记录 基元/字符串/日期 值的路径;
  266. /// 集合展开前 3 个元素标 [i](如 Results.GetBlobs[0].CenterOfMassX)。
  267. /// 值为 null 的属性也登记(路径有效,只是此刻无值——Blob 未启用的测量项即此类)。
  268. /// </summary>
  269. public static List<string> LearnTerminalPaths(object root)
  270. {
  271. var paths = new List<string>();
  272. if (root == null) return paths;
  273. var seen = new HashSet<object>();
  274. const int MaxNodes = 300;
  275. void Walk(object obj, string prefix, int depth)
  276. {
  277. if (obj == null || depth > 5 || paths.Count >= MaxNodes) return;
  278. if (!seen.Add(obj)) return;
  279. var members = obj.GetType()
  280. .GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
  281. .Select(p => (Name: p.Name, Type: (Type)p.PropertyType, Get: (Func<object>)(() => p.GetValue(obj))))
  282. .Concat(obj.GetType()
  283. .GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)
  284. .Select(f => (Name: f.Name, Type: f.FieldType, Get: (Func<object>)(() => f.GetValue(obj)))));
  285. foreach (var m in members)
  286. {
  287. if (paths.Count >= MaxNodes) return;
  288. var name = m.Name;
  289. if (name == "Name" || name == "RunStatus" || name.StartsWith("Site") || name.StartsWith("Tag")) continue;
  290. object v;
  291. try { v = m.Get(); } catch { continue; }
  292. var path = string.IsNullOrEmpty(prefix) ? name : prefix + "." + name;
  293. var vt = m.Type;
  294. if (vt.IsPrimitive || v is string || v is decimal || v is DateTime)
  295. {
  296. if (!paths.Contains(path)) paths.Add(path);
  297. continue;
  298. }
  299. if (v == null)
  300. {
  301. // 路径有效但此刻无值(如 Blob 未启用的测量项)→ 登记为 null 值路径
  302. if (vt.IsPrimitive || vt == typeof(string) || vt == typeof(decimal) || vt == typeof(DateTime))
  303. if (!paths.Contains(path)) paths.Add(path);
  304. continue;
  305. }
  306. if (v is System.Collections.IEnumerable en && !(v is string))
  307. {
  308. // 集合:展开前 3 个元素标 [i](Blob 列表等)
  309. var idx = 0;
  310. try
  311. {
  312. foreach (var e in en)
  313. {
  314. if (idx >= 3 || paths.Count >= MaxNodes) break;
  315. if (e == null) { idx++; continue; }
  316. var et = e.GetType();
  317. if (et.IsPrimitive || e is string || e is decimal || e is DateTime)
  318. {
  319. var p2 = path + "[" + idx + "]";
  320. if (!paths.Contains(p2)) paths.Add(p2);
  321. }
  322. else if (!(et.Namespace ?? "").StartsWith("System"))
  323. {
  324. Walk(e, path + "[" + idx + "]", depth + 1);
  325. }
  326. idx++;
  327. }
  328. }
  329. catch { }
  330. continue;
  331. }
  332. if (!(vt.Namespace ?? "").StartsWith("System"))
  333. {
  334. Walk(v, path, depth + 1);
  335. }
  336. }
  337. }
  338. Walk(root, "", 0);
  339. // 输出统一为树格式(索引段方法化:GetBlobs[0] → GetBlobs()[0]),与弹窗勾选回显一一对应
  340. return paths.Select(NormalizePath).Distinct().ToList();
  341. }
  342. /// <summary>
  343. /// 严格路径取值:exists=false 表示路径中途断裂(属性/字段不存在),value 为 null 仅表示"此刻无值"。
  344. /// 供预设终端预勾选时实测验证——只保留真实存在的路径。
  345. /// </summary>
  346. public static object GetPathStrict(object root, string path, out bool exists)
  347. {
  348. exists = false;
  349. if (root == null || string.IsNullOrWhiteSpace(path)) return null;
  350. object cur = root;
  351. foreach (var seg in path.Split('.'))
  352. {
  353. var m = System.Text.RegularExpressions.Regex.Match(seg, @"^(?<name>[^\[]+)(\[(?<idx>\d+)\])?$");
  354. if (!m.Success) return null;
  355. var name = m.Groups["name"].Value;
  356. var hasMember = MemberExists(cur, name);
  357. if (!hasMember) return null; // 属性/字段不存在 → 路径无效
  358. cur = GetMember(cur, name);
  359. if (m.Groups["idx"].Success)
  360. {
  361. if (cur == null) { exists = true; return null; } // 集合为 null:路径结构存在
  362. if (!int.TryParse(m.Groups["idx"].Value, out var idx)) return null;
  363. var before = cur;
  364. cur = GetIndexed(cur, idx);
  365. if (cur == null && before != null && CountOf(before) == 0)
  366. { exists = true; return null; } // 空集合:路径结构存在(本次运行无 Blob)
  367. if (cur == null) { exists = true; return null; } // 索引越界:结构存在
  368. }
  369. }
  370. exists = true;
  371. return cur;
  372. }
  373. /// <summary>对象上是否存在指定名称的公共属性或字段(name 兼容带 "()" 的方法段)。</summary>
  374. private static bool MemberExists(object obj, string name)
  375. {
  376. if (obj == null || string.IsNullOrEmpty(name)) return false;
  377. name = name.Replace("()", "");
  378. var t = obj.GetType();
  379. if (t.GetProperty(name) != null) return true;
  380. if (t.GetField(name) != null) return true;
  381. if (t.GetMethod(name, Type.EmptyTypes) != null) return true;
  382. return false;
  383. }
  384. /// <summary>
  385. /// 旧格式路径 → 树格式规范化:索引加括号(GetBlobs[0] → GetBlobs()[0],兼容属性式旧流程数据)。
  386. /// 规则:"[n]" 前的段若不是以 ")" 结尾(即不是方法调用),在段名与 [n] 之间插入 "()"。
  387. /// </summary>
  388. public static string NormalizePath(string path)
  389. {
  390. if (string.IsNullOrWhiteSpace(path)) return path;
  391. var segs = path.Split('.');
  392. for (int i = 0; i < segs.Length; i++)
  393. {
  394. var m = System.Text.RegularExpressions.Regex.Match(segs[i], @"^(?<name>[^\[]+)\[(?<idx>\d+)\](?<rest>.*)$");
  395. if (m.Success && !m.Groups["name"].Value.EndsWith(")"))
  396. {
  397. segs[i] = m.Groups["name"].Value + "()" + "[" + m.Groups["idx"].Value + "]" + m.Groups["rest"].Value;
  398. }
  399. }
  400. return string.Join(".", segs);
  401. }
  402. /// <summary>终端路径的输出名 = 路径末段(如 ...CenterOfMassX → CenterOfMassX)。</summary>
  403. public static string TerminalLeafName(string path)
  404. {
  405. if (string.IsNullOrWhiteSpace(path)) return path;
  406. var segs = path.Split('.');
  407. return segs[segs.Length - 1];
  408. }
  409. /// <summary>终端输出名(末段 + 与现有名单去重:重名追加 _2/_3)。</summary>
  410. public static string TerminalOutputName(string path, ICollection<string> existing)
  411. {
  412. var n = TerminalLeafName(path);
  413. if (existing == null || !existing.Contains(n)) return n;
  414. var i = 2;
  415. while (existing.Contains(n + "_" + i)) i++;
  416. return n + "_" + i;
  417. }
  418. /// <summary>
  419. /// Blob 官方测量项清单(CogBlobMeasureConstants 全量,docs.cognex.com)。
  420. /// CogBlobResult 不暴露这些属性 —— 只能经 GetMeasure(枚举) 查询(未启用的测量抛异常)。
  421. /// </summary>
  422. public static readonly string[] BlobMeasures =
  423. {
  424. "Label", "Area", "BoundaryPixelLength", "Perimeter", "NumUnfilteredChildren",
  425. "CenterMassX", "CenterMassY", "InertiaX", "InertiaY", "InertiaMin", "InertiaMax",
  426. "Elongation", "Angle", "Acircularity", "AcircularityRms",
  427. "BoundingBoxPixelAlignedNoExcludeCenterX", "BoundingBoxPixelAlignedNoExcludeCenterY",
  428. "BoundingBoxPixelAlignedNoExcludeMinX", "BoundingBoxPixelAlignedNoExcludeMaxX",
  429. "BoundingBoxPixelAlignedNoExcludeMinY", "BoundingBoxPixelAlignedNoExcludeMaxY",
  430. "BoundingBoxPixelAlignedNoExcludeWidth", "BoundingBoxPixelAlignedNoExcludeHeight",
  431. "BoundingBoxPixelAlignedNoExcludeAspect",
  432. "MedianExtremaAngleX", "MedianExtremaAngleY",
  433. "BoundingBoxExtremaAngleCenterX", "BoundingBoxExtremaAngleCenterY",
  434. "BoundingBoxExtremaAngleMinX", "BoundingBoxExtremaAngleMaxX",
  435. "BoundingBoxExtremaAngleMinY", "BoundingBoxExtremaAngleMaxY",
  436. "BoundingBoxExtremaAngleWidth", "BoundingBoxExtremaAngleHeight",
  437. "BoundingBoxExtremaAngleAspect",
  438. "BoundingBoxPrincipalAxisMinX", "BoundingBoxPrincipalAxisMaxX",
  439. "BoundingBoxPrincipalAxisMinY", "BoundingBoxPrincipalAxisMaxY",
  440. "BoundingBoxPrincipalAxisWidth", "BoundingBoxPrincipalAxisHeight",
  441. "BoundingBoxPrincipalAxisAspect",
  442. "NotClipped",
  443. };
  444. /// <summary>
  445. /// GetMeasure 终端:路径形如 "...@Measure:Area",经 CogBlobResult.GetMeasure(枚举) 取值。
  446. /// 路径前半段定位到 CogBlobResult 对象(如 Results.GetBlobs[0])。
  447. /// </summary>
  448. /// <summary>解析测量终端路径 → (对象路径, 测量名)。非测量路径返回 false。</summary>
  449. public static bool TryParseMeasurePath(string path, out string objectPath, out string measureName)
  450. {
  451. objectPath = null;
  452. measureName = null;
  453. if (string.IsNullOrWhiteSpace(path)) return false;
  454. var i = path.IndexOf(VpPresetTerminal.MeasureMarker, StringComparison.Ordinal);
  455. if (i < 0) return false;
  456. objectPath = path.Substring(0, i);
  457. measureName = path.Substring(i + VpPresetTerminal.MeasureMarker.Length);
  458. return !string.IsNullOrEmpty(measureName);
  459. }
  460. /// <summary>取测量值(对象路径 + 测量名)。对象缺失/未启用该测量 → null(不抛错)。</summary>
  461. public static object GetMeasureValue(object root, string objectPath, string measureName)
  462. {
  463. var target = string.IsNullOrEmpty(objectPath) ? root : GetPath(root, objectPath);
  464. if (target == null) return null;
  465. try
  466. {
  467. var enumType = target.GetType().Assembly.GetType(target.GetType().FullName.Replace(target.GetType().Name, "CogBlobMeasureConstants"), false)
  468. ?? FindMeasureEnumType(target.GetType());
  469. if (enumType == null) return null;
  470. var value = Enum.Parse(enumType, measureName);
  471. var mi = target.GetType().GetMethod("GetMeasure", new[] { enumType });
  472. return mi?.Invoke(target, new[] { value });
  473. }
  474. catch { return null; }
  475. }
  476. /// <summary>在对象程序集里找 CogBlobMeasureConstants 枚举类型。</summary>
  477. private static Type FindMeasureEnumType(Type resultType)
  478. {
  479. try { return resultType.Assembly.GetType(resultType.Namespace + ".CogBlobMeasureConstants", false); }
  480. catch { return null; }
  481. }
  482. }
  483. /// <summary>
  484. /// VP 算子节点模型基类:持有原生工具实例(随流程文件序列化,配置不丢)+ 输入图像绑定 + 高级编辑按钮。
  485. /// </summary>
  486. [Serializable]
  487. public abstract class VpToolModelBase : BasePluginModel, IVpPresetSource
  488. {
  489. [Browsable(false)]
  490. public object Tool;
  491. /// <summary>工具简单类名(如 "CogBlobTool")——由对应插件基类写入,用于懒创建默认工具。</summary>
  492. [Browsable(false)]
  493. public string ToolTypeName;
  494. [Category("I.输入")]
  495. [DisplayName("1.输入图像")]
  496. [Description("上游图像(GenImage/CogImage)。通常绑定容器输入 &{输入.图像} 或前序节点输出")]
  497. [FormulaEditor(typeof(PluginDataProvider))]
  498. public FormulaBound<object> ImageSource { get; set; } = new FormulaBound<object>();
  499. /// <summary>
  500. /// 输出图层列表(多选):勾选哪些记录图层,哪些就作为节点输出往下游传递。
  501. /// 可选项来自最近一次运行的图层学习(LastRecordKeys,随流程持久化)。
  502. /// </summary>
  503. [Category("II.输出")]
  504. [DisplayName("1.输出图层列表")]
  505. [Description("多选要往下游传递的图层(运行一次后勾选列表会列出全部可选项)")]
  506. [MultiSelect(typeof(VpOutputLayerMultiProvider))]
  507. public List<string> OutputLayers { get; set; } = new List<string>();
  508. /// <summary>最近一次运行学习到的图层名清单(「输出图层列表」可选项;随流程持久化)。</summary>
  509. [Browsable(false)]
  510. public List<string> LastRecordKeys { get; set; } = new List<string>();
  511. /// <summary>
  512. /// 输出终端(已勾选的属性路径清单):由「编辑输出终端」弹窗树勾选管理,下游按路径末段名绑定。
  513. /// 首次使用时自动预勾选该工具的预设结果项(面积1/分数2 等已在树中呈勾选态)。
  514. /// </summary>
  515. [Browsable(false)]
  516. public List<string> OutputTerminals { get; set; } = new List<string>();
  517. /// <summary>最近一次运行学习到的工具属性路径清单(编辑弹窗树的数据补充;随流程持久化)。</summary>
  518. [Browsable(false)]
  519. public List<string> LastTerminalPaths { get; set; } = new List<string>();
  520. [Category("II.输出")]
  521. [DisplayName("3.编辑输出终端")]
  522. [Description("弹出工具属性/结果树,勾选任意项作为输出终端(迁移 VisionPro「添加终端」)")]
  523. [Button("编辑输出终端", nameof(OpenTerminalEditor))]
  524. public object TerminalEditorButton { get; set; }
  525. public void OpenTerminalEditor()
  526. {
  527. Views.VpTerminalEditorWindow.Show(this);
  528. }
  529. /// <summary>预设终端路径(来自对应插件类的 PresetTerminals;经 PluginLoader 按工具名取)。
  530. /// 路径统一为【树生成格式】(方法带括号:Results.GetBlobs()[0]@Measure:Area),与弹窗勾选回显一一对应。</summary>
  531. public List<string> GetPresetPaths()
  532. {
  533. try
  534. {
  535. var plugin = TeamAAS.FlowEngine.PluginLoader.Instance.CreateInstance(ToolName);
  536. var prop = plugin?.GetType().GetProperty("PresetTerminals");
  537. if (prop?.GetValue(plugin) is IEnumerable<VpPresetTerminal> presets)
  538. return presets.Where(p => !string.IsNullOrWhiteSpace(p?.Path))
  539. .Select(p => VpToolSupport.NormalizePath(p.Path)).ToList();
  540. }
  541. catch { }
  542. return new List<string>();
  543. }
  544. // ── 高级编辑(属性面板按钮)──
  545. [NonSerialized]
  546. public Func<object, string, bool> NativeEditorOpener = VpToolSupport.OpenNativeEditor;
  547. [Category("III.高级编辑")]
  548. [DisplayName("1.编辑工具")]
  549. [Description("打开该 VisionPro 工具的原生编辑界面(区域/参数/调试全部在原生界面里改)")]
  550. [Button("高级编辑", nameof(OpenAdvancedEditor))]
  551. public object AdvancedEditButton { get; set; }
  552. public void OpenAdvancedEditor()
  553. {
  554. EnsureTool();
  555. // 注意:NativeEditorOpener 是 [NonSerialized] 字段,流程反序列化后字段初始化器不会执行
  556. // (BinaryFormatter 不走构造器)→ 此时为 null,必须兜底回默认打开器,否则"高级编辑进不去"
  557. var opener = NativeEditorOpener ?? VpToolSupport.OpenNativeEditor;
  558. var ok = opener.Invoke(Tool, ToolName ?? "VisionPro 工具");
  559. if (!ok)
  560. {
  561. TeamAAS.AppLogger.Warning($"未找到 {Tool?.GetType().Name} 的原生编辑控件,请改用属性面板配置", "Vp算子");
  562. }
  563. }
  564. internal void EnsureTool()
  565. {
  566. if (Tool == null && !string.IsNullOrEmpty(ToolTypeName))
  567. Tool = VpToolSupport.CreateTool(ToolTypeName);
  568. }
  569. }
  570. /// <summary>
  571. /// 「输出图层列表」多选可选项来源:该工具最近一次运行学习到的图层名。
  572. /// (PropertyGridLib 不一定把模型本体传进上下文 —— 回退到当前正在编辑的节点取模型。)
  573. /// </summary>
  574. public class VpOutputLayerMultiProvider : IMultiSelectProvider
  575. {
  576. public List<string> GetAvailableItems(PropertyItem propertyItem)
  577. {
  578. var model = TeamAAS.FlowEngine.PluginLoader.Instance.SelectFlow?.PluginModel?.GetModel as VpToolModelBase;
  579. return model?.LastRecordKeys ?? new List<string>();
  580. }
  581. }
  582. /// <summary>
  583. /// 「输出终端」多选可选项来源:该工具最近一次运行学习到的属性路径树。
  584. /// </summary>
  585. public class VpTerminalPathProvider : IMultiSelectProvider
  586. {
  587. public List<string> GetAvailableItems(PropertyItem propertyItem)
  588. {
  589. var model = TeamAAS.FlowEngine.PluginLoader.Instance.SelectFlow?.PluginModel?.GetModel as VpToolModelBase;
  590. return model?.LastTerminalPaths ?? new List<string>();
  591. }
  592. }
  593. /// <summary>预设输出终端:名称 + 工具属性路径(如 Results.GetBlobs[0].Area)。</summary>
  594. public sealed class VpPresetTerminal
  595. {
  596. /// <summary>测量终端标记:路径形如 "Results.GetBlobs[0]@Measure:Area"(经 GetMeasure 取值)。</summary>
  597. public const string MeasureMarker = "@Measure:";
  598. public string Name { get; }
  599. public string Path { get; }
  600. public VpPresetTerminal(string name, string path) { Name = name; Path = path; }
  601. }
  602. /// <summary>预设终端来源契约:终端编辑弹窗据此在首次打开时预勾选常用结果。</summary>
  603. public interface IVpPresetSource
  604. {
  605. /// <summary>预设终端路径清单。</summary>
  606. List<string> GetPresetPaths();
  607. }
  608. public static class VpPresetTerminals
  609. {
  610. public static readonly VpPresetTerminal[] Empty = new VpPresetTerminal[0];
  611. }
  612. /// <summary>
  613. /// VP 算子节点基类:统一的 取图→喂图→运行→提取结果 流程。
  614. /// 子类只需声明 工具类型名 与 结果提取(输出"全一些":数量/分数/位置/尺寸…逐项 try/catch)。
  615. /// </summary>
  616. [Serializable]
  617. public abstract class VpToolPluginBase<TModel> : BasePlugin<TModel>
  618. where TModel : VpToolModelBase, new()
  619. {
  620. /// <summary>VisionPro 工具简单类名(如 "CogBlobTool")。</summary>
  621. protected abstract string ToolTypeName { get; }
  622. /// <summary>
  623. /// 预设输出终端:各工具定义自己的常用返回结果(中文名 + 属性路径)。
  624. /// 打开终端编辑弹窗时自动呈勾选态(预选),并自动声明为输出、每次运行后赋值。
  625. /// </summary>
  626. public virtual VpPresetTerminal[] PresetTerminals => VpPresetTerminals.Empty;
  627. /// <summary>结果提取:子类把工具运行结果写进 results(全部走 VpToolSupport.GetPath 容错取值)。</summary>
  628. protected abstract void ExtractResults(object tool, Dictionary<string, object> results);
  629. public override List<OutputField> DeclareOutputs()
  630. {
  631. var list = new List<OutputField> { new OutputField("结果", typeof(bool)) };
  632. // 类型化结果(数量/分数/位置... 子类定义)
  633. foreach (var f in DeclareResultFields())
  634. list.Add(f);
  635. // 输出图层列表:勾选的每一层都作为输出往下游传(清单首次运行后学习并随流程持久化)
  636. var outs = Model?.OutputLayers;
  637. if (outs != null)
  638. {
  639. foreach (var k in outs)
  640. {
  641. if (string.IsNullOrWhiteSpace(k)) continue;
  642. list.Add(new OutputField(k.Trim(), typeof(object)));
  643. }
  644. }
  645. // 输出终端:自定义属性路径终端(迁移 VisionPro「添加终端」,输出名=路径末段)
  646. var terminals = Model?.OutputTerminals;
  647. if (terminals != null)
  648. {
  649. foreach (var p in terminals)
  650. {
  651. if (string.IsNullOrWhiteSpace(p)) continue;
  652. var existing = list.Select(x => x.Key).ToList();
  653. list.Add(new OutputField(VpToolSupport.TerminalOutputName(p.Trim(), existing), typeof(object)));
  654. }
  655. }
  656. // 根记录(默认合成视图)的稳定输出口
  657. list.Add(new OutputField("输出图像", typeof(object)));
  658. return list;
  659. }
  660. /// <summary>子类声明的结果字段(供运行前绑定树索引)。</summary>
  661. protected abstract IEnumerable<OutputField> DeclareResultFields();
  662. public override bool InitPlugin()
  663. {
  664. EnsureTool();
  665. return true;
  666. }
  667. protected void EnsureTool()
  668. {
  669. Model.ToolTypeName = ToolTypeName;
  670. Model.EnsureTool();
  671. }
  672. /// <summary>当前工具实例(EnsureTool 之后可用)。</summary>
  673. protected object Tool => Model.Tool;
  674. public override NodeRunStatus PluginRun(CancellationToken token, out Dictionary<string, object> results)
  675. {
  676. var res = new Dictionary<string, object>();
  677. results = res;
  678. NodeRunStatus Fail(string m) { res["Error"] = m; res["结果"] = false; Log(1, $"{Model.NodeName} 失败: {m}", TeamAAS.LogLevel.Error); return NodeRunStatus.Failed; }
  679. try
  680. {
  681. EnsureTool();
  682. var src = GetResolve(Model.ImageSource);
  683. if (src == null) return Fail("未获取到输入图像(请绑定「输入图像」)");
  684. // GenImage 统一转 CogImage(喂图与图层发布共用一份,避免重复转换)
  685. object cogImage = src is GenImage gi ? GenImageConverter.ToCogImage(gi) : src;
  686. VpToolSupport.SetInputImage(Tool, cogImage);
  687. VpToolSupport.Run(Tool);
  688. Log(3, $"{ToolTypeName} 运行完成");
  689. res["结果"] = true;
  690. ExtractResults(Tool, res);
  691. // 编辑器图层显示 + 输出:发布工具【运行记录】的全部图层 —— 镜像 VisionPro 原生下拉
  692. Dictionary<string, object> layerContents = null;
  693. try
  694. {
  695. var record = (Tool as Cognex.VisionPro.Implementation.CogToolBase)?.CreateLastRunRecord();
  696. if (record != null)
  697. {
  698. layerContents = VppEditorPreview.ShowRecord(Model, record);
  699. // 学习图层清单(「输出图层列表」可选项,随流程持久化)
  700. var layerKeys = layerContents.Keys.ToList();
  701. if (layerKeys.Count > 0 && !layerKeys.SequenceEqual(Model.LastRecordKeys ?? new List<string>()))
  702. Model.LastRecordKeys = layerKeys;
  703. // 输出图层列表:勾选的每一层写入节点结果(下游公式绑定)
  704. foreach (var k in Model.OutputLayers ?? new List<string>())
  705. {
  706. if (string.IsNullOrWhiteSpace(k)) continue;
  707. if (layerContents.TryGetValue(k, out var rec))
  708. res[k.Trim()] = rec;
  709. }
  710. res["输出图像"] = record;
  711. }
  712. }
  713. catch { }
  714. // 输出终端:预设终端未初始化时自动预勾选(首次使用即有常用结果);
  715. // 路径统一为树格式(方法带括号);勾选项写入节点结果(输出名=路径末段)
  716. try
  717. {
  718. if (Model.OutputTerminals == null || Model.OutputTerminals.Count == 0)
  719. {
  720. // 预勾选 = 该工具的预设终端(用户可在弹窗里取消/增选,一旦有勾选就不再覆盖)
  721. Model.OutputTerminals = PresetTerminals.Where(p => !string.IsNullOrWhiteSpace(p?.Path))
  722. .Select(p => VpToolSupport.NormalizePath(p.Path)).ToList();
  723. }
  724. else
  725. {
  726. // 旧格式兼容迁移:属性式索引 → 方法式(一次性规范化,保存流程后固定)
  727. Model.OutputTerminals = Model.OutputTerminals
  728. .Where(p => !string.IsNullOrWhiteSpace(p))
  729. .Select(VpToolSupport.NormalizePath).Distinct().ToList();
  730. }
  731. var terminalPaths = VpToolSupport.LearnTerminalPaths(Tool);
  732. if (terminalPaths.Count > 0 && !terminalPaths.SequenceEqual(Model.LastTerminalPaths ?? new List<string>()))
  733. Model.LastTerminalPaths = terminalPaths;
  734. foreach (var p in Model.OutputTerminals ?? new List<string>())
  735. {
  736. if (string.IsNullOrWhiteSpace(p)) continue;
  737. var name = VpToolSupport.TerminalOutputName(p, res.Keys.ToList());
  738. // 测量终端(...@Measure:Area):经 CogBlobResult.GetMeasure 取值
  739. if (VpToolSupport.TryParseMeasurePath(p, out var objPath, out var measureName))
  740. res[name] = VpToolSupport.GetMeasureValue(Tool, objPath, measureName);
  741. else
  742. res[name] = VpToolSupport.GetPath(Tool, p);
  743. }
  744. }
  745. catch { }
  746. return NodeRunStatus.Success;
  747. }
  748. catch (Exception ex)
  749. {
  750. return Fail(ex.Message);
  751. }
  752. }
  753. /// <summary>容错取结果值(路径取不到时输出 null,不抛错)。</summary>
  754. protected object Get(object tool, string path) => VpToolSupport.GetPath(tool, path);
  755. }
  756. }