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:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Language": "zh-CN",
|
"Language": "zh-CN",
|
||||||
"LogLevel": "Debug"
|
"LogLevel": "Debug",
|
||||||
|
"CameraType": "Hikvision"
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ public static class PixelConverter
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将原始像素数据转换为 WPF 的 BitmapSource 对象。
|
/// 将原始像素数据转换为 WPF 的 BitmapSource 对象。
|
||||||
|
/// 支持 Mono8、BGR8、RGB8、BGRA8 以及 Bayer 8-bit 格式(自动解码为 BGR24)。
|
||||||
/// 返回的 BitmapSource 已调用 Freeze(),可跨线程访问。
|
/// 返回的 BitmapSource 已调用 Freeze(),可跨线程访问。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static BitmapSource ToBitmapSource(byte[] pixelData, int width, int height, string pixelFormat)
|
public static BitmapSource ToBitmapSource(byte[] pixelData, int width, int height, string pixelFormat)
|
||||||
@@ -19,11 +20,23 @@ public static class PixelConverter
|
|||||||
if (height <= 0) throw new ArgumentException("Height must be a positive integer.", nameof(height));
|
if (height <= 0) throw new ArgumentException("Height must be a positive integer.", nameof(height));
|
||||||
ArgumentNullException.ThrowIfNull(pixelFormat);
|
ArgumentNullException.ThrowIfNull(pixelFormat);
|
||||||
|
|
||||||
var (format, stride) = pixelFormat switch
|
string normalized = NormalizePixelFormat(pixelFormat);
|
||||||
|
|
||||||
|
// Bayer 格式需要解码
|
||||||
|
if (normalized.StartsWith("Bayer"))
|
||||||
|
{
|
||||||
|
byte[] bgrData = DemosaicBayer(pixelData, width, height, normalized);
|
||||||
|
var bmp = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgr24, null, bgrData, width * 3);
|
||||||
|
bmp.Freeze();
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (format, stride) = normalized switch
|
||||||
{
|
{
|
||||||
"Mono8" => (PixelFormats.Gray8, width),
|
"Mono8" => (PixelFormats.Gray8, width),
|
||||||
"BGR8" => (PixelFormats.Bgr24, width * 3),
|
"BGR8" => (PixelFormats.Bgr24, width * 3),
|
||||||
"BGRA8" => (PixelFormats.Bgra32, width * 4),
|
"BGRA8" => (PixelFormats.Bgra32, width * 4),
|
||||||
|
"RGB8" => (PixelFormats.Rgb24, width * 3),
|
||||||
_ => throw new NotSupportedException($"Pixel format '{pixelFormat}' is not supported.")
|
_ => throw new NotSupportedException($"Pixel format '{pixelFormat}' is not supported.")
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,4 +44,136 @@ public static class PixelConverter
|
|||||||
bitmap.Freeze();
|
bitmap.Freeze();
|
||||||
return bitmap;
|
return bitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将不同 SDK 的像素格式名称统一为标准名称。
|
||||||
|
/// </summary>
|
||||||
|
private static string NormalizePixelFormat(string pixelFormat)
|
||||||
|
{
|
||||||
|
if (pixelFormat is "Mono8" or "BGR8" or "BGRA8" or "RGB8")
|
||||||
|
return pixelFormat;
|
||||||
|
|
||||||
|
var upper = pixelFormat.ToUpperInvariant();
|
||||||
|
|
||||||
|
if (upper.Contains("MONO8")) return "Mono8";
|
||||||
|
if (upper.Contains("BGR8")) return "BGR8";
|
||||||
|
if (upper.Contains("BGRA8")) return "BGRA8";
|
||||||
|
if (upper.Contains("RGB8") && !upper.Contains("BAYER")) return "RGB8";
|
||||||
|
|
||||||
|
// Bayer 格式
|
||||||
|
if (upper.Contains("BAYERRG8") || upper.Contains("BAYER_RG8")) return "BayerRG8";
|
||||||
|
if (upper.Contains("BAYERGR8") || upper.Contains("BAYER_GR8")) return "BayerGR8";
|
||||||
|
if (upper.Contains("BAYERGB8") || upper.Contains("BAYER_GB8")) return "BayerGB8";
|
||||||
|
if (upper.Contains("BAYERBG8") || upper.Contains("BAYER_BG8")) return "BayerBG8";
|
||||||
|
|
||||||
|
return pixelFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 简单 Bayer 解码(双线性插值),输出 BGR24。
|
||||||
|
/// </summary>
|
||||||
|
private static byte[] DemosaicBayer(byte[] bayer, int width, int height, string pattern)
|
||||||
|
{
|
||||||
|
// pattern: BayerRG8, BayerGR8, BayerGB8, BayerBG8
|
||||||
|
// RG: R G GR: G R GB: G B BG: B G
|
||||||
|
// G B B G R G G R
|
||||||
|
|
||||||
|
int rRow, rCol; // 红色像素在2x2块中的位置
|
||||||
|
switch (pattern)
|
||||||
|
{
|
||||||
|
case "BayerRG8": rRow = 0; rCol = 0; break;
|
||||||
|
case "BayerGR8": rRow = 0; rCol = 1; break;
|
||||||
|
case "BayerGB8": rRow = 1; rCol = 0; break;
|
||||||
|
case "BayerBG8": rRow = 1; rCol = 1; break;
|
||||||
|
default: rRow = 0; rCol = 0; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bgr = new byte[width * height * 3];
|
||||||
|
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
int srcIdx = y * width + x;
|
||||||
|
int dstIdx = (y * width + x) * 3;
|
||||||
|
|
||||||
|
// 确定当前像素在 Bayer 模式中的角色
|
||||||
|
int py = (y + rRow) % 2; // 0=红行, 1=蓝行
|
||||||
|
int px = (x + rCol) % 2; // 0=红列/蓝列, 1=绿列
|
||||||
|
|
||||||
|
byte r, g, b;
|
||||||
|
|
||||||
|
if (py == 0 && px == 0)
|
||||||
|
{
|
||||||
|
// 红色像素位置
|
||||||
|
r = bayer[srcIdx];
|
||||||
|
g = AvgNeighbors4(bayer, width, height, x, y);
|
||||||
|
b = AvgDiagonal(bayer, width, height, x, y);
|
||||||
|
}
|
||||||
|
else if (py == 1 && px == 1)
|
||||||
|
{
|
||||||
|
// 蓝色像素位置
|
||||||
|
b = bayer[srcIdx];
|
||||||
|
g = AvgNeighbors4(bayer, width, height, x, y);
|
||||||
|
r = AvgDiagonal(bayer, width, height, x, y);
|
||||||
|
}
|
||||||
|
else if (py == 0 && px == 1)
|
||||||
|
{
|
||||||
|
// 绿色像素(红行)
|
||||||
|
g = bayer[srcIdx];
|
||||||
|
r = AvgHorizontal(bayer, width, x, y);
|
||||||
|
b = AvgVertical(bayer, width, height, x, y);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 绿色像素(蓝行)
|
||||||
|
g = bayer[srcIdx];
|
||||||
|
b = AvgHorizontal(bayer, width, x, y);
|
||||||
|
r = AvgVertical(bayer, width, height, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
bgr[dstIdx] = b;
|
||||||
|
bgr[dstIdx + 1] = g;
|
||||||
|
bgr[dstIdx + 2] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte AvgNeighbors4(byte[] data, int w, int h, int x, int y)
|
||||||
|
{
|
||||||
|
int sum = 0, count = 0;
|
||||||
|
if (x > 0) { sum += data[y * w + x - 1]; count++; }
|
||||||
|
if (x < w - 1) { sum += data[y * w + x + 1]; count++; }
|
||||||
|
if (y > 0) { sum += data[(y - 1) * w + x]; count++; }
|
||||||
|
if (y < h - 1) { sum += data[(y + 1) * w + x]; count++; }
|
||||||
|
return count > 0 ? (byte)(sum / count) : (byte)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte AvgDiagonal(byte[] data, int w, int h, int x, int y)
|
||||||
|
{
|
||||||
|
int sum = 0, count = 0;
|
||||||
|
if (x > 0 && y > 0) { sum += data[(y - 1) * w + x - 1]; count++; }
|
||||||
|
if (x < w - 1 && y > 0) { sum += data[(y - 1) * w + x + 1]; count++; }
|
||||||
|
if (x > 0 && y < h - 1) { sum += data[(y + 1) * w + x - 1]; count++; }
|
||||||
|
if (x < w - 1 && y < h - 1) { sum += data[(y + 1) * w + x + 1]; count++; }
|
||||||
|
return count > 0 ? (byte)(sum / count) : (byte)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte AvgHorizontal(byte[] data, int w, int x, int y)
|
||||||
|
{
|
||||||
|
int sum = 0, count = 0;
|
||||||
|
if (x > 0) { sum += data[y * w + x - 1]; count++; }
|
||||||
|
if (x < w - 1) { sum += data[y * w + x + 1]; count++; }
|
||||||
|
return count > 0 ? (byte)(sum / count) : (byte)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte AvgVertical(byte[] data, int w, int h, int x, int y)
|
||||||
|
{
|
||||||
|
int sum = 0, count = 0;
|
||||||
|
if (y > 0) { sum += data[(y - 1) * w + x]; count++; }
|
||||||
|
if (y < h - 1) { sum += data[(y + 1) * w + x]; count++; }
|
||||||
|
return count > 0 ? (byte)(sum / count) : (byte)0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,8 @@ public class CameraFactory : ICameraFactory
|
|||||||
return cameraType switch
|
return cameraType switch
|
||||||
{
|
{
|
||||||
"Basler" => new BaslerCameraController(),
|
"Basler" => new BaslerCameraController(),
|
||||||
// "Hikvision" => new HikvisionCameraController(),
|
"Hikvision" => new HikvisionCameraController(),
|
||||||
_ => throw new NotSupportedException($"不支持的相机品牌: {cameraType}")
|
_ => throw new NotSupportedException($"Unsupported Camera Type: {cameraType}")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,12 +7,19 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>XP.Camera</RootNamespace>
|
<RootNamespace>XP.Camera</RootNamespace>
|
||||||
<AssemblyName>XP.Camera</AssemblyName>
|
<AssemblyName>XP.Camera</AssemblyName>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Basler.Pylon">
|
<Reference Include="Basler.Pylon">
|
||||||
<HintPath>..\ExternalLibraries\Basler.Pylon.dll</HintPath>
|
<HintPath>..\ExternalLibraries\Basler.Pylon.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="MvCameraControl.Net">
|
||||||
|
<HintPath>..\ExternalLibraries\MvCameraControl.Net.dll</HintPath>
|
||||||
|
<Private>true</Private>
|
||||||
|
<CopyLocal>true</CopyLocal>
|
||||||
|
</Reference>
|
||||||
<PackageReference Include="Emgu.CV" Version="4.10.0.5680" />
|
<PackageReference Include="Emgu.CV" Version="4.10.0.5680" />
|
||||||
<PackageReference Include="Emgu.CV.Bitmap" Version="4.10.0.5680" />
|
<PackageReference Include="Emgu.CV.Bitmap" Version="4.10.0.5680" />
|
||||||
<PackageReference Include="Prism.DryIoc" Version="9.0.537" />
|
<PackageReference Include="Prism.DryIoc" Version="9.0.537" />
|
||||||
|
|||||||
+34
-15
@@ -321,7 +321,7 @@ namespace XplorePlane
|
|||||||
shell.Loaded += (s, e) =>
|
shell.Loaded += (s, e) =>
|
||||||
{
|
{
|
||||||
// [DEV] 导航相机连接已屏蔽,开发阶段跳过以加快启动速度
|
// [DEV] 导航相机连接已屏蔽,开发阶段跳过以加快启动速度
|
||||||
// TryConnectCamera();
|
TryConnectCamera();
|
||||||
|
|
||||||
// 初始化主界面探测器帧流水线,开始接收探测器图像事件
|
// 初始化主界面探测器帧流水线,开始接收探测器图像事件
|
||||||
try
|
try
|
||||||
@@ -334,20 +334,20 @@ namespace XplorePlane
|
|||||||
}
|
}
|
||||||
|
|
||||||
// [DEV] 相机状态通知已屏蔽
|
// [DEV] 相机状态通知已屏蔽
|
||||||
// try
|
try
|
||||||
// {
|
{
|
||||||
// var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
|
var cameraVm = Container.Resolve<NavigationPropertyPanelViewModel>();
|
||||||
// cameraVm.OnCameraReady();
|
cameraVm.OnCameraReady();
|
||||||
// }
|
}
|
||||||
// catch (Exception ex)
|
catch (Exception ex)
|
||||||
// {
|
{
|
||||||
// Log.Error(ex, "通知相机 ViewModel 失败");
|
Log.Error(ex, "通知相机 ViewModel 失败");
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if (_cameraError != null)
|
if (_cameraError != null)
|
||||||
// {
|
{
|
||||||
// HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error);
|
HexMessageBox.Show(_cameraError, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
// }
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return shell;
|
return shell;
|
||||||
@@ -462,7 +462,26 @@ namespace XplorePlane
|
|||||||
// ── 导航相机服务(单例)──
|
// ── 导航相机服务(单例)──
|
||||||
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
||||||
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
||||||
new CameraFactory().CreateController("Basler"));
|
{
|
||||||
|
string cameraType = "Hikvision"; // 默认值
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var configPath = Path.Combine(AppContext.BaseDirectory, "config.json");
|
||||||
|
if (File.Exists(configPath))
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(configPath);
|
||||||
|
var match = System.Text.RegularExpressions.Regex.Match(json, "\"CameraType\"\\s*:\\s*\"([^\"]+)\"");
|
||||||
|
if (match.Success)
|
||||||
|
cameraType = match.Groups[1].Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "读取 CameraType 配置失败,使用默认值 Hikvision");
|
||||||
|
}
|
||||||
|
Log.Information("相机类型: {CameraType}", cameraType);
|
||||||
|
return new CameraFactory().CreateController(cameraType);
|
||||||
|
});
|
||||||
containerRegistry.RegisterSingleton<ICameraService, CameraService>();
|
containerRegistry.RegisterSingleton<ICameraService, CameraService>();
|
||||||
|
|
||||||
Log.Information("依赖注入容器配置完成");
|
Log.Information("依赖注入容器配置完成");
|
||||||
|
|||||||
@@ -55,9 +55,6 @@ namespace XplorePlane.ViewModels
|
|||||||
StopGrabCommand.RaiseCanExecuteChanged();
|
StopGrabCommand.RaiseCanExecuteChanged();
|
||||||
ApplyExposureCommand.RaiseCanExecuteChanged();
|
ApplyExposureCommand.RaiseCanExecuteChanged();
|
||||||
ApplyGainCommand.RaiseCanExecuteChanged();
|
ApplyGainCommand.RaiseCanExecuteChanged();
|
||||||
ApplyWidthCommand.RaiseCanExecuteChanged();
|
|
||||||
ApplyHeightCommand.RaiseCanExecuteChanged();
|
|
||||||
ApplyPixelFormatCommand.RaiseCanExecuteChanged();
|
|
||||||
RefreshCameraParamsCommand.RaiseCanExecuteChanged();
|
RefreshCameraParamsCommand.RaiseCanExecuteChanged();
|
||||||
OpenCameraSettingsCommand.RaiseCanExecuteChanged();
|
OpenCameraSettingsCommand.RaiseCanExecuteChanged();
|
||||||
}
|
}
|
||||||
@@ -79,7 +76,7 @@ namespace XplorePlane.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _cameraStatusText = "未连接";
|
private string _cameraStatusText = "Disconnected";
|
||||||
|
|
||||||
public string CameraStatusText
|
public string CameraStatusText
|
||||||
{
|
{
|
||||||
@@ -152,8 +149,6 @@ namespace XplorePlane.ViewModels
|
|||||||
set => SetProperty(ref _selectedPixelFormat, value);
|
set => SetProperty(ref _selectedPixelFormat, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<string> PixelFormatOptions { get; } = new() { "Mono8", "BGR8", "BGRA8" };
|
|
||||||
|
|
||||||
#endregion Properties
|
#endregion Properties
|
||||||
|
|
||||||
#region Commands
|
#region Commands
|
||||||
@@ -164,9 +159,6 @@ namespace XplorePlane.ViewModels
|
|||||||
public DelegateCommand StopGrabCommand { get; }
|
public DelegateCommand StopGrabCommand { get; }
|
||||||
public DelegateCommand ApplyExposureCommand { get; }
|
public DelegateCommand ApplyExposureCommand { get; }
|
||||||
public DelegateCommand ApplyGainCommand { get; }
|
public DelegateCommand ApplyGainCommand { get; }
|
||||||
public DelegateCommand ApplyWidthCommand { get; }
|
|
||||||
public DelegateCommand ApplyHeightCommand { get; }
|
|
||||||
public DelegateCommand ApplyPixelFormatCommand { get; }
|
|
||||||
public DelegateCommand RefreshCameraParamsCommand { get; }
|
public DelegateCommand RefreshCameraParamsCommand { get; }
|
||||||
public DelegateCommand OpenCameraSettingsCommand { get; }
|
public DelegateCommand OpenCameraSettingsCommand { get; }
|
||||||
|
|
||||||
@@ -183,9 +175,6 @@ namespace XplorePlane.ViewModels
|
|||||||
StopGrabCommand = new DelegateCommand(StopGrab, () => IsCameraGrabbing);
|
StopGrabCommand = new DelegateCommand(StopGrab, () => IsCameraGrabbing);
|
||||||
ApplyExposureCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetExposureTime(ExposureTime)), () => IsCameraConnected);
|
ApplyExposureCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetExposureTime(ExposureTime)), () => IsCameraConnected);
|
||||||
ApplyGainCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetGain(GainValue)), () => IsCameraConnected);
|
ApplyGainCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetGain(GainValue)), () => IsCameraConnected);
|
||||||
ApplyWidthCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetWidth(ImageWidth)), () => IsCameraConnected);
|
|
||||||
ApplyHeightCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetHeight(ImageHeight)), () => IsCameraConnected);
|
|
||||||
ApplyPixelFormatCommand = new DelegateCommand(() => ApplyCameraParam(() => _camera.SetPixelFormat(SelectedPixelFormat)), () => IsCameraConnected);
|
|
||||||
RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => IsCameraConnected);
|
RefreshCameraParamsCommand = new DelegateCommand(RefreshCameraParams, () => IsCameraConnected);
|
||||||
OpenCameraSettingsCommand = new DelegateCommand(OpenCameraSettings, () => IsCameraConnected);
|
OpenCameraSettingsCommand = new DelegateCommand(OpenCameraSettings, () => IsCameraConnected);
|
||||||
|
|
||||||
@@ -193,7 +182,7 @@ namespace XplorePlane.ViewModels
|
|||||||
_defaultImageSource = new BitmapImage(new Uri("pack://application:,,,/Assets/Icons/NoCamera.png"));
|
_defaultImageSource = new BitmapImage(new Uri("pack://application:,,,/Assets/Icons/NoCamera.png"));
|
||||||
_cameraImageSource = _defaultImageSource;
|
_cameraImageSource = _defaultImageSource;
|
||||||
|
|
||||||
CameraStatusText = "正在检索相机...";
|
CameraStatusText = "Searching camera...";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -203,7 +192,7 @@ namespace XplorePlane.ViewModels
|
|||||||
{
|
{
|
||||||
if (!_camera.IsConnected)
|
if (!_camera.IsConnected)
|
||||||
{
|
{
|
||||||
CameraStatusText = "未检测到相机";
|
CameraStatusText = "No camera detected";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +201,7 @@ namespace XplorePlane.ViewModels
|
|||||||
_camera.ConnectionLost += OnCameraConnectionLost;
|
_camera.ConnectionLost += OnCameraConnectionLost;
|
||||||
|
|
||||||
IsCameraConnected = true;
|
IsCameraConnected = true;
|
||||||
CameraStatusText = "已连接";
|
CameraStatusText = "Connected";
|
||||||
RefreshCameraParams();
|
RefreshCameraParams();
|
||||||
SyncCameraStateToAppState();
|
SyncCameraStateToAppState();
|
||||||
StartGrab();
|
StartGrab();
|
||||||
@@ -231,7 +220,7 @@ namespace XplorePlane.ViewModels
|
|||||||
|
|
||||||
var info = _camera.Open();
|
var info = _camera.Open();
|
||||||
IsCameraConnected = true;
|
IsCameraConnected = true;
|
||||||
CameraStatusText = $"已连接: {info.ModelName} (SN: {info.SerialNumber})";
|
CameraStatusText = $"Connected: {info.ModelName} (SN: {info.SerialNumber})";
|
||||||
_logger.Information("Camera connected: {ModelName}", info.ModelName);
|
_logger.Information("Camera connected: {ModelName}", info.ModelName);
|
||||||
RefreshCameraParams();
|
RefreshCameraParams();
|
||||||
SyncCameraStateToAppState();
|
SyncCameraStateToAppState();
|
||||||
@@ -239,7 +228,7 @@ namespace XplorePlane.ViewModels
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to connect camera");
|
_logger.Error(ex, "Failed to connect camera");
|
||||||
CameraStatusText = $"连接失败: {ex.Message}";
|
CameraStatusText = $"Connection failed: {ex.Message}";
|
||||||
IsCameraConnected = false;
|
IsCameraConnected = false;
|
||||||
SyncCameraStateToAppState();
|
SyncCameraStateToAppState();
|
||||||
}
|
}
|
||||||
@@ -263,7 +252,7 @@ namespace XplorePlane.ViewModels
|
|||||||
_camera.ConnectionLost -= OnCameraConnectionLost;
|
_camera.ConnectionLost -= OnCameraConnectionLost;
|
||||||
IsCameraConnected = false;
|
IsCameraConnected = false;
|
||||||
IsCameraGrabbing = false;
|
IsCameraGrabbing = false;
|
||||||
CameraStatusText = "未连接";
|
CameraStatusText = "Disconnected";
|
||||||
CameraImageSource = null;
|
CameraImageSource = null;
|
||||||
SyncCameraStateToAppState();
|
SyncCameraStateToAppState();
|
||||||
_logger.Information("Camera disconnected");
|
_logger.Information("Camera disconnected");
|
||||||
@@ -276,7 +265,7 @@ namespace XplorePlane.ViewModels
|
|||||||
{
|
{
|
||||||
_camera.StartGrabbing();
|
_camera.StartGrabbing();
|
||||||
IsCameraGrabbing = true;
|
IsCameraGrabbing = true;
|
||||||
CameraStatusText = "采集中...";
|
CameraStatusText = "Grabbing...";
|
||||||
SyncCameraStateToAppState();
|
SyncCameraStateToAppState();
|
||||||
|
|
||||||
// 如果已勾选实时,自动启动 Live View
|
// 如果已勾选实时,自动启动 Live View
|
||||||
@@ -288,7 +277,7 @@ namespace XplorePlane.ViewModels
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to start grabbing");
|
_logger.Error(ex, "Failed to start grabbing");
|
||||||
CameraStatusText = $"采集失败: {ex.Message}";
|
CameraStatusText = $"Grab failed: {ex.Message}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +288,7 @@ namespace XplorePlane.ViewModels
|
|||||||
IsLiveViewEnabled = false;
|
IsLiveViewEnabled = false;
|
||||||
_camera.StopGrabbing();
|
_camera.StopGrabbing();
|
||||||
IsCameraGrabbing = false;
|
IsCameraGrabbing = false;
|
||||||
CameraStatusText = "已停止采集";
|
CameraStatusText = "Grab stopped";
|
||||||
SyncCameraStateToAppState();
|
SyncCameraStateToAppState();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -313,7 +302,7 @@ namespace XplorePlane.ViewModels
|
|||||||
if (!IsCameraGrabbing) return;
|
if (!IsCameraGrabbing) return;
|
||||||
|
|
||||||
_liveViewRunning = true;
|
_liveViewRunning = true;
|
||||||
CameraStatusText = "实时采集中...";
|
CameraStatusText = "Live...";
|
||||||
|
|
||||||
try { _camera.ExecuteSoftwareTrigger(); }
|
try { _camera.ExecuteSoftwareTrigger(); }
|
||||||
catch (Exception ex) { _logger.Error(ex, "Live view trigger failed"); }
|
catch (Exception ex) { _logger.Error(ex, "Live view trigger failed"); }
|
||||||
@@ -323,7 +312,7 @@ namespace XplorePlane.ViewModels
|
|||||||
{
|
{
|
||||||
_liveViewRunning = false;
|
_liveViewRunning = false;
|
||||||
if (IsCameraGrabbing)
|
if (IsCameraGrabbing)
|
||||||
CameraStatusText = "采集中...";
|
CameraStatusText = "Grabbing...";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshCameraParams()
|
private void RefreshCameraParams()
|
||||||
@@ -334,13 +323,16 @@ namespace XplorePlane.ViewModels
|
|||||||
GainValue = _camera.GetGain();
|
GainValue = _camera.GetGain();
|
||||||
ImageWidth = _camera.GetWidth();
|
ImageWidth = _camera.GetWidth();
|
||||||
ImageHeight = _camera.GetHeight();
|
ImageHeight = _camera.GetHeight();
|
||||||
SelectedPixelFormat = _camera.GetPixelFormat();
|
|
||||||
|
var currentFormat = _camera.GetPixelFormat();
|
||||||
|
SelectedPixelFormat = currentFormat;
|
||||||
|
|
||||||
_logger.Information("Camera parameters refreshed");
|
_logger.Information("Camera parameters refreshed");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to read camera parameters");
|
_logger.Error(ex, "Failed to read camera parameters");
|
||||||
CameraStatusText = $"读取参数失败: {ex.Message}";
|
CameraStatusText = $"Read params failed: {ex.Message}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +346,7 @@ namespace XplorePlane.ViewModels
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "Failed to apply camera parameter");
|
_logger.Error(ex, "Failed to apply camera parameter");
|
||||||
CameraStatusText = $"设置参数失败: {ex.Message}";
|
CameraStatusText = $"Set param failed: {ex.Message}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,17 +377,21 @@ namespace XplorePlane.ViewModels
|
|||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
CameraImageSource = bitmap;
|
CameraImageSource = bitmap;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_liveViewRunning)
|
|
||||||
{
|
|
||||||
_camera.ExecuteSoftwareTrigger();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
_logger.Error(ex, "Failed to process camera image");
|
_logger.Error(ex, "Failed to process camera image");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// 无论图像处理是否成功,都继续触发下一帧,保持采集链不断
|
||||||
|
if (_liveViewRunning && !_disposed)
|
||||||
|
{
|
||||||
|
try { _camera.ExecuteSoftwareTrigger(); }
|
||||||
|
catch { /* 忽略触发失败 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCameraGrabError(object? sender, GrabErrorEventArgs e)
|
private void OnCameraGrabError(object? sender, GrabErrorEventArgs e)
|
||||||
@@ -407,7 +403,7 @@ namespace XplorePlane.ViewModels
|
|||||||
app.Dispatcher.BeginInvoke(() =>
|
app.Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
CameraStatusText = $"采集错误: {e.ErrorDescription}";
|
CameraStatusText = $"Grab error: {e.ErrorDescription}";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +418,7 @@ namespace XplorePlane.ViewModels
|
|||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
IsCameraConnected = false;
|
IsCameraConnected = false;
|
||||||
IsCameraGrabbing = false;
|
IsCameraGrabbing = false;
|
||||||
CameraStatusText = "连接已断开";
|
CameraStatusText = "Connection lost";
|
||||||
CameraImageSource = null;
|
CameraImageSource = null;
|
||||||
SyncCameraStateToAppState();
|
SyncCameraStateToAppState();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,21 +59,11 @@
|
|||||||
<Border Style="{StaticResource CardStyle}">
|
<Border Style="{StaticResource CardStyle}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="图像宽度 (px)" Style="{StaticResource ParamLabel}" />
|
<TextBlock Text="图像宽度 (px)" Style="{StaticResource ParamLabel}" />
|
||||||
<DockPanel Margin="0,0,0,8">
|
<TextBox Text="{Binding ImageWidth, Mode=OneWay}" IsReadOnly="True" Background="#F0F0F0"
|
||||||
<TextBox DockPanel.Dock="Right" Width="65"
|
Height="28" FontSize="11.5" VerticalContentAlignment="Center" Margin="0,0,0,8" />
|
||||||
Text="{Binding ImageWidth, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
|
||||||
<Slider Minimum="64" Maximum="8192" Value="{Binding ImageWidth}"
|
|
||||||
SmallChange="1" LargeChange="100" VerticalAlignment="Center" />
|
|
||||||
</DockPanel>
|
|
||||||
<TextBlock Text="图像高度 (px)" Style="{StaticResource ParamLabel}" />
|
<TextBlock Text="图像高度 (px)" Style="{StaticResource ParamLabel}" />
|
||||||
<DockPanel>
|
<TextBox Text="{Binding ImageHeight, Mode=OneWay}" IsReadOnly="True" Background="#F0F0F0"
|
||||||
<TextBox DockPanel.Dock="Right" Width="65"
|
Height="28" FontSize="11.5" VerticalContentAlignment="Center" />
|
||||||
Text="{Binding ImageHeight, UpdateSourceTrigger=PropertyChanged}"
|
|
||||||
VerticalContentAlignment="Center" Margin="6,0,0,0" />
|
|
||||||
<Slider Minimum="64" Maximum="8192" Value="{Binding ImageHeight}"
|
|
||||||
SmallChange="1" LargeChange="100" VerticalAlignment="Center" />
|
|
||||||
</DockPanel>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -81,8 +71,8 @@
|
|||||||
<Border Style="{StaticResource CardStyle}">
|
<Border Style="{StaticResource CardStyle}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="像素格式" Style="{StaticResource ParamLabel}" />
|
<TextBlock Text="像素格式" Style="{StaticResource ParamLabel}" />
|
||||||
<ComboBox SelectedItem="{Binding SelectedPixelFormat}"
|
<TextBox Text="{Binding SelectedPixelFormat, Mode=OneWay}"
|
||||||
ItemsSource="{Binding PixelFormatOptions}"
|
IsReadOnly="True" Background="#F0F0F0"
|
||||||
Height="28" FontSize="11.5" VerticalContentAlignment="Center" />
|
Height="28" FontSize="11.5" VerticalContentAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ namespace XplorePlane.Views
|
|||||||
var type = dc.GetType();
|
var type = dc.GetType();
|
||||||
ExecuteCommand(type, dc, "ApplyExposureCommand");
|
ExecuteCommand(type, dc, "ApplyExposureCommand");
|
||||||
ExecuteCommand(type, dc, "ApplyGainCommand");
|
ExecuteCommand(type, dc, "ApplyGainCommand");
|
||||||
ExecuteCommand(type, dc, "ApplyWidthCommand");
|
|
||||||
ExecuteCommand(type, dc, "ApplyHeightCommand");
|
|
||||||
ExecuteCommand(type, dc, "ApplyPixelFormatCommand");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ExecuteCommand(System.Type type, object dc, string cmdName)
|
private static void ExecuteCommand(System.Type type, object dc, string cmdName)
|
||||||
|
|||||||
@@ -50,6 +50,12 @@
|
|||||||
<Private>True</Private>
|
<Private>True</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
|
||||||
|
<!-- 海康威视相机 SDK (.NET Framework 4.0) -->
|
||||||
|
<Reference Include="MvCameraControl.Net">
|
||||||
|
<HintPath>..\ExternalLibraries\MvCameraControl.Net.dll</HintPath>
|
||||||
|
<Private>true</Private>
|
||||||
|
</Reference>
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Telerik UI for WPF -->
|
<!-- Telerik UI for WPF -->
|
||||||
|
|||||||
Reference in New Issue
Block a user