合并图像处理库,删除图像lib库
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XP.Camera;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
|
||||
namespace XplorePlane.Services.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机管理服务实现,封装 ICameraController 并将状态同步到 IAppStateService。
|
||||
/// </summary>
|
||||
public class CameraService : ICameraService
|
||||
{
|
||||
private readonly ICameraController _controller;
|
||||
private readonly IAppStateService _appState;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
private volatile bool _liveViewRunning;
|
||||
private bool _disposed;
|
||||
private readonly Stopwatch _fpsStopwatch = new();
|
||||
private int _frameCount;
|
||||
|
||||
public bool IsConnected => _controller.IsConnected;
|
||||
public bool IsGrabbing => _controller.IsGrabbing;
|
||||
public bool IsLiveView => _liveViewRunning;
|
||||
|
||||
public event EventHandler<BitmapSource> FrameArrived;
|
||||
public event EventHandler ConnectionLost;
|
||||
|
||||
public CameraService(
|
||||
ICameraController controller,
|
||||
IAppStateService appState,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
|
||||
_appState = appState ?? throw new ArgumentNullException(nameof(appState));
|
||||
_logger = logger?.ForModule<CameraService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
_controller.ImageGrabbed += OnImageGrabbed;
|
||||
_controller.GrabError += OnGrabError;
|
||||
_controller.ConnectionLost += OnConnectionLost;
|
||||
}
|
||||
|
||||
public CameraInfo Connect()
|
||||
{
|
||||
var info = _controller.Open();
|
||||
_logger.Info("相机已连接: {ModelName} (SN: {SerialNumber})", info.ModelName, info.SerialNumber);
|
||||
|
||||
var resolution = $"{_controller.GetWidth()}x{_controller.GetHeight()}";
|
||||
_appState.UpdateCameraState(new CameraState(
|
||||
IsConnected: true,
|
||||
IsStreaming: false,
|
||||
CurrentFrame: null,
|
||||
Width: _controller.GetWidth(),
|
||||
Height: _controller.GetHeight(),
|
||||
FrameRate: 0));
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
_liveViewRunning = false;
|
||||
_controller.Close();
|
||||
_appState.UpdateCameraState(CameraState.Default);
|
||||
_logger.Info("相机已断开");
|
||||
}
|
||||
|
||||
public void StartGrabbing()
|
||||
{
|
||||
_controller.StartGrabbing();
|
||||
_fpsStopwatch.Restart();
|
||||
_frameCount = 0;
|
||||
UpdateStreamingState(true);
|
||||
}
|
||||
|
||||
public void TriggerOnce()
|
||||
{
|
||||
_controller.ExecuteSoftwareTrigger();
|
||||
}
|
||||
|
||||
public void StopGrabbing()
|
||||
{
|
||||
_liveViewRunning = false;
|
||||
_controller.StopGrabbing();
|
||||
_fpsStopwatch.Stop();
|
||||
UpdateStreamingState(false);
|
||||
}
|
||||
|
||||
public void StartLiveView()
|
||||
{
|
||||
if (!_controller.IsGrabbing)
|
||||
StartGrabbing();
|
||||
|
||||
_liveViewRunning = true;
|
||||
_controller.ExecuteSoftwareTrigger();
|
||||
_logger.Info("实时预览已启动");
|
||||
}
|
||||
|
||||
public void StopLiveView()
|
||||
{
|
||||
_liveViewRunning = false;
|
||||
_logger.Info("实时预览已停止");
|
||||
}
|
||||
|
||||
// ── 参数读写(直接委托给 controller)──
|
||||
|
||||
public double GetExposureTime() => _controller.GetExposureTime();
|
||||
public void SetExposureTime(double microseconds) => _controller.SetExposureTime(microseconds);
|
||||
public double GetGain() => _controller.GetGain();
|
||||
public void SetGain(double value) => _controller.SetGain(value);
|
||||
public int GetWidth() => _controller.GetWidth();
|
||||
public void SetWidth(int value) => _controller.SetWidth(value);
|
||||
public int GetHeight() => _controller.GetHeight();
|
||||
public void SetHeight(int value) => _controller.SetHeight(value);
|
||||
public string GetPixelFormat() => _controller.GetPixelFormat();
|
||||
public void SetPixelFormat(string format) => _controller.SetPixelFormat(format);
|
||||
|
||||
// ── 事件处理 ──
|
||||
|
||||
private void OnImageGrabbed(object sender, ImageGrabbedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||
|
||||
// 计算帧率
|
||||
_frameCount++;
|
||||
double fps = 0;
|
||||
if (_fpsStopwatch.ElapsedMilliseconds > 0)
|
||||
fps = _frameCount / (_fpsStopwatch.ElapsedMilliseconds / 1000.0);
|
||||
|
||||
// 更新全局状态
|
||||
_appState.UpdateCameraState(new CameraState(
|
||||
IsConnected: true,
|
||||
IsStreaming: true,
|
||||
CurrentFrame: bitmap,
|
||||
Width: e.Width,
|
||||
Height: e.Height,
|
||||
FrameRate: fps));
|
||||
|
||||
// 通知 UI
|
||||
var app = Application.Current;
|
||||
app?.Dispatcher.BeginInvoke(() => FrameArrived?.Invoke(this, bitmap));
|
||||
|
||||
// 链式触发下一帧
|
||||
if (_liveViewRunning)
|
||||
_controller.ExecuteSoftwareTrigger();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "处理相机图像帧时出错");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGrabError(object sender, GrabErrorEventArgs e)
|
||||
{
|
||||
_logger.Warn($"相机采集错误: ErrorCode={e.ErrorCode}, {e.ErrorDescription}");
|
||||
}
|
||||
|
||||
private void OnConnectionLost(object sender, EventArgs e)
|
||||
{
|
||||
_liveViewRunning = false;
|
||||
_appState.UpdateCameraState(CameraState.Default);
|
||||
_logger.Warn("相机连接意外断开");
|
||||
|
||||
var app = Application.Current;
|
||||
app?.Dispatcher.BeginInvoke(() => ConnectionLost?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
|
||||
private void UpdateStreamingState(bool isStreaming)
|
||||
{
|
||||
var current = _appState.CameraState;
|
||||
_appState.UpdateCameraState(current with { IsStreaming = isStreaming });
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
_liveViewRunning = false;
|
||||
_controller.ImageGrabbed -= OnImageGrabbed;
|
||||
_controller.GrabError -= OnGrabError;
|
||||
_controller.ConnectionLost -= OnConnectionLost;
|
||||
_controller.Dispose();
|
||||
|
||||
_logger.Info("CameraService 已释放");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XP.Camera;
|
||||
|
||||
namespace XplorePlane.Services.Camera
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机管理服务接口,封装 ICameraController 并桥接到 AppStateService。
|
||||
/// </summary>
|
||||
public interface ICameraService : IDisposable
|
||||
{
|
||||
bool IsConnected { get; }
|
||||
bool IsGrabbing { get; }
|
||||
bool IsLiveView { get; }
|
||||
|
||||
/// <summary>连接相机,返回设备信息。</summary>
|
||||
CameraInfo Connect();
|
||||
|
||||
/// <summary>断开相机连接。</summary>
|
||||
void Disconnect();
|
||||
|
||||
/// <summary>启动单帧采集(软件触发模式)。</summary>
|
||||
void StartGrabbing();
|
||||
|
||||
/// <summary>触发一次采集。</summary>
|
||||
void TriggerOnce();
|
||||
|
||||
/// <summary>停止采集。</summary>
|
||||
void StopGrabbing();
|
||||
|
||||
/// <summary>启动实时预览(链式触发)。</summary>
|
||||
void StartLiveView();
|
||||
|
||||
/// <summary>停止实时预览。</summary>
|
||||
void StopLiveView();
|
||||
|
||||
// ── 参数读写 ──
|
||||
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);
|
||||
|
||||
/// <summary>最新一帧图像(已 Freeze,可跨线程)。</summary>
|
||||
event EventHandler<BitmapSource> FrameArrived;
|
||||
|
||||
/// <summary>相机连接断开事件。</summary>
|
||||
event EventHandler ConnectionLost;
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,5 @@ namespace XplorePlane.Services
|
||||
IProgress<double> progress = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ushort[]> ProcessRawFrameAsync(
|
||||
ushort[] pixelData,
|
||||
int width,
|
||||
int height,
|
||||
string processorName,
|
||||
IDictionary<string, object> parameters,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,27 +58,5 @@ namespace XplorePlane.Services
|
||||
return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride);
|
||||
}
|
||||
|
||||
public static Image<Gray, ushort> ToEmguCV16(BitmapSource bitmapSource)
|
||||
{
|
||||
if (bitmapSource == null) throw new ArgumentNullException(nameof(bitmapSource));
|
||||
|
||||
var formatted = new FormatConvertedBitmap(bitmapSource, PixelFormats.Gray16, null, 0);
|
||||
int width = formatted.PixelWidth;
|
||||
int height = formatted.PixelHeight;
|
||||
int stride = width * 2; // 2 bytes per pixel for 16-bit
|
||||
byte[] rawBytes = new byte[height * stride];
|
||||
formatted.CopyPixels(rawBytes, stride, 0);
|
||||
|
||||
ushort[] pixels = new ushort[width * height];
|
||||
Buffer.BlockCopy(rawBytes, 0, pixels, 0, rawBytes.Length);
|
||||
|
||||
var image = new Image<Gray, ushort>(width, height);
|
||||
// Copy pixel data row by row
|
||||
for (int y = 0; y < height; y++)
|
||||
for (int x = 0; x < width; x++)
|
||||
image.Data[y, x, 0] = pixels[y * width + x];
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,11 @@ namespace XplorePlane.Services
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly ConcurrentDictionary<string, ImageProcessorBase> _processorRegistry;
|
||||
private readonly ConcurrentDictionary<string, ImageProcessorBase16> _processorRegistry16;
|
||||
|
||||
public ImageProcessingService(ILoggerService logger)
|
||||
{
|
||||
_logger = logger?.ForModule<ImageProcessingService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_processorRegistry = new ConcurrentDictionary<string, ImageProcessorBase>();
|
||||
_processorRegistry16 = new ConcurrentDictionary<string, ImageProcessorBase16>();
|
||||
RegisterBuiltInProcessors();
|
||||
}
|
||||
|
||||
@@ -39,20 +37,10 @@ namespace XplorePlane.Services
|
||||
_processorRegistry["ShockFilter"] = new ShockFilterProcessor();
|
||||
_processorRegistry["BandPassFilter"] = new BandPassFilterProcessor();
|
||||
|
||||
// 16-bit processors (separate registry due to different base class)
|
||||
_processorRegistry16["GaussianBlur16"] = new GaussianBlurProcessor16();
|
||||
_processorRegistry16["FlatFieldCorrection16"] = new FlatFieldCorrectionProcessor16();
|
||||
|
||||
_logger.Info("Registered {Count8} 8-bit and {Count16} 16-bit built-in image processors",
|
||||
_processorRegistry.Count, _processorRegistry16.Count);
|
||||
_logger.Info("Registered {Count} built-in image processors", _processorRegistry.Count);
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetAvailableProcessors()
|
||||
{
|
||||
var all = new List<string>(_processorRegistry.Keys);
|
||||
all.AddRange(_processorRegistry16.Keys);
|
||||
return all.AsReadOnly();
|
||||
}
|
||||
public IReadOnlyList<string> GetAvailableProcessors() => new List<string>(_processorRegistry.Keys).AsReadOnly();
|
||||
|
||||
public void RegisterProcessor(string name, ImageProcessorBase processor)
|
||||
{
|
||||
@@ -66,8 +54,6 @@ namespace XplorePlane.Services
|
||||
{
|
||||
if (_processorRegistry.TryGetValue(processorName, out var processor))
|
||||
return processor.GetParameters().AsReadOnly();
|
||||
if (_processorRegistry16.TryGetValue(processorName, out var processor16))
|
||||
return processor16.GetParameters().AsReadOnly();
|
||||
throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName));
|
||||
}
|
||||
|
||||
@@ -82,8 +68,6 @@ namespace XplorePlane.Services
|
||||
{
|
||||
if (_processorRegistry.TryGetValue(processorName, out var p))
|
||||
return string.IsNullOrWhiteSpace(p.Name) ? processorName : p.Name;
|
||||
if (_processorRegistry16.TryGetValue(processorName, out var p16))
|
||||
return string.IsNullOrWhiteSpace(p16.Name) ? processorName : p16.Name;
|
||||
return processorName;
|
||||
}
|
||||
|
||||
@@ -142,49 +126,6 @@ namespace XplorePlane.Services
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<ushort[]> ProcessRawFrameAsync(
|
||||
ushort[] pixelData,
|
||||
int width,
|
||||
int height,
|
||||
string processorName,
|
||||
IDictionary<string, object> parameters,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (pixelData == null)
|
||||
throw new ArgumentException("pixelData cannot be null", nameof(pixelData));
|
||||
if (pixelData.Length != width * height)
|
||||
throw new ArgumentException(
|
||||
$"pixelData length {pixelData.Length} does not match width*height {width * height}");
|
||||
|
||||
if (!_processorRegistry16.TryGetValue(processorName, out var processor))
|
||||
throw new ArgumentException($"Processor not registered: {processorName}", nameof(processorName));
|
||||
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var image = new Image<Gray, ushort>(width, height);
|
||||
for (int y = 0; y < height; y++)
|
||||
for (int x = 0; x < width; x++)
|
||||
image.Data[y, x, 0] = pixelData[y * width + x];
|
||||
|
||||
if (parameters != null)
|
||||
{
|
||||
foreach (var kvp in parameters)
|
||||
processor.SetParameter(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
var processed = processor.Process(image);
|
||||
|
||||
var result = new ushort[width * height];
|
||||
for (int y = 0; y < height; y++)
|
||||
for (int x = 0; x < width; x++)
|
||||
result[y * width + x] = processed.Data[y, x, 0];
|
||||
|
||||
return result;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var processor in _processorRegistry.Values)
|
||||
@@ -194,12 +135,6 @@ namespace XplorePlane.Services
|
||||
}
|
||||
_processorRegistry.Clear();
|
||||
|
||||
foreach (var processor in _processorRegistry16.Values)
|
||||
{
|
||||
if (processor is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
_processorRegistry16.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user