视觉修改

This commit is contained in:
17860779768
2025-08-25 16:33:58 +08:00
commit 2e46747ba9
49 changed files with 11062 additions and 0 deletions

View File

@@ -0,0 +1,838 @@
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上显示警告。
// }
// }
//}
}
}

View File

@@ -0,0 +1,211 @@
using Check.Main.Common;
using Check.Main.Infer;
using SkiaSharp;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using YoloDotNet.Extensions;
namespace Check.Main.Camera
{
public class CameraProcessor : IDisposable
{
private readonly int _cameraIndex;
private readonly int _modeId;
// private readonly ModelSettings _model;
private readonly BlockingCollection<ImageData> _imageQueue = new BlockingCollection<ImageData>();
private readonly Thread _workerThread;
private volatile bool _isRunning = false;
private long _imageCounter = 0;
private readonly object _counterLock = new object(); // 用于线程安全地重置计数器
public event EventHandler<ProcessingCompletedEventArgs> OnProcessingCompleted;
public CameraProcessor(int cameraIndex, int modelId)//, ModelSettings model
{
_cameraIndex = cameraIndex;
_modeId = modelId;
//_model = model;
_workerThread = new Thread(ProcessQueue) { IsBackground = true, Name = $"Cam_{_cameraIndex}_Processor" };
}
public void Start()
{
_isRunning = true;
_workerThread.Start();
}
public void EnqueueImage(Bitmap bmp)
{
if (!_isRunning)
{
bmp?.Dispose();
return;
}
_imageCounter++;
_imageQueue.Add(new ImageData(_imageCounter, _cameraIndex, bmp));
}
private void ProcessQueue()
{
// 从模型管理器获取此线程专属的YOLO模型
var yoloModel = YoloModelManager.GetModel(_modeId);
if (yoloModel == null)
{
ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型处理线程已中止。");
return; // 如果没有模型,此线程无法工作
}
while (_isRunning)
{
try
{
// 阻塞式地从队列中取出图像,如果队列为空则等待
ImageData data = _imageQueue.Take();
using (data)
{
//SKImage resultSkImage = null; // 用于存储最终绘制好结果的图像
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";
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
));
}
}
}
}
}
catch (InvalidOperationException)
{
// 当调用 Stop 时,会 CompleteAdding 队列Take 会抛出此异常,是正常退出流程
break;
}
catch (Exception ex)
{
ThreadSafeLogger.Log($"[ERROR] 相机 #{_cameraIndex} 处理线程异常: {ex.Message}");
}
}
}
/// <summary>
/// 将 System.Drawing.Bitmap 安全地转换为 SkiaSharp.SKImage。
/// </summary>
private SKImage ConvertBitmapToSKImage(Bitmap bitmap)
{
if (bitmap == null) return null;
try
{
// 使用 using 确保 SKBitmap 被正确释放
using (var skBitmap = ToSKBitmapFast(bitmap))
{
return SKImage.FromBitmap(skBitmap);
}
}
catch (Exception ex)
{
ThreadSafeLogger.Log($"[错误] Bitmap to SKImage 转换失败: {ex.Message}");
return null;
}
}
public static SKBitmap ToSKBitmapFast(Bitmap bitmap)
{
// 确保是 32bppArgbBGRA 内存布局)
Bitmap src = bitmap;
if (bitmap.PixelFormat != PixelFormat.Format32bppArgb)
{
src = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format32bppArgb);
using (Graphics g = Graphics.FromImage(src))
{
g.DrawImage(bitmap, 0, 0, bitmap.Width, bitmap.Height);
}
}
var rect = new Rectangle(0, 0, src.Width, src.Height);
var bmpData = src.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
var info = new SKImageInfo(src.Width, src.Height, SKColorType.Bgra8888, SKAlphaType.Premul);
var skBitmap = new SKBitmap(info);
IntPtr destPtr = skBitmap.GetPixels(); // 目标内存
IntPtr srcRowPtr = bmpData.Scan0; // 源行首
int srcStride = bmpData.Stride;
int destRowBytes = skBitmap.RowBytes;
int copyBytesPerRow = Math.Min(srcStride, destRowBytes);
// 使用一次分配的缓冲区并用 Marshal.Copy 行拷贝(不分配每行)
byte[] row = new byte[copyBytesPerRow];
for (int y = 0; y < src.Height; y++)
{
IntPtr s = IntPtr.Add(bmpData.Scan0, y * srcStride);
IntPtr d = IntPtr.Add(destPtr, y * destRowBytes);
Marshal.Copy(s, row, 0, copyBytesPerRow);
Marshal.Copy(row, 0, d, copyBytesPerRow);
}
return skBitmap;
}
finally
{
src.UnlockBits(bmpData);
if (!ReferenceEquals(src, bitmap))
{
src.Dispose();
}
}
}
public void Stop()
{
_isRunning = false;
// 解除阻塞,让线程可以检查 _isRunning 标志并退出
_imageQueue.CompleteAdding();
_workerThread.Join(500); // 等待线程结束
}
/// <summary>
/// 线程安全地重置该相机的图像计数器。
/// </summary>
public void ResetCounter()
{
lock (_counterLock)
{
_imageCounter = 0;
}
}
// 别忘了在 DetectionCoordinator 中添加一个辅助方法来获取处理器
public void Dispose()
{
Stop();
_imageQueue.Dispose();
}
}
}

