| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- 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<ImageAcqModel>
- {
- /// <summary>本地目录图像文件缓存(运行态,不序列化)</summary>
- [NonSerialized] private List<string> _dirCache;
- /// <summary>缓存对应的目录路径,用于判断目录是否变更</summary>
- [NonSerialized] private string _dirCachePath;
- /// <summary>支持的本地图像扩展名</summary>
- private static readonly string[] ImageExtensions =
- { ".bmp", ".jpg", ".jpeg", ".png", ".tif", ".tiff" };
- public override List<OutputField> DeclareOutputs()
- {
- return new List<OutputField>
- {
- 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<string, object> results)
- {
- var res = new Dictionary<string, object>();
- 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<CameraManager>().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}");
- }
- }
- /// <summary>
- /// 获取目录下的图像文件列表(带缓存,目录变化时重建并复位游标)。
- /// </summary>
- private List<string> 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;
- }
- /// <summary>
- /// 从文件加载图像为 GenImage。读入内存再解码,避免锁定源文件;失败返回 null。
- /// </summary>
- 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;
- }
- }
- }
- }
|