修改框架(未完全完成)实现单个相机分开绑定算法

This commit is contained in:
2025-10-20 17:47:48 +08:00
parent 31d9f8d6b6
commit 73249ee6c2
11 changed files with 1226 additions and 422 deletions

View File

@@ -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上显示警告。
// }
// }
//}
}
}