From 73249ee6c2d73a252b5f27bd00d0c4d434739710 Mon Sep 17 00:00:00 2001 From: Maikouce China Date: Mon, 20 Oct 2025 17:47:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=A1=86=E6=9E=B6=EF=BC=88?= =?UTF-8?q?=E6=9C=AA=E5=AE=8C=E5=85=A8=E5=AE=8C=E6=88=90=EF=BC=89=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=8D=95=E4=B8=AA=E7=9B=B8=E6=9C=BA=E5=88=86=E5=BC=80?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=E7=AE=97=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Check.Main/Camera/CameraManager.cs | 265 ++++--------- Check.Main/Camera/CameraProcessor.cs | 213 ++++------- Check.Main/Check.Main.csproj | 2 +- Check.Main/Common/LogoMatcher.cs | 216 ++++++++++- Check.Main/Infer/DetectionCoordinator.cs | 274 ++++++++++++-- Check.Main/Infer/HalconTemplateDetector.cs | 57 +++ Check.Main/Infer/IDetector.cs | 62 ++++ Check.Main/Infer/ModelSettings.cs | 120 ++++-- Check.Main/Infer/YoloDetector.cs | 409 +++++++++++++++++++++ Check.Main/Infer/YoloModelManager.cs | 27 +- Check.Main/UI/FrmConfig.cs | 3 +- 11 files changed, 1226 insertions(+), 422 deletions(-) create mode 100644 Check.Main/Infer/HalconTemplateDetector.cs create mode 100644 Check.Main/Infer/IDetector.cs create mode 100644 Check.Main/Infer/YoloDetector.cs diff --git a/Check.Main/Camera/CameraManager.cs b/Check.Main/Camera/CameraManager.cs index bf3ecc0..a3fd5e6 100644 --- a/Check.Main/Camera/CameraManager.cs +++ b/Check.Main/Camera/CameraManager.cs @@ -25,39 +25,25 @@ namespace Check.Main.Camera public static class CameraManager { //1、相机与UI管理----管理每台相机对象和对应的图像显示窗口。 - // 活动的相机实例字典(键值对),键 (string):使用相机的名称,值 (HikvisionCamera):存储实际的相机实例,{ get; } 创建了一个只读的公共属性 + public static Dictionary ActiveCameras { get; } = new Dictionary(); - - // 相机对应的图像显示窗口字典 - //public static Dictionary CameraDisplays { get; } = new Dictionary(); - public static Dictionary OriginalImageDisplays { get; } = new Dictionary();//原始图像窗口 public static Dictionary ResultImageDisplays { get; } = new Dictionary();//结果图像窗口 //2、多相机同步逻辑 - // 【队列】一个产品需要多台相机拍完,才算完整。 private static readonly Queue ProductQueue = new Queue(); - // 【(队列)锁】保证队列在多线程下安全 private static readonly object QueueLock = new object(); - // 当前启用的相机数量,用于判断产品是否检测完毕 private static int EnabledCameraCount = 0; - //public static event EventHandler 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; - /// - /// 获取或设置模拟硬触发的间隔时间(毫秒)。 - /// + + // 获取或设置模拟硬触发的间隔时间(毫秒)。 public static double TriggerInterval { get; set; } = 1000; // 默认1秒触发一次 - /// - /// 获取一个值,该值指示硬件触发模拟器当前是否正在运行。 - /// + // 获取一个值,该值指示硬件触发模拟器当前是否正在运行。 public static bool IsHardwareTriggerSimulating { get; private set; } = false; /// @@ -73,113 +59,36 @@ namespace Check.Main.Camera } - //// 事件:用于向UI发送日志消息 - //public static event Action OnLogMessage; - //// 用于写入日志文件的 StreamWriter - //private static StreamWriter _logFileWriter; - - ////私有的、静态的、只读的对象,专门用作线程同步的“锁”。 - //private static readonly object _logLock = new object(); - - /// - /// 初始化文件日志记录器。应在程序启动时调用一次。 - /// - //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}"); - // } - //} - - /// - /// 【新增】一个完整的业务流程方法: - /// 1. 根据配置初始化所有相机并显示它们的窗口。 - /// 2. 在所有窗口都显示后,命令所有相机开始采集。 - /// 这个方法是响应“开启设备”或“应用配置并启动”按钮的理想入口点。 - /// - /// 要应用的相机配置列表。 - /// 主窗体,用于停靠显示窗口。 - //public static void ApplyConfigurationAndStartGrabbing(List 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("“开启设备”流程已完成。相机正在采集中。"); - //} + + ////********************初始化和启动流程******************* ///// - ///// 根据配置列表初始化或更新所有相机 + ///// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。 + ///// 这是“启动设备”的第一阶段。 ///// - //public static void Initialize(List 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); - // var deviceList = new HikvisionCamera().FindDevices(); + // // 3. 创建相机硬件实例和UI窗口------ + // var deviceList = new HikvisionCamera().FindDevices(); // if (deviceList.Count == 0) // { // ThreadSafeLogger.Log("错误:未找到任何相机设备!"); // 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 +112,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("所有设备和模型已准备就绪。"); //} - //********************初始化和启动流程******************* /// /// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。 /// 这是“启动设备”的第一阶段。 /// - 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 +151,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 +184,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 +212,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 +437,6 @@ namespace Check.Main.Camera // 3. 关闭检测协调器,它会负责清理所有后台线程和队列 DetectionCoordinator.Shutdown(); - YoloModelManager.Shutdown(); - ThreadSafeLogger.Log("所有相机及协调器资源已释放。"); } @@ -517,10 +445,11 @@ namespace Check.Main.Camera /// public static void ResetProductCounter() { - lock (_counterLock) - { - _productCounter = 0; - } + //lock (_counterLock) + //{ + // _productCounter = 0; + //} + DetectionCoordinator.ResetAllCounters(); // 调用协调器的重置方法 ThreadSafeLogger.Log("产品计数器已重置。"); } @@ -620,6 +549,7 @@ namespace Check.Main.Camera //} //【相机回调】 + // 【相机回调】现在只负责图像分发 private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp) { Bitmap bmpForDisplay = null; @@ -627,66 +557,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(); } - } /// @@ -808,32 +723,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上显示警告。 - // } - // } - //} - } } diff --git a/Check.Main/Camera/CameraProcessor.cs b/Check.Main/Camera/CameraProcessor.cs index e54b9ea..05af612 100644 --- a/Check.Main/Camera/CameraProcessor.cs +++ b/Check.Main/Camera/CameraProcessor.cs @@ -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 _imageQueue = new BlockingCollection(); private readonly Thread _workerThread; private volatile bool _isRunning = false; @@ -28,11 +30,12 @@ namespace Check.Main.Camera public event EventHandler 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)); } /// @@ -58,42 +62,38 @@ namespace Check.Main.Camera /// 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 + // { + // @"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 + // { + // @"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 - { - @"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 - { - @"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 } + /// /// 线程安全地重置该相机的图像计数器。 /// @@ -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(); } } diff --git a/Check.Main/Check.Main.csproj b/Check.Main/Check.Main.csproj index e03b440..e8aa4b0 100644 --- a/Check.Main/Check.Main.csproj +++ b/Check.Main/Check.Main.csproj @@ -39,7 +39,7 @@ ..\..\..\HslCommunication-master\HslCommunication.dll - ..\..\XKRS2025\CheckDevice\Check.Main\bin\Debug\MvCameraControl.Net.dll + C:\Program Files (x86)\MVS\Development\DotNet\win64\MvCameraControl.Net.dll diff --git a/Check.Main/Common/LogoMatcher.cs b/Check.Main/Common/LogoMatcher.cs index 61422ca..8725c13 100644 --- a/Check.Main/Common/LogoMatcher.cs +++ b/Check.Main/Common/LogoMatcher.cs @@ -158,6 +158,9 @@ namespace HalconTemplateMatch { private readonly List modelHandles = new List(); + // 构造函数用于初始化 + public LogoMatcher() { } + /// /// 从指定目录加载所有 .shm 模板文件 /// @@ -207,54 +210,231 @@ namespace HalconTemplateMatch } } - /// - /// 匹配并返回最高得分(double返回) - /// - public double FindLogo(Bitmap bmp) + // /// + // /// 匹配并返回最高得分(double返回) + // /// + // 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(), - 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 -); - + 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; + 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; + } + + + /// /// Bitmap 转 Halcon HObject /// diff --git a/Check.Main/Infer/DetectionCoordinator.cs b/Check.Main/Infer/DetectionCoordinator.cs index e472ece..a454118 100644 --- a/Check.Main/Infer/DetectionCoordinator.cs +++ b/Check.Main/Infer/DetectionCoordinator.cs @@ -25,6 +25,8 @@ namespace Check.Main.Infer /// private static int _enabledCameraCount = 0; + private static long _productCounter = 0; // 新增产品计数器10.22 + public static event EventHandler OnDetectionCompleted; public static bool IsDetectionRunning { get; private set; } = false; @@ -51,72 +53,220 @@ namespace Check.Main.Infer public static void Initialize(List cameraSettings, List 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) + { + // GetOrAdd 确保 ProductAssembly 只被创建一次 + var assembly = _productAssemblies.GetOrAdd(productId, (id) => new ProductAssembly(id, _enabledCameraCount)); + + assembly.AddResult(cameraIndex, isOk, resultImage); + + // 检查产品是否已完成所有相机的检测 + if (assembly.IsComplete()) { - string finalResult = assembly.GetFinalResult(); + 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 } } } - /// - /// 命令所有活动的相机处理器重置它们的内部计数器。 - /// + + ///// + ///// 命令所有活动的相机处理器重置它们的内部计数器。 + ///// + //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 _cameraResults = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _resultImages = new ConcurrentDictionary(); // 存储每个相机的结果图像 + private readonly object _lock = new object(); + + public ProductAssembly(long productId, int expectedCameraCount) + { + ProductId = productId; + _expectedCameraCount = expectedCameraCount; + } + + /// + /// 添加单个相机的检测结果。 + /// + /// 相机编号。 + /// 检测结果是否为OK。 + /// 带有检测结果的图像。 + 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(); // 释放传入的原始图像副本 + } + } + } + + /// + /// 检查是否所有相机都已提交结果。 + /// + public bool IsComplete() + { + lock (_lock) + { + return _cameraResults.Count == _expectedCameraCount; + } + } + + /// + /// 获取最终产品检测结果(所有相机都OK才为OK)。 + /// + public bool GetFinalResult() + { + lock (_lock) + { + return _cameraResults.Values.All(r => r); + } + } + + /// + /// 获取某个相机的结果图像。 + /// + 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(); + } + } + } } diff --git a/Check.Main/Infer/HalconTemplateDetector.cs b/Check.Main/Infer/HalconTemplateDetector.cs new file mode 100644 index 0000000..ffc1f41 --- /dev/null +++ b/Check.Main/Infer/HalconTemplateDetector.cs @@ -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 匹配参数 + } +} diff --git a/Check.Main/Infer/IDetector.cs b/Check.Main/Infer/IDetector.cs new file mode 100644 index 0000000..eabc8ba --- /dev/null +++ b/Check.Main/Infer/IDetector.cs @@ -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 + { + /// + /// 初始化检测器。 + /// + /// 模型文件路径(对于传统算法可能是模板目录) + /// 特定于检测器的设置对象(可选,可以用于传递阈值等) + void Initialize(string modelPath, object detectionSettings = null); + + /// + /// 执行图像检测。 + /// + /// 待检测的图像。 + /// 包含检测结果(如OK/NG,得分,边界框等)的统一对象。 + DetectionResult Detect(Bitmap image); + } + + /// + /// 统一的检测结果类。 + /// + public class DetectionResult + { + public bool IsOk { get; set; } + public string Message { get; set; } + public double Score { get; set; } + public List BoundingBoxes { get; set; } // 深度学习可能返回多个目标框 + + // 如果需要,可以添加带有绘制结果的图像 + public Bitmap ResultImage { get; set; } + + public DetectionResult(bool isOk, string message = "Unknown", double score = 0.0, List boundingBoxes = null, Bitmap resultImage = null) + { + IsOk = isOk; + Message = message; + Score = score; + BoundingBoxes = boundingBoxes ?? new List(); + 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; + } + +} diff --git a/Check.Main/Infer/ModelSettings.cs b/Check.Main/Infer/ModelSettings.cs index ee4241b..08a66aa 100644 --- a/Check.Main/Infer/ModelSettings.cs +++ b/Check.Main/Infer/ModelSettings.cs @@ -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,18 +121,18 @@ namespace Check.Main.Infer set { if (_mType != value) { _mType = value; OnPropertyChanged(); } } } + [Category("基本信息"), DisplayName("是否启用"), Description("是否在程序启动时是否启用模型")] public bool IsEnabled { get => _isEnabled; - set - { - if (_isEnabled != value) - { - _isEnabled = value; - OnPropertyChanged(); - } - } + set { + if (_isEnabled != value) + { + _isEnabled = value; + OnPropertyChanged(); + } + } } [Category("文件"), DisplayName("模型路径"), Description("选择模型文件(.onnx, .bin, etc., .pt)。")] @@ -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)); diff --git a/Check.Main/Infer/YoloDetector.cs b/Check.Main/Infer/YoloDetector.cs new file mode 100644 index 0000000..9baa019 --- /dev/null +++ b/Check.Main/Infer/YoloDetector.cs @@ -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 +{ + /// + /// YOLO 检测结果对象,包含标签、置信度与检测框 + /// + public class YoloPrediction + { + public YoloLabel Label { get; set; } + public float Score { get; set; } + public BoundingBox BoundingBox { get; set; } + } + + /// + /// YOLO 类别标签 + /// + public class YoloLabel + { + public string Name { get; set; } + } + + /// + /// 检测框坐标结构体 + /// + 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; + } + } + + /// + /// YOLO 目标检测器实现类 + /// + public class YoloDetector : IDetector, IDisposable + { + private Yolo _yoloModel; + private int _modelID; + private float _confidenceThreshold = 0.25f; + private float _nmsThreshold = 0.45f; + + /// + /// 初始化 YOLO 检测器并加载模型 + /// + /// 模型 ID 字符串 + /// 可选检测参数 + /// ID 无效 + /// 模型未加载 + 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); + } + } + + /// + /// 执行检测,判断是否含有 logo 类对象 + /// + 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}"); + } + } + + /// + /// 在图像上绘制检测框与标签 + /// + public Bitmap DrawYoloPredictions(Bitmap source, IEnumerable 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; + } + + /// + /// 无需显式释放模型,资源由 YoloModelManager 管理 + /// + 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 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 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 IEnumerable +//private Bitmap DrawYoloPredictions(Bitmap originalImage, IEnumerable 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 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; +//} + + + + + diff --git a/Check.Main/Infer/YoloModelManager.cs b/Check.Main/Infer/YoloModelManager.cs index 927da00..385e2da 100644 --- a/Check.Main/Infer/YoloModelManager.cs +++ b/Check.Main/Infer/YoloModelManager.cs @@ -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} 个模型。"); diff --git a/Check.Main/UI/FrmConfig.cs b/Check.Main/UI/FrmConfig.cs index f68d61a..79c67e1 100644 --- a/Check.Main/UI/FrmConfig.cs +++ b/Check.Main/UI/FrmConfig.cs @@ -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();