VppFlowNodes.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Linq;
  5. using System.Text.RegularExpressions;
  6. using System.Threading;
  7. using Cognex.VisionPro;
  8. using PropertyGridLib.Attributes;
  9. using PropertyGridLib.Controls;
  10. using TeamAAS.FlowEditor.Execution;
  11. using TeamAAS.FlowEditor.Models;
  12. using TeamAAS.FlowEditor.Plugins;
  13. using TeamAAS.FlowEngine.FormulaData;
  14. using TeamAAS.Vision;
  15. namespace Plugins.Vpp
  16. {
  17. // ─────────────────────────────────────────────────────────────────────
  18. // VPP(VisionPro)流程容器 —— 镜像 HalconFlowPlugin 的"平台子流程"模式:
  19. // 主流程拖一个「VPP流程」容器节点,双击进入 VPP 专用子流程编辑器(工具箱只显
  20. // Vpp模块 的算子),配置输入/输出映射后循环执行子图。
  21. // ─────────────────────────────────────────────────────────────────────
  22. /// <summary>
  23. /// VPP 编辑预览挂接点:容器节点运行前开启新一轮运行批次(换 RunStamp + 清图层总线,
  24. /// 只保留当前运行的图层),Vp 算子节点运行后发布图层,VPP 流程编辑器右侧大显示窗消费。
  25. /// </summary>
  26. public static class VppEditorPreview
  27. {
  28. private static long _runSeq;
  29. /// <summary>当前运行批次号(同批次重复发布=整体替换)。</summary>
  30. public static string RunStamp { get; private set; } = "0";
  31. /// <summary>容器节点新一轮运行开始:批次号+1 并清空图层总线。</summary>
  32. public static void BeginRunCycle()
  33. {
  34. _runSeq++;
  35. RunStamp = _runSeq.ToString();
  36. VisionLayerStore.Instance.ClearAll();
  37. }
  38. /// <summary>
  39. /// 发布工具的【运行记录】为图层集 —— 镜像 VisionPro 原生下拉的体验:
  40. /// 一条记录一个图层(InputImage/Histogram/BlobImage/BlobImageUnfiltered...),
  41. /// 图层内容就是 ICogRecord 本身,编辑器选中后交给 Cognex 原生显示控件渲染
  42. /// (图像与图形叠加按 VisionPro 的方式原样呈现)。
  43. /// 返回:图层名 → 记录对象(保持树序;供节点作为输出往下游传递 + 模型学习图层清单)。
  44. /// </summary>
  45. public static Dictionary<string, object> ShowRecord(BasePluginModel model, object rootRecord, string preferredLayer = null)
  46. {
  47. var layerContents = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
  48. if (model == null || rootRecord == null) return layerContents;
  49. try
  50. {
  51. var args = new LayerPublishArgs
  52. {
  53. FlowId = model.FlowId,
  54. NodeName = model.NodeName,
  55. RunStamp = RunStamp,
  56. PreferredLayer = string.IsNullOrWhiteSpace(preferredLayer) ? null : preferredLayer,
  57. };
  58. var seen = new HashSet<object>();
  59. WalkRecord(rootRecord, null, 0, args, seen);
  60. foreach (var l in args.Layers)
  61. layerContents[l.Name] = l.Content;
  62. if (args.Layers.Count > 0)
  63. VisionLayerStore.Instance.Publish(args);
  64. }
  65. catch { /* 图层发布失败不影响流程 */ }
  66. return layerContents;
  67. }
  68. /// <summary>递归把记录树展开为图层(根记录在最前 = 工具默认合成视图;子记录按路径命名防重名)。
  69. /// 注意 ICogRecord 的名字属性是 RecordKey(即 VisionPro 下拉里的 "InputImage" 等显示名)。</summary>
  70. private static void WalkRecord(object recObj, string parentPath, int depth, LayerPublishArgs args, HashSet<object> seen)
  71. {
  72. var rec = recObj as ICogRecord;
  73. if (rec == null || !seen.Add(rec) || depth > 3) return;
  74. if (depth > 0)
  75. {
  76. var leaf = string.IsNullOrWhiteSpace(rec.RecordKey) ? "图层" + (args.Layers.Count + 1) : rec.RecordKey.Trim();
  77. var name = depth == 1 ? leaf : parentPath + "." + leaf;
  78. if (args.Layers.Any(l => string.Equals(l.Name, name, StringComparison.OrdinalIgnoreCase)))
  79. name = name + " (" + args.Layers.Count + ")";
  80. var isImage = rec.Content is ICogImage ||
  81. (rec.ContentType != null && typeof(ICogImage).IsAssignableFrom(rec.ContentType));
  82. args.Layers.Add(new LayerItem(name, isImage ? LayerKind.Image : LayerKind.Other, rec, owned: false));
  83. }
  84. var subs = rec.SubRecords;
  85. if (subs == null) return;
  86. var path = depth == 0 ? (rec.RecordKey ?? "LastRun") : (parentPath + "." + (rec.RecordKey ?? "N"));
  87. foreach (ICogRecord sub in subs)
  88. WalkRecord(sub, path, depth + 1, args, seen);
  89. }
  90. }
  91. /// <summary>
  92. /// VPP 流程容器节点。镜像 HalconFlowPlugin 的行为:
  93. /// 配置输入映射(把主流程图像等注入子流程)→ 运行 VPP 子流程(可循环)→ 收集输出映射回主流程。
  94. /// 双击打开 <see cref="Views.VppFlowEditorView"/>:一个只含 VPP 算子(按 VisionPlugin 分类)的流程编辑器。
  95. /// </summary>
  96. [Serializable]
  97. [Plugin("VPP流程", PluginCategory.Vpp模块, typeof(Models.VppFlowModel), typeof(Views.VppFlowEditorView),
  98. "ImageFilterCenterFocus", NodeShape = NodeCategory.Group,
  99. Description = "VisionPro 视觉子流程容器 - 图像经输入映射进入,内部用 VPP 算子处理,结果映射回主流程")]
  100. public class VppFlowPlugin : BasePlugin<Models.VppFlowModel>
  101. {
  102. public override List<OutputField> DeclareOutputs()
  103. {
  104. var list = new List<OutputField>();
  105. if (Model?.ModuleOutputs != null)
  106. {
  107. foreach (var kv in Model.ModuleOutputs)
  108. {
  109. if (string.IsNullOrWhiteSpace(kv.OutName)) continue;
  110. list.Add(new OutputField(kv.OutName, typeof(object)));
  111. }
  112. }
  113. list.Add(new OutputField("实际循环次数", typeof(int)));
  114. list.Add(new OutputField("循环设置", typeof(string)));
  115. list.Add(new OutputField("结果", typeof(bool)));
  116. list.Add(new OutputField("Message", typeof(string)));
  117. return list;
  118. }
  119. public override bool InitPlugin()
  120. {
  121. Model.EnsureSubGraph();
  122. return true;
  123. }
  124. [Newtonsoft.Json.JsonIgnore]
  125. public int CurrentCount { get; set; } = 0;
  126. public override NodeRunStatus PluginRun(CancellationToken token, out Dictionary<string, object> results)
  127. {
  128. var res = new Dictionary<string, object>();
  129. results = res;
  130. Model.EnsureSubGraph();
  131. NodeRunStatus Fail(string message)
  132. {
  133. res["Error"] = message;
  134. res["结果"] = false;
  135. Log(1, $"VPP 流程失败: {message}", TeamAAS.LogLevel.Error);
  136. return NodeRunStatus.Failed;
  137. }
  138. if (Model.LoopCount == 0)
  139. {
  140. Log(2, "循环次数为 0,跳过 VPP 流程执行");
  141. res["结果"] = true;
  142. res["实际循环次数"] = 0;
  143. res["循环设置"] = "0";
  144. return NodeRunStatus.Skipped;
  145. }
  146. if (Model.SubGraph.Nodes.Count == 0)
  147. return Fail("VPP 子流程为空(双击节点进入编辑器拖入 VPP 算子)");
  148. Log(3, $"VPP 流程开始,循环设置={(Model.LoopCount == -1 ? "无限" : Model.LoopCount.ToString())},输入 {Model.ModuleInputs?.Count ?? 0} 个、输出 {Model.ModuleOutputs?.Count ?? 0} 个、子节点 {Model.SubGraph.Nodes.Count} 个");
  149. Model.IsBreakRequested = false;
  150. int maxLoops = Model.LoopCount == -1 ? int.MaxValue : Model.LoopCount;
  151. int actualLoops = 0;
  152. // 新一轮运行:换图层批次并清掉上一轮遗留图层(显示窗只保留当前运行的图层)
  153. VppEditorPreview.BeginRunCycle();
  154. try
  155. {
  156. for (int i = 0; i < maxLoops; i++)
  157. {
  158. if (token.IsCancellationRequested) break;
  159. CurrentCount = i + 1;
  160. foreach (var node in Model.SubGraph.Nodes)
  161. {
  162. node.Status = NodeRunStatus.NotStarted;
  163. node.CostTime = 0;
  164. }
  165. // ResetAllNodes 会 ClearFlow 清掉子流程所有结果(含刚注册的输入),
  166. // 所以用 SkipReset=true 保留输入变量;节点状态由上面的循环手动复位(与 GroupPlugin 一致)
  167. RegisterInputs();
  168. var executor = new FlowExecutor(Model.SubGraph) { SkipReset = true };
  169. executor.EndNodeEncountered += () => { Model.IsBreakRequested = true; };
  170. executor.ExecuteAsync(token).Wait();
  171. actualLoops++;
  172. if (Model.IsBreakRequested)
  173. {
  174. res["Message"] = $"第 {i + 1} 次循环遇到结束节点";
  175. Log(2, $"第 {i + 1} 次循环遇到结束节点,跳出循环");
  176. break;
  177. }
  178. }
  179. // 收集输出:把子流程内部节点结果映射回主流程
  180. SaveOutputs(res);
  181. res["实际循环次数"] = actualLoops;
  182. res["循环设置"] = Model.LoopCount == -1 ? "无限" : Model.LoopCount.ToString();
  183. if (token.IsCancellationRequested)
  184. {
  185. Log(1, $"VPP 流程被取消,已执行 {actualLoops} 次循环", TeamAAS.LogLevel.Warning);
  186. res["结果"] = false;
  187. return NodeRunStatus.Failed;
  188. }
  189. res["结果"] = true;
  190. Log(2, $"VPP 流程执行完成,实际循环 {actualLoops} 次");
  191. return NodeRunStatus.Success;
  192. }
  193. catch (Exception ex)
  194. {
  195. return Fail(ex.Message);
  196. }
  197. }
  198. /// <summary>
  199. /// 把 ModuleInputs 映射到子流程注册表的「输入」区,内部节点通过 &amp;{输入.变量名} 引用。
  200. /// </summary>
  201. private void RegisterInputs()
  202. {
  203. if (Model?.ModuleInputs == null || Model.ModuleInputs.Count == 0) return;
  204. var subGraphId = Model.SubGraph.GraphId;
  205. var flowId = GetModel.FlowId;
  206. foreach (var input in Model.ModuleInputs)
  207. {
  208. if (string.IsNullOrWhiteSpace(input.InputName)) continue;
  209. if (input.SourceValue == null) continue;
  210. object value = null;
  211. if (!string.IsNullOrWhiteSpace(input.SourceValue.Formula))
  212. value = RegistryOrDebug.ResolveFormula(flowId, input.SourceValue.Formula);
  213. else
  214. value = input.SourceValue.Value;
  215. RegistryOrDebug.SetInputVariable(subGraphId, input.InputName, value);
  216. Log(4, $"输入[{input.InputName}] ← {(value?.GetType().Name ?? "null")}");
  217. }
  218. }
  219. /// <summary>把每个 ModuleOutput 的公式在子流程内解析后写入结果(键=OutName)。</summary>
  220. private void SaveOutputs(Dictionary<string, object> results)
  221. {
  222. if (Model?.ModuleOutputs == null) return;
  223. foreach (var kv in Model.ModuleOutputs)
  224. {
  225. if (string.IsNullOrWhiteSpace(kv.OutName)) continue;
  226. object value = ResolveFromSubGraph(kv.OutValue?.Formula, kv.OutValue?.Value);
  227. results[kv.OutName] = value;
  228. Log(4, $"输出[{kv.OutName}] = {(value?.ToString() ?? "null")}");
  229. }
  230. }
  231. /// <summary>
  232. /// 解析 &amp;{节点名.属性名}(从子流程节点本地 RawResults 读),
  233. /// 或 &amp;{流程名.节点名.属性名}(回退到注册表);无公式时返回 fallback。
  234. /// </summary>
  235. private object ResolveFromSubGraph(string formula, object fallback)
  236. {
  237. if (string.IsNullOrEmpty(formula)) return fallback;
  238. var m2 = Regex.Match(formula, @"^&\{(?<node>[^.}]+)\.(?<prop>[^.}]+)\}$");
  239. if (m2.Success)
  240. {
  241. var nodeName = m2.Groups["node"].Value;
  242. var propName = m2.Groups["prop"].Value;
  243. // 「输入」伪节点直接查注册表
  244. if (nodeName == "输入")
  245. return RegistryOrDebug.GetValue(Model.SubGraph.GraphId, "输入", propName);
  246. var node = Model.SubGraph?.Nodes?.FirstOrDefault(n => n.NodeName == nodeName);
  247. if (node?.RawResults != null && node.RawResults.TryGetValue(propName, out var val))
  248. return val;
  249. return null;
  250. }
  251. var m3 = Regex.Match(formula, @"^&\{(?<flow>[^.}]+)\.(?<node>[^.}]+)\.(?<prop>[^.}]+)\}$");
  252. if (m3.Success)
  253. return RegistryOrDebug.GetValue(m3.Groups["flow"].Value, m3.Groups["node"].Value, m3.Groups["prop"].Value);
  254. return formula;
  255. }
  256. }
  257. }
  258. namespace Plugins.Vpp.Models
  259. {
  260. using TeamAAS.FlowEditor.Plugins;
  261. /// <summary>
  262. /// VPP 流程容器节点模型。镜像 HalconFlowModel:持有一张子流程图 <see cref="SubGraph"/>,
  263. /// 支持把主流程前序节点的输出(尤其是图像)映射进来,并把子流程结果映射回主流程。
  264. /// 子流程内部的节点是 VPP 专用算子(按 VisionPlugin 分类),不含主流程节点。
  265. /// </summary>
  266. [Serializable]
  267. public class VppFlowModel : BasePluginModel
  268. {
  269. [Category("I.参数")]
  270. [DisplayName("1.输入参数")]
  271. [Description("把主流程前序节点的输出映射进来(图像通常映射为「图像」);子流程内部节点用 &{输入.变量名} 引用")]
  272. [CollectionEditor(CanAdd = true, CanRemove = true, CanSort = true)]
  273. public List<VppFlowInput> ModuleInputs { get; set; } = new List<VppFlowInput>();
  274. [Category("I.参数")]
  275. [DisplayName("2.输出参数")]
  276. [Description("把子流程内部节点的结果映射回主流程(如分数、位置、OK/NG)")]
  277. [CollectionEditor(CanAdd = true, CanRemove = true, CanSort = true)]
  278. public List<VppFlowOutput> ModuleOutputs { get; set; } = new List<VppFlowOutput>();
  279. private int _loopCount = 1;
  280. [Category("II.循环设置")]
  281. [DisplayName("1.循环次数")]
  282. [Description("循环次数。-1=无限循环, 0=跳过不执行, 其他=按次数执行")]
  283. public int LoopCount
  284. {
  285. get => _loopCount;
  286. set => _loopCount = value;
  287. }
  288. [Browsable(false)]
  289. public FlowGraph SubGraph { get; set; }
  290. [Browsable(false)]
  291. public bool IsBreakRequested { get; set; } = false;
  292. public void EnsureSubGraph()
  293. {
  294. if (SubGraph == null)
  295. SubGraph = new FlowGraph { GraphName = ToolName ?? "VPP流程", GraphId = NodeId };
  296. }
  297. }
  298. /// <summary>容器输入映射项:主流程来源值 → 子流程内 &amp;{输入.InputName}。</summary>
  299. [Serializable]
  300. public class VppFlowInput
  301. {
  302. public VppFlowInput()
  303. {
  304. SourceValue = new FormulaBound<object>();
  305. }
  306. [Category("输入参数")]
  307. [DisplayName("变量名")]
  308. [Description("自定义变量名,内部节点通过 &{输入.变量名} 引用(图像一般命名为「图像」)")]
  309. public string InputName { get; set; } = "图像";
  310. [Category("输入参数")]
  311. [DisplayName("来源值")]
  312. [Description("从主流程前序节点的输出结果中选择(运行时自动读取最新值)")]
  313. [FormulaEditor(typeof(InputSourceDataProvider))]
  314. public FormulaBound<object> SourceValue { get; set; }
  315. public override string ToString() => InputName;
  316. }
  317. /// <summary>容器输出映射项:子流程内 &amp;{节点名.属性名} → 主流程 OutName。</summary>
  318. [Serializable]
  319. public class VppFlowOutput
  320. {
  321. public VppFlowOutput()
  322. {
  323. OutValue = new FormulaBound<object>();
  324. }
  325. private string _outName = string.Empty;
  326. [Category("输出参数")]
  327. [DisplayName("参数名")]
  328. [Description("输出到主流程的参数名(下游节点据此绑定)")]
  329. public string OutName
  330. {
  331. get => _outName;
  332. set => _outName = value;
  333. }
  334. [Category("输出参数")]
  335. [DisplayName("参数值")]
  336. [Description("从子流程内部节点的输出中选择(如 &{Vp模板匹配1.分数}、&{Vp找圆1.半径})")]
  337. [FormulaEditor(typeof(PluginDataProvider))]
  338. public FormulaBound<object> OutValue { get; set; }
  339. public override string ToString() => OutName;
  340. }
  341. }