View File

@@ -0,0 +1,182 @@
using Check.Main.Common;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms.Design;
namespace Check.Main.Camera
{
// 相机触发方式枚举
public enum TriggerModeType
{
[Description("连续采集")]
Continuous,
[Description("软件触发")]
Software,
[Description("硬件触发")]
Hardware
}
public enum CheckType
{
[Description("传统算法")]
Traditional,
[Description("深度学习")]
DeepLearning
}
/// <summary>
/// 相机配置信息类用于PropertyGrid显示和编辑
/// </summary>
public class CameraSettings : INotifyPropertyChanged, ICloneable
{
// 1. 实现 INotifyPropertyChanged 接口所要求的事件
public event PropertyChangedEventHandler PropertyChanged;
private int _cameraIndex = 0;
private string _name = "Camera-1";
private string _ipAddress = "192.168.1.100";
private string _ipDeviceAddress = "192.168.1.101";
private TriggerModeType _triggerMode = TriggerModeType.Continuous;
private bool _isEnabled = true;
private CheckType _checkType = CheckType.DeepLearning;
private int _modelID = 0;
[Category("基本信息"), DisplayName("相机编号"), Description("相机的唯一标识符,用于与模型编号对应。")]
public int CameraIndex
{
get => _cameraIndex;
set
{
if (_cameraIndex != value)
{
_cameraIndex = value;
OnPropertyChanged();
}
}
}
[Category("基本信息"), DisplayName("相机名称"), Description("给相机起一个唯一的别名")]
public string Name
{
get => _name;
set
{
// 2. 在setter中检查值是否真的改变了
if (_name != value)
{
_name = value;
// 3. 如果改变了就调用通知方法广播“Name”属性已变更的消息
OnPropertyChanged();
}
}
}
[Category("网络信息"), DisplayName("相机IP地址"), Description("相机的IP地址GigE相机需要")]
public string IPAddress
{
get => _ipAddress;
set
{
if (_ipAddress != value)
{
_ipAddress = value;
OnPropertyChanged();
}
}
}
[Category("网络信息"), DisplayName("上位机IP地址"), Description("相机对应的上位机的IP地址GigE相机需要")]
public string IPDeviceAddress
{
get => _ipDeviceAddress;
set
{
if (_ipDeviceAddress != value)
{
_ipDeviceAddress = value;
OnPropertyChanged();
}
}
}
[Category("采集控制"), DisplayName("触发模式"), Description("设置相机的图像采集触发方式")]
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
public TriggerModeType TriggerMode
{
get => _triggerMode;
set
{
if (_triggerMode != value)
{
_triggerMode = value;
OnPropertyChanged();
}
}
}
[Category("采集控制"), DisplayName("是否启用"), Description("是否在程序启动时初始化并使用此相机")]
public bool IsEnabled
{
get => _isEnabled;
set
{
if (_isEnabled != value)
{
_isEnabled = value;
OnPropertyChanged();
}
}
}
[Category("检测配置"), DisplayName("检测类型"), Description("使用传统算法或深度学习")]
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
public CheckType CheckType
{
get => _checkType;
set
{
if (_checkType != value)
{
_checkType = value;
OnPropertyChanged();
}
}
}
[Category("基本信息"), DisplayName("模型编号"), Description("相机调用的模型编号")]
[TypeConverter(typeof(ModelSelectionConverter))]
public int ModelID
{
get => _modelID;
set
{
if (_modelID != value)
{
_modelID = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// 4. 触发 PropertyChanged 事件的辅助方法
/// [CallerMemberName] 特性是一个“语法糖”,它会自动获取调用此方法的属性的名称。
/// 例如在Name属性的setter中调用OnPropertyChanged()C#编译器会自动传入"Name"作为参数。
/// </summary>
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public object Clone()
{
// MemberwiseClone() 会创建一个新对象,并将当前对象的所有非静态字段的值
// 复制到新对象中。对于值类型,这是值的拷贝;对于引用类型,这是引用的拷贝。
// 在本类中,这已经足够了。
return this.MemberwiseClone();
}
}
}

View File

@@ -0,0 +1,502 @@
using MvCamCtrl.NET;
using Sunny.UI;
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.Threading;
using System.Threading.Tasks;
namespace Check.Main.Camera
{
public class HikvisionCamera : IDisposable
{
[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
// 事件委托
public delegate void ImageAcquiredEventHandler(HikvisionCamera sender, Bitmap bmp);
// public delegate void ImageAcquiredEventHandler(object sender, Bitmap bmp);
public delegate void CameraMessageEventHandler(object sender, string message, int errorCode = 0);
// 事件
public event ImageAcquiredEventHandler ImageAcquired;
public event CameraMessageEventHandler CameraMessage;
// 私有成员变量
private MyCamera m_MyCamera;
private MyCamera.MV_CC_DEVICE_INFO_LIST m_stDeviceList;
private bool m_bGrabbing = false;
private Thread m_hReceiveThread = null;
private Bitmap m_bitmap = null;
private IntPtr m_ConvertDstBuf = IntPtr.Zero;
private uint m_nConvertDstBufLen = 0;
public string Name { get; set; }
/// <summary>
/// 由外部逻辑分配的、唯一的相机软件编号。
/// 主要用作图像路由和与逻辑处理器匹配的键。
/// </summary>
public int CameraIndex { get; set; }
/// <summary>
/// 相机是否已经打开
/// </summary>
public bool IsOpen { get; private set; } = false;
public TriggerModeType TriggerMode { get; internal set; } // internal set 保证只有 CameraManager 能设置它
/// <summary>
/// 相机是否正在采集
/// </summary>
public bool IsGrabbing => m_bGrabbing;
public HikvisionCamera()
{
// 初始化SDK
MyCamera.MV_CC_Initialize_NET();
}
/// <summary>
/// 查找所有可用的相机设备
/// </summary>
/// <returns>设备名称列表</returns>
public List<string> FindDevices()
{
var deviceList = new List<string>();
m_stDeviceList = new MyCamera.MV_CC_DEVICE_INFO_LIST();
int nRet = MyCamera.MV_CC_EnumDevices_NET(MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE, ref m_stDeviceList);
if (nRet != MyCamera.MV_OK)
{
OnCameraMessage("设备枚举失败!", nRet);
return deviceList;
}
for (int i = 0; i < m_stDeviceList.nDeviceNum; i++)
{
var device = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(m_stDeviceList.pDeviceInfo[i], typeof(MyCamera.MV_CC_DEVICE_INFO));
if (device.nTLayerType == MyCamera.MV_GIGE_DEVICE)
{
var gigeInfo = (MyCamera.MV_GIGE_DEVICE_INFO_EX)MyCamera.ByteToStruct(device.SpecialInfo.stGigEInfo, typeof(MyCamera.MV_GIGE_DEVICE_INFO_EX));
if (!string.IsNullOrEmpty(Encoding.Default.GetString(gigeInfo.chUserDefinedName).TrimEnd('\0')))
deviceList.Add("GEV: " + gigeInfo.chUserDefinedName + " (" + gigeInfo.chSerialNumber + ")");
else
deviceList.Add("GEV: " + gigeInfo.chManufacturerName + " " + gigeInfo.chModelName + " (" + gigeInfo.chSerialNumber + ")");
}
else if (device.nTLayerType == MyCamera.MV_USB_DEVICE)
{
var usbInfo = (MyCamera.MV_USB3_DEVICE_INFO_EX)MyCamera.ByteToStruct(device.SpecialInfo.stUsb3VInfo, typeof(MyCamera.MV_USB3_DEVICE_INFO_EX));
if (!string.IsNullOrEmpty(Encoding.Default.GetString(usbInfo.chUserDefinedName).TrimEnd('\0')))
deviceList.Add("U3V: " + usbInfo.chUserDefinedName + " (" + usbInfo.chSerialNumber + ")");
else
deviceList.Add("U3V: " + usbInfo.chManufacturerName + " " + usbInfo.chModelName + " (" + usbInfo.chSerialNumber + ")");
}
}
return deviceList;
}
/// <summary>
/// 根据索引打开相机
/// </summary>
/// <param name="index">设备索引</param>
/// <returns>成功返回true否则返回false</returns>
public bool Open(CameraSettings camConfig)
{
string ipCam = "";
string ipDevice = "";
FindDevices();
if (camConfig.IsEnabled == true)
{
ipCam = camConfig.IPAddress;
ipDevice = camConfig.IPDeviceAddress;
}
try
{
if (m_stDeviceList.nDeviceNum == 0 || string.IsNullOrEmpty(camConfig.IPAddress))
{
OnCameraMessage("没有找到设备或索引无效。", -1);
return false;
}
if (IsOpen)
{
OnCameraMessage("相机已经打开。", 0);
return true;
}
MyCamera.MV_CC_DEVICE_INFO device = new MyCamera.MV_CC_DEVICE_INFO();
device.nTLayerType = MyCamera.MV_GIGE_DEVICE;
MyCamera.MV_GIGE_DEVICE_INFO stGigEDev = new MyCamera.MV_GIGE_DEVICE_INFO();
var parts = ipCam.Split('.');
int nIp1 = Convert.ToInt32(parts[0]);
int nIp2 = Convert.ToInt32(parts[1]);
int nIp3 = Convert.ToInt32(parts[2]);
int nIp4 = Convert.ToInt32(parts[3]);
stGigEDev.nCurrentIp = (uint)((nIp1 << 24) | (nIp2 << 16) | (nIp3 << 8) | nIp4);
parts = ipDevice.Split('.');
nIp1 = Convert.ToInt32(parts[0]);
nIp2 = Convert.ToInt32(parts[1]);
nIp3 = Convert.ToInt32(parts[2]);
nIp4 = Convert.ToInt32(parts[3]);
stGigEDev.nNetExport = (uint)((nIp1 << 24) | (nIp2 << 16) | (nIp3 << 8) | nIp4);
IntPtr stGigeInfoPtr = Marshal.AllocHGlobal(216);
Marshal.StructureToPtr(stGigEDev, stGigeInfoPtr, false);
device.SpecialInfo.stGigEInfo = new Byte[540];
Marshal.Copy(stGigeInfoPtr, device.SpecialInfo.stGigEInfo, 0, 540);
//释放内存空间
Marshal.FreeHGlobal(stGigeInfoPtr);
//var device = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(m_stDeviceList.pDeviceInfo[index], typeof(MyCamera.MV_CC_DEVICE_INFO));
m_MyCamera = new MyCamera();
int nRet = m_MyCamera.MV_CC_CreateDevice_NET(ref device);
if (nRet != MyCamera.MV_OK)
{
OnCameraMessage("创建设备句柄失败!", nRet);
return false;
}
nRet = m_MyCamera.MV_CC_OpenDevice_NET();
if (nRet != MyCamera.MV_OK)
{
OnCameraMessage("打开设备失败!", nRet);
m_MyCamera.MV_CC_DestroyDevice_NET();
return false;
}
// 探测网络最佳包大小(只对GigE相机有效)
if (device.nTLayerType == MyCamera.MV_GIGE_DEVICE)
{
int nPacketSize = m_MyCamera.MV_CC_GetOptimalPacketSize_NET();
if (nPacketSize > 0)
{
m_MyCamera.MV_CC_SetIntValueEx_NET("GevSCPSPacketSize", nPacketSize);
}
}
// 默认设置为连续模式
SetContinuousMode();
IsOpen = true;
OnCameraMessage("相机打开成功。", 0);
return true;
}
catch (Exception ex)
{
OnCameraMessage("相机打开失败。"+ ex.ToString(), 0);
return false;
}
}
/// <summary>
/// 关闭相机
/// </summary>
public void Close()
{
if (!IsOpen) return;
if (m_bGrabbing)
{
StopGrabbing();
}
m_MyCamera.MV_CC_CloseDevice_NET();
m_MyCamera.MV_CC_DestroyDevice_NET();
IsOpen = false;
m_MyCamera = null;
OnCameraMessage("相机已关闭。", 0);
}
/// <summary>
/// 开始采集图像
/// </summary>
/// <returns>成功返回true</returns>
public bool StartGrabbing()
{
if (!IsOpen || m_bGrabbing)
{
OnCameraMessage(m_bGrabbing ? "已经在采集中。" : "相机未打开。", -1);
return false;
}
// 取图前的必要操作
if (NecessaryOperBeforeGrab() != MyCamera.MV_OK)
{
return false;
}
int nRet = m_MyCamera.MV_CC_StartGrabbing_NET();
if (nRet != MyCamera.MV_OK)
{
OnCameraMessage("开始采集失败!", nRet);
return false;
}
m_bGrabbing = true;
m_hReceiveThread = new Thread(ReceiveThreadProcess);
m_hReceiveThread.IsBackground = true;
m_hReceiveThread.Start();
OnCameraMessage("开始采集。", 0);
return true;
}
/// <summary>
/// 停止采集图像
/// </summary>
public void StopGrabbing()
{
if (!m_bGrabbing) return;
m_bGrabbing = false;
if (m_hReceiveThread != null && m_hReceiveThread.IsAlive)
{
m_hReceiveThread.Join(1000); // 等待线程退出
}
int nRet = m_MyCamera.MV_CC_StopGrabbing_NET();
if (nRet != MyCamera.MV_OK)
{
OnCameraMessage("停止采集失败!", nRet);
}
OnCameraMessage("停止采集。", 0);
}
/// <summary>
/// 设置为连续采集模式
/// </summary>
public void SetContinuousMode()
{
if (!IsOpen) return;
m_MyCamera.MV_CC_SetEnumValue_NET("AcquisitionMode", (uint)MyCamera.MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS);
m_MyCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_OFF);
OnCameraMessage("已设置为连续模式。", 0);
}
/// <summary>
/// 设置为触发模式
/// </summary>
/// <param name="isSoftwareTrigger">true为软触发, false为硬触发(Line0)</param>
public void SetTriggerMode(bool isSoftwareTrigger)
{
if (!IsOpen) return;
m_MyCamera.MV_CC_SetEnumValue_NET("AcquisitionMode", (uint)MyCamera.MV_CAM_ACQUISITION_MODE.MV_ACQ_MODE_CONTINUOUS); // 触发模式也需要在Continuous模式下
m_MyCamera.MV_CC_SetEnumValue_NET("TriggerMode", (uint)MyCamera.MV_CAM_TRIGGER_MODE.MV_TRIGGER_MODE_ON);
if (isSoftwareTrigger)
{
m_MyCamera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_SOFTWARE);
OnCameraMessage("已设置为软触发模式。", 0);
}
else
{
m_MyCamera.MV_CC_SetEnumValue_NET("TriggerSource", (uint)MyCamera.MV_CAM_TRIGGER_SOURCE.MV_TRIGGER_SOURCE_LINE0);
OnCameraMessage("已设置为硬触发(Line0)模式。", 0);
}
}
/// <summary>
/// 执行一次软触发
/// </summary>
/// <returns>成功返回true</returns>
public bool SoftwareTrigger()
{
if (!IsOpen || !m_bGrabbing)
{
OnCameraMessage("请先打开相机并开始采集。", -1);
return false;
}
int nRet = m_MyCamera.MV_CC_SetCommandValue_NET("TriggerSoftware");
if (nRet != MyCamera.MV_OK)
{
OnCameraMessage("软触发失败!", nRet);
return false;
}
return true;
}
/// <summary>
/// 图像接收线程
/// </summary>
private void ReceiveThreadProcess()
{
MyCamera.MV_FRAME_OUT stFrameInfo = new MyCamera.MV_FRAME_OUT();
var stConvertParam = new MyCamera.MV_PIXEL_CONVERT_PARAM();
while (m_bGrabbing)
{
int nRet = m_MyCamera.MV_CC_GetImageBuffer_NET(ref stFrameInfo, 1000);
if (nRet == MyCamera.MV_OK)
{
Bitmap bmpForEvent = null; // 在 try 外部声明
try
{
// 初始化转换参数
stConvertParam.nWidth = stFrameInfo.stFrameInfo.nWidth;
stConvertParam.nHeight = stFrameInfo.stFrameInfo.nHeight;
stConvertParam.pSrcData = stFrameInfo.pBufAddr;
stConvertParam.nSrcDataLen = stFrameInfo.stFrameInfo.nFrameLen;
stConvertParam.enSrcPixelType = stFrameInfo.stFrameInfo.enPixelType;
stConvertParam.pDstBuffer = m_ConvertDstBuf;
stConvertParam.nDstBufferSize = m_nConvertDstBufLen;
// 创建用于事件的Bitmap它的格式与类成员m_bitmap一致
bmpForEvent = new Bitmap((int)stConvertParam.nWidth, (int)stConvertParam.nHeight, m_bitmap.PixelFormat);
// 判断并设置目标像素格式
if (bmpForEvent.PixelFormat == PixelFormat.Format8bppIndexed)
{
stConvertParam.enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8;
}
else
{
stConvertParam.enDstPixelType = MyCamera.MvGvspPixelType.PixelType_Gvsp_BGR8_Packed;
}
// 执行像素格式转换
nRet = m_MyCamera.MV_CC_ConvertPixelType_NET(ref stConvertParam);
if (nRet == MyCamera.MV_OK)
{
// 转换成功,锁定位图内存并拷贝数据
BitmapData bmpData = bmpForEvent.LockBits(new Rectangle(0, 0, stConvertParam.nWidth, stConvertParam.nHeight), ImageLockMode.WriteOnly, bmpForEvent.PixelFormat);
// 使用更健壮的逐行拷贝
int pixelSize = (bmpForEvent.PixelFormat == PixelFormat.Format8bppIndexed) ? 1 : 3;
for (int i = 0; i < bmpData.Height; ++i)
{
IntPtr pDst = new IntPtr(bmpData.Scan0.ToInt64() + i * bmpData.Stride);
IntPtr pSrc = new IntPtr(m_ConvertDstBuf.ToInt64() + i * stConvertParam.nWidth * pixelSize);
CopyMemory(pDst, pSrc, (uint)(stConvertParam.nWidth * pixelSize));
}
bmpForEvent.UnlockBits(bmpData);
// 触发图像采集事件,转移 bmpForEvent 的所有权
ImageAcquired?.Invoke(this, bmpForEvent);
// 【关键】所有权已转移将本地引用设为null防止它在finally块中被错误地释放
bmpForEvent = null;
}
// 如果转换失败,不执行任何操作,让 finally 块来处理 bmpForEvent 的释放
}
finally
{
// 【关键】如果 bmpForEvent 不为 null意味着它被成功创建
// 但没有被成功传递给事件处理器(可能因为转换失败或发生其他异常),
// 在这里必须将其释放。
bmpForEvent?.Dispose();
// 无论成功与否都必须释放相机SDK的内部图像缓存
m_MyCamera.MV_CC_FreeImageBuffer_NET(ref stFrameInfo);
}
}
else
{
// 超时,继续等待
Thread.Sleep(5);
}
}
}
/// <summary>
/// 取图前的内存和参数准备
/// </summary>
private int NecessaryOperBeforeGrab()
{
var stWidth = new MyCamera.MVCC_INTVALUE_EX();
int nRet = m_MyCamera.MV_CC_GetIntValueEx_NET("Width", ref stWidth);
if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取宽度失败!", nRet); return nRet; }
var stHeight = new MyCamera.MVCC_INTVALUE_EX();
nRet = m_MyCamera.MV_CC_GetIntValueEx_NET("Height", ref stHeight);
if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取高度失败!", nRet); return nRet; }
var stPixelFormat = new MyCamera.MVCC_ENUMVALUE();
nRet = m_MyCamera.MV_CC_GetEnumValue_NET("PixelFormat", ref stPixelFormat);
if (nRet != MyCamera.MV_OK) { OnCameraMessage("获取像素格式失败!", nRet); return nRet; }
PixelFormat bitmapPixelFormat;
// 判断当前像素格式是否为Mono
bool isMono = IsMono(stPixelFormat.nCurValue);
if (isMono)
{
bitmapPixelFormat = PixelFormat.Format8bppIndexed;
m_nConvertDstBufLen = (uint)(stWidth.nCurValue * stHeight.nCurValue);
}
else
{
bitmapPixelFormat = PixelFormat.Format24bppRgb;
m_nConvertDstBufLen = (uint)(stWidth.nCurValue * stHeight.nCurValue * 3);
}
// 分配转换后的图像数据缓存
if (m_ConvertDstBuf != IntPtr.Zero) Marshal.FreeHGlobal(m_ConvertDstBuf);
m_ConvertDstBuf = Marshal.AllocHGlobal((int)m_nConvertDstBufLen);
// 创建Bitmap对象
if (m_bitmap != null) m_bitmap.Dispose();
m_bitmap = new Bitmap((int)stWidth.nCurValue, (int)stHeight.nCurValue, bitmapPixelFormat);
// 如果是Mono8格式设置调色板
if (isMono)
{
ColorPalette palette = m_bitmap.Palette;
for (int i = 0; i < 256; i++) palette.Entries[i] = Color.FromArgb(i, i, i);
m_bitmap.Palette = palette;
}
return MyCamera.MV_OK;
}
private bool IsMono(uint enPixelType)
{
switch (enPixelType)
{
case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8:
case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono10:
case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono10_Packed:
case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono12:
case (uint)MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono12_Packed:
return true;
default:
return false;
}
}
/// <summary>
/// 触发相机消息事件
/// </summary>
private void OnCameraMessage(string message, int errorCode = 0)
{
CameraMessage?.Invoke(this, message, errorCode);
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
Close();
if (m_ConvertDstBuf != IntPtr.Zero)
{
Marshal.FreeHGlobal(m_ConvertDstBuf);
m_ConvertDstBuf = IntPtr.Zero;
}
if (m_bitmap != null)
{
m_bitmap.Dispose();
m_bitmap = null;
}
MyCamera.MV_CC_Finalize_NET();
}
}
}