修改框架(未完全完成)实现单个相机分开绑定算法
This commit is contained in:
@@ -25,39 +25,25 @@ namespace Check.Main.Camera
|
||||
public static class CameraManager
|
||||
{
|
||||
//1、相机与UI管理----管理每台相机对象和对应的图像显示窗口。
|
||||
// 活动的相机实例字典(键值对),键 (string):使用相机的名称,值 (HikvisionCamera):存储实际的相机实例,{ get; } 创建了一个只读的公共属性
|
||||
|
||||
public static Dictionary<string, HikvisionCamera> ActiveCameras { get; } = new Dictionary<string, HikvisionCamera>();
|
||||
|
||||
// 相机对应的图像显示窗口字典
|
||||
//public static Dictionary<string, FormImageDisplay> CameraDisplays { get; } = new Dictionary<string, FormImageDisplay>();
|
||||
|
||||
public static Dictionary<string, FormImageDisplay> OriginalImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();//原始图像窗口
|
||||
public static Dictionary<string, FormImageDisplay> ResultImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();//结果图像窗口
|
||||
|
||||
//2、多相机同步逻辑
|
||||
// 【队列】一个产品需要多台相机拍完,才算完整。
|
||||
private static readonly Queue<ProductResult> ProductQueue = new Queue<ProductResult>();
|
||||
// 【(队列)锁】保证队列在多线程下安全
|
||||
private static readonly object QueueLock = new object();
|
||||
// 当前启用的相机数量,用于判断产品是否检测完毕
|
||||
private static int EnabledCameraCount = 0;
|
||||
//public static event EventHandler<DetectionResultEventArgs> OnDetectionCompleted;
|
||||
// public static bool IsDetectionRunning { get; private set; } = false;
|
||||
// 产品ID【计数器】
|
||||
private static long _productCounter = 0;
|
||||
// 用于同步计数器访问的锁,虽然long的自增是原子操作,但为清晰和未来扩展,使用锁是好习惯
|
||||
private static readonly object _counterLock = new object();
|
||||
|
||||
// 3、--- 新增:硬触发模拟器 ---
|
||||
private static readonly System.Timers.Timer _hardwareTriggerSimulator;
|
||||
/// <summary>
|
||||
/// 获取或设置模拟硬触发的间隔时间(毫秒)。
|
||||
/// </summary>
|
||||
|
||||
// 获取或设置模拟硬触发的间隔时间(毫秒)。
|
||||
public static double TriggerInterval { get; set; } = 1000; // 默认1秒触发一次
|
||||
|
||||
/// <summary>
|
||||
/// 获取一个值,该值指示硬件触发模拟器当前是否正在运行。
|
||||
/// </summary>
|
||||
// 获取一个值,该值指示硬件触发模拟器当前是否正在运行。
|
||||
public static bool IsHardwareTriggerSimulating { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
@@ -73,113 +59,36 @@ namespace Check.Main.Camera
|
||||
}
|
||||
|
||||
|
||||
//// 事件:用于向UI发送日志消息
|
||||
//public static event Action<string> OnLogMessage;
|
||||
//// 用于写入日志文件的 StreamWriter
|
||||
//private static StreamWriter _logFileWriter;
|
||||
|
||||
////私有的、静态的、只读的对象,专门用作线程同步的“锁”。
|
||||
//private static readonly object _logLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// 初始化文件日志记录器。应在程序启动时调用一次。
|
||||
/// </summary>
|
||||
//public static void InitializeLogger()
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// string logDirectory = Path.Combine(Application.StartupPath, "Logs");
|
||||
// Directory.CreateDirectory(logDirectory); // 确保Logs文件夹存在
|
||||
|
||||
// string logFileName = $"Log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt";
|
||||
// string logFilePath = Path.Combine(logDirectory, logFileName);
|
||||
|
||||
// // 创建StreamWriter,设置为追加模式和自动刷新
|
||||
// _logFileWriter = new StreamWriter(logFilePath, append: true, encoding: System.Text.Encoding.UTF8)
|
||||
// {
|
||||
// AutoFlush = true
|
||||
// };
|
||||
|
||||
// Log("文件日志记录器已初始化。");
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// // 如果文件日志初始化失败,在UI上报告错误
|
||||
// OnLogMessage?.Invoke($"[CRITICAL] 文件日志初始化失败: {ex.Message}");
|
||||
// }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 【新增】一个完整的业务流程方法:
|
||||
/// 1. 根据配置初始化所有相机并显示它们的窗口。
|
||||
/// 2. 在所有窗口都显示后,命令所有相机开始采集。
|
||||
/// 这个方法是响应“开启设备”或“应用配置并启动”按钮的理想入口点。
|
||||
/// </summary>
|
||||
/// <param name="settingsList">要应用的相机配置列表。</param>
|
||||
/// <param name="mainForm">主窗体,用于停靠显示窗口。</param>
|
||||
//public static void ApplyConfigurationAndStartGrabbing(List<CameraSettings> settingsList, FrmMain mainForm)
|
||||
//{
|
||||
// ThreadSafeLogger.Log("开始执行“开启设备”流程...");
|
||||
|
||||
// // 步骤 1: 初始化所有相机和UI窗口
|
||||
// // Initialize 方法会负责 Shutdown 旧实例、创建新实例、打开硬件、显示窗口等。
|
||||
// // 因为 Initialize 方法中的 displayForm.Show() 是非阻塞的,它会立即返回,
|
||||
// // 窗体的创建和显示过程会被调度到UI线程的消息队列中。
|
||||
// Initialize(settingsList, mainForm);
|
||||
|
||||
// // 检查是否有任何相机成功初始化
|
||||
// if (ActiveCameras.Count == 0)
|
||||
// {
|
||||
// ThreadSafeLogger.Log("“开启设备”流程中止,因为没有相机被成功初始化。");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // 步骤 2: 在 Initialize 完成后(意味着所有窗口都已创建并 Show),开始采集
|
||||
// // 这个调用会紧接着 Initialize 执行,此时窗体可能还在绘制过程中,但这没关系。
|
||||
// // StartAll() 会启动后台线程开始拉取图像,一旦图像到达,就会通过事件推送到已经存在的窗体上。
|
||||
// ThreadSafeLogger.Log("所有相机窗口已创建,现在开始采集图像...");
|
||||
// StartAll();
|
||||
|
||||
// ThreadSafeLogger.Log("“开启设备”流程已完成。相机正在采集中。");
|
||||
//}
|
||||
|
||||
|
||||
////********************初始化和启动流程*******************
|
||||
///// <summary>
|
||||
///// 根据配置列表初始化或更新所有相机
|
||||
///// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。
|
||||
///// 这是“启动设备”的第一阶段。
|
||||
///// </summary>
|
||||
//public static void Initialize(List<CameraSettings> settingsList, FrmMain mainForm)
|
||||
//public static void PrepareAll(ProcessConfig config, FrmMain mainForm)//①准备所有相机和模型
|
||||
//{
|
||||
|
||||
// // 先停止并释放所有旧的相机
|
||||
// // 1. 清理旧资源和UI
|
||||
// mainForm.ClearStatusStrip();
|
||||
// Shutdown();
|
||||
// ThreadSafeLogger.Log("开始应用新的相机配置...");
|
||||
// ThreadSafeLogger.Log("开始准备设备和模型...");
|
||||
|
||||
// EnabledCameraCount = settingsList.Count(s => s.IsEnabled);
|
||||
// if (EnabledCameraCount == 0)
|
||||
// {
|
||||
// ThreadSafeLogger.Log("没有启用的相机。");
|
||||
// return;
|
||||
// }
|
||||
// // 2. 初始化检测协调器和AI模型
|
||||
// // 注意:YoloModelManager 的 Initialize 现在也应在这里被调用,以确保逻辑集中
|
||||
// YoloModelManager.Initialize(config.ModelSettings);
|
||||
// DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings);
|
||||
|
||||
// var deviceList = new HikvisionCamera().FindDevices();
|
||||
// // 3. 创建相机硬件实例和UI窗口------
|
||||
// var deviceList = new HikvisionCamera().FindDevices();
|
||||
// if (deviceList.Count == 0)
|
||||
// {
|
||||
// ThreadSafeLogger.Log("错误:未找到任何相机设备!");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// int deviceIndex = 0;
|
||||
// foreach (var setting in settingsList)
|
||||
// foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled))//打开相机 → 设置触发模式 → 绑定事件(图像采集、检测完成)。
|
||||
// {
|
||||
// if (!setting.IsEnabled) continue;
|
||||
|
||||
// if (deviceIndex >= deviceList.Count)
|
||||
// {
|
||||
// ThreadSafeLogger.Log($"警告:相机配置'{setting.Name}'无法分配物理设备,因为设备数量不足。");
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // --- 创建相机实例 ---
|
||||
// var cam = new HikvisionCamera { Name = setting.Name };
|
||||
// var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex };
|
||||
// cam.TriggerMode = setting.TriggerMode;
|
||||
// if (!cam.Open(setting))
|
||||
// {
|
||||
@@ -203,40 +112,38 @@ namespace Check.Main.Camera
|
||||
|
||||
// // --- 订阅事件 ---
|
||||
// cam.ImageAcquired += OnCameraImageAcquired;
|
||||
// cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}");
|
||||
|
||||
// // --- 创建显示窗口 ---
|
||||
// var displayForm = new FormImageDisplay { Text = setting.Name, CameraName = setting.Name };
|
||||
// displayForm.OnDisplayEvent += ThreadSafeLogger.Log;
|
||||
// displayForm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
|
||||
// var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex);
|
||||
// if (processor != null) { processor.OnProcessingCompleted += Processor_OnProcessingCompleted; }
|
||||
|
||||
// // --- 创建【但不显示】图像的UI窗口 ---
|
||||
// var originalDisplay = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name };
|
||||
// var resultDisplay = new FormImageDisplay { Text = $"{setting.Name} - 结果", CameraName = setting.Name };
|
||||
// originalDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
|
||||
// resultDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
|
||||
|
||||
// // --- 保存引用 ---
|
||||
// ActiveCameras.Add(setting.Name, cam);
|
||||
// CameraDisplays.Add(setting.Name, displayForm);
|
||||
// OriginalImageDisplays.Add(setting.Name, originalDisplay);
|
||||
// ResultImageDisplays.Add(setting.Name, resultDisplay);
|
||||
// mainForm.AddCameraToStatusStrip(setting.Name);
|
||||
// ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。");
|
||||
// deviceIndex++;
|
||||
// }
|
||||
// ThreadSafeLogger.Log("所有设备和模型已准备就绪。");
|
||||
//}
|
||||
|
||||
//********************初始化和启动流程*******************
|
||||
/// <summary>
|
||||
/// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。
|
||||
/// 这是“启动设备”的第一阶段。
|
||||
/// </summary>
|
||||
public static void PrepareAll(ProcessConfig config, FrmMain mainForm)//①准备所有相机和模型
|
||||
public static void PrepareAll(ProcessConfig config, FrmMain mainForm)
|
||||
{
|
||||
// 1. 清理旧资源和UI
|
||||
mainForm.ClearStatusStrip();
|
||||
Shutdown();
|
||||
mainForm.ClearStatusStrip(); // 清理旧的状态条
|
||||
Shutdown(); // 清理旧的相机和协调器资源
|
||||
ThreadSafeLogger.Log("开始准备设备和模型...");
|
||||
|
||||
// 2. 初始化检测协调器和AI模型
|
||||
// 注意:YoloModelManager 的 Initialize 现在也应在这里被调用,以确保逻辑集中
|
||||
YoloModelManager.Initialize(config.ModelSettings);
|
||||
// 1. 初始化检测协调器和AI模型(此步骤会加载YOLO模型并创建CameraProcessor)
|
||||
DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings);
|
||||
|
||||
// 3. 创建相机硬件实例和UI窗口------
|
||||
// 2. 创建相机硬件实例和UI窗口
|
||||
var deviceList = new HikvisionCamera().FindDevices();
|
||||
if (deviceList.Count == 0)
|
||||
{
|
||||
@@ -244,8 +151,15 @@ namespace Check.Main.Camera
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled))//打开相机 → 设置触发模式 → 绑定事件(图像采集、检测完成)。
|
||||
int deviceIndex = 0;
|
||||
foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled))
|
||||
{
|
||||
if (deviceIndex >= deviceList.Count)
|
||||
{
|
||||
ThreadSafeLogger.Log($"警告:相机配置'{setting.Name}'无法分配物理设备,因为设备数量不足。");
|
||||
continue;
|
||||
}
|
||||
|
||||
var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex };
|
||||
cam.TriggerMode = setting.TriggerMode;
|
||||
if (!cam.Open(setting))
|
||||
@@ -270,12 +184,26 @@ namespace Check.Main.Camera
|
||||
|
||||
// --- 订阅事件 ---
|
||||
cam.ImageAcquired += OnCameraImageAcquired;
|
||||
var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex);
|
||||
if (processor != null) { processor.OnProcessingCompleted += Processor_OnProcessingCompleted; }
|
||||
cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}");
|
||||
|
||||
// --- 创建【但不显示】图像的UI窗口 ---
|
||||
// 获取 CameraProcessor 并订阅其完成事件
|
||||
var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex);
|
||||
if (processor != null)
|
||||
{
|
||||
processor.OnProcessingCompleted += Processor_OnProcessingCompleted;
|
||||
}
|
||||
else
|
||||
{
|
||||
ThreadSafeLogger.Log($"[警告] 未能为相机 '{setting.Name}' (Index: {setting.CameraIndex}) 获取处理器,检测结果将不会显示。");
|
||||
}
|
||||
|
||||
|
||||
// --- 创建【并显示】图像的UI窗口 ---
|
||||
var originalDisplay = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name };
|
||||
var resultDisplay = new FormImageDisplay { Text = $"{setting.Name} - 结果", CameraName = setting.Name };
|
||||
originalDisplay.OnDisplayEvent += ThreadSafeLogger.Log;
|
||||
resultDisplay.OnDisplayEvent += ThreadSafeLogger.Log;
|
||||
|
||||
originalDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
|
||||
resultDisplay.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
|
||||
|
||||
@@ -284,6 +212,8 @@ namespace Check.Main.Camera
|
||||
OriginalImageDisplays.Add(setting.Name, originalDisplay);
|
||||
ResultImageDisplays.Add(setting.Name, resultDisplay);
|
||||
mainForm.AddCameraToStatusStrip(setting.Name);
|
||||
ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。");
|
||||
deviceIndex++;
|
||||
}
|
||||
ThreadSafeLogger.Log("所有设备和模型已准备就绪。");
|
||||
}
|
||||
@@ -507,8 +437,6 @@ namespace Check.Main.Camera
|
||||
// 3. 关闭检测协调器,它会负责清理所有后台线程和队列
|
||||
DetectionCoordinator.Shutdown();
|
||||
|
||||
YoloModelManager.Shutdown();
|
||||
|
||||
ThreadSafeLogger.Log("所有相机及协调器资源已释放。");
|
||||
}
|
||||
|
||||
@@ -517,10 +445,11 @@ namespace Check.Main.Camera
|
||||
/// </summary>
|
||||
public static void ResetProductCounter()
|
||||
{
|
||||
lock (_counterLock)
|
||||
{
|
||||
_productCounter = 0;
|
||||
}
|
||||
//lock (_counterLock)
|
||||
//{
|
||||
// _productCounter = 0;
|
||||
//}
|
||||
DetectionCoordinator.ResetAllCounters(); // 调用协调器的重置方法
|
||||
ThreadSafeLogger.Log("产品计数器已重置。");
|
||||
}
|
||||
|
||||
@@ -620,6 +549,7 @@ namespace Check.Main.Camera
|
||||
//}
|
||||
|
||||
//【相机回调】
|
||||
// 【相机回调】现在只负责图像分发
|
||||
private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp)
|
||||
{
|
||||
Bitmap bmpForDisplay = null;
|
||||
@@ -627,66 +557,51 @@ namespace Check.Main.Camera
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 深度克隆 Bitmap → 分成 显示副本 和 处理副本。
|
||||
bmpForDisplay = DeepCloneBitmap(bmp, "Display");
|
||||
bmpForProcessing = DeepCloneBitmap(bmp, "Processing");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 2.无论克隆成功与否,都必须立即释放事件传递过来的原始bmp,防止泄漏。
|
||||
bmp?.Dispose();//空条件运算符 ?.-----在访问对象成员前检查对象是否为 null,如果 bmp 不是 null,则正常调用 Dispose() 方法,替代传统的 if (bmp != null) 检查
|
||||
bmp?.Dispose();
|
||||
}
|
||||
// 分支 A: 显示副本 → 交给 FormImageDisplay.UpdateImage()。
|
||||
|
||||
if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow))
|
||||
{
|
||||
// displayWindow.UpdateImage 会处理线程安全问题
|
||||
displayWindow.UpdateImage(bmpForDisplay);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本
|
||||
bmpForDisplay?.Dispose();
|
||||
}
|
||||
// 分支 B: 处理副本 → 交给 DetectionCoordinator.EnqueueImage() 进行检测。
|
||||
|
||||
// 直接将处理副本和相机编号交给 DetectionCoordinator
|
||||
if (bmpForProcessing != null)
|
||||
{
|
||||
// bmpForProcessing 的所有权在这里被转移给了协调器
|
||||
DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing);
|
||||
}
|
||||
//// 深度克隆图像以确保线程安全
|
||||
//Bitmap bmpForProcessing = DeepCloneBitmap(bmp, "Processing");
|
||||
//bmp?.Dispose(); // 立即释放原始图
|
||||
|
||||
//// 直接将图像和相机编号交给协调器,无需任何本地处理
|
||||
//DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing);
|
||||
}
|
||||
// 事件处理器
|
||||
|
||||
// 事件处理器:从 CameraProcessor 接收带有结果的图像
|
||||
private static void Processor_OnProcessingCompleted(object sender, ProcessingCompletedEventArgs e)
|
||||
{
|
||||
|
||||
// 1. 找到与此相机匹配的相机名称
|
||||
var cameraEntry = ActiveCameras.FirstOrDefault(kvp => kvp.Value.CameraIndex == e.CameraIndex);
|
||||
if (cameraEntry.Key == null)
|
||||
{
|
||||
e.Dispose(); // 如果找不到接收者,必须释放事件参数中的图像
|
||||
e.Dispose();
|
||||
return;
|
||||
}
|
||||
// 2. 找到此相机的结果显示窗口
|
||||
|
||||
if (ResultImageDisplays.TryGetValue(cameraEntry.Key, out var resultDisplay))
|
||||
{
|
||||
//var bmp = ConvertSKImageToBitmap(e.ResultImage);
|
||||
if (e.ResultImage != null)
|
||||
{
|
||||
// UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可
|
||||
resultDisplay.UpdateImage(e.ResultImage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果找到了相机但没有对应的结果窗口,也要释放图像
|
||||
e.Dispose();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -808,32 +723,6 @@ namespace Check.Main.Camera
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
// 触发日志事件的辅助方法
|
||||
//public static void Log(string message)
|
||||
//{
|
||||
// string formattedMessage = $"[{DateTime.Now:HH:mm:ss.fff}] {message}";
|
||||
|
||||
// // 1. 触发事件,更新UI
|
||||
// OnLogMessage?.Invoke(formattedMessage);
|
||||
|
||||
// // 2. 【关键修改】在写入文件前,先获取锁。
|
||||
// // lock 语句块确保了花括号内的代码在同一时间只能被一个线程执行。
|
||||
// // 如果另一个线程也想执行这段代码,它必须等待前一个线程执行完毕并释放锁。
|
||||
// lock (_logLock)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// _logFileWriter?.WriteLine(formattedMessage);
|
||||
// }
|
||||
// catch (Exception)
|
||||
// {
|
||||
// // 即使有锁,也保留try-catch以防万一(如磁盘满了等IO问题)。
|
||||
// // 可以在这里决定是否要在UI上显示警告。
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,13 +12,15 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using YoloDotNet.Extensions;
|
||||
using OpenCvSharp;
|
||||
using Check.Main.Infer; // 引入 IDetector, DetectionResult
|
||||
|
||||
namespace Check.Main.Camera
|
||||
{
|
||||
public class CameraProcessor : IDisposable
|
||||
{
|
||||
private readonly int _cameraIndex;
|
||||
private readonly int _modeId;
|
||||
// private readonly ModelSettings _model;
|
||||
private readonly IDetector _detector; // 替换为接口
|
||||
private readonly ModelSettings _modelSettings; // 保留模型设置以获取更多参数
|
||||
private readonly BlockingCollection<ImageData> _imageQueue = new BlockingCollection<ImageData>();
|
||||
private readonly Thread _workerThread;
|
||||
private volatile bool _isRunning = false;
|
||||
@@ -28,11 +30,12 @@ namespace Check.Main.Camera
|
||||
public event EventHandler<ProcessingCompletedEventArgs> OnProcessingCompleted;
|
||||
|
||||
|
||||
public CameraProcessor(int cameraIndex, int modelId)//, ModelSettings model
|
||||
// 构造函数现在接受 IDetector 实例和 ModelSettings
|
||||
public CameraProcessor(int cameraIndex, IDetector detector, ModelSettings modelSettings)
|
||||
{
|
||||
_cameraIndex = cameraIndex;
|
||||
_modeId = modelId;
|
||||
//_model = model;
|
||||
_detector = detector ?? throw new ArgumentNullException(nameof(detector));
|
||||
_modelSettings = modelSettings ?? throw new ArgumentNullException(nameof(modelSettings));
|
||||
_workerThread = new Thread(ProcessQueue) { IsBackground = true, Name = $"Cam_{_cameraIndex}_Processor" };
|
||||
}
|
||||
|
||||
@@ -42,15 +45,16 @@ namespace Check.Main.Camera
|
||||
_workerThread.Start();
|
||||
}
|
||||
|
||||
public void EnqueueImage(Bitmap bmp)
|
||||
public void EnqueueImage(Bitmap bmp, long productId) // 接收产品ID
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
bmp?.Dispose();
|
||||
return;
|
||||
}
|
||||
_imageCounter++;
|
||||
_imageQueue.Add(new ImageData(_imageCounter, _cameraIndex, bmp));
|
||||
// _imageCounter 在此用于内部跟踪,不是产品ID
|
||||
// 产品ID现在由 DetectionCoordinator 生成并传递
|
||||
_imageQueue.Add(new ImageData(productId, _cameraIndex, bmp));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -58,42 +62,38 @@ namespace Check.Main.Camera
|
||||
/// </summary>
|
||||
private void ProcessQueue()
|
||||
{
|
||||
//// 从模型管理器获取此线程专属的YOLO模型
|
||||
//var yoloModel = YoloModelManager.GetModel(_modeId);
|
||||
//if (yoloModel == null)
|
||||
|
||||
ThreadSafeLogger.Log($"相机#{_cameraIndex} 启动处理线程,算法类型:{_modelSettings.M_AType}");
|
||||
|
||||
////训练HALCON模型
|
||||
////训练阶段(相机2)
|
||||
//var trainer = new LogoTemplateTrainer();
|
||||
|
||||
//trainer.TrainAndSaveTemplates(
|
||||
// new List<string>
|
||||
// {
|
||||
// @"D:\HalconTemplateMatch\train2\logo1.bmp",
|
||||
// @"D:\HalconTemplateMatch\train2\logo2.bmp",
|
||||
// @"D:\HalconTemplateMatch\train2\logo3.bmp"
|
||||
// },
|
||||
// @"D:\HalconTemplateMatch\model_2");
|
||||
|
||||
////训练阶段(相机3)9.25修改!!
|
||||
//trainer.TrainAndSaveTemplates(
|
||||
// new List<string>
|
||||
// {
|
||||
// @"D:\HalconTemplateMatch\train3\3C_1.bmp",
|
||||
// @"D:\HalconTemplateMatch\train3\3C_2.bmp",
|
||||
// @"D:\HalconTemplateMatch\train3\3C_3.bmp"
|
||||
// },
|
||||
// @"D:\HalconTemplateMatch\model_3");
|
||||
|
||||
//if (trainer == null)
|
||||
//{
|
||||
// ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型,处理线程已中止。");
|
||||
// return; // 如果没有模型,此线程无法工作
|
||||
// ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 未加载模板,处理线程已中止。");
|
||||
// return;
|
||||
//}
|
||||
|
||||
//训练阶段(相机2)
|
||||
var trainer = new LogoTemplateTrainer();
|
||||
|
||||
trainer.TrainAndSaveTemplates(
|
||||
new List<string>
|
||||
{
|
||||
@"D:\HalconTemplateMatch\train2\logo1.bmp",
|
||||
@"D:\HalconTemplateMatch\train2\logo2.bmp",
|
||||
@"D:\HalconTemplateMatch\train2\logo3.bmp"
|
||||
},
|
||||
@"D:\HalconTemplateMatch\model_2");
|
||||
|
||||
//训练阶段(相机3)9.25修改!!
|
||||
trainer.TrainAndSaveTemplates(
|
||||
new List<string>
|
||||
{
|
||||
@"D:\HalconTemplateMatch\train3\3C_1.bmp",
|
||||
@"D:\HalconTemplateMatch\train3\3C_2.bmp",
|
||||
@"D:\HalconTemplateMatch\train3\3C_3.bmp"
|
||||
},
|
||||
@"D:\HalconTemplateMatch\model_3");
|
||||
|
||||
if (trainer == null)
|
||||
{
|
||||
ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 未加载模板,处理线程已中止。");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
while (_isRunning)
|
||||
{
|
||||
@@ -103,116 +103,23 @@ namespace Check.Main.Camera
|
||||
ImageData data = _imageQueue.Take();
|
||||
using (data)
|
||||
{
|
||||
|
||||
//using (var skImage = ConvertBitmapToSKImage(data.Image)) // 转换图像格式并管理其生命周期
|
||||
//{
|
||||
// if (skImage == null) continue;
|
||||
// var predictions = yoloModel.RunObjectDetection(skImage);
|
||||
// string result = predictions.Any() ? "NG" : "OK";
|
||||
|
||||
// ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},检测到 {predictions.Count} 个目标,结果: {result}");
|
||||
|
||||
// // 将处理结果交给协调器进行组装
|
||||
// DetectionCoordinator.AssembleProduct(data, result);
|
||||
|
||||
// if (OnProcessingCompleted != null)
|
||||
// {
|
||||
// using (var resultSkImage = skImage.Draw(predictions))
|
||||
// {
|
||||
// // 4. 触发事件,将绘制好的 resultSkImage 传递出去
|
||||
// // 所有权在这里被转移
|
||||
// OnProcessingCompleted?.Invoke(this, new ProcessingCompletedEventArgs(
|
||||
// _cameraIndex,
|
||||
// data.ProductId,
|
||||
// resultSkImage
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
//***********************************使用Halcon模板匹配进行检测****************************************************
|
||||
if (data.Image == null) continue;
|
||||
// 统一定义预测结果
|
||||
var matcher = new LogoMatcher();
|
||||
|
||||
//9.25(增加一根据不同的相机编号调用不同的模型!)
|
||||
string filepath = "";
|
||||
if (_cameraIndex == 2)
|
||||
{
|
||||
matcher.LoadTemplates(@"D:\HalconTemplateMatch\model_2");
|
||||
filepath = "D:\\HalconTemplateMatch\\train2";
|
||||
}
|
||||
else if (_cameraIndex == 3)
|
||||
{
|
||||
matcher.LoadTemplates(@"D:\HalconTemplateMatch\model_3");
|
||||
filepath = "D:\\HalconTemplateMatch\\train3";
|
||||
}
|
||||
|
||||
////原bool返回的处理
|
||||
//bool found = matcher.FindLogo(data.Image);
|
||||
//string result = found ? "OK":"NG";
|
||||
|
||||
//double返回的处理
|
||||
double score = matcher.FindLogo(data.Image);
|
||||
|
||||
|
||||
//Mat cam = ProcessImg.BitmapToMat(data.Image);
|
||||
|
||||
//score= ProcessImg.ProcessImagesInFolder(filepath,cam);
|
||||
|
||||
string result = (score > 0.5) ? "OK" : "NG";
|
||||
|
||||
ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},结果: {result},得分: {score}");
|
||||
DetectionResult detectionResult = _detector.Detect(data.Image);
|
||||
ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},结果: {(detectionResult.IsOk ? "OK" : "NG")}, 信息: {detectionResult.Message}, 得分: {detectionResult.Score:F2}");
|
||||
|
||||
// 将处理结果交给协调器进行组装
|
||||
DetectionCoordinator.AssembleProduct(data, result);
|
||||
DetectionCoordinator.AssembleProduct(data.ProductId, data.CameraIndex, detectionResult.IsOk, detectionResult.ResultImage);
|
||||
|
||||
|
||||
|
||||
//给PLC的M90、M91写值(10.10)
|
||||
if (FrmMain.PlcClient != null)
|
||||
{
|
||||
if (result == "OK")
|
||||
{
|
||||
//吹气到合格框
|
||||
FrmMain.PlcClient.WriteBool("90", true); // 写入M90为1
|
||||
// 延时复位
|
||||
Task.Run(async () =>
|
||||
{
|
||||
//await Task.Delay(300); // 延时300毫秒,可根据实际气动时间调整
|
||||
//await FrmMain.PlcClient.WriteAsync("M90", 0);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
//吹气到不合格框
|
||||
FrmMain.PlcClient.WriteBool("91", true);// 写入M91为1
|
||||
// 延时复位
|
||||
Task.Run(async () =>
|
||||
{
|
||||
//await Task.Delay(300);
|
||||
//await FrmMain.PlcClient.WriteAsync("M91", 0);
|
||||
});
|
||||
}
|
||||
//完成一次检测进行刷新
|
||||
Thread.Sleep(2000);
|
||||
FrmMain.PlcClient.WriteBool("90", false); //
|
||||
FrmMain.PlcClient.WriteBool("91", false); // 写入M90为1
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
ThreadSafeLogger.Log("6,跳过写入。");
|
||||
}
|
||||
|
||||
|
||||
// ③ 外部订阅事件
|
||||
// 外部订阅事件,传递结果图像
|
||||
OnProcessingCompleted?.Invoke(
|
||||
this,
|
||||
new ProcessingCompletedEventArgs
|
||||
(
|
||||
_cameraIndex,
|
||||
data.ProductId,
|
||||
data.Image // 原图传出去
|
||||
detectionResult.ResultImage // 传递带有绘制结果的图像
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -252,6 +159,7 @@ namespace Check.Main.Camera
|
||||
}
|
||||
public static SKBitmap ToSKBitmapFast(Bitmap bitmap)
|
||||
{
|
||||
if (bitmap == null) return null;
|
||||
// 确保是 32bppArgb(BGRA 内存布局)
|
||||
Bitmap src = bitmap;
|
||||
if (bitmap.PixelFormat != PixelFormat.Format32bppArgb)
|
||||
@@ -298,13 +206,15 @@ namespace Check.Main.Camera
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_isRunning = false;
|
||||
// 解除阻塞,让线程可以检查 _isRunning 标志并退出
|
||||
_imageQueue.CompleteAdding();
|
||||
_workerThread.Join(500); // 等待线程结束
|
||||
_workerThread.Join(5000); // 等待线程结束。10.22修改,原来是500
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 线程安全地重置该相机的图像计数器。
|
||||
/// </summary>
|
||||
@@ -322,6 +232,27 @@ namespace Check.Main.Camera
|
||||
{
|
||||
Stop();
|
||||
_imageQueue.Dispose();
|
||||
_detector?.Dispose(); // 释放检测器资源
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ProcessingCompletedEventArgs : EventArgs, IDisposable
|
||||
{
|
||||
public int CameraIndex { get; }
|
||||
public long ProductId { get; }
|
||||
public Bitmap ResultImage { get; } // 新增:带有检测结果的图像
|
||||
|
||||
public ProcessingCompletedEventArgs(int cameraIndex, long productId, Bitmap resultImage)
|
||||
{
|
||||
CameraIndex = cameraIndex;
|
||||
ProductId = productId;
|
||||
ResultImage = resultImage;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ResultImage?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user