Files
XplorePlane/XP.Camera/Hikvision/HikvisionCameraController.cs
T
李伟 843c4d67a6 feat: 集成海康威视相机接口
- 新增 HikvisionCameraController 实现 ICameraController
- CameraFactory 支持 Basler/Hikvision 动态切换(config.json 配置)
- PixelConverter 支持 Bayer RG/GR/GB/BG 8-bit 解码
- 修复采集链断裂问题(finally 中触发下一帧)
- 相机设置面板:宽高和像素格式改为只读显示
- NavigationPropertyPanelViewModel 日志和状态文本改为英文
- 添加 MvCameraControl.Net.dll 到 ExternalLibraries
2026-05-18 13:11:26 +08:00

536 lines
18 KiB
C#

using MvCameraControl;
using Serilog;
namespace XP.Camera;
/// <summary>
/// 海康威视相机控制器,封装 MvCameraControl.Net SDK 实现 <see cref="ICameraController"/>。
/// </summary>
/// <remarks>
/// <para>所有公共方法通过内部 <c>_syncLock</c> 对象进行 lock 同步,保证线程安全。</para>
/// <para>事件回调(ImageGrabbed、GrabError)在 SDK 回调线程上触发,不持有 _syncLock,避免死锁。</para>
/// </remarks>
public class HikvisionCameraController : ICameraController
{
private static readonly ILogger _logger = Log.ForContext<HikvisionCameraController>();
private static bool _sdkInitialized;
private static readonly object _sdkInitLock = new();
private readonly object _syncLock = new();
private IDevice? _device;
private CameraInfo? _cachedCameraInfo;
private bool _isConnected;
private bool _isGrabbing;
public HikvisionCameraController()
{
// SDK 初始化延迟到 Open() 中执行
}
/// <inheritdoc />
public bool IsConnected
{
get { lock (_syncLock) { return _isConnected; } }
}
/// <inheritdoc />
public bool IsGrabbing
{
get { lock (_syncLock) { return _isGrabbing; } }
}
/// <inheritdoc />
public event EventHandler<ImageGrabbedEventArgs>? ImageGrabbed;
/// <inheritdoc />
public event EventHandler<GrabErrorEventArgs>? GrabError;
/// <inheritdoc />
public event EventHandler? ConnectionLost;
/// <inheritdoc />
public CameraInfo Open()
{
lock (_syncLock)
{
if (_isConnected && _cachedCameraInfo != null)
{
_logger.Information("Hikvision camera already connected, returning cached info.");
return _cachedCameraInfo;
}
try
{
_logger.Information("Opening Hikvision camera connection...");
// 确保 SDK 初始化
EnsureSdkInitialized();
// 枚举设备
DeviceTLayerType layerType = DeviceTLayerType.MvGigEDevice
| DeviceTLayerType.MvUsbDevice;
List<IDeviceInfo> deviceInfoList;
int ret = DeviceEnumerator.EnumDevices(layerType, out deviceInfoList);
_logger.Information("EnumDevices(GigE|USB) returned: 0x{RetCode:X8}, device count: {Count}",
ret, deviceInfoList?.Count ?? 0);
// 如果没找到,分别尝试
if (ret == MvError.MV_OK && (deviceInfoList == null || deviceInfoList.Count == 0))
{
// 单独尝试 GigE
List<IDeviceInfo> gigeList;
int retGige = DeviceEnumerator.EnumDevices(DeviceTLayerType.MvGigEDevice, out gigeList);
_logger.Information("EnumDevices(GigE only) returned: 0x{RetCode:X8}, count: {Count}",
retGige, gigeList?.Count ?? 0);
// 单独尝试 USB
List<IDeviceInfo> usbList;
int retUsb = DeviceEnumerator.EnumDevices(DeviceTLayerType.MvUsbDevice, out usbList);
_logger.Information("EnumDevices(USB only) returned: 0x{RetCode:X8}, count: {Count}",
retUsb, usbList?.Count ?? 0);
// 合并结果
deviceInfoList = new List<IDeviceInfo>();
if (gigeList != null) deviceInfoList.AddRange(gigeList);
if (usbList != null) deviceInfoList.AddRange(usbList);
}
if (ret != MvError.MV_OK)
{
throw new CameraException($"Enumerate Hikvision devices failed: 0x{ret:X8}");
}
if (deviceInfoList == null || deviceInfoList.Count == 0)
{
throw new DeviceNotFoundException("No Hikvision camera device found.");
}
// 选择第一个设备
IDeviceInfo deviceInfo = deviceInfoList[0];
_logger.Information("Found Hikvision device: {Model} (SN: {Serial})",
deviceInfo.ModelName, deviceInfo.SerialNumber);
// 创建设备
_device = DeviceFactory.CreateDevice(deviceInfo);
// 打开设备
ret = _device.Open();
if (ret != MvError.MV_OK)
{
_device.Dispose();
_device = null;
throw new CameraException($"Open Hikvision device failed: 0x{ret:X8}");
}
// GigE 设备优化包大小
if (_device is IGigEDevice gigEDevice)
{
int packetSize;
ret = gigEDevice.GetOptimalPacketSize(out packetSize);
if (ret == MvError.MV_OK && packetSize > 0)
{
_device.Parameters.SetIntValue("GevSCPSPacketSize", packetSize);
_logger.Debug("Set GigE packet size to {PacketSize}", packetSize);
}
}
// 配置软件触发模式
_device.Parameters.SetEnumValueByString("TriggerMode", "On");
_device.Parameters.SetEnumValueByString("TriggerSource", "Software");
// 彩色相机:尝试设置输出为 BGR8 以便直接显示
// 如果相机不支持 BGR8(如只支持 Bayer),则保持默认
int fmtRet = _device.Parameters.SetEnumValueByString("PixelFormat", "BGR8Packed");
if (fmtRet != MvError.MV_OK)
{
// 尝试 Mono8(黑白相机)
fmtRet = _device.Parameters.SetEnumValueByString("PixelFormat", "Mono8");
}
_logger.Debug("Set PixelFormat result: 0x{Ret:X8}", fmtRet);
_cachedCameraInfo = new CameraInfo(
ModelName: deviceInfo.ModelName ?? "",
SerialNumber: deviceInfo.SerialNumber ?? "",
VendorName: deviceInfo.ManufacturerName ?? "",
DeviceType: deviceInfo.TLayerType.ToString()
);
_isConnected = true;
_logger.Information("Hikvision camera connected: {ModelName} (SN: {SerialNumber})",
_cachedCameraInfo.ModelName, _cachedCameraInfo.SerialNumber);
return _cachedCameraInfo;
}
catch (Exception ex) when (ex is not CameraException)
{
_device?.Dispose();
_device = null;
_logger.Error(ex, "Failed to open Hikvision camera.");
throw new CameraException("Failed to open Hikvision camera device.", ex);
}
}
}
/// <inheritdoc />
public void Close()
{
lock (_syncLock)
{
if (!_isConnected)
{
_logger.Information("Hikvision camera not connected, Close() ignored.");
return;
}
try
{
if (_isGrabbing)
{
StopGrabbingInternal();
}
_logger.Information("Closing Hikvision camera connection...");
_device?.Close();
_device?.Dispose();
_device = null;
_isConnected = false;
_cachedCameraInfo = null;
_logger.Information("Hikvision camera connection closed.");
}
catch (Exception ex) when (ex is not CameraException)
{
_device = null;
_isConnected = false;
_isGrabbing = false;
_cachedCameraInfo = null;
_logger.Error(ex, "Error while closing Hikvision camera.");
throw new CameraException("Failed to close Hikvision camera device.", ex);
}
}
}
/// <inheritdoc />
public void StartGrabbing()
{
lock (_syncLock)
{
EnsureConnected();
if (_isGrabbing)
{
_logger.Information("Already grabbing, StartGrabbing() ignored.");
return;
}
try
{
_logger.Information("Starting Hikvision grabbing with software trigger...");
// 设置缓存节点数
_device!.StreamGrabber.SetImageNodeNum(5);
// 注册回调
_device.StreamGrabber.FrameGrabedEvent += OnFrameGrabbed;
// 开始采集
int ret = _device.StreamGrabber.StartGrabbing();
if (ret != MvError.MV_OK)
{
_device.StreamGrabber.FrameGrabedEvent -= OnFrameGrabbed;
throw new CameraException($"Start grabbing failed: 0x{ret:X8}");
}
_isGrabbing = true;
_logger.Information("Hikvision grabbing started.");
}
catch (Exception ex) when (ex is not CameraException)
{
_logger.Error(ex, "Failed to start Hikvision grabbing.");
throw new CameraException("Failed to start grabbing.", ex);
}
}
}
/// <inheritdoc />
public void ExecuteSoftwareTrigger()
{
lock (_syncLock)
{
if (!_isGrabbing)
{
throw new InvalidOperationException("Cannot execute software trigger: camera is not grabbing.");
}
try
{
int ret = _device!.Parameters.SetCommandValue("TriggerSoftware");
if (ret != MvError.MV_OK)
{
throw new CameraException($"Execute software trigger failed: 0x{ret:X8}");
}
}
catch (Exception ex) when (ex is not CameraException and not InvalidOperationException)
{
_logger.Error(ex, "Failed to execute software trigger.");
throw new CameraException("Failed to execute software trigger.", ex);
}
}
}
/// <inheritdoc />
public void StopGrabbing()
{
lock (_syncLock)
{
if (!_isGrabbing) return;
StopGrabbingInternal();
}
}
/// <inheritdoc />
public double GetExposureTime()
{
lock (_syncLock)
{
EnsureConnected();
IFloatValue floatValue;
int ret = _device!.Parameters.GetFloatValue("ExposureTime", out floatValue);
if (ret != MvError.MV_OK)
throw new CameraException($"Get ExposureTime failed: 0x{ret:X8}");
return floatValue.CurValue;
}
}
/// <inheritdoc />
public void SetExposureTime(double microseconds)
{
lock (_syncLock)
{
EnsureConnected();
// 关闭自动曝光
_device!.Parameters.SetEnumValueByString("ExposureAuto", "Off");
int ret = _device.Parameters.SetFloatValue("ExposureTime", (float)microseconds);
if (ret != MvError.MV_OK)
throw new CameraException($"Set ExposureTime failed: 0x{ret:X8}");
_logger.Information("Hikvision exposure time set to {Microseconds} µs.", microseconds);
}
}
/// <inheritdoc />
public double GetGain()
{
lock (_syncLock)
{
EnsureConnected();
IFloatValue floatValue;
int ret = _device!.Parameters.GetFloatValue("Gain", out floatValue);
if (ret != MvError.MV_OK)
throw new CameraException($"Get Gain failed: 0x{ret:X8}");
return floatValue.CurValue;
}
}
/// <inheritdoc />
public void SetGain(double value)
{
lock (_syncLock)
{
EnsureConnected();
_device!.Parameters.SetEnumValueByString("GainAuto", "Off");
int ret = _device.Parameters.SetFloatValue("Gain", (float)value);
if (ret != MvError.MV_OK)
throw new CameraException($"Set Gain failed: 0x{ret:X8}");
_logger.Information("Hikvision gain set to {Value}.", value);
}
}
/// <inheritdoc />
public int GetWidth()
{
lock (_syncLock)
{
EnsureConnected();
IIntValue intValue;
int ret = _device!.Parameters.GetIntValue("Width", out intValue);
if (ret != MvError.MV_OK)
throw new CameraException($"Get Width failed: 0x{ret:X8}");
return (int)intValue.CurValue;
}
}
/// <inheritdoc />
public void SetWidth(int value)
{
lock (_syncLock)
{
EnsureConnected();
int ret = _device!.Parameters.SetIntValue("Width", value);
if (ret != MvError.MV_OK)
throw new CameraException($"Set Width failed: 0x{ret:X8}");
_logger.Information("Hikvision width set to {Value}.", value);
}
}
/// <inheritdoc />
public int GetHeight()
{
lock (_syncLock)
{
EnsureConnected();
IIntValue intValue;
int ret = _device!.Parameters.GetIntValue("Height", out intValue);
if (ret != MvError.MV_OK)
throw new CameraException($"Get Height failed: 0x{ret:X8}");
return (int)intValue.CurValue;
}
}
/// <inheritdoc />
public void SetHeight(int value)
{
lock (_syncLock)
{
EnsureConnected();
int ret = _device!.Parameters.SetIntValue("Height", value);
if (ret != MvError.MV_OK)
throw new CameraException($"Set Height failed: 0x{ret:X8}");
_logger.Information("Hikvision height set to {Value}.", value);
}
}
/// <inheritdoc />
public string GetPixelFormat()
{
lock (_syncLock)
{
EnsureConnected();
IEnumValue enumValue;
int ret = _device!.Parameters.GetEnumValue("PixelFormat", out enumValue);
if (ret != MvError.MV_OK)
throw new CameraException($"Get PixelFormat failed: 0x{ret:X8}");
return enumValue.CurEnumEntry.Symbolic;
}
}
/// <inheritdoc />
public void SetPixelFormat(string format)
{
lock (_syncLock)
{
EnsureConnected();
int ret = _device!.Parameters.SetEnumValueByString("PixelFormat", format);
if (ret != MvError.MV_OK)
throw new CameraException($"Set PixelFormat failed: 0x{ret:X8}");
_logger.Information("Hikvision pixel format set to {Format}.", format);
}
}
/// <inheritdoc />
public void Dispose()
{
Close();
GC.SuppressFinalize(this);
}
// ══════════════════════════════════════════════════════════════
// 私有方法
// ══════════════════════════════════════════════════════════════
/// <summary>
/// SDK 回调:图像采集完成
/// </summary>
private void OnFrameGrabbed(object? sender, FrameGrabbedEventArgs e)
{
try
{
var frameOut = e.FrameOut;
if (frameOut == null || frameOut.Image == null)
{
_logger.Warning("Hikvision OnFrameGrabbed: FrameOut or Image is null");
GrabError?.Invoke(this, new GrabErrorEventArgs(-1, "FrameOut or Image is null."));
return;
}
var image = frameOut.Image;
int width = (int)image.Width;
int height = (int)image.Height;
int imageSize = (int)image.ImageSize;
string pixelFormat = image.PixelType.ToString();
// 提取像素数据
byte[] pixelData = image.PixelData ?? Array.Empty<byte>();
_logger.Debug("Hikvision frame: {Width}x{Height}, format={Format}, dataLen={Len}",
width, height, pixelFormat, pixelData.Length);
if (pixelData.Length == 0)
{
_logger.Warning("Hikvision OnFrameGrabbed: PixelData is empty");
return;
}
var args = new ImageGrabbedEventArgs(pixelData, width, height, pixelFormat);
ImageGrabbed?.Invoke(this, args);
}
catch (Exception ex)
{
_logger.Error(ex, "Exception in Hikvision OnFrameGrabbed handler.");
}
}
private void StopGrabbingInternal()
{
if (!_isGrabbing) return;
try
{
_device?.StreamGrabber.StopGrabbing();
if (_device != null)
_device.StreamGrabber.FrameGrabedEvent -= OnFrameGrabbed;
_isGrabbing = false;
_logger.Information("Hikvision grabbing stopped.");
}
catch (Exception ex) when (ex is not CameraException)
{
_isGrabbing = false;
_logger.Error(ex, "Error while stopping Hikvision grabbing.");
throw new CameraException("Failed to stop grabbing.", ex);
}
}
private void EnsureConnected()
{
if (!_isConnected)
throw new InvalidOperationException("Hikvision camera is not connected. Call Open() first.");
}
/// <summary>
/// 确保 SDK 全局初始化(只调用一次)
/// </summary>
private static void EnsureSdkInitialized()
{
if (_sdkInitialized) return;
lock (_sdkInitLock)
{
if (_sdkInitialized) return;
try
{
int ret = SDKSystem.Initialize();
if (ret != MvError.MV_OK)
{
_logger.Error("Hikvision SDK Initialize failed: 0x{ErrorCode:X8}", ret);
throw new CameraException($"Hikvision SDK Initialize failed: 0x{ret:X8}");
}
_sdkInitialized = true;
_logger.Information("Hikvision SDK initialized successfully.");
}
catch (Exception ex) when (ex is not CameraException)
{
_logger.Error(ex, "Failed to initialize Hikvision SDK.");
throw new CameraException("Failed to initialize Hikvision SDK.", ex);
}
}
}
}