合并图像处理库,删除图像lib库
This commit is contained in:
@@ -0,0 +1,517 @@
|
||||
using Basler.Pylon;
|
||||
using Serilog;
|
||||
|
||||
using CameraImageGrabbedEventArgs = XP.Camera.ImageGrabbedEventArgs;
|
||||
using CameraGrabErrorEventArgs = XP.Camera.GrabErrorEventArgs;
|
||||
|
||||
namespace XP.Camera;
|
||||
|
||||
/// <summary>
|
||||
/// Basler 相机控制器,封装 Basler pylon .NET SDK 实现 <see cref="ICameraController"/>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>所有公共方法通过内部 <c>_syncLock</c> 对象进行 lock 同步,保证线程安全。</para>
|
||||
/// <para>事件回调(ImageGrabbed、GrabError)在 StreamGrabber 回调线程上触发,不持有 _syncLock,避免死锁。</para>
|
||||
/// <para>ConnectionLost 事件在 pylon SDK 事件线程上触发。</para>
|
||||
/// </remarks>
|
||||
public class BaslerCameraController : ICameraController
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<BaslerCameraController>();
|
||||
private readonly object _syncLock = new();
|
||||
|
||||
private Basler.Pylon.Camera? _camera;
|
||||
private CameraInfo? _cachedCameraInfo;
|
||||
private bool _isConnected;
|
||||
private bool _isGrabbing;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 <see cref="BaslerCameraController"/> 实例。
|
||||
/// </summary>
|
||||
public BaslerCameraController()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected
|
||||
{
|
||||
get { lock (_syncLock) { return _isConnected; } }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsGrabbing
|
||||
{
|
||||
get { lock (_syncLock) { return _isGrabbing; } }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<CameraImageGrabbedEventArgs>? ImageGrabbed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<CameraGrabErrorEventArgs>? GrabError;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? ConnectionLost;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CameraInfo Open()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_isConnected && _cachedCameraInfo != null)
|
||||
{
|
||||
_logger.Information("Camera already connected, returning cached info.");
|
||||
return _cachedCameraInfo;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Information("Opening camera connection...");
|
||||
_camera = new Basler.Pylon.Camera(CameraSelectionStrategy.FirstFound);
|
||||
_camera.CameraOpened += (sender, e) => Configuration.SoftwareTrigger(sender!, e);
|
||||
_camera.ConnectionLost += OnConnectionLost;
|
||||
_camera.Open();
|
||||
|
||||
_cachedCameraInfo = new CameraInfo(
|
||||
ModelName: _camera.CameraInfo![CameraInfoKey.ModelName] ?? "",
|
||||
SerialNumber: _camera.CameraInfo[CameraInfoKey.SerialNumber] ?? "",
|
||||
VendorName: _camera.CameraInfo[CameraInfoKey.VendorName] ?? "",
|
||||
DeviceType: _camera.CameraInfo[CameraInfoKey.DeviceType] ?? ""
|
||||
);
|
||||
|
||||
_isConnected = true;
|
||||
_logger.Information("Camera connected: {ModelName} (SN: {SerialNumber})",
|
||||
_cachedCameraInfo.ModelName, _cachedCameraInfo.SerialNumber);
|
||||
|
||||
return _cachedCameraInfo;
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
// Clean up partially created camera on failure
|
||||
_camera?.Dispose();
|
||||
_camera = null;
|
||||
|
||||
if (ex.Message.Contains("No device", StringComparison.OrdinalIgnoreCase)
|
||||
|| ex.Message.Contains("not found", StringComparison.OrdinalIgnoreCase)
|
||||
|| ex is InvalidOperationException)
|
||||
{
|
||||
_logger.Error(ex, "No camera device found.");
|
||||
throw new DeviceNotFoundException("No Basler camera device found.", ex);
|
||||
}
|
||||
|
||||
_logger.Error(ex, "Failed to open camera.");
|
||||
throw new CameraException("Failed to open camera device.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Close()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_isConnected)
|
||||
{
|
||||
_logger.Information("Camera not connected, Close() ignored.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_isGrabbing)
|
||||
{
|
||||
StopGrabbingInternal();
|
||||
}
|
||||
|
||||
_logger.Information("Closing camera connection...");
|
||||
_camera?.Close();
|
||||
_camera?.Dispose();
|
||||
_camera = null;
|
||||
_isConnected = false;
|
||||
_cachedCameraInfo = null;
|
||||
_logger.Information("Camera connection closed.");
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Error while closing camera.");
|
||||
// Still clean up state even if close fails
|
||||
_camera = null;
|
||||
_isConnected = false;
|
||||
_isGrabbing = false;
|
||||
_cachedCameraInfo = null;
|
||||
throw new CameraException("Failed to close camera device.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StartGrabbing()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
|
||||
if (_isGrabbing)
|
||||
{
|
||||
_logger.Information("Already grabbing, StartGrabbing() ignored.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Information("Starting grabbing with software trigger...");
|
||||
|
||||
// Register ImageReady event for grab results (task 6.2 will implement the handler)
|
||||
_camera!.StreamGrabber!.ImageGrabbed += OnImageGrabbed;
|
||||
|
||||
// Start grabbing using the grab loop thread provided by StreamGrabber
|
||||
_camera.StreamGrabber!.Start(GrabStrategy.OneByOne, GrabLoop.ProvidedByStreamGrabber);
|
||||
|
||||
_isGrabbing = true;
|
||||
_logger.Information("Grabbing started.");
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to start 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
|
||||
{
|
||||
// Wait until the camera is ready to accept the next frame trigger
|
||||
if (!_camera!.WaitForFrameTriggerReady(1000, TimeoutHandling.Return))
|
||||
{
|
||||
throw new TimeoutException("Camera was not ready for frame trigger within 1000 ms.");
|
||||
}
|
||||
|
||||
_camera.ExecuteSoftwareTrigger();
|
||||
_logger.Debug("Software trigger executed.");
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
throw; // Re-throw our own TimeoutException
|
||||
}
|
||||
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();
|
||||
try
|
||||
{
|
||||
return _camera!.Parameters[PLCamera.ExposureTime].GetValue();
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to get exposure time.");
|
||||
throw new CameraException("Failed to get exposure time.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetExposureTime(double microseconds)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
try
|
||||
{
|
||||
_camera!.Parameters[PLCamera.ExposureTime].SetValue(microseconds);
|
||||
_logger.Information("Exposure time set to {Microseconds} µs.", microseconds);
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to set exposure time to {Microseconds} µs.", microseconds);
|
||||
throw new CameraException("Failed to set exposure time.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double GetGain()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
try
|
||||
{
|
||||
return _camera!.Parameters[PLCamera.Gain].GetValue();
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to get gain.");
|
||||
throw new CameraException("Failed to get gain.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetGain(double value)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
try
|
||||
{
|
||||
_camera!.Parameters[PLCamera.Gain].SetValue(value);
|
||||
_logger.Information("Gain set to {Value}.", value);
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to set gain to {Value}.", value);
|
||||
throw new CameraException("Failed to set gain.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetWidth()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
try
|
||||
{
|
||||
return (int)_camera!.Parameters[PLCamera.Width].GetValue();
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to get width.");
|
||||
throw new CameraException("Failed to get width.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetWidth(int value)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
try
|
||||
{
|
||||
_camera!.Parameters[PLCamera.Width].SetValue(value, IntegerValueCorrection.Nearest);
|
||||
_logger.Information("Width set to {Value}.", value);
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to set width to {Value}.", value);
|
||||
throw new CameraException("Failed to set width.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetHeight()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
try
|
||||
{
|
||||
return (int)_camera!.Parameters[PLCamera.Height].GetValue();
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to get height.");
|
||||
throw new CameraException("Failed to get height.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetHeight(int value)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
try
|
||||
{
|
||||
_camera!.Parameters[PLCamera.Height].SetValue(value, IntegerValueCorrection.Nearest);
|
||||
_logger.Information("Height set to {Value}.", value);
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to set height to {Value}.", value);
|
||||
throw new CameraException("Failed to set height.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetPixelFormat()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
try
|
||||
{
|
||||
return _camera!.Parameters[PLCamera.PixelFormat].GetValue();
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to get pixel format.");
|
||||
throw new CameraException("Failed to get pixel format.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetPixelFormat(string format)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
try
|
||||
{
|
||||
_camera!.Parameters[PLCamera.PixelFormat].SetValue(format);
|
||||
_logger.Information("Pixel format set to {Format}.", format);
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to set pixel format to {Format}.", format);
|
||||
throw new CameraException("Failed to set pixel format.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// StreamGrabber.ImageGrabbed 事件处理。在 StreamGrabber 回调线程上调用,不持有 _syncLock。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>当图像采集成功时,提取像素数据、宽高和像素格式,触发 <see cref="ImageGrabbed"/> 事件。</para>
|
||||
/// <para>当图像采集失败时,提取错误码和错误描述,触发 <see cref="GrabError"/> 事件。</para>
|
||||
/// <para>此方法在 StreamGrabber 回调线程上执行,不持有 _syncLock,以避免死锁。
|
||||
/// 调用方如需在 WPF UI 线程上处理事件,应自行通过 Dispatcher 调度。</para>
|
||||
/// </remarks>
|
||||
private void OnImageGrabbed(object? sender, Basler.Pylon.ImageGrabbedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
IGrabResult grabResult = e.GrabResult;
|
||||
|
||||
if (grabResult.GrabSucceeded)
|
||||
{
|
||||
byte[] pixelData = grabResult.PixelData as byte[] ?? Array.Empty<byte>();
|
||||
int width = grabResult.Width;
|
||||
int height = grabResult.Height;
|
||||
string pixelFormat = grabResult.PixelTypeValue.ToString();
|
||||
|
||||
var args = new CameraImageGrabbedEventArgs(pixelData, width, height, pixelFormat);
|
||||
ImageGrabbed?.Invoke(this, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
int errorCode = (int)grabResult.ErrorCode;
|
||||
string errorDescription = grabResult.ErrorDescription ?? "Unknown grab error.";
|
||||
|
||||
_logger.Error("Image grab failed. ErrorCode: {ErrorCode}, Description: {ErrorDescription}",
|
||||
errorCode, errorDescription);
|
||||
|
||||
var args = new CameraGrabErrorEventArgs(errorCode, errorDescription);
|
||||
GrabError?.Invoke(this, args);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Exception in OnImageGrabbed handler.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// ConnectionLost 事件处理。在 pylon SDK 事件线程上调用。
|
||||
/// </summary>
|
||||
private void OnConnectionLost(object? sender, EventArgs e)
|
||||
{
|
||||
_logger.Warning("Camera connection lost.");
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
_isGrabbing = false;
|
||||
_isConnected = false;
|
||||
_cachedCameraInfo = null;
|
||||
}
|
||||
|
||||
// Raise event outside lock to avoid deadlock
|
||||
ConnectionLost?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal stop grabbing without lock (caller must hold _syncLock).
|
||||
/// </summary>
|
||||
private void StopGrabbingInternal()
|
||||
{
|
||||
if (!_isGrabbing)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_camera?.StreamGrabber?.Stop();
|
||||
if (_camera != null)
|
||||
_camera.StreamGrabber!.ImageGrabbed -= OnImageGrabbed;
|
||||
_isGrabbing = false;
|
||||
_logger.Information("Grabbing stopped.");
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_isGrabbing = false;
|
||||
_logger.Error(ex, "Error while stopping grabbing.");
|
||||
throw new CameraException("Failed to stop grabbing.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws <see cref="InvalidOperationException"/> if the camera is not connected.
|
||||
/// Must be called within a lock on <see cref="_syncLock"/>.
|
||||
/// </summary>
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (!_isConnected)
|
||||
{
|
||||
throw new InvalidOperationException("Camera is not connected. Call Open() first.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace XP.Camera;
|
||||
|
||||
/// <summary>所有相机相关错误的基类异常。</summary>
|
||||
public class CameraException : Exception
|
||||
{
|
||||
public CameraException() { }
|
||||
public CameraException(string message) : base(message) { }
|
||||
public CameraException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
/// <summary>当相机连接意外断开时抛出的异常。</summary>
|
||||
public class ConnectionLostException : CameraException
|
||||
{
|
||||
public ConnectionLostException() { }
|
||||
public ConnectionLostException(string message) : base(message) { }
|
||||
public ConnectionLostException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
|
||||
/// <summary>当系统中无可用相机设备时抛出的异常。</summary>
|
||||
public class DeviceNotFoundException : CameraException
|
||||
{
|
||||
public DeviceNotFoundException() { }
|
||||
public DeviceNotFoundException(string message) : base(message) { }
|
||||
public DeviceNotFoundException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace XP.Camera;
|
||||
|
||||
/// <summary>
|
||||
/// 统一相机工厂,根据品牌名称创建对应的相机控制器。
|
||||
/// </summary>
|
||||
public class CameraFactory : ICameraFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ICameraController CreateController(string cameraType)
|
||||
{
|
||||
return cameraType switch
|
||||
{
|
||||
"Basler" => new BaslerCameraController(),
|
||||
// "Hikvision" => new HikvisionCameraController(),
|
||||
_ => throw new NotSupportedException($"不支持的相机品牌: {cameraType}")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace XP.Camera;
|
||||
|
||||
/// <summary>相机设备信息。</summary>
|
||||
public record CameraInfo(
|
||||
string ModelName,
|
||||
string SerialNumber,
|
||||
string VendorName,
|
||||
string DeviceType
|
||||
);
|
||||
|
||||
/// <summary>图像采集成功事件参数。</summary>
|
||||
public class ImageGrabbedEventArgs : EventArgs
|
||||
{
|
||||
public byte[] PixelData { get; }
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
public string PixelFormat { get; }
|
||||
|
||||
public ImageGrabbedEventArgs(byte[] pixelData, int width, int height, string pixelFormat)
|
||||
{
|
||||
PixelData = pixelData;
|
||||
Width = width;
|
||||
Height = height;
|
||||
PixelFormat = pixelFormat;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>图像采集失败事件参数。</summary>
|
||||
public class GrabErrorEventArgs : EventArgs
|
||||
{
|
||||
public int ErrorCode { get; }
|
||||
public string ErrorDescription { get; }
|
||||
|
||||
public GrabErrorEventArgs(int errorCode, string errorDescription)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
ErrorDescription = errorDescription;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace XP.Camera;
|
||||
|
||||
/// <summary>
|
||||
/// 相机控制器接口,定义与品牌无关的相机操作契约。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>所有公共方法(Open/Close/StartGrabbing/StopGrabbing/ExecuteSoftwareTrigger/参数读写)保证线程安全。</para>
|
||||
/// <para>事件回调在非 UI 线程上触发,调用方如需更新 WPF 界面,应通过 Dispatcher 调度。</para>
|
||||
/// </remarks>
|
||||
public interface ICameraController : IDisposable
|
||||
{
|
||||
bool IsConnected { get; }
|
||||
bool IsGrabbing { get; }
|
||||
|
||||
/// <summary>打开相机连接并返回设备信息。</summary>
|
||||
CameraInfo Open();
|
||||
|
||||
/// <summary>关闭相机连接并释放资源。</summary>
|
||||
void Close();
|
||||
|
||||
/// <summary>以软件触发模式启动图像采集。</summary>
|
||||
void StartGrabbing();
|
||||
|
||||
/// <summary>发送一次软件触发信号以采集一帧图像。</summary>
|
||||
void ExecuteSoftwareTrigger();
|
||||
|
||||
/// <summary>停止图像采集。</summary>
|
||||
void StopGrabbing();
|
||||
|
||||
double GetExposureTime();
|
||||
void SetExposureTime(double microseconds);
|
||||
double GetGain();
|
||||
void SetGain(double value);
|
||||
int GetWidth();
|
||||
void SetWidth(int value);
|
||||
int GetHeight();
|
||||
void SetHeight(int value);
|
||||
string GetPixelFormat();
|
||||
void SetPixelFormat(string format);
|
||||
|
||||
event EventHandler<ImageGrabbedEventArgs> ImageGrabbed;
|
||||
event EventHandler<GrabErrorEventArgs> GrabError;
|
||||
event EventHandler ConnectionLost;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 相机工厂接口,负责根据品牌创建相机控制器实例。
|
||||
/// </summary>
|
||||
public interface ICameraFactory
|
||||
{
|
||||
/// <summary>根据相机品牌创建控制器实例。</summary>
|
||||
ICameraController CreateController(string cameraType);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace XP.Camera;
|
||||
|
||||
/// <summary>
|
||||
/// 提供像素数据到 WPF BitmapSource 的转换工具方法。
|
||||
/// </summary>
|
||||
public static class PixelConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将原始像素数据转换为 WPF 的 BitmapSource 对象。
|
||||
/// 返回的 BitmapSource 已调用 Freeze(),可跨线程访问。
|
||||
/// </summary>
|
||||
public static BitmapSource ToBitmapSource(byte[] pixelData, int width, int height, string pixelFormat)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(pixelData);
|
||||
if (width <= 0) throw new ArgumentException("Width must be a positive integer.", nameof(width));
|
||||
if (height <= 0) throw new ArgumentException("Height must be a positive integer.", nameof(height));
|
||||
ArgumentNullException.ThrowIfNull(pixelFormat);
|
||||
|
||||
var (format, stride) = pixelFormat switch
|
||||
{
|
||||
"Mono8" => (PixelFormats.Gray8, width),
|
||||
"BGR8" => (PixelFormats.Bgr24, width * 3),
|
||||
"BGRA8" => (PixelFormats.Bgra32, width * 4),
|
||||
_ => throw new NotSupportedException($"Pixel format '{pixelFormat}' is not supported.")
|
||||
};
|
||||
|
||||
var bitmap = BitmapSource.Create(width, height, 96, 96, format, null, pixelData, stride);
|
||||
bitmap.Freeze();
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
# XP.Camera 使用说明
|
||||
|
||||
基于 .NET 8 WPF 的工业相机控制类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
|
||||
|
||||
## 环境要求
|
||||
|
||||
- .NET 8 SDK
|
||||
- Windows 操作系统
|
||||
- Basler pylon 8 SDK(已安装并配置环境变量)
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
XP.Camera/
|
||||
├── ICameraController.cs # 控制器接口 + 工厂接口
|
||||
├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
|
||||
├── BaslerCameraController.cs # Basler 实现
|
||||
├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
|
||||
├── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
|
||||
├── PixelConverter.cs # 像素数据 → WPF BitmapSource 转换工具
|
||||
└── XP.Camera.csproj
|
||||
```
|
||||
|
||||
所有类型统一在 `XP.Camera` 命名空间下。
|
||||
|
||||
## 项目引用
|
||||
|
||||
```xml
|
||||
<ProjectReference Include="..\XP.Camera\XP.Camera.csproj" />
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 通过工厂创建控制器
|
||||
|
||||
```csharp
|
||||
using XP.Camera;
|
||||
|
||||
ICameraFactory factory = new CameraFactory();
|
||||
using ICameraController camera = factory.CreateController("Basler");
|
||||
|
||||
CameraInfo info = camera.Open();
|
||||
Console.WriteLine($"已连接: {info.ModelName} (SN: {info.SerialNumber})");
|
||||
```
|
||||
|
||||
### 2. 依赖注入方式(推荐)
|
||||
|
||||
在 Prism / DI 容器中注册:
|
||||
|
||||
```csharp
|
||||
// App.xaml.cs
|
||||
var config = AppConfig.Load();
|
||||
containerRegistry.RegisterSingleton<ICameraFactory, CameraFactory>();
|
||||
containerRegistry.RegisterSingleton<ICameraController>(() =>
|
||||
new CameraFactory().CreateController(config.CameraType));
|
||||
```
|
||||
|
||||
ViewModel 中注入使用:
|
||||
|
||||
```csharp
|
||||
public class MyViewModel
|
||||
{
|
||||
private readonly ICameraController _camera;
|
||||
|
||||
public MyViewModel(ICameraController camera)
|
||||
{
|
||||
_camera = camera;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
相机品牌通过配置文件 `config.json` 指定:
|
||||
|
||||
```json
|
||||
{
|
||||
"CameraType": "Basler"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 实时图像显示(WPF 绑定)
|
||||
|
||||
```csharp
|
||||
_camera.ImageGrabbed += (s, e) =>
|
||||
{
|
||||
// PixelConverter 返回已 Freeze 的 BitmapSource,可跨线程传递
|
||||
var bitmap = PixelConverter.ToBitmapSource(
|
||||
e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
CameraImageSource = bitmap;
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
XAML 绑定:
|
||||
|
||||
```xml
|
||||
<Image Source="{Binding CameraImageSource}" Stretch="Uniform" />
|
||||
```
|
||||
|
||||
### 4. 软件触发采集流程
|
||||
|
||||
```csharp
|
||||
camera.Open();
|
||||
camera.SetExposureTime(10000); // 10ms
|
||||
camera.StartGrabbing();
|
||||
|
||||
// 每次需要采集时调用(结果通过 ImageGrabbed 事件返回)
|
||||
camera.ExecuteSoftwareTrigger();
|
||||
|
||||
camera.StopGrabbing();
|
||||
camera.Close();
|
||||
```
|
||||
|
||||
### 5. 实时连续采集(链式触发)
|
||||
|
||||
收到上一帧后立即触发下一帧,自动适配任何帧率:
|
||||
|
||||
```csharp
|
||||
private volatile bool _liveViewRunning;
|
||||
|
||||
_camera.ImageGrabbed += (s, e) =>
|
||||
{
|
||||
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||
Application.Current.Dispatcher.Invoke(() => CameraImageSource = bitmap);
|
||||
|
||||
if (_liveViewRunning)
|
||||
_camera.ExecuteSoftwareTrigger(); // 链式触发下一帧
|
||||
};
|
||||
|
||||
// 启动实时
|
||||
_camera.StartGrabbing();
|
||||
_liveViewRunning = true;
|
||||
_camera.ExecuteSoftwareTrigger(); // 触发第一帧
|
||||
|
||||
// 停止实时
|
||||
_liveViewRunning = false;
|
||||
```
|
||||
|
||||
## 核心接口
|
||||
|
||||
### ICameraController
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `Open()` | 打开连接,返回 `CameraInfo` |
|
||||
| `Close()` | 关闭连接(自动停止采集) |
|
||||
| `StartGrabbing()` | 以软件触发模式启动采集 |
|
||||
| `ExecuteSoftwareTrigger()` | 触发一帧采集 |
|
||||
| `StopGrabbing()` | 停止采集 |
|
||||
|
||||
### 参数读写
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `Get/SetExposureTime(double)` | 曝光时间(微秒) |
|
||||
| `Get/SetGain(double)` | 增益值 |
|
||||
| `Get/SetWidth(int)` | 图像宽度(自动校正到有效值) |
|
||||
| `Get/SetHeight(int)` | 图像高度(自动校正到有效值) |
|
||||
| `Get/SetPixelFormat(string)` | 像素格式(Mono8 / BGR8 / BGRA8) |
|
||||
|
||||
### ICameraFactory
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `CreateController(string cameraType)` | 根据品牌名创建控制器 |
|
||||
|
||||
当前支持的 `cameraType` 值:`"Basler"`
|
||||
|
||||
## 事件
|
||||
|
||||
| 事件 | 说明 | 触发线程 |
|
||||
|------|------|----------|
|
||||
| `ImageGrabbed` | 成功采集一帧图像 | StreamGrabber 回调线程 |
|
||||
| `GrabError` | 图像采集失败 | StreamGrabber 回调线程 |
|
||||
| `ConnectionLost` | 相机连接意外断开 | pylon SDK 事件线程 |
|
||||
|
||||
> 所有事件均在非 UI 线程触发。更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
|
||||
> `PixelConverter.ToBitmapSource()` 返回的 BitmapSource 已调用 `Freeze()`,可直接跨线程传递。
|
||||
|
||||
## 异常处理
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
camera.Open();
|
||||
}
|
||||
catch (DeviceNotFoundException)
|
||||
{
|
||||
// 无可用相机设备
|
||||
}
|
||||
catch (CameraException ex)
|
||||
{
|
||||
// 其他相机错误,ex.InnerException 包含原始 SDK 异常
|
||||
}
|
||||
```
|
||||
|
||||
| 异常类型 | 场景 |
|
||||
|---------|------|
|
||||
| `DeviceNotFoundException` | 无可用相机 |
|
||||
| `ConnectionLostException` | 相机物理断开 |
|
||||
| `CameraException` | SDK 操作失败(基类) |
|
||||
| `InvalidOperationException` | 未连接时访问参数,未采集时触发 |
|
||||
| `TimeoutException` | 软件触发等待超时 |
|
||||
|
||||
## 扩展其他品牌相机
|
||||
|
||||
1. 实现 `ICameraController` 接口:
|
||||
|
||||
```csharp
|
||||
public class HikvisionCameraController : ICameraController
|
||||
{
|
||||
// 实现所有接口方法...
|
||||
}
|
||||
```
|
||||
|
||||
2. 在 `CameraFactory.cs` 中注册:
|
||||
|
||||
```csharp
|
||||
public ICameraController CreateController(string cameraType)
|
||||
{
|
||||
return cameraType switch
|
||||
{
|
||||
"Basler" => new BaslerCameraController(),
|
||||
"Hikvision" => new HikvisionCameraController(),
|
||||
_ => throw new NotSupportedException($"不支持的相机品牌: {cameraType}")
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
3. 配置文件切换品牌即可,业务代码无需修改。
|
||||
|
||||
## 线程安全
|
||||
|
||||
- 所有公共方法(Open / Close / StartGrabbing / StopGrabbing / ExecuteSoftwareTrigger / 参数读写)均线程安全
|
||||
- 事件回调不持有内部锁,不会导致死锁
|
||||
- `Open()` / `Close()` 幂等,重复调用安全
|
||||
|
||||
## 日志
|
||||
|
||||
使用 Serilog 静态 API(`Log.ForContext<T>()`),与宿主应用共享同一个日志管道。宿主应用只需在启动时配置 `Log.Logger` 即可。
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>XP.Camera</RootNamespace>
|
||||
<AssemblyName>XP.Camera</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Basler.Pylon">
|
||||
<HintPath>C:\Program Files\Basler\pylon 8\Development\Assemblies\Basler.Pylon\x64\Basler.Pylon.dll</HintPath>
|
||||
</Reference>
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user