主界面实时图像与探测器双队列

This commit is contained in:
zhengxuan.zhang
2026-04-24 13:34:10 +08:00
parent f64a0f7b31
commit 5d2a74d64b
12 changed files with 713 additions and 289 deletions
@@ -0,0 +1,31 @@
using System;
using System.Windows.Media.Imaging;
namespace XplorePlane.Services.MainViewport
{
public sealed class DetectorFrame
{
public DetectorFrame(
long frameId,
DateTime captureTime,
int width,
int height,
ushort[] rawPixels,
BitmapSource previewImage)
{
FrameId = frameId;
CaptureTime = captureTime;
Width = width;
Height = height;
RawPixels = rawPixels;
PreviewImage = previewImage;
}
public long FrameId { get; }
public DateTime CaptureTime { get; }
public int Width { get; }
public int Height { get; }
public ushort[] RawPixels { get; }
public BitmapSource PreviewImage { get; }
}
}
@@ -0,0 +1,160 @@
using Prism.Events;
using System;
using System.Collections.Concurrent;
using System.Configuration;
using System.Threading;
using System.Threading.Tasks;
using XP.Common.Converters;
using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions;
using XP.Hardware.Detector.Abstractions.Events;
namespace XplorePlane.Services.MainViewport
{
public sealed class DetectorFramePipelineService : IDetectorFramePipelineService
{
private readonly ConcurrentQueue<DetectorFrame> _acquireQueue = new();
private readonly ConcurrentQueue<DetectorFrame> _processQueue = new();
private readonly SemaphoreSlim _processSignal = new(0);
private readonly CancellationTokenSource _shutdown = new();
private readonly IMainViewportService _mainViewportService;
private readonly ILoggerService _logger;
private readonly Task _processConsumerTask;
private int _acquireQueueCount;
private int _processQueueCount;
private long _receivedFrameCount;
private bool _disposed;
public DetectorFramePipelineService(
IEventAggregator eventAggregator,
IMainViewportService mainViewportService,
ILoggerService logger)
{
ArgumentNullException.ThrowIfNull(eventAggregator);
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
_logger = logger?.ForModule<DetectorFramePipelineService>() ?? throw new ArgumentNullException(nameof(logger));
AcquireQueueCapacity = ReadInt("DetectorPipeline:AcquireQueueCapacity", 16, 1);
ProcessQueueCapacity = ReadInt("DetectorPipeline:ProcessQueueCapacity", 8, 1);
ProcessEveryNFrames = ReadInt("DetectorPipeline:ProcessEveryNFrames", 1, 1);
eventAggregator.GetEvent<ImageCapturedEvent>()
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
_processConsumerTask = Task.Run(ProcessLoopAsync);
}
public int AcquireQueueCount => Volatile.Read(ref _acquireQueueCount);
public int ProcessQueueCount => Volatile.Read(ref _processQueueCount);
public int AcquireQueueCapacity { get; }
public int ProcessQueueCapacity { get; }
public int ProcessEveryNFrames { get; }
public event EventHandler<DetectorFrame> ProcessFrameDequeued;
private void OnImageCaptured(ImageCapturedEventArgs args)
{
if (_disposed || args?.ImageData == null || args.Width <= 0 || args.Height <= 0)
return;
try
{
var rawPixels = new ushort[args.ImageData.Length];
Array.Copy(args.ImageData, rawPixels, rawPixels.Length);
var bitmap = XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(rawPixels, (int)args.Width, (int)args.Height);
bitmap.Freeze();
var frame = new DetectorFrame(
frameId: args.FrameNumber,
captureTime: args.CaptureTime,
width: (int)args.Width,
height: (int)args.Height,
rawPixels: rawPixels,
previewImage: bitmap);
EnqueueBounded(_acquireQueue, frame, AcquireQueueCapacity, ref _acquireQueueCount);
_mainViewportService.UpdateDetectorFrame(frame);
var sequence = Interlocked.Increment(ref _receivedFrameCount);
if ((sequence - 1) % ProcessEveryNFrames == 0)
{
EnqueueBounded(_processQueue, frame, ProcessQueueCapacity, ref _processQueueCount);
_processSignal.Release();
}
}
catch (Exception ex)
{
_logger.Error(ex, "探测器帧进入主界面流水线失败");
}
}
private async Task ProcessLoopAsync()
{
try
{
while (!_shutdown.IsCancellationRequested)
{
await _processSignal.WaitAsync(_shutdown.Token).ConfigureAwait(false);
while (_processQueue.TryDequeue(out var frame))
{
Interlocked.Decrement(ref _processQueueCount);
ProcessFrameDequeued?.Invoke(this, frame);
}
}
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
_logger.Error(ex, "探测器处理队列后台消费者异常退出");
}
}
private static void EnqueueBounded(
ConcurrentQueue<DetectorFrame> queue,
DetectorFrame frame,
int capacity,
ref int queueCount)
{
queue.Enqueue(frame);
var count = Interlocked.Increment(ref queueCount);
while (count > capacity && queue.TryDequeue(out _))
{
count = Interlocked.Decrement(ref queueCount);
}
}
private static int ReadInt(string key, int defaultValue, int minValue)
{
var raw = ConfigurationManager.AppSettings[key];
if (!int.TryParse(raw, out var parsed))
return defaultValue;
return parsed < minValue ? minValue : parsed;
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_shutdown.Cancel();
try
{
_processConsumerTask.Wait(TimeSpan.FromSeconds(2));
}
catch
{
}
_processSignal.Dispose();
_shutdown.Dispose();
}
}
}
@@ -0,0 +1,15 @@
using System;
namespace XplorePlane.Services.MainViewport
{
public interface IDetectorFramePipelineService : IDisposable
{
int AcquireQueueCount { get; }
int ProcessQueueCount { get; }
int AcquireQueueCapacity { get; }
int ProcessQueueCapacity { get; }
int ProcessEveryNFrames { get; }
event EventHandler<DetectorFrame> ProcessFrameDequeued;
}
}
@@ -0,0 +1,25 @@
using System;
using System.Windows.Media;
namespace XplorePlane.Services.MainViewport
{
public interface IMainViewportService
{
MainViewportSourceMode CurrentSourceMode { get; }
bool IsRealtimeDisplayEnabled { get; }
ImageSource CurrentDisplayImage { get; }
string CurrentDisplayInfo { get; }
ImageSource LatestDetectorImage { get; }
ImageSource LatestManualImage { get; }
event EventHandler StateChanged;
void SetRealtimeDisplayEnabled(bool isEnabled);
void SetSourceMode(MainViewportSourceMode sourceMode);
void UpdateDetectorFrame(DetectorFrame frame);
void SetManualImage(ImageSource image, string filePath);
}
}
@@ -0,0 +1,214 @@
using System;
using System.Configuration;
using System.IO;
using System.Windows.Media;
using XP.Common.Logging.Interfaces;
namespace XplorePlane.Services.MainViewport
{
public sealed class MainViewportService : IMainViewportService
{
private readonly object _syncRoot = new();
private readonly ILoggerService _logger;
private MainViewportSourceMode _currentSourceMode = MainViewportSourceMode.LiveDetector;
private bool _isRealtimeDisplayEnabled;
private ImageSource _currentDisplayImage;
private string _currentDisplayInfo = "等待探测器图像...";
private ImageSource _latestDetectorImage;
private string _latestDetectorInfo = "等待探测器图像...";
private ImageSource _latestManualImage;
private string _latestManualInfo = "未加载手动图像";
public MainViewportService(ILoggerService logger)
{
_logger = logger?.ForModule<MainViewportService>() ?? throw new ArgumentNullException(nameof(logger));
_isRealtimeDisplayEnabled = ReadBoolean("MainViewport:RealtimeEnabledDefault", true);
}
public MainViewportSourceMode CurrentSourceMode
{
get
{
lock (_syncRoot)
{
return _currentSourceMode;
}
}
}
public bool IsRealtimeDisplayEnabled
{
get
{
lock (_syncRoot)
{
return _isRealtimeDisplayEnabled;
}
}
}
public ImageSource CurrentDisplayImage
{
get
{
lock (_syncRoot)
{
return _currentDisplayImage;
}
}
}
public string CurrentDisplayInfo
{
get
{
lock (_syncRoot)
{
return _currentDisplayInfo;
}
}
}
public ImageSource LatestDetectorImage
{
get
{
lock (_syncRoot)
{
return _latestDetectorImage;
}
}
}
public ImageSource LatestManualImage
{
get
{
lock (_syncRoot)
{
return _latestManualImage;
}
}
}
public event EventHandler StateChanged;
public void SetRealtimeDisplayEnabled(bool isEnabled)
{
bool changed;
lock (_syncRoot)
{
changed = _isRealtimeDisplayEnabled != isEnabled;
_isRealtimeDisplayEnabled = isEnabled;
if (_currentSourceMode == MainViewportSourceMode.LiveDetector && isEnabled)
{
ApplyLiveDetectorDisplay_NoLock();
}
}
if (!changed)
return;
_logger.Info("主界面实时刷新已{State}", isEnabled ? "开启" : "关闭");
RaiseStateChanged();
}
public void SetSourceMode(MainViewportSourceMode sourceMode)
{
bool changed;
lock (_syncRoot)
{
changed = _currentSourceMode != sourceMode;
_currentSourceMode = sourceMode;
switch (sourceMode)
{
case MainViewportSourceMode.LiveDetector:
ApplyLiveDetectorDisplay_NoLock();
break;
case MainViewportSourceMode.ManualImage:
ApplyManualDisplay_NoLock();
break;
}
}
if (!changed)
return;
_logger.Info("主界面图像来源已切换为 {Mode}", sourceMode);
RaiseStateChanged();
}
public void UpdateDetectorFrame(DetectorFrame frame)
{
if (frame == null)
return;
bool shouldRaise = false;
lock (_syncRoot)
{
_latestDetectorImage = frame.PreviewImage;
_latestDetectorInfo = $"实时探测器图像 {frame.Width}x{frame.Height} 帧#{frame.FrameId} {frame.CaptureTime:HH:mm:ss.fff}";
if (_currentSourceMode == MainViewportSourceMode.LiveDetector && _isRealtimeDisplayEnabled)
{
_currentDisplayImage = _latestDetectorImage;
_currentDisplayInfo = _latestDetectorInfo;
shouldRaise = true;
}
}
if (shouldRaise)
RaiseStateChanged();
}
public void SetManualImage(ImageSource image, string filePath)
{
if (image == null)
return;
var fileName = string.IsNullOrWhiteSpace(filePath) ? "未命名图像" : Path.GetFileName(filePath);
lock (_syncRoot)
{
_latestManualImage = image;
_latestManualInfo = $"手动加载图像 {fileName}";
_currentSourceMode = MainViewportSourceMode.ManualImage;
_currentDisplayImage = _latestManualImage;
_currentDisplayInfo = _latestManualInfo;
}
_logger.Info("主界面已加载手动图像 {FileName}", fileName);
RaiseStateChanged();
}
private void ApplyLiveDetectorDisplay_NoLock()
{
_currentDisplayImage = _latestDetectorImage;
_currentDisplayInfo = _latestDetectorImage == null
? "等待探测器图像..."
: _latestDetectorInfo;
}
private void ApplyManualDisplay_NoLock()
{
_currentDisplayImage = _latestManualImage;
_currentDisplayInfo = _latestManualImage == null
? "未加载手动图像"
: _latestManualInfo;
}
private void RaiseStateChanged()
{
StateChanged?.Invoke(this, EventArgs.Empty);
}
private static bool ReadBoolean(string key, bool defaultValue)
{
var raw = ConfigurationManager.AppSettings[key];
return bool.TryParse(raw, out var parsed) ? parsed : defaultValue;
}
}
}
@@ -0,0 +1,8 @@
namespace XplorePlane.Services.MainViewport
{
public enum MainViewportSourceMode
{
LiveDetector = 0,
ManualImage = 1
}
}