This commit is contained in:
2025-10-20 14:47:17 +08:00
parent 2e46747ba9
commit 546b894e6b
16 changed files with 917 additions and 141 deletions

View File

@@ -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;

View File

@@ -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");
//训练阶段相机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)
{
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>

View File

@@ -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;

View File

@@ -179,7 +179,9 @@ namespace Check.Main.Camera
}
// 默认设置为连续模式
SetContinuousMode();
//SetContinuousMode();
// 设置为硬触发模式Line0
SetTriggerMode(false);
IsOpen = true;
OnCameraMessage("相机打开成功。", 0);