feat: 集成海康威视相机接口
- 新增 HikvisionCameraController 实现 ICameraController - CameraFactory 支持 Basler/Hikvision 动态切换(config.json 配置) - PixelConverter 支持 Bayer RG/GR/GB/BG 8-bit 解码 - 修复采集链断裂问题(finally 中触发下一帧) - 相机设置面板:宽高和像素格式改为只读显示 - NavigationPropertyPanelViewModel 日志和状态文本改为英文 - 添加 MvCameraControl.Net.dll 到 ExternalLibraries
This commit is contained in:
@@ -0,0 +1,535 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user