Compare commits

...

8 Commits

Author SHA1 Message Date
Maikouce China
bf111011d4 Merge branch 'master' of https://gitea.star-rising.cn/vcrmn/CheckDevice 2025-10-23 17:42:07 +08:00
Maikouce China
73249ee6c2 修改框架(未完全完成)实现单个相机分开绑定算法 2025-10-23 17:41:44 +08:00
Maikouce China
07c8e6ec91 222 2025-10-20 17:47:48 +08:00
TYF
31d9f8d6b6 取消 2025-10-20 17:44:48 +08:00
玉芬 唐
24a905b517 测试111 2025-10-20 17:42:34 +08:00
42f8bb7642 取消注释 2025-10-20 17:32:50 +08:00
Maikouce China
165963d337 Merge branch 'master' of https://gitea.star-rising.cn/vcrmn/CheckDevice 2025-10-20 17:28:25 +08:00
Maikouce China
715d7166bc 增加注释 2025-10-20 17:28:18 +08:00
12 changed files with 1229 additions and 424 deletions

View File

@@ -22,42 +22,29 @@ namespace Check.Main.Camera
/// <summary> /// <summary>
/// 静态全局相机管理器,负责所有相机的生命周期、配置应用和多相机同步 /// 静态全局相机管理器,负责所有相机的生命周期、配置应用和多相机同步
/// </summary> /// </summary>
/// 222222
public static class CameraManager public static class CameraManager
{ {
//1、相机与UI管理----管理每台相机对象和对应的图像显示窗口。 //1、相机与UI管理----管理每台相机对象和对应的图像显示窗口。
// 活动的相机实例字典(键值对),键 (string):使用相机的名称,值 (HikvisionCamera):存储实际的相机实例,{ get; } 创建了一个只读的公共属性
public static Dictionary<string, HikvisionCamera> ActiveCameras { get; } = new Dictionary<string, HikvisionCamera>(); 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> OriginalImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();//原始图像窗口
public static Dictionary<string, FormImageDisplay> ResultImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();//结果图像窗口 public static Dictionary<string, FormImageDisplay> ResultImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();//结果图像窗口
//2、多相机同步逻辑 //2、多相机同步逻辑
// 【队列】一个产品需要多台相机拍完,才算完整。
private static readonly Queue<ProductResult> ProductQueue = new Queue<ProductResult>(); private static readonly Queue<ProductResult> ProductQueue = new Queue<ProductResult>();
// 【(队列)锁】保证队列在多线程下安全
private static readonly object QueueLock = new object(); private static readonly object QueueLock = new object();
// 当前启用的相机数量,用于判断产品是否检测完毕
private static int EnabledCameraCount = 0; 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; private static long _productCounter = 0;
// 用于同步计数器访问的锁虽然long的自增是原子操作但为清晰和未来扩展使用锁是好习惯
private static readonly object _counterLock = new object(); private static readonly object _counterLock = new object();
// 3、--- 新增:硬触发模拟器 --- // 3、--- 新增:硬触发模拟器 ---
private static readonly System.Timers.Timer _hardwareTriggerSimulator; private static readonly System.Timers.Timer _hardwareTriggerSimulator;
/// <summary>
/// 获取或设置模拟硬触发的间隔时间(毫秒)。 // 获取或设置模拟硬触发的间隔时间(毫秒)。
/// </summary>
public static double TriggerInterval { get; set; } = 1000; // 默认1秒触发一次 public static double TriggerInterval { get; set; } = 1000; // 默认1秒触发一次
/// <summary> // 获取一个值,该值指示硬件触发模拟器当前是否正在运行。
/// 获取一个值,该值指示硬件触发模拟器当前是否正在运行。
/// </summary>
public static bool IsHardwareTriggerSimulating { get; private set; } = false; public static bool IsHardwareTriggerSimulating { get; private set; } = false;
/// <summary> /// <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> ///// <summary>
///// 根据配置列表初始化或更新所有相机 ///// 准备所有相机硬件、UI窗口和后台处理器但不开始采集。
///// 这是“启动设备”的第一阶段。
///// </summary> ///// </summary>
//public static void Initialize(List<CameraSettings> settingsList, FrmMain mainForm) //public static void PrepareAll(ProcessConfig config, FrmMain mainForm)//①准备所有相机和模型
//{ //{
// // 1. 清理旧资源和UI
// // 先停止并释放所有旧的相机 // mainForm.ClearStatusStrip();
// Shutdown(); // Shutdown();
// ThreadSafeLogger.Log("开始应用新的相机配置..."); // ThreadSafeLogger.Log("开始准备设备和模型...");
// EnabledCameraCount = settingsList.Count(s => s.IsEnabled); // // 2. 初始化检测协调器和AI模型
// if (EnabledCameraCount == 0) // // 注意YoloModelManager 的 Initialize 现在也应在这里被调用,以确保逻辑集中
// { // YoloModelManager.Initialize(config.ModelSettings);
// ThreadSafeLogger.Log("没有启用的相机。"); // DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings);
// return;
// }
// // 3. 创建相机硬件实例和UI窗口------
// var deviceList = new HikvisionCamera().FindDevices(); // var deviceList = new HikvisionCamera().FindDevices();
// if (deviceList.Count == 0) // if (deviceList.Count == 0)
// { // {
@@ -167,19 +87,9 @@ namespace Check.Main.Camera
// return; // return;
// } // }
// int deviceIndex = 0; // foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled))//打开相机 → 设置触发模式 → 绑定事件(图像采集、检测完成)。
// foreach (var setting in settingsList)
// { // {
// if (!setting.IsEnabled) continue; // var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex };
// if (deviceIndex >= deviceList.Count)
// {
// ThreadSafeLogger.Log($"警告:相机配置'{setting.Name}'无法分配物理设备,因为设备数量不足。");
// continue;
// }
// // --- 创建相机实例 ---
// var cam = new HikvisionCamera { Name = setting.Name };
// cam.TriggerMode = setting.TriggerMode; // cam.TriggerMode = setting.TriggerMode;
// if (!cam.Open(setting)) // if (!cam.Open(setting))
// { // {
@@ -203,40 +113,38 @@ namespace Check.Main.Camera
// // --- 订阅事件 --- // // --- 订阅事件 ---
// cam.ImageAcquired += OnCameraImageAcquired; // cam.ImageAcquired += OnCameraImageAcquired;
// cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}"); // var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex);
// if (processor != null) { processor.OnProcessingCompleted += Processor_OnProcessingCompleted; }
// // --- 创建显示窗口 ---
// var displayForm = new FormImageDisplay { Text = setting.Name, CameraName = setting.Name };
// displayForm.OnDisplayEvent += ThreadSafeLogger.Log;
// displayForm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
// // --- 创建【但不显示】图像的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); // ActiveCameras.Add(setting.Name, cam);
// CameraDisplays.Add(setting.Name, displayForm); // OriginalImageDisplays.Add(setting.Name, originalDisplay);
// ResultImageDisplays.Add(setting.Name, resultDisplay);
// mainForm.AddCameraToStatusStrip(setting.Name); // mainForm.AddCameraToStatusStrip(setting.Name);
// ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。");
// deviceIndex++;
// } // }
// ThreadSafeLogger.Log("所有设备和模型已准备就绪。");
//} //}
//********************初始化和启动流程*******************
/// <summary> /// <summary>
/// 准备所有相机硬件、UI窗口和后台处理器但不开始采集。 /// 准备所有相机硬件、UI窗口和后台处理器但不开始采集。
/// 这是“启动设备”的第一阶段。 /// 这是“启动设备”的第一阶段。
/// </summary> /// </summary>
public static void PrepareAll(ProcessConfig config, FrmMain mainForm)//①准备所有相机和模型 public static void PrepareAll(ProcessConfig config, FrmMain mainForm)
{ {
// 1. 清理旧资源和UI mainForm.ClearStatusStrip(); // 清理旧的状态条
mainForm.ClearStatusStrip(); Shutdown(); // 清理旧的相机和协调器资源
Shutdown();
ThreadSafeLogger.Log("开始准备设备和模型..."); ThreadSafeLogger.Log("开始准备设备和模型...");
// 2. 初始化检测协调器和AI模型 // 1. 初始化检测协调器和AI模型此步骤会加载YOLO模型并创建CameraProcessor
// 注意YoloModelManager 的 Initialize 现在也应在这里被调用,以确保逻辑集中
YoloModelManager.Initialize(config.ModelSettings);
DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings); DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings);
// 3. 创建相机硬件实例和UI窗口------ // 2. 创建相机硬件实例和UI窗口
var deviceList = new HikvisionCamera().FindDevices(); var deviceList = new HikvisionCamera().FindDevices();
if (deviceList.Count == 0) if (deviceList.Count == 0)
{ {
@@ -244,8 +152,15 @@ namespace Check.Main.Camera
return; 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 }; var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex };
cam.TriggerMode = setting.TriggerMode; cam.TriggerMode = setting.TriggerMode;
if (!cam.Open(setting)) if (!cam.Open(setting))
@@ -270,12 +185,26 @@ namespace Check.Main.Camera
// --- 订阅事件 --- // --- 订阅事件 ---
cam.ImageAcquired += OnCameraImageAcquired; cam.ImageAcquired += OnCameraImageAcquired;
var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex); cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}");
if (processor != null) { processor.OnProcessingCompleted += Processor_OnProcessingCompleted; }
// --- 创建【但不显示】图像的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 originalDisplay = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name };
var resultDisplay = 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); originalDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
resultDisplay.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); OriginalImageDisplays.Add(setting.Name, originalDisplay);
ResultImageDisplays.Add(setting.Name, resultDisplay); ResultImageDisplays.Add(setting.Name, resultDisplay);
mainForm.AddCameraToStatusStrip(setting.Name); mainForm.AddCameraToStatusStrip(setting.Name);
ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。");
deviceIndex++;
} }
ThreadSafeLogger.Log("所有设备和模型已准备就绪。"); ThreadSafeLogger.Log("所有设备和模型已准备就绪。");
} }
@@ -507,8 +438,6 @@ namespace Check.Main.Camera
// 3. 关闭检测协调器,它会负责清理所有后台线程和队列 // 3. 关闭检测协调器,它会负责清理所有后台线程和队列
DetectionCoordinator.Shutdown(); DetectionCoordinator.Shutdown();
YoloModelManager.Shutdown();
ThreadSafeLogger.Log("所有相机及协调器资源已释放。"); ThreadSafeLogger.Log("所有相机及协调器资源已释放。");
} }
@@ -517,10 +446,11 @@ namespace Check.Main.Camera
/// </summary> /// </summary>
public static void ResetProductCounter() public static void ResetProductCounter()
{ {
lock (_counterLock) //lock (_counterLock)
{ //{
_productCounter = 0; // _productCounter = 0;
} //}
DetectionCoordinator.ResetAllCounters(); // 调用协调器的重置方法
ThreadSafeLogger.Log("产品计数器已重置。"); ThreadSafeLogger.Log("产品计数器已重置。");
} }
@@ -620,6 +550,7 @@ namespace Check.Main.Camera
//} //}
//【相机回调】 //【相机回调】
// 【相机回调】现在只负责图像分发
private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp) private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp)
{ {
Bitmap bmpForDisplay = null; Bitmap bmpForDisplay = null;
@@ -627,66 +558,51 @@ namespace Check.Main.Camera
try try
{ {
// 1. 深度克隆 Bitmap → 分成 显示副本 和 处理副本。
bmpForDisplay = DeepCloneBitmap(bmp, "Display"); bmpForDisplay = DeepCloneBitmap(bmp, "Display");
bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); bmpForProcessing = DeepCloneBitmap(bmp, "Processing");
} }
finally finally
{ {
// 2.无论克隆成功与否都必须立即释放事件传递过来的原始bmp防止泄漏。 bmp?.Dispose();
bmp?.Dispose();//空条件运算符 ?.-----在访问对象成员前检查对象是否为 null如果 bmp 不是 null则正常调用 Dispose() 方法,替代传统的 if (bmp != null) 检查
} }
// 分支 A: 显示副本 → 交给 FormImageDisplay.UpdateImage()。
if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow)) if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow))
{ {
// displayWindow.UpdateImage 会处理线程安全问题
displayWindow.UpdateImage(bmpForDisplay); displayWindow.UpdateImage(bmpForDisplay);
} }
else else
{ {
// 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本
bmpForDisplay?.Dispose(); bmpForDisplay?.Dispose();
} }
// 分支 B: 处理副本 → 交给 DetectionCoordinator.EnqueueImage() 进行检测。
// 直接将处理副本和相机编号交给 DetectionCoordinator
if (bmpForProcessing != null) if (bmpForProcessing != null)
{ {
// bmpForProcessing 的所有权在这里被转移给了协调器
DetectionCoordinator.EnqueueImage(sender.CameraIndex, 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) private static void Processor_OnProcessingCompleted(object sender, ProcessingCompletedEventArgs e)
{ {
// 1. 找到与此相机匹配的相机名称
var cameraEntry = ActiveCameras.FirstOrDefault(kvp => kvp.Value.CameraIndex == e.CameraIndex); var cameraEntry = ActiveCameras.FirstOrDefault(kvp => kvp.Value.CameraIndex == e.CameraIndex);
if (cameraEntry.Key == null) if (cameraEntry.Key == null)
{ {
e.Dispose(); // 如果找不到接收者,必须释放事件参数中的图像 e.Dispose();
return; return;
} }
// 2. 找到此相机的结果显示窗口
if (ResultImageDisplays.TryGetValue(cameraEntry.Key, out var resultDisplay)) if (ResultImageDisplays.TryGetValue(cameraEntry.Key, out var resultDisplay))
{ {
//var bmp = ConvertSKImageToBitmap(e.ResultImage);
if (e.ResultImage != null) if (e.ResultImage != null)
{ {
// UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可
resultDisplay.UpdateImage(e.ResultImage); resultDisplay.UpdateImage(e.ResultImage);
} }
} }
else else
{ {
// 如果找到了相机但没有对应的结果窗口,也要释放图像
e.Dispose(); e.Dispose();
} }
} }
/// <summary> /// <summary>
@@ -808,32 +724,6 @@ namespace Check.Main.Camera
return clone; 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上显示警告。
// }
// }
//}
} }
} }

