10.20PLC+相机2.3视觉修改
This commit is contained in:
839
Check.Main/Camera/CameraManager.cs
Normal file
839
Check.Main/Camera/CameraManager.cs
Normal file
@@ -0,0 +1,839 @@
|
||||
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
|
||||
{
|
||||
//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>
|
||||
/// 静态构造函数,用于一次性初始化静态资源。
|
||||
/// </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>
|
||||
/// 根据配置列表初始化或更新所有相机
|
||||
/// 类似 PrepareAll,但更侧重于更新配置,会检查物理设备数量,不足时报 warning。
|
||||
/// </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>
|
||||
/// 定时器触发事件。(定时器 OnHardwareTriggerTimerElapsed 会遍历相机,对处于软件触发模式的相机执行 SoftwareTrigger()。)
|
||||
/// </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("所有相机资源已释放。");
|
||||
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// 停止所有相机 + 关闭窗口 + 清空字典 + 关闭检测协调器 + 销毁模型
|
||||
/// </summary>
|
||||
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. 深度克隆 Bitmap → 分成 显示副本 和 处理副本。
|
||||
bmpForDisplay = DeepCloneBitmap(bmp, "Display");
|
||||
bmpForProcessing = DeepCloneBitmap(bmp, "Processing");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 2.无论克隆成功与否,都必须立即释放事件传递过来的原始bmp,防止泄漏。
|
||||
bmp?.Dispose();//空条件运算符 ?.-----在访问对象成员前检查对象是否为 null,如果 bmp 不是 null,则正常调用 Dispose() 方法,替代传统的 if (bmp != null) 检查
|
||||
}
|
||||
// 分支 A: 显示副本 → 交给 FormImageDisplay.UpdateImage()。
|
||||
if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow))
|
||||
{
|
||||
// displayWindow.UpdateImage 会处理线程安全问题
|
||||
displayWindow.UpdateImage(bmpForDisplay);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本
|
||||
bmpForDisplay?.Dispose();
|
||||
}
|
||||
// 分支 B: 处理副本 → 交给 DetectionCoordinator.EnqueueImage() 进行检测。
|
||||
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 (e.ResultImage != null)
|
||||
{
|
||||
// UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可
|
||||
resultDisplay.UpdateImage(e.ResultImage);
|
||||
}
|
||||
}
|
||||
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上显示警告。
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
328
Check.Main/Camera/CameraProcessor.cs
Normal file
328
Check.Main/Camera/CameraProcessor.cs
Normal file
@@ -0,0 +1,328 @@
|
||||
using Check.Main.Common;
|
||||
using Check.Main.Infer;
|
||||
using HalconTemplateMatch;
|
||||
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;
|
||||
using OpenCvSharp;
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图像处理主循环
|
||||
/// </summary>
|
||||
private void ProcessQueue()
|
||||
{
|
||||
//// 从模型管理器获取此线程专属的YOLO模型
|
||||
//var yoloModel = YoloModelManager.GetModel(_modeId);
|
||||
//if (yoloModel == null)
|
||||
//{
|
||||
// ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型,处理线程已中止。");
|
||||
// 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)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 阻塞式地从队列中取出图像,如果队列为空则等待
|
||||
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}");
|
||||
|
||||
// 将处理结果交给协调器进行组装
|
||||
DetectionCoordinator.AssembleProduct(data, result);
|
||||
|
||||
|
||||
//给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 // 原图传出去
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
// 确保是 32bppArgb(BGRA 内存布局)
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
182
Check.Main/Camera/CameraSettings.cs
Normal file
182
Check.Main/Camera/CameraSettings.cs
Normal 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>
|
||||
/// 相机配置信息类CameraSettings,用于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 = "169.254.51.253";
|
||||
private string _ipDeviceAddress = "169.254.51.45";
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
504
Check.Main/Camera/HikvisionCamera.cs
Normal file
504
Check.Main/Camera/HikvisionCamera.cs
Normal file
@@ -0,0 +1,504 @@
|
||||
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();
|
||||
// 设置为硬触发模式(Line0)
|
||||
SetTriggerMode(false);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user