主界面实时图像与探测器双队列
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user