Files
CheckDevice/Check.Main/Camera/CameraManager.cs
17860779768 2e46747ba9 视觉修改
2025-08-25 16:33:58 +08:00

839 lines
35 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Check.Main.Common;
using Check.Main.Infer;
using Check.Main.Result;
using Check.Main.UI;
using OpenCvSharp;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Forms;
namespace Check.Main.Camera
{
/// <summary>
/// 静态全局相机管理器,负责所有相机的生命周期、配置应用和多相机同步
/// </summary>
public static class CameraManager
{
// 活动的相机实例字典,键为相机名称
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>();
// --- 多相机同步逻辑 ---
// 产品检测队列
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();
// --- 新增:硬触发模拟器 ---
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>
/// 静态构造函数,用于一次性初始化静态资源。
/// </summary>
static CameraManager()
{
//初始化硬触发模拟器
_hardwareTriggerSimulator = new System.Timers.Timer();
_hardwareTriggerSimulator.Elapsed += OnHardwareTriggerTimerElapsed;
_hardwareTriggerSimulator.AutoReset = true; // 确保定时器持续触发
_hardwareTriggerSimulator.Enabled = false; // 默认不启动
}
//// 事件用于向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>
///// 根据配置列表初始化或更新所有相机
///// </summary>
//public static void Initialize(List<CameraSettings> settingsList, FrmMain mainForm)
//{
// // 先停止并释放所有旧的相机
// Shutdown();
// ThreadSafeLogger.Log("开始应用新的相机配置...");
// EnabledCameraCount = settingsList.Count(s => s.IsEnabled);
// if (EnabledCameraCount == 0)
// {
// ThreadSafeLogger.Log("没有启用的相机。");
// return;
// }
// var deviceList = new HikvisionCamera().FindDevices();
// if (deviceList.Count == 0)
// {
// ThreadSafeLogger.Log("错误:未找到任何相机设备!");
// return;
// }
// int deviceIndex = 0;
// foreach (var setting in settingsList)
// {
// if (!setting.IsEnabled) continue;
// if (deviceIndex >= deviceList.Count)
// {
// ThreadSafeLogger.Log($"警告:相机配置'{setting.Name}'无法分配物理设备,因为设备数量不足。");
// continue;
// }
// // --- 创建相机实例 ---
// var cam = new HikvisionCamera { Name = setting.Name };
// cam.TriggerMode = setting.TriggerMode;
// if (!cam.Open(setting))
// {
// ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。");
// cam.Dispose();
// continue;
// }
// // --- 设置触发模式 ---
// switch (setting.TriggerMode)
// {
// case TriggerModeType.Continuous:
// cam.SetContinuousMode();
// break;
// case TriggerModeType.Software:
// cam.SetTriggerMode(true);
// break;
// case TriggerModeType.Hardware:
// cam.SetTriggerMode(false);
// break;
// }
// // --- 订阅事件 ---
// 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);
// ActiveCameras.Add(setting.Name, cam);
// CameraDisplays.Add(setting.Name, displayForm);
// mainForm.AddCameraToStatusStrip(setting.Name);
// ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。");
// deviceIndex++;
// }
//}
/// <summary>
/// 准备所有相机硬件、UI窗口和后台处理器但不开始采集。
/// 这是“启动设备”的第一阶段。
/// </summary>
public static void PrepareAll(ProcessConfig config, FrmMain mainForm)
{
// 1. 清理旧资源和UI
mainForm.ClearStatusStrip();
Shutdown();
ThreadSafeLogger.Log("开始准备设备和模型...");
// 2. 初始化检测协调器和AI模型
// 注意YoloModelManager 的 Initialize 现在也应在这里被调用,以确保逻辑集中
YoloModelManager.Initialize(config.ModelSettings);
DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings);
// 3. 创建相机硬件实例和UI窗口
var deviceList = new HikvisionCamera().FindDevices();
if (deviceList.Count == 0)
{
ThreadSafeLogger.Log("错误:未找到任何相机设备!");
return;
}
foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled))
{
var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex };
cam.TriggerMode = setting.TriggerMode;
if (!cam.Open(setting))
{
ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。");
cam.Dispose();
continue;
}
// --- 设置触发模式 ---
switch (setting.TriggerMode)
{
case TriggerModeType.Continuous:
cam.SetContinuousMode();
break;
case TriggerModeType.Software:
cam.SetTriggerMode(true);
break;
case TriggerModeType.Hardware:
cam.SetTriggerMode(false);
break;
}
// --- 订阅事件 ---
cam.ImageAcquired += OnCameraImageAcquired;
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);
OriginalImageDisplays.Add(setting.Name, originalDisplay);
ResultImageDisplays.Add(setting.Name, resultDisplay);
mainForm.AddCameraToStatusStrip(setting.Name);
}
ThreadSafeLogger.Log("所有设备和模型已准备就绪。");
}
/// <summary>
/// 根据配置列表初始化或更新所有相机
/// </summary>
public static void Initialize(ProcessConfig config, FrmMain mainForm)
{
mainForm?.ClearStatusStrip();
// 先停止并释放所有旧的相机
Shutdown();
ThreadSafeLogger.Log("开始应用新的相机配置...");
// 2. 初始化新的检测协调器
DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings);
var deviceList = new HikvisionCamera().FindDevices();
if (deviceList.Count == 0)
{
ThreadSafeLogger.Log("错误:未找到任何相机设备!");
return;
}
int deviceIndex = 0;
foreach (var device in config.ModelSettings.Where(s => s.IsEnabled))
{
if (!device.IsEnabled) continue;
mainForm.AddCameraToStatusStrip(device.Name);
}
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, CameraIndex = setting.CameraIndex };
cam.TriggerMode = setting.TriggerMode;
if (!cam.Open(setting))
{
ThreadSafeLogger.Log($"错误:打开相机'{setting.Name}'失败。");
cam.Dispose();
continue;
}
// --- 设置触发模式 ---
switch (setting.TriggerMode)
{
case TriggerModeType.Continuous:
cam.SetContinuousMode();
break;
case TriggerModeType.Software:
cam.SetTriggerMode(true);
break;
case TriggerModeType.Hardware:
cam.SetTriggerMode(false);
break;
}
// --- 订阅事件 ---
cam.ImageAcquired += OnCameraImageAcquired;
cam.CameraMessage += (sender, msg, err) => ThreadSafeLogger.Log($"{(sender as HikvisionCamera).Name}: {msg}");
ActiveCameras.Add(setting.Name, cam);
// --- 创建显示窗口 ---
var displayForm = new FormImageDisplay { Text = $"{setting.Name} - 原图", CameraName = setting.Name };
var checkFrm = new FormImageDisplay { Text = $"{setting.Name} - 结果", CameraName = setting.Name };
displayForm.OnDisplayEvent += ThreadSafeLogger.Log;
displayForm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
checkFrm.Show(mainForm.MainDockPanel, WeifenLuo.WinFormsUI.Docking.DockState.Document);
OriginalImageDisplays.Add(setting.Name, displayForm);
ResultImageDisplays.Add(setting.Name, checkFrm);
mainForm.AddCameraToStatusStrip(setting.Name);
var processor = DetectionCoordinator.GetProcessor(cam.CameraIndex);
if (processor != null)
{
processor.OnProcessingCompleted += Processor_OnProcessingCompleted;
}
ThreadSafeLogger.Log($"相机'{setting.Name}'初始化成功,分配到物理设备 {deviceIndex}。");
deviceIndex++;
}
}
/// <summary>
/// 启动硬件触发模拟器。
/// </summary>
public static void StartHardwareTriggerSimulator()
{
if (IsHardwareTriggerSimulating) return;
_hardwareTriggerSimulator.Interval = TriggerInterval;
_hardwareTriggerSimulator.Start();
IsHardwareTriggerSimulating = true;
ThreadSafeLogger.Log($"硬件触发模拟器已启动,触发间隔: {TriggerInterval} ms。");
}
/// <summary>
/// 停止硬件触发模拟器。
/// </summary>
public static void StopHardwareTriggerSimulator()
{
if (!IsHardwareTriggerSimulating) return;
_hardwareTriggerSimulator.Stop();
IsHardwareTriggerSimulating = false;
ThreadSafeLogger.Log("硬件触发模拟器已停止。");
}
/// <summary>
/// 定时器触发事件。
/// </summary>
private static void OnHardwareTriggerTimerElapsed(object sender, ElapsedEventArgs e)
{
// 遍历所有活动的相机
foreach (var cam in ActiveCameras.Values)
{
// 仅对配置为“硬件触发”模式的相机执行操作
// 重要:我们使用软触发命令(SoftwareTrigger)来“模拟”一个外部硬件信号的到达。
if (cam.TriggerMode == TriggerModeType.Software && cam.IsGrabbing)
{
// ThreadSafeLogger.Log($"模拟硬触发信号,触发相机: {cam.Name}"); // 如果需要详细日志可以取消注释
cam.SoftwareTrigger();
}
}
}
/// <summary>
/// 所有启用的相机开始采集
/// </summary>
public static void StartAll()
{
foreach (var cam in ActiveCameras.Values)
{
cam.StartGrabbing();
}
ThreadSafeLogger.Log("所有相机已开始采集。");
}
//public static void StartDetection()
//{
// if (!IsDetectionRunning)
// {
// IsDetectionRunning = true;
// ThreadSafeLogger.Log("检测已启动,开始统计数据。");
// }
//}
//public static void StopDetection()
//{
// if (IsDetectionRunning)
// {
// IsDetectionRunning = false;
// ThreadSafeLogger.Log("检测已停止。");
// }
//}
/// <summary>
/// 所有启用的相机停止采集
/// </summary>
public static void StopAll()
{
foreach (var cam in ActiveCameras.Values)
{
cam.StopGrabbing();
}
ThreadSafeLogger.Log("所有相机已停止采集。");
}
///// <summary>
///// 停止并释放所有相机资源
///// </summary>
//public static void Shutdown()
//{
// // --- 新增:确保在关闭时停止模拟器并释放资源 ---
// StopHardwareTriggerSimulator();
// //_hardwareTriggerSimulator?.Dispose();
// StopAll();
// foreach (var cam in ActiveCameras.Values)
// {
// cam.Dispose();
// }
// foreach (var display in CameraDisplays.Values)
// {
// display.Close();
// }
// lock (QueueLock)
// {
// while (ProductQueue.Count > 0)
// {
// ProductQueue.Dequeue()?.Dispose();
// }
// }
// ResetProductCounter();
// ActiveCameras.Clear();
// CameraDisplays.Clear();
// ThreadSafeLogger.Log("正在关闭文件日志记录器...");
// ThreadSafeLogger.Log("所有相机资源已释放。");
//}
// Shutdown 方法也简化
public static void Shutdown()
{
// 1. 停止硬件和模拟器
StopAll();
StopHardwareTriggerSimulator();
// 2. 关闭相机实例和窗口
foreach (var cam in ActiveCameras.Values) { cam.Dispose(); }
foreach (var display in OriginalImageDisplays.Values) { display.Close(); }
foreach (var display in ResultImageDisplays.Values) { display.Close(); }
ActiveCameras.Clear();
OriginalImageDisplays.Clear();
ResultImageDisplays.Clear();
// 3. 关闭检测协调器,它会负责清理所有后台线程和队列
DetectionCoordinator.Shutdown();
YoloModelManager.Shutdown();
ThreadSafeLogger.Log("所有相机及协调器资源已释放。");
}
/// <summary>
/// 重置产品计数器的公共方法
/// </summary>
public static void ResetProductCounter()
{
lock (_counterLock)
{
_productCounter = 0;
}
ThreadSafeLogger.Log("产品计数器已重置。");
}
/// <summary>
/// 接收到相机图像时的核心处理逻辑
/// </summary>
//private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp)
//{
// Bitmap bmpForDisplay =null;
// Bitmap bmpForQueue = null;
// try
// {
// bmpForDisplay?.Dispose();
// bmpForQueue?.Dispose();
// // 1. 为“显示”和“处理队列”创建独立的深克隆副本
// bmpForDisplay = DeepCloneBitmap(bmp, "Display");
// bmpForQueue = DeepCloneBitmap(bmp, "Queue");
// }
// finally
// {
// // 【关键】无论克隆成功与否都必须立即释放事件传递过来的原始bmp。
// // 这是保证没有泄漏的第一道防线。
// bmp?.Dispose();
// }
// // --- 现在我们使用完全独立的副本进行后续操作 ---
// // 4. 将显示副本传递给UI
// if (bmpForDisplay != null && CameraDisplays.TryGetValue(sender.Name, out var display))
// {
// display.UpdateImage(bmpForDisplay);
// }
// else
// {
// // 如果不需要显示,或者显示失败,必须释放掉为它创建的副本
// bmpForDisplay?.Dispose();
// }
// // 5. 将队列副本添加到产品中
// if (bmpForQueue != null)
// {
// lock (QueueLock)
// {
// ProductResult currentProduct;
// bool isNewProductCycle = ProductQueue.Count == 0 || ProductQueue.Last().CapturedImages.ContainsKey(sender.Name);
// if (isNewProductCycle)
// {
// if (ProductQueue.Count > 0 && !ProductQueue.Peek().IsComplete(EnabledCameraCount))
// {
// var orphanedProduct = ProductQueue.Dequeue(); // 从队列头部移除这个不完整的产品
// ThreadSafeLogger.Log($"[警告] 产品 #{orphanedProduct.ProductID} 未能集齐所有相机图像而被丢弃,以防止内存泄漏。");
// orphanedProduct.Dispose(); // 确保释放其占用的所有资源
// }
// long newProductId;
// lock (_counterLock)
// {
// _productCounter++;
// newProductId = _productCounter;
// }
// currentProduct = new ProductResult(newProductId);
// ProductQueue.Enqueue(currentProduct);
// }
// else
// {
// currentProduct = ProductQueue.Last();
// }
// currentProduct.AddImage(sender.Name, bmpForQueue); // bmpForQueue的所有权转移给了产品队列
// while (ProductQueue.Count > 0 && ProductQueue.Peek().IsComplete(EnabledCameraCount))
// {
// // Peek() 查看队头元素但不移除它
// var finishedProduct = ProductQueue.Dequeue(); // Dequeue() 移除队头元素
// ThreadSafeLogger.Log($"产品 #{finishedProduct.ProductID} 已完整,出队处理。");
// // --- 这是您已有的处理逻辑 ---
// bool isOk = new Random().NextDouble() > 0.1;
// string finalResult = isOk ? "OK" : "NG";
// ThreadSafeLogger.Log($"产品 #{finishedProduct.ProductID} 检测结果: {finalResult}");
// if (IsDetectionRunning)
// {
// OnDetectionCompleted?.Invoke(null, new DetectionResultEventArgs(isOk));
// }
// try
// {
// // 确保完成的产品被完全释放
// finishedProduct.Dispose();
// }
// catch (Exception ex)
// {
// ThreadSafeLogger.Log($"[ERROR] 释放产品 #{finishedProduct.ProductID} 资源时出错: {ex.Message}");
// }
// }
// }
// }
//}
// 图像回调方法现在极其简单
private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp)
{
Bitmap bmpForDisplay = null;
Bitmap bmpForProcessing = null;
try
{
// 1. 为“显示”和“处理”创建两个完全独立的深克隆副本
bmpForDisplay = DeepCloneBitmap(bmp, "Display");
bmpForProcessing = DeepCloneBitmap(bmp, "Processing");
}
finally
{
// 2.无论克隆成功与否都必须立即释放事件传递过来的原始bmp防止泄漏。
bmp?.Dispose();
}
// 分支 A: 将用于显示的副本发送到对应的UI窗口
if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow))
{
// displayWindow.UpdateImage 会处理线程安全问题
displayWindow.UpdateImage(bmpForDisplay);
}
else
{
// 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本
bmpForDisplay?.Dispose();
}
// 分支 B: 将用于处理的副本发送到检测协调器的后台队列
if (bmpForProcessing != null)
{
// bmpForProcessing 的所有权在这里被转移给了协调器
DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing);
}
//// 深度克隆图像以确保线程安全
//Bitmap bmpForProcessing = DeepCloneBitmap(bmp, "Processing");
//bmp?.Dispose(); // 立即释放原始图
//// 直接将图像和相机编号交给协调器,无需任何本地处理
//DetectionCoordinator.EnqueueImage(sender.CameraIndex, bmpForProcessing);
}
// 事件处理器
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(); // 如果找不到接收者,必须释放事件参数中的图像
return;
}
// 2. 找到此相机的结果显示窗口
if (ResultImageDisplays.TryGetValue(cameraEntry.Key, out var resultDisplay))
{
var bmp = ConvertSKImageToBitmap(e.ResultImage);
if (bmp != null)
{
// UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可
resultDisplay.UpdateImage(bmp);
}
}
else
{
// 如果找到了相机但没有对应的结果窗口,也要释放图像
e.Dispose();
}
}
/// <summary>
/// 【将 SkiaSharp.SKImage 安全地转换为 System.Drawing.Bitmap。
/// </summary>
private static Bitmap ConvertSKImageToBitmap(SKImage skImage)
{
if (skImage == null) return null;
try
{
// SKImage -> SKBitmap -> System.Drawing.Bitmap
using (var skBitmap = SKBitmap.FromImage(skImage))
{
// SKBitmap.ToBitmap() 会创建一个新的 Bitmap 对象
return SKBitmapToGdiBitmapFast(skBitmap);
}
}
catch (Exception ex)
{
ThreadSafeLogger.Log($"[错误] SKImage to Bitmap 转换失败: {ex.Message}");
return null;
}
}
public static Bitmap SKBitmapToGdiBitmapFast(SKBitmap skBitmap)
{
if (skBitmap == null) throw new ArgumentNullException(nameof(skBitmap));
if (skBitmap.ColorType != SKColorType.Bgra8888 || skBitmap.AlphaType != SKAlphaType.Premul)
throw new ArgumentException("skBitmap must be Bgra8888 + Premul for the fast path.");
int w = skBitmap.Width;
int h = skBitmap.Height;
Bitmap bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb);
var rect = new Rectangle(0, 0, w, h);
var bmpData = bmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
try
{
IntPtr srcPtr = skBitmap.GetPixels();
int srcRowBytes = skBitmap.RowBytes;
int dstRowBytes = bmpData.Stride;
int copyBytesPerRow = Math.Min(srcRowBytes, dstRowBytes);
byte[] row = new byte[copyBytesPerRow]; // 复用同一行缓冲区,避免每行分配
for (int y = 0; y < h; y++)
{
IntPtr s = IntPtr.Add(srcPtr, y * srcRowBytes);
IntPtr d = IntPtr.Add(bmpData.Scan0, y * dstRowBytes);
Marshal.Copy(s, row, 0, copyBytesPerRow);
Marshal.Copy(row, 0, d, copyBytesPerRow);
}
}
finally
{
bmp.UnlockBits(bmpData);
}
return bmp;
}
/// <summary>
/// 对Bitmap进行真正的深度克隆确保内存完全独立。
/// 这是解决跨线程GDI+问题的最可靠方法。
/// </summary>
/// <param name="source">源Bitmap对象。</param>
/// <returns>一个与源图像在内存上完全独立的全新Bitmap对象。</returns>
private static Bitmap DeepCloneBitmap(Bitmap source, string cloneFor)
{
if (source == null) return null;
// 创建一个新的Bitmap对象具有与源相同的尺寸和像素格式
Bitmap clone = new Bitmap(source.Width, source.Height, source.PixelFormat);
// 如果有调色板,复制调色板
if (source.Palette.Entries.Length > 0)
{
clone.Palette = source.Palette;
}
// 锁定源和目标Bitmap的内存区域
var rect = new Rectangle(0, 0, source.Width, source.Height);
BitmapData sourceData = null; //source.LockBits(rect, ImageLockMode.ReadOnly, source.PixelFormat);
BitmapData cloneData = null;//clone.LockBits(rect, ImageLockMode.WriteOnly, clone.PixelFormat);
try
{
sourceData = source.LockBits(rect, ImageLockMode.ReadOnly, source.PixelFormat);
cloneData = clone.LockBits(rect, ImageLockMode.WriteOnly, clone.PixelFormat);
// 计算需要拷贝的字节数
int byteCount = Math.Abs(sourceData.Stride) * source.Height;
byte[] buffer = new byte[byteCount];
// 从源图像拷贝数据到字节数组
Marshal.Copy(sourceData.Scan0, buffer, 0, byteCount);
// 从字节数组拷贝数据到目标图像
Marshal.Copy(buffer, 0, cloneData.Scan0, byteCount);
}
catch (Exception ex)
{
ThreadSafeLogger.Log($"[克隆错误] 在 DeepCloneBitmap ({cloneFor}) 中发生异常: {ex.Message}");
// 如果发生错误确保返回null并且释放可能已经创建的clone对象
clone?.Dispose();
return null;
}
finally
{
// 确保即使在发生错误时也能尝试解锁
if (sourceData != null)
{
source.UnlockBits(sourceData);
}
if (cloneData != null)
{
clone.UnlockBits(cloneData);
}
//ThreadSafeLogger.Log($"[克隆完成] 解锁并完成克隆 ({cloneFor}).");
}
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上显示警告。
// }
// }
//}
}
}