View File

@@ -12,13 +12,15 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using YoloDotNet.Extensions; using YoloDotNet.Extensions;
using OpenCvSharp; using OpenCvSharp;
using Check.Main.Infer; // 引入 IDetector, DetectionResult
namespace Check.Main.Camera namespace Check.Main.Camera
{ {
public class CameraProcessor : IDisposable public class CameraProcessor : IDisposable
{ {
private readonly int _cameraIndex; private readonly int _cameraIndex;
private readonly int _modeId; private readonly IDetector _detector; // 替换为接口
// private readonly ModelSettings _model; private readonly ModelSettings _modelSettings; // 保留模型设置以获取更多参数
private readonly BlockingCollection<ImageData> _imageQueue = new BlockingCollection<ImageData>(); private readonly BlockingCollection<ImageData> _imageQueue = new BlockingCollection<ImageData>();
private readonly Thread _workerThread; private readonly Thread _workerThread;
private volatile bool _isRunning = false; private volatile bool _isRunning = false;
@@ -28,11 +30,12 @@ namespace Check.Main.Camera
public event EventHandler<ProcessingCompletedEventArgs> OnProcessingCompleted; 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; _cameraIndex = cameraIndex;
_modeId = modelId; _detector = detector ?? throw new ArgumentNullException(nameof(detector));
//_model = model; _modelSettings = modelSettings ?? throw new ArgumentNullException(nameof(modelSettings));
_workerThread = new Thread(ProcessQueue) { IsBackground = true, Name = $"Cam_{_cameraIndex}_Processor" }; _workerThread = new Thread(ProcessQueue) { IsBackground = true, Name = $"Cam_{_cameraIndex}_Processor" };
} }
@@ -42,15 +45,16 @@ namespace Check.Main.Camera
_workerThread.Start(); _workerThread.Start();
} }
public void EnqueueImage(Bitmap bmp) public void EnqueueImage(Bitmap bmp, long productId) // 接收产品ID
{ {
if (!_isRunning) if (!_isRunning)
{ {
bmp?.Dispose(); bmp?.Dispose();
return; return;
} }
_imageCounter++; // _imageCounter 在此用于内部跟踪不是产品ID
_imageQueue.Add(new ImageData(_imageCounter, _cameraIndex, bmp)); // 产品ID现在由 DetectionCoordinator 生成并传递
_imageQueue.Add(new ImageData(productId, _cameraIndex, bmp));
} }
/// <summary> /// <summary>
@@ -58,42 +62,38 @@ namespace Check.Main.Camera
/// </summary> /// </summary>
private void ProcessQueue() private void ProcessQueue()
{ {
//// 从模型管理器获取此线程专属的YOLO模型
//var yoloModel = YoloModelManager.GetModel(_modeId); ThreadSafeLogger.Log($"相机#{_cameraIndex} 启动处理线程,算法类型:{_modelSettings.M_AType}");
//if (yoloModel == null)
////训练HALCON模型
////训练阶段相机2
//var trainer = new LogoTemplateTrainer();
//trainer.TrainAndSaveTemplates(
// new List<string>
// { // {
// ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型处理线程已中止。"); // @"D:\HalconTemplateMatch\train2\logo1.bmp",
// return; // 如果没有模型,此线程无法工作 // @"D:\HalconTemplateMatch\train2\logo2.bmp",
// @"D:\HalconTemplateMatch\train2\logo3.bmp"
// },
// @"D:\HalconTemplateMatch\model_2");
////训练阶段相机39.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;
//} //}
//训练阶段相机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");
//训练阶段相机39.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) while (_isRunning)
{ {
@@ -103,116 +103,23 @@ namespace Check.Main.Camera
ImageData data = _imageQueue.Take(); ImageData data = _imageQueue.Take();
using (data) 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; if (data.Image == null) continue;
// 统一定义预测结果
var matcher = new LogoMatcher();
//9.25(增加一根据不同的相机编号调用不同的模型!) DetectionResult detectionResult = _detector.Detect(data.Image);
string filepath = ""; ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},结果: {(detectionResult.IsOk ? "OK" : "NG")}, 信息: {detectionResult.Message}, 得分: {detectionResult.Score:F2}");
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}");
// 将处理结果交给协调器进行组装 // 将处理结果交给协调器进行组装
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( OnProcessingCompleted?.Invoke(
this, this,
new ProcessingCompletedEventArgs new ProcessingCompletedEventArgs
( (
_cameraIndex, _cameraIndex,
data.ProductId, data.ProductId,
data.Image // 原图传出去 detectionResult.ResultImage // 传递带有绘制结果的图像
) )
); );
} }
@@ -252,6 +159,7 @@ namespace Check.Main.Camera
} }
public static SKBitmap ToSKBitmapFast(Bitmap bitmap) public static SKBitmap ToSKBitmapFast(Bitmap bitmap)
{ {
if (bitmap == null) return null;
// 确保是 32bppArgbBGRA 内存布局) // 确保是 32bppArgbBGRA 内存布局)
Bitmap src = bitmap; Bitmap src = bitmap;
if (bitmap.PixelFormat != PixelFormat.Format32bppArgb) if (bitmap.PixelFormat != PixelFormat.Format32bppArgb)
@@ -298,13 +206,15 @@ namespace Check.Main.Camera
} }
} }
} }
public void Stop() public void Stop()
{ {
_isRunning = false; _isRunning = false;
// 解除阻塞,让线程可以检查 _isRunning 标志并退出 // 解除阻塞,让线程可以检查 _isRunning 标志并退出
_imageQueue.CompleteAdding(); _imageQueue.CompleteAdding();
_workerThread.Join(500); // 等待线程结束 _workerThread.Join(5000); // 等待线程结束。10.22修改原来是500
} }
/// <summary> /// <summary>
/// 线程安全地重置该相机的图像计数器。 /// 线程安全地重置该相机的图像计数器。
/// </summary> /// </summary>
@@ -322,6 +232,27 @@ namespace Check.Main.Camera
{ {
Stop(); Stop();
_imageQueue.Dispose(); _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();
} }
} }

