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;