111
This commit is contained in:
		| @@ -24,34 +24,32 @@ namespace Check.Main.Camera | ||||
|     /// </summary> | ||||
|     public static class CameraManager | ||||
|     { | ||||
|         // 活动的相机实例字典,键为相机名称 | ||||
|         //1、相机与UI管理----管理每台相机对象和对应的图像显示窗口。 | ||||
|         // 活动的相机实例字典(键值对),键 (string):使用相机的名称,值 (HikvisionCamera):存储实际的相机实例,{ get; } 创建了一个只读的公共属性 | ||||
|         public static Dictionary<string, HikvisionCamera> ActiveCameras { get; } = new Dictionary<string, HikvisionCamera>(); | ||||
|  | ||||
|         // 相机对应的图像显示窗口字典 | ||||
|         //public static Dictionary<string, FormImageDisplay> CameraDisplays { get; } = new Dictionary<string, FormImageDisplay>(); | ||||
|  | ||||
|         public static Dictionary<string, FormImageDisplay> OriginalImageDisplays { get; } = new Dictionary<string, FormImageDisplay>(); | ||||
|         public static Dictionary<string, FormImageDisplay> ResultImageDisplays { get; } = new Dictionary<string, FormImageDisplay>(); | ||||
|         public static Dictionary<string, FormImageDisplay> OriginalImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();//原始图像窗口 | ||||
|         public static Dictionary<string, FormImageDisplay> ResultImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();//结果图像窗口 | ||||
|  | ||||
|         // --- 多相机同步逻辑 --- | ||||
|         // 产品检测队列 | ||||
|         //2、多相机同步逻辑 | ||||
|         // 【队列】一个产品需要多台相机拍完,才算完整。 | ||||
|         private static readonly Queue<ProductResult> ProductQueue = new Queue<ProductResult>(); | ||||
|         // 队列锁,保证线程安全 | ||||
|         // 【(队列)锁】保证队列在多线程下安全 | ||||
|         private static readonly object QueueLock = new object(); | ||||
|         // 当前启用的相机数量,用于判断产品是否检测完毕 | ||||
|         private static int EnabledCameraCount = 0; | ||||
|  | ||||
|         //public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted; | ||||
|         // public static bool IsDetectionRunning { get; private set; } = false; | ||||
|  | ||||
|         // 产品ID计数器 | ||||
|         // 产品ID【计数器】 | ||||
|         private static long _productCounter = 0; | ||||
|         // 用于同步计数器访问的锁,虽然long的自增是原子操作,但为清晰和未来扩展,使用锁是好习惯 | ||||
|         private static readonly object _counterLock = new object(); | ||||
|  | ||||
|         // --- 新增:硬触发模拟器 --- | ||||
|         // 3、--- 新增:硬触发模拟器 --- | ||||
|         private static readonly System.Timers.Timer _hardwareTriggerSimulator; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 获取或设置模拟硬触发的间隔时间(毫秒)。 | ||||
|         /// </summary> | ||||
| @@ -221,12 +219,12 @@ namespace Check.Main.Camera | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|  | ||||
|         //********************初始化和启动流程******************* | ||||
|         /// <summary> | ||||
|         /// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。 | ||||
|         /// 这是“启动设备”的第一阶段。 | ||||
|         /// </summary> | ||||
|         public static void PrepareAll(ProcessConfig config, FrmMain mainForm) | ||||
|         public static void PrepareAll(ProcessConfig config, FrmMain mainForm)//①准备所有相机和模型 | ||||
|         { | ||||
|             // 1. 清理旧资源和UI | ||||
|             mainForm.ClearStatusStrip(); | ||||
| @@ -238,7 +236,7 @@ namespace Check.Main.Camera | ||||
|             YoloModelManager.Initialize(config.ModelSettings); | ||||
|             DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings); | ||||
|  | ||||
|             // 3. 创建相机硬件实例和UI窗口 | ||||
|             // 3. 创建相机硬件实例和UI窗口------ | ||||
|             var deviceList = new HikvisionCamera().FindDevices(); | ||||
|             if (deviceList.Count == 0) | ||||
|             { | ||||
| @@ -246,7 +244,7 @@ namespace Check.Main.Camera | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled)) | ||||
|             foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled))//打开相机 → 设置触发模式 → 绑定事件(图像采集、检测完成)。 | ||||
|             { | ||||
|                 var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex }; | ||||
|                 cam.TriggerMode = setting.TriggerMode; | ||||
| @@ -293,6 +291,7 @@ namespace Check.Main.Camera | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 根据配置列表初始化或更新所有相机 | ||||
|         /// 类似 PrepareAll,但更侧重于更新配置,会检查物理设备数量,不足时报 warning。 | ||||
|         /// </summary> | ||||
|         public static void Initialize(ProcessConfig config, FrmMain mainForm) | ||||
|         { | ||||
| @@ -398,7 +397,7 @@ namespace Check.Main.Camera | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 定时器触发事件。 | ||||
|         /// 定时器触发事件。(定时器 OnHardwareTriggerTimerElapsed 会遍历相机,对处于软件触发模式的相机执行 SoftwareTrigger()。) | ||||
|         /// </summary> | ||||
|         private static void OnHardwareTriggerTimerElapsed(object sender, ElapsedEventArgs e) | ||||
|         { | ||||
| @@ -490,7 +489,9 @@ namespace Check.Main.Camera | ||||
|  | ||||
|         //} | ||||
|  | ||||
|         // Shutdown 方法也简化 | ||||
|         /// <summary> | ||||
|         /// 停止所有相机 + 关闭窗口 + 清空字典 + 关闭检测协调器 + 销毁模型 | ||||
|         /// </summary> | ||||
|         public static void Shutdown() | ||||
|         { | ||||
|             // 1. 停止硬件和模拟器 | ||||
| @@ -618,7 +619,7 @@ namespace Check.Main.Camera | ||||
|         //    } | ||||
|         //} | ||||
|  | ||||
|         // 图像回调方法现在极其简单 | ||||
|         //【相机回调】 | ||||
|         private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp) | ||||
|         { | ||||
|             Bitmap bmpForDisplay = null; | ||||
| @@ -626,16 +627,16 @@ namespace Check.Main.Camera | ||||
|  | ||||
|             try | ||||
|             { | ||||
|                 // 1. 为“显示”和“处理”创建两个完全独立的深克隆副本 | ||||
|                 // 1. 深度克隆 Bitmap → 分成 显示副本 和 处理副本。 | ||||
|                 bmpForDisplay = DeepCloneBitmap(bmp, "Display"); | ||||
|                 bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); | ||||
|             } | ||||
|             finally | ||||
|             { | ||||
|                 // 2.无论克隆成功与否,都必须立即释放事件传递过来的原始bmp,防止泄漏。 | ||||
|                 bmp?.Dispose(); | ||||
|                 bmp?.Dispose();//空条件运算符 ?.-----在访问对象成员前检查对象是否为 null,如果 bmp 不是 null,则正常调用 Dispose() 方法,替代传统的 if (bmp != null) 检查 | ||||
|             } | ||||
|             // 分支 A: 将用于显示的副本发送到对应的UI窗口 | ||||
|             // 分支 A: 显示副本 → 交给 FormImageDisplay.UpdateImage()。 | ||||
|             if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow)) | ||||
|             { | ||||
|                 // displayWindow.UpdateImage 会处理线程安全问题 | ||||
| @@ -646,7 +647,7 @@ namespace Check.Main.Camera | ||||
|                 // 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本 | ||||
|                 bmpForDisplay?.Dispose(); | ||||
|             } | ||||
|             // 分支 B: 将用于处理的副本发送到检测协调器的后台队列 | ||||
|             // 分支 B: 处理副本 → 交给 DetectionCoordinator.EnqueueImage() 进行检测。 | ||||
|             if (bmpForProcessing != null) | ||||
|             { | ||||
|                 // bmpForProcessing 的所有权在这里被转移给了协调器 | ||||
| @@ -673,11 +674,11 @@ namespace Check.Main.Camera | ||||
|             // 2. 找到此相机的结果显示窗口 | ||||
|             if (ResultImageDisplays.TryGetValue(cameraEntry.Key, out var resultDisplay)) | ||||
|             { | ||||
|                 var bmp = ConvertSKImageToBitmap(e.ResultImage); | ||||
|                 if (bmp != null) | ||||
|                 //var bmp = ConvertSKImageToBitmap(e.ResultImage); | ||||
|                 if (e.ResultImage != null) | ||||
|                 { | ||||
|                     // UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可 | ||||
|                     resultDisplay.UpdateImage(bmp); | ||||
|                     resultDisplay.UpdateImage(e.ResultImage); | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
| @@ -691,7 +692,7 @@ namespace Check.Main.Camera | ||||
|         /// <summary> | ||||
|         /// 【将 SkiaSharp.SKImage 安全地转换为 System.Drawing.Bitmap。 | ||||
|         /// </summary> | ||||
|         private static Bitmap ConvertSKImageToBitmap(SKImage skImage) | ||||
|         public static Bitmap ConvertSKImageToBitmap(SKImage skImage) | ||||
|         { | ||||
|             if (skImage == null) return null; | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| using Check.Main.Common; | ||||
| using Check.Main.Infer; | ||||
| using HalconTemplateMatch; | ||||
| using SkiaSharp; | ||||
| using System; | ||||
| using System.Collections.Concurrent; | ||||
| @@ -10,7 +11,7 @@ using System.Runtime.InteropServices; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using YoloDotNet.Extensions; | ||||
|  | ||||
| using OpenCvSharp; | ||||
| namespace Check.Main.Camera | ||||
| { | ||||
|     public class CameraProcessor : IDisposable | ||||
| @@ -52,6 +53,9 @@ namespace Check.Main.Camera | ||||
|             _imageQueue.Add(new ImageData(_imageCounter, _cameraIndex, bmp)); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 图像处理主循环 | ||||
|         /// </summary> | ||||
|         private void ProcessQueue() | ||||
|         { | ||||
|             // 从模型管理器获取此线程专属的YOLO模型 | ||||
| @@ -61,6 +65,36 @@ namespace Check.Main.Camera | ||||
|                 ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型,处理线程已中止。"); | ||||
|                 return; // 如果没有模型,此线程无法工作 | ||||
|             } | ||||
|  | ||||
|             //训练阶段(相机2) | ||||
|             var trainer = new LogoTemplateTrainer(); | ||||
|  | ||||
|             trainer.TrainAndSaveTemplates( | ||||
|                 new List<string> | ||||
|                 { | ||||
|                 @"D:\HalconTemplateMatch\train2\logo1.bmp", | ||||
|                 @"D:\HalconTemplateMatch\train2\logo2.bmp", | ||||
|                 @"D:\HalconTemplateMatch\train2\logo3.bmp" | ||||
|                 }, | ||||
|                 @"D:\HalconTemplateMatch\model_2"); | ||||
|  | ||||
|             //训练阶段(相机3)9.25修改!! | ||||
|             trainer.TrainAndSaveTemplates( | ||||
|                new List<string> | ||||
|                { | ||||
|                 @"D:\HalconTemplateMatch\train3\3C_1.bmp", | ||||
|                 @"D:\HalconTemplateMatch\train3\3C_2.bmp", | ||||
|                 @"D:\HalconTemplateMatch\train3\3C_3.bmp" | ||||
|                }, | ||||
|                @"D:\HalconTemplateMatch\model_3"); | ||||
|  | ||||
|             if (trainer == null) | ||||
|             { | ||||
|                 ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 未加载模板,处理线程已中止。"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             | ||||
|             while (_isRunning) | ||||
|             { | ||||
|                 try | ||||
| @@ -69,19 +103,15 @@ namespace Check.Main.Camera | ||||
|                     ImageData data = _imageQueue.Take(); | ||||
|                     using (data) | ||||
|                     { | ||||
|                         //SKImage resultSkImage = null; // 用于存储最终绘制好结果的图像 | ||||
|  | ||||
|                         string result = ""; | ||||
|                         using (var skImage = ConvertBitmapToSKImage(data.Image)) // 转换图像格式并管理其生命周期 | ||||
|                         { | ||||
|                             if (skImage == null) continue; | ||||
|                             var predictions = yoloModel.RunObjectDetection(skImage); | ||||
|                             // 模拟模型处理 | ||||
|                             //Thread.Sleep(50); // 模拟耗时 | ||||
|                             //bool isOk = new Random().NextDouble() > 0.1; | ||||
|                             //string result = isOk ? "OK" : "NG"; | ||||
|                              result = predictions.Any() ? "NG" : "OK"; | ||||
|  | ||||
|                             string result = predictions.Any() ? "NG" : "OK"; | ||||
|                             ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},检测到 {predictions.Count} 个目标,结果: {result}"); | ||||
|  | ||||
|                             // 将处理结果交给协调器进行组装 | ||||
|                             DetectionCoordinator.AssembleProduct(data, result); | ||||
|  | ||||
| @@ -90,16 +120,111 @@ namespace Check.Main.Camera | ||||
|                                 using (var resultSkImage = skImage.Draw(predictions)) | ||||
|                                 { | ||||
|                                     // 4. 触发事件,将绘制好的 resultSkImage 传递出去 | ||||
|  | ||||
|                                     Bitmap bitmap = CameraManager.ConvertSKImageToBitmap(resultSkImage); | ||||
|                                     // 所有权在这里被转移 | ||||
|                                     OnProcessingCompleted?.Invoke(this, new ProcessingCompletedEventArgs( | ||||
|                                         _cameraIndex, | ||||
|                                         data.ProductId, | ||||
|                                         resultSkImage | ||||
|                                         bitmap | ||||
|                                     )); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|  | ||||
|                         //***********************************使用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}"); | ||||
|  | ||||
|                         // 将处理结果交给协调器进行组装 | ||||
|                         DetectionCoordinator.AssembleProduct(data, result); | ||||
|  | ||||
|  | ||||
|                         //给PLC的M90、M91写值(10.10) | ||||
|                         if (FrmMain.PlcClient != null && FrmMain.PlcClient.IsConnected) | ||||
|                         { | ||||
|                             if (result == "OK") | ||||
|                             { | ||||
|                                 //吹气到合格框 | ||||
|                                 FrmMain.PlcClient.WriteAsync("M90", 1).Wait(); // 写入M90为1 | ||||
|                                 //Thread.Sleep(100); | ||||
|                                 FrmMain.PlcClient.WriteAsync("M90", 0).Wait(); // 写入M90为1 | ||||
|  | ||||
|                                 //var a = FrmMain.PlcClient.ReadAsync("M90"); | ||||
|                                 //Console.WriteLine(a); | ||||
|  | ||||
|                                 //FrmMain.PlcClient.WriteAsync("M91", 0).Wait(); | ||||
|  | ||||
|                                 // 延时复位 | ||||
|                                 Task.Run(async () => | ||||
|                                 { | ||||
|                                     //await Task.Delay(300); // 延时300毫秒,可根据实际气动时间调整 | ||||
|                                     //await FrmMain.PlcClient.WriteAsync("M90", 0); | ||||
|                                 }); | ||||
|                             } | ||||
|                             else | ||||
|                             { | ||||
|                                 //吹气到不合格框 | ||||
|                                 //FrmMain.PlcClient.WriteAsync("M90", 0).Wait(); | ||||
|                                 FrmMain.PlcClient.WriteAsync("M91", 1).Wait();// 写入M91为1 | ||||
|                                 FrmMain.PlcClient.WriteAsync("M91", 0).Wait(); // 写入M90为1 | ||||
|  | ||||
|                                 //var a = FrmMain.PlcClient.ReadAsync("M90"); | ||||
|                                 //Console.WriteLine(a); | ||||
|                                 // 延时复位 | ||||
|                                 Task.Run(async () => | ||||
|                                 { | ||||
|                                     //await Task.Delay(300); | ||||
|                                     //await FrmMain.PlcClient.WriteAsync("M91", 0); | ||||
|                                 }); | ||||
|                             } | ||||
|                         } | ||||
|                         else | ||||
|                         { | ||||
|                             ThreadSafeLogger.Log("[PLC] 未连接,跳过写入。"); | ||||
|                         } | ||||
|  | ||||
|  | ||||
|                         // ③ 外部订阅事件 | ||||
|                         OnProcessingCompleted?.Invoke( | ||||
|                             this, | ||||
|                             new ProcessingCompletedEventArgs | ||||
|                             ( | ||||
|                                 _cameraIndex, | ||||
|                                 data.ProductId, | ||||
|                                 data.Image // 原图传出去 | ||||
|                             ) | ||||
|                         ); | ||||
|                     } | ||||
|                 } | ||||
|                 catch (InvalidOperationException) | ||||
| @@ -107,12 +232,14 @@ namespace Check.Main.Camera | ||||
|                     // 当调用 Stop 时,会 CompleteAdding 队列,Take 会抛出此异常,是正常退出流程 | ||||
|                     break; | ||||
|                 } | ||||
|                 catch (Exception ex) | ||||
|                 catch (Exception ex)  | ||||
|                 { | ||||
|                     ThreadSafeLogger.Log($"[ERROR] 相机 #{_cameraIndex} 处理线程异常: {ex.Message}"); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
|         /// <summary> | ||||
|         /// 将 System.Drawing.Bitmap 安全地转换为 SkiaSharp.SKImage。 | ||||
|         /// </summary> | ||||
|   | ||||
| @@ -30,7 +30,7 @@ namespace Check.Main.Camera | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// 相机配置信息类,用于PropertyGrid显示和编辑 | ||||
|     /// 相机配置信息类CameraSettings,用于PropertyGrid显示和编辑(****核心!****) | ||||
|     /// </summary> | ||||
|     public class CameraSettings : INotifyPropertyChanged, ICloneable | ||||
|     { | ||||
| @@ -39,8 +39,8 @@ namespace Check.Main.Camera | ||||
|  | ||||
|         private int _cameraIndex = 0; | ||||
|         private string _name = "Camera-1"; | ||||
|         private string _ipAddress = "192.168.1.100"; | ||||
|         private string _ipDeviceAddress = "192.168.1.101"; | ||||
|         private string _ipAddress = "169.254.51.253"; | ||||
|         private string _ipDeviceAddress = "169.254.51.45"; | ||||
|         private TriggerModeType _triggerMode = TriggerModeType.Continuous; | ||||
|         private bool _isEnabled = true; | ||||
|         private CheckType _checkType = CheckType.DeepLearning; | ||||
|   | ||||
| @@ -179,7 +179,9 @@ namespace Check.Main.Camera | ||||
|                 } | ||||
|  | ||||
|                 // 默认设置为连续模式 | ||||
|                 SetContinuousMode(); | ||||
|                 //SetContinuousMode(); | ||||
|                 // 设置为硬触发模式(Line0) | ||||
|                 SetTriggerMode(false); | ||||
|  | ||||
|                 IsOpen = true; | ||||
|                 OnCameraMessage("相机打开成功。", 0); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user