View File

@@ -39,7 +39,7 @@
<HintPath>..\..\..\HslCommunication-master\HslCommunication.dll</HintPath> <HintPath>..\..\..\HslCommunication-master\HslCommunication.dll</HintPath>
</Reference> </Reference>
<Reference Include="MvCameraControl.Net"> <Reference Include="MvCameraControl.Net">
<HintPath>..\..\XKRS2025\CheckDevice\Check.Main\bin\Debug\MvCameraControl.Net.dll</HintPath> <HintPath>C:\Program Files (x86)\MVS\Development\DotNet\win64\MvCameraControl.Net.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>

View File

@@ -158,6 +158,9 @@ namespace HalconTemplateMatch
{ {
private readonly List<HTuple> modelHandles = new List<HTuple>(); private readonly List<HTuple> modelHandles = new List<HTuple>();
// 构造函数用于初始化
public LogoMatcher() { }
/// <summary> /// <summary>
/// 从指定目录加载所有 .shm 模板文件 /// 从指定目录加载所有 .shm 模板文件
/// </summary> /// </summary>
@@ -207,33 +210,80 @@ namespace HalconTemplateMatch
} }
} }
/// <summary> // /// <summary>
/// 匹配并返回最高得分double返回 // /// 匹配并返回最高得分double返回
/// </summary> // /// </summary>
public double FindLogo(Bitmap bmp) // 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) if (modelHandles.Count == 0)
{ {
Console.WriteLine("[警告] 尚未加载任何模板。"); Console.WriteLine("[警告] 尚未加载任何模板。");
resultImage = (Bitmap)bmp.Clone();
return -1; return -1;
} }
// Bitmap 转 Halcon 对象
HObject ho_TestImage; HObject ho_TestImage;
Bitmap2HObject(bmp, out ho_TestImage); Bitmap2HObject(bmp, out ho_TestImage);
HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage); HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage);
double bestScore = -1; 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 try
{ {
HOperatorSet.FindScaledShapeModel( HOperatorSet.FindScaledShapeModel(
ho_TestImage, ho_TestImage,
modelID, modelID,
new HTuple(0).TupleRad(), new HTuple(0).TupleRad(), new HTuple(360).TupleRad(),
new HTuple(360).TupleRad(),
0.8, 1.2, 0.8, 1.2,
0.5, 1, 0.5, 0.5, 1, 0.5,
"least_squares_high", "least_squares_high",
@@ -241,20 +291,150 @@ namespace HalconTemplateMatch
out HTuple row, out HTuple col, out HTuple angle, out HTuple scale, out HTuple score out HTuple row, out HTuple col, out HTuple angle, out HTuple scale, out HTuple score
); );
if (score.Length > 0 && score[0].D > bestScore) if (score.Length > 0 && score[0].D > bestScore)
{
bestScore = score[0].D; bestScore = score[0].D;
bestRow = row;
bestCol = col;
bestAngle = angle;
bestScale = scale;
bestModelIndex = i;
}
} }
catch (HOperatorException ex) 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(); ho_TestImage.Dispose();
return bestScore; 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> /// <summary>
/// Bitmap 转 Halcon HObject /// Bitmap 转 Halcon HObject
/// </summary> /// </summary>

