using Microsoft.Win32; using System.ComponentModel; using MvCamCtrl.NET; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using TeamAAS.Camera.Images; using TeamAAS.Camera.Models; using TeamAAS.Camera.Attributes; using TeamAAS.Camera.Enums; namespace TeamAAS.Camera.Cameras { /// /// 海康威视 MVS 相机(原生 MVS SDK,无需 VisionPro 依赖) /// [Camera("HikRobot MVS", "海康威视 MVS")] public class MvCamera : CameraBase { private MyCamera _mvCamera; private bool _isGrabbing; /// 本实例是否已完成 CreateDevice + OpenDevice(自维护状态)。 /// 不要依赖 MV_CC_IsDeviceConnected_NET 判断“是否已打开”——对新 new 的裸句柄它会返回不可靠的 true。 private bool _deviceOpened; [Browsable(false)] public MyCamera.MV_CC_DEVICE_INFO CameraInfo { get; private set; } [Browsable(false)] public MyCamera.MvGvspPixelType PixelType { get; private set; } [Browsable(false)] public override string CameraBrand => "海康威视 MVS"; public MvCamera(Guid id, int index, string name, string serialNumber, string cameraIp, bool isFindByIp) : base(id, index, name, serialNumber, cameraIp, isFindByIp) { FindDevice(); } private void FindDevice() { var deviceList = new List(); CSystem.EnumDevices(CSystem.MV_GIGE_DEVICE | CSystem.MV_USB_DEVICE, ref deviceList); for (int i = 0; i < deviceList.Count; i++) { var device = deviceList[i]; if (device.nTLayerType == CSystem.MV_GIGE_DEVICE) { var gigeInfo = (CGigECameraInfo)device; uint nIp1 = ((gigeInfo.nCurrentIp & 0xff000000) >> 24); uint nIp2 = ((gigeInfo.nCurrentIp & 0x00ff0000) >> 16); uint nIp3 = ((gigeInfo.nCurrentIp & 0x0000ff00) >> 8); uint nIp4 = (gigeInfo.nCurrentIp & 0x000000ff); string ip = string.Format("{0}.{1}.{2}.{3}", nIp1, nIp2, nIp3, nIp4); bool match = IsFindByIp ? CameraIp == ip : string.Equals(gigeInfo.chSerialNumber, SerialNumber); if (match) { ManufacturerName = gigeInfo.chManufacturerName; ModelName = gigeInfo.chModelName; SerialNumber = gigeInfo.chSerialNumber; CameraType = CameraType.GIGE; var gigeInfoData = new MyCamera.MV_GIGE_DEVICE_INFO { chSerialNumber = gigeInfo.chSerialNumber, chUserDefinedName = gigeInfo.UserDefinedName, chManufacturerName = gigeInfo.chManufacturerName, chModelName = gigeInfo.chModelName, nCurrentIp = gigeInfo.nCurrentIp, nCurrentSubNetMask = gigeInfo.nCurrentSubNetMask, nDefultGateWay = gigeInfo.nDefultGateWay }; // 关键:必须补零到 SPECIAL_INFO 联合体的声明长度, // 否则 P/Invoke 封送时抛“嵌入数组实例的长度与布局中声明的长度不匹配”。 byte[] gigeBytes = PadToUnionSize(StructToBytes(gigeInfoData), typeof(MyCamera.MV_CC_DEVICE_INFO.SPECIAL_INFO), "stGigEInfo"); CameraInfo = new MyCamera.MV_CC_DEVICE_INFO { nTLayerType = CSystem.MV_GIGE_DEVICE, SpecialInfo = new MyCamera.MV_CC_DEVICE_INFO.SPECIAL_INFO { stGigEInfo = gigeBytes } }; _isHaveCamera = true; break; } } else if (device.nTLayerType == CSystem.MV_USB_DEVICE) { var usbInfo = (CUSBCameraInfo)device; if (string.Equals(usbInfo.chSerialNumber, SerialNumber)) { ManufacturerName = usbInfo.chManufacturerName; ModelName = usbInfo.chModelName; SerialNumber = usbInfo.chSerialNumber; CameraType = CameraType.USB; var usbInfoData = new MyCamera.MV_USB3_DEVICE_INFO { chSerialNumber = usbInfo.chSerialNumber, chUserDefinedName = usbInfo.UserDefinedName, chManufacturerName = usbInfo.chManufacturerName, chModelName = usbInfo.chModelName }; byte[] usbBytes = PadToUnionSize(StructToBytes(usbInfoData), typeof(MyCamera.MV_CC_DEVICE_INFO.SPECIAL_INFO), "stUsb3VInfo"); CameraInfo = new MyCamera.MV_CC_DEVICE_INFO { nTLayerType = CSystem.MV_USB_DEVICE, SpecialInfo = new MyCamera.MV_CC_DEVICE_INFO.SPECIAL_INFO { stUsb3VInfo = usbBytes } }; _isHaveCamera = true; break; } } } } /// /// 把子设备结构转出的字节流补零到 SPECIAL_INFO 联合体中对应字段的声明长度。 /// 海康封装中 SPECIAL_INFO 的 stGigEInfo/stUsb3VInfo 是 ByValArray 定长字段, /// 实际长度不足时 P/Invoke 封送会抛“嵌入数组实例的长度与布局中声明的长度不匹配”。 /// private static byte[] PadToUnionSize(byte[] payload, Type unionType, string fieldName) { if (payload == null) return null; try { var field = unionType.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance); var attr = field? .GetCustomAttributes(typeof(MarshalAsAttribute), false) .OfType() .FirstOrDefault(); if (attr != null && attr.Value == UnmanagedType.ByValArray && attr.SizeConst > payload.Length) { var padded = new byte[attr.SizeConst]; Buffer.BlockCopy(payload, 0, padded, 0, payload.Length); return padded; } } catch { } return payload; } #region 设备发现 public static CameraInfo[] GetDevices() { var cameras = new List(); try { if (!CheckSoftwareInstalled().isInstalled) return cameras.ToArray(); var deviceList = new List(); int nRet = CSystem.EnumDevices(CSystem.MV_GIGE_DEVICE | CSystem.MV_USB_DEVICE, ref deviceList); if (nRet != 0) return cameras.ToArray(); for (int i = 0; i < deviceList.Count; i++) { if (deviceList[i].nTLayerType == CSystem.MV_GIGE_DEVICE) { var gigeInfo = (CGigECameraInfo)deviceList[i]; uint nIp1 = ((gigeInfo.nCurrentIp & 0xff000000) >> 24); uint nIp2 = ((gigeInfo.nCurrentIp & 0x00ff0000) >> 16); uint nIp3 = ((gigeInfo.nCurrentIp & 0x0000ff00) >> 8); uint nIp4 = (gigeInfo.nCurrentIp & 0x000000ff); string ip = string.Format("{0}.{1}.{2}.{3}", nIp1, nIp2, nIp3, nIp4); cameras.Add(new CameraInfo { CameraNo = i + 1, CameraName = gigeInfo.UserDefinedName, CameraBrand = "海康威视 MVS", CameraType = CameraType.GIGE, ManufacturerName = gigeInfo.chManufacturerName, Model = gigeInfo.chModelName, SerialNumber = gigeInfo.chSerialNumber, CameraIp = ip, IsFindByIp = true }); } else if (deviceList[i].nTLayerType == CSystem.MV_USB_DEVICE) { var usbInfo = (CUSBCameraInfo)deviceList[i]; cameras.Add(new CameraInfo { CameraNo = i + 1, CameraName = usbInfo.UserDefinedName, CameraBrand = "海康威视 MVS", CameraType = CameraType.USB, ManufacturerName = usbInfo.chManufacturerName, Model = usbInfo.chModelName, SerialNumber = usbInfo.chSerialNumber, CameraIp = "" }); } else if (deviceList[i].nTLayerType == CSystem.MV_CAMERALINK_DEVICE) { var clInfo = (CCamLCameraInfo)deviceList[i]; cameras.Add(new CameraInfo { CameraNo = i + 1, CameraName = clInfo.chModelName, CameraBrand = "海康威视 MVS", CameraType = CameraType.Link, ManufacturerName = clInfo.chManufacturerName, Model = clInfo.chModelName, SerialNumber = clInfo.chSerialNumber, CameraIp = "" }); } } } catch { } return cameras.ToArray(); } public static (bool isInstalled, string version) CheckSoftwareInstalled() { string softwareName = "MVS"; string softwareVersion = "4.4.0"; string[] registryPaths = { @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall" }; foreach (var path in registryPaths) { using (RegistryKey key = Registry.LocalMachine.OpenSubKey(path)) { if (key == null) continue; foreach (string subKeyName in key.GetSubKeyNames()) { using (RegistryKey subKey = key.OpenSubKey(subKeyName)) { string displayName = subKey?.GetValue("DisplayName")?.ToString(); if (displayName != null && displayName.ToUpper().Trim() == softwareName.ToUpper()) { string version = subKey.GetValue("DisplayVersion")?.ToString(); if (new Version(version) >= new Version(softwareVersion)) return (true, version); return (false, version); } } } } } return (false, ""); } #endregion #region SDK 实现 public override bool OpenDevice() { using (WriteLockScope()) { try { if (_mvCamera == null) _mvCamera = new MyCamera(); // 上次意外掉线留下的旧句柄:先销毁再重建,保证链路干净。 if (_deviceOpened && !_mvCamera.MV_CC_IsDeviceConnected_NET()) { try { _mvCamera.MV_CC_CloseDevice_NET(); } catch { } try { _mvCamera.MV_CC_DestroyDevice_NET(); } catch { } _deviceOpened = false; _isGrabbing = false; _mvCamera = new MyCamera(); } if (!_deviceOpened) { // ★ 官方推荐流程:现场枚举得到“由 runtime 按原生布局反序列化”的设备信息, // 其嵌入数组长度天然与声明一致;不要手工拼接 MV_CC_DEVICE_INFO—— // 否则 P/Invoke 会抛“嵌入数组实例的长度与布局中声明的长度不匹配”。 var matched = EnumerateAndMatch(); if (matched.nTLayerType == 0) { ErrorMessage = "未找到相机设备(请确认相机/海康虚拟相机是否在线)"; return false; } int nRet = _mvCamera.MV_CC_CreateDevice_NET(ref matched); if (nRet != 0) { ErrorMessage = $"创建设备失败: 0x{nRet:X8}"; return false; } nRet = _mvCamera.MV_CC_OpenDevice_NET(); if (nRet != 0) { ErrorMessage = $"打开设备失败: 0x{nRet:X8}"; return false; } // 按当前触发模式下发(默认连续采集 = TriggerMode Off,与历史硬编码一致) ApplyTriggerToHardware(TriggerMode); _deviceOpened = true; } if (!IsConnected) OnConnectChanged(true); IsConnected = true; return true; } catch (Exception ex) { ErrorMessage = ex.Message; return false; } } } /// /// 现场枚举(GigE + USB)并按 序列号/IP 匹配本实例配置的相机。 /// 返回值直接来自 MV_CC_EnumDevices_NET 的原生结构经 PtrToStructure 反序列化, /// 嵌入数组长度与声明完全一致;未匹配到时返回 default(nTLayerType == 0)。 /// private MyCamera.MV_CC_DEVICE_INFO EnumerateAndMatch() { uint typeFlags = CSystem.MV_GIGE_DEVICE | CSystem.MV_USB_DEVICE; var list = new MyCamera.MV_CC_DEVICE_INFO_LIST(); int nRet = MyCamera.MV_CC_EnumDevices_NET(typeFlags, ref list); if (nRet != 0 || list.pDeviceInfo == null) return default(MyCamera.MV_CC_DEVICE_INFO); int count = Math.Min((int)list.nDeviceNum, list.pDeviceInfo.Length); for (int i = 0; i < count; i++) { var ptr = list.pDeviceInfo[i]; if (ptr == IntPtr.Zero) continue; var dev = (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure( ptr, typeof(MyCamera.MV_CC_DEVICE_INFO)); string sn = string.Empty; string ip = string.Empty; if (dev.nTLayerType == CSystem.MV_GIGE_DEVICE && dev.SpecialInfo.stGigEInfo != null) { var gige = BytesToStruct(dev.SpecialInfo.stGigEInfo); sn = gige.chSerialNumber ?? string.Empty; ip = string.Format("{0}.{1}.{2}.{3}", (gige.nCurrentIp & 0xff000000) >> 24, (gige.nCurrentIp & 0x00ff0000) >> 16, (gige.nCurrentIp & 0x0000ff00) >> 8, (gige.nCurrentIp & 0x000000ff)); } else if (dev.nTLayerType == CSystem.MV_USB_DEVICE && dev.SpecialInfo.stUsb3VInfo != null) { var usb = BytesToStruct(dev.SpecialInfo.stUsb3VInfo); sn = usb.chSerialNumber ?? string.Empty; } bool match = IsFindByIp ? CameraIp == ip : string.Equals(sn, SerialNumber, StringComparison.OrdinalIgnoreCase); if (!match) continue; // 同步展示信息(首次连接后 Manufacturer/Model 即为设备真实值) if (dev.nTLayerType == CSystem.MV_GIGE_DEVICE) { var gige = BytesToStruct(dev.SpecialInfo.stGigEInfo); CameraType = CameraType.GIGE; if (!string.IsNullOrEmpty(gige.chManufacturerName)) ManufacturerName = gige.chManufacturerName; if (!string.IsNullOrEmpty(gige.chModelName)) ModelName = gige.chModelName; if (!string.IsNullOrEmpty(gige.chSerialNumber)) SerialNumber = gige.chSerialNumber; _isHaveCamera = true; } else if (dev.nTLayerType == CSystem.MV_USB_DEVICE) { var usb = BytesToStruct(dev.SpecialInfo.stUsb3VInfo); CameraType = CameraType.USB; if (!string.IsNullOrEmpty(usb.chManufacturerName)) ManufacturerName = usb.chManufacturerName; if (!string.IsNullOrEmpty(usb.chModelName)) ModelName = usb.chModelName; if (!string.IsNullOrEmpty(usb.chSerialNumber)) SerialNumber = usb.chSerialNumber; _isHaveCamera = true; } return dev; } return default(MyCamera.MV_CC_DEVICE_INFO); } /// 把定长 byte[] 反序列化为结构体(SPECIAL_INFO 联合体成员 → 具体设备信息)。 private static T BytesToStruct(byte[] data) where T : struct { var handle = GCHandle.Alloc(data, GCHandleType.Pinned); try { return (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); } finally { handle.Free(); } } public override void CloseDevice() { using (WriteLockScope()) { // 无论 SDK 当前上报什么状态,只要本实例打开过就执行清理(防句柄泄漏)。 if (_deviceOpened && _mvCamera != null) { try { _mvCamera.MV_CC_CloseDevice_NET(); } catch { } try { _mvCamera.MV_CC_DestroyDevice_NET(); } catch { } } _deviceOpened = false; _isGrabbing = false; _mvCamera = null; IsConnected = false; OnConnectChanged(false); } } #region 触发模式 /// 海康 MVS 支持连续/软触发/硬触发(Line0) public override IReadOnlyList SupportedTriggerModes => FullTriggerModes; /// 历史行为:OpenDevice 硬编码 TriggerMode=Off,故默认连续采集 protected override CameraTriggerMode DefaultTriggerMode => CameraTriggerMode.连续采集; protected override bool ApplyTriggerMode(CameraTriggerMode mode) { using (WriteLockScope()) return ApplyTriggerToHardware(mode); } /// /// 把触发模式下发到海康 SDK(GenICam 字符串接口,返回 0 = MV_OK)。 /// 调用方需已持有写锁(OpenDevice/Grab 内部已在锁内;锁可重入)。 /// private bool ApplyTriggerToHardware(CameraTriggerMode mode) { if (_mvCamera == null || !_mvCamera.MV_CC_IsDeviceConnected_NET()) return false; _mvCamera.MV_CC_SetEnumValueByString_NET("AcquisitionMode", "Continuous"); switch (mode) { case CameraTriggerMode.连续采集: return _mvCamera.MV_CC_SetEnumValueByString_NET("TriggerMode", "Off") == 0; case CameraTriggerMode.软触发: _mvCamera.MV_CC_SetEnumValueByString_NET("TriggerSource", "Software"); return _mvCamera.MV_CC_SetEnumValueByString_NET("TriggerMode", "On") == 0; case CameraTriggerMode.硬触发上升沿: case CameraTriggerMode.硬触发下降沿: _mvCamera.MV_CC_SetEnumValueByString_NET("TriggerSource", "Line0"); _mvCamera.MV_CC_SetEnumValueByString_NET("TriggerActivation", mode == CameraTriggerMode.硬触发上升沿 ? "Rising" : "Falling"); return _mvCamera.MV_CC_SetEnumValueByString_NET("TriggerMode", "On") == 0; default: // 跟随相机:不下发,沿用相机自身配置 return true; } } #endregion public override GenImage Grab() { using (WriteLockScope()) { var sw = Stopwatch.StartNew(); try { if (_mvCamera == null || !_mvCamera.MV_CC_IsDeviceConnected_NET()) { ErrorMessage = "相机未连接"; return null; } if (!_isGrabbing) { _mvCamera.MV_CC_StartGrabbing_NET(); _isGrabbing = true; } // 仅软触发模式需主动发触发命令;连续采集自由出图,硬触发等外部信号 if (IsSoftwareTrigger) _mvCamera.MV_CC_TriggerSoftwareExecute_NET(); var frame = new MyCamera.MV_FRAME_OUT(); int ret2 = _mvCamera.MV_CC_GetImageBuffer_NET(ref frame, 1000); if (ret2 != 0) { ErrorMessage = $"取相失败: {ret2}"; _isGrabbing = false; return null; } try { int w = frame.stFrameInfo.nWidth; int h = frame.stFrameInfo.nHeight; Images.PixelType pixelType = Images.PixelType.Grey8; PixelType = frame.stFrameInfo.enPixelType; GenImage genImg = CreateGenImageFromFrame(frame, w, h, ref pixelType); if (genImg != null) { ErrorMessage = ""; Image = genImg; ImageWidth = (uint)w; ImageHeight = (uint)h; ApplyGammaToImage(Image); if (!IsConnected) { IsConnected = true; OnConnectChanged(true); } TotalTime = sw.Elapsed; return Image; } } finally { _mvCamera.MV_CC_FreeImageBuffer_NET(ref frame); } return null; } catch (Exception ex) { _isGrabbing = false; ErrorMessage = ex.Message; return null; } } } public override bool SetExposureTime(float exposureTime) { using (WriteLockScope()) { try { if (_mvCamera != null && _mvCamera.MV_CC_IsDeviceConnected_NET()) { _mvCamera.MV_CC_SetFloatValue_NET("ExposureTime", exposureTime); } return true; } catch { return false; } } } public override float GetExposureTime() { using (ReadLockScope()) { try { if (_mvCamera != null && _mvCamera.MV_CC_IsDeviceConnected_NET()) { return 0; } } catch { } return 0; } } public override bool SetGain(float gain) { using (WriteLockScope()) { try { if (_mvCamera != null && _mvCamera.MV_CC_IsDeviceConnected_NET()) { _mvCamera.MV_CC_SetFloatValue_NET("Gain", gain); } return true; } catch { return false; } } } public override float GetGain() { using (ReadLockScope()) { try { if (_mvCamera != null && _mvCamera.MV_CC_IsDeviceConnected_NET()) { return 0; } } catch { } return 0; } } public override bool SetGamma(float gamma) { using (WriteLockScope()) { try { if (_mvCamera != null && _mvCamera.MV_CC_IsDeviceConnected_NET()) { _mvCamera.MV_CC_SetFloatValue_NET("Gamma", gamma); return true; } } catch { } UpdateGammaSnapshot(gamma); return true; } } public override float GetGamma() { using (ReadLockScope()) { try { if (_mvCamera != null && _mvCamera.MV_CC_IsDeviceConnected_NET()) { return Gamma; } } catch { } return Gamma; } } #endregion #region 辅助方法 private static byte[] StructToBytes(T obj) where T : struct { int size = Marshal.SizeOf(obj); var bytes = new byte[size]; IntPtr ptr = Marshal.AllocHGlobal(size); try { Marshal.StructureToPtr(obj, ptr, false); Marshal.Copy(ptr, bytes, 0, size); } finally { Marshal.FreeHGlobal(ptr); } return bytes; } private static GenImage CreateGenImageFromFrame(MyCamera.MV_FRAME_OUT frame, int w, int h, ref Images.PixelType pixelType) { int stride = (int)(frame.stFrameInfo.nFrameLen / (uint)h); IntPtr dataPtr = frame.pBufAddr; switch (frame.stFrameInfo.enPixelType) { case MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono8: { pixelType = Images.PixelType.Grey8; int dataSize = w * h; var data = new byte[dataSize]; for (int y = 0; y < h; y++) Marshal.Copy(dataPtr + y * stride, data, y * w, w); return new GenImage(w, h, pixelType, data, w); } case MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono10_Packed: case MyCamera.MvGvspPixelType.PixelType_Gvsp_Mono12_Packed: { pixelType = Images.PixelType.Grey8; int dataSize = w * h; var data = new byte[dataSize]; for (int y = 0; y < h; y++) { int srcOff = y * stride; for (int x = 0; x < w; x++) { byte hi = Marshal.ReadByte(dataPtr, srcOff + x * 2 + 1); data[y * w + x] = hi; } } return new GenImage(w, h, pixelType, data, w); } case MyCamera.MvGvspPixelType.PixelType_Gvsp_RGB8_Packed: { pixelType = Images.PixelType.RGB24; int dataSize = w * 3 * h; var data = new byte[dataSize]; for (int y = 0; y < h; y++) Marshal.Copy(dataPtr + y * stride, data, y * w * 3, w * 3); return new GenImage(w, h, pixelType, data, w * 3); } case MyCamera.MvGvspPixelType.PixelType_Gvsp_BGR8_Packed: { pixelType = Images.PixelType.BGR24; int dataSize = w * 3 * h; var data = new byte[dataSize]; for (int y = 0; y < h; y++) Marshal.Copy(dataPtr + y * stride, data, y * w * 3, w * 3); return new GenImage(w, h, pixelType, data, w * 3); } case MyCamera.MvGvspPixelType.PixelType_Gvsp_BayerGR8: case MyCamera.MvGvspPixelType.PixelType_Gvsp_BayerRG8: case MyCamera.MvGvspPixelType.PixelType_Gvsp_BayerGB8: case MyCamera.MvGvspPixelType.PixelType_Gvsp_BayerBG8: { pixelType = Images.PixelType.BGR24; return DemosaicBayer8(dataPtr, w, h, stride, frame.stFrameInfo.enPixelType); } default: { int dataSize = (int)frame.stFrameInfo.nFrameLen; var data = new byte[dataSize]; Marshal.Copy(dataPtr, data, 0, dataSize); pixelType = Images.PixelType.Grey8; return new GenImage(w, h, pixelType, data, stride); } } } private static GenImage DemosaicBayer8(IntPtr src, int w, int h, int srcStride, MyCamera.MvGvspPixelType bayerType) { int dstStride = w * 3; var dst = new byte[dstStride * h]; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { byte val = Marshal.ReadByte(src, y * srcStride + x); int di = y * dstStride + x * 3; dst[di] = val; dst[di + 1] = val; dst[di + 2] = val; } } return new GenImage(w, h, Images.PixelType.BGR24, dst, dstStride); } #endregion } }