842 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			842 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| 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>
 | ||
|     /// 111
 | ||
|     /// 
 | ||
|     public static class CameraManager
 | ||
|     {
 | ||
|         //1、相机与UI管理----管理每台相机对象和对应的图像显示窗口。
 | ||
|         // 活动的相机实例字典(键值对),键 (string):使用相机的名称,值 (HikvisionCamera):存储实际的相机实例,{ get; } 创建了一个只读的公共属性
 | ||
|         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>();//结果图像窗口
 | ||
| 
 | ||
|         //2、多相机同步逻辑
 | ||
|         // 【队列】一个产品需要多台相机拍完,才算完整。
 | ||
|         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();
 | ||
| 
 | ||
|         // 3、--- 新增:硬触发模拟器 ---
 | ||
|         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>
 | ||
|         /// 根据配置列表初始化或更新所有相机
 | ||
|         /// 类似 PrepareAll,但更侧重于更新配置,会检查物理设备数量,不足时报 warning。
 | ||
|         /// </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>
 | ||
|         /// 定时器触发事件。(定时器 OnHardwareTriggerTimerElapsed 会遍历相机,对处于软件触发模式的相机执行 SoftwareTrigger()。)
 | ||
|         /// </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("所有相机资源已释放。");
 | ||
| 
 | ||
|         //}
 | ||
| 
 | ||
|         /// <summary>
 | ||
|         /// 停止所有相机 + 关闭窗口 + 清空字典 + 关闭检测协调器 + 销毁模型
 | ||
|         /// </summary>
 | ||
|         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. 深度克隆 Bitmap → 分成 显示副本 和 处理副本。
 | ||
|                 bmpForDisplay = DeepCloneBitmap(bmp, "Display");
 | ||
|                 bmpForProcessing = DeepCloneBitmap(bmp, "Processing");
 | ||
|             }
 | ||
|             finally
 | ||
|             {
 | ||
|                 // 2.无论克隆成功与否,都必须立即释放事件传递过来的原始bmp,防止泄漏。
 | ||
|                 bmp?.Dispose();//空条件运算符 ?.-----在访问对象成员前检查对象是否为 null,如果 bmp 不是 null,则正常调用 Dispose() 方法,替代传统的 if (bmp != null) 检查
 | ||
|             }
 | ||
|             // 分支 A: 显示副本 → 交给 FormImageDisplay.UpdateImage()。
 | ||
|             if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow))
 | ||
|             {
 | ||
|                 // displayWindow.UpdateImage 会处理线程安全问题
 | ||
|                 displayWindow.UpdateImage(bmpForDisplay);
 | ||
|             }
 | ||
|             else
 | ||
|             {
 | ||
|                 // 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本
 | ||
|                 bmpForDisplay?.Dispose();
 | ||
|             }
 | ||
|             // 分支 B: 处理副本 → 交给 DetectionCoordinator.EnqueueImage() 进行检测。
 | ||
|             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 (e.ResultImage != null)
 | ||
|                 {
 | ||
|                     // UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可
 | ||
|                     resultDisplay.UpdateImage(e.ResultImage);
 | ||
|                 }
 | ||
|             }
 | ||
|             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上显示警告。
 | ||
|         //        }
 | ||
|         //    }
 | ||
|         //}
 | ||
| 
 | ||
|     }
 | ||
| 
 | ||
| }
 |