View File

@@ -2,13 +2,13 @@
using HslCommunication.ModBus; using HslCommunication.ModBus;
using System; using System;
using System.Net.Sockets; using System.Net.Sockets;
//
// ModbusTcp读写服务类线程安全互斥锁 // ModbusTcp读写服务类线程安全互斥锁
namespace Check.Main.Common namespace Check.Main.Common
{ {
public class ModbusTcpService public class ModbusTcpService
{ {
private readonly TcpClient _tcpClient = new();// private readonly TcpClient _tcpClient = new();
private readonly ModbusTcpNet _plc; private readonly ModbusTcpNet _plc;
private readonly object _lock = new(); private readonly object _lock = new();
public bool IsConnected => _tcpClient != null && _tcpClient.Connected;// public bool IsConnected => _tcpClient != null && _tcpClient.Connected;//

View File

@@ -25,6 +25,8 @@ namespace Check.Main.Infer
/// </summary> /// </summary>
private static int _enabledCameraCount = 0; private static int _enabledCameraCount = 0;
private static long _productCounter = 0; // 新增产品计数器10.22
public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted; public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted;
public static bool IsDetectionRunning { get; private set; } = false; 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) public static void Initialize(List<CameraSettings> cameraSettings, List<ModelSettings> modelSettings)
{ {
Shutdown(); // 先关闭旧的 Shutdown(); // 先关闭旧的
YoloModelManager.Initialize(modelSettings); // 确保 YOLO 模型在初始化协调器时加载。10.22新增
var enabledCameras = cameraSettings.Where(c => c.IsEnabled).ToList(); var enabledCameras = cameraSettings.Where(c => c.IsEnabled).ToList();
_enabledCameraCount = enabledCameras.Count; _enabledCameraCount = enabledCameras.Count;
if (_enabledCameraCount == 0) return; //if (_enabledCameraCount == 0) return;
if (_enabledCameraCount == 0)
{
ThreadSafeLogger.Log("没有启用的相机,检测协调器未初始化。");
return;
}
foreach (var camSetting in enabledCameras) 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) if (model == null)
{ {
ThreadSafeLogger.Log($"[警告] 找不到与相机 #{camSetting.CameraIndex} 匹配的模型,该相机将无法处理图像。"); //ThreadSafeLogger.Log($"[警告] 找不到与相机 #{camSetting.CameraIndex} 匹配的模型,该相机将无法处理图像");
ThreadSafeLogger.Log($"[警告] 找不到与相机 #{camSetting.CameraIndex} (Name: {camSetting.Name}) 匹配的模型 (ID: {camSetting.ModelID})。该相机将无法处理图像。");
continue; 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
{
// 对于YOLOmodelPath实际上传递的是ModelID
// 对于HALCONmodelPath是实际的模板目录
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); _processors.TryAdd(camSetting.CameraIndex, processor);
processor.Start(); processor.Start();
} }
ThreadSafeLogger.Log($"检测协调器已初始化,启动了 {_processors.Count} 个相机处理线程。"); 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) public static void EnqueueImage(int cameraIndex, Bitmap bmp)
{ {
// 在图像进入队列之前生成一个新的产品ID
long currentProductId;
lock (_productAssemblies) // 同步访问产品计数器
{
_productCounter++;
currentProductId = _productCounter;
}
if (_processors.TryGetValue(cameraIndex, out var processor)) if (_processors.TryGetValue(cameraIndex, out var processor))
{ {
processor.EnqueueImage(bmp); processor.EnqueueImage(bmp, currentProductId); // 传递产品ID
} }
else else
{ {
// 如果找不到处理器必须释放Bitmap防止泄漏 bmp?.Dispose(); // 如果找不到处理器必须释放Bitmap防止泄漏
bmp?.Dispose(); ThreadSafeLogger.Log($"[警告] 未能为相机 {cameraIndex} 找到处理器,产品 {currentProductId} 的图像被丢弃。");
// 如果没有处理器,不需要在 _productAssemblies 中添加,因为不会有结果返回
} }
} }
// 供 CameraProcessor 回调,用以组装产品 //// 供 CameraProcessor 回调,用以组装产品
public static void AssembleProduct(ImageData data, string result) //public static void AssembleProduct(ImageData data, string result)
{ //{
var assembly = _productAssemblies.GetOrAdd(data.ProductId, (id) => new ProductAssembly(id, _enabledCameraCount)); // 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}"); ThreadSafeLogger.Log($"产品 #{assembly.ProductId} 已检测完毕,最终结果: {finalResult}");
// 只有在检测运行时,才触发事件 // 触发事件 (例如更新主UI上的总OK/NG计数)
if (IsDetectionRunning) 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} 检测结果未能写入PLCPLC客户端未连接。");
}
// 移除并释放 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() public static void ResetAllCounters()
{ {
lock (_productAssemblies)
{
_productCounter = 0;
// 清空所有未完成的产品,并释放其资源
foreach (var assembly in _productAssemblies.Values)
{
assembly.Dispose();
}
_productAssemblies.Clear();
}
foreach (var processor in _processors.Values) foreach (var processor in _processors.Values)
{ {
processor.ResetCounter(); processor.ResetCounter();
} }
ThreadSafeLogger.Log("所有相机处理器产品计数器已重置。"); ThreadSafeLogger.Log("所有相机处理器产品计数器已重置。");
} }
public static CameraProcessor GetProcessor(int cameraIndex) public static CameraProcessor GetProcessor(int cameraIndex)
@@ -142,8 +292,88 @@ namespace Check.Main.Infer
{ {
assembly.Dispose(); assembly.Dispose();
} }
_productAssemblies.Clear(); YoloModelManager.Shutdown(); // 确保YOLO模型也关闭
ThreadSafeLogger.Log("检测协调器已关闭。"); 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();
}
}
}
} }

View 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 匹配参数
}
}

View 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;
}
}

View File

@@ -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;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@@ -7,28 +17,12 @@ using System.Runtime.CompilerServices;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms.Design;
using System.Drawing.Design;
namespace Check.Main.Infer 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 public enum CheckModelType
{ {
[Description("分类")] [Description("分类")]
@@ -47,9 +41,32 @@ namespace Check.Main.Infer
PoseEstimation PoseEstimation
} }
[Serializable] // 确保可被XML序列化 public enum AlgorithmType
public class ModelSettings : INotifyPropertyChanged, ICloneable
{ {
[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; public event PropertyChangedEventHandler PropertyChanged;
private int _id; private int _id;
@@ -57,9 +74,14 @@ namespace Check.Main.Infer
private string _path = ""; private string _path = "";
private DetectDevice _checkDevice=DetectDevice.CPU; private DetectDevice _checkDevice=DetectDevice.CPU;
private AlgorithmType _mAType = AlgorithmType.Tradition; private AlgorithmType _mAType = AlgorithmType.Tradition;
private CheckModelType _mType = CheckModelType.Classification; private CheckModelType _mType = CheckModelType.ObjectDetection;
private bool _isEnabled = true; 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("模型的唯一标识符,用于与相机编号对应。")] [Category("基本信息"), DisplayName("模型编号"), Description("模型的唯一标识符,用于与相机编号对应。")]
public int Id public int Id
{ {
@@ -73,13 +95,6 @@ namespace Check.Main.Infer
get => _name; get => _name;
set { if (_name != value) { _name = value; OnPropertyChanged(); } } 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("所使用的算法的类型。")] [Category("基本信息"), DisplayName("算法类型"), Description("所使用的算法的类型。")]
[TypeConverter(typeof(EnumDescriptionTypeConverter))] [TypeConverter(typeof(EnumDescriptionTypeConverter))]
@@ -88,6 +103,16 @@ namespace Check.Main.Infer
get => _mAType; get => _mAType;
set { if (_mAType != value) { _mAType = value; OnPropertyChanged(); } } 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("推理模型的类型。")] [Category("基本信息"), DisplayName("模型类型"), Description("推理模型的类型。")]
[TypeConverter(typeof(EnumDescriptionTypeConverter))] [TypeConverter(typeof(EnumDescriptionTypeConverter))]
public CheckModelType MType public CheckModelType MType
@@ -96,12 +121,12 @@ namespace Check.Main.Infer
set { if (_mType != value) { _mType = value; OnPropertyChanged(); } } set { if (_mType != value) { _mType = value; OnPropertyChanged(); } }
} }
[Category("基本信息"), DisplayName("是否启用"), Description("是否在程序启动时是否启用模型")] [Category("基本信息"), DisplayName("是否启用"), Description("是否在程序启动时是否启用模型")]
public bool IsEnabled public bool IsEnabled
{ {
get => _isEnabled; get => _isEnabled;
set set {
{
if (_isEnabled != value) if (_isEnabled != value)
{ {
_isEnabled = value; _isEnabled = value;
@@ -118,6 +143,27 @@ namespace Check.Main.Infer
set { if (_path != value) { _path = value; OnPropertyChanged(); } } 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) protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

View 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;
//}

View File

@@ -31,40 +31,41 @@ namespace Check.Main.Infer
if (modelSettings == null) return; if (modelSettings == null) return;
ThreadSafeLogger.Log("开始加载YOLO模型..."); 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; bool gpuUse = setting.CheckDevice == DetectDevice.GPU;
if (setting.CheckDevice == DetectDevice.GPU)
{
gpuUse = true;
}
if (string.IsNullOrEmpty(setting.Path) || !File.Exists(setting.Path)) 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; continue;
} }
try try
{ {
// 创建YOLO实例
var yolo = new Yolo(new YoloOptions var yolo = new Yolo(new YoloOptions
{ {
OnnxModel = setting.Path, OnnxModel = setting.Path,
// 您可以根据需要从配置中读取这些值
ModelType = (YoloDotNet.Enums.ModelType)setting.MType, ModelType = (YoloDotNet.Enums.ModelType)setting.MType,
Cuda = gpuUse, // 推荐使用GPU Cuda = gpuUse,
PrimeGpu = false //Confidence = setting.YoloConfidenceThreshold, // 从 ModelSettings 读取
//Nms = setting.YoloNmsThreshold, // 从 ModelSettings 读取
PrimeGpu = false // 根据需求设置
}); });
// 保存阈值配置
var conf = setting.YoloConfidenceThreshold;
var nms = setting.YoloNmsThreshold;
if (_loadedModels.TryAdd(setting.Id, yolo)) if (_loadedModels.TryAdd(setting.Id, yolo))
{ {
ThreadSafeLogger.Log($"成功加载模型 '{setting.Name}' (ID: {setting.Id})。"); ThreadSafeLogger.Log($"成功加载YOLO模型 '{setting.Name}' (ID: {setting.Id})。");
} }
} }
catch (Exception ex) 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} 个模型。"); ThreadSafeLogger.Log($"YOLO模型加载完成共成功加载 {_loadedModels.Count} 个模型。");

View File

@@ -17,8 +17,7 @@ namespace Check.Main.UI
{ {
public partial class FrmConfig : DockContent public partial class FrmConfig : DockContent
{ {
//private ProcessConfig _mainSettings=new ProcessConfig();
//private readonly string _configFilePath = Path.Combine(Application.StartupPath, "main_config.xml");
public FrmConfig() public FrmConfig()
{ {
InitializeComponent(); InitializeComponent();