using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Threading; using TeamAAS.Camera; using TeamAAS.Camera.Enums; using TeamAAS.Camera.Images; using TeamAAS.FlowEditor.Execution; using TeamAAS.FlowEditor.Plugins; using TeamAAS.FlowEditor.Models; using Plugins.Standard.Models; using Plugins.Standard.Views; namespace Plugins.Standard { [Serializable] [Plugin("图像采集", PluginCategory.硬件模块, typeof(ImageAcqModel), typeof(ImageAcqView), "Camera", Description = "从相机采集或从本地文件/目录加载图像,输出 GenImage,并带可视化预览窗体")] public class ImageAcqPlugin : BasePlugin { /// 本地目录图像文件缓存(运行态,不序列化) [NonSerialized] private List _dirCache; /// 缓存对应的目录路径,用于判断目录是否变更 [NonSerialized] private string _dirCachePath; /// 支持的本地图像扩展名 private static readonly string[] ImageExtensions = { ".bmp", ".jpg", ".jpeg", ".png", ".tif", ".tiff" }; public override List DeclareOutputs() { return new List { new OutputField("图像", typeof(GenImage)), new OutputField("相机", typeof(string)), new OutputField("曝光", typeof(float)), new OutputField("增益", typeof(float)), new OutputField("Gamma", typeof(float)), new OutputField("触发模式", typeof(string)), }; } public override NodeRunStatus PluginRun(CancellationToken token, out Dictionary results) { var res = new Dictionary(); results = res; // 统一失败出口:写 Error + 记 1 级错误日志(任何日志阈值都会落盘),返回 Failed NodeRunStatus Fail(string message) { res["Error"] = message; Log(1, $"图像采集失败: {message}", TeamAAS.LogLevel.Error); return NodeRunStatus.Failed; } try { GenImage image; string sourceDesc; string triggerDesc = ""; float exposure = 0f; float gain = 0f; float gamma = GetValue(Model.Gamma); switch (Model.Source) { case AcqImageSource.本地图像: { string path = Model.LocalImagePath; Log(3, $"开始加载本地图像: {(string.IsNullOrEmpty(path) ? "(未选择)" : path)}"); if (string.IsNullOrWhiteSpace(path)) return Fail("未指定本地图像文件"); if (!File.Exists(path)) return Fail($"图像文件不存在: {path}"); image = LoadImageFile(path); if (image == null) return Fail($"图像加载失败(格式不支持或文件损坏): {path}"); if (Model.ApplyGamma && gamma > 0) image.ApplyGamma(gamma); sourceDesc = Path.GetFileName(path); Log(2, $"本地图像加载成功: {sourceDesc} ({image.Width}x{image.Height})"); break; } case AcqImageSource.本地目录: { string dir = Model.LocalImageDir; Log(3, $"开始加载目录图像: {(string.IsNullOrEmpty(dir) ? "(未选择)" : dir)}"); if (string.IsNullOrWhiteSpace(dir)) return Fail("未指定图像目录"); if (!Directory.Exists(dir)) return Fail($"图像目录不存在: {dir}"); var files = GetDirectoryImages(dir); if (files.Count == 0) return Fail($"目录中没有可用图像: {dir}"); // 游标越界(目录内容变化)时回到起点 if (Model.DirCursor < 0 || Model.DirCursor >= files.Count) Model.DirCursor = 0; string path = files[Model.DirCursor]; int picked = Model.DirCursor + 1; image = LoadImageFile(path); if (image == null) return Fail($"图像加载失败(格式不支持或文件损坏): {path}"); if (Model.ApplyGamma && gamma > 0) image.ApplyGamma(gamma); if (Model.CyclicRead) Model.DirCursor = (Model.DirCursor + 1) % files.Count; sourceDesc = Path.GetFileName(path); Log(2, $"目录图像加载成功: {sourceDesc} (第 {picked}/{files.Count} 张)"); break; } default: // AcqImageSource.相机采集 { string cameraName = Model.CameraName; Log(3, $"开始采集,相机={(string.IsNullOrEmpty(cameraName) ? "(未选择)" : cameraName)}"); if (string.IsNullOrEmpty(cameraName)) return Fail("未选择相机设备"); var camera = Service().Devices .FirstOrDefault(c => c.Name == cameraName); if (camera == null) return Fail($"找不到相机: {cameraName}"); if (token.IsCancellationRequested) return Fail("采集已取消"); if (Model.AutoOpen && !camera.IsConnected) { bool opened = camera.OpenDevice(); if (!opened) return Fail($"相机打开失败: {camera.ErrorMessage}"); Log(4, $"相机 {cameraName} 已自动打开"); } if (token.IsCancellationRequested) return Fail("采集已取消"); // 触发模式:仅在节点显式指定(非“跟随相机”)时下发, // 避免改写相机自身配置或影响共用同一相机的其它节点。 // 可选项随相机能力动态变化,不支持的模式直接失败并告知它到底支持哪些。 if (Model.TriggerMode != CameraTriggerMode.跟随相机) { if (camera.SupportedTriggerModes == null || !camera.SupportedTriggerModes.Contains(Model.TriggerMode)) return Fail($"相机 {cameraName} 不支持触发模式「{Model.TriggerMode}」" + $"(该相机支持:{string.Join("、", camera.SupportedTriggerModes ?? new CameraTriggerMode[0])})"); if (!camera.SetTriggerMode(Model.TriggerMode)) return Fail($"设置触发模式「{Model.TriggerMode}」失败: {camera.ErrorMessage}"); Log(4, $"触发模式已设为 {Model.TriggerMode}"); } exposure = GetValue(Model.ExposureTime); if (exposure > 0) camera.SetExposureTime(exposure); gain = GetValue(Model.Gain); if (gain >= 0) camera.SetGain(gain); if (gamma > 0) camera.SetGamma(gamma); Log(4, $"采集参数: 曝光={exposure}us, 增益={gain}dB, Gamma={gamma}"); if (token.IsCancellationRequested) return Fail("采集已取消"); image = camera.Grab(); if (image == null) { // 硬触发下相机要等外部信号(PLC/光电),无信号就是超时,与“相机坏了”区分开 bool waitingExternal = Model.TriggerMode == CameraTriggerMode.硬触发上升沿 || Model.TriggerMode == CameraTriggerMode.硬触发下降沿; return Fail(waitingExternal ? $"等待外部触发信号超时({Model.TriggerMode}): {camera.ErrorMessage}" : $"采集失败: {camera.ErrorMessage}"); } image.SetMetadata("ExposureTime", exposure); image.SetMetadata("Gain", gain); sourceDesc = cameraName; triggerDesc = camera.TriggerMode.ToString(); Log(2, $"采集成功,相机={cameraName}, 曝光={exposure}us, 增益={gain}dB, 触发={triggerDesc}"); break; } } res["图像"] = image; res["相机"] = sourceDesc; res["曝光"] = exposure; res["增益"] = gain; res["Gamma"] = gamma; res["触发模式"] = triggerDesc; // 供自定义视图预览(运行态字段,不写入流程文件) Model.PreviewImage = image; return NodeRunStatus.Success; } catch (OperationCanceledException) { return Fail("采集已取消"); } catch (Exception ex) { return Fail($"图像采集异常: {ex.Message}"); } } /// /// 获取目录下的图像文件列表(带缓存,目录变化时重建并复位游标)。 /// private List GetDirectoryImages(string dir) { if (_dirCache != null && string.Equals(_dirCachePath, dir, StringComparison.OrdinalIgnoreCase)) return _dirCache; var list = Directory.EnumerateFiles(dir, "*.*", SearchOption.TopDirectoryOnly) .Where(f => ImageExtensions.Contains(Path.GetExtension(f).ToLowerInvariant())) .OrderBy(f => f, StringComparer.OrdinalIgnoreCase) .ToList(); _dirCache = list; _dirCachePath = dir; Model.DirCursor = 0; return list; } /// /// 从文件加载图像为 GenImage。读入内存再解码,避免锁定源文件;失败返回 null。 /// internal static GenImage LoadImageFile(string path) { try { var bytes = File.ReadAllBytes(path); using (var ms = new MemoryStream(bytes)) using (var bmp = new Bitmap(ms)) { return GenImage.FromBitmap(bmp); } } catch { return null; } } } }