Files
CheckDevice/Check.Main/Camera/HikvisionCamera.cs
2025-10-20 14:47:17 +08:00

505 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

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

using 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();
}
}
}