Compare commits
	
		
			11 Commits
		
	
	
		
			e7341bea87
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | bf111011d4 | ||
|  | 73249ee6c2 | ||
|  | 07c8e6ec91 | ||
|  | 31d9f8d6b6 | ||
|  | 24a905b517 | ||
| 42f8bb7642 | |||
|  | 165963d337 | ||
|  | 715d7166bc | ||
| 679320311d | |||
| 6eee4eda24 | |||
| 5dedb1a632 | 
| @@ -22,42 +22,29 @@ namespace Check.Main.Camera | ||||
|     /// <summary> | ||||
|     /// 静态全局相机管理器,负责所有相机的生命周期、配置应用和多相机同步 | ||||
|     /// </summary> | ||||
|     /// 222222 | ||||
|     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> | ||||
| @@ -73,93 +60,26 @@ namespace Check.Main.Camera | ||||
|         } | ||||
|  | ||||
|  | ||||
|         //// 事件:用于向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> | ||||
|         ///// 根据配置列表初始化或更新所有相机 | ||||
|         ///// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。 | ||||
|         ///// 这是“启动设备”的第一阶段。 | ||||
|         ///// </summary> | ||||
|         //public static void Initialize(List<CameraSettings> settingsList, FrmMain mainForm) | ||||
|         //public static void PrepareAll(ProcessConfig config, FrmMain mainForm)//①准备所有相机和模型 | ||||
|         //{ | ||||
|  | ||||
|         //    // 先停止并释放所有旧的相机 | ||||
|         //    // 1. 清理旧资源和UI | ||||
|         //    mainForm.ClearStatusStrip(); | ||||
|         //    Shutdown(); | ||||
|         //    ThreadSafeLogger.Log("开始应用新的相机配置..."); | ||||
|         //    ThreadSafeLogger.Log("开始准备设备和模型..."); | ||||
|  | ||||
|         //    EnabledCameraCount = settingsList.Count(s => s.IsEnabled); | ||||
|         //    if (EnabledCameraCount == 0) | ||||
|         //    { | ||||
|         //        ThreadSafeLogger.Log("没有启用的相机。"); | ||||
|         //        return; | ||||
|         //    } | ||||
|         //    // 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) | ||||
|         //    { | ||||
| @@ -167,19 +87,9 @@ namespace Check.Main.Camera | ||||
|         //        return; | ||||
|         //    } | ||||
|  | ||||
|         //    int deviceIndex = 0; | ||||
|         //    foreach (var setting in settingsList) | ||||
|         //    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 }; | ||||
|         //        var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex }; | ||||
|         //        cam.TriggerMode = setting.TriggerMode; | ||||
|         //        if (!cam.Open(setting)) | ||||
|         //        { | ||||
| @@ -203,40 +113,38 @@ namespace Check.Main.Camera | ||||
|  | ||||
|         //        // --- 订阅事件 --- | ||||
|         //        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); | ||||
|         //        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); | ||||
|         //        CameraDisplays.Add(setting.Name, displayForm); | ||||
|         //        OriginalImageDisplays.Add(setting.Name, originalDisplay); | ||||
|         //        ResultImageDisplays.Add(setting.Name, resultDisplay); | ||||
|         //        mainForm.AddCameraToStatusStrip(setting.Name); | ||||
|         //        ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。"); | ||||
|         //        deviceIndex++; | ||||
|         //    } | ||||
|         //    ThreadSafeLogger.Log("所有设备和模型已准备就绪。"); | ||||
|         //} | ||||
|  | ||||
|         //********************初始化和启动流程******************* | ||||
|         /// <summary> | ||||
|         /// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。 | ||||
|         /// 这是“启动设备”的第一阶段。 | ||||
|         /// </summary> | ||||
|         public static void PrepareAll(ProcessConfig config, FrmMain mainForm)//①准备所有相机和模型 | ||||
|         public static void PrepareAll(ProcessConfig config, FrmMain mainForm) | ||||
|         { | ||||
|             // 1. 清理旧资源和UI | ||||
|             mainForm.ClearStatusStrip(); | ||||
|             Shutdown(); | ||||
|             mainForm.ClearStatusStrip(); // 清理旧的状态条 | ||||
|             Shutdown(); // 清理旧的相机和协调器资源 | ||||
|             ThreadSafeLogger.Log("开始准备设备和模型..."); | ||||
|  | ||||
|             // 2. 初始化检测协调器和AI模型 | ||||
|             // 注意:YoloModelManager 的 Initialize 现在也应在这里被调用,以确保逻辑集中 | ||||
|             YoloModelManager.Initialize(config.ModelSettings); | ||||
|             // 1. 初始化检测协调器和AI模型(此步骤会加载YOLO模型并创建CameraProcessor) | ||||
|             DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings); | ||||
|  | ||||
|             // 3. 创建相机硬件实例和UI窗口------ | ||||
|             // 2. 创建相机硬件实例和UI窗口 | ||||
|             var deviceList = new HikvisionCamera().FindDevices(); | ||||
|             if (deviceList.Count == 0) | ||||
|             { | ||||
| @@ -244,8 +152,15 @@ namespace Check.Main.Camera | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled))//打开相机 → 设置触发模式 → 绑定事件(图像采集、检测完成)。 | ||||
|             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)) | ||||
| @@ -270,12 +185,26 @@ namespace Check.Main.Camera | ||||
|  | ||||
|                 // --- 订阅事件 --- | ||||
|                 cam.ImageAcquired += OnCameraImageAcquired; | ||||
|                 var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex); | ||||
|                 if (processor != null) { processor.OnProcessingCompleted += Processor_OnProcessingCompleted; } | ||||
|                 cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}"); | ||||
|  | ||||
|                 // --- 创建【但不显示】图像的UI窗口 --- | ||||
|                 // 获取 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); | ||||
|  | ||||
| @@ -284,6 +213,8 @@ namespace Check.Main.Camera | ||||
|                 OriginalImageDisplays.Add(setting.Name, originalDisplay); | ||||
|                 ResultImageDisplays.Add(setting.Name, resultDisplay); | ||||
|                 mainForm.AddCameraToStatusStrip(setting.Name); | ||||
|                 ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。"); | ||||
|                 deviceIndex++; | ||||
|             } | ||||
|             ThreadSafeLogger.Log("所有设备和模型已准备就绪。"); | ||||
|         } | ||||
| @@ -507,8 +438,6 @@ namespace Check.Main.Camera | ||||
|             // 3. 关闭检测协调器,它会负责清理所有后台线程和队列 | ||||
|             DetectionCoordinator.Shutdown(); | ||||
|  | ||||
|             YoloModelManager.Shutdown(); | ||||
|  | ||||
|             ThreadSafeLogger.Log("所有相机及协调器资源已释放。"); | ||||
|         } | ||||
|  | ||||
| @@ -517,10 +446,11 @@ namespace Check.Main.Camera | ||||
|         /// </summary> | ||||
|         public static void ResetProductCounter() | ||||
|         { | ||||
|             lock (_counterLock) | ||||
|             { | ||||
|                 _productCounter = 0; | ||||
|             } | ||||
|             //lock (_counterLock) | ||||
|             //{ | ||||
|             //    _productCounter = 0; | ||||
|             //} | ||||
|             DetectionCoordinator.ResetAllCounters(); // 调用协调器的重置方法             | ||||
|             ThreadSafeLogger.Log("产品计数器已重置。"); | ||||
|         } | ||||
|  | ||||
| @@ -620,6 +550,7 @@ namespace Check.Main.Camera | ||||
|         //} | ||||
|  | ||||
|         //【相机回调】 | ||||
|         // 【相机回调】现在只负责图像分发 | ||||
|         private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp) | ||||
|         { | ||||
|             Bitmap bmpForDisplay = null; | ||||
| @@ -627,66 +558,51 @@ namespace Check.Main.Camera | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 // 1. 深度克隆 Bitmap → 分成 显示副本 和 处理副本。 | ||||
|                 bmpForDisplay = DeepCloneBitmap(bmp, "Display"); | ||||
|                 bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 // 2.无论克隆成功与否,都必须立即释放事件传递过来的原始bmp,防止泄漏。 | ||||
|                 bmp?.Dispose();//空条件运算符 ?.-----在访问对象成员前检查对象是否为 null,如果 bmp 不是 null,则正常调用 Dispose() 方法,替代传统的 if (bmp != null) 检查 | ||||
|                 bmp?.Dispose(); | ||||
|             } | ||||
|             // 分支 A: 显示副本 → 交给 FormImageDisplay.UpdateImage()。 | ||||
|  | ||||
|             if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow)) | ||||
|             { | ||||
|                 // displayWindow.UpdateImage 会处理线程安全问题 | ||||
|                 displayWindow.UpdateImage(bmpForDisplay); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本 | ||||
|                 bmpForDisplay?.Dispose(); | ||||
|             } | ||||
|             // 分支 B: 处理副本 → 交给 DetectionCoordinator.EnqueueImage() 进行检测。 | ||||
|  | ||||
|             // 直接将处理副本和相机编号交给 DetectionCoordinator | ||||
|             if (bmpForProcessing != null) | ||||
|             { | ||||
|                 // bmpForProcessing 的所有权在这里被转移给了协调器 | ||||
|                 DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing); | ||||
|             } | ||||
|             //// 深度克隆图像以确保线程安全 | ||||
|             //Bitmap bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); | ||||
|             //bmp?.Dispose(); // 立即释放原始图 | ||||
|  | ||||
|             //// 直接将图像和相机编号交给协调器,无需任何本地处理 | ||||
|             //DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing); | ||||
|         } | ||||
|         // 事件处理器 | ||||
|  | ||||
|         // 事件处理器:从 CameraProcessor 接收带有结果的图像 | ||||
|         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(); // 如果找不到接收者,必须释放事件参数中的图像 | ||||
|                 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> | ||||
| @@ -808,32 +724,6 @@ namespace Check.Main.Camera | ||||
|  | ||||
|             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上显示警告。 | ||||
|         //        } | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -12,13 +12,15 @@ using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using YoloDotNet.Extensions; | ||||
| using OpenCvSharp; | ||||
| using Check.Main.Infer; // 引入 IDetector, DetectionResult | ||||
|  | ||||
| namespace Check.Main.Camera | ||||
| { | ||||
|     public class CameraProcessor : IDisposable | ||||
|     { | ||||
|         private readonly int _cameraIndex; | ||||
|         private readonly int _modeId; | ||||
|         // private readonly ModelSettings _model; | ||||
|         private readonly IDetector _detector; // 替换为接口 | ||||
|         private readonly ModelSettings _modelSettings; // 保留模型设置以获取更多参数 | ||||
|         private readonly BlockingCollection<ImageData> _imageQueue = new BlockingCollection<ImageData>(); | ||||
|         private readonly Thread _workerThread; | ||||
|         private volatile bool _isRunning = false; | ||||
| @@ -28,11 +30,12 @@ namespace Check.Main.Camera | ||||
|         public event EventHandler<ProcessingCompletedEventArgs> OnProcessingCompleted; | ||||
|  | ||||
|  | ||||
|         public CameraProcessor(int cameraIndex, int modelId)//, ModelSettings model | ||||
|         // 构造函数现在接受 IDetector 实例和 ModelSettings | ||||
|         public CameraProcessor(int cameraIndex, IDetector detector, ModelSettings modelSettings) | ||||
|         { | ||||
|             _cameraIndex = cameraIndex; | ||||
|             _modeId = modelId; | ||||
|             //_model = model; | ||||
|             _detector = detector ?? throw new ArgumentNullException(nameof(detector)); | ||||
|             _modelSettings = modelSettings ?? throw new ArgumentNullException(nameof(modelSettings)); | ||||
|             _workerThread = new Thread(ProcessQueue) { IsBackground = true, Name = $"Cam_{_cameraIndex}_Processor" }; | ||||
|         } | ||||
|  | ||||
| @@ -42,15 +45,16 @@ namespace Check.Main.Camera | ||||
|             _workerThread.Start(); | ||||
|         } | ||||
|  | ||||
|         public void EnqueueImage(Bitmap bmp) | ||||
|         public void EnqueueImage(Bitmap bmp, long productId) // 接收产品ID | ||||
|         { | ||||
|             if (!_isRunning) | ||||
|             { | ||||
|                 bmp?.Dispose(); | ||||
|                 return; | ||||
|             } | ||||
|             _imageCounter++; | ||||
|             _imageQueue.Add(new ImageData(_imageCounter, _cameraIndex, bmp)); | ||||
|             // _imageCounter 在此用于内部跟踪,不是产品ID | ||||
|             // 产品ID现在由 DetectionCoordinator 生成并传递 | ||||
|             _imageQueue.Add(new ImageData(productId, _cameraIndex, bmp)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
| @@ -58,42 +62,38 @@ namespace Check.Main.Camera | ||||
|         /// </summary> | ||||
|         private void ProcessQueue() | ||||
|         { | ||||
|             //// 从模型管理器获取此线程专属的YOLO模型 | ||||
|             //var yoloModel = YoloModelManager.GetModel(_modeId); | ||||
|             //if (yoloModel == null) | ||||
|  | ||||
|             ThreadSafeLogger.Log($"相机#{_cameraIndex} 启动处理线程,算法类型:{_modelSettings.M_AType}"); | ||||
|  | ||||
|             ////训练HALCON模型 | ||||
|             ////训练阶段(相机2) | ||||
|             //var trainer = new LogoTemplateTrainer(); | ||||
|  | ||||
|             //trainer.TrainAndSaveTemplates( | ||||
|             //    new List<string> | ||||
|             //    { | ||||
|             //    @"D:\HalconTemplateMatch\train2\logo1.bmp", | ||||
|             //    @"D:\HalconTemplateMatch\train2\logo2.bmp", | ||||
|             //    @"D:\HalconTemplateMatch\train2\logo3.bmp" | ||||
|             //    }, | ||||
|             //    @"D:\HalconTemplateMatch\model_2"); | ||||
|  | ||||
|             ////训练阶段(相机3)9.25修改!! | ||||
|             //trainer.TrainAndSaveTemplates( | ||||
|             //   new List<string> | ||||
|             //   { | ||||
|             //    @"D:\HalconTemplateMatch\train3\3C_1.bmp", | ||||
|             //    @"D:\HalconTemplateMatch\train3\3C_2.bmp", | ||||
|             //    @"D:\HalconTemplateMatch\train3\3C_3.bmp" | ||||
|             //   }, | ||||
|             //   @"D:\HalconTemplateMatch\model_3"); | ||||
|  | ||||
|             //if (trainer == null) | ||||
|             //{ | ||||
|             //    ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型,处理线程已中止。"); | ||||
|             //    return; // 如果没有模型,此线程无法工作 | ||||
|             //    ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 未加载模板,处理线程已中止。"); | ||||
|             //    return; | ||||
|             //} | ||||
|  | ||||
|             //训练阶段(相机2) | ||||
|             var trainer = new LogoTemplateTrainer(); | ||||
|  | ||||
|             trainer.TrainAndSaveTemplates( | ||||
|                 new List<string> | ||||
|                 { | ||||
|                 @"D:\HalconTemplateMatch\train2\logo1.bmp", | ||||
|                 @"D:\HalconTemplateMatch\train2\logo2.bmp", | ||||
|                 @"D:\HalconTemplateMatch\train2\logo3.bmp" | ||||
|                 }, | ||||
|                 @"D:\HalconTemplateMatch\model_2"); | ||||
|  | ||||
|             //训练阶段(相机3)9.25修改!! | ||||
|             trainer.TrainAndSaveTemplates( | ||||
|                new List<string> | ||||
|                { | ||||
|                 @"D:\HalconTemplateMatch\train3\3C_1.bmp", | ||||
|                 @"D:\HalconTemplateMatch\train3\3C_2.bmp", | ||||
|                 @"D:\HalconTemplateMatch\train3\3C_3.bmp" | ||||
|                }, | ||||
|                @"D:\HalconTemplateMatch\model_3"); | ||||
|  | ||||
|             if (trainer == null) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 未加载模板,处理线程已中止。"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|  | ||||
|             while (_isRunning) | ||||
|             { | ||||
| @@ -103,116 +103,23 @@ namespace Check.Main.Camera | ||||
|                     ImageData data = _imageQueue.Take(); | ||||
|                     using (data) | ||||
|                     { | ||||
|  | ||||
|                         //using (var skImage = ConvertBitmapToSKImage(data.Image)) // 转换图像格式并管理其生命周期 | ||||
|                         //{ | ||||
|                         //    if (skImage == null) continue; | ||||
|                         //    var predictions = yoloModel.RunObjectDetection(skImage); | ||||
|                         //    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 | ||||
|                         //            )); | ||||
|                         //        } | ||||
|                         //    } | ||||
|                         //} | ||||
|  | ||||
|                         //***********************************使用Halcon模板匹配进行检测**************************************************** | ||||
|                         if (data.Image == null) continue; | ||||
|                         // 统一定义预测结果 | ||||
|                         var matcher = new LogoMatcher(); | ||||
|  | ||||
|                         //9.25(增加一根据不同的相机编号调用不同的模型!) | ||||
|                         string filepath = ""; | ||||
|                         if (_cameraIndex == 2) | ||||
|                         { | ||||
|                             matcher.LoadTemplates(@"D:\HalconTemplateMatch\model_2"); | ||||
|                             filepath = "D:\\HalconTemplateMatch\\train2"; | ||||
|                         } | ||||
|                         else if (_cameraIndex == 3) | ||||
|                         { | ||||
|                             matcher.LoadTemplates(@"D:\HalconTemplateMatch\model_3"); | ||||
|                             filepath = "D:\\HalconTemplateMatch\\train3"; | ||||
|                         } | ||||
|  | ||||
|                         ////原bool返回的处理 | ||||
|                         //bool found = matcher.FindLogo(data.Image); | ||||
|                         //string result = found ? "OK":"NG"; | ||||
|  | ||||
|                         //double返回的处理 | ||||
|                         double score = matcher.FindLogo(data.Image); | ||||
|                           | ||||
|  | ||||
|                         //Mat cam = ProcessImg.BitmapToMat(data.Image); | ||||
|                          | ||||
|                         //score= ProcessImg.ProcessImagesInFolder(filepath,cam); | ||||
|  | ||||
|                         string result = (score > 0.5) ? "OK" : "NG"; | ||||
|  | ||||
|                         ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},结果: {result},得分: {score}"); | ||||
|                         DetectionResult detectionResult = _detector.Detect(data.Image); | ||||
|                         ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},结果: {(detectionResult.IsOk ? "OK" : "NG")}, 信息: {detectionResult.Message}, 得分: {detectionResult.Score:F2}"); | ||||
|  | ||||
|                         // 将处理结果交给协调器进行组装 | ||||
|                         DetectionCoordinator.AssembleProduct(data, result); | ||||
|                         DetectionCoordinator.AssembleProduct(data.ProductId, data.CameraIndex, detectionResult.IsOk, detectionResult.ResultImage); | ||||
|                | ||||
|  | ||||
|                         //给PLC的M90、M91写值(10.10) | ||||
|                         if (FrmMain.PlcClient != null) | ||||
|                         { | ||||
|                             if (result == "OK") | ||||
|                             { | ||||
|                                 //吹气到合格框 | ||||
|                                 FrmMain.PlcClient.WriteBool("90", true); // 写入M90为1 | ||||
|                                 // 延时复位 | ||||
|                                 Task.Run(async () => | ||||
|                                 { | ||||
|                                     //await Task.Delay(300); // 延时300毫秒,可根据实际气动时间调整 | ||||
|                                     //await FrmMain.PlcClient.WriteAsync("M90", 0); | ||||
|                                 }); | ||||
|                             } | ||||
|                             else | ||||
|                             { | ||||
|                                 //吹气到不合格框 | ||||
|                                 FrmMain.PlcClient.WriteBool("91", true);// 写入M91为1 | ||||
|                                 // 延时复位 | ||||
|                                 Task.Run(async () => | ||||
|                                 { | ||||
|                                     //await Task.Delay(300); | ||||
|                                     //await FrmMain.PlcClient.WriteAsync("M91", 0); | ||||
|                                 }); | ||||
|                             } | ||||
|                             //完成一次检测进行刷新 | ||||
|                             Thread.Sleep(2000); | ||||
|                             FrmMain.PlcClient.WriteBool("90", false); //  | ||||
|                             FrmMain.PlcClient.WriteBool("91", false); // 写入M90为1 | ||||
|  | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             ThreadSafeLogger.Log("6,跳过写入。"); | ||||
|                         } | ||||
|  | ||||
|  | ||||
|                         // ③ 外部订阅事件 | ||||
|                         // 外部订阅事件,传递结果图像 | ||||
|                         OnProcessingCompleted?.Invoke( | ||||
|                             this, | ||||
|                             new ProcessingCompletedEventArgs | ||||
|                             ( | ||||
|                                 _cameraIndex, | ||||
|                                 data.ProductId, | ||||
|                                 data.Image // 原图传出去 | ||||
|                                 detectionResult.ResultImage // 传递带有绘制结果的图像 | ||||
|                             ) | ||||
|                         ); | ||||
|                     } | ||||
| @@ -252,6 +159,7 @@ namespace Check.Main.Camera | ||||
|         } | ||||
|         public static SKBitmap ToSKBitmapFast(Bitmap bitmap) | ||||
|         { | ||||
|             if (bitmap == null) return null; | ||||
|             // 确保是 32bppArgb(BGRA 内存布局) | ||||
|             Bitmap src = bitmap; | ||||
|             if (bitmap.PixelFormat != PixelFormat.Format32bppArgb) | ||||
| @@ -298,13 +206,15 @@ namespace Check.Main.Camera | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void Stop() | ||||
|         { | ||||
|             _isRunning = false; | ||||
|             // 解除阻塞,让线程可以检查 _isRunning 标志并退出 | ||||
|             _imageQueue.CompleteAdding(); | ||||
|             _workerThread.Join(500); // 等待线程结束 | ||||
|             _workerThread.Join(5000); // 等待线程结束。10.22修改,原来是500 | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 线程安全地重置该相机的图像计数器。 | ||||
|         /// </summary> | ||||
| @@ -322,6 +232,27 @@ namespace Check.Main.Camera | ||||
|         { | ||||
|             Stop(); | ||||
|             _imageQueue.Dispose(); | ||||
|             _detector?.Dispose(); // 释放检测器资源 | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class ProcessingCompletedEventArgs : EventArgs, IDisposable | ||||
|     { | ||||
|         public int CameraIndex { get; } | ||||
|         public long ProductId { get; } | ||||
|         public Bitmap ResultImage { get; } // 新增:带有检测结果的图像 | ||||
|  | ||||
|         public ProcessingCompletedEventArgs(int cameraIndex, long productId, Bitmap resultImage) | ||||
|         { | ||||
|             CameraIndex = cameraIndex; | ||||
|             ProductId = productId; | ||||
|             ResultImage = resultImage; | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             ResultImage?.Dispose(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -158,6 +158,9 @@ namespace HalconTemplateMatch | ||||
|     { | ||||
|         private readonly List<HTuple> modelHandles = new List<HTuple>(); | ||||
|  | ||||
|         // 构造函数用于初始化 | ||||
|         public LogoMatcher() { } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 从指定目录加载所有 .shm 模板文件 | ||||
|         /// </summary> | ||||
| @@ -207,54 +210,231 @@ namespace HalconTemplateMatch | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 匹配并返回最高得分(double返回) | ||||
|         /// </summary> | ||||
|         public double FindLogo(Bitmap bmp) | ||||
|         //        /// <summary> | ||||
|         //        /// 匹配并返回最高得分(double返回) | ||||
|         //        /// </summary> | ||||
|         //        public double FindLogo(Bitmap bmp) | ||||
|         //        { | ||||
|         //            if (modelHandles.Count == 0) | ||||
|         //            { | ||||
|         //                Console.WriteLine("[警告] 尚未加载任何模板。"); | ||||
|         //                return -1; | ||||
|         //            } | ||||
|  | ||||
|         //            // Bitmap 转 Halcon 对象 | ||||
|         //            HObject ho_TestImage; | ||||
|         //            Bitmap2HObject(bmp, out ho_TestImage); | ||||
|         //            HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage); | ||||
|  | ||||
|         //            double bestScore = -1; | ||||
|  | ||||
|         //            foreach (var modelID in modelHandles) | ||||
|         //            { | ||||
|         //                try | ||||
|         //                { | ||||
|         //                    HOperatorSet.FindScaledShapeModel( | ||||
|         //    ho_TestImage, | ||||
|         //    modelID, | ||||
|         //    new HTuple(0).TupleRad(), | ||||
|         //    new HTuple(360).TupleRad(), | ||||
|         //    0.8, 1.2, | ||||
|         //    0.5, 1, 0.5, | ||||
|         //    "least_squares_high", | ||||
|         //    0, 0.9, | ||||
|         //    out HTuple row, out HTuple col, out HTuple angle, out HTuple scale, out HTuple score | ||||
|         //); | ||||
|  | ||||
|  | ||||
|         //                    if (score.Length > 0 && score[0].D > bestScore) | ||||
|         //                        bestScore = score[0].D; | ||||
|         //                } | ||||
|         //                catch (HOperatorException ex) | ||||
|         //                { | ||||
|         //                    Console.WriteLine($"[错误] 模板匹配失败: {ex.Message}"); | ||||
|         //                } | ||||
|         //            } | ||||
|  | ||||
|         //            ho_TestImage.Dispose(); | ||||
|         //            return bestScore; | ||||
|         //        } | ||||
|  | ||||
|         public double FindLogo(Bitmap bmp, out Bitmap resultImage) // 返回结果图像 | ||||
|         { | ||||
|             if (modelHandles.Count == 0) | ||||
|             { | ||||
|                 Console.WriteLine("[警告] 尚未加载任何模板。"); | ||||
|                 resultImage = (Bitmap)bmp.Clone(); | ||||
|                 return -1; | ||||
|             } | ||||
|  | ||||
|             // Bitmap 转 Halcon 对象 | ||||
|             HObject ho_TestImage; | ||||
|             Bitmap2HObject(bmp, out ho_TestImage); | ||||
|             HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage); | ||||
|  | ||||
|             double bestScore = -1; | ||||
|             HTuple bestRow = new HTuple(), bestCol = new HTuple(), bestAngle = new HTuple(), bestScale = new HTuple(); | ||||
|             int bestModelIndex = -1; | ||||
|  | ||||
|             foreach (var modelID in modelHandles) | ||||
|             for (int i = 0; i < modelHandles.Count; i++) | ||||
|             { | ||||
|                 var modelID = modelHandles[i]; | ||||
|                 try | ||||
|                 { | ||||
|                     HOperatorSet.FindScaledShapeModel( | ||||
|                         ho_TestImage, | ||||
|                         modelID, | ||||
|     new HTuple(0).TupleRad(), | ||||
|     new HTuple(360).TupleRad(), | ||||
|                         new HTuple(0).TupleRad(), new HTuple(360).TupleRad(), | ||||
|                         0.8, 1.2, | ||||
|                         0.5, 1, 0.5, | ||||
|                         "least_squares_high", | ||||
|                         0, 0.9, | ||||
|                         out HTuple row, out HTuple col, out HTuple angle, out HTuple scale, out HTuple score | ||||
| ); | ||||
|  | ||||
|                     ); | ||||
|  | ||||
|                     if (score.Length > 0 && score[0].D > bestScore) | ||||
|                     { | ||||
|                         bestScore = score[0].D; | ||||
|                         bestRow = row; | ||||
|                         bestCol = col; | ||||
|                         bestAngle = angle; | ||||
|                         bestScale = scale; | ||||
|                         bestModelIndex = i; | ||||
|                     } | ||||
|                 } | ||||
|                 catch (HOperatorException ex) | ||||
|                 { | ||||
|                     Console.WriteLine($"[错误] 模板匹配失败: {ex.Message}"); | ||||
|                     //Console.WriteLine($"[错误] 模板匹配失败: {ex.Message}"); // 避免过多日志 | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             resultImage = DrawHalconResults(bmp, bestModelIndex >= 0 ? modelHandles[bestModelIndex] : new HTuple(), bestRow, bestCol, bestAngle, bestScale, bestScore); | ||||
|  | ||||
|             ho_TestImage.Dispose(); | ||||
|             return bestScore; | ||||
|         } | ||||
|  | ||||
|         //private Bitmap DrawHalconResults(Bitmap originalBmp, HTuple modelID, HTuple row, HTuple col, HTuple angle, HTuple scale, double score) | ||||
|         //{ | ||||
|         //    Bitmap drawnBmp = (Bitmap)originalBmp.Clone(); | ||||
|         //    if (score <= 0 || modelID.Length == 0) return drawnBmp; // 未找到或分数过低不绘制 | ||||
|  | ||||
|         //    using (Graphics g = Graphics.FromImage(drawnBmp)) | ||||
|         //    { | ||||
|         //        Pen pen = (score > 0.7) ? new Pen(Color.Green, 3) : new Pen(Color.Orange, 3); // 可以根据分数改变颜色 | ||||
|         //        Font font = new Font("Arial", 12, FontStyle.Bold); | ||||
|         //        Brush brush = (score > 0.7) ? new SolidBrush(Color.Green) : new SolidBrush(Color.Orange); | ||||
|  | ||||
|         //        // 获取匹配模板的轮廓 | ||||
|         //        HOperatorSet.GetShapeModelContours(out HObject modelContours, modelID, 1); | ||||
|  | ||||
|         //        // 转换到图像坐标 | ||||
|         //        HOperatorSet.AffineTransContourXld(modelContours, out HObject transformedContours, | ||||
|         //            new HTuple(angle), new HTuple(scale), new HTuple(row), new HTuple(col), | ||||
|         //            "fit_origin"); // 假设 FindScaledShapeModel 的 row/col 是中心 | ||||
|  | ||||
|         //        // 绘制 XLD 轮廓 | ||||
|         //        HTuple numContours; | ||||
|         //        HOperatorSet.CountObj(transformedContours, out numContours); | ||||
|  | ||||
|         //        for (int i = 1; i <= numContours; i++) | ||||
|         //        { | ||||
|         //            HOperatorSet.SelectObj(transformedContours, out HObject currentContour, i); | ||||
|         //            HOperatorSet.GetContourXld(currentContour, out HTuple contourRow, out HTuple contourCol); | ||||
|  | ||||
|         //            if (contourRow.Length > 1) | ||||
|         //            { | ||||
|         //                Point[] points = new Point[contourRow.Length]; | ||||
|         //                for (int j = 0; j < contourRow.Length; j++) | ||||
|         //                { | ||||
|         //                    points[j] = new Point((int)contourCol.DArr[j], (int)contourRow.DArr[j]); | ||||
|         //                } | ||||
|         //                g.DrawPolygon(pen, points); | ||||
|         //            } | ||||
|         //            currentContour.Dispose(); | ||||
|         //        } | ||||
|  | ||||
|         //        // 绘制得分 | ||||
|         //        if (row.Length > 0 && col.Length > 0) | ||||
|         //        { | ||||
|         //            g.DrawString($"Score: {score:F2}", font, brush, (float)col.D - 50, (float)row.D - 50); | ||||
|         //        } | ||||
|  | ||||
|         //        modelContours.Dispose(); | ||||
|         //        transformedContours.Dispose(); | ||||
|         //    } | ||||
|         //    return drawnBmp; | ||||
|         //} | ||||
|  | ||||
|         private Bitmap DrawHalconResults(Bitmap originalBmp, HTuple modelID, HTuple row, HTuple col, HTuple angle, HTuple scale, double score) | ||||
|         { | ||||
|             // 克隆输入图像用于绘制 | ||||
|             Bitmap drawnBmp = (Bitmap)originalBmp.Clone(); | ||||
|  | ||||
|             // 如果未找到匹配,不绘制任何内容 | ||||
|             if (score <= 0 || modelID.Length == 0) | ||||
|                 return drawnBmp; | ||||
|  | ||||
|             using (Graphics g = Graphics.FromImage(drawnBmp)) | ||||
|             { | ||||
|                 // 绘制样式:高分绿色、低分橙色 | ||||
|                 Pen pen = (score > 0.7) ? new Pen(Color.Green, 3) : new Pen(Color.Orange, 3); | ||||
|                 Font font = new Font("Arial", 12, FontStyle.Bold); | ||||
|                 Brush brush = (score > 0.7) ? new SolidBrush(Color.Green) : new SolidBrush(Color.Orange); | ||||
|  | ||||
|                 // 1️⃣ 获取模板的轮廓 | ||||
|                 HOperatorSet.GetShapeModelContours(out HObject modelContours, modelID, 1); | ||||
|  | ||||
|                 // 2️⃣ 构建仿射变换矩阵 | ||||
|                 HTuple homMat2D; | ||||
|                 HOperatorSet.HomMat2dIdentity(out homMat2D);                                     // 初始化单位矩阵 | ||||
|                 HOperatorSet.HomMat2dScale(homMat2D, scale, scale, 0, 0, out homMat2D);          // 缩放 | ||||
|                 HOperatorSet.HomMat2dRotate(homMat2D, angle, 0, 0, out homMat2D);                // 旋转 | ||||
|                 HOperatorSet.HomMat2dTranslate(homMat2D, col, row, out homMat2D);                // 平移 | ||||
|  | ||||
|                 // 3️⃣ 将轮廓按仿射矩阵变换 | ||||
|                 HOperatorSet.AffineTransContourXld(modelContours, out HObject transformedContours, homMat2D); | ||||
|  | ||||
|                 // 4️⃣ 统计轮廓数量并逐一绘制 | ||||
|                 HOperatorSet.CountObj(transformedContours, out HTuple numContours); | ||||
|  | ||||
|                 for (int i = 1; i <= numContours; i++) | ||||
|                 { | ||||
|                     HOperatorSet.SelectObj(transformedContours, out HObject currentContour, i); | ||||
|                     HOperatorSet.GetContourXld(currentContour, out HTuple contourRow, out HTuple contourCol); | ||||
|  | ||||
|                     if (contourRow.Length > 1) | ||||
|                     { | ||||
|                         Point[] points = new Point[contourRow.Length]; | ||||
|                         for (int j = 0; j < contourRow.Length; j++) | ||||
|                         { | ||||
|                             points[j] = new Point((int)contourCol[j].D, (int)contourRow[j].D); | ||||
|                         } | ||||
|                         g.DrawPolygon(pen, points); | ||||
|                     } | ||||
|                     currentContour.Dispose(); | ||||
|                 } | ||||
|  | ||||
|                 // 5️⃣ 绘制得分文字 | ||||
|                 if (row.Length > 0 && col.Length > 0) | ||||
|                 { | ||||
|                     g.DrawString($"Score: {score:F2}", font, brush, (float)col.D - 50, (float)row.D - 50); | ||||
|                 } | ||||
|  | ||||
|                 // 6️⃣ 清理资源 | ||||
|                 modelContours.Dispose(); | ||||
|                 transformedContours.Dispose(); | ||||
|  | ||||
|                 // 释放 GDI+ 对象 | ||||
|                 pen.Dispose(); | ||||
|                 font.Dispose(); | ||||
|                 brush.Dispose(); | ||||
|             } | ||||
|  | ||||
|             return drawnBmp; | ||||
|         } | ||||
|  | ||||
|  | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Bitmap 转 Halcon HObject | ||||
|         /// </summary> | ||||
|   | ||||
| @@ -8,11 +8,11 @@ namespace Check.Main.Common | ||||
| { | ||||
|     public class ModbusTcpService | ||||
|     { | ||||
|         private readonly TcpClient _tcpClient = new();// | ||||
|         private readonly TcpClient _tcpClient = new(); | ||||
|         private readonly ModbusTcpNet _plc; | ||||
|         private readonly object _lock = new(); | ||||
|         public bool IsConnected => _tcpClient != null && _tcpClient.Connected;// | ||||
|  | ||||
|         // | ||||
|         public ModbusTcpService(string ip, int port = 502, byte station = 1) | ||||
|         { | ||||
|             _plc = new ModbusTcpNet(ip, port, station) | ||||
|   | ||||
| @@ -25,6 +25,8 @@ namespace Check.Main.Infer | ||||
|         /// </summary> | ||||
|         private static int _enabledCameraCount = 0; | ||||
|  | ||||
|         private static long _productCounter = 0; // 新增产品计数器10.22 | ||||
|  | ||||
|         public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted; | ||||
|         public static bool IsDetectionRunning { get; private set; } = false; | ||||
|  | ||||
| @@ -51,72 +53,220 @@ namespace Check.Main.Infer | ||||
|         public static void Initialize(List<CameraSettings> cameraSettings, List<ModelSettings> modelSettings) | ||||
|         { | ||||
|             Shutdown(); // 先关闭旧的 | ||||
|             YoloModelManager.Initialize(modelSettings); // 确保 YOLO 模型在初始化协调器时加载。10.22新增 | ||||
|  | ||||
|             var enabledCameras = cameraSettings.Where(c => c.IsEnabled).ToList(); | ||||
|             _enabledCameraCount = enabledCameras.Count; | ||||
|             if (_enabledCameraCount == 0) return; | ||||
|             //if (_enabledCameraCount == 0) return; | ||||
|             if (_enabledCameraCount == 0) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log("没有启用的相机,检测协调器未初始化。"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             foreach (var camSetting in enabledCameras) | ||||
|             { | ||||
|                 // 找到与相机编号匹配的模型 | ||||
|                 var model = modelSettings.FirstOrDefault(m => m.Id == camSetting.ModelID); | ||||
|                 ModelSettings model = modelSettings.FirstOrDefault(m => m.Id == camSetting.ModelID); | ||||
|                 if (model == null) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[警告] 找不到与相机 #{camSetting.CameraIndex} 匹配的模型,该相机将无法处理图像。"); | ||||
|                     //ThreadSafeLogger.Log($"[警告] 找不到与相机 #{camSetting.CameraIndex} 匹配的模型,该相机将无法处理图像"); | ||||
|                     ThreadSafeLogger.Log($"[警告] 找不到与相机 #{camSetting.CameraIndex} (Name: {camSetting.Name}) 匹配的模型 (ID: {camSetting.ModelID})。该相机将无法处理图像。"); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 var processor = new CameraProcessor(camSetting.CameraIndex,camSetting.ModelID); | ||||
|                 IDetector detector = null; | ||||
|                 object detectorSettings = null; // 用于传递特定检测器的设置 | ||||
|  | ||||
|                 // 根据相机的 CheckType 和模型的 AlgorithmType 决定使用哪个检测器 | ||||
|                 if (camSetting.CheckType == CheckType.Traditional && model.M_AType == AlgorithmType.Tradition) | ||||
|                 { | ||||
|                     detector = new HalconTemplateDetector(); | ||||
|                     detectorSettings = new HalconDetectionSettings { ScoreThreshold = model.HalconScoreThreshold }; | ||||
|                     ThreadSafeLogger.Log($"为相机 #{camSetting.CameraIndex} (Name: {camSetting.Name}) 绑定 HALCON 传统算法。"); | ||||
|                 } | ||||
|                 else if (camSetting.CheckType == CheckType.DeepLearning && model.M_AType == AlgorithmType.DeepLearning) | ||||
|                 { | ||||
|                     detector = new YoloDetector(); | ||||
|                     detectorSettings = new YoloDetectionSettings | ||||
|                     { | ||||
|                         ConfidenceThreshold = model.YoloConfidenceThreshold, | ||||
|                         NmsThreshold = model.YoloNmsThreshold | ||||
|                     }; | ||||
|                     ThreadSafeLogger.Log($"为相机 #{camSetting.CameraIndex} (Name: {camSetting.Name}) 绑定 YOLO 深度学习算法。"); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[警告] 相机 #{camSetting.CameraIndex} (Name: {camSetting.Name}) 的 CheckType ({camSetting.CheckType}) 与模型 (ID: {model.Id}, AlgorithmType: {model.M_AType}) 不匹配或不支持。跳过此相机。"); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 // 初始化检测器 | ||||
|                 try | ||||
|                 { | ||||
|                     // 对于YOLO,modelPath实际上传递的是ModelID | ||||
|                     // 对于HALCON,modelPath是实际的模板目录 | ||||
|                     string initPath = (detector is YoloDetector) ? model.Id.ToString() : model.Path; | ||||
|                     detector.Initialize(initPath, detectorSettings); | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[错误] 初始化相机 #{camSetting.CameraIndex} 的检测器失败: {ex.Message}"); | ||||
|                     detector?.Dispose(); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 var processor = new CameraProcessor(camSetting.CameraIndex, detector, model); | ||||
|                 _processors.TryAdd(camSetting.CameraIndex, processor); | ||||
|                 processor.Start(); | ||||
|             } | ||||
|             ThreadSafeLogger.Log($"检测协调器已初始化,启动了 {_processors.Count} 个相机处理线程。"); | ||||
|         } | ||||
|  | ||||
|         //public static void EnqueueImage(int cameraIndex, Bitmap bmp) | ||||
|         //{ | ||||
|         //    if (_processors.TryGetValue(cameraIndex, out var processor)) | ||||
|         //    { | ||||
|         //        processor.EnqueueImage(bmp); | ||||
|         //    } | ||||
|         //    else | ||||
|         //    { | ||||
|         //        // 如果找不到处理器,必须释放Bitmap防止泄漏 | ||||
|         //        bmp?.Dispose(); | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|         public static void EnqueueImage(int cameraIndex, Bitmap bmp) | ||||
|         { | ||||
|             // 在图像进入队列之前生成一个新的产品ID | ||||
|             long currentProductId; | ||||
|             lock (_productAssemblies) // 同步访问产品计数器 | ||||
|             { | ||||
|                 _productCounter++; | ||||
|                 currentProductId = _productCounter; | ||||
|             } | ||||
|  | ||||
|             if (_processors.TryGetValue(cameraIndex, out var processor)) | ||||
|             { | ||||
|                 processor.EnqueueImage(bmp); | ||||
|                 processor.EnqueueImage(bmp, currentProductId); // 传递产品ID | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // 如果找不到处理器,必须释放Bitmap防止泄漏 | ||||
|                 bmp?.Dispose(); | ||||
|                 bmp?.Dispose(); // 如果找不到处理器,必须释放Bitmap防止泄漏 | ||||
|                 ThreadSafeLogger.Log($"[警告] 未能为相机 {cameraIndex} 找到处理器,产品 {currentProductId} 的图像被丢弃。"); | ||||
|                 // 如果没有处理器,不需要在 _productAssemblies 中添加,因为不会有结果返回 | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 供 CameraProcessor 回调,用以组装产品 | ||||
|         public static void AssembleProduct(ImageData data, string result) | ||||
|         { | ||||
|             var assembly = _productAssemblies.GetOrAdd(data.ProductId, (id) => new ProductAssembly(id, _enabledCameraCount)); | ||||
|         //// 供 CameraProcessor 回调,用以组装产品 | ||||
|         //public static void AssembleProduct(ImageData data, string result) | ||||
|         //{ | ||||
|         //    var assembly = _productAssemblies.GetOrAdd(data.ProductId, (id) => new ProductAssembly(id, _enabledCameraCount)); | ||||
|  | ||||
|             if (assembly.AddResult(data.CameraIndex, result)) | ||||
|         //    if (assembly.AddResult(data.CameraIndex, result)) | ||||
|         //    { | ||||
|         //        string finalResult = assembly.GetFinalResult(); | ||||
|         //        ThreadSafeLogger.Log($"产品 #{assembly.ProductId} 已检测完毕,最终结果: {finalResult}"); | ||||
|  | ||||
|         //        // 只有在检测运行时,才触发事件 | ||||
|         //        if (IsDetectionRunning) | ||||
|         //        { | ||||
|         //            OnDetectionCompleted?.Invoke(null, new DetectionResultEventArgs(finalResult == "OK")); | ||||
|         //        } | ||||
|  | ||||
|         //        if (_productAssemblies.TryRemove(assembly.ProductId, out var finishedAssembly)) | ||||
|         //        { | ||||
|         //            finishedAssembly.Dispose(); | ||||
|         //        } | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|         // CameraProcessor 回调,用以组装产品 | ||||
|         public static void AssembleProduct(long productId, int cameraIndex, bool isOk, Bitmap resultImage) | ||||
|         { | ||||
|                 string finalResult = assembly.GetFinalResult(); | ||||
|             // GetOrAdd 确保 ProductAssembly 只被创建一次 | ||||
|             var assembly = _productAssemblies.GetOrAdd(productId, (id) => new ProductAssembly(id, _enabledCameraCount)); | ||||
|  | ||||
|             assembly.AddResult(cameraIndex, isOk, resultImage); | ||||
|  | ||||
|             // 检查产品是否已完成所有相机的检测 | ||||
|             if (assembly.IsComplete()) | ||||
|             { | ||||
|                 string finalResult = assembly.GetFinalResult() ? "OK" : "NG"; | ||||
|                 ThreadSafeLogger.Log($"产品 #{assembly.ProductId} 已检测完毕,最终结果: {finalResult}"); | ||||
|  | ||||
|                 // 只有在检测运行时,才触发事件 | ||||
|                 // 触发事件 (例如更新主UI上的总OK/NG计数) | ||||
|                 if (IsDetectionRunning) | ||||
|                 { | ||||
|                     OnDetectionCompleted?.Invoke(null, new DetectionResultEventArgs(finalResult == "OK")); | ||||
|                     OnDetectionCompleted?.Invoke(null, new DetectionResultEventArgs(assembly.GetFinalResult())); | ||||
|                 } | ||||
|  | ||||
|                 if (_productAssemblies.TryRemove(assembly.ProductId, out var finishedAssembly)) | ||||
|                 // PLC 写入逻辑 | ||||
|                 if (FrmMain.PlcClient != null) // 假设 FrmMain.PlcClient 可访问 | ||||
|                 { | ||||
|                     finishedAssembly.Dispose(); | ||||
|                     try | ||||
|                     { | ||||
|                         if (assembly.GetFinalResult()) // 最终结果 OK | ||||
|                         { | ||||
|                             FrmMain.PlcClient.WriteBool("M90", true); // 写入M90为1 | ||||
|                             Thread.Sleep(50); // 短暂延时 | ||||
|                             FrmMain.PlcClient.WriteBool("M90", false); | ||||
|                         } | ||||
|                         else // 最终结果 NG | ||||
|                         { | ||||
|                             FrmMain.PlcClient.WriteBool("M91", true); // 写入M91为1 | ||||
|                             Thread.Sleep(50); // 短暂延时 | ||||
|                             FrmMain.PlcClient.WriteBool("M91", false); | ||||
|                         } | ||||
|                         ThreadSafeLogger.Log($"产品 #{assembly.ProductId} 最终结果 {finalResult} 已写入PLC。"); | ||||
|                     } | ||||
|                     catch (Exception ex) | ||||
|                     { | ||||
|                         ThreadSafeLogger.Log($"[错误] 写入PLC失败:{ex.Message}"); | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[警告] 产品 #{assembly.ProductId} 检测结果未能写入PLC:PLC客户端未连接。"); | ||||
|                 } | ||||
|  | ||||
|                 // 移除并释放 ProductAssembly | ||||
|                 if (_productAssemblies.TryRemove(productId, out var finishedAssembly)) | ||||
|                 { | ||||
|                     finishedAssembly.Dispose(); // 释放所有存储的 Bitmap | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         /// <summary> | ||||
|         /// 命令所有活动的相机处理器重置它们的内部计数器。 | ||||
|         /// </summary> | ||||
|  | ||||
|         ///// <summary> | ||||
|         ///// 命令所有活动的相机处理器重置它们的内部计数器。 | ||||
|         ///// </summary> | ||||
|         //public static void ResetAllCounters() | ||||
|         //{ | ||||
|         //    foreach (var processor in _processors.Values) | ||||
|         //    { | ||||
|         //        processor.ResetCounter(); | ||||
|         //    } | ||||
|         //    ThreadSafeLogger.Log("所有相机处理器的产品计数器已重置。"); | ||||
|         //} | ||||
|  | ||||
|         public static void ResetAllCounters() | ||||
|         { | ||||
|             lock (_productAssemblies) | ||||
|             { | ||||
|                 _productCounter = 0; | ||||
|                 // 清空所有未完成的产品,并释放其资源 | ||||
|                 foreach (var assembly in _productAssemblies.Values) | ||||
|                 { | ||||
|                     assembly.Dispose(); | ||||
|                 } | ||||
|                 _productAssemblies.Clear(); | ||||
|             } | ||||
|             foreach (var processor in _processors.Values) | ||||
|             { | ||||
|                 processor.ResetCounter(); | ||||
|             } | ||||
|             ThreadSafeLogger.Log("所有相机处理器的产品计数器已重置。"); | ||||
|             ThreadSafeLogger.Log("所有相机处理器和产品计数器已重置。"); | ||||
|         } | ||||
|  | ||||
|         public static CameraProcessor GetProcessor(int cameraIndex) | ||||
| @@ -142,8 +292,88 @@ namespace Check.Main.Infer | ||||
|             { | ||||
|                 assembly.Dispose(); | ||||
|             } | ||||
|             _productAssemblies.Clear(); | ||||
|             YoloModelManager.Shutdown(); // 确保YOLO模型也关闭 | ||||
|             ThreadSafeLogger.Log("检测协调器已关闭。"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // 新增 ProductAssembly 类,用于集中管理一个产品的检测结果和图像 | ||||
|     public class ProductAssembly : IDisposable | ||||
|     { | ||||
|         public long ProductId { get; } | ||||
|         private readonly int _expectedCameraCount; | ||||
|         private readonly ConcurrentDictionary<int, bool> _cameraResults = new ConcurrentDictionary<int, bool>(); | ||||
|         private readonly ConcurrentDictionary<int, Bitmap> _resultImages = new ConcurrentDictionary<int, Bitmap>(); // 存储每个相机的结果图像 | ||||
|         private readonly object _lock = new object(); | ||||
|  | ||||
|         public ProductAssembly(long productId, int expectedCameraCount) | ||||
|         { | ||||
|             ProductId = productId; | ||||
|             _expectedCameraCount = expectedCameraCount; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 添加单个相机的检测结果。 | ||||
|         /// </summary> | ||||
|         /// <param name="cameraIndex">相机编号。</param> | ||||
|         /// <param name="isOk">检测结果是否为OK。</param> | ||||
|         /// <param name="resultImage">带有检测结果的图像。</param> | ||||
|         public void AddResult(int cameraIndex, bool isOk, Bitmap resultImage) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 _cameraResults.TryAdd(cameraIndex, isOk); | ||||
|                 if (resultImage != null) | ||||
|                 { | ||||
|                     // 克隆图像,确保 ProductAssembly 拥有其所有权 | ||||
|                     _resultImages.TryAdd(cameraIndex, (Bitmap)resultImage.Clone()); | ||||
|                     resultImage.Dispose(); // 释放传入的原始图像副本 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 检查是否所有相机都已提交结果。 | ||||
|         /// </summary> | ||||
|         public bool IsComplete() | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 return _cameraResults.Count == _expectedCameraCount; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 获取最终产品检测结果(所有相机都OK才为OK)。 | ||||
|         /// </summary> | ||||
|         public bool GetFinalResult() | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 return _cameraResults.Values.All(r => r); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 获取某个相机的结果图像。 | ||||
|         /// </summary> | ||||
|         public Bitmap GetResultImage(int cameraIndex) | ||||
|         { | ||||
|             _resultImages.TryGetValue(cameraIndex, out var bmp); | ||||
|             return bmp; | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 foreach (var bmp in _resultImages.Values) | ||||
|                 { | ||||
|                     bmp?.Dispose(); | ||||
|                 } | ||||
|                 _resultImages.Clear(); | ||||
|                 _cameraResults.Clear(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										57
									
								
								Check.Main/Infer/HalconTemplateDetector.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								Check.Main/Infer/HalconTemplateDetector.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| using HalconTemplateMatch; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Drawing; | ||||
|  | ||||
| namespace Check.Main.Infer | ||||
| { | ||||
|     public class HalconTemplateDetector : IDetector | ||||
|     { | ||||
|         private LogoMatcher _matcher; // 使用您现有的 LogoMatcher | ||||
|         private double _scoreThreshold = 0.5; // 可以从 ModelSettings 中配置 | ||||
|  | ||||
|         public void Initialize(string modelPath, object detectionSettings = null) | ||||
|         { | ||||
|             _matcher = new LogoMatcher(); | ||||
|             _matcher.LoadTemplates(modelPath); // modelPath 现在是 Halcon 模板目录 | ||||
|  | ||||
|             // 如果 detectionSettings 包含阈值,可以在这里解析 | ||||
|             if (detectionSettings is HalconDetectionSettings halconSettings) | ||||
|             { | ||||
|                 _scoreThreshold = halconSettings.ScoreThreshold; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public DetectionResult Detect(Bitmap image) | ||||
|         { | ||||
|             if (_matcher == null) | ||||
|                 throw new InvalidOperationException("HalconTemplateDetector 未初始化。"); | ||||
|  | ||||
|             Bitmap resultImage; | ||||
|             double score = _matcher.FindLogo(image, out resultImage); | ||||
|             bool isOk = score >= _scoreThreshold; | ||||
|             string message = isOk ? "OK" : "NG"; | ||||
|  | ||||
|             // 如果需要绘制结果图像,可以在 LogoMatcher 中添加绘制逻辑并返回 | ||||
|             // 假设 LogoMatcher 也可以返回一个带有匹配标记的 Bitmap | ||||
|             // Bitmap resultImage = _matcher.DrawResults(image, score); | ||||
|  | ||||
|             return new DetectionResult(isOk, message, score); //, resultImage | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             // LogoMatcher 如果有需要释放的资源,可以在这里处理 | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // 辅助设置类,用于传递给 HalconTemplateDetector | ||||
|     public class HalconDetectionSettings | ||||
|     { | ||||
|         public double ScoreThreshold { get; set; } = 0.5; | ||||
|         // 可以添加其他 Halcon 匹配参数 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										62
									
								
								Check.Main/Infer/IDetector.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								Check.Main/Infer/IDetector.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace Check.Main.Infer | ||||
| { | ||||
|     public interface IDetector : IDisposable | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// 初始化检测器。 | ||||
|         /// </summary> | ||||
|         /// <param name="modelPath">模型文件路径(对于传统算法可能是模板目录)</param> | ||||
|         /// <param name="detectionSettings">特定于检测器的设置对象(可选,可以用于传递阈值等)</param> | ||||
|         void Initialize(string modelPath, object detectionSettings = null); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 执行图像检测。 | ||||
|         /// </summary> | ||||
|         /// <param name="image">待检测的图像。</param> | ||||
|         /// <returns>包含检测结果(如OK/NG,得分,边界框等)的统一对象。</returns> | ||||
|         DetectionResult Detect(Bitmap image); | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 统一的检测结果类。 | ||||
|     /// </summary> | ||||
|     public class DetectionResult | ||||
|     { | ||||
|         public bool IsOk { get; set; } | ||||
|         public string Message { get; set; } | ||||
|         public double Score { get; set; } | ||||
|         public List<RectangleF> BoundingBoxes { get; set; } // 深度学习可能返回多个目标框 | ||||
|  | ||||
|         // 如果需要,可以添加带有绘制结果的图像 | ||||
|         public Bitmap ResultImage { get; set; } | ||||
|  | ||||
|         public DetectionResult(bool isOk, string message = "Unknown", double score = 0.0, List<RectangleF> boundingBoxes = null, Bitmap resultImage = null) | ||||
|         { | ||||
|             IsOk = isOk; | ||||
|             Message = message; | ||||
|             Score = score; | ||||
|             BoundingBoxes = boundingBoxes ?? new List<RectangleF>(); | ||||
|             ResultImage = resultImage; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     //// 辅助设置类,用于传递给 HalconTemplateDetector | ||||
|     //public class HalconDetectionSettings | ||||
|     //{ | ||||
|     //    public double ScoreThreshold { get; set; } = 0.5; | ||||
|     //} | ||||
|  | ||||
|     // 辅助设置类,用于传递给 YoloDetector | ||||
|     public class YoloDetectionSettings | ||||
|     { | ||||
|         public float ConfidenceThreshold { get; set; } = 0.25f; | ||||
|         public float NmsThreshold { get; set; } = 0.45f; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,4 +1,14 @@ | ||||
| using Check.Main.Common; | ||||
| //using Check.Main.Common; | ||||
| //using System; | ||||
| //using System.Collections.Generic; | ||||
| //using System.ComponentModel;// 需要引入此命名空间以使用 INotifyPropertyChanged | ||||
| //using System.Linq; | ||||
| //using System.Runtime.CompilerServices; | ||||
| //using System.Runtime.Serialization; | ||||
| //using System.Text; | ||||
| //using System.Threading.Tasks; | ||||
|  | ||||
| using Check.Main.Common; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel; | ||||
| @@ -7,28 +17,12 @@ using System.Runtime.CompilerServices; | ||||
| using System.Runtime.Serialization; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using System.Windows.Forms.Design; | ||||
| using System.Drawing.Design; | ||||
|  | ||||
| namespace Check.Main.Infer | ||||
| { | ||||
|     public enum DetectDevice | ||||
|     { | ||||
|         [Description("CPU")] | ||||
|         CPU = 0, | ||||
|  | ||||
|         [Description("GPU")] | ||||
|         GPU, | ||||
|  | ||||
|         //[Description("VPU")] | ||||
|         //VPU, | ||||
|     } | ||||
|     public enum AlgorithmType | ||||
|     { | ||||
|         [Description("传统算法")] | ||||
|         Tradition = 0, | ||||
|  | ||||
|         [Description("深度学习")] | ||||
|         DeepLearning, | ||||
|     } | ||||
|     public enum CheckModelType | ||||
|     { | ||||
|         [Description("分类")] | ||||
| @@ -47,9 +41,32 @@ namespace Check.Main.Infer | ||||
|         PoseEstimation | ||||
|     } | ||||
|  | ||||
|     [Serializable] // 确保可被XML序列化 | ||||
|     public class ModelSettings : INotifyPropertyChanged, ICloneable | ||||
|     public enum AlgorithmType | ||||
|     { | ||||
|         [Description("传统算法")] | ||||
|         Tradition = 0, | ||||
|  | ||||
|         [Description("深度学习")] | ||||
|         DeepLearning, | ||||
|     } | ||||
|  | ||||
|     public enum DetectDevice | ||||
|     { | ||||
|         [Description("CPU")] | ||||
|         CPU = 0, | ||||
|  | ||||
|         [Description("GPU")] | ||||
|         GPU, | ||||
|  | ||||
|         //[Description("VPU")] | ||||
|         //VPU, | ||||
|     } | ||||
|     | ||||
|  | ||||
|     [Serializable] // 确保可被XML序列化 | ||||
|     public class ModelSettings : INotifyPropertyChanged, ICloneable//类ModelSettings继承或实现两个接口:①提供一个事件,用于在属性值更改时发出通知;②提供一个方法,用于创建对象的独立副本 | ||||
|     { | ||||
|         //1. 实现 INotifyPropertyChanged 接口 | ||||
|         public event PropertyChangedEventHandler PropertyChanged; | ||||
|  | ||||
|         private int _id; | ||||
| @@ -57,9 +74,14 @@ namespace Check.Main.Infer | ||||
|         private string _path = ""; | ||||
|         private DetectDevice _checkDevice=DetectDevice.CPU; | ||||
|         private AlgorithmType _mAType = AlgorithmType.Tradition; | ||||
|         private CheckModelType _mType = CheckModelType.Classification; | ||||
|         private CheckModelType _mType = CheckModelType.ObjectDetection; | ||||
|         private bool _isEnabled = true; | ||||
|  | ||||
|         // 新增 HALCON 和 YOLO 的参数。10.22 | ||||
|         private double _halconScoreThreshold = 0.5; | ||||
|         private float _yoloConfidenceThreshold = 0.25f; | ||||
|         private float _yoloNmsThreshold = 0.45f; | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("模型编号"), Description("模型的唯一标识符,用于与相机编号对应。")] | ||||
|         public int Id | ||||
|         { | ||||
| @@ -73,13 +95,6 @@ namespace Check.Main.Infer | ||||
|             get => _name; | ||||
|             set { if (_name != value) { _name = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|         [Category("基本信息"), DisplayName("推理设备"), Description("推理模型的设备。")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
|         public DetectDevice CheckDevice | ||||
|         { | ||||
|             get => _checkDevice; | ||||
|             set { if (_checkDevice != value) { _checkDevice = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("算法类型"), Description("所使用的算法的类型。")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
| @@ -88,6 +103,16 @@ namespace Check.Main.Infer | ||||
|             get => _mAType; | ||||
|             set { if (_mAType != value) { _mAType = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("推理设备"), Description("推理模型的设备。")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
|         public DetectDevice CheckDevice | ||||
|         { | ||||
|             get => _checkDevice; | ||||
|             set { if (_checkDevice != value) { _checkDevice = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("模型类型"), Description("推理模型的类型。")] | ||||
|         [TypeConverter(typeof(EnumDescriptionTypeConverter))] | ||||
|         public CheckModelType MType | ||||
| @@ -96,12 +121,12 @@ namespace Check.Main.Infer | ||||
|             set { if (_mType != value) { _mType = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         [Category("基本信息"), DisplayName("是否启用"), Description("是否在程序启动时是否启用模型")] | ||||
|         public bool IsEnabled | ||||
|         { | ||||
|             get => _isEnabled; | ||||
|             set | ||||
|             { | ||||
|             set  {  | ||||
|                         if (_isEnabled != value) | ||||
|                         { | ||||
|                             _isEnabled = value; | ||||
| @@ -118,6 +143,27 @@ namespace Check.Main.Infer | ||||
|             set { if (_path != value) { _path = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|         [Category("模型参数"), DisplayName("HALCON得分阈值"), Description("HALCON模板匹配的得分阈值(0-1)。")] | ||||
|         public double HalconScoreThreshold | ||||
|         { | ||||
|             get => _halconScoreThreshold; | ||||
|             set { if (_halconScoreThreshold != value) { _halconScoreThreshold = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|         [Category("模型参数"), DisplayName("YOLO置信度阈值"), Description("YOLO检测的置信度阈值(0-1)。")] | ||||
|         public float YoloConfidenceThreshold | ||||
|         { | ||||
|             get => _yoloConfidenceThreshold; | ||||
|             set { if (_yoloConfidenceThreshold != value) { _yoloConfidenceThreshold = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|         [Category("模型参数"), DisplayName("YOLO NMS阈值"), Description("YOLO检测的非极大值抑制(NMS)阈值(0-1)。")] | ||||
|         public float YoloNmsThreshold | ||||
|         { | ||||
|             get => _yoloNmsThreshold; | ||||
|             set { if (_yoloNmsThreshold != value) { _yoloNmsThreshold = value; OnPropertyChanged(); } } | ||||
|         } | ||||
|  | ||||
|         protected void OnPropertyChanged([CallerMemberName] string propertyName = null) | ||||
|         { | ||||
|             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||||
|   | ||||
							
								
								
									
										409
									
								
								Check.Main/Infer/YoloDetector.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										409
									
								
								Check.Main/Infer/YoloDetector.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,409 @@ | ||||
| using Check.Main.Camera; | ||||
| using Check.Main.Common; | ||||
| using OpenCvSharp; | ||||
| using SkiaSharp; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Drawing; | ||||
| using System.Linq; | ||||
| using YoloDotNet; | ||||
| using YoloDotNet.Models; | ||||
|  | ||||
| namespace Check.Main.Infer | ||||
| { | ||||
|     /// <summary> | ||||
|     /// YOLO 检测结果对象,包含标签、置信度与检测框 | ||||
|     /// </summary> | ||||
|     public class YoloPrediction | ||||
|     { | ||||
|         public YoloLabel Label { get; set; } | ||||
|         public float Score { get; set; } | ||||
|         public BoundingBox BoundingBox { get; set; } | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// YOLO 类别标签 | ||||
|     /// </summary> | ||||
|     public class YoloLabel | ||||
|     { | ||||
|         public string Name { get; set; } | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 检测框坐标结构体 | ||||
|     /// </summary> | ||||
|     public struct BoundingBox | ||||
|     { | ||||
|         public float Left { get; set; } | ||||
|         public float Top { get; set; } | ||||
|         public float Width { get; set; } | ||||
|         public float Height { get; set; } | ||||
|  | ||||
|         public BoundingBox(float left, float top, float width, float height) | ||||
|         { | ||||
|             Left = left; | ||||
|             Top = top; | ||||
|             Width = width; | ||||
|             Height = height; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// YOLO 目标检测器实现类 | ||||
|     /// </summary> | ||||
|     public class YoloDetector : IDetector, IDisposable | ||||
|     { | ||||
|         private Yolo _yoloModel; | ||||
|         private int _modelID; | ||||
|         private float _confidenceThreshold = 0.25f; | ||||
|         private float _nmsThreshold = 0.45f; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 初始化 YOLO 检测器并加载模型 | ||||
|         /// </summary> | ||||
|         /// <param name="modelIdStr">模型 ID 字符串</param> | ||||
|         /// <param name="detectionSettings">可选检测参数</param> | ||||
|         /// <exception cref="ArgumentException">ID 无效</exception> | ||||
|         /// <exception cref="InvalidOperationException">模型未加载</exception> | ||||
|         public void Initialize(string modelIdStr, object detectionSettings = null) | ||||
|         { | ||||
|             if (string.IsNullOrWhiteSpace(modelIdStr)) | ||||
|                 throw new ArgumentException("模型ID字符串不能为空。", nameof(modelIdStr)); | ||||
|  | ||||
|             if (!int.TryParse(modelIdStr, out _modelID)) | ||||
|                 throw new ArgumentException("模型ID必须为有效整数。", nameof(modelIdStr)); | ||||
|  | ||||
|             _yoloModel = YoloModelManager.GetModel(_modelID) | ||||
|                 ?? throw new InvalidOperationException($"YOLO 模型 (ID: {_modelID}) 未加载或找不到。"); | ||||
|  | ||||
|             if (detectionSettings is YoloDetectionSettings yoloSettings) | ||||
|             { | ||||
|                 _confidenceThreshold = Math.Clamp(yoloSettings.ConfidenceThreshold, 0f, 1f); | ||||
|                 _nmsThreshold = Math.Clamp(yoloSettings.NmsThreshold, 0f, 1f); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 执行检测,判断是否含有 logo 类对象 | ||||
|         /// </summary> | ||||
|         public DetectionResult Detect(Bitmap image) | ||||
|         { | ||||
|             if (_yoloModel == null) | ||||
|                 throw new InvalidOperationException("YoloDetector 未初始化或模型未加载。"); | ||||
|  | ||||
|             if (image == null) | ||||
|                 return new DetectionResult(false, "输入图像为空。"); | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 using var skBitmap = CameraProcessor.ToSKBitmapFast(image); | ||||
|                 if (skBitmap == null) | ||||
|                     return new DetectionResult(false, "图像转换失败。"); | ||||
|  | ||||
|                 using var skImage = SKImage.FromBitmap(skBitmap); | ||||
|                 if (skImage == null) | ||||
|                     return new DetectionResult(false, "无法生成 SKImage。"); | ||||
|  | ||||
|                 var predictions = _yoloModel.RunObjectDetection( | ||||
|                     skImage, | ||||
|                     confidence: _confidenceThreshold, | ||||
|                     iou: _nmsThreshold | ||||
|                 ); | ||||
|  | ||||
|                 // 检查是否检测到 logo | ||||
|                 bool foundLogo = predictions.Any(p => | ||||
|                     p.Label.Name.Equals("logo", StringComparison.OrdinalIgnoreCase)); | ||||
|  | ||||
|                 return foundLogo | ||||
|                     ? new DetectionResult(false, "NG") | ||||
|                     : new DetectionResult(true, "OK"); | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 return new DetectionResult(false, $"检测失败: {ex.Message}"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 在图像上绘制检测框与标签 | ||||
|         /// </summary> | ||||
|         public Bitmap DrawYoloPredictions(Bitmap source, IEnumerable<YoloPrediction> predictions) | ||||
|         { | ||||
|             if (source == null) | ||||
|                 throw new ArgumentNullException(nameof(source)); | ||||
|  | ||||
|             if (predictions == null || !predictions.Any()) | ||||
|                 return (Bitmap)source.Clone(); | ||||
|  | ||||
|             Bitmap output = (Bitmap)source.Clone(); | ||||
|  | ||||
|             using var graphics = Graphics.FromImage(output); | ||||
|             using var pen = new Pen(Color.Yellow, 2); | ||||
|             using var font = new Font("Arial", 10, FontStyle.Bold); | ||||
|             using var brush = new SolidBrush(Color.Yellow); | ||||
|  | ||||
|             foreach (var pred in predictions) | ||||
|             { | ||||
|                 var box = pred.BoundingBox; | ||||
|                 var rect = new RectangleF(box.Left, box.Top, box.Width, box.Height); | ||||
|  | ||||
|                 // 绘制检测框 | ||||
|                 graphics.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height); | ||||
|  | ||||
|                 // 绘制标签 | ||||
|                 string label = $"{pred.Label?.Name ?? "unknown"} ({pred.Score:P1})"; | ||||
|                 graphics.DrawString(label, font, brush, rect.X, rect.Y - 15); | ||||
|             } | ||||
|  | ||||
|             return output; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 无需显式释放模型,资源由 YoloModelManager 管理 | ||||
|         /// </summary> | ||||
|         public void Dispose() | ||||
|         { | ||||
|             //_yoloModel = null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| //总结,区分好YoloModelManager.cs和YoloDetector.cs各自的职能,谁负责模型管理,谁负责yolo算法的执行,现在这两个文件是交织在一起比较乱的,有时间去处理一下 | ||||
| //看上面的定义的一些结构体胡总和类,就可以调用啦 | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| //using Check.Main.Camera; | ||||
| //using Check.Main.Common; | ||||
| //using SkiaSharp; | ||||
| //using System; | ||||
| //using System.Collections.Generic; | ||||
| //using System.Drawing; | ||||
| //using System.Linq; | ||||
| //using System.Text; | ||||
| //using System.Threading.Tasks; | ||||
| //using YoloDotNet; | ||||
| //using YoloDotNet.Models; | ||||
|  | ||||
|  | ||||
|  | ||||
| //namespace Check.Main.Infer | ||||
| //{ | ||||
| //    public class YoloDetector : IDetector | ||||
| //    { | ||||
| //        private Yolo _yoloModel; | ||||
| //        private int _modelID; // 需要知道模型ID来从 YoloModelManager 获取 | ||||
| //        private float _confidenceThreshold = 0.25f; | ||||
| //        private float _nmsThreshold = 0.45f; | ||||
|  | ||||
| //        public void Initialize(string modelIdStr, object detectionSettings = null) | ||||
| //        { | ||||
| //            if (!int.TryParse(modelIdStr, out _modelID)) | ||||
| //            { | ||||
| //                throw new ArgumentException("YoloDetector 初始化需要有效的模型ID字符串。", nameof(modelIdStr)); | ||||
| //            } | ||||
| //            _yoloModel = YoloModelManager.GetModel(_modelID); | ||||
| //            if (_yoloModel == null) | ||||
| //            { | ||||
| //                throw new InvalidOperationException($"YOLO 模型 (ID: {_modelID}) 未加载或找不到。"); | ||||
| //            } | ||||
|  | ||||
| //            if (detectionSettings is YoloDetectionSettings yoloSettings) | ||||
| //            { | ||||
| //                _confidenceThreshold = yoloSettings.ConfidenceThreshold; | ||||
| //                _nmsThreshold = yoloSettings.NmsThreshold; | ||||
| //                // 注意:YOLO模型的置信度和NMS阈值最好在YoloModelManager加载时设置 | ||||
| //                // 这里如果需要运行时调整,可能需要Yolo.SetThresholds方法 | ||||
| //            } | ||||
| //        } | ||||
|  | ||||
| //        public DetectionResult Detect(Bitmap image) | ||||
| //        { | ||||
| //            if (_yoloModel == null) | ||||
| //                throw new InvalidOperationException("YoloDetector 未初始化或模型未加载。"); | ||||
|  | ||||
| //            using (var skImage = CameraProcessor.ToSKBitmapFast(image)) // 使用 CameraProcessor 的静态方法 | ||||
| //            { | ||||
| //                if (skImage == null) | ||||
| //                { | ||||
| //                    return new DetectionResult(false, "图像转换失败"); | ||||
| //                } | ||||
|  | ||||
| //                // 在这里可以应用运行时阈值,如果Yolo模型支持 | ||||
| //                // _yoloModel.Confidence = _confidenceThreshold; | ||||
| //                // _yoloModel.Nms = _nmsThreshold; | ||||
|  | ||||
| //                var predictions = _yoloModel.RunObjectDetection(skImage); | ||||
|  | ||||
| //                bool isOk = !predictions.Any(); // 假设没有检测到任何目标为 OK | ||||
| //                string message = isOk ? "OK" : "NG"; | ||||
|  | ||||
| //                List<RectangleF> boundingBoxes = predictions.Select(p => new RectangleF(p.Rectangle.X, p.Rectangle.Y, p.Rectangle.Width, p.Rectangle.Height)).ToList(); | ||||
| //                Bitmap resultImage = DrawYoloPredictions(image, predictions); | ||||
|  | ||||
| //                return new DetectionResult(isOk, message, 0, boundingBoxes, resultImage); | ||||
| //            } | ||||
| //        } | ||||
|  | ||||
| //        private Bitmap DrawYoloPredictions(Bitmap originalImage, IEnumerable<YoloPrediction> predictions) | ||||
| //        { | ||||
| //            Bitmap resultBmp = (Bitmap)originalImage.Clone(); | ||||
| //            using (Graphics g = Graphics.FromImage(resultBmp)) | ||||
| //            { | ||||
| //                Pen ngPen = new Pen(Color.Red, 3); | ||||
| //                Font font = new Font("Arial", 12, FontStyle.Bold); | ||||
| //                Brush brush = new SolidBrush(Color.Red); | ||||
|  | ||||
| //                foreach (var p in predictions) | ||||
| //                { | ||||
| //                    Rectangle rect = new Rectangle((int)p.Rectangle.X, (int)p.Rectangle.Y, (int)p.Rectangle.Width, (int)p.Rectangle.Height); | ||||
| //                    g.DrawRectangle(ngPen, rect); | ||||
| //                    g.DrawString($"{p.Label} ({p.Confidence:P})", font, brush, rect.X, rect.Y - 20); | ||||
| //                } | ||||
| //            } | ||||
| //            return resultBmp; | ||||
| //        } | ||||
|  | ||||
| //        public void Dispose() | ||||
| //        { | ||||
| //            // YOLO 模型生命周期由 YoloModelManager 管理,这里不需要额外释放 | ||||
| //        } | ||||
| //    } | ||||
| //} | ||||
|  | ||||
|  | ||||
| //public DetectionResult Detect(Bitmap image) | ||||
| //{ | ||||
| //    if (_yoloModel == null) | ||||
| //        throw new InvalidOperationException("YoloDetector 未初始化或模型未加载。"); | ||||
|  | ||||
| //    using (var skBitmap = CameraProcessor.ToSKBitmapFast(image)) | ||||
| //    using (var skImage = SKImage.FromBitmap(skBitmap)) | ||||
| //    { | ||||
| //        //注意深拷贝浅拷贝的概念,复制的图片别忘了释放 | ||||
| //        var output = image.Clone(); | ||||
|  | ||||
| //        if (skImage == null) | ||||
| //            return new DetectionResult(false, "图像转换失败"); | ||||
|  | ||||
| //        //var results = _yoloModel.RunObjectDetection(skImage, confidence: 0.4f, iou: 0.5f); | ||||
|  | ||||
| //        var predictions = _yoloModel.RunObjectDetection( | ||||
| //            skImage, | ||||
| //            confidence: _confidenceThreshold, | ||||
| //            iou: _nmsThreshold | ||||
| //        ); | ||||
| //        var ExistBool = predictions.FirstOrDefault(res => res.Label.Name.Equals("logo", StringComparison.OrdinalIgnoreCase)); | ||||
| //        if (ExistBool != null) | ||||
| //        { | ||||
| //            var box = ExistBool.BoundingBox; | ||||
| //            var rect = new Rect(box.Left, box.Top, box.Width, box.Height); | ||||
|  | ||||
| //            //Cv2.Rectangle(output, rect, Scalar.Yellow, 2); | ||||
| //            //Cv2.PutText(output, $"{logoResult.Label.Name}: {logoResult.Confidence:P2}", new CvPoint(rect.X, rect.Y - 10), HersheyFonts.HersheySimplex, 0.6, Scalar.Yellow, 2); | ||||
| //        } | ||||
| //        bool isOk = !predictions.Any(); // 没有检测结果为OK | ||||
| //        string message = isOk ? "OK" : "NG"; | ||||
|  | ||||
| //        //var boundingBoxes = predictions | ||||
| //        //    .Select(p => new RectangleF(p.Rectangle.X, p.Rectangle.Y, p.Rectangle.Width, p.Rectangle.Height)) | ||||
| //        //    .ToList(); | ||||
|  | ||||
| //        //Bitmap resultImage = DrawYoloPredictions(image, predictions); | ||||
|  | ||||
| //        //return new DetectionResult(isOk, message, 0, boundingBoxes, resultImage); | ||||
| //    } | ||||
| //} | ||||
|  | ||||
| //// 注意:如果你使用的是 YoloResult,请改为 IEnumerable<YoloResult>     IEnumerable<YoloPrediction>  | ||||
| //private Bitmap DrawYoloPredictions(Bitmap originalImage, IEnumerable<YoloPrediction> predictions) | ||||
| //{ | ||||
| //    Bitmap resultBmp = (Bitmap)originalImage.Clone(); | ||||
| //    using (Graphics g = Graphics.FromImage(resultBmp)) | ||||
| //    { | ||||
| //        Pen boxPen = new Pen(Color.Red, 3); | ||||
| //        Font font = new Font("Arial", 12, FontStyle.Bold); | ||||
| //        Brush brush = new SolidBrush(Color.Red); | ||||
|  | ||||
| //        foreach (var p in predictions) | ||||
| //        { | ||||
| //            Rectangle rect = new Rectangle( | ||||
| //                (int)p.Rectangle.X, | ||||
| //                (int)p.Rectangle.Y, | ||||
| //                (int)p.Rectangle.Width, | ||||
| //                (int)p.Rectangle.Height | ||||
| //            ); | ||||
| //            g.DrawRectangle(boxPen, rect); | ||||
| //            g.DrawString($"{p.Label} ({p.Confidence:P0})", font, brush, rect.X, rect.Y - 20); | ||||
| //        } | ||||
|  | ||||
| //        boxPen.Dispose(); | ||||
| //        font.Dispose(); | ||||
| //        brush.Dispose(); | ||||
| //    } | ||||
| //    return resultBmp; | ||||
| //} | ||||
| // 错误②:CS0246 解决方法:确保 YoloPrediction 的完整命名空间正确引用。 | ||||
| // 由于你的文件开头已经有了 `using YoloDotNet.Models;` | ||||
| // 所以这里直接使用 `YoloPrediction` 应该是正确的,除非 `YoloPrediction` 不在该命名空间下。 | ||||
| // 如果问题依然存在,检查 YoloDotNet.Models 命名空间中 YoloPrediction 的具体定义。 | ||||
| //private Bitmap DrawYoloPredictions(Bitmap originalImage, IEnumerable<YoloPrediction> predictions) | ||||
| //{ | ||||
| //    Bitmap resultBmp = (Bitmap)originalImage.Clone(); | ||||
| //    using (Graphics g = Graphics.FromImage(resultBmp)) | ||||
| //    { | ||||
| //        Pen boxPen = new Pen(Color.Red, 3); | ||||
| //        Font font = new Font("Arial", 12, FontStyle.Bold); | ||||
| //        Brush brush = new SolidBrush(Color.Red); | ||||
|  | ||||
| //        foreach (var p in predictions) | ||||
| //        { | ||||
| //            Rectangle rect = new Rectangle( | ||||
| //                (int)p.Rectangle.X, | ||||
| //                (int)p.Rectangle.Y, | ||||
| //                (int)p.Rectangle.Width, | ||||
| //                (int)p.Rectangle.Height | ||||
| //            ); | ||||
| //            g.DrawRectangle(boxPen, rect); | ||||
| //            g.DrawString($"{p.Label} ({p.Confidence:P0})", font, brush, rect.X, rect.Y - 20); | ||||
| //        } | ||||
|  | ||||
| //        boxPen.Dispose(); | ||||
| //        font.Dispose(); | ||||
| //        brush.Dispose(); | ||||
| //    } | ||||
| //    return resultBmp; | ||||
| //} | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -31,40 +31,41 @@ namespace Check.Main.Infer | ||||
|             if (modelSettings == null) return; | ||||
|  | ||||
|             ThreadSafeLogger.Log("开始加载YOLO模型..."); | ||||
|             foreach (var setting in modelSettings) | ||||
|             // 筛选出启用的深度学习模型进行加载 | ||||
|             foreach (var setting in modelSettings.Where(s => s.IsEnabled && s.M_AType == AlgorithmType.DeepLearning)) | ||||
|             { | ||||
|                 bool gpuUse = false; | ||||
|                 if (setting.CheckDevice == DetectDevice.GPU) | ||||
|                 { | ||||
|                     gpuUse = true; | ||||
|                 } | ||||
|                 bool gpuUse = setting.CheckDevice == DetectDevice.GPU; | ||||
|  | ||||
|                 if (string.IsNullOrEmpty(setting.Path) || !File.Exists(setting.Path)) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[警告] 模型 '{setting.Name}' (ID: {setting.Id}) 路径无效或文件不存在,已跳过加载。"); | ||||
|                     ThreadSafeLogger.Log($"[警告] YOLO模型 '{setting.Name}' (ID: {setting.Id}) 路径无效或文件不存在,已跳过加载。"); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 try | ||||
|                 { | ||||
|                     // 创建YOLO实例 | ||||
|                     var yolo = new Yolo(new YoloOptions | ||||
|                     { | ||||
|                         OnnxModel = setting.Path, | ||||
|                         // 您可以根据需要从配置中读取这些值 | ||||
|                         ModelType = (YoloDotNet.Enums.ModelType)setting.MType, | ||||
|                         Cuda = gpuUse, // 推荐使用GPU | ||||
|                         PrimeGpu = false | ||||
|                         Cuda = gpuUse, | ||||
|                         //Confidence = setting.YoloConfidenceThreshold, // 从 ModelSettings 读取 | ||||
|                         //Nms = setting.YoloNmsThreshold,           // 从 ModelSettings 读取 | ||||
|                         PrimeGpu = false // 根据需求设置 | ||||
|                     }); | ||||
|  | ||||
|                     // 保存阈值配置 | ||||
|                     var conf = setting.YoloConfidenceThreshold; | ||||
|                     var nms = setting.YoloNmsThreshold; | ||||
|  | ||||
|                     if (_loadedModels.TryAdd(setting.Id, yolo)) | ||||
|                     { | ||||
|                         ThreadSafeLogger.Log($"成功加载模型 '{setting.Name}' (ID: {setting.Id})。"); | ||||
|                         ThreadSafeLogger.Log($"成功加载YOLO模型 '{setting.Name}' (ID: {setting.Id})。"); | ||||
|                     } | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[错误] 加载模型 '{setting.Name}' (ID: {setting.Id}) 失败: {ex.Message}"); | ||||
|                     ThreadSafeLogger.Log($"[错误] 加载YOLO模型 '{setting.Name}' (ID: {setting.Id}) 失败: {ex.Message}"); | ||||
|                 } | ||||
|             } | ||||
|             ThreadSafeLogger.Log($"YOLO模型加载完成,共成功加载 {_loadedModels.Count} 个模型。"); | ||||
|   | ||||
| @@ -17,8 +17,7 @@ namespace Check.Main.UI | ||||
| { | ||||
|     public partial class FrmConfig : DockContent | ||||
|     { | ||||
|         //private ProcessConfig _mainSettings=new ProcessConfig(); | ||||
|         //private readonly string _configFilePath = Path.Combine(Application.StartupPath, "main_config.xml"); | ||||
|  | ||||
|         public FrmConfig() | ||||
|         { | ||||
|             InitializeComponent(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user