ImageAcqPlugin.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Threading;
  7. using TeamAAS.Camera;
  8. using TeamAAS.Camera.Enums;
  9. using TeamAAS.Camera.Images;
  10. using TeamAAS.FlowEditor.Execution;
  11. using TeamAAS.FlowEditor.Plugins;
  12. using TeamAAS.FlowEditor.Models;
  13. using Plugins.Standard.Models;
  14. using Plugins.Standard.Views;
  15. namespace Plugins.Standard
  16. {
  17. [Serializable]
  18. [Plugin("图像采集", PluginCategory.硬件模块, typeof(ImageAcqModel), typeof(ImageAcqView),
  19. "Camera", Description = "从相机采集或从本地文件/目录加载图像,输出 GenImage,并带可视化预览窗体")]
  20. public class ImageAcqPlugin : BasePlugin<ImageAcqModel>
  21. {
  22. /// <summary>本地目录图像文件缓存(运行态,不序列化)</summary>
  23. [NonSerialized] private List<string> _dirCache;
  24. /// <summary>缓存对应的目录路径,用于判断目录是否变更</summary>
  25. [NonSerialized] private string _dirCachePath;
  26. /// <summary>支持的本地图像扩展名</summary>
  27. private static readonly string[] ImageExtensions =
  28. { ".bmp", ".jpg", ".jpeg", ".png", ".tif", ".tiff" };
  29. public override List<OutputField> DeclareOutputs()
  30. {
  31. return new List<OutputField>
  32. {
  33. new OutputField("图像", typeof(GenImage)),
  34. new OutputField("相机", typeof(string)),
  35. new OutputField("曝光", typeof(float)),
  36. new OutputField("增益", typeof(float)),
  37. new OutputField("Gamma", typeof(float)),
  38. new OutputField("触发模式", typeof(string)),
  39. };
  40. }
  41. public override NodeRunStatus PluginRun(CancellationToken token, out Dictionary<string, object> results)
  42. {
  43. var res = new Dictionary<string, object>();
  44. results = res;
  45. // 统一失败出口:写 Error + 记 1 级错误日志(任何日志阈值都会落盘),返回 Failed
  46. NodeRunStatus Fail(string message)
  47. {
  48. res["Error"] = message;
  49. Log(1, $"图像采集失败: {message}", TeamAAS.LogLevel.Error);
  50. return NodeRunStatus.Failed;
  51. }
  52. try
  53. {
  54. GenImage image;
  55. string sourceDesc;
  56. string triggerDesc = "";
  57. float exposure = 0f;
  58. float gain = 0f;
  59. float gamma = GetValue(Model.Gamma);
  60. switch (Model.Source)
  61. {
  62. case AcqImageSource.本地图像:
  63. {
  64. string path = Model.LocalImagePath;
  65. Log(3, $"开始加载本地图像: {(string.IsNullOrEmpty(path) ? "(未选择)" : path)}");
  66. if (string.IsNullOrWhiteSpace(path))
  67. return Fail("未指定本地图像文件");
  68. if (!File.Exists(path))
  69. return Fail($"图像文件不存在: {path}");
  70. image = LoadImageFile(path);
  71. if (image == null)
  72. return Fail($"图像加载失败(格式不支持或文件损坏): {path}");
  73. if (Model.ApplyGamma && gamma > 0)
  74. image.ApplyGamma(gamma);
  75. sourceDesc = Path.GetFileName(path);
  76. Log(2, $"本地图像加载成功: {sourceDesc} ({image.Width}x{image.Height})");
  77. break;
  78. }
  79. case AcqImageSource.本地目录:
  80. {
  81. string dir = Model.LocalImageDir;
  82. Log(3, $"开始加载目录图像: {(string.IsNullOrEmpty(dir) ? "(未选择)" : dir)}");
  83. if (string.IsNullOrWhiteSpace(dir))
  84. return Fail("未指定图像目录");
  85. if (!Directory.Exists(dir))
  86. return Fail($"图像目录不存在: {dir}");
  87. var files = GetDirectoryImages(dir);
  88. if (files.Count == 0)
  89. return Fail($"目录中没有可用图像: {dir}");
  90. // 游标越界(目录内容变化)时回到起点
  91. if (Model.DirCursor < 0 || Model.DirCursor >= files.Count)
  92. Model.DirCursor = 0;
  93. string path = files[Model.DirCursor];
  94. int picked = Model.DirCursor + 1;
  95. image = LoadImageFile(path);
  96. if (image == null)
  97. return Fail($"图像加载失败(格式不支持或文件损坏): {path}");
  98. if (Model.ApplyGamma && gamma > 0)
  99. image.ApplyGamma(gamma);
  100. if (Model.CyclicRead)
  101. Model.DirCursor = (Model.DirCursor + 1) % files.Count;
  102. sourceDesc = Path.GetFileName(path);
  103. Log(2, $"目录图像加载成功: {sourceDesc} (第 {picked}/{files.Count} 张)");
  104. break;
  105. }
  106. default: // AcqImageSource.相机采集
  107. {
  108. string cameraName = Model.CameraName;
  109. Log(3, $"开始采集,相机={(string.IsNullOrEmpty(cameraName) ? "(未选择)" : cameraName)}");
  110. if (string.IsNullOrEmpty(cameraName))
  111. return Fail("未选择相机设备");
  112. var camera = Service<CameraManager>().Devices
  113. .FirstOrDefault(c => c.Name == cameraName);
  114. if (camera == null)
  115. return Fail($"找不到相机: {cameraName}");
  116. if (token.IsCancellationRequested)
  117. return Fail("采集已取消");
  118. if (Model.AutoOpen && !camera.IsConnected)
  119. {
  120. bool opened = camera.OpenDevice();
  121. if (!opened)
  122. return Fail($"相机打开失败: {camera.ErrorMessage}");
  123. Log(4, $"相机 {cameraName} 已自动打开");
  124. }
  125. if (token.IsCancellationRequested)
  126. return Fail("采集已取消");
  127. // 触发模式:仅在节点显式指定(非“跟随相机”)时下发,
  128. // 避免改写相机自身配置或影响共用同一相机的其它节点。
  129. // 可选项随相机能力动态变化,不支持的模式直接失败并告知它到底支持哪些。
  130. if (Model.TriggerMode != CameraTriggerMode.跟随相机)
  131. {
  132. if (camera.SupportedTriggerModes == null ||
  133. !camera.SupportedTriggerModes.Contains(Model.TriggerMode))
  134. return Fail($"相机 {cameraName} 不支持触发模式「{Model.TriggerMode}」" +
  135. $"(该相机支持:{string.Join("、", camera.SupportedTriggerModes ?? new CameraTriggerMode[0])})");
  136. if (!camera.SetTriggerMode(Model.TriggerMode))
  137. return Fail($"设置触发模式「{Model.TriggerMode}」失败: {camera.ErrorMessage}");
  138. Log(4, $"触发模式已设为 {Model.TriggerMode}");
  139. }
  140. exposure = GetValue(Model.ExposureTime);
  141. if (exposure > 0)
  142. camera.SetExposureTime(exposure);
  143. gain = GetValue(Model.Gain);
  144. if (gain >= 0)
  145. camera.SetGain(gain);
  146. if (gamma > 0)
  147. camera.SetGamma(gamma);
  148. Log(4, $"采集参数: 曝光={exposure}us, 增益={gain}dB, Gamma={gamma}");
  149. if (token.IsCancellationRequested)
  150. return Fail("采集已取消");
  151. image = camera.Grab();
  152. if (image == null)
  153. {
  154. // 硬触发下相机要等外部信号(PLC/光电),无信号就是超时,与“相机坏了”区分开
  155. bool waitingExternal =
  156. Model.TriggerMode == CameraTriggerMode.硬触发上升沿 ||
  157. Model.TriggerMode == CameraTriggerMode.硬触发下降沿;
  158. return Fail(waitingExternal
  159. ? $"等待外部触发信号超时({Model.TriggerMode}): {camera.ErrorMessage}"
  160. : $"采集失败: {camera.ErrorMessage}");
  161. }
  162. image.SetMetadata("ExposureTime", exposure);
  163. image.SetMetadata("Gain", gain);
  164. sourceDesc = cameraName;
  165. triggerDesc = camera.TriggerMode.ToString();
  166. Log(2, $"采集成功,相机={cameraName}, 曝光={exposure}us, 增益={gain}dB, 触发={triggerDesc}");
  167. break;
  168. }
  169. }
  170. res["图像"] = image;
  171. res["相机"] = sourceDesc;
  172. res["曝光"] = exposure;
  173. res["增益"] = gain;
  174. res["Gamma"] = gamma;
  175. res["触发模式"] = triggerDesc;
  176. // 供自定义视图预览(运行态字段,不写入流程文件)
  177. Model.PreviewImage = image;
  178. return NodeRunStatus.Success;
  179. }
  180. catch (OperationCanceledException)
  181. {
  182. return Fail("采集已取消");
  183. }
  184. catch (Exception ex)
  185. {
  186. return Fail($"图像采集异常: {ex.Message}");
  187. }
  188. }
  189. /// <summary>
  190. /// 获取目录下的图像文件列表(带缓存,目录变化时重建并复位游标)。
  191. /// </summary>
  192. private List<string> GetDirectoryImages(string dir)
  193. {
  194. if (_dirCache != null &&
  195. string.Equals(_dirCachePath, dir, StringComparison.OrdinalIgnoreCase))
  196. return _dirCache;
  197. var list = Directory.EnumerateFiles(dir, "*.*", SearchOption.TopDirectoryOnly)
  198. .Where(f => ImageExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
  199. .OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
  200. .ToList();
  201. _dirCache = list;
  202. _dirCachePath = dir;
  203. Model.DirCursor = 0;
  204. return list;
  205. }
  206. /// <summary>
  207. /// 从文件加载图像为 GenImage。读入内存再解码,避免锁定源文件;失败返回 null。
  208. /// </summary>
  209. internal static GenImage LoadImageFile(string path)
  210. {
  211. try
  212. {
  213. var bytes = File.ReadAllBytes(path);
  214. using (var ms = new MemoryStream(bytes))
  215. using (var bmp = new Bitmap(ms))
  216. {
  217. return GenImage.FromBitmap(bmp);
  218. }
  219. }
  220. catch
  221. {
  222. return null;
  223. }
  224. }
  225. }
  226. }