视觉修改
This commit is contained in:
		
							
								
								
									
										838
									
								
								Check.Main/Camera/CameraManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										838
									
								
								Check.Main/Camera/CameraManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,838 @@ | ||||
| using Check.Main.Common; | ||||
| using Check.Main.Infer; | ||||
| using Check.Main.Result; | ||||
| using Check.Main.UI; | ||||
| using OpenCvSharp; | ||||
| using SkiaSharp; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Drawing; | ||||
| using System.Drawing.Imaging; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Runtime.InteropServices; | ||||
| using System.Text; | ||||
| using System.Text.Json.Serialization; | ||||
| using System.Threading.Tasks; | ||||
| using System.Timers; | ||||
| using System.Windows.Forms; | ||||
|  | ||||
| namespace Check.Main.Camera | ||||
| { | ||||
|     /// <summary> | ||||
|     /// 静态全局相机管理器,负责所有相机的生命周期、配置应用和多相机同步 | ||||
|     /// </summary> | ||||
|     public static class CameraManager | ||||
|     { | ||||
|         // 活动的相机实例字典,键为相机名称 | ||||
|         public static Dictionary<string, HikvisionCamera> ActiveCameras { get; } = new Dictionary<string, HikvisionCamera>(); | ||||
|  | ||||
|         // 相机对应的图像显示窗口字典 | ||||
|         //public static Dictionary<string, FormImageDisplay> CameraDisplays { get; } = new Dictionary<string, FormImageDisplay>(); | ||||
|  | ||||
|         public static Dictionary<string, FormImageDisplay> OriginalImageDisplays { get; } = new Dictionary<string, FormImageDisplay>(); | ||||
|         public static Dictionary<string, FormImageDisplay> ResultImageDisplays { get; } = new Dictionary<string, FormImageDisplay>(); | ||||
|  | ||||
|         // --- 多相机同步逻辑 --- | ||||
|         // 产品检测队列 | ||||
|         private static readonly Queue<ProductResult> ProductQueue = new Queue<ProductResult>(); | ||||
|         // 队列锁,保证线程安全 | ||||
|         private static readonly object QueueLock = new object(); | ||||
|         // 当前启用的相机数量,用于判断产品是否检测完毕 | ||||
|         private static int EnabledCameraCount = 0; | ||||
|  | ||||
|         //public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted; | ||||
|         // public static bool IsDetectionRunning { get; private set; } = false; | ||||
|  | ||||
|         // 产品ID计数器 | ||||
|         private static long _productCounter = 0; | ||||
|         // 用于同步计数器访问的锁,虽然long的自增是原子操作,但为清晰和未来扩展,使用锁是好习惯 | ||||
|         private static readonly object _counterLock = new object(); | ||||
|  | ||||
|         // --- 新增:硬触发模拟器 --- | ||||
|         private static readonly System.Timers.Timer _hardwareTriggerSimulator; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 获取或设置模拟硬触发的间隔时间(毫秒)。 | ||||
|         /// </summary> | ||||
|         public static double TriggerInterval { get; set; } = 1000; // 默认1秒触发一次 | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 获取一个值,该值指示硬件触发模拟器当前是否正在运行。 | ||||
|         /// </summary> | ||||
|         public static bool IsHardwareTriggerSimulating { get; private set; } = false; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 静态构造函数,用于一次性初始化静态资源。 | ||||
|         /// </summary> | ||||
|         static CameraManager() | ||||
|         { | ||||
|             //初始化硬触发模拟器 | ||||
|             _hardwareTriggerSimulator = new System.Timers.Timer(); | ||||
|             _hardwareTriggerSimulator.Elapsed += OnHardwareTriggerTimerElapsed; | ||||
|             _hardwareTriggerSimulator.AutoReset = true; // 确保定时器持续触发 | ||||
|             _hardwareTriggerSimulator.Enabled = false; // 默认不启动 | ||||
|         } | ||||
|  | ||||
|  | ||||
|         //// 事件:用于向UI发送日志消息 | ||||
|         //public static event Action<string> OnLogMessage; | ||||
|         //// 用于写入日志文件的 StreamWriter | ||||
|         //private static StreamWriter _logFileWriter; | ||||
|  | ||||
|         ////私有的、静态的、只读的对象,专门用作线程同步的“锁”。 | ||||
|         //private static readonly object _logLock = new object(); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 初始化文件日志记录器。应在程序启动时调用一次。 | ||||
|         /// </summary> | ||||
|         //public static void InitializeLogger() | ||||
|         //{ | ||||
|         //    try | ||||
|         //    { | ||||
|         //        string logDirectory = Path.Combine(Application.StartupPath, "Logs"); | ||||
|         //        Directory.CreateDirectory(logDirectory); // 确保Logs文件夹存在 | ||||
|  | ||||
|         //        string logFileName = $"Log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt"; | ||||
|         //        string logFilePath = Path.Combine(logDirectory, logFileName); | ||||
|  | ||||
|         //        // 创建StreamWriter,设置为追加模式和自动刷新 | ||||
|         //        _logFileWriter = new StreamWriter(logFilePath, append: true, encoding: System.Text.Encoding.UTF8) | ||||
|         //        { | ||||
|         //            AutoFlush = true | ||||
|         //        }; | ||||
|  | ||||
|         //        Log("文件日志记录器已初始化。"); | ||||
|         //    } | ||||
|         //    catch (Exception ex) | ||||
|         //    { | ||||
|         //        // 如果文件日志初始化失败,在UI上报告错误 | ||||
|         //        OnLogMessage?.Invoke($"[CRITICAL] 文件日志初始化失败: {ex.Message}"); | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 【新增】一个完整的业务流程方法: | ||||
|         /// 1. 根据配置初始化所有相机并显示它们的窗口。 | ||||
|         /// 2. 在所有窗口都显示后,命令所有相机开始采集。 | ||||
|         /// 这个方法是响应“开启设备”或“应用配置并启动”按钮的理想入口点。 | ||||
|         /// </summary> | ||||
|         /// <param name="settingsList">要应用的相机配置列表。</param> | ||||
|         /// <param name="mainForm">主窗体,用于停靠显示窗口。</param> | ||||
|         //public static void ApplyConfigurationAndStartGrabbing(List<CameraSettings> settingsList, FrmMain mainForm) | ||||
|         //{ | ||||
|         //    ThreadSafeLogger.Log("开始执行“开启设备”流程..."); | ||||
|  | ||||
|         //    // 步骤 1: 初始化所有相机和UI窗口 | ||||
|         //    // Initialize 方法会负责 Shutdown 旧实例、创建新实例、打开硬件、显示窗口等。 | ||||
|         //    // 因为 Initialize 方法中的 displayForm.Show() 是非阻塞的,它会立即返回, | ||||
|         //    // 窗体的创建和显示过程会被调度到UI线程的消息队列中。 | ||||
|         //    Initialize(settingsList, mainForm); | ||||
|  | ||||
|         //    // 检查是否有任何相机成功初始化 | ||||
|         //    if (ActiveCameras.Count == 0) | ||||
|         //    { | ||||
|         //        ThreadSafeLogger.Log("“开启设备”流程中止,因为没有相机被成功初始化。"); | ||||
|         //        return; | ||||
|         //    } | ||||
|  | ||||
|         //    // 步骤 2: 在 Initialize 完成后(意味着所有窗口都已创建并 Show),开始采集 | ||||
|         //    // 这个调用会紧接着 Initialize 执行,此时窗体可能还在绘制过程中,但这没关系。 | ||||
|         //    // StartAll() 会启动后台线程开始拉取图像,一旦图像到达,就会通过事件推送到已经存在的窗体上。 | ||||
|         //    ThreadSafeLogger.Log("所有相机窗口已创建,现在开始采集图像..."); | ||||
|         //    StartAll(); | ||||
|  | ||||
|         //    ThreadSafeLogger.Log("“开启设备”流程已完成。相机正在采集中。"); | ||||
|         //} | ||||
|  | ||||
|         ///// <summary> | ||||
|         ///// 根据配置列表初始化或更新所有相机 | ||||
|         ///// </summary> | ||||
|         //public static void Initialize(List<CameraSettings> settingsList, FrmMain mainForm) | ||||
|         //{ | ||||
|  | ||||
|         //    // 先停止并释放所有旧的相机 | ||||
|         //    Shutdown(); | ||||
|         //    ThreadSafeLogger.Log("开始应用新的相机配置..."); | ||||
|  | ||||
|         //    EnabledCameraCount = settingsList.Count(s => s.IsEnabled); | ||||
|         //    if (EnabledCameraCount == 0) | ||||
|         //    { | ||||
|         //        ThreadSafeLogger.Log("没有启用的相机。"); | ||||
|         //        return; | ||||
|         //    } | ||||
|  | ||||
|         //    var deviceList = new HikvisionCamera().FindDevices();  | ||||
|         //    if (deviceList.Count == 0) | ||||
|         //    { | ||||
|         //        ThreadSafeLogger.Log("错误:未找到任何相机设备!"); | ||||
|         //        return; | ||||
|         //    } | ||||
|  | ||||
|         //    int deviceIndex = 0; | ||||
|         //    foreach (var setting in settingsList) | ||||
|         //    { | ||||
|         //        if (!setting.IsEnabled) continue; | ||||
|  | ||||
|         //        if (deviceIndex >= deviceList.Count) | ||||
|         //        { | ||||
|         //            ThreadSafeLogger.Log($"警告:相机配置'{setting.Name}'无法分配物理设备,因为设备数量不足。"); | ||||
|         //            continue; | ||||
|         //        } | ||||
|  | ||||
|         //        // --- 创建相机实例 --- | ||||
|         //        var cam = new HikvisionCamera { Name = setting.Name }; | ||||
|         //        cam.TriggerMode = setting.TriggerMode; | ||||
|         //        if (!cam.Open(setting)) | ||||
|         //        { | ||||
|         //            ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。"); | ||||
|         //            cam.Dispose(); | ||||
|         //            continue; | ||||
|         //        } | ||||
|         //        // --- 设置触发模式 --- | ||||
|         //        switch (setting.TriggerMode) | ||||
|         //        { | ||||
|         //            case TriggerModeType.Continuous: | ||||
|         //                cam.SetContinuousMode(); | ||||
|         //                break; | ||||
|         //            case TriggerModeType.Software: | ||||
|         //                cam.SetTriggerMode(true); | ||||
|         //                break; | ||||
|         //            case TriggerModeType.Hardware: | ||||
|         //                cam.SetTriggerMode(false); | ||||
|         //                break; | ||||
|         //        } | ||||
|  | ||||
|         //        // --- 订阅事件 --- | ||||
|         //        cam.ImageAcquired += OnCameraImageAcquired; | ||||
|         //        cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}"); | ||||
|  | ||||
|         //        // --- 创建显示窗口 --- | ||||
|         //        var displayForm = new FormImageDisplay { Text = setting.Name, CameraName = setting.Name }; | ||||
|         //        displayForm.OnDisplayEvent += ThreadSafeLogger.Log; | ||||
|         //        displayForm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document); | ||||
|  | ||||
|  | ||||
|         //        ActiveCameras.Add(setting.Name, cam); | ||||
|         //        CameraDisplays.Add(setting.Name, displayForm); | ||||
|         //        mainForm.AddCameraToStatusStrip(setting.Name); | ||||
|         //        ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。"); | ||||
|         //        deviceIndex++; | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。 | ||||
|         /// 这是“启动设备”的第一阶段。 | ||||
|         /// </summary> | ||||
|         public static void PrepareAll(ProcessConfig config, FrmMain mainForm) | ||||
|         { | ||||
|             // 1. 清理旧资源和UI | ||||
|             mainForm.ClearStatusStrip(); | ||||
|             Shutdown(); | ||||
|             ThreadSafeLogger.Log("开始准备设备和模型..."); | ||||
|  | ||||
|             // 2. 初始化检测协调器和AI模型 | ||||
|             // 注意:YoloModelManager 的 Initialize 现在也应在这里被调用,以确保逻辑集中 | ||||
|             YoloModelManager.Initialize(config.ModelSettings); | ||||
|             DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings); | ||||
|  | ||||
|             // 3. 创建相机硬件实例和UI窗口 | ||||
|             var deviceList = new HikvisionCamera().FindDevices(); | ||||
|             if (deviceList.Count == 0) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log("错误:未找到任何相机设备!"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled)) | ||||
|             { | ||||
|                 var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex }; | ||||
|                 cam.TriggerMode = setting.TriggerMode; | ||||
|                 if (!cam.Open(setting)) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。"); | ||||
|                     cam.Dispose(); | ||||
|                     continue; | ||||
|                 } | ||||
|                 // --- 设置触发模式 --- | ||||
|                 switch (setting.TriggerMode) | ||||
|                 { | ||||
|                     case TriggerModeType.Continuous: | ||||
|                         cam.SetContinuousMode(); | ||||
|                         break; | ||||
|                     case TriggerModeType.Software: | ||||
|                         cam.SetTriggerMode(true); | ||||
|                         break; | ||||
|                     case TriggerModeType.Hardware: | ||||
|                         cam.SetTriggerMode(false); | ||||
|                         break; | ||||
|                 } | ||||
|  | ||||
|                 // --- 订阅事件 --- | ||||
|                 cam.ImageAcquired += OnCameraImageAcquired; | ||||
|                 var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex); | ||||
|                 if (processor != null) { processor.OnProcessingCompleted += Processor_OnProcessingCompleted; } | ||||
|  | ||||
|                 // --- 创建【但不显示】图像的UI窗口 --- | ||||
|                 var originalDisplay = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name }; | ||||
|                 var resultDisplay = new FormImageDisplay { Text = $"{setting.Name} - 结果", CameraName = setting.Name }; | ||||
|                 originalDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document); | ||||
|                 resultDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document); | ||||
|  | ||||
|                 // --- 保存引用 --- | ||||
|                 ActiveCameras.Add(setting.Name, cam); | ||||
|                 OriginalImageDisplays.Add(setting.Name, originalDisplay); | ||||
|                 ResultImageDisplays.Add(setting.Name, resultDisplay); | ||||
|                 mainForm.AddCameraToStatusStrip(setting.Name); | ||||
|             } | ||||
|             ThreadSafeLogger.Log("所有设备和模型已准备就绪。"); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 根据配置列表初始化或更新所有相机 | ||||
|         /// </summary> | ||||
|         public static void Initialize(ProcessConfig config, FrmMain mainForm) | ||||
|         { | ||||
|             mainForm?.ClearStatusStrip(); | ||||
|  | ||||
|             // 先停止并释放所有旧的相机 | ||||
|             Shutdown(); | ||||
|             ThreadSafeLogger.Log("开始应用新的相机配置..."); | ||||
|             // 2. 初始化新的检测协调器 | ||||
|             DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings); | ||||
|  | ||||
|             var deviceList = new HikvisionCamera().FindDevices(); | ||||
|             if (deviceList.Count == 0) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log("错误:未找到任何相机设备!"); | ||||
|                 return; | ||||
|             } | ||||
|             int deviceIndex = 0; | ||||
|             foreach (var device in config.ModelSettings.Where(s => s.IsEnabled)) | ||||
|             { | ||||
|                 if (!device.IsEnabled) continue; | ||||
|                 mainForm.AddCameraToStatusStrip(device.Name); | ||||
|             } | ||||
|             foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled)) | ||||
|             { | ||||
|                 if (!setting.IsEnabled) continue; | ||||
|  | ||||
|                 if (deviceIndex >= deviceList.Count) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"警告:相机配置'{setting.Name}'无法分配物理设备,因为设备数量不足。"); | ||||
|                     continue; | ||||
|                 } | ||||
|                 // --- 创建相机实例 --- | ||||
|                 var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex }; | ||||
|                 cam.TriggerMode = setting.TriggerMode; | ||||
|                 if (!cam.Open(setting)) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。"); | ||||
|                     cam.Dispose(); | ||||
|                     continue; | ||||
|                 } | ||||
|                 // --- 设置触发模式 --- | ||||
|                 switch (setting.TriggerMode) | ||||
|                 { | ||||
|                     case TriggerModeType.Continuous: | ||||
|                         cam.SetContinuousMode(); | ||||
|                         break; | ||||
|                     case TriggerModeType.Software: | ||||
|                         cam.SetTriggerMode(true); | ||||
|                         break; | ||||
|                     case TriggerModeType.Hardware: | ||||
|                         cam.SetTriggerMode(false); | ||||
|                         break; | ||||
|                 } | ||||
|  | ||||
|                 // --- 订阅事件 --- | ||||
|                 cam.ImageAcquired += OnCameraImageAcquired; | ||||
|                 cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}"); | ||||
|                 ActiveCameras.Add(setting.Name, cam); | ||||
|                 // --- 创建显示窗口 --- | ||||
|                 var displayForm = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name }; | ||||
|                 var checkFrm = new FormImageDisplay { Text = $"{setting.Name} - 结果", CameraName = setting.Name }; | ||||
|                 displayForm.OnDisplayEvent += ThreadSafeLogger.Log; | ||||
|                 displayForm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document); | ||||
|                 checkFrm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document); | ||||
|  | ||||
|                 OriginalImageDisplays.Add(setting.Name, displayForm); | ||||
|                 ResultImageDisplays.Add(setting.Name, checkFrm); | ||||
|                 mainForm.AddCameraToStatusStrip(setting.Name); | ||||
|                 var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex); | ||||
|                 if (processor != null) | ||||
|                 { | ||||
|                     processor.OnProcessingCompleted += Processor_OnProcessingCompleted; | ||||
|                 } | ||||
|                 ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。"); | ||||
|                 deviceIndex++; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 启动硬件触发模拟器。 | ||||
|         /// </summary> | ||||
|         public static void StartHardwareTriggerSimulator() | ||||
|         { | ||||
|             if (IsHardwareTriggerSimulating) return; | ||||
|  | ||||
|             _hardwareTriggerSimulator.Interval = TriggerInterval; | ||||
|             _hardwareTriggerSimulator.Start(); | ||||
|             IsHardwareTriggerSimulating = true; | ||||
|             ThreadSafeLogger.Log($"硬件触发模拟器已启动,触发间隔: {TriggerInterval} ms。"); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 停止硬件触发模拟器。 | ||||
|         /// </summary> | ||||
|         public static void StopHardwareTriggerSimulator() | ||||
|         { | ||||
|             if (!IsHardwareTriggerSimulating) return; | ||||
|  | ||||
|             _hardwareTriggerSimulator.Stop(); | ||||
|             IsHardwareTriggerSimulating = false; | ||||
|             ThreadSafeLogger.Log("硬件触发模拟器已停止。"); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 定时器触发事件。 | ||||
|         /// </summary> | ||||
|         private static void OnHardwareTriggerTimerElapsed(object sender, ElapsedEventArgs e) | ||||
|         { | ||||
|             // 遍历所有活动的相机 | ||||
|             foreach (var cam in ActiveCameras.Values) | ||||
|             { | ||||
|                 // 仅对配置为“硬件触发”模式的相机执行操作 | ||||
|                 // 重要:我们使用软触发命令(SoftwareTrigger)来“模拟”一个外部硬件信号的到达。 | ||||
|                 if (cam.TriggerMode == TriggerModeType.Software && cam.IsGrabbing) | ||||
|                 { | ||||
|                     // ThreadSafeLogger.Log($"模拟硬触发信号,触发相机: {cam.Name}"); // 如果需要详细日志可以取消注释 | ||||
|                     cam.SoftwareTrigger(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 所有启用的相机开始采集 | ||||
|         /// </summary> | ||||
|         public static void StartAll() | ||||
|         { | ||||
|             foreach (var cam in ActiveCameras.Values) | ||||
|             { | ||||
|                 cam.StartGrabbing(); | ||||
|             } | ||||
|             ThreadSafeLogger.Log("所有相机已开始采集。"); | ||||
|         } | ||||
|  | ||||
|         //public static void StartDetection() | ||||
|         //{ | ||||
|         //    if (!IsDetectionRunning) | ||||
|         //    { | ||||
|         //        IsDetectionRunning = true; | ||||
|         //        ThreadSafeLogger.Log("检测已启动,开始统计数据。"); | ||||
|         //    } | ||||
|         //} | ||||
|         //public static void StopDetection() | ||||
|         //{ | ||||
|         //    if (IsDetectionRunning) | ||||
|         //    { | ||||
|         //        IsDetectionRunning = false; | ||||
|         //        ThreadSafeLogger.Log("检测已停止。"); | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 所有启用的相机停止采集 | ||||
|         /// </summary> | ||||
|         public static void StopAll() | ||||
|         { | ||||
|             foreach (var cam in ActiveCameras.Values) | ||||
|             { | ||||
|                 cam.StopGrabbing(); | ||||
|             } | ||||
|             ThreadSafeLogger.Log("所有相机已停止采集。"); | ||||
|         } | ||||
|  | ||||
|         ///// <summary> | ||||
|         ///// 停止并释放所有相机资源 | ||||
|         ///// </summary> | ||||
|         //public static void Shutdown() | ||||
|         //{ | ||||
|         //    // --- 新增:确保在关闭时停止模拟器并释放资源 --- | ||||
|         //    StopHardwareTriggerSimulator(); | ||||
|         //    //_hardwareTriggerSimulator?.Dispose(); | ||||
|  | ||||
|         //    StopAll(); | ||||
|         //    foreach (var cam in ActiveCameras.Values) | ||||
|         //    { | ||||
|         //        cam.Dispose(); | ||||
|         //    } | ||||
|         //    foreach (var display in CameraDisplays.Values) | ||||
|         //    { | ||||
|         //        display.Close(); | ||||
|         //    } | ||||
|         //    lock (QueueLock) | ||||
|         //    { | ||||
|         //        while (ProductQueue.Count > 0) | ||||
|         //        { | ||||
|         //            ProductQueue.Dequeue()?.Dispose(); | ||||
|         //        } | ||||
|         //    } | ||||
|         //    ResetProductCounter(); | ||||
|         //    ActiveCameras.Clear(); | ||||
|         //    CameraDisplays.Clear(); | ||||
|         //    ThreadSafeLogger.Log("正在关闭文件日志记录器..."); | ||||
|         //    ThreadSafeLogger.Log("所有相机资源已释放。"); | ||||
|  | ||||
|         //} | ||||
|  | ||||
|         // Shutdown 方法也简化 | ||||
|         public static void Shutdown() | ||||
|         { | ||||
|             // 1. 停止硬件和模拟器 | ||||
|             StopAll(); | ||||
|             StopHardwareTriggerSimulator(); | ||||
|             // 2. 关闭相机实例和窗口 | ||||
|             foreach (var cam in ActiveCameras.Values) { cam.Dispose(); } | ||||
|             foreach (var display in OriginalImageDisplays.Values) { display.Close(); } | ||||
|             foreach (var display in ResultImageDisplays.Values) { display.Close(); } | ||||
|             ActiveCameras.Clear(); | ||||
|             OriginalImageDisplays.Clear(); | ||||
|             ResultImageDisplays.Clear(); | ||||
|             // 3. 关闭检测协调器,它会负责清理所有后台线程和队列 | ||||
|             DetectionCoordinator.Shutdown(); | ||||
|  | ||||
|             YoloModelManager.Shutdown(); | ||||
|  | ||||
|             ThreadSafeLogger.Log("所有相机及协调器资源已释放。"); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 重置产品计数器的公共方法 | ||||
|         /// </summary> | ||||
|         public static void ResetProductCounter() | ||||
|         { | ||||
|             lock (_counterLock) | ||||
|             { | ||||
|                 _productCounter = 0; | ||||
|             } | ||||
|             ThreadSafeLogger.Log("产品计数器已重置。"); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 接收到相机图像时的核心处理逻辑 | ||||
|         /// </summary> | ||||
|         //private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp) | ||||
|         //{ | ||||
|  | ||||
|         //    Bitmap bmpForDisplay =null; | ||||
|         //    Bitmap bmpForQueue = null; | ||||
|         //    try | ||||
|         //    { | ||||
|         //        bmpForDisplay?.Dispose(); | ||||
|         //        bmpForQueue?.Dispose(); | ||||
|         //        // 1. 为“显示”和“处理队列”创建独立的深克隆副本 | ||||
|         //        bmpForDisplay = DeepCloneBitmap(bmp, "Display"); | ||||
|         //        bmpForQueue = DeepCloneBitmap(bmp, "Queue"); | ||||
|         //    } | ||||
|         //    finally | ||||
|         //    { | ||||
|         //        // 【关键】无论克隆成功与否,都必须立即释放事件传递过来的原始bmp。 | ||||
|         //        // 这是保证没有泄漏的第一道防线。 | ||||
|         //        bmp?.Dispose(); | ||||
|         //    } | ||||
|         //    // --- 现在我们使用完全独立的副本进行后续操作 --- | ||||
|  | ||||
|         //    // 4. 将显示副本传递给UI | ||||
|         //    if (bmpForDisplay != null && CameraDisplays.TryGetValue(sender.Name, out var display)) | ||||
|         //    { | ||||
|         //        display.UpdateImage(bmpForDisplay); | ||||
|         //    } | ||||
|         //    else | ||||
|         //    { | ||||
|         //        // 如果不需要显示,或者显示失败,必须释放掉为它创建的副本 | ||||
|         //        bmpForDisplay?.Dispose(); | ||||
|         //    } | ||||
|         //    // 5. 将队列副本添加到产品中 | ||||
|         //    if (bmpForQueue != null) | ||||
|         //    { | ||||
|         //        lock (QueueLock) | ||||
|         //        { | ||||
|         //            ProductResult currentProduct; | ||||
|         //            bool isNewProductCycle = ProductQueue.Count == 0 || ProductQueue.Last().CapturedImages.ContainsKey(sender.Name); | ||||
|         //            if (isNewProductCycle) | ||||
|         //            { | ||||
|         //                if (ProductQueue.Count > 0 && !ProductQueue.Peek().IsComplete(EnabledCameraCount)) | ||||
|         //                { | ||||
|         //                    var orphanedProduct = ProductQueue.Dequeue(); // 从队列头部移除这个不完整的产品 | ||||
|         //                    ThreadSafeLogger.Log($"[警告] 产品 #{orphanedProduct.ProductID} 未能集齐所有相机图像而被丢弃,以防止内存泄漏。"); | ||||
|         //                    orphanedProduct.Dispose(); // 确保释放其占用的所有资源 | ||||
|         //                } | ||||
|         //                long newProductId; | ||||
|         //                lock (_counterLock) | ||||
|         //                { | ||||
|         //                    _productCounter++; | ||||
|         //                    newProductId = _productCounter; | ||||
|         //                } | ||||
|         //                currentProduct = new ProductResult(newProductId); | ||||
|         //                ProductQueue.Enqueue(currentProduct); | ||||
|         //            } | ||||
|         //            else | ||||
|         //            { | ||||
|         //                currentProduct = ProductQueue.Last(); | ||||
|         //            } | ||||
|  | ||||
|         //            currentProduct.AddImage(sender.Name, bmpForQueue); // bmpForQueue的所有权转移给了产品队列 | ||||
|  | ||||
|         //            while (ProductQueue.Count > 0 && ProductQueue.Peek().IsComplete(EnabledCameraCount)) | ||||
|         //            { | ||||
|         //                // Peek() 查看队头元素但不移除它 | ||||
|         //                var finishedProduct = ProductQueue.Dequeue(); // Dequeue() 移除队头元素 | ||||
|         //                ThreadSafeLogger.Log($"产品 #{finishedProduct.ProductID} 已完整,出队处理。"); | ||||
|  | ||||
|         //                // --- 这是您已有的处理逻辑 --- | ||||
|         //                bool isOk = new Random().NextDouble() > 0.1; | ||||
|         //                string finalResult = isOk ? "OK" : "NG"; | ||||
|         //                ThreadSafeLogger.Log($"产品 #{finishedProduct.ProductID} 检测结果: {finalResult}"); | ||||
|  | ||||
|         //                if (IsDetectionRunning) | ||||
|         //                { | ||||
|         //                    OnDetectionCompleted?.Invoke(null, new DetectionResultEventArgs(isOk)); | ||||
|         //                } | ||||
|  | ||||
|         //                try | ||||
|         //                { | ||||
|         //                    // 确保完成的产品被完全释放 | ||||
|         //                    finishedProduct.Dispose(); | ||||
|         //                } | ||||
|         //                catch (Exception ex) | ||||
|         //                { | ||||
|         //                    ThreadSafeLogger.Log($"[ERROR] 释放产品 #{finishedProduct.ProductID} 资源时出错: {ex.Message}"); | ||||
|         //                } | ||||
|         //            } | ||||
|         //        } | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|         // 图像回调方法现在极其简单 | ||||
|         private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp) | ||||
|         { | ||||
|             Bitmap bmpForDisplay = null; | ||||
|             Bitmap bmpForProcessing = null; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 // 1. 为“显示”和“处理”创建两个完全独立的深克隆副本 | ||||
|                 bmpForDisplay = DeepCloneBitmap(bmp, "Display"); | ||||
|                 bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 // 2.无论克隆成功与否,都必须立即释放事件传递过来的原始bmp,防止泄漏。 | ||||
|                 bmp?.Dispose(); | ||||
|             } | ||||
|             // 分支 A: 将用于显示的副本发送到对应的UI窗口 | ||||
|             if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow)) | ||||
|             { | ||||
|                 // displayWindow.UpdateImage 会处理线程安全问题 | ||||
|                 displayWindow.UpdateImage(bmpForDisplay); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本 | ||||
|                 bmpForDisplay?.Dispose(); | ||||
|             } | ||||
|             // 分支 B: 将用于处理的副本发送到检测协调器的后台队列 | ||||
|             if (bmpForProcessing != null) | ||||
|             { | ||||
|                 // bmpForProcessing 的所有权在这里被转移给了协调器 | ||||
|                 DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing); | ||||
|             } | ||||
|             //// 深度克隆图像以确保线程安全 | ||||
|             //Bitmap bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); | ||||
|             //bmp?.Dispose(); // 立即释放原始图 | ||||
|  | ||||
|             //// 直接将图像和相机编号交给协调器,无需任何本地处理 | ||||
|             //DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing); | ||||
|         } | ||||
|         // 事件处理器 | ||||
|         private static void Processor_OnProcessingCompleted(object sender, ProcessingCompletedEventArgs e) | ||||
|         { | ||||
|  | ||||
|             // 1. 找到与此相机匹配的相机名称 | ||||
|             var cameraEntry = ActiveCameras.FirstOrDefault(kvp => kvp.Value.CameraIndex == e.CameraIndex); | ||||
|             if (cameraEntry.Key == null) | ||||
|             { | ||||
|                 e.Dispose(); // 如果找不到接收者,必须释放事件参数中的图像 | ||||
|                 return; | ||||
|             } | ||||
|             // 2. 找到此相机的结果显示窗口 | ||||
|             if (ResultImageDisplays.TryGetValue(cameraEntry.Key, out var resultDisplay)) | ||||
|             { | ||||
|                 var bmp = ConvertSKImageToBitmap(e.ResultImage); | ||||
|                 if (bmp != null) | ||||
|                 { | ||||
|                     // UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可 | ||||
|                     resultDisplay.UpdateImage(bmp); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 如果找到了相机但没有对应的结果窗口,也要释放图像 | ||||
|                 e.Dispose(); | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 【将 SkiaSharp.SKImage 安全地转换为 System.Drawing.Bitmap。 | ||||
|         /// </summary> | ||||
|         private static Bitmap ConvertSKImageToBitmap(SKImage skImage) | ||||
|         { | ||||
|             if (skImage == null) return null; | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 // SKImage -> SKBitmap -> System.Drawing.Bitmap | ||||
|                 using (var skBitmap = SKBitmap.FromImage(skImage)) | ||||
|                 { | ||||
|                     // SKBitmap.ToBitmap() 会创建一个新的 Bitmap 对象 | ||||
|                     return SKBitmapToGdiBitmapFast(skBitmap); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] SKImage to Bitmap 转换失败: {ex.Message}"); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|         public static Bitmap SKBitmapToGdiBitmapFast(SKBitmap skBitmap) | ||||
|         { | ||||
|             if (skBitmap == null) throw new ArgumentNullException(nameof(skBitmap)); | ||||
|             if (skBitmap.ColorType != SKColorType.Bgra8888 || skBitmap.AlphaType != SKAlphaType.Premul) | ||||
|                 throw new ArgumentException("skBitmap must be Bgra8888 + Premul for the fast path."); | ||||
|  | ||||
|             int w = skBitmap.Width; | ||||
|             int h = skBitmap.Height; | ||||
|  | ||||
|             Bitmap bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb); | ||||
|             var rect = new Rectangle(0, 0, w, h); | ||||
|             var bmpData = bmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); | ||||
|             try | ||||
|             { | ||||
|                 IntPtr srcPtr = skBitmap.GetPixels(); | ||||
|                 int srcRowBytes = skBitmap.RowBytes; | ||||
|                 int dstRowBytes = bmpData.Stride; | ||||
|                 int copyBytesPerRow = Math.Min(srcRowBytes, dstRowBytes); | ||||
|  | ||||
|                 byte[] row = new byte[copyBytesPerRow]; // 复用同一行缓冲区,避免每行分配 | ||||
|                 for (int y = 0; y < h; y++) | ||||
|                 { | ||||
|                     IntPtr s = IntPtr.Add(srcPtr, y * srcRowBytes); | ||||
|                     IntPtr d = IntPtr.Add(bmpData.Scan0, y * dstRowBytes); | ||||
|  | ||||
|                     Marshal.Copy(s, row, 0, copyBytesPerRow); | ||||
|                     Marshal.Copy(row, 0, d, copyBytesPerRow); | ||||
|                 } | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 bmp.UnlockBits(bmpData); | ||||
|             } | ||||
|  | ||||
|             return bmp; | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 对Bitmap进行真正的深度克隆,确保内存完全独立。 | ||||
|         /// 这是解决跨线程GDI+问题的最可靠方法。 | ||||
|         /// </summary> | ||||
|         /// <param name="source">源Bitmap对象。</param> | ||||
|         /// <returns>一个与源图像在内存上完全独立的全新Bitmap对象。</returns> | ||||
|         private static Bitmap DeepCloneBitmap(Bitmap source, string cloneFor) | ||||
|         { | ||||
|             if (source == null) return null; | ||||
|  | ||||
|             // 创建一个新的Bitmap对象,具有与源相同的尺寸和像素格式 | ||||
|             Bitmap clone = new Bitmap(source.Width, source.Height, source.PixelFormat); | ||||
|  | ||||
|             // 如果有调色板,复制调色板 | ||||
|             if (source.Palette.Entries.Length > 0) | ||||
|             { | ||||
|                 clone.Palette = source.Palette; | ||||
|             } | ||||
|  | ||||
|             // 锁定源和目标Bitmap的内存区域 | ||||
|             var rect = new Rectangle(0, 0, source.Width, source.Height); | ||||
|             BitmapData sourceData = null; //source.LockBits(rect, ImageLockMode.ReadOnly, source.PixelFormat); | ||||
|             BitmapData cloneData = null;//clone.LockBits(rect, ImageLockMode.WriteOnly, clone.PixelFormat); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 sourceData = source.LockBits(rect, ImageLockMode.ReadOnly, source.PixelFormat); | ||||
|                 cloneData = clone.LockBits(rect, ImageLockMode.WriteOnly, clone.PixelFormat); | ||||
|                 // 计算需要拷贝的字节数 | ||||
|                 int byteCount = Math.Abs(sourceData.Stride) * source.Height; | ||||
|                 byte[] buffer = new byte[byteCount]; | ||||
|  | ||||
|                 // 从源图像拷贝数据到字节数组 | ||||
|                 Marshal.Copy(sourceData.Scan0, buffer, 0, byteCount); | ||||
|  | ||||
|                 // 从字节数组拷贝数据到目标图像 | ||||
|                 Marshal.Copy(buffer, 0, cloneData.Scan0, byteCount); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[克隆错误] 在 DeepCloneBitmap ({cloneFor}) 中发生异常: {ex.Message}"); | ||||
|                 // 如果发生错误,确保返回null,并且释放可能已经创建的clone对象 | ||||
|                 clone?.Dispose(); | ||||
|                 return null; | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 // 确保即使在发生错误时也能尝试解锁 | ||||
|                 if (sourceData != null) | ||||
|                 { | ||||
|                     source.UnlockBits(sourceData); | ||||
|                 } | ||||
|                 if (cloneData != null) | ||||
|                 { | ||||
|                     clone.UnlockBits(cloneData); | ||||
|                 } | ||||
|                 //ThreadSafeLogger.Log($"[克隆完成] 解锁并完成克隆 ({cloneFor})."); | ||||
|             } | ||||
|  | ||||
|             return clone; | ||||
|         } | ||||
|  | ||||
|         // 触发日志事件的辅助方法 | ||||
|         //public static void Log(string message) | ||||
|         //{ | ||||
|         //    string formattedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}"; | ||||
|  | ||||
|         //    // 1. 触发事件,更新UI | ||||
|         //    OnLogMessage?.Invoke(formattedMessage); | ||||
|  | ||||
|         //    // 2. 【关键修改】在写入文件前,先获取锁。 | ||||
|         //    // lock 语句块确保了花括号内的代码在同一时间只能被一个线程执行。 | ||||
|         //    // 如果另一个线程也想执行这段代码,它必须等待前一个线程执行完毕并释放锁。 | ||||
|         //    lock (_logLock) | ||||
|         //    { | ||||
|         //        try | ||||
|         //        { | ||||
|         //            _logFileWriter?.WriteLine(formattedMessage); | ||||
|         //        } | ||||
|         //        catch (Exception) | ||||
|         //        { | ||||
|         //            // 即使有锁,也保留try-catch以防万一(如磁盘满了等IO问题)。 | ||||
|         //            // 可以在这里决定是否要在UI上显示警告。 | ||||
|         //        } | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										211
									
								
								Check.Main/Camera/CameraProcessor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								Check.Main/Camera/CameraProcessor.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,211 @@ | ||||
| using Check.Main.Common; | ||||
| using Check.Main.Infer; | ||||
| using SkiaSharp; | ||||
| using System; | ||||
| using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | ||||
| using System.Drawing.Imaging; | ||||
| using System.Linq; | ||||
| using System.Runtime.InteropServices; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using YoloDotNet.Extensions; | ||||
|  | ||||
| namespace Check.Main.Camera | ||||
| { | ||||
|     public class CameraProcessor : IDisposable | ||||
|     { | ||||
|         private readonly int _cameraIndex; | ||||
|         private readonly int _modeId; | ||||
|         // private readonly ModelSettings _model; | ||||
|         private readonly BlockingCollection<ImageData> _imageQueue = new BlockingCollection<ImageData>(); | ||||
|         private readonly Thread _workerThread; | ||||
|         private volatile bool _isRunning = false; | ||||
|         private long _imageCounter = 0; | ||||
|         private readonly object _counterLock = new object(); // 用于线程安全地重置计数器 | ||||
|  | ||||
|         public event EventHandler<ProcessingCompletedEventArgs> OnProcessingCompleted; | ||||
|  | ||||
|  | ||||
|         public CameraProcessor(int cameraIndex, int modelId)//, ModelSettings model | ||||
|         { | ||||
|             _cameraIndex = cameraIndex; | ||||
|             _modeId = modelId; | ||||
|             //_model = model; | ||||
|             _workerThread = new Thread(ProcessQueue) { IsBackground = true, Name = $"Cam_{_cameraIndex}_Processor" }; | ||||
|         } | ||||
|  | ||||
|         public void Start() | ||||
|         { | ||||
|             _isRunning = true; | ||||
|             _workerThread.Start(); | ||||
|         } | ||||
|  | ||||
|         public void EnqueueImage(Bitmap bmp) | ||||
|         { | ||||
|             if (!_isRunning) | ||||
|             { | ||||
|                 bmp?.Dispose(); | ||||
|                 return; | ||||
|             } | ||||
|             _imageCounter++; | ||||
|             _imageQueue.Add(new ImageData(_imageCounter, _cameraIndex, bmp)); | ||||
|         } | ||||
|  | ||||
|         private void ProcessQueue() | ||||
|         { | ||||
|             // 从模型管理器获取此线程专属的YOLO模型 | ||||
|             var yoloModel = YoloModelManager.GetModel(_modeId); | ||||
|             if (yoloModel == null) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型,处理线程已中止。"); | ||||
|                 return; // 如果没有模型,此线程无法工作 | ||||
|             } | ||||
|             while (_isRunning) | ||||
|             { | ||||
|                 try | ||||
|                 { | ||||
|                     // 阻塞式地从队列中取出图像,如果队列为空则等待 | ||||
|                     ImageData data = _imageQueue.Take(); | ||||
|                     using (data) | ||||
|                     { | ||||
|                         //SKImage resultSkImage = null; // 用于存储最终绘制好结果的图像 | ||||
|  | ||||
|                         using (var skImage = ConvertBitmapToSKImage(data.Image)) // 转换图像格式并管理其生命周期 | ||||
|                         { | ||||
|                             if (skImage == null) continue; | ||||
|                             var predictions = yoloModel.RunObjectDetection(skImage); | ||||
|                             // 模拟模型处理 | ||||
|                             //Thread.Sleep(50); // 模拟耗时 | ||||
|                             //bool isOk = new Random().NextDouble() > 0.1; | ||||
|                             //string result = isOk ? "OK" : "NG"; | ||||
|  | ||||
|                             string result = predictions.Any() ? "NG" : "OK"; | ||||
|                             ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},检测到 {predictions.Count} 个目标,结果: {result}"); | ||||
|                             // 将处理结果交给协调器进行组装 | ||||
|                             DetectionCoordinator.AssembleProduct(data, result); | ||||
|  | ||||
|                             if (OnProcessingCompleted != null) | ||||
|                             { | ||||
|                                 using (var resultSkImage = skImage.Draw(predictions)) | ||||
|                                 { | ||||
|                                     // 4. 触发事件,将绘制好的 resultSkImage 传递出去 | ||||
|                                     // 所有权在这里被转移 | ||||
|                                     OnProcessingCompleted?.Invoke(this, new ProcessingCompletedEventArgs( | ||||
|                                         _cameraIndex, | ||||
|                                         data.ProductId, | ||||
|                                         resultSkImage | ||||
|                                     )); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                     } | ||||
|                 } | ||||
|                 catch (InvalidOperationException) | ||||
|                 { | ||||
|                     // 当调用 Stop 时,会 CompleteAdding 队列,Take 会抛出此异常,是正常退出流程 | ||||
|                     break; | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[ERROR] 相机 #{_cameraIndex} 处理线程异常: {ex.Message}"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 将 System.Drawing.Bitmap 安全地转换为 SkiaSharp.SKImage。 | ||||
|         /// </summary> | ||||
|         private SKImage ConvertBitmapToSKImage(Bitmap bitmap) | ||||
|         { | ||||
|             if (bitmap == null) return null; | ||||
|             try | ||||
|             { | ||||
|                 // 使用 using 确保 SKBitmap 被正确释放 | ||||
|                 using (var skBitmap = ToSKBitmapFast(bitmap)) | ||||
|                 { | ||||
|                     return SKImage.FromBitmap(skBitmap); | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] Bitmap to SKImage 转换失败: {ex.Message}"); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|         public static SKBitmap ToSKBitmapFast(Bitmap bitmap) | ||||
|         { | ||||
|             // 确保是 32bppArgb(BGRA 内存布局) | ||||
|             Bitmap src = bitmap; | ||||
|             if (bitmap.PixelFormat != PixelFormat.Format32bppArgb) | ||||
|             { | ||||
|                 src = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb); | ||||
|                 using (Graphics g = Graphics.FromImage(src)) | ||||
|                 { | ||||
|                     g.DrawImage(bitmap, 0, 0, bitmap.Width, bitmap.Height); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             var rect = new Rectangle(0, 0, src.Width, src.Height); | ||||
|             var bmpData = src.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); | ||||
|             try | ||||
|             { | ||||
|                 var info = new SKImageInfo(src.Width, src.Height, SKColorType.Bgra8888, SKAlphaType.Premul); | ||||
|                 var skBitmap = new SKBitmap(info); | ||||
|  | ||||
|                 IntPtr destPtr = skBitmap.GetPixels();             // 目标内存 | ||||
|                 IntPtr srcRowPtr = bmpData.Scan0;                  // 源行首 | ||||
|                 int srcStride = bmpData.Stride; | ||||
|                 int destRowBytes = skBitmap.RowBytes; | ||||
|                 int copyBytesPerRow = Math.Min(srcStride, destRowBytes); | ||||
|  | ||||
|                 // 使用一次分配的缓冲区并用 Marshal.Copy 行拷贝(不分配每行) | ||||
|                 byte[] row = new byte[copyBytesPerRow]; | ||||
|                 for (int y = 0; y < src.Height; y++) | ||||
|                 { | ||||
|                     IntPtr s = IntPtr.Add(bmpData.Scan0, y * srcStride); | ||||
|                     IntPtr d = IntPtr.Add(destPtr, y * destRowBytes); | ||||
|  | ||||
|                     Marshal.Copy(s, row, 0, copyBytesPerRow); | ||||
|                     Marshal.Copy(row, 0, d, copyBytesPerRow); | ||||
|                 } | ||||
|  | ||||
|                 return skBitmap; | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 src.UnlockBits(bmpData); | ||||
|                 if (!ReferenceEquals(src, bitmap)) | ||||
|                 { | ||||
|                     src.Dispose(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         public void Stop() | ||||
|         { | ||||
|             _isRunning = false; | ||||
|             // 解除阻塞,让线程可以检查 _isRunning 标志并退出 | ||||
|             _imageQueue.CompleteAdding(); | ||||
|             _workerThread.Join(500); // 等待线程结束 | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 线程安全地重置该相机的图像计数器。 | ||||
|         /// </summary> | ||||
|         public void ResetCounter() | ||||
|         { | ||||
|             lock (_counterLock) | ||||
|             { | ||||
|                 _imageCounter = 0; | ||||
|             } | ||||
|         } | ||||
|         // 别忘了在 DetectionCoordinator 中添加一个辅助方法来获取处理器 | ||||
|  | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             Stop(); | ||||
|             _imageQueue.Dispose(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										182
									
								
								Check.Main/Camera/CameraSettings.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								Check.Main/Camera/CameraSettings.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| using Check.Main.Common; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| using System.Drawing.Design; | ||||
| using System.Linq; | ||||
| using System.Runtime.CompilerServices; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms.Design; | ||||
|  | ||||
| namespace Check.Main.Camera | ||||
| { | ||||
|     // 相机触发方式枚举 | ||||
|     public enum TriggerModeType | ||||
|     { | ||||
|         [Description("连续采集")] | ||||
|         Continuous, | ||||
|         [Description("软件触发")] | ||||
|         Software, | ||||
|         [Description("硬件触发")] | ||||
|         Hardware | ||||
|     } | ||||
|     public enum CheckType | ||||
|     { | ||||
|         [Description("传统算法")] | ||||
|         Traditional, | ||||
|         [Description("深度学习")] | ||||
|         DeepLearning | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 相机配置信息类,用于PropertyGrid显示和编辑 | ||||
|     /// </summary> | ||||
|     public class CameraSettings : INotifyPropertyChanged, ICloneable | ||||
|     { | ||||
|         // 1. 实现 INotifyPropertyChanged 接口所要求的事件 | ||||
|         public event PropertyChangedEventHandler PropertyChanged; | ||||
|  | ||||
|         private int _cameraIndex = 0; | ||||
|         private string _name = "Camera-1"; | ||||
|         private string _ipAddress = "192.168.1.100"; | ||||
|         private string _ipDeviceAddress = "192.168.1.101"; | ||||
|         private TriggerModeType _triggerMode = TriggerModeType.Continuous; | ||||
|         private bool _isEnabled = true; | ||||
|         private CheckType _checkType = CheckType.DeepLearning; | ||||
|         private int _modelID = 0; | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("相机编号"), Description("相机的唯一标识符,用于与模型编号对应。")] | ||||
|         public int CameraIndex | ||||
|         { | ||||
|             get => _cameraIndex; | ||||
|             set | ||||
|             { | ||||
|                 if (_cameraIndex != value) | ||||
|                 { | ||||
|                     _cameraIndex = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         [Category("基本信息"), DisplayName("相机名称"), Description("给相机起一个唯一的别名")] | ||||
|         public string Name | ||||
|         { | ||||
|             get => _name; | ||||
|             set | ||||
|             { | ||||
|                 // 2. 在setter中检查值是否真的改变了 | ||||
|                 if (_name != value) | ||||
|                 { | ||||
|                     _name = value; | ||||
|                     // 3. 如果改变了,就调用通知方法,广播“Name”属性已变更的消息 | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("网络信息"), DisplayName("相机IP地址"), Description("相机的IP地址(GigE相机需要)")] | ||||
|         public string IPAddress | ||||
|         { | ||||
|             get => _ipAddress; | ||||
|             set | ||||
|             { | ||||
|                 if (_ipAddress != value) | ||||
|                 { | ||||
|                     _ipAddress = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("网络信息"), DisplayName("上位机IP地址"), Description("相机对应的上位机的IP地址(GigE相机需要)")] | ||||
|         public string IPDeviceAddress | ||||
|         { | ||||
|             get => _ipDeviceAddress; | ||||
|             set | ||||
|             { | ||||
|                 if (_ipDeviceAddress != value) | ||||
|                 { | ||||
|                     _ipDeviceAddress = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("采集控制"), DisplayName("触发模式"), Description("设置相机的图像采集触发方式")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
|         public TriggerModeType TriggerMode | ||||
|         { | ||||
|             get => _triggerMode; | ||||
|             set | ||||
|             { | ||||
|                 if (_triggerMode != value) | ||||
|                 { | ||||
|                     _triggerMode = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("采集控制"), DisplayName("是否启用"), Description("是否在程序启动时初始化并使用此相机")] | ||||
|         public bool IsEnabled | ||||
|         { | ||||
|             get => _isEnabled; | ||||
|             set | ||||
|             { | ||||
|                 if (_isEnabled != value) | ||||
|                 { | ||||
|                     _isEnabled = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("检测配置"), DisplayName("检测类型"), Description("使用传统算法或深度学习")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
|         public CheckType CheckType | ||||
|         { | ||||
|             get => _checkType; | ||||
|             set | ||||
|             { | ||||
|                 if (_checkType != value) | ||||
|                 { | ||||
|                     _checkType = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("模型编号"), Description("相机调用的模型编号")] | ||||
|         [TypeConverter(typeof(ModelSelectionConverter))] | ||||
|         public int ModelID | ||||
|         { | ||||
|             get => _modelID; | ||||
|             set | ||||
|             { | ||||
|                 if (_modelID != value) | ||||
|                 { | ||||
|                     _modelID = value; | ||||
|                     OnPropertyChanged(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 4. 触发 PropertyChanged 事件的辅助方法 | ||||
|         /// [CallerMemberName] 特性是一个“语法糖”,它会自动获取调用此方法的属性的名称。 | ||||
|         /// 例如,在Name属性的setter中调用OnPropertyChanged(),C#编译器会自动传入"Name"作为参数。 | ||||
|         /// </summary> | ||||
|         protected void OnPropertyChanged([CallerMemberName] string propertyName = null) | ||||
|         { | ||||
|             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||||
|         } | ||||
|         public object Clone() | ||||
|         { | ||||
|             // MemberwiseClone() 会创建一个新对象,并将当前对象的所有非静态字段的值 | ||||
|             // 复制到新对象中。对于值类型,这是值的拷贝;对于引用类型,这是引用的拷贝。 | ||||
|             // 在本类中,这已经足够了。 | ||||
|             return this.MemberwiseClone(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										502
									
								
								Check.Main/Camera/HikvisionCamera.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										502
									
								
								Check.Main/Camera/HikvisionCamera.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,502 @@ | ||||
| using MvCamCtrl.NET; | ||||
| using Sunny.UI; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Drawing; | ||||
| using System.Drawing.Imaging; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Runtime.InteropServices; | ||||
| using System.Text; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Camera | ||||
| { | ||||
|     public class HikvisionCamera : IDisposable | ||||
|     { | ||||
|         [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)] | ||||
|         private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); | ||||
|         // 事件委托 | ||||
|         public delegate void ImageAcquiredEventHandler(HikvisionCamera sender, Bitmap bmp); | ||||
|         // public delegate void ImageAcquiredEventHandler(object sender, Bitmap bmp); | ||||
|         public delegate void CameraMessageEventHandler(object sender, string message, int errorCode = 0); | ||||
|  | ||||
|         // 事件 | ||||
|         public event ImageAcquiredEventHandler ImageAcquired; | ||||
|         public event CameraMessageEventHandler CameraMessage; | ||||
|  | ||||
|         // 私有成员变量 | ||||
|         private MyCamera m_MyCamera; | ||||
|         private MyCamera.MV_CC_DEVICE_INFO_LIST m_stDeviceList; | ||||
|         private bool m_bGrabbing = false; | ||||
|         private Thread m_hReceiveThread = null; | ||||
|  | ||||
|         private Bitmap m_bitmap = null; | ||||
|         private IntPtr m_ConvertDstBuf = IntPtr.Zero; | ||||
|         private uint m_nConvertDstBufLen = 0; | ||||
|         public string Name { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 由外部逻辑分配的、唯一的相机软件编号。 | ||||
|         /// 主要用作图像路由和与逻辑处理器匹配的键。 | ||||
|         /// </summary> | ||||
|         public int CameraIndex { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 相机是否已经打开 | ||||
|         /// </summary> | ||||
|         public bool IsOpen { get; private set; } = false; | ||||
|  | ||||
|         public TriggerModeType TriggerMode { get; internal set; } // internal set 保证只有 CameraManager 能设置它 | ||||
|  | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 相机是否正在采集 | ||||
|         /// </summary> | ||||
|         public bool IsGrabbing => m_bGrabbing; | ||||
|  | ||||
|         public HikvisionCamera() | ||||
|         { | ||||
|             // 初始化SDK | ||||
|             MyCamera.MV_CC_Initialize_NET(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 查找所有可用的相机设备 | ||||
|         /// </summary> | ||||
|         /// <returns>设备名称列表</returns> | ||||
|         public List<string> FindDevices() | ||||
|         { | ||||
|             var deviceList = new List<string>(); | ||||
|             m_stDeviceList = new MyCamera.MV_CC_DEVICE_INFO_LIST(); | ||||
|             int nRet = MyCamera.MV_CC_EnumDevices_NET(MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE, ref m_stDeviceList); | ||||
|             if (nRet != MyCamera.MV_OK) | ||||
|             { | ||||
|                 OnCameraMessage("设备枚举失败!", nRet); | ||||
|                 return deviceList; | ||||
|             } | ||||
|  | ||||
|             for (int i = 0; i < m_stDeviceList.nDeviceNum; i++) | ||||
|             { | ||||
|                 var device = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(m_stDeviceList.pDeviceInfo[i], typeof(MyCamera.MV_CC_DEVICE_INFO)); | ||||
|                 if (device.nTLayerType == MyCamera.MV_GIGE_DEVICE) | ||||
|                 { | ||||
|                     var gigeInfo = (MyCamera.MV_GIGE_DEVICE_INFO_EX)MyCamera.ByteToStruct(device.SpecialInfo.stGigEInfo, typeof(MyCamera.MV_GIGE_DEVICE_INFO_EX)); | ||||
|                     if (!string.IsNullOrEmpty(Encoding.Default.GetString(gigeInfo.chUserDefinedName).TrimEnd('\0'))) | ||||
|                         deviceList.Add("GEV: " + gigeInfo.chUserDefinedName + " (" + gigeInfo.chSerialNumber + ")"); | ||||
|                     else | ||||
|                         deviceList.Add("GEV: " + gigeInfo.chManufacturerName + " " + gigeInfo.chModelName + " (" + gigeInfo.chSerialNumber + ")"); | ||||
|                 } | ||||
|                 else if (device.nTLayerType == MyCamera.MV_USB_DEVICE) | ||||
|                 { | ||||
|                     var usbInfo = (MyCamera.MV_USB3_DEVICE_INFO_EX)MyCamera.ByteToStruct(device.SpecialInfo.stUsb3VInfo, typeof(MyCamera.MV_USB3_DEVICE_INFO_EX)); | ||||
|                     if (!string.IsNullOrEmpty(Encoding.Default.GetString(usbInfo.chUserDefinedName).TrimEnd('\0'))) | ||||
|                         deviceList.Add("U3V: " + usbInfo.chUserDefinedName + " (" + usbInfo.chSerialNumber + ")"); | ||||
|                     else | ||||
|                         deviceList.Add("U3V: " + usbInfo.chManufacturerName + " " + usbInfo.chModelName + " (" + usbInfo.chSerialNumber + ")"); | ||||
|                 } | ||||
|             } | ||||
|             return deviceList; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 根据索引打开相机 | ||||
|         /// </summary> | ||||
|         /// <param name="index">设备索引</param> | ||||
|         /// <returns>成功返回true,否则返回false</returns> | ||||
|         public bool Open(CameraSettings camConfig) | ||||
|         { | ||||
|             string ipCam = ""; | ||||
|             string ipDevice = ""; | ||||
|             FindDevices(); | ||||
|             if (camConfig.IsEnabled == true) | ||||
|             { | ||||
|                 ipCam = camConfig.IPAddress; | ||||
|                 ipDevice = camConfig.IPDeviceAddress; | ||||
|             } | ||||
|             try | ||||
|             { | ||||
|                 if (m_stDeviceList.nDeviceNum == 0 || string.IsNullOrEmpty(camConfig.IPAddress)) | ||||
|                 { | ||||
|                     OnCameraMessage("没有找到设备或索引无效。", -1); | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 if (IsOpen) | ||||
|                 { | ||||
|                     OnCameraMessage("相机已经打开。", 0); | ||||
|                     return true; | ||||
|                 } | ||||
|                 MyCamera.MV_CC_DEVICE_INFO device = new MyCamera.MV_CC_DEVICE_INFO(); | ||||
|                 device.nTLayerType = MyCamera.MV_GIGE_DEVICE; | ||||
|                 MyCamera.MV_GIGE_DEVICE_INFO stGigEDev = new MyCamera.MV_GIGE_DEVICE_INFO(); | ||||
|                 var parts = ipCam.Split('.'); | ||||
|                 int nIp1 = Convert.ToInt32(parts[0]); | ||||
|                 int nIp2 = Convert.ToInt32(parts[1]); | ||||
|                 int nIp3 = Convert.ToInt32(parts[2]); | ||||
|                 int nIp4 = Convert.ToInt32(parts[3]); | ||||
|                 stGigEDev.nCurrentIp = (uint)((nIp1 << 24) | (nIp2 << 16) | (nIp3 << 8) | nIp4); | ||||
|  | ||||
|                 parts = ipDevice.Split('.'); | ||||
|                 nIp1 = Convert.ToInt32(parts[0]); | ||||
|                 nIp2 = Convert.ToInt32(parts[1]); | ||||
|                 nIp3 = Convert.ToInt32(parts[2]); | ||||
|                 nIp4 = Convert.ToInt32(parts[3]); | ||||
|                 stGigEDev.nNetExport = (uint)((nIp1 << 24) | (nIp2 << 16) | (nIp3 << 8) | nIp4); | ||||
|                 IntPtr stGigeInfoPtr = Marshal.AllocHGlobal(216); | ||||
|                 Marshal.StructureToPtr(stGigEDev, stGigeInfoPtr, false); | ||||
|                 device.SpecialInfo.stGigEInfo = new Byte[540]; | ||||
|                 Marshal.Copy(stGigeInfoPtr, device.SpecialInfo.stGigEInfo, 0, 540); | ||||
|                 //释放内存空间 | ||||
|                 Marshal.FreeHGlobal(stGigeInfoPtr); | ||||
|  | ||||
|                 //var device = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(m_stDeviceList.pDeviceInfo[index], typeof(MyCamera.MV_CC_DEVICE_INFO)); | ||||
|                 m_MyCamera = new MyCamera(); | ||||
|                 int nRet = m_MyCamera.MV_CC_CreateDevice_NET(ref device); | ||||
|                 if (nRet != MyCamera.MV_OK) | ||||
|                 { | ||||
|                     OnCameraMessage("创建设备句柄失败!", nRet); | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 nRet = m_MyCamera.MV_CC_OpenDevice_NET(); | ||||
|                 if (nRet != MyCamera.MV_OK) | ||||
|                 { | ||||
|                     OnCameraMessage("打开设备失败!", nRet); | ||||
|                     m_MyCamera.MV_CC_DestroyDevice_NET(); | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 // 探测网络最佳包大小(只对GigE相机有效) | ||||
|                 if (device.nTLayerType == MyCamera.MV_GIGE_DEVICE) | ||||
|                 { | ||||
|                     int nPacketSize = m_MyCamera.MV_CC_GetOptimalPacketSize_NET(); | ||||
|                     if (nPacketSize > 0) | ||||
|                     { | ||||
|                         m_MyCamera.MV_CC_SetIntValueEx_NET("GevSCPSPacketSize", nPacketSize); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // 默认设置为连续模式 | ||||
|                 SetContinuousMode(); | ||||
|  | ||||
|                 IsOpen = true; | ||||
|                 OnCameraMessage("相机打开成功。", 0); | ||||
|                 return true; | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 OnCameraMessage("相机打开失败。"+ ex.ToString(), 0); | ||||
|                 return false; | ||||
|             } | ||||
|              | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 关闭相机 | ||||
|         /// </summary> | ||||
|         public void Close() | ||||
|         { | ||||
|             if (!IsOpen) return; | ||||
|  | ||||
|             if (m_bGrabbing) | ||||
|             { | ||||
|                 StopGrabbing(); | ||||
|             } | ||||
|  | ||||
|             m_MyCamera.MV_CC_CloseDevice_NET(); | ||||
|             m_MyCamera.MV_CC_DestroyDevice_NET(); | ||||
|  | ||||
|             IsOpen = false; | ||||
|             m_MyCamera = null; | ||||
|             OnCameraMessage("相机已关闭。", 0); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 开始采集图像 | ||||
|         /// </summary> | ||||
|         /// <returns>成功返回true</returns> | ||||
|         public bool StartGrabbing() | ||||
|         { | ||||
|             if (!IsOpen || m_bGrabbing) | ||||
|             { | ||||
|                 OnCameraMessage(m_bGrabbing ? "已经在采集中。" : "相机未打开。", -1); | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             // 取图前的必要操作 | ||||
|             if (NecessaryOperBeforeGrab() != MyCamera.MV_OK) | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             int nRet = m_MyCamera.MV_CC_StartGrabbing_NET(); | ||||
|             if (nRet != MyCamera.MV_OK) | ||||
|             { | ||||
|                 OnCameraMessage("开始采集失败!", nRet); | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             m_bGrabbing = true; | ||||
|             m_hReceiveThread = new Thread(ReceiveThreadProcess); | ||||
|             m_hReceiveThread.IsBackground = true; | ||||
|             m_hReceiveThread.Start(); | ||||
|  | ||||
|             OnCameraMessage("开始采集。", 0); | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 停止采集图像 | ||||
|         /// </summary> | ||||
|         public void StopGrabbing() | ||||
|         { | ||||
|             if (!m_bGrabbing) return; | ||||
|  | ||||
|             m_bGrabbing = false; | ||||
|             if (m_hReceiveThread != null && m_hReceiveThread.IsAlive) | ||||
|             { | ||||
|                 m_hReceiveThread.Join(1000); // 等待线程退出 | ||||
|             } | ||||
|  | ||||
|             int nRet = m_MyCamera.MV_CC_StopGrabbing_NET(); | ||||
|             if (nRet != MyCamera.MV_OK) | ||||
|             { | ||||
|                 OnCameraMessage("停止采集失败!", nRet); | ||||
|             } | ||||
|  | ||||
|             OnCameraMessage("停止采集。", 0); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 设置为连续采集模式 | ||||
|         /// </summary> | ||||
|         public void SetContinuousMode() | ||||
|         { | ||||
|             if (!IsOpen) return; | ||||
|             m_MyCamera.MV_CC_SetEnumValue_NET("AcquisitionMode", (uint)MyCamera.MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS); | ||||
|             m_MyCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_OFF); | ||||
|             OnCameraMessage("已设置为连续模式。", 0); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 设置为触发模式 | ||||
|         /// </summary> | ||||
|         /// <param name="isSoftwareTrigger">true为软触发, false为硬触发(Line0)</param> | ||||
|         public void SetTriggerMode(bool isSoftwareTrigger) | ||||
|         { | ||||
|             if (!IsOpen) return; | ||||
|  | ||||
|             m_MyCamera.MV_CC_SetEnumValue_NET("AcquisitionMode", (uint)MyCamera.MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS); // 触发模式也需要在Continuous模式下 | ||||
|             m_MyCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_ON); | ||||
|  | ||||
|             if (isSoftwareTrigger) | ||||
|             { | ||||
|                 m_MyCamera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_SOFTWARE); | ||||
|                 OnCameraMessage("已设置为软触发模式。", 0); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 m_MyCamera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_LINE0); | ||||
|                 OnCameraMessage("已设置为硬触发(Line0)模式。", 0); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 执行一次软触发 | ||||
|         /// </summary> | ||||
|         /// <returns>成功返回true</returns> | ||||
|         public bool SoftwareTrigger() | ||||
|         { | ||||
|             if (!IsOpen || !m_bGrabbing) | ||||
|             { | ||||
|                 OnCameraMessage("请先打开相机并开始采集。", -1); | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             int nRet = m_MyCamera.MV_CC_SetCommandValue_NET("TriggerSoftware"); | ||||
|             if (nRet != MyCamera.MV_OK) | ||||
|             { | ||||
|                 OnCameraMessage("软触发失败!", nRet); | ||||
|                 return false; | ||||
|             } | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 图像接收线程 | ||||
|         /// </summary> | ||||
|         private void ReceiveThreadProcess() | ||||
|         { | ||||
|             MyCamera.MV_FRAME_OUT stFrameInfo = new MyCamera.MV_FRAME_OUT(); | ||||
|             var stConvertParam = new MyCamera.MV_PIXEL_CONVERT_PARAM(); | ||||
|  | ||||
|             while (m_bGrabbing) | ||||
|             { | ||||
|                 int nRet = m_MyCamera.MV_CC_GetImageBuffer_NET(ref stFrameInfo, 1000); | ||||
|                 if (nRet == MyCamera.MV_OK) | ||||
|                 { | ||||
|                     Bitmap bmpForEvent = null; // 在 try 外部声明 | ||||
|                     try | ||||
|                     { | ||||
|                         // 初始化转换参数 | ||||
|                         stConvertParam.nWidth = stFrameInfo.stFrameInfo.nWidth; | ||||
|                         stConvertParam.nHeight = stFrameInfo.stFrameInfo.nHeight; | ||||
|                         stConvertParam.pSrcData = stFrameInfo.pBufAddr; | ||||
|                         stConvertParam.nSrcDataLen = stFrameInfo.stFrameInfo.nFrameLen; | ||||
|                         stConvertParam.enSrcPixelType = stFrameInfo.stFrameInfo.enPixelType; | ||||
|                         stConvertParam.pDstBuffer = m_ConvertDstBuf; | ||||
|                         stConvertParam.nDstBufferSize = m_nConvertDstBufLen; | ||||
|  | ||||
|                         // 创建用于事件的Bitmap,它的格式与类成员m_bitmap一致 | ||||
|                         bmpForEvent = new Bitmap((int)stConvertParam.nWidth, (int)stConvertParam.nHeight, m_bitmap.PixelFormat); | ||||
|  | ||||
|                         // 判断并设置目标像素格式 | ||||
|                         if (bmpForEvent.PixelFormat == PixelFormat.Format8bppIndexed) | ||||
|                         { | ||||
|                             stConvertParam.enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8; | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             stConvertParam.enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_BGR8_Packed; | ||||
|                         } | ||||
|  | ||||
|                         // 执行像素格式转换 | ||||
|                         nRet = m_MyCamera.MV_CC_ConvertPixelType_NET(ref stConvertParam); | ||||
|                         if (nRet == MyCamera.MV_OK) | ||||
|                         { | ||||
|                             // 转换成功,锁定位图内存并拷贝数据 | ||||
|                             BitmapData bmpData = bmpForEvent.LockBits(new Rectangle(0, 0, stConvertParam.nWidth, stConvertParam.nHeight), ImageLockMode.WriteOnly, bmpForEvent.PixelFormat); | ||||
|  | ||||
|                             // 使用更健壮的逐行拷贝 | ||||
|                             int pixelSize = (bmpForEvent.PixelFormat == PixelFormat.Format8bppIndexed) ? 1 : 3; | ||||
|                             for (int i = 0; i < bmpData.Height; ++i) | ||||
|                             { | ||||
|                                 IntPtr pDst = new IntPtr(bmpData.Scan0.ToInt64() + i * bmpData.Stride); | ||||
|                                 IntPtr pSrc = new IntPtr(m_ConvertDstBuf.ToInt64() + i * stConvertParam.nWidth * pixelSize); | ||||
|                                 CopyMemory(pDst, pSrc, (uint)(stConvertParam.nWidth * pixelSize)); | ||||
|                             } | ||||
|                             bmpForEvent.UnlockBits(bmpData); | ||||
|  | ||||
|                             // 触发图像采集事件,转移 bmpForEvent 的所有权 | ||||
|                             ImageAcquired?.Invoke(this, bmpForEvent); | ||||
|  | ||||
|                             // 【关键】所有权已转移,将本地引用设为null,防止它在finally块中被错误地释放 | ||||
|                             bmpForEvent = null; | ||||
|                         } | ||||
|                         // 如果转换失败,不执行任何操作,让 finally 块来处理 bmpForEvent 的释放 | ||||
|                     } | ||||
|                     finally | ||||
|                     { | ||||
|                         // 【关键】如果 bmpForEvent 不为 null,意味着它被成功创建, | ||||
|                         // 但没有被成功传递给事件处理器(可能因为转换失败或发生其他异常), | ||||
|                         // 在这里必须将其释放。 | ||||
|                         bmpForEvent?.Dispose(); | ||||
|  | ||||
|                         // 无论成功与否,都必须释放相机SDK的内部图像缓存 | ||||
|                         m_MyCamera.MV_CC_FreeImageBuffer_NET(ref stFrameInfo); | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     // 超时,继续等待 | ||||
|                     Thread.Sleep(5); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 取图前的内存和参数准备 | ||||
|         /// </summary> | ||||
|         private int NecessaryOperBeforeGrab() | ||||
|         { | ||||
|             var stWidth = new MyCamera.MVCC_INTVALUE_EX(); | ||||
|             int nRet = m_MyCamera.MV_CC_GetIntValueEx_NET("Width", ref stWidth); | ||||
|             if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取宽度失败!", nRet); return nRet; } | ||||
|  | ||||
|             var stHeight = new MyCamera.MVCC_INTVALUE_EX(); | ||||
|             nRet = m_MyCamera.MV_CC_GetIntValueEx_NET("Height", ref stHeight); | ||||
|             if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取高度失败!", nRet); return nRet; } | ||||
|  | ||||
|             var stPixelFormat = new MyCamera.MVCC_ENUMVALUE(); | ||||
|             nRet = m_MyCamera.MV_CC_GetEnumValue_NET("PixelFormat", ref stPixelFormat); | ||||
|             if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取像素格式失败!", nRet); return nRet; } | ||||
|  | ||||
|             PixelFormat bitmapPixelFormat; | ||||
|             // 判断当前像素格式是否为Mono | ||||
|             bool isMono = IsMono(stPixelFormat.nCurValue); | ||||
|  | ||||
|             if (isMono) | ||||
|             { | ||||
|                 bitmapPixelFormat = PixelFormat.Format8bppIndexed; | ||||
|                 m_nConvertDstBufLen = (uint)(stWidth.nCurValue * stHeight.nCurValue); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 bitmapPixelFormat = PixelFormat.Format24bppRgb; | ||||
|                 m_nConvertDstBufLen = (uint)(stWidth.nCurValue * stHeight.nCurValue * 3); | ||||
|             } | ||||
|  | ||||
|             // 分配转换后的图像数据缓存 | ||||
|             if (m_ConvertDstBuf != IntPtr.Zero) Marshal.FreeHGlobal(m_ConvertDstBuf); | ||||
|             m_ConvertDstBuf = Marshal.AllocHGlobal((int)m_nConvertDstBufLen); | ||||
|  | ||||
|             // 创建Bitmap对象 | ||||
|             if (m_bitmap != null) m_bitmap.Dispose(); | ||||
|             m_bitmap = new Bitmap((int)stWidth.nCurValue, (int)stHeight.nCurValue, bitmapPixelFormat); | ||||
|  | ||||
|             // 如果是Mono8格式,设置调色板 | ||||
|             if (isMono) | ||||
|             { | ||||
|                 ColorPalette palette = m_bitmap.Palette; | ||||
|                 for (int i = 0; i < 256; i++) palette.Entries[i] = Color.FromArgb(i, i, i); | ||||
|                 m_bitmap.Palette = palette; | ||||
|             } | ||||
|  | ||||
|             return MyCamera.MV_OK; | ||||
|         } | ||||
|  | ||||
|         private bool IsMono(uint enPixelType) | ||||
|         { | ||||
|             switch (enPixelType) | ||||
|             { | ||||
|                 case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8: | ||||
|                 case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono10: | ||||
|                 case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono10_Packed: | ||||
|                 case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono12: | ||||
|                 case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono12_Packed: | ||||
|                     return true; | ||||
|                 default: | ||||
|                     return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 触发相机消息事件 | ||||
|         /// </summary> | ||||
|         private void OnCameraMessage(string message, int errorCode = 0) | ||||
|         { | ||||
|             CameraMessage?.Invoke(this, message, errorCode); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 释放资源 | ||||
|         /// </summary> | ||||
|         public void Dispose() | ||||
|         { | ||||
|             Close(); | ||||
|             if (m_ConvertDstBuf != IntPtr.Zero) | ||||
|             { | ||||
|                 Marshal.FreeHGlobal(m_ConvertDstBuf); | ||||
|                 m_ConvertDstBuf = IntPtr.Zero; | ||||
|             } | ||||
|             if (m_bitmap != null) | ||||
|             { | ||||
|                 m_bitmap.Dispose(); | ||||
|                 m_bitmap = null; | ||||
|             } | ||||
|             MyCamera.MV_CC_Finalize_NET(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user