Files
XplorePlane/XP.Hardware.Detector/ViewModels/DetectorImageWindowViewModel.cs
T

658 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions;
using XP.Hardware.Detector.Abstractions.Enums;
using XP.Hardware.Detector.Abstractions.Events;
using XP.Hardware.Detector.Services;
namespace XP.Hardware.Detector.ViewModels
{
/// <summary>
/// 探测器操作 ViewModel | Detector operation ViewModel
/// 负责连接/断开、连续采集、单帧采集、图像显示及状态跟踪
/// </summary>
public class DetectorImageWindowViewModel : BindableBase
{
private readonly IDetectorService _detectorService;
private readonly IEventAggregator _eventAggregator;
private readonly IImageService _imageSaveService;
private readonly ILoggerService _logger;
/// <summary>
/// 帧处理节流标志,防止后台线程堆积 | Frame processing throttle flag to prevent thread pile-up
/// 0 = 空闲,1 = 正在处理
/// </summary>
private int _isProcessingFrame;
#region | Connection status
private bool _isConnected;
/// <summary>
/// 探测器是否已连接 | Whether detector is connected
/// </summary>
public bool IsConnected
{
get => _isConnected;
private set
{
if (SetProperty(ref _isConnected, value))
{
RaiseAllCommandsCanExecuteChanged();
RaisePropertyChanged(nameof(ConnectionStatusText));
RaisePropertyChanged(nameof(ConnectionStatusColor));
}
}
}
/// <summary>
/// 连接状态文本 | Connection status text
/// </summary>
public string ConnectionStatusText => _isConnected ? "已连接 | Connected" : "未连接 | Disconnected";
/// <summary>
/// 连接状态指示颜色 | Connection status indicator color
/// </summary>
public Brush ConnectionStatusColor => _isConnected
? new SolidColorBrush(Color.FromRgb(0x4C, 0xAF, 0x50)) // 绿色 | Green
: new SolidColorBrush(Color.FromRgb(0xF4, 0x43, 0x36)); // 红色 | Red
#endregion
#region | Detector status
private DetectorStatus _detectorStatus = DetectorStatus.Uninitialized;
/// <summary>
/// 当前探测器状态 | Current detector status
/// </summary>
public DetectorStatus DetectorStatus
{
get => _detectorStatus;
private set
{
if (SetProperty(ref _detectorStatus, value))
{
RaisePropertyChanged(nameof(DetectorStatusText));
RaiseAllCommandsCanExecuteChanged();
}
}
}
/// <summary>
/// 探测器状态文本 | Detector status text
/// </summary>
public string DetectorStatusText => _detectorStatus switch
{
Abstractions.Enums.DetectorStatus.Uninitialized => "未初始化 | Uninitialized",
Abstractions.Enums.DetectorStatus.Initializing => "初始化中 | Initializing",
Abstractions.Enums.DetectorStatus.Ready => "就绪 | Ready",
Abstractions.Enums.DetectorStatus.Acquiring => "采集中 | Acquiring",
Abstractions.Enums.DetectorStatus.Correcting => "校正中 | Correcting",
Abstractions.Enums.DetectorStatus.Error => "错误 | Error",
_ => "未知 | Unknown"
};
#endregion
#region | Detector info
private string _detectorModel = "--";
/// <summary>
/// 探测器型号 | Detector model
/// </summary>
public string DetectorModel
{
get => _detectorModel;
private set => SetProperty(ref _detectorModel, value);
}
private string _detectorResolution = "--";
/// <summary>
/// 探测器分辨率 | Detector resolution
/// </summary>
public string DetectorResolution
{
get => _detectorResolution;
private set => SetProperty(ref _detectorResolution, value);
}
private string _detectorPixelSize = "--";
/// <summary>
/// 探测器像素尺寸 | Detector pixel size
/// </summary>
public string DetectorPixelSize
{
get => _detectorPixelSize;
private set => SetProperty(ref _detectorPixelSize, value);
}
private string _detectorType = "--";
/// <summary>
/// 探测器类型 | Detector type
/// </summary>
public string DetectorTypeText
{
get => _detectorType;
private set => SetProperty(ref _detectorType, value);
}
#endregion
#region | Image properties
private BitmapSource _imageSource;
/// <summary>
/// 当前显示的图像 | Currently displayed image
/// </summary>
public BitmapSource ImageSource
{
get => _imageSource;
private set => SetProperty(ref _imageSource, value);
}
private int _frameNumber;
/// <summary>
/// 当前帧号 | Current frame number
/// </summary>
public int FrameNumber
{
get => _frameNumber;
private set => SetProperty(ref _frameNumber, value);
}
private string _imageInfo = "等待采集... | Waiting for acquisition...";
/// <summary>
/// 图像信息文本 | Image info text
/// </summary>
public string ImageInfo
{
get => _imageInfo;
private set => SetProperty(ref _imageInfo, value);
}
private bool _isAcquiring;
/// <summary>
/// 是否正在连续采集 | Whether continuous acquisition is in progress
/// </summary>
public bool IsAcquiring
{
get => _isAcquiring;
private set
{
if (SetProperty(ref _isAcquiring, value))
RaiseAllCommandsCanExecuteChanged();
}
}
#endregion
#region | Operation busy state
private bool _isBusy;
/// <summary>
/// 是否正在执行连接/断开操作 | Whether connect/disconnect is in progress
/// </summary>
public bool IsBusy
{
get => _isBusy;
private set
{
if (SetProperty(ref _isBusy, value))
RaiseAllCommandsCanExecuteChanged();
}
}
private string _statusMessage = "";
/// <summary>
/// 操作状态消息 | Operation status message
/// </summary>
public string StatusMessage
{
get => _statusMessage;
private set => SetProperty(ref _statusMessage, value);
}
#endregion
#region | Commands
/// <summary>
/// 连接探测器命令 | Connect detector command
/// </summary>
public DelegateCommand ConnectCommand { get; }
/// <summary>
/// 断开探测器命令 | Disconnect detector command
/// </summary>
public DelegateCommand DisconnectCommand { get; }
/// <summary>
/// 启动连续采集命令 | Start continuous acquisition command
/// </summary>
public DelegateCommand StartAcquisitionCommand { get; }
/// <summary>
/// 停止采集命令 | Stop acquisition command
/// </summary>
public DelegateCommand StopAcquisitionCommand { get; }
/// <summary>
/// 单帧采集命令 | Single frame acquisition command
/// </summary>
public DelegateCommand SingleFrameCommand { get; }
/// <summary>
/// 保存图像命令 | Save image command
/// </summary>
public DelegateCommand SaveImageCommand { get; }
#endregion
#region | Constructor
public DetectorImageWindowViewModel(
IDetectorService detectorService,
IEventAggregator eventAggregator,
IImageService imageSaveService,
ILoggerService logger)
{
_detectorService = detectorService ?? throw new ArgumentNullException(nameof(detectorService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_imageSaveService = imageSaveService ?? throw new ArgumentNullException(nameof(imageSaveService));
_logger = logger?.ForModule<DetectorImageWindowViewModel>() ?? throw new ArgumentNullException(nameof(logger));
ConnectCommand = new DelegateCommand(ExecuteConnectAsync, CanExecuteConnect);
DisconnectCommand = new DelegateCommand(ExecuteDisconnectAsync, CanExecuteDisconnect);
StartAcquisitionCommand = new DelegateCommand(ExecuteStartAcquisitionAsync, CanExecuteStartAcquisition);
StopAcquisitionCommand = new DelegateCommand(ExecuteStopAcquisitionAsync, CanExecuteStopAcquisition);
SingleFrameCommand = new DelegateCommand(ExecuteSingleFrameAsync, CanExecuteSingleFrame);
SaveImageCommand = new DelegateCommand(ExecuteSaveImageAsync, CanExecuteSaveImage);
// 订阅图像采集事件(后台线程)| Subscribe to image captured event (background thread)
// 图像转换在后台线程执行,避免阻塞 UI | Image conversion runs on background thread to avoid blocking UI
_eventAggregator.GetEvent<ImageCapturedEvent>()
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
// 订阅探测器状态变更事件 | Subscribe to detector status changed event
_eventAggregator.GetEvent<StatusChangedEvent>()
.Subscribe(OnDetectorStatusChanged, ThreadOption.UIThread);
// 同步初始状态 | Sync initial state
SyncStatusFromService();
}
#endregion
#region CanExecute
private bool CanExecuteConnect() => !_isBusy && !_isConnected;
private bool CanExecuteDisconnect() => !_isBusy && _isConnected && !_isAcquiring;
private bool CanExecuteStartAcquisition() => !_isBusy && _isConnected &&
_detectorStatus == Abstractions.Enums.DetectorStatus.Ready &&
!_isAcquiring;
private bool CanExecuteStopAcquisition() => _isConnected && _isAcquiring;
private bool CanExecuteSingleFrame() => !_isBusy && _isConnected &&
_detectorStatus == Abstractions.Enums.DetectorStatus.Ready &&
!_isAcquiring;
private bool CanExecuteSaveImage() => _isConnected && _imageSaveService.LatestFrame != null;
private void RaiseAllCommandsCanExecuteChanged()
{
ConnectCommand.RaiseCanExecuteChanged();
DisconnectCommand.RaiseCanExecuteChanged();
StartAcquisitionCommand.RaiseCanExecuteChanged();
StopAcquisitionCommand.RaiseCanExecuteChanged();
SingleFrameCommand.RaiseCanExecuteChanged();
SaveImageCommand.RaiseCanExecuteChanged();
}
#endregion
#region | Command execution
/// <summary>
/// 连接探测器 | Connect detector
/// </summary>
private async void ExecuteConnectAsync()
{
_logger?.Info("开始连接探测器 | Connecting detector");
IsBusy = true;
StatusMessage = "正在连接探测器... | Connecting...";
try
{
var result = await _detectorService.InitializeAsync();
if (result.IsSuccess)
{
IsConnected = true;
StatusMessage = "探测器连接成功,正在应用参数... | Detector connected, applying parameters...";
_logger?.Info("探测器连接成功 | Detector connected successfully");
RefreshDetectorInfo();
// 连接成功后自动应用持久化的探测器参数 | Auto apply saved detector parameters after connection
var saved = Config.ConfigLoader.LoadSavedParameters();
if (saved.HasValue)
{
var (binIdx, pga, fr, _) = saved.Value;
var applyResult = await _detectorService.ApplyParametersAsync(binIdx, pga, fr);
if (applyResult.IsSuccess)
{
StatusMessage = "探测器就绪 | Detector ready";
_logger?.Info("探测器参数已自动应用,Binning={Binning}PGA={PGA},帧率={FrameRate} | Detector parameters auto-applied", binIdx, pga, fr);
}
else
{
StatusMessage = $"参数应用失败:{applyResult.ErrorMessage}";
_logger?.Warn("自动应用参数失败:{Message} | Auto apply parameters failed: {Message}", applyResult.ErrorMessage);
}
}
else
{
StatusMessage = "探测器就绪(使用默认参数)| Detector ready (default parameters)";
_logger?.Info("未找到持久化参数,使用默认配置 | No saved parameters found, using default config");
}
}
else
{
StatusMessage = $"连接失败:{result.ErrorMessage}";
_logger?.Error(result.Exception, "探测器连接失败:{Message} | Detector connection failed: {Message}", result.ErrorMessage);
}
}
catch (Exception ex)
{
StatusMessage = $"连接异常:{ex.Message}";
_logger?.Error(ex, "探测器连接异常:{Message} | Detector connection exception: {Message}", ex.Message);
}
finally
{
IsBusy = false;
}
}
/// <summary>
/// 断开探测器 | Disconnect detector
/// </summary>
private async void ExecuteDisconnectAsync()
{
_logger?.Info("开始断开探测器 | Disconnecting detector");
IsBusy = true;
StatusMessage = "正在断开探测器... | Disconnecting...";
try
{
var result = await _detectorService.DisconnectAsync();
IsConnected = false;
IsAcquiring = false;
ClearDetectorInfo();
ImageInfo = "等待采集... | Waiting for acquisition...";
if (result.IsSuccess)
{
StatusMessage = "探测器已断开 | Detector disconnected";
_logger?.Info("探测器已断开 | Detector disconnected");
}
else
{
StatusMessage = $"断开时发生错误:{result.ErrorMessage}";
_logger?.Warn("断开探测器时发生错误:{Message} | Error during disconnect: {Message}", result.ErrorMessage);
}
}
catch (Exception ex)
{
IsConnected = false;
IsAcquiring = false;
ClearDetectorInfo();
StatusMessage = $"断开异常:{ex.Message}";
_logger?.Error(ex, "断开探测器异常:{Message} | Disconnect exception: {Message}", ex.Message);
}
finally
{
IsBusy = false;
}
}
/// <summary>
/// 启动连续采集 | Start continuous acquisition
/// </summary>
private async void ExecuteStartAcquisitionAsync()
{
_logger?.Info("启动连续采集 | Starting continuous acquisition");
IsAcquiring = true;
StatusMessage = "采集中... | Acquiring...";
try
{
var result = await _detectorService.StartAcquisitionAsync();
if (!result.IsSuccess)
{
IsAcquiring = false;
StatusMessage = $"启动采集失败:{result.ErrorMessage}";
_logger?.Error(result.Exception, "启动采集失败:{Message} | Start acquisition failed: {Message}", result.ErrorMessage);
}
}
catch (Exception ex)
{
IsAcquiring = false;
StatusMessage = $"启动采集异常:{ex.Message}";
_logger?.Error(ex, "启动采集异常:{Message} | Start acquisition exception: {Message}", ex.Message);
}
}
/// <summary>
/// 停止采集 | Stop acquisition
/// </summary>
private async void ExecuteStopAcquisitionAsync()
{
_logger?.Info("停止采集 | Stopping acquisition");
StatusMessage = "正在停止采集... | Stopping...";
try
{
var result = await _detectorService.StopAcquisitionAsync();
IsAcquiring = false;
StatusMessage = result.IsSuccess ? "采集已停止 | Acquisition stopped" : $"停止采集失败:{result.ErrorMessage}";
if (!result.IsSuccess)
_logger?.Error(result.Exception, "停止采集失败:{Message} | Stop acquisition failed: {Message}", result.ErrorMessage);
}
catch (Exception ex)
{
IsAcquiring = false;
StatusMessage = $"停止采集异常:{ex.Message}";
_logger?.Error(ex, "停止采集异常:{Message} | Stop acquisition exception: {Message}", ex.Message);
}
}
/// <summary>
/// 单帧采集 | Single frame acquisition
/// </summary>
private async void ExecuteSingleFrameAsync()
{
_logger?.Info("单帧采集 | Single frame acquisition");
IsBusy = true;
StatusMessage = "单帧采集中... | Acquiring single frame...";
try
{
var result = await _detectorService.AcquireSingleFrameAsync();
StatusMessage = result.IsSuccess ? "单帧采集完成 | Single frame acquired" : $"单帧采集失败:{result.ErrorMessage}";
if (!result.IsSuccess)
_logger?.Error(result.Exception, "单帧采集失败:{Message} | Single frame failed: {Message}", result.ErrorMessage);
}
catch (Exception ex)
{
StatusMessage = $"单帧采集异常:{ex.Message}";
_logger?.Error(ex, "单帧采集异常:{Message} | Single frame exception: {Message}", ex.Message);
}
finally
{
IsBusy = false;
}
}
/// <summary>
/// 保存当前图像为 16 位 TIFF | Save current image as 16-bit TIFF
/// </summary>
private async void ExecuteSaveImageAsync()
{
var frame = _imageSaveService.LatestFrame;
if (frame?.ImageData == null)
{
StatusMessage = "无可保存的图像 | No image to save";
return;
}
_logger?.Info("保存图像,帧号:{FrameNumber} | Saving image, frame: {FrameNumber}", frame.FrameNumber);
StatusMessage = "正在保存图像... | Saving image...";
try
{
var saveDir = _imageSaveService.GetDefaultSaveDirectory();
var result = await _imageSaveService.SaveLatestFrameAsync(saveDir, "IMG");
StatusMessage = result.IsSuccess
? $"图像已保存 | Image saved"
: $"保存失败:{result.ErrorMessage}";
}
catch (Exception ex)
{
StatusMessage = $"保存异常:{ex.Message}";
_logger?.Error(ex, "保存图像异常:{Message} | Save image exception: {Message}", ex.Message);
}
}
#endregion
#region | Event callbacks
/// <summary>
/// 图像采集回调,将 16 位原始数据归一化为 8 位灰度图 | Image captured callback
/// </summary>
private void OnImageCaptured(ImageCapturedEventArgs args)
{
if (args?.ImageData == null || args.Width == 0 || args.Height == 0) return;
// 帧节流:上一帧尚未被 UI 线程消费完毕时,跳过当前帧
// Frame throttle: skip current frame if previous frame hasn't been consumed by UI thread yet
if (Interlocked.CompareExchange(ref _isProcessingFrame, 1, 0) != 0) return;
try
{
// 在后台线程执行图像转换 | Perform image conversion on background thread
var bitmap = ConvertToBitmapSource(args.ImageData, (int)args.Width, (int)args.Height);
bitmap.Freeze(); // Freeze 后可跨线程访问 | Freeze allows cross-thread access
var frameNumber = args.FrameNumber;
var imageInfo = $"{args.Width}×{args.Height} 帧#{args.FrameNumber} {args.CaptureTime:HH:mm:ss.fff}";
// 将属性赋值调度到 UI 线程,完成后才释放节流标志
// Dispatch property assignment to UI thread; release throttle flag only after UI completes
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
{
try
{
ImageSource = bitmap;
FrameNumber = frameNumber;
ImageInfo = imageInfo;
}
finally
{
// UI 线程完成渲染后才允许下一帧进入 | Allow next frame only after UI thread finishes rendering
Interlocked.Exchange(ref _isProcessingFrame, 0);
}
}));
}
catch (Exception ex)
{
// 异常时释放标志,避免永久锁死 | Release flag on exception to avoid permanent lock
Interlocked.Exchange(ref _isProcessingFrame, 0);
_logger?.Error(ex, "图像转换失败:{Message} | Image conversion failed: {Message}", ex.Message);
}
}
/// <summary>
/// 探测器状态变更回调 | Detector status changed callback
/// </summary>
private void OnDetectorStatusChanged(DetectorStatus status)
{
DetectorStatus = status;
// 根据状态同步 IsAcquiring | Sync IsAcquiring from status
if (status == Abstractions.Enums.DetectorStatus.Acquiring)
IsAcquiring = true;
else if (status == Abstractions.Enums.DetectorStatus.Ready ||
status == Abstractions.Enums.DetectorStatus.Error)
IsAcquiring = false;
// 探测器错误时同步连接状态 | Sync connection state on error
if (status == Abstractions.Enums.DetectorStatus.Uninitialized)
{
IsConnected = false;
ClearDetectorInfo();
}
}
#endregion
#region | Private helpers
/// <summary>
/// 从服务层同步当前状态(ViewModel 初始化时调用)| Sync state from service (called on init)
/// </summary>
private void SyncStatusFromService()
{
IsConnected = _detectorService.IsConnected;
DetectorStatus = _detectorService.Status;
if (IsConnected)
RefreshDetectorInfo();
}
/// <summary>
/// 刷新探测器信息显示 | Refresh detector info display
/// </summary>
private void RefreshDetectorInfo()
{
try
{
var info = _detectorService.GetInfo();
if (info == null) return;
DetectorModel = string.IsNullOrEmpty(info.Model) ? "--" : info.Model;
DetectorResolution = (info.MaxWidth > 0 && info.MaxHeight > 0)
? $"{info.MaxWidth}×{info.MaxHeight}"
: "--";
DetectorPixelSize = info.PixelSize > 0
? $"{info.PixelSize:F3} mm"
: "--";
DetectorTypeText = _detectorService.Type?.ToString() ?? "--";
}
catch (Exception ex)
{
_logger?.Warn("获取探测器信息失败:{Message} | Failed to get detector info: {Message}", ex.Message);
}
}
/// <summary>
/// 清空探测器信息 | Clear detector info
/// </summary>
private void ClearDetectorInfo()
{
DetectorModel = "--";
DetectorResolution = "--";
DetectorPixelSize = "--";
DetectorTypeText = "--";
}
/// <summary>
/// 将 16 位灰度数据转换为 8 位 BitmapSource(委托给 XP.Common 通用转换器)
/// Convert 16-bit grayscale data to 8-bit BitmapSource (delegates to XP.Common common converter)
/// </summary>
private static BitmapSource ConvertToBitmapSource(ushort[] data, int width, int height)
{
return XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(data, width, height);
}
#endregion
}
}