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 { /// /// 静态全局相机管理器,负责所有相机的生命周期、配置应用和多相机同步 /// /// 222222 public static class CameraManager { //1、相机与UI管理----管理每台相机对象和对应的图像显示窗口。 public static Dictionary ActiveCameras { get; } = new Dictionary(); public static Dictionary OriginalImageDisplays { get; } = new Dictionary();//原始图像窗口 public static Dictionary ResultImageDisplays { get; } = new Dictionary();//结果图像窗口 //2、多相机同步逻辑 private static readonly Queue ProductQueue = new Queue(); private static readonly object QueueLock = new object(); private static int EnabledCameraCount = 0; private static long _productCounter = 0; private static readonly object _counterLock = new object(); // 3、--- 新增:硬触发模拟器 --- private static readonly System.Timers.Timer _hardwareTriggerSimulator; // 获取或设置模拟硬触发的间隔时间(毫秒)。 public static double TriggerInterval { get; set; } = 1000; // 默认1秒触发一次 // 获取一个值,该值指示硬件触发模拟器当前是否正在运行。 public static bool IsHardwareTriggerSimulating { get; private set; } = false; /// /// 静态构造函数,用于一次性初始化静态资源。 /// static CameraManager() { //初始化硬触发模拟器 _hardwareTriggerSimulator = new System.Timers.Timer(); _hardwareTriggerSimulator.Elapsed += OnHardwareTriggerTimerElapsed; _hardwareTriggerSimulator.AutoReset = true; // 确保定时器持续触发 _hardwareTriggerSimulator.Enabled = false; // 默认不启动 } ////********************初始化和启动流程******************* ///// ///// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。 ///// 这是“启动设备”的第一阶段。 ///// //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("所有设备和模型已准备就绪。"); //} /// /// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。 /// 这是“启动设备”的第一阶段。 /// public static void PrepareAll(ProcessConfig config, FrmMain mainForm) { mainForm.ClearStatusStrip(); // 清理旧的状态条 Shutdown(); // 清理旧的相机和协调器资源 ThreadSafeLogger.Log("开始准备设备和模型..."); // 1. 初始化检测协调器和AI模型(此步骤会加载YOLO模型并创建CameraProcessor) DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings); // 2. 创建相机硬件实例和UI窗口 var deviceList = new HikvisionCamera().FindDevices(); if (deviceList.Count == 0) { ThreadSafeLogger.Log("错误:未找到任何相机设备!"); return; } int deviceIndex = 0; foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled)) { 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}"); // 获取 CameraProcessor 并订阅其完成事件 var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex); if (processor != null) { processor.OnProcessingCompleted += Processor_OnProcessingCompleted; } else { ThreadSafeLogger.Log($"[警告] 未能为相机 '{setting.Name}' (Index: {setting.CameraIndex}) 获取处理器,检测结果将不会显示。"); } // --- 创建【并显示】图像的UI窗口 --- var originalDisplay = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name }; var resultDisplay = new FormImageDisplay { Text = $"{setting.Name} - 结果", CameraName = setting.Name }; originalDisplay.OnDisplayEvent += ThreadSafeLogger.Log; resultDisplay.OnDisplayEvent += ThreadSafeLogger.Log; 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($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。"); deviceIndex++; } ThreadSafeLogger.Log("所有设备和模型已准备就绪。"); } /// /// 根据配置列表初始化或更新所有相机 /// 类似 PrepareAll,但更侧重于更新配置,会检查物理设备数量,不足时报 warning。 /// 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++; } } /// /// 启动硬件触发模拟器。 /// public static void StartHardwareTriggerSimulator() { if (IsHardwareTriggerSimulating) return; _hardwareTriggerSimulator.Interval = TriggerInterval; _hardwareTriggerSimulator.Start(); IsHardwareTriggerSimulating = true; ThreadSafeLogger.Log($"硬件触发模拟器已启动,触发间隔: {TriggerInterval} ms。"); } /// /// 停止硬件触发模拟器。 /// public static void StopHardwareTriggerSimulator() { if (!IsHardwareTriggerSimulating) return; _hardwareTriggerSimulator.Stop(); IsHardwareTriggerSimulating = false; ThreadSafeLogger.Log("硬件触发模拟器已停止。"); } /// /// 定时器触发事件。(定时器 OnHardwareTriggerTimerElapsed 会遍历相机,对处于软件触发模式的相机执行 SoftwareTrigger()。) /// 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(); } } } /// /// 所有启用的相机开始采集 /// 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("检测已停止。"); // } //} /// /// 所有启用的相机停止采集 /// public static void StopAll() { foreach (var cam in ActiveCameras.Values) { cam.StopGrabbing(); } ThreadSafeLogger.Log("所有相机已停止采集。"); } ///// ///// 停止并释放所有相机资源 ///// //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("所有相机资源已释放。"); //} /// /// 停止所有相机 + 关闭窗口 + 清空字典 + 关闭检测协调器 + 销毁模型 /// 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(); ThreadSafeLogger.Log("所有相机及协调器资源已释放。"); } /// /// 重置产品计数器的公共方法 /// public static void ResetProductCounter() { //lock (_counterLock) //{ // _productCounter = 0; //} DetectionCoordinator.ResetAllCounters(); // 调用协调器的重置方法 ThreadSafeLogger.Log("产品计数器已重置。"); } /// /// 接收到相机图像时的核心处理逻辑 /// //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 { bmpForDisplay = DeepCloneBitmap(bmp, "Display"); bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); } finally { bmp?.Dispose(); } if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow)) { displayWindow.UpdateImage(bmpForDisplay); } else { bmpForDisplay?.Dispose(); } // 直接将处理副本和相机编号交给 DetectionCoordinator if (bmpForProcessing != null) { DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing); } } // 事件处理器:从 CameraProcessor 接收带有结果的图像 private static void Processor_OnProcessingCompleted(object sender, ProcessingCompletedEventArgs e) { var cameraEntry = ActiveCameras.FirstOrDefault(kvp => kvp.Value.CameraIndex == e.CameraIndex); if (cameraEntry.Key == null) { e.Dispose(); return; } if (ResultImageDisplays.TryGetValue(cameraEntry.Key, out var resultDisplay)) { if (e.ResultImage != null) { resultDisplay.UpdateImage(e.ResultImage); } } else { e.Dispose(); } } /// /// 【将 SkiaSharp.SKImage 安全地转换为 System.Drawing.Bitmap。 /// 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; } /// /// 对Bitmap进行真正的深度克隆,确保内存完全独立。 /// 这是解决跨线程GDI+问题的最可靠方法。 /// /// 源Bitmap对象。 /// 一个与源图像在内存上完全独立的全新Bitmap对象。 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; } } }