diff --git a/ReleaseFiles/App.config b/ReleaseFiles/App.config index d058de3..dfda658 100644 --- a/ReleaseFiles/App.config +++ b/ReleaseFiles/App.config @@ -71,7 +71,6 @@ - @@ -83,14 +82,13 @@ - - - + + diff --git a/ReleaseFiles/XplorePlane.deps.json b/ReleaseFiles/XplorePlane.deps.json index d92f969..ead026a 100644 --- a/ReleaseFiles/XplorePlane.deps.json +++ b/ReleaseFiles/XplorePlane.deps.json @@ -1018,6 +1018,14 @@ } } }, + "SixLabors.ImageSharp/3.1.12": { + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "assemblyVersion": "3.0.0.0", + "fileVersion": "3.1.12.0" + } + } + }, "SQLitePCLRaw.bundle_e_sqlite3/2.1.11": { "dependencies": { "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", @@ -2256,6 +2264,7 @@ "Serilog.Settings.Configuration": "10.0.0", "Serilog.Sinks.Console": "6.1.1", "Serilog.Sinks.File": "7.0.0", + "SixLabors.ImageSharp": "3.1.12", "Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408" }, "runtime": { @@ -3312,6 +3321,13 @@ "path": "sharpdx.mathematics/4.2.0", "hashPath": "sharpdx.mathematics.4.2.0.nupkg.sha512" }, + "SixLabors.ImageSharp/3.1.12": { + "type": "package", + "serviceable": true, + "sha512": "sha512-iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==", + "path": "sixlabors.imagesharp/3.1.12", + "hashPath": "sixlabors.imagesharp.3.1.12.nupkg.sha512" + }, "SQLitePCLRaw.bundle_e_sqlite3/2.1.11": { "type": "package", "serviceable": true, diff --git a/XP.Common/Controls/ImageHistogram/ChartRenderer.cs b/XP.Common/Controls/ImageHistogram/ChartRenderer.cs new file mode 100644 index 0000000..b02d5fc --- /dev/null +++ b/XP.Common/Controls/ImageHistogram/ChartRenderer.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Telerik.Windows.Controls; +using Telerik.Windows.Controls.ChartView; + +namespace XP.Common.Controls.ImageHistogram +{ + /// + /// RadChartView 渲染适配器 | RadChartView rendering adapter + /// 负责将直方图频次数据渲染到 Telerik RadCartesianChart 控件 + /// + internal sealed class ChartRenderer + { + private readonly RadCartesianChart _chart; + private readonly ScatterAreaSeries _areaSeries; + private readonly LinearAxis _xAxis; + + /// + /// 16 位数据聚合因子 | 16-bit data aggregation factor + /// + private const int AggregationFactor = 256; + + /// + /// 构造函数,接收 RadCartesianChart 实例 | Constructor, receives RadCartesianChart instance + /// + /// 图表控件实例 | Chart control instance + /// 面积图系列 | Area series + /// X 轴 | X axis + public ChartRenderer(RadCartesianChart chart, ScatterAreaSeries areaSeries, LinearAxis xAxis) + { + _chart = chart ?? throw new ArgumentNullException(nameof(chart)); + _areaSeries = areaSeries ?? throw new ArgumentNullException(nameof(areaSeries)); + _xAxis = xAxis ?? throw new ArgumentNullException(nameof(xAxis)); + } + + /// + /// 更新直方图数据 | Update histogram data + /// + /// 频次数组(256 或 65536 长度)| Frequency array (256 or 65536 length) + /// 是否使用对数 Y 轴 | Whether to use logarithmic Y axis + public void UpdateData(long[] histogram, bool isLogarithmic) + { + if (histogram == null || histogram.Length == 0) + return; + + // 确定是否需要聚合(16 位数据)| Determine if aggregation needed (16-bit data) + long[] displayData; + int xAxisMax; + + if (histogram.Length == 65536) + { + // 16 位数据聚合为 256 个柱体 | Aggregate 16-bit data to 256 bars + displayData = Aggregate16BitHistogram(histogram); + xAxisMax = 65535; + } + else + { + // 8 位数据直接显示 | Display 8-bit data directly + displayData = histogram; + xAxisMax = 255; + } + + // 设置 X 轴范围 | Set X axis range + _xAxis.Minimum = 0; + _xAxis.Maximum = xAxisMax; + // 根据范围自动设置刻度间隔(保持 4-5 个刻度)| Auto set major step based on range (keep 4-5 ticks) + _xAxis.MajorStep = xAxisMax <= 255 ? 64 : 16384; + + // 构建数据点 | Build data points + var dataPoints = new List(); + + if (isLogarithmic) + { + // 对数模式:频次为 0 的不绘制 | Logarithmic mode: skip zero frequency + for (int i = 0; i < displayData.Length; i++) + { + if (displayData[i] > 0) + { + double xValue = histogram.Length == 65536 + ? i * AggregationFactor + AggregationFactor / 2.0 + : i; + dataPoints.Add(new HistogramDataPoint + { + GrayLevel = xValue, + Frequency = displayData[i] + }); + } + } + } + else + { + // 线性模式:所有灰度级别都绘制 | Linear mode: draw all gray levels + for (int i = 0; i < displayData.Length; i++) + { + double xValue = histogram.Length == 65536 + ? i * AggregationFactor + AggregationFactor / 2.0 + : i; + dataPoints.Add(new HistogramDataPoint + { + GrayLevel = xValue, + Frequency = displayData[i] + }); + } + } + + // 更新图表数据 | Update chart data + _areaSeries.ItemsSource = dataPoints; + + // 设置 Y 轴范围 | Set Y axis range + UpdateYAxis(displayData, isLogarithmic); + } + + /// + /// 清空图表,恢复初始状态 | Clear chart, restore initial state + /// + public void Clear() + { + _areaSeries.ItemsSource = null; + + // X 轴范围重置为 0-255 | Reset X axis range to 0-255 + _xAxis.Minimum = 0; + _xAxis.Maximum = 255; + + // Y 轴范围重置为 0-1 | Reset Y axis range to 0-1 + SetYAxisRange(0, 1, isLogarithmic: false); + } + + /// + /// 获取当前数据点数量 | Get current data point count + /// + public int DataPointCount + { + get + { + if (_areaSeries.ItemsSource is ICollection collection) + return collection.Count; + if (_areaSeries.ItemsSource is IEnumerable enumerable) + return enumerable.Count(); + return 0; + } + } + + /// + /// 将 65536 长度的频次数组聚合为 256 个柱体 | Aggregate 65536-length array to 256 bars + /// + private static long[] Aggregate16BitHistogram(long[] histogram) + { + var aggregated = new long[256]; + for (int i = 0; i < 256; i++) + { + long sum = 0; + int startIndex = i * AggregationFactor; + for (int j = 0; j < AggregationFactor; j++) + { + sum += histogram[startIndex + j]; + } + aggregated[i] = sum; + } + return aggregated; + } + + /// + /// 更新 Y 轴范围 | Update Y axis range + /// + private void UpdateYAxis(long[] displayData, bool isLogarithmic) + { + long maxValue = 0; + for (int i = 0; i < displayData.Length; i++) + { + if (displayData[i] > maxValue) + maxValue = displayData[i]; + } + + if (maxValue == 0) + maxValue = 1; + + // 计算取整的 MajorStep(约 4 个刻度,对齐到 K/M 整数倍)| Calculate rounded MajorStep (~4 ticks, aligned to K/M multiples) + if (_chart.VerticalAxis is LinearAxis linearAxis) + { + long rawStep = maxValue / 4; + long step = RoundStepToNice(rawStep); + if (step < 1) step = 1; + linearAxis.MajorStep = step; + + // 将最大值向上取整到 step 的整数倍 | Round max up to multiple of step + long roundedMax = ((maxValue / step) + 1) * step; + SetYAxisRange(0, roundedMax, isLogarithmic); + } + else + { + SetYAxisRange(0, maxValue, isLogarithmic); + } + } + + /// + /// 将步长取整为"好看"的数值(1, 2, 5 的倍数 × 10^n)| Round step to "nice" value (multiples of 1, 2, 5 × 10^n) + /// 例如:123456 → 100000,350000 → 500000,780000 → 1000000 + /// + private static long RoundStepToNice(long rawStep) + { + if (rawStep <= 0) return 1; + + // 找到数量级 | Find order of magnitude + double magnitude = Math.Pow(10, Math.Floor(Math.Log10(rawStep))); + double normalized = rawStep / magnitude; + + // 取整到 1, 2, 5 中最近的 | Round to nearest of 1, 2, 5 + double niceNormalized; + if (normalized <= 1.5) + niceNormalized = 1; + else if (normalized <= 3.5) + niceNormalized = 2; + else if (normalized <= 7.5) + niceNormalized = 5; + else + niceNormalized = 10; + + return (long)(niceNormalized * magnitude); + } + + /// + /// 设置 Y 轴范围和刻度类型 | Set Y axis range and scale type + /// + private void SetYAxisRange(double minimum, double maximum, bool isLogarithmic) + { + // 获取或创建 Y 轴 | Get or create Y axis + var verticalAxis = _chart.VerticalAxis; + + if (isLogarithmic) + { + // 对数刻度 | Logarithmic scale + if (verticalAxis is LogarithmicAxis logAxis) + { + logAxis.Minimum = 1; + logAxis.Maximum = maximum; + logAxis.LogarithmBase = 10; + } + else + { + // 需要切换为对数轴 | Need to switch to logarithmic axis + var newLogAxis = new LogarithmicAxis + { + Minimum = 1, + Maximum = maximum, + LogarithmBase = 10 + }; + _chart.VerticalAxis = newLogAxis; + } + } + else + { + // 线性刻度 | Linear scale + if (verticalAxis is LinearAxis linearAxis) + { + linearAxis.Minimum = minimum; + linearAxis.Maximum = maximum; + } + else + { + // 需要切换为线性轴 | Need to switch to linear axis + var newLinearAxis = new LinearAxis + { + Minimum = minimum, + Maximum = maximum + }; + _chart.VerticalAxis = newLinearAxis; + } + } + } + } + + /// + /// 直方图数据点模型 | Histogram data point model + /// + internal class HistogramDataPoint + { + /// + /// 灰度级别(X 轴值)| Gray level (X axis value) + /// + public double GrayLevel { get; set; } + + /// + /// 像素频次(Y 轴值)| Pixel frequency (Y axis value) + /// + public long Frequency { get; set; } + } +} diff --git a/XP.Common/Controls/ImageHistogram/FrameThrottler.cs b/XP.Common/Controls/ImageHistogram/FrameThrottler.cs new file mode 100644 index 0000000..bb3c598 --- /dev/null +++ b/XP.Common/Controls/ImageHistogram/FrameThrottler.cs @@ -0,0 +1,207 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace XP.Common.Controls.ImageHistogram +{ + /// + /// 帧率限流器,确保计算频率不超过 MaxFrameRate | Frame rate throttler + /// 支持从任意线程调用,使用 lock 保护内部状态 + /// + internal sealed class FrameThrottler : IDisposable + { + private readonly object _lock = new(); + private DateTime _lastProcessTime = DateTime.MinValue; + private Action? _pendingAction; + private CancellationTokenSource? _delayCts; + private bool _isProcessing; + private bool _disposed; + private int _maxFrameRate = 15; + + /// + /// 最大刷新帧率(fps),有效范围 1-60,超出范围自动钳位 | Max frame rate (fps), valid range 1-60, auto-clamped + /// + public int MaxFrameRate + { + get => _maxFrameRate; + set => _maxFrameRate = Math.Clamp(value, 1, 60); + } + + /// + /// 获取当前帧间隔(毫秒)| Get current frame interval (ms) + /// + private double FrameIntervalMs => 1000.0 / _maxFrameRate; + + /// + /// 提交一帧计算动作 | Submit a frame compute action + /// 若未超过帧率限制则立即执行,否则缓存最新帧并延迟触发 + /// + /// 计算动作 | Compute action + /// 是否被立即接受处理 | Whether it was immediately accepted + public bool TrySubmit(Action computeAction) + { + if (_disposed || computeAction == null) + return false; + + lock (_lock) + { + var now = DateTime.UtcNow; + var elapsed = (now - _lastProcessTime).TotalMilliseconds; + + if (elapsed >= FrameIntervalMs && !_isProcessing) + { + // 已超过间隔且无正在处理的任务,立即执行 | Interval exceeded and no processing, execute immediately + _isProcessing = true; + _lastProcessTime = now; + ExecuteAction(computeAction); + return true; + } + else + { + // 未超过间隔或正在处理中,缓存最新帧(丢弃之前的中间帧)| Cache latest frame, discard previous + _pendingAction = computeAction; + ScheduleDelayedExecution(elapsed); + return false; + } + } + } + + /// + /// 执行计算动作(异步,完成后检查待处理帧)| Execute compute action asynchronously + /// + private void ExecuteAction(Action action) + { + Task.Run(() => + { + try + { + action.Invoke(); + } + catch + { + // 异常不外抛 | Do not propagate exceptions + } + finally + { + OnActionCompleted(); + } + }); + } + + /// + /// 计算动作完成后的回调 | Callback after compute action completes + /// + private void OnActionCompleted() + { + Action? nextAction = null; + + lock (_lock) + { + _isProcessing = false; + + if (_pendingAction != null && !_disposed) + { + var now = DateTime.UtcNow; + var elapsed = (now - _lastProcessTime).TotalMilliseconds; + + if (elapsed >= FrameIntervalMs) + { + // 间隔已到,立即执行待处理帧 | Interval reached, execute pending frame + nextAction = _pendingAction; + _pendingAction = null; + _isProcessing = true; + _lastProcessTime = now; + } + // 否则等待延迟触发 | Otherwise wait for delayed trigger + } + } + + if (nextAction != null) + { + ExecuteAction(nextAction); + } + } + + /// + /// 安排延迟执行(等待帧间隔到期后处理最新帧)| Schedule delayed execution + /// + private void ScheduleDelayedExecution(double elapsedMs) + { + // 取消之前的延迟任务 | Cancel previous delay task + _delayCts?.Cancel(); + _delayCts?.Dispose(); + _delayCts = new CancellationTokenSource(); + var token = _delayCts.Token; + + var delayMs = Math.Max(0, FrameIntervalMs - elapsedMs); + + Task.Run(async () => + { + try + { + await Task.Delay((int)delayMs, token); + + if (token.IsCancellationRequested) + return; + + Action? actionToExecute = null; + + lock (_lock) + { + if (_pendingAction != null && !_isProcessing && !_disposed) + { + actionToExecute = _pendingAction; + _pendingAction = null; + _isProcessing = true; + _lastProcessTime = DateTime.UtcNow; + } + } + + if (actionToExecute != null) + { + ExecuteAction(actionToExecute); + } + } + catch (OperationCanceledException) + { + // 延迟被取消,正常情况 | Delay cancelled, normal case + } + catch + { + // 异常不外抛 | Do not propagate exceptions + } + }); + } + + /// + /// 取消所有待处理任务 | Cancel all pending tasks + /// + public void Cancel() + { + lock (_lock) + { + _pendingAction = null; + _delayCts?.Cancel(); + _delayCts?.Dispose(); + _delayCts = null; + } + } + + /// + /// 释放所有资源 | Dispose all resources + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + lock (_lock) + { + _pendingAction = null; + _delayCts?.Cancel(); + _delayCts?.Dispose(); + _delayCts = null; + } + } + } +} diff --git a/XP.Common/Controls/ImageHistogram/FrequencyLabelConverter.cs b/XP.Common/Controls/ImageHistogram/FrequencyLabelConverter.cs new file mode 100644 index 0000000..76a76c8 --- /dev/null +++ b/XP.Common/Controls/ImageHistogram/FrequencyLabelConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.Globalization; +using System.Windows.Data; + +namespace XP.Common.Controls.ImageHistogram +{ + /// + /// 频次标签转换器:将大数值转为 K/M 缩写格式 | Frequency label converter: converts large values to K/M abbreviation format + /// 例如:500000 → "500K",1500000 → "1.5M",800 → "800" + /// + internal sealed class FrequencyLabelConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) return "0"; + + double num; + if (value is double d) + num = d; + else if (value is decimal dec) + num = (double)dec; + else if (!double.TryParse(value.ToString(), out num)) + return value.ToString() ?? "0"; + + if (num >= 1_000_000) + { + double mValue = num / 1_000_000.0; + return mValue == Math.Floor(mValue) + ? $"{(int)mValue}M" + : $"{mValue:0.#}M"; + } + + if (num >= 1_000) + { + double kValue = num / 1_000.0; + return kValue == Math.Floor(kValue) + ? $"{(int)kValue}K" + : $"{kValue:0.#}K"; + } + + return $"{(int)num}"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/XP.Common/Controls/ImageHistogram/HistogramEngine.cs b/XP.Common/Controls/ImageHistogram/HistogramEngine.cs new file mode 100644 index 0000000..cafa8c7 --- /dev/null +++ b/XP.Common/Controls/ImageHistogram/HistogramEngine.cs @@ -0,0 +1,211 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace XP.Common.Controls.ImageHistogram +{ + /// + /// 直方图后台计算引擎 | Histogram background computation engine + /// 负责在后台线程中执行灰度值遍历和统计计算 + /// + internal sealed class HistogramEngine : IDisposable + { + /// + /// 单帧计算超时时间(毫秒)| Single frame computation timeout (ms) + /// + private const int ComputeTimeoutMs = 5000; + + private CancellationTokenSource? _timeoutCts; + private readonly object _lock = new(); + private bool _disposed; + + /// + /// 从 Image<Rgba32> 计算灰度直方图 | Compute histogram from Image<Rgba32> + /// 使用 ITU-R BT.601 亮度公式:Gray = 0.299R + 0.587G + 0.114B + /// + /// 输入图像 | Input image + /// 取消令牌 | Cancellation token + /// 256 长度的频次数组,失败返回 null | 256-length frequency array, null on failure + public Task ComputeAsync(Image image, CancellationToken ct) + { + if (image == null) + return Task.FromResult(null); + + // 创建超时令牌 | Create timeout token + var linkedCts = CreateLinkedTimeoutToken(ct); + var linkedToken = linkedCts.Token; + + return Task.Run(() => + { + try + { + var width = image.Width; + var height = image.Height; + var histogram = new long[256]; + + // 遍历像素,使用亮度公式计算灰度值 | Iterate pixels, compute grayscale using luminance formula + for (int y = 0; y < height; y++) + { + linkedToken.ThrowIfCancellationRequested(); + + for (int x = 0; x < width; x++) + { + var pixel = image[x, y]; + // ITU-R BT.601 亮度公式 | ITU-R BT.601 luminance formula + var gray = (int)(0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B); + // 钳位到 0-255 范围 | Clamp to 0-255 range + gray = Math.Clamp(gray, 0, 255); + histogram[gray]++; + } + } + + return (long[]?)histogram; + } + catch (OperationCanceledException) + { + // 超时或取消,返回 null | Timeout or cancelled, return null + return null; + } + catch + { + // 所有异常内部捕获,不向外抛出 | Catch all exceptions internally + return null; + } + finally + { + linkedCts.Dispose(); + } + }, linkedToken); + } + + /// + /// 从原始字节数组计算灰度直方图 | Compute histogram from raw byte array + /// + /// 原始像素数据 | Raw pixel data + /// 图像宽度 | Image width + /// 图像高度 | Image height + /// 位深度(8 或 16)| Bit depth (8 or 16) + /// 取消令牌 | Cancellation token + /// 频次数组(8位:256长度,16位:65536长度),失败返回 null | Frequency array, null on failure + public Task ComputeAsync(byte[] rawData, int width, int height, int bitDepth, CancellationToken ct) + { + // 参数有效性验证 | Parameter validation + if (rawData == null || width <= 0 || height <= 0) + return Task.FromResult(null); + + if (bitDepth != 8 && bitDepth != 16) + return Task.FromResult(null); + + int expectedLength = bitDepth == 8 ? width * height : width * height * 2; + if (rawData.Length != expectedLength) + return Task.FromResult(null); + + // 创建超时令牌 | Create timeout token + var linkedCts = CreateLinkedTimeoutToken(ct); + var linkedToken = linkedCts.Token; + + return Task.Run(() => + { + try + { + if (bitDepth == 8) + { + return ComputeHistogram8Bit(rawData, width, height, linkedToken); + } + else + { + return ComputeHistogram16Bit(rawData, width, height, linkedToken); + } + } + catch (OperationCanceledException) + { + return null; + } + catch + { + return null; + } + finally + { + linkedCts.Dispose(); + } + }, linkedToken); + } + + /// + /// 计算 8 位灰度直方图 | Compute 8-bit grayscale histogram + /// + private static long[]? ComputeHistogram8Bit(byte[] rawData, int width, int height, CancellationToken ct) + { + var histogram = new long[256]; + int totalPixels = width * height; + + for (int i = 0; i < totalPixels; i++) + { + if (i % 65536 == 0) + ct.ThrowIfCancellationRequested(); + + histogram[rawData[i]]++; + } + + return histogram; + } + + /// + /// 计算 16 位灰度直方图 | Compute 16-bit grayscale histogram + /// + private static long[]? ComputeHistogram16Bit(byte[] rawData, int width, int height, CancellationToken ct) + { + var histogram = new long[65536]; + int totalPixels = width * height; + + for (int i = 0; i < totalPixels; i++) + { + if (i % 65536 == 0) + ct.ThrowIfCancellationRequested(); + + // 小端序读取 16 位值 | Read 16-bit value in little-endian + int offset = i * 2; + ushort value = (ushort)(rawData[offset] | (rawData[offset + 1] << 8)); + histogram[value]++; + } + + return histogram; + } + + /// + /// 创建带超时的链接取消令牌 | Create linked cancellation token with timeout + /// + private CancellationTokenSource CreateLinkedTimeoutToken(CancellationToken externalToken) + { + var timeoutCts = new CancellationTokenSource(ComputeTimeoutMs); + var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalToken, timeoutCts.Token); + + lock (_lock) + { + _timeoutCts?.Dispose(); + _timeoutCts = timeoutCts; + } + + return linkedCts; + } + + /// + /// 释放资源 | Dispose resources + /// + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + lock (_lock) + { + _timeoutCts?.Cancel(); + _timeoutCts?.Dispose(); + _timeoutCts = null; + } + } + } +} diff --git a/XP.Common/Controls/ImageHistogram/ImageHistogramControl.xaml b/XP.Common/Controls/ImageHistogram/ImageHistogramControl.xaml new file mode 100644 index 0000000..f274cf1 --- /dev/null +++ b/XP.Common/Controls/ImageHistogram/ImageHistogramControl.xaml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XP.Common/Controls/ImageHistogram/ImageHistogramControl.xaml.cs b/XP.Common/Controls/ImageHistogram/ImageHistogramControl.xaml.cs new file mode 100644 index 0000000..ae98696 --- /dev/null +++ b/XP.Common/Controls/ImageHistogram/ImageHistogramControl.xaml.cs @@ -0,0 +1,348 @@ +using System; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Threading; +using Prism.Ioc; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using XP.Common.Logging.Interfaces; + +namespace XP.Common.Controls.ImageHistogram +{ + /// + /// 图像灰度直方图通用控件 | Image grayscale histogram control + /// 支持单帧静态图像和高频流式图像输入,使用 Telerik RadChartView 进行可视化渲染 + /// + public partial class ImageHistogramControl : UserControl + { + #region 依赖属性 | Dependency Properties + + /// + /// 最大刷新帧率依赖属性 | MaxFrameRate dependency property + /// + public static readonly DependencyProperty MaxFrameRateProperty = + DependencyProperty.Register( + nameof(MaxFrameRate), + typeof(int), + typeof(ImageHistogramControl), + new PropertyMetadata(15, OnMaxFrameRateChanged, CoerceMaxFrameRate)); + + /// + /// 是否使用对数 Y 轴依赖属性 | IsLogarithmic dependency property + /// + public static readonly DependencyProperty IsLogarithmicProperty = + DependencyProperty.Register( + nameof(IsLogarithmic), + typeof(bool), + typeof(ImageHistogramControl), + new PropertyMetadata(false)); + + /// + /// 最大刷新帧率(fps),有效范围 1-60,默认 15 | Max frame rate (fps), valid range 1-60, default 15 + /// + public int MaxFrameRate + { + get => (int)GetValue(MaxFrameRateProperty); + set => SetValue(MaxFrameRateProperty, value); + } + + /// + /// 是否使用对数 Y 轴,默认 false | Whether to use logarithmic Y axis, default false + /// + public bool IsLogarithmic + { + get => (bool)GetValue(IsLogarithmicProperty); + set => SetValue(IsLogarithmicProperty, value); + } + + private static void OnMaxFrameRateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ImageHistogramControl control) + { + var newValue = (int)e.NewValue; + control._frameThrottler.MaxFrameRate = newValue; + } + } + + private static object CoerceMaxFrameRate(DependencyObject d, object baseValue) + { + var value = (int)baseValue; + var clamped = Math.Clamp(value, 1, 60); + + if (clamped != value && d is ImageHistogramControl control) + { + control._logger?.Warn( + "MaxFrameRate 值 {Value} 超出有效范围,已钳位为 {Clamped} | MaxFrameRate value {Value} out of range, clamped to {Clamped}", + value, clamped); + } + + return clamped; + } + + #endregion + + #region 私有字段 | Private Fields + + private readonly FrameThrottler _frameThrottler; + private readonly HistogramEngine _histogramEngine; + private ChartRenderer? _chartRenderer; + private ILoggerService? _logger; + private CancellationTokenSource? _currentCts; + private readonly object _ctsLock = new(); + + #endregion + + #region 构造函数 | Constructor + + /// + /// 构造函数 | Constructor + /// + public ImageHistogramControl() + { + InitializeComponent(); + + // 初始化内部组件 | Initialize internal components + _frameThrottler = new FrameThrottler(); + _histogramEngine = new HistogramEngine(); + + // 尝试解析日志服务 | Try to resolve logger service + try + { + var loggerService = ContainerLocator.Current?.Resolve(); + _logger = loggerService?.ForModule(); + } + catch + { + // 日志服务不可用,静默降级 | Logger service unavailable, silent degradation + _logger = null; + } + + // 订阅 Loaded 事件初始化 ChartRenderer | Subscribe Loaded event to initialize ChartRenderer + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + // 初始化 ChartRenderer | Initialize ChartRenderer + _chartRenderer = new ChartRenderer(HistogramChart, HistogramBarSeries, XAxis); + } + + #endregion + + #region 公共 API | Public API + + /// + /// 传入 ImageSharp 图像对象,计算并显示灰度直方图 | Update histogram from ImageSharp image + /// + /// ImageSharp 图像对象 | ImageSharp image object + public void UpdateImage(Image image) + { + try + { + if (image == null) + { + _logger?.Warn("UpdateImage 收到 null 图像,已忽略 | UpdateImage received null image, ignored"); + return; + } + + SubmitComputation(() => _histogramEngine.ComputeAsync(image, GetOrCreateCancellationToken())); + } + catch (Exception ex) + { + _logger?.Error(ex, "UpdateImage(Image) 异常:{Message} | UpdateImage(Image) error: {Message}", ex.Message); + } + } + + /// + /// 传入原始像素数组,计算并显示灰度直方图 | Update histogram from raw byte array + /// + /// 原始像素数据 | Raw pixel data + /// 图像宽度 | Image width + /// 图像高度 | Image height + /// 位深度(8 或 16)| Bit depth (8 or 16) + public void UpdateImage(byte[] rawData, int width, int height, int bitDepth) + { + try + { + // 参数有效性验证 | Parameter validation + if (rawData == null) + { + _logger?.Warn("UpdateImage 收到 null rawData,已忽略 | UpdateImage received null rawData, ignored"); + return; + } + + if (width <= 0 || height <= 0) + { + _logger?.Warn( + "UpdateImage 参数无效:width={Width}, height={Height} | Invalid params: width={Width}, height={Height}", + width, height); + return; + } + + if (bitDepth != 8 && bitDepth != 16) + { + _logger?.Warn( + "UpdateImage 参数无效:bitDepth={BitDepth},仅支持 8 或 16 | Invalid bitDepth={BitDepth}, only 8 or 16 supported", + bitDepth); + return; + } + + int expectedLength = bitDepth == 8 ? width * height : width * height * 2; + if (rawData.Length != expectedLength) + { + _logger?.Warn( + "UpdateImage 参数无效:rawData.Length={Length}, 预期={Expected} | Invalid params: rawData.Length={Length}, expected={Expected}", + rawData.Length, expectedLength); + return; + } + + SubmitComputation(() => _histogramEngine.ComputeAsync(rawData, width, height, bitDepth, GetOrCreateCancellationToken())); + } + catch (Exception ex) + { + _logger?.Error(ex, "UpdateImage(byte[]) 异常:{Message} | UpdateImage(byte[]) error: {Message}", ex.Message); + } + } + + /// + /// 清空直方图显示,恢复初始空白状态 | Clear histogram display, restore initial blank state + /// + public void Clear() + { + try + { + // 取消正在执行的后台任务 | Cancel running background task + CancelCurrentComputation(); + + // 取消帧率限流器中的待处理任务 | Cancel pending tasks in throttler + _frameThrottler.Cancel(); + + // 清空图表(捕获局部引用避免异步执行时为 null)| Clear chart (capture local ref to avoid null during async) + var renderer = _chartRenderer; + if (renderer != null) + { + Dispatcher.InvokeAsync(() => + { + try + { + renderer.Clear(); + + // 显示无数据提示 | Show no-data placeholder + NoDataPlaceholder.Visibility = Visibility.Visible; + } + catch + { + // 控件已卸载时忽略 | Ignore if control already unloaded + } + }); + } + } + catch (Exception ex) + { + _logger?.Error(ex, "Clear() 异常:{Message} | Clear() error: {Message}", ex.Message); + } + } + + #endregion + + #region 私有方法 | Private Methods + + /// + /// 通过帧率限流器提交计算任务 | Submit computation through frame throttler + /// + private void SubmitComputation(Func> computeFunc) + { + _frameThrottler.TrySubmit(() => + { + try + { + var task = computeFunc(); + task.ContinueWith(t => + { + if (t.IsCompletedSuccessfully && t.Result != null) + { + var histogram = t.Result; + var isLog = false; + + // 在 UI 线程获取 IsLogarithmic 值并更新图表 | Get IsLogarithmic on UI thread and update chart + Dispatcher.InvokeAsync(() => + { + try + { + isLog = IsLogarithmic; + _chartRenderer?.UpdateData(histogram, isLog); + + // 隐藏无数据提示 | Hide no-data placeholder + NoDataPlaceholder.Visibility = Visibility.Collapsed; + } + catch (Exception ex) + { + _logger?.Error(ex, "图表更新异常:{Message} | Chart update error: {Message}", ex.Message); + } + }); + } + else if (t.IsFaulted) + { + _logger?.Error(t.Exception, "直方图计算异常:{Message} | Histogram computation error: {Message}", + t.Exception?.InnerException?.Message ?? "Unknown"); + } + }, System.Threading.Tasks.TaskScheduler.Default); + } + catch (Exception ex) + { + _logger?.Error(ex, "提交计算任务异常:{Message} | Submit computation error: {Message}", ex.Message); + } + }); + } + + /// + /// 获取或创建取消令牌(取消上一个)| Get or create cancellation token (cancel previous) + /// + private CancellationToken GetOrCreateCancellationToken() + { + lock (_ctsLock) + { + _currentCts?.Cancel(); + _currentCts?.Dispose(); + _currentCts = new CancellationTokenSource(); + return _currentCts.Token; + } + } + + /// + /// 取消当前计算 | Cancel current computation + /// + private void CancelCurrentComputation() + { + lock (_ctsLock) + { + _currentCts?.Cancel(); + _currentCts?.Dispose(); + _currentCts = null; + } + } + + /// + /// Unloaded 事件处理:释放所有资源 | Unloaded event handler: release all resources + /// + private void OnUnloaded(object sender, RoutedEventArgs e) + { + // 取消所有后台任务 | Cancel all background tasks + CancelCurrentComputation(); + + // 释放帧率限流器 | Dispose frame throttler + _frameThrottler.Cancel(); + _frameThrottler.Dispose(); + + // 释放计算引擎 | Dispose histogram engine + _histogramEngine.Dispose(); + + // 清空引用 | Clear references + _chartRenderer = null; + } + + #endregion + } +} diff --git a/XP.Common/Controls/JoystickCalculator.cs b/XP.Common/Controls/Joystick/JoystickCalculator.cs similarity index 99% rename from XP.Common/Controls/JoystickCalculator.cs rename to XP.Common/Controls/Joystick/JoystickCalculator.cs index f1bb028..438b254 100644 --- a/XP.Common/Controls/JoystickCalculator.cs +++ b/XP.Common/Controls/Joystick/JoystickCalculator.cs @@ -1,6 +1,6 @@ using System; -namespace XP.Common.Controls +namespace XP.Common.Controls.Joystick { /// /// 虚拟摇杆核心计算逻辑(纯函数,无副作用)| Virtual joystick core calculation logic (pure functions, no side effects) diff --git a/XP.Common/Controls/JoystickMode.cs b/XP.Common/Controls/Joystick/JoystickMode.cs similarity index 92% rename from XP.Common/Controls/JoystickMode.cs rename to XP.Common/Controls/Joystick/JoystickMode.cs index 0c44ede..0e32f46 100644 --- a/XP.Common/Controls/JoystickMode.cs +++ b/XP.Common/Controls/Joystick/JoystickMode.cs @@ -1,4 +1,4 @@ -namespace XP.Common.Controls +namespace XP.Common.Controls.Joystick { /// /// 虚拟摇杆轴模式枚举 | Virtual joystick axis mode enumeration diff --git a/XP.Common/Controls/MouseButtonType.cs b/XP.Common/Controls/Joystick/MouseButtonType.cs similarity index 92% rename from XP.Common/Controls/MouseButtonType.cs rename to XP.Common/Controls/Joystick/MouseButtonType.cs index 9cd0753..b468ae8 100644 --- a/XP.Common/Controls/MouseButtonType.cs +++ b/XP.Common/Controls/Joystick/MouseButtonType.cs @@ -1,4 +1,4 @@ -namespace XP.Common.Controls +namespace XP.Common.Controls.Joystick { /// /// 鼠标按键类型枚举 | Mouse button type enumeration diff --git a/XP.Common/Controls/VirtualJoystick.cs b/XP.Common/Controls/Joystick/VirtualJoystick.cs similarity index 99% rename from XP.Common/Controls/VirtualJoystick.cs rename to XP.Common/Controls/Joystick/VirtualJoystick.cs index 64b6e3b..1ff85b0 100644 --- a/XP.Common/Controls/VirtualJoystick.cs +++ b/XP.Common/Controls/Joystick/VirtualJoystick.cs @@ -6,7 +6,7 @@ using System.Windows.Media; using System.Windows.Shapes; using XP.Common.Logging.Interfaces; -namespace XP.Common.Controls +namespace XP.Common.Controls.Joystick { /// /// 虚拟摇杆 UserControl,提供圆形区域内的鼠标拖拽操控能力 | Virtual joystick UserControl providing mouse drag interaction within a circular area diff --git a/XP.Common/Controls/VirtualJoystick.xaml b/XP.Common/Controls/Joystick/VirtualJoystick.xaml similarity index 98% rename from XP.Common/Controls/VirtualJoystick.xaml rename to XP.Common/Controls/Joystick/VirtualJoystick.xaml index afa6d25..68ab67f 100644 --- a/XP.Common/Controls/VirtualJoystick.xaml +++ b/XP.Common/Controls/Joystick/VirtualJoystick.xaml @@ -1,9 +1,9 @@ - diff --git a/XP.Common/Resources/Resources.en-US.resx b/XP.Common/Resources/Resources.en-US.resx index 75a2e64..b115e45 100644 --- a/XP.Common/Resources/Resources.en-US.resx +++ b/XP.Common/Resources/Resources.en-US.resx @@ -1887,7 +1887,6 @@ Reprojection error: {1:F4} pixels Image{0}: {1:F4} pixels - Edge Find Line Fit @@ -2005,4 +2004,8 @@ Reprojection error: {1:F4} pixels Drawing thickness for result visualization + + Histogram — No data + ImageHistogramControl - Placeholder text when no image data + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.resx b/XP.Common/Resources/Resources.resx index e102321..2009be3 100644 --- a/XP.Common/Resources/Resources.resx +++ b/XP.Common/Resources/Resources.resx @@ -1920,7 +1920,6 @@ 图像{0}: {1:F4} 像素 - 边缘查找拟合直线 @@ -2038,4 +2037,8 @@ 绘制结果的线条粗细 + + 直方图 — 暂无数据 + ImageHistogramControl - 无图像输入时的提示文本 | Placeholder text when no image data + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.zh-CN.resx b/XP.Common/Resources/Resources.zh-CN.resx index f3ce870..87926f0 100644 --- a/XP.Common/Resources/Resources.zh-CN.resx +++ b/XP.Common/Resources/Resources.zh-CN.resx @@ -1881,7 +1881,6 @@ 图像{0}: {1:F4} 像素 - 边缘查找拟合直线 @@ -1999,4 +1998,8 @@ 绘制结果的线条粗细 + + 直方图 — 暂无数据 + ImageHistogramControl - 无图像输入时的提示文本 | Placeholder text when no image data + \ No newline at end of file diff --git a/XP.Common/Resources/Resources.zh-TW.resx b/XP.Common/Resources/Resources.zh-TW.resx index 946e1f5..630a189 100644 --- a/XP.Common/Resources/Resources.zh-TW.resx +++ b/XP.Common/Resources/Resources.zh-TW.resx @@ -1881,4 +1881,8 @@ 图像{0}: {1:F4} 像素 + + 直方圖 — 暫無資料 + ImageHistogramControl - 無圖像輸入時的提示文字 | Placeholder text when no image data + \ No newline at end of file diff --git a/XP.Common/XP.Common.csproj b/XP.Common/XP.Common.csproj index 90e15ca..c22729e 100644 --- a/XP.Common/XP.Common.csproj +++ b/XP.Common/XP.Common.csproj @@ -27,6 +27,7 @@ + diff --git a/XP.Hardware.Detector/Abstractions/AreaDetectorBase.cs b/XP.Hardware.Detector/Abstractions/AreaDetectorBase.cs index 8964a33..258021d 100644 --- a/XP.Hardware.Detector/Abstractions/AreaDetectorBase.cs +++ b/XP.Hardware.Detector/Abstractions/AreaDetectorBase.cs @@ -386,6 +386,44 @@ namespace XP.Hardware.Detector.Abstractions /// public abstract DetectorInfo GetInfo(); + /// + /// 应用探测器参数 | Apply detector parameters + /// + public async Task ApplyParametersAsync(int binningIndex, int pga, decimal frameRate, CancellationToken cancellationToken = default) + { + if (Status != DetectorStatus.Ready) + { + return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}"); + } + + try + { + return await ApplyParametersInternalAsync(binningIndex, pga, frameRate, cancellationToken); + } + catch (Exception ex) + { + var errorResult = DetectorResult.Failure($"应用参数异常 | Apply parameters exception: {ex.Message}", ex); + PublishError(errorResult); + return errorResult; + } + } + + /// + /// 获取校正能力描述(子类可重写)| Get correction capabilities (subclass can override) + /// + public virtual CorrectionCapabilities GetCorrectionCapabilities() + { + return new CorrectionCapabilities(); + } + + /// + /// 显式失效校正数据(子类可重写)| Explicitly invalidate correction data (subclass can override) + /// + public virtual void InvalidateCorrectionData() + { + // 默认空实现,子类按需重写 | Default empty implementation, subclass overrides as needed + } + // 模板方法,由子类实现 | Template methods, implemented by derived classes protected abstract Task InitializeInternalAsync(CancellationToken cancellationToken); protected abstract Task StartAcquisitionInternalAsync(CancellationToken cancellationToken); @@ -395,6 +433,7 @@ namespace XP.Hardware.Detector.Abstractions protected abstract Task GainCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken); protected abstract Task AutoCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken); protected abstract Task BadPixelCorrectionInternalAsync(CancellationToken cancellationToken); + protected abstract Task ApplyParametersInternalAsync(int binningIndex, int pga, decimal frameRate, CancellationToken cancellationToken); /// /// 更新状态并发布事件 | Update status and publish event diff --git a/XP.Hardware.Detector/Abstractions/CorrectionCapabilities.cs b/XP.Hardware.Detector/Abstractions/CorrectionCapabilities.cs new file mode 100644 index 0000000..3136fb1 --- /dev/null +++ b/XP.Hardware.Detector/Abstractions/CorrectionCapabilities.cs @@ -0,0 +1,49 @@ +namespace XP.Hardware.Detector.Abstractions +{ + /// + /// 校正能力描述 | Correction capabilities description + /// 描述探测器支持的校正行为和参数范围,不同探测器可返回不同配置 + /// + public class CorrectionCapabilities + { + /// + /// 是否需要在校正前停止采集 | Whether to stop acquisition before correction + /// + public bool RequiresStopBeforeCorrection { get; set; } = true; + + /// + /// 是否需要在暗场校正前应用参数 | Whether to apply parameters before dark correction + /// + public bool RequiresParameterApplyBeforeDark { get; set; } = true; + + /// + /// 亮场校正后是否自动执行坏像素校正 | Auto bad pixel correction after gain correction + /// + public bool AutoBadPixelAfterGain { get; set; } = true; + + /// + /// 停止采集后等待时间(ms)| Post-stop delay (ms) + /// + public int PostStopDelayMs { get; set; } = 500; + + /// + /// 暗场校正帧数(从配置文件加载)| Dark correction frame count (loaded from config) + /// + public int DarkFrameCount { get; set; } = 64; + + /// + /// 亮场校正帧数(从配置文件加载)| Gain correction frame count (loaded from config) + /// + public int GainFrameCount { get; set; } = 64; + + /// + /// 校正帧数最小值 | Correction frame count minimum + /// + public int FrameCountMin { get; set; } = 1; + + /// + /// 校正帧数最大值 | Correction frame count maximum + /// + public int FrameCountMax { get; set; } = 128; + } +} diff --git a/XP.Hardware.Detector/Abstractions/IAreaDetector.cs b/XP.Hardware.Detector/Abstractions/IAreaDetector.cs index 527742d..6c10e20 100644 --- a/XP.Hardware.Detector/Abstractions/IAreaDetector.cs +++ b/XP.Hardware.Detector/Abstractions/IAreaDetector.cs @@ -85,5 +85,26 @@ namespace XP.Hardware.Detector.Abstractions /// /// 探测器信息 | Detector information DetectorInfo GetInfo(); + + /// + /// 应用探测器参数(Binning/PGA/帧率)| Apply detector parameters (Binning/PGA/FrameRate) + /// + /// Binning 索引 | Binning index + /// PGA 灵敏度值 | PGA sensitivity value + /// 帧率 | Frame rate + /// 取消令牌 | Cancellation token + /// 操作结果 | Operation result + Task ApplyParametersAsync(int binningIndex, int pga, decimal frameRate, CancellationToken cancellationToken = default); + + /// + /// 获取校正能力描述 | Get correction capabilities + /// + /// 校正能力描述 | Correction capabilities + CorrectionCapabilities GetCorrectionCapabilities(); + + /// + /// 显式失效校正数据(参数变更后调用)| Explicitly invalidate correction data (called after parameter change) + /// + void InvalidateCorrectionData(); } } diff --git a/XP.Hardware.Detector/Config/ConfigLoader.cs b/XP.Hardware.Detector/Config/ConfigLoader.cs index 5225eaf..f325b6c 100644 --- a/XP.Hardware.Detector/Config/ConfigLoader.cs +++ b/XP.Hardware.Detector/Config/ConfigLoader.cs @@ -49,6 +49,16 @@ namespace XP.Hardware.Detector.Config config.SavePath = ConfigurationManager.AppSettings["Detector:SavePath"] ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Images"); config.AutoSave = bool.TryParse(ConfigurationManager.AppSettings["Detector:AutoSave"], out var autoSave) && autoSave; + // 加载校正帧数配置(钳位到 1-128)| Load correction frame count config (clamp to 1-128) + if (int.TryParse(ConfigurationManager.AppSettings["Detector:Correction:DarkFrameCount"], out var darkFrames)) + { + config.DarkCorrectionFrameCount = Math.Clamp(darkFrames, 1, 128); + } + if (int.TryParse(ConfigurationManager.AppSettings["Detector:Correction:GainFrameCount"], out var gainFrames)) + { + config.GainCorrectionFrameCount = Math.Clamp(gainFrames, 1, 128); + } + // 验证配置 | Validate configuration var validationResult = ValidateConfiguration(config); if (!validationResult.IsSuccess) diff --git a/XP.Hardware.Detector/Config/DetectorConfig.cs b/XP.Hardware.Detector/Config/DetectorConfig.cs index d0c437d..813fe8c 100644 --- a/XP.Hardware.Detector/Config/DetectorConfig.cs +++ b/XP.Hardware.Detector/Config/DetectorConfig.cs @@ -34,6 +34,16 @@ namespace XP.Hardware.Detector.Config /// public bool AutoSave { get; set; } + /// + /// 暗场校正帧数(1-128,默认 64)| Dark correction frame count (1-128, default 64) + /// + public int DarkCorrectionFrameCount { get; set; } = 64; + + /// + /// 亮场校正帧数(1-128,默认 64)| Gain correction frame count (1-128, default 64) + /// + public int GainCorrectionFrameCount { get; set; } = 64; + /// /// 获取支持的 Binning 选项(显示名称 → 索引)| Get supported binning options (display name → index) /// 子类可重写以提供不同的选项列表 diff --git a/XP.Hardware.Detector/Config/VarexDetectorConfig.cs b/XP.Hardware.Detector/Config/VarexDetectorConfig.cs index 66230ab..f2a4c36 100644 --- a/XP.Hardware.Detector/Config/VarexDetectorConfig.cs +++ b/XP.Hardware.Detector/Config/VarexDetectorConfig.cs @@ -46,6 +46,7 @@ namespace XP.Hardware.Detector.Config /// /// Varex 支持 1×1、2×2、4×4 三种 Binning | Varex supports 1×1, 2×2, 4×4 binning + /// 注意:索引值必须与 BinningMode 枚举对齐(0=1×1, 1=2×2, 2=4×4) /// public override List GetSupportedBinnings() { @@ -53,8 +54,7 @@ namespace XP.Hardware.Detector.Config { new BinningOption("1×1", 0), new BinningOption("2×2", 1), - new BinningOption("3×3", 2), - new BinningOption("4×4", 3), + new BinningOption("4×4", 2), }; } @@ -73,8 +73,7 @@ namespace XP.Hardware.Detector.Config { 0 => 15m, // 1×1 1 => 30m, // 2×2 - 2 => 45m, // 3×3 - 3 => 60m, // 4×4 + 2 => 60m, // 4×4 _ => 15m }; @@ -86,8 +85,7 @@ namespace XP.Hardware.Detector.Config { 0 => new BinningImageSpec(0.139, 0.139, 3072, 3060), // 1×1 1 => new BinningImageSpec(0.278, 0.278, 1536, 1530), // 2×2 - 2 => new BinningImageSpec(0.417, 0.417, 1024, 1020), // 3×3 - 3 => new BinningImageSpec(0.556, 0.556, 768, 765), // 4×4 + 2 => new BinningImageSpec(0.556, 0.556, 768, 765), // 4×4 _ => new BinningImageSpec(0.139, 0.139, 3072, 3060) }; } diff --git a/XP.Hardware.Detector/Implementations/IRayDetector.cs b/XP.Hardware.Detector/Implementations/IRayDetector.cs index 1a6c501..36fcbcd 100644 --- a/XP.Hardware.Detector/Implementations/IRayDetector.cs +++ b/XP.Hardware.Detector/Implementations/IRayDetector.cs @@ -111,6 +111,15 @@ namespace XP.Hardware.Detector.Implementations throw new NotImplementedException("iRay 探测器坏像素校正尚未实现 | iRay detector bad pixel correction not implemented yet"); } + /// + /// 应用参数(内部实现)| Apply parameters (internal implementation) + /// + protected override Task ApplyParametersInternalAsync(int binningIndex, int pga, decimal frameRate, CancellationToken cancellationToken) + { + // TODO: 实现 iRay 探测器参数应用逻辑 | Implement iRay detector parameter application logic + throw new NotImplementedException("iRay 探测器参数应用尚未实现 | iRay detector parameter application not implemented yet"); + } + /// /// 获取探测器信息 | Get detector information /// diff --git a/XP.Hardware.Detector/Implementations/VarexDetector.cs b/XP.Hardware.Detector/Implementations/VarexDetector.cs index 16f7664..45419d1 100644 --- a/XP.Hardware.Detector/Implementations/VarexDetector.cs +++ b/XP.Hardware.Detector/Implementations/VarexDetector.cs @@ -54,6 +54,7 @@ namespace XP.Hardware.Detector.Implementations // 采集控制 | Acquisition Control private bool _isAcquiring = false; + private volatile bool _isCorrecting = false; // 校正中标志,回调应忽略 | Correcting flag, callback should skip private CancellationTokenSource _acquisitionCts; private readonly object _acquisitionLock = new object(); @@ -61,6 +62,10 @@ namespace XP.Hardware.Detector.Implementations private uint _cachedRows; private uint _cachedColumns; + // 暗场校正时的分辨率快照(用于亮场校正时校验一致性)| Resolution snapshot at dark correction time (for gain correction validation) + private uint _offsetBufferRows; + private uint _offsetBufferColumns; + #endregion #region 属性 | Properties @@ -399,7 +404,7 @@ namespace XP.Hardware.Detector.Implementations 0, // 跳过帧数 | Skip frames (uint)XISLApi.HIS_SEQ_CONTINUOUS, _pOffsetBuffer, // 未校正时为 IntPtr.Zero | IntPtr.Zero before calibration - _pGainAvgBuffer, // 未校正时为 IntPtr.Zero,校正后为增益映射 | IntPtr.Zero before calibration, gain map after + _pGainBuffer, // 未校正时为 IntPtr.Zero,校正后为增益数据 | IntPtr.Zero before calibration, gain data after _pCorrList); // 未校正时为 IntPtr.Zero | IntPtr.Zero before calibration if (result != XISLApi.HIS_RETURN.HIS_ALL_OK) @@ -537,7 +542,7 @@ namespace XP.Hardware.Detector.Implementations // 等待硬件完全释放采集资源,避免后续操作出现 HIS_ERROR_TIMEOUT // Wait for hardware to fully release acquisition resources to avoid HIS_ERROR_TIMEOUT on subsequent operations - Thread.Sleep(200); + Thread.Sleep(500); _logger?.Info("采集已停止 | Acquisition stopped"); return DetectorResult.Success("采集已停止 | Acquisition stopped"); @@ -597,7 +602,7 @@ namespace XP.Hardware.Detector.Implementations 0, // 跳过帧数 | Skip frames (uint)XISLApi.HIS_SEQ_ONE_BUFFER, _pOffsetBuffer, - _pGainAvgBuffer, + _pGainBuffer, _pCorrList); if (result != XISLApi.HIS_RETURN.HIS_ALL_OK) @@ -646,6 +651,7 @@ namespace XP.Hardware.Detector.Implementations try { _logger?.Info($"开始暗场校正,帧数:{frameCount} | Starting dark correction, frame count: {frameCount}"); + _isCorrecting = true; // 1. 获取探测器配置信息 | Get detector configuration uint dwFrames, dwRows, dwColumns, dwDataType, dwSortFlags; @@ -660,16 +666,41 @@ namespace XP.Hardware.Detector.Implementations { var errorMsg = $"获取探测器配置失败 | Failed to get detector configuration: {result}"; _logger?.Error(null, errorMsg + $",返回码:{(int)result}"); + _isCorrecting = false; return DetectorResult.Failure(errorMsg, null, (int)result); } - // 2. 分配暗场缓冲区(如果尚未分配或大小不匹配)| Allocate offset buffer if not allocated or size mismatch + // 2. 分配暗场缓冲区(仅在未分配或分辨率变化时重新分配)| Allocate offset buffer (only when not allocated or resolution changed) int requiredSize = (int)(dwRows * dwColumns) * sizeof(ushort); - if (_pOffsetBuffer == IntPtr.Zero) + bool needReallocOffset = _pOffsetBuffer == IntPtr.Zero + || _offsetBufferRows != dwRows + || _offsetBufferColumns != dwColumns; + + if (needReallocOffset) { + if (_pOffsetBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_pOffsetBuffer); + _pOffsetBuffer = IntPtr.Zero; + } _pOffsetBuffer = Marshal.AllocHGlobal(requiredSize); - _logger?.Debug($"已分配暗场缓冲区,大小:{requiredSize} 字节 | Allocated offset buffer, size: {requiredSize} bytes"); + if (_pOffsetBuffer == IntPtr.Zero) + { + var errorMsg = $"分配暗场缓冲区失败,所需大小:{requiredSize} 字节 | Failed to allocate offset buffer, required size: {requiredSize} bytes"; + _logger?.Error(null, errorMsg); + _isCorrecting = false; + return DetectorResult.Failure(errorMsg, null, -1); + } + _offsetBufferRows = dwRows; + _offsetBufferColumns = dwColumns; + _logger?.Debug($"已分配暗场缓冲区,大小:{requiredSize} 字节,分辨率:{dwColumns}x{dwRows} | Allocated offset buffer, size: {requiredSize} bytes, resolution: {dwColumns}x{dwRows}"); } + else + { + _logger?.Debug($"复用已有暗场缓冲区,分辨率:{dwColumns}x{dwRows} | Reusing existing offset buffer, resolution: {dwColumns}x{dwRows}"); + } + // 零初始化缓冲区 | Zero-initialize buffer + unsafe { new Span((void*)_pOffsetBuffer, requiredSize).Clear(); } // 3. 调用 XISL API 采集暗场图像 | Call XISL API to acquire offset image _logger?.Debug($"调用 Acquisition_Acquire_OffsetImage,分辨率:{dwColumns}x{dwRows} | Calling Acquisition_Acquire_OffsetImage, resolution: {dwColumns}x{dwRows}"); @@ -680,6 +711,7 @@ namespace XP.Hardware.Detector.Implementations { var errorMsg = $"采集暗场图像失败 | Failed to acquire offset image: {result}"; _logger?.Error(null, errorMsg + $",返回码:{(int)result}"); + _isCorrecting = false; return DetectorResult.Failure(errorMsg, null, (int)result); } @@ -689,6 +721,7 @@ namespace XP.Hardware.Detector.Implementations { var errorMsg = $"等待暗场采集完成超时 | Timeout waiting for offset acquisition: {waitResult}"; _logger?.Warn(errorMsg); + _isCorrecting = false; return DetectorResult.Failure(errorMsg, null, (int)waitResult); } @@ -720,10 +753,12 @@ namespace XP.Hardware.Detector.Implementations PublishCorrectionCompleted(CorrectionType.Dark, correctionResult); _logger?.Info("暗场校正完成 | Dark correction completed"); + _isCorrecting = false; return correctionResult; } catch (Exception ex) { + _isCorrecting = false; var errorMsg = $"暗场校正异常 | Dark correction exception: {ex.Message}"; _logger?.Error(ex, errorMsg); var errorResult = DetectorResult.Failure(errorMsg, ex, -1); @@ -743,6 +778,7 @@ namespace XP.Hardware.Detector.Implementations try { _logger?.Info($"开始增益校正,帧数:{frameCount} | Starting gain correction, frame count: {frameCount}"); + _isCorrecting = true; // 1. 获取探测器配置信息 | Get detector configuration uint dwFrames, dwRows, dwColumns, dwDataType, dwSortFlags; @@ -757,27 +793,64 @@ namespace XP.Hardware.Detector.Implementations { var errorMsg = $"获取探测器配置失败 | Failed to get detector configuration: {result}"; _logger?.Error(null, errorMsg + $",返回码:{(int)result}"); + _isCorrecting = false; return DetectorResult.Failure(errorMsg, null, (int)result); } - // 2. 分配增益采集缓冲区(ushort,用于 Acquire_GainImage 输入)| Allocate gain acquisition buffer (ushort, for Acquire_GainImage) - int gainAcqSize = (int)(dwRows * dwColumns) * sizeof(ushort); + // 2. 校验 _pOffsetBuffer 有效性(必须由暗场校正填充,且分辨率一致) + // Validate _pOffsetBuffer (must be filled by dark correction with matching resolution) + if (_pOffsetBuffer == IntPtr.Zero) + { + var errorMsg = "暗场缓冲区为空,请先执行暗场校正 | Offset buffer is null, please perform dark correction first"; + _logger?.Error(null, errorMsg); + _isCorrecting = false; + return DetectorResult.Failure(errorMsg, null, -1); + } + if (_offsetBufferRows != dwRows || _offsetBufferColumns != dwColumns) + { + var errorMsg = $"暗场缓冲区分辨率({_offsetBufferColumns}x{_offsetBufferRows})与当前分辨率({dwColumns}x{dwRows})不匹配,请重新执行暗场校正 | " + + $"Offset buffer resolution ({_offsetBufferColumns}x{_offsetBufferRows}) does not match current resolution ({dwColumns}x{dwRows}), please redo dark correction"; + _logger?.Error(null, errorMsg); + _isCorrecting = false; + return DetectorResult.Failure(errorMsg, null, -1); + } + + // 3. 分配增益采集缓冲区(仅在大小不匹配时重新分配,避免频繁释放/分配导致指针失效) + // 注意:SDK 内部按 DWORD (uint, 4字节) 写入增益数据,必须用 sizeof(uint) 分配! + // Note: SDK writes gain data as DWORD (uint, 4 bytes), must allocate with sizeof(uint)! + int gainAcqSize = (int)(dwRows * dwColumns) * sizeof(uint); if (_pGainBuffer == IntPtr.Zero) { _pGainBuffer = Marshal.AllocHGlobal(gainAcqSize); - _logger?.Debug($"已分配增益采集缓冲区,大小:{gainAcqSize} 字节 | Allocated gain acquisition buffer, size: {gainAcqSize} bytes"); + if (_pGainBuffer == IntPtr.Zero) + { + var errorMsg = $"分配增益采集缓冲区失败 | Failed to allocate gain buffer, size: {gainAcqSize}"; + _logger?.Error(null, errorMsg); + _isCorrecting = false; + return DetectorResult.Failure(errorMsg, null, -1); + } } + // 零初始化 | Zero-initialize + unsafe { new Span((void*)_pGainBuffer, gainAcqSize).Clear(); } + _logger?.Debug($"增益采集缓冲区就绪,大小:{gainAcqSize} 字节(uint) | Gain acquisition buffer ready, size: {gainAcqSize} bytes (uint)"); - // 分配增益映射输出缓冲区(uint/DWORD,用于 CreateGainMap 输出)| Allocate gain map output buffer (uint/DWORD, for CreateGainMap) - int gainAvgSize = (int)(dwRows * dwColumns) * sizeof(uint); - if (_pGainAvgBuffer == IntPtr.Zero) + // 4. 设置采集数据标志为 ACQ_GAIN(告知 SDK 和回调当前处于增益校正模式) + // Set acquisition data flag to ACQ_GAIN (inform SDK and callback that we're in gain correction mode) + uint acqGainFlag = 4; // ACQ_GAIN = 4 + var setAcqResult = XISLApi.Acquisition_SetAcqData(_hAcqDesc, ref acqGainFlag); + if (setAcqResult != XISLApi.HIS_RETURN.HIS_ALL_OK) { - _pGainAvgBuffer = Marshal.AllocHGlobal(gainAvgSize); - _logger?.Debug($"已分配增益映射缓冲区,大小:{gainAvgSize} 字节 | Allocated gain map buffer, size: {gainAvgSize} bytes"); + _logger?.Warn($"设置 ACQ_GAIN 标志失败:{setAcqResult}(继续执行)| Failed to set ACQ_GAIN flag: {setAcqResult} (continuing)"); } - // 3. 调用 XISL API 采集增益图像 | Call XISL API to acquire gain image - _logger?.Debug($"调用 Acquisition_Acquire_GainImage,分辨率:{dwColumns}x{dwRows} | Calling Acquisition_Acquire_GainImage, resolution: {dwColumns}x{dwRows}"); + // 6. 等待 SDK 内部完全释放采集资源(防止 Abort 后 SDK 仍持有旧缓冲区引用) + // Wait for SDK to fully release acquisition resources (prevent SDK holding stale buffer references after Abort) + Thread.Sleep(500); + + // 6. 调用 XISL API 采集增益图像 | Call XISL API to acquire gain image + _logger?.Info($"调用 Acquisition_Acquire_GainImage:分辨率={dwColumns}x{dwRows},帧数={frameCount}," + + $"pOffsetBuffer=0x{_pOffsetBuffer:X},pGainBuffer=0x{_pGainBuffer:X} | " + + $"Calling Acquisition_Acquire_GainImage: resolution={dwColumns}x{dwRows}, frames={frameCount}"); result = XISLApi.Acquisition_Acquire_GainImage( _hAcqDesc, _pOffsetBuffer, _pGainBuffer, dwRows, dwColumns, (uint)frameCount); @@ -785,6 +858,7 @@ namespace XP.Hardware.Detector.Implementations { var errorMsg = $"采集增益图像失败 | Failed to acquire gain image: {result}"; _logger?.Error(null, errorMsg + $",返回码:{(int)result}"); + _isCorrecting = false; return DetectorResult.Failure(errorMsg, null, (int)result); } @@ -794,18 +868,15 @@ namespace XP.Hardware.Detector.Implementations { var errorMsg = $"等待增益采集完成超时 | Timeout waiting for gain acquisition: {waitResult}"; _logger?.Warn(errorMsg); + _isCorrecting = false; return DetectorResult.Failure(errorMsg, null, (int)waitResult); } - // 4. 创建增益映射(输入为采集的 ushort 数据,输出为 DWORD 映射)| Create gain map (input: ushort data, output: DWORD map) - _logger?.Debug("创建增益映射 | Creating gain map"); - result = XISLApi.Acquisition_CreateGainMap(_pGainBuffer, _pGainAvgBuffer, (int)(dwRows * dwColumns), frameCount); - if (result != XISLApi.HIS_RETURN.HIS_ALL_OK) - { - var errorMsg = $"创建增益映射失败 | Failed to create gain map: {result}"; - _logger?.Error(null, errorMsg + $",返回码:{(int)result}"); - return DetectorResult.Failure(errorMsg, null, (int)result); - } + // 4. 增益校正完成(Acquisition_Acquire_GainImage 已内部完成增益计算,pGainBuffer 可直接用于实时补偿) + // Gain correction done (Acquisition_Acquire_GainImage internally computes gain, pGainBuffer can be used directly for real-time compensation) + // 注意:旧代码不调用 CreateGainMap,SDK 内部已完成增益映射计算 + // Note: Old code does not call CreateGainMap, SDK internally completes gain map calculation + _logger?.Debug("增益校正采集完成,跳过 CreateGainMap(SDK 内部已处理)| Gain correction acquisition done, skipping CreateGainMap (SDK handles internally)"); // 5. 保存增益数据到配置的存储路径 | Save gain data to configured storage path if (_config.AutoSave && !string.IsNullOrEmpty(_config.SavePath)) @@ -817,7 +888,7 @@ namespace XP.Hardware.Detector.Implementations int bufferSize = (int)(dwRows * dwColumns * sizeof(uint)); byte[] gainData = new byte[bufferSize]; - Marshal.Copy(_pGainAvgBuffer, gainData, 0, bufferSize); + Marshal.Copy(_pGainBuffer, gainData, 0, bufferSize); System.IO.File.WriteAllBytes(gainFilePath, gainData); _logger?.Info($"增益数据已保存到:{gainFilePath} | Gain data saved to: {gainFilePath}"); } @@ -835,10 +906,12 @@ namespace XP.Hardware.Detector.Implementations PublishCorrectionCompleted(CorrectionType.Gain, correctionResult); _logger?.Info("增益校正完成 | Gain correction completed"); + _isCorrecting = false; return correctionResult; } catch (Exception ex) { + _isCorrecting = false; var errorMsg = $"增益校正异常 | Gain correction exception: {ex.Message}"; _logger?.Error(ex, errorMsg); var errorResult = DetectorResult.Failure(errorMsg, ex, -1); @@ -1082,10 +1155,120 @@ namespace XP.Hardware.Detector.Implementations #endregion + #region 统一接口实现 | Unified Interface Implementations + + /// + /// 应用参数内部实现 | Apply parameters internal implementation + /// + protected override Task ApplyParametersInternalAsync(int binningIndex, int pga, decimal frameRate, CancellationToken cancellationToken) + { + return Task.Run(() => + { + try + { + _logger?.Info($"应用参数:Binning={binningIndex},PGA={pga},帧率={frameRate} | Applying parameters: Binning={binningIndex}, PGA={pga}, FrameRate={frameRate}"); + + // 设置 Binning 模式 | Set binning mode + var binningMode = (BinningMode)binningIndex; + var result = XISLApi.Acquisition_SetCameraBinningMode(_hAcqDesc, (uint)binningMode + 1); + if (result != XISLApi.HIS_RETURN.HIS_ALL_OK) + { + return DetectorResult.Failure($"设置 Binning 模式失败 | Failed to set binning mode: {result}"); + } + + // Binning 变化时失效校正数据 | Invalidate correction data on binning change + if (_binningMode != binningMode) + { + _logger?.Info($"Binning 模式从 {_binningMode} 变更为 {binningMode},校正数据已失效 | Binning changed, correction data invalidated"); + InvalidateCorrectionData(); + } + _binningMode = binningMode; + + // 设置增益模式 | Set gain mode + var gainMode = (GainMode)pga; + result = XISLApi.Acquisition_SetCameraGain(_hAcqDesc, (uint)gainMode); + if (result != XISLApi.HIS_RETURN.HIS_ALL_OK) + { + return DetectorResult.Failure($"设置增益模式失败 | Failed to set gain mode: {result}"); + } + + // PGA 变化时失效校正数据 | Invalidate correction data on PGA change + if (_gainMode != gainMode) + { + _logger?.Info($"PGA 从 {_gainMode} 变更为 {gainMode},校正数据已失效 | PGA changed, correction data invalidated"); + InvalidateCorrectionData(); + } + _gainMode = gainMode; + + // 设置曝光时间(帧率→微秒)| Set exposure time (frame rate → microseconds) + uint exposureUs = frameRate > 0 ? (uint)(1_000_000m / frameRate) : 66667; + result = XISLApi.Acquisition_SetTimerSync(_hAcqDesc, ref exposureUs); + if (result != XISLApi.HIS_RETURN.HIS_ALL_OK) + { + return DetectorResult.Failure($"设置曝光时间失败 | Failed to set exposure time: {result}"); + } + _exposureTime = exposureUs; + + _logger?.Info("参数应用成功 | Parameters applied successfully"); + return DetectorResult.Success("参数应用成功 | Parameters applied successfully"); + } + catch (Exception ex) + { + return DetectorResult.Failure($"应用参数异常 | Apply parameters exception: {ex.Message}", ex); + } + }, cancellationToken); + } + + /// + /// 获取 Varex 校正能力描述 | Get Varex correction capabilities + /// + public override CorrectionCapabilities GetCorrectionCapabilities() + { + return new CorrectionCapabilities + { + RequiresStopBeforeCorrection = true, + RequiresParameterApplyBeforeDark = true, + AutoBadPixelAfterGain = true, + PostStopDelayMs = 500, + DarkFrameCount = _config.DarkCorrectionFrameCount, + GainFrameCount = _config.GainCorrectionFrameCount, + FrameCountMin = 1, + FrameCountMax = 128 + }; + } + + /// + /// 失效校正数据(释放校正缓冲区)| Invalidate correction data (free correction buffers) + /// + public override void InvalidateCorrectionData() + { + if (_pOffsetBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_pOffsetBuffer); + _pOffsetBuffer = IntPtr.Zero; + _offsetBufferRows = 0; + _offsetBufferColumns = 0; + } + if (_pGainBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_pGainBuffer); + _pGainBuffer = IntPtr.Zero; + } + if (_pCorrList != IntPtr.Zero) + { + Marshal.FreeHGlobal(_pCorrList); + _pCorrList = IntPtr.Zero; + } + _logger?.Debug("校正数据已失效并释放 | Correction data invalidated and freed"); + } + + #endregion + #region IVarexDetector 接口实现(占位符)| IVarexDetector Interface Implementations (Placeholders) /// /// 设置 Binning 模式 | Set binning mode + /// Binning 变化后校正数据失效,需要重新校正 | Correction data becomes invalid after binning change, recalibration needed /// public Task SetBinningModeAsync(BinningMode mode) { @@ -1096,6 +1279,31 @@ namespace XP.Hardware.Detector.Implementations var result = XISLApi.Acquisition_SetCameraBinningMode(_hAcqDesc, (uint)mode + 1); if (result == XISLApi.HIS_RETURN.HIS_ALL_OK) { + // Binning 变化后,旧的校正缓冲区大小不匹配新分辨率,必须释放 + // After binning change, old correction buffers don't match new resolution, must free them + if (_binningMode != mode) + { + _logger?.Info($"Binning 模式从 {_binningMode} 变更为 {mode},校正数据已失效 | Binning mode changed from {_binningMode} to {mode}, correction data invalidated"); + + if (_pOffsetBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_pOffsetBuffer); + _pOffsetBuffer = IntPtr.Zero; + _offsetBufferRows = 0; + _offsetBufferColumns = 0; + } + if (_pGainBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_pGainBuffer); + _pGainBuffer = IntPtr.Zero; + } + if (_pCorrList != IntPtr.Zero) + { + Marshal.FreeHGlobal(_pCorrList); + _pCorrList = IntPtr.Zero; + } + } + _binningMode = mode; return DetectorResult.Success($"Binning 模式已设置为 {mode} | Binning mode set to {mode}"); } @@ -1118,6 +1326,8 @@ namespace XP.Hardware.Detector.Implementations /// /// 设置增益模式 | Set gain mode + /// PGA 变化后校正数据在物理意义上失效(噪声特性不同),需要重新校正 + /// After PGA change, correction data is physically invalid (different noise characteristics), recalibration needed /// public Task SetGainModeAsync(GainMode mode) { @@ -1128,6 +1338,28 @@ namespace XP.Hardware.Detector.Implementations var result = XISLApi.Acquisition_SetCameraGain(_hAcqDesc, (uint)mode); if (result == XISLApi.HIS_RETURN.HIS_ALL_OK) { + // PGA 变化后校正数据失效(不会崩溃,但补偿结果不正确) + // Correction data invalidated after PGA change (won't crash, but compensation results are incorrect) + if (_gainMode != mode && _pOffsetBuffer != IntPtr.Zero) + { + _logger?.Info($"PGA 从 {_gainMode} 变更为 {mode},校正数据已失效 | PGA changed from {_gainMode} to {mode}, correction data invalidated"); + Marshal.FreeHGlobal(_pOffsetBuffer); + _pOffsetBuffer = IntPtr.Zero; + _offsetBufferRows = 0; + _offsetBufferColumns = 0; + + if (_pGainBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_pGainBuffer); + _pGainBuffer = IntPtr.Zero; + } + if (_pCorrList != IntPtr.Zero) + { + Marshal.FreeHGlobal(_pCorrList); + _pCorrList = IntPtr.Zero; + } + } + _gainMode = mode; return DetectorResult.Success($"增益模式已设置为 {mode} | Gain mode set to {mode}"); } @@ -1224,6 +1456,9 @@ namespace XP.Hardware.Detector.Implementations { try { + // 校正期间跳过帧回调(校正使用独立缓冲区,_pAcqBuffer 可能无效)| Skip during correction (correction uses separate buffers, _pAcqBuffer may be invalid) + if (_isCorrecting) return; + uint dwRows = _cachedRows; uint dwColumns = _cachedColumns; int imageSize = (int)(dwRows * dwColumns); diff --git a/XP.Hardware.Detector/Resources/Resources.en-US.resx b/XP.Hardware.Detector/Resources/Resources.en-US.resx index 1b4c802..84c9ea8 100644 --- a/XP.Hardware.Detector/Resources/Resources.en-US.resx +++ b/XP.Hardware.Detector/Resources/Resources.en-US.resx @@ -145,4 +145,64 @@ Status: + + Dark Correction Confirmation + + + Please confirm the X-ray source is OFF before starting dark correction. + + + Dark Correction + + + Light Correction Confirmation + + + Please confirm the object has been removed from the detector field of view. + + + Please confirm the X-ray source is ON and stable before starting light correction. + + + Light Correction + + + Bad Pixel Correction + + + Stopping acquisition... + + + Applying parameters... + + + Acquiring dark field data (64 frames), please wait... + + + Dark correction completed + + + Acquiring light field data (64 frames), please wait... + + + Executing bad pixel correction... + + + Light and bad pixel correction completed + + + Light correction done, bad pixel correction failed + + + Detecting bad pixels... + + + Bad pixel correction completed + + + Parameter Mismatch + + + Current parameters differ from dark correction. Please redo dark correction. + diff --git a/XP.Hardware.Detector/Resources/Resources.resx b/XP.Hardware.Detector/Resources/Resources.resx index 9faad9f..39096cf 100644 --- a/XP.Hardware.Detector/Resources/Resources.resx +++ b/XP.Hardware.Detector/Resources/Resources.resx @@ -169,4 +169,84 @@ 状态: DetectorImageWindow - 探测器状态标签 | Detector status label + + 暗场校正确认 + 暗场校正确认对话框标题 | Dark correction confirmation dialog title + + + 请确认射线源已关闭,即将开始暗场校正。 + 暗场校正确认对话框消息 | Dark correction confirmation dialog message + + + 暗场校正 + 暗场校正进度窗口标题 | Dark correction progress window title + + + 亮场校正确认 + 亮场校正确认对话框标题 | Light correction confirmation dialog title + + + 请确认物体已移出探测器视野。 + 亮场校正物体确认消息 | Light correction object removal confirmation message + + + 请确认射线源已开启且稳定,即将开始亮场校正。 + 亮场校正射线源确认消息 | Light correction ray source confirmation message + + + 亮场校正 + 亮场校正进度窗口标题 | Light correction progress window title + + + 坏像素校正 + 坏像素校正进度窗口标题 | Bad pixel correction progress window title + + + 正在停止采集... + 进度提示:停止采集 | Progress: stopping acquisition + + + 正在应用参数... + 进度提示:应用参数 | Progress: applying parameters + + + 正在采集暗场数据(64帧),请等待... + 进度提示:采集暗场数据 | Progress: acquiring dark field data + + + 暗场校正完成 + 进度提示:暗场校正完成 | Progress: dark correction done + + + 正在采集亮场数据(64帧),请等待... + 进度提示:采集亮场数据 | Progress: acquiring light field data + + + 正在执行坏像素校正... + 进度提示:坏像素校正中 | Progress: bad pixel correcting + + + 亮场校正及坏像素校正完成 + 进度提示:亮场和坏像素校正完成 | Progress: light and bad pixel correction done + + + 亮场校正完成,但坏像素校正失败 + 进度提示:亮场完成但坏像素失败 | Progress: light done but bad pixel failed + + + 正在检测坏像素... + 进度提示:检测坏像素 | Progress: detecting bad pixels + + + 坏像素校正完成 + 进度提示:坏像素校正完成 | Progress: bad pixel correction done + + + 参数不一致 + 参数不一致对话框标题 | Parameter mismatch dialog title + + + 当前参数与暗场校正时不一致,请重新进行暗场校正。 + 参数不一致对话框消息 | Parameter mismatch dialog message + diff --git a/XP.Hardware.Detector/Resources/Resources.zh-CN.resx b/XP.Hardware.Detector/Resources/Resources.zh-CN.resx index d01462d..b3a2744 100644 --- a/XP.Hardware.Detector/Resources/Resources.zh-CN.resx +++ b/XP.Hardware.Detector/Resources/Resources.zh-CN.resx @@ -145,4 +145,64 @@ 状态: + + 暗场校正确认 + + + 请确认射线源已关闭,即将开始暗场校正。 + + + 暗场校正 + + + 亮场校正确认 + + + 请确认物体已移出探测器视野。 + + + 请确认射线源已开启且稳定,即将开始亮场校正。 + + + 亮场校正 + + + 坏像素校正 + + + 正在停止采集... + + + 正在应用参数... + + + 正在采集暗场数据(64帧),请等待... + + + 暗场校正完成 + + + 正在采集亮场数据(64帧),请等待... + + + 正在执行坏像素校正... + + + 亮场校正及坏像素校正完成 + + + 亮场校正完成,但坏像素校正失败 + + + 正在检测坏像素... + + + 坏像素校正完成 + + + 参数不一致 + + + 当前参数与暗场校正时不一致,请重新进行暗场校正。 + diff --git a/XP.Hardware.Detector/Resources/Resources.zh-TW.resx b/XP.Hardware.Detector/Resources/Resources.zh-TW.resx index 378001b..4c6cf06 100644 --- a/XP.Hardware.Detector/Resources/Resources.zh-TW.resx +++ b/XP.Hardware.Detector/Resources/Resources.zh-TW.resx @@ -145,4 +145,64 @@ 狀態: + + 暗場校正確認 + + + 請確認射線源已關閉,即將開始暗場校正。 + + + 暗場校正 + + + 亮場校正確認 + + + 請確認物體已移出探測器視野。 + + + 請確認射線源已開啟且穩定,即將開始亮場校正。 + + + 亮場校正 + + + 壞像素校正 + + + 正在停止採集... + + + 正在套用參數... + + + 正在採集暗場資料(64幀),請等待... + + + 暗場校正完成 + + + 正在採集亮場資料(64幀),請等待... + + + 正在執行壞像素校正... + + + 亮場校正及壞像素校正完成 + + + 亮場校正完成,但壞像素校正失敗 + + + 正在檢測壞像素... + + + 壞像素校正完成 + + + 參數不一致 + + + 當前參數與暗場校正時不一致,請重新進行暗場校正。 + diff --git a/XP.Hardware.Detector/Services/DetectorService.cs b/XP.Hardware.Detector/Services/DetectorService.cs index 715d20f..ccc4fc5 100644 --- a/XP.Hardware.Detector/Services/DetectorService.cs +++ b/XP.Hardware.Detector/Services/DetectorService.cs @@ -62,7 +62,11 @@ namespace XP.Hardware.Detector.Services { lock (_lock) { - return _detector != null && _detector.Status != DetectorStatus.Uninitialized; + if (_detector == null) return false; + var status = _detector.Status; + return status == DetectorStatus.Ready + || status == DetectorStatus.Acquiring + || status == DetectorStatus.Correcting; } } } @@ -309,12 +313,39 @@ namespace XP.Hardware.Detector.Services { _logger?.Info("服务层:执行暗场校正,帧数:{FrameCount} | Service layer: Executing dark correction, frame count: {FrameCount}", frameCount); var detector = GetDetectorOrThrow(); + + // 如果正在采集,先停止(XISL SDK 不允许采集中执行校正)| Stop acquisition first if running + bool wasAcquiring = detector.Status == DetectorStatus.Acquiring; + if (wasAcquiring) + { + _logger?.Info("探测器正在采集,先停止采集再执行暗场校正 | Detector is acquiring, stopping before dark correction"); + var stopResult = await detector.StopAcquisitionAsync(cancellationToken); + if (!stopResult.IsSuccess) + { + _lastError = stopResult; + _logger?.Error(stopResult.Exception, "停止采集失败,无法执行暗场校正:{Message} | Failed to stop acquisition, cannot perform dark correction: {Message}", stopResult.ErrorMessage); + return DetectorResult.Failure($"停止采集失败,无法执行暗场校正 | Failed to stop acquisition: {stopResult.ErrorMessage}"); + } + } + var result = await detector.DarkCorrectionAsync(frameCount, cancellationToken); if (!result.IsSuccess) { _lastError = result; _logger?.Error(result.Exception, "暗场校正失败:{Message} | Dark correction failed: {Message}", result.ErrorMessage); } + + // 如果之前在采集,恢复采集 | Resume acquisition if it was running before + if (wasAcquiring) + { + _logger?.Info("暗场校正完成,恢复连续采集 | Dark correction done, resuming continuous acquisition"); + var startResult = await detector.StartAcquisitionAsync(cancellationToken); + if (!startResult.IsSuccess) + { + _logger?.Warn("恢复采集失败:{Message} | Failed to resume acquisition: {Message}", startResult.ErrorMessage); + } + } + return result; } catch (Exception ex) @@ -339,12 +370,39 @@ namespace XP.Hardware.Detector.Services { _logger?.Info("服务层:执行亮场校正,帧数:{FrameCount} | Service layer: Executing gain correction, frame count: {FrameCount}", frameCount); var detector = GetDetectorOrThrow(); + + // 如果正在采集,先停止(XISL SDK 不允许采集中执行校正)| Stop acquisition first if running + bool wasAcquiring = detector.Status == DetectorStatus.Acquiring; + if (wasAcquiring) + { + _logger?.Info("探测器正在采集,先停止采集再执行亮场校正 | Detector is acquiring, stopping before gain correction"); + var stopResult = await detector.StopAcquisitionAsync(cancellationToken); + if (!stopResult.IsSuccess) + { + _lastError = stopResult; + _logger?.Error(stopResult.Exception, "停止采集失败,无法执行亮场校正:{Message} | Failed to stop acquisition, cannot perform gain correction: {Message}", stopResult.ErrorMessage); + return DetectorResult.Failure($"停止采集失败,无法执行亮场校正 | Failed to stop acquisition: {stopResult.ErrorMessage}"); + } + } + var result = await detector.GainCorrectionAsync(frameCount, cancellationToken); if (!result.IsSuccess) { _lastError = result; _logger?.Error(result.Exception, "亮场校正失败:{Message} | Gain correction failed: {Message}", result.ErrorMessage); } + + // 如果之前在采集,恢复采集 | Resume acquisition if it was running before + if (wasAcquiring) + { + _logger?.Info("亮场校正完成,恢复连续采集 | Gain correction done, resuming continuous acquisition"); + var startResult = await detector.StartAcquisitionAsync(cancellationToken); + if (!startResult.IsSuccess) + { + _logger?.Warn("恢复采集失败:{Message} | Failed to resume acquisition: {Message}", startResult.ErrorMessage); + } + } + return result; } catch (Exception ex) @@ -368,12 +426,39 @@ namespace XP.Hardware.Detector.Services { _logger?.Info("服务层:执行坏像素校正 | Service layer: Executing bad pixel correction"); var detector = GetDetectorOrThrow(); + + // 如果正在采集,先停止(XISL SDK 不允许采集中执行校正)| Stop acquisition first if running + bool wasAcquiring = detector.Status == DetectorStatus.Acquiring; + if (wasAcquiring) + { + _logger?.Info("探测器正在采集,先停止采集再执行坏像素校正 | Detector is acquiring, stopping before bad pixel correction"); + var stopResult = await detector.StopAcquisitionAsync(cancellationToken); + if (!stopResult.IsSuccess) + { + _lastError = stopResult; + _logger?.Error(stopResult.Exception, "停止采集失败,无法执行坏像素校正:{Message} | Failed to stop acquisition, cannot perform bad pixel correction: {Message}", stopResult.ErrorMessage); + return DetectorResult.Failure($"停止采集失败,无法执行坏像素校正 | Failed to stop acquisition: {stopResult.ErrorMessage}"); + } + } + var result = await detector.BadPixelCorrectionAsync(cancellationToken); if (!result.IsSuccess) { _lastError = result; _logger?.Error(result.Exception, "坏像素校正失败:{Message} | Bad pixel correction failed: {Message}", result.ErrorMessage); } + + // 如果之前在采集,恢复采集 | Resume acquisition if it was running before + if (wasAcquiring) + { + _logger?.Info("坏像素校正完成,恢复连续采集 | Bad pixel correction done, resuming continuous acquisition"); + var startResult = await detector.StartAcquisitionAsync(cancellationToken); + if (!startResult.IsSuccess) + { + _logger?.Warn("恢复采集失败:{Message} | Failed to resume acquisition: {Message}", startResult.ErrorMessage); + } + } + return result; } catch (Exception ex) @@ -428,39 +513,44 @@ namespace XP.Hardware.Detector.Services var detector = GetDetectorOrThrow(); - // 通过 IVarexDetector 接口下发参数 | Apply parameters via IVarexDetector interface - if (detector is IVarexDetector varexDetector) + // 如果正在采集,先停止 | Stop acquisition first if running + bool wasAcquiring = detector.Status == DetectorStatus.Acquiring; + if (wasAcquiring) { - // 设置 Binning | Set binning - var binningResult = await varexDetector.SetBinningModeAsync((BinningMode)binningIndex); - if (!binningResult.IsSuccess) + _logger?.Info("探测器正在采集,先停止采集再应用参数 | Detector is acquiring, stopping before applying parameters"); + var stopResult = await detector.StopAcquisitionAsync(cancellationToken); + if (!stopResult.IsSuccess) { - _lastError = binningResult; - return binningResult; + _lastError = stopResult; + _logger?.Error(stopResult.Exception, "停止采集失败,无法应用参数:{Message} | Failed to stop acquisition, cannot apply parameters: {Message}", stopResult.ErrorMessage); + return DetectorResult.Failure($"停止采集失败,无法应用参数 | Failed to stop acquisition, cannot apply parameters: {stopResult.ErrorMessage}"); } - - // 设置增益(PGA)| Set gain (PGA) - var gainResult = await varexDetector.SetGainModeAsync((GainMode)pga); - if (!gainResult.IsSuccess) - { - _lastError = gainResult; - return gainResult; - } - - // 设置曝光时间(帧率→微秒:1000*1000/帧率)| Set exposure time (frame rate → microseconds) - uint exposureUs = frameRate > 0 ? (uint)(1_000_000m / frameRate) : 66667; - var exposureResult = await varexDetector.SetExposureTimeAsync(exposureUs); - if (!exposureResult.IsSuccess) - { - _lastError = exposureResult; - return exposureResult; - } - - _logger?.Info("参数应用成功 | Parameters applied successfully"); - return DetectorResult.Success("参数应用成功 | Parameters applied successfully"); } - return DetectorResult.Failure("当前探测器不支持参数下发 | Current detector does not support parameter application"); + // 通过统一接口下发参数(不依赖具体探测器类型)| Apply parameters via unified interface (no dependency on specific detector type) + var result = await detector.ApplyParametersAsync(binningIndex, pga, frameRate, cancellationToken); + if (!result.IsSuccess) + { + _lastError = result; + _logger?.Error(result.Exception, "应用参数失败:{Message} | Apply parameters failed: {Message}", result.ErrorMessage); + } + else + { + _logger?.Info("参数应用成功 | Parameters applied successfully"); + } + + // 如果之前在采集,恢复采集 | Resume acquisition if it was running before + if (wasAcquiring) + { + _logger?.Info("参数应用完成,恢复连续采集 | Parameters applied, resuming continuous acquisition"); + var startResult = await detector.StartAcquisitionAsync(cancellationToken); + if (!startResult.IsSuccess) + { + _logger?.Warn("恢复采集失败:{Message}(参数已成功应用)| Failed to resume acquisition: {Message} (parameters were applied successfully)", startResult.ErrorMessage); + } + } + + return result; } catch (Exception ex) { @@ -534,6 +624,29 @@ namespace XP.Hardware.Detector.Services return null; } + /// + /// 获取当前探测器的校正能力描述 | Get correction capabilities of current detector + /// 未初始化时返回基于配置文件的默认值 + /// + public CorrectionCapabilities GetCorrectionCapabilities() + { + lock (_lock) + { + if (_detector != null) + { + return _detector.GetCorrectionCapabilities(); + } + } + + // 未初始化时从配置文件构建默认值 | Build default from config when not initialized + var config = GetCurrentConfig(); + return new CorrectionCapabilities + { + DarkFrameCount = config?.DarkCorrectionFrameCount ?? 64, + GainFrameCount = config?.GainCorrectionFrameCount ?? 64 + }; + } + /// /// 获取探测器实例或抛出异常 | Get detector instance or throw exception /// diff --git a/XP.Hardware.Detector/Services/IDetectorService.cs b/XP.Hardware.Detector/Services/IDetectorService.cs index 17c864f..d563d61 100644 --- a/XP.Hardware.Detector/Services/IDetectorService.cs +++ b/XP.Hardware.Detector/Services/IDetectorService.cs @@ -130,5 +130,11 @@ namespace XP.Hardware.Detector.Services /// /// 探测器配置,未初始化时返回 null | Detector config, null if not initialized DetectorConfig GetCurrentConfig(); + + /// + /// 获取当前探测器的校正能力描述 | Get correction capabilities of current detector + /// + /// 校正能力描述,未初始化时返回默认值 | Correction capabilities, default if not initialized + CorrectionCapabilities GetCorrectionCapabilities(); } } diff --git a/XP.Hardware.Detector/ViewModels/DetectorConfigViewModel.cs b/XP.Hardware.Detector/ViewModels/DetectorConfigViewModel.cs index 39bcb26..5ac2c47 100644 --- a/XP.Hardware.Detector/ViewModels/DetectorConfigViewModel.cs +++ b/XP.Hardware.Detector/ViewModels/DetectorConfigViewModel.cs @@ -1,11 +1,14 @@ -using System; +using System; using System.Collections.ObjectModel; using System.Linq; +using System.Windows; using Prism.Commands; using Prism.Events; using Prism.Mvvm; using XP.Common.GeneralForm.Views; +using XP.Common.Localization; using XP.Common.Logging.Interfaces; +using XP.Hardware.Detector.Abstractions; using XP.Hardware.Detector.Abstractions.Events; using XP.Hardware.Detector.Abstractions.Enums; using XP.Hardware.Detector.Config; @@ -252,6 +255,9 @@ namespace XP.Hardware.Detector.ViewModels // 从配置加载 UI 选项 | Load UI options from config LoadOptionsFromConfig(); + + // 初始化连接状态(ViewModel 可能在探测器已连接后才创建)| Initialize connection status (ViewModel may be created after detector is already connected) + IsConnected = _detectorService.IsConnected; } #endregion @@ -409,23 +415,16 @@ namespace XP.Hardware.Detector.ViewModels } /// - /// 探测器状态变更回调,用于扫描期间自动锁定/解锁参数 | Detector status changed callback + /// 探测器状态变更回调,用于同步连接状态 | Detector status changed callback for connection status sync + /// 注意:参数锁定由外部扫描流程通过 LockParameters()/UnlockParameters() 显式控制, + /// 普通预览采集不应锁定配置页面按钮 /// private void OnDetectorStatusChanged(DetectorStatus status) { - // 同步连接状态:非 Uninitialized 即视为已连接 | Sync connection status: connected if not Uninitialized - IsConnected = status != DetectorStatus.Uninitialized; - - if (status == DetectorStatus.Acquiring) - { - IsParametersLocked = true; - _logger?.Debug("探测器进入采集状态,参数已自动锁定 | Detector acquiring, parameters auto-locked"); - } - else if (status == DetectorStatus.Ready) - { - IsParametersLocked = false; - _logger?.Debug("探测器就绪,参数已自动解锁 | Detector ready, parameters auto-unlocked"); - } + // 同步连接状态:只有 Ready、Acquiring、Correcting 视为已连接 | Only Ready, Acquiring, Correcting are considered connected + IsConnected = status == DetectorStatus.Ready + || status == DetectorStatus.Acquiring + || status == DetectorStatus.Correcting; } /// @@ -479,28 +478,60 @@ namespace XP.Hardware.Detector.ViewModels RaisePropertyChanged(nameof(IsParametersEditable)); } + /// + /// 获取校正帧数(从配置加载)| Get correction frame count (loaded from config) + /// + private CorrectionCapabilities GetCorrectionCaps() => _detectorService.GetCorrectionCapabilities(); + /// /// 执行暗场校正 | Execute dark correction /// private async void ExecuteDarkCorrectionAsync() { + // 弹出用户确认对话框 | Show user confirmation dialog + var confirmResult = MessageBox.Show( + LocalizationHelper.Get("Detector_DarkCorrection_ConfirmMessage"), + LocalizationHelper.Get("Detector_DarkCorrection_ConfirmTitle"), + MessageBoxButton.OKCancel, + MessageBoxImage.Question); + + if (confirmResult != MessageBoxResult.OK) + { + _logger?.Info("用户取消暗场校正 | User cancelled dark correction"); + return; + } + var binningName = _selectedBinningIndex < BinningItems.Count ? BinningItems[_selectedBinningIndex].DisplayName : "?"; - _logger?.Info("开始暗场校正,Binning={Binning},PGA={PGA},帧率={FrameRate} | Starting dark correction", - binningName, _selectedPga, _frameRate); + _logger?.Info("开始暗场校正,Binning={Binning},PGA={PGA},帧率={FrameRate},校正帧数={FrameCount} | Starting dark correction", + binningName, _selectedPga, _frameRate, GetCorrectionCaps().DarkFrameCount); // 显示进度条窗口 | Show progress window var progressWindow = new ProgressWindow( - title: "暗场校正 | Dark Correction", - message: "正在应用参数... | Applying parameters...", + title: LocalizationHelper.Get("Detector_DarkCorrection_Title"), + message: LocalizationHelper.Get("Detector_Progress_ApplyingParameters"), isCancelable: false, logger: _logger); progressWindow.Show(); IsBusy = true; + bool wasAcquiring = false; try { - // 1. 应用参数到硬件 | Apply parameters to hardware - progressWindow.UpdateProgress("正在应用参数... | Applying parameters...", 10); + // 1. 如果正在采集,先停止(后续应用参数和校正都需要探测器空闲)| Stop acquisition if running + wasAcquiring = _detectorService.Status == DetectorStatus.Acquiring; + if (wasAcquiring) + { + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_StoppingAcquisition"), 5); + var stopResult = await _detectorService.StopAcquisitionAsync(); + if (!stopResult.IsSuccess) + { + _logger?.Error(stopResult.Exception, "停止采集失败,暗场校正中止:{Message} | Stop acquisition failed, dark correction aborted: {Message}", stopResult.ErrorMessage); + return; + } + } + + // 2. 应用参数到硬件 | Apply parameters to hardware + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_ApplyingParameters"), 10); var applyResult = await _detectorService.ApplyParametersAsync(_selectedBinningIndex, _selectedPga, _frameRate); if (!applyResult.IsSuccess) { @@ -508,13 +539,13 @@ namespace XP.Hardware.Detector.ViewModels return; } - // 2. 执行暗场校正 | Execute dark correction - progressWindow.UpdateProgress("正在采集暗场数据... | Acquiring dark field data...", 30); - var result = await _detectorService.DarkCorrectionAsync(_avgFrames); + // 3. 执行暗场校正 | Execute dark correction + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_AcquiringDarkData"), 30); + var result = await _detectorService.DarkCorrectionAsync(GetCorrectionCaps().DarkFrameCount); if (result.IsSuccess) { - progressWindow.UpdateProgress("暗场校正完成 | Dark correction completed", 100); + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_DarkCorrectionDone"), 100); RecordDarkCorrectionParameters(); DarkCorrectionDone = true; _logger?.Info("暗场校正完成 | Dark correction completed"); @@ -531,6 +562,12 @@ namespace XP.Hardware.Detector.ViewModels } finally { + // 如果之前在采集,恢复连续采集 | Resume acquisition if it was running before + if (wasAcquiring) + { + _logger?.Info("暗场校正流程结束,恢复连续采集 | Dark correction flow done, resuming continuous acquisition"); + await _detectorService.StartAcquisitionAsync(); + } IsBusy = false; progressWindow.Close(); } @@ -545,32 +582,92 @@ namespace XP.Hardware.Detector.ViewModels if (!ValidateCorrectionParametersConsistency()) { _logger?.Warn("暗场校正与亮场校正参数不一致,请重新进行暗场校正 | Parameter mismatch, please redo dark correction"); + MessageBox.Show( + LocalizationHelper.Get("Detector_ParameterMismatch_Message"), + LocalizationHelper.Get("Detector_ParameterMismatch_Title"), + MessageBoxButton.OK, + MessageBoxImage.Warning); DarkCorrectionDone = false; return; } + // 弹出确认对话框:物体移出视野 | Confirm object removed from field of view + var confirmObjectResult = MessageBox.Show( + LocalizationHelper.Get("Detector_LightCorrection_ConfirmObjectMessage"), + LocalizationHelper.Get("Detector_LightCorrection_ConfirmObjectTitle"), + MessageBoxButton.OKCancel, + MessageBoxImage.Question); + + if (confirmObjectResult != MessageBoxResult.OK) + { + _logger?.Info("用户取消亮场校正(物体确认)| User cancelled light correction (object confirmation)"); + return; + } + + // 弹出确认对话框:射线源已开启 | Confirm X-ray source is ON + var confirmRayResult = MessageBox.Show( + LocalizationHelper.Get("Detector_LightCorrection_ConfirmRayMessage"), + LocalizationHelper.Get("Detector_LightCorrection_ConfirmObjectTitle"), + MessageBoxButton.OKCancel, + MessageBoxImage.Question); + + if (confirmRayResult != MessageBoxResult.OK) + { + _logger?.Info("用户取消亮场校正(射线源确认)| User cancelled light correction (ray source confirmation)"); + return; + } + var binningName = _selectedBinningIndex < BinningItems.Count ? BinningItems[_selectedBinningIndex].DisplayName : "?"; - _logger?.Info("开始亮场校正,Binning={Binning},PGA={PGA},帧率={FrameRate} | Starting light correction", - binningName, _selectedPga, _frameRate); + _logger?.Info("开始亮场校正,Binning={Binning},PGA={PGA},帧率={FrameRate},校正帧数={FrameCount} | Starting light correction", + binningName, _selectedPga, _frameRate, GetCorrectionCaps().GainFrameCount); // 显示进度条窗口 | Show progress window var progressWindow = new ProgressWindow( - title: "亮场校正 | Light Correction", - message: "正在采集亮场数据... | Acquiring light field data...", + title: LocalizationHelper.Get("Detector_LightCorrection_Title"), + message: LocalizationHelper.Get("Detector_Progress_AcquiringLightData"), isCancelable: false, logger: _logger); progressWindow.Show(); IsBusy = true; + bool wasAcquiring = false; try { - progressWindow.UpdateProgress("正在采集亮场数据... | Acquiring light field data...", 30); - var result = await _detectorService.GainCorrectionAsync(_avgFrames); + // 0. 如果正在采集,先停止 | Stop acquisition if running + wasAcquiring = _detectorService.Status == DetectorStatus.Acquiring; + if (wasAcquiring) + { + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_StoppingAcquisition"), 5); + var stopResult = await _detectorService.StopAcquisitionAsync(); + if (!stopResult.IsSuccess) + { + _logger?.Error(stopResult.Exception, "停止采集失败,亮场校正中止:{Message} | Stop acquisition failed, light correction aborted: {Message}", stopResult.ErrorMessage); + return; + } + } + + // 1. 执行亮场校正 | Execute light correction + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_AcquiringLightData"), 20); + var result = await _detectorService.GainCorrectionAsync(GetCorrectionCaps().GainFrameCount); if (result.IsSuccess) { - progressWindow.UpdateProgress("亮场校正完成 | Light correction completed", 100); - _logger?.Info("亮场校正完成 | Light correction completed"); + _logger?.Info("亮场校正完成,开始执行坏像素校正 | Light correction completed, starting bad pixel correction"); + + // 2. 亮场校正完成后自动执行坏像素校正 | Auto execute bad pixel correction after light correction + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_BadPixelCorrecting"), 70); + var badPixelResult = await _detectorService.BadPixelCorrectionAsync(); + + if (badPixelResult.IsSuccess) + { + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_LightAndBadPixelDone"), 100); + _logger?.Info("亮场校正及坏像素校正全部完成 | Light correction and bad pixel correction all completed"); + } + else + { + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_LightDoneBadPixelFailed"), 90); + _logger?.Error(badPixelResult.Exception, "坏像素校正失败:{Message} | Bad pixel correction failed: {Message}", badPixelResult.ErrorMessage); + } } else { @@ -583,6 +680,12 @@ namespace XP.Hardware.Detector.ViewModels } finally { + // 如果之前在采集,恢复连续采集 | Resume acquisition if it was running before + if (wasAcquiring) + { + _logger?.Info("亮场校正流程结束,恢复连续采集 | Light correction flow done, resuming continuous acquisition"); + await _detectorService.StartAcquisitionAsync(); + } IsBusy = false; progressWindow.Close(); } @@ -597,8 +700,8 @@ namespace XP.Hardware.Detector.ViewModels // 显示进度条窗口 | Show progress window var progressWindow = new ProgressWindow( - title: "坏像素校正 | Bad Pixel Correction", - message: "正在检测坏像素... | Detecting bad pixels...", + title: LocalizationHelper.Get("Detector_BadPixelCorrection_Title"), + message: LocalizationHelper.Get("Detector_Progress_DetectingBadPixels"), isCancelable: false, logger: _logger); progressWindow.Show(); @@ -606,12 +709,12 @@ namespace XP.Hardware.Detector.ViewModels IsBusy = true; try { - progressWindow.UpdateProgress("正在检测坏像素... | Detecting bad pixels...", 30); + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_DetectingBadPixels"), 30); var result = await _detectorService.BadPixelCorrectionAsync(); if (result.IsSuccess) { - progressWindow.UpdateProgress("坏像素校正完成 | Bad pixel correction completed", 100); + progressWindow.UpdateProgress(LocalizationHelper.Get("Detector_Progress_BadPixelDone"), 100); _logger?.Info("坏像素校正完成 | Bad pixel correction completed"); } else diff --git a/XP.Hardware.Detector/Views/DetectorConfigView.xaml b/XP.Hardware.Detector/Views/DetectorConfigView.xaml index 0c7acd7..2a975a1 100644 --- a/XP.Hardware.Detector/Views/DetectorConfigView.xaml +++ b/XP.Hardware.Detector/Views/DetectorConfigView.xaml @@ -6,12 +6,13 @@ xmlns:prism="http://prismlibrary.com/" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" xmlns:loc="clr-namespace:XP.Common.Localization.Extensions;assembly=XP.Common" + xmlns:hist="clr-namespace:XP.Common.Controls.ImageHistogram;assembly=XP.Common" mc:Ignorable="d" - d:DesignWidth="420" d:DesignHeight="210" + d:DesignWidth="420" d:DesignHeight="360" prism:ViewModelLocator.AutoWireViewModel="True" Background="White"> - + @@ -22,6 +23,8 @@ + + @@ -94,6 +97,7 @@ @@ -112,6 +116,7 @@ @@ -126,6 +131,7 @@ @@ -136,5 +142,13 @@ + + + diff --git a/XP.Hardware.Detector/Views/DetectorConfigView.xaml.cs b/XP.Hardware.Detector/Views/DetectorConfigView.xaml.cs index 5485bc3..50d4eef 100644 --- a/XP.Hardware.Detector/Views/DetectorConfigView.xaml.cs +++ b/XP.Hardware.Detector/Views/DetectorConfigView.xaml.cs @@ -1,15 +1,82 @@ +using System; +using System.Windows; using System.Windows.Controls; +using Prism.Events; +using Prism.Ioc; +using XP.Hardware.Detector.Abstractions; +using XP.Hardware.Detector.Abstractions.Events; namespace XP.Hardware.Detector.Views { /// /// 面阵探测器配置视图 | Area detector configuration view + /// 订阅探测器图像采集事件,将图像数据传递给直方图控件 /// public partial class DetectorConfigView : UserControl { + private IEventAggregator _eventAggregator; + private SubscriptionToken _imageSubscriptionToken; + public DetectorConfigView() { InitializeComponent(); + Loaded += OnLoaded; + Unloaded += OnUnloaded; + } + + /// + /// 加载时订阅图像采集事件 | Subscribe to image captured event on load + /// + private void OnLoaded(object sender, RoutedEventArgs e) + { + try + { + _eventAggregator = ContainerLocator.Current?.Resolve(); + if (_eventAggregator != null) + { + _imageSubscriptionToken = _eventAggregator.GetEvent() + .Subscribe(OnImageCaptured, ThreadOption.BackgroundThread); + } + } + catch + { + // 事件聚合器不可用时静默降级 | Silent degradation when event aggregator unavailable + } + } + + /// + /// 卸载时取消订阅 | Unsubscribe on unload + /// + private void OnUnloaded(object sender, RoutedEventArgs e) + { + if (_eventAggregator != null && _imageSubscriptionToken != null) + { + _eventAggregator.GetEvent().Unsubscribe(_imageSubscriptionToken); + _imageSubscriptionToken = null; + } + } + + /// + /// 图像采集回调:将 ushort[] 转为 byte[] 后传给直方图控件 | Image captured callback + /// + private void OnImageCaptured(ImageCapturedEventArgs args) + { + if (args?.ImageData == null || args.Width == 0 || args.Height == 0) + return; + + try + { + // 将 ushort[] 转换为 little-endian byte[] | Convert ushort[] to little-endian byte[] + var rawBytes = new byte[args.ImageData.Length * 2]; + Buffer.BlockCopy(args.ImageData, 0, rawBytes, 0, rawBytes.Length); + + // 调用直方图控件更新(控件内部支持从非 UI 线程调用)| Update histogram control (supports non-UI thread calls) + HistogramControl?.UpdateImage(rawBytes, (int)args.Width, (int)args.Height, 16); + } + catch + { + // 异常不影响主流程 | Exception does not affect main flow + } } } } diff --git a/XP.Hardware.Detector/Views/DetectorConfigWindow.xaml b/XP.Hardware.Detector/Views/DetectorConfigWindow.xaml index 500ec99..b2830d7 100644 --- a/XP.Hardware.Detector/Views/DetectorConfigWindow.xaml +++ b/XP.Hardware.Detector/Views/DetectorConfigWindow.xaml @@ -4,8 +4,8 @@ xmlns:loc="clr-namespace:XP.Common.Localization.Extensions;assembly=XP.Common" xmlns:detectorViews="clr-namespace:XP.Hardware.Detector.Views" Title="{loc:Localization Detector_ConfigWindowTitle}" - Height="230" Width="400" - MinHeight="230" MinWidth="360" + Height="450" Width="400" + MinHeight="400" MinWidth="360" WindowStartupLocation="CenterOwner" ResizeMode="CanResize"> diff --git a/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json b/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json index 6861e9d..fb5a616 100644 --- a/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json +++ b/XP.Hardware.Detector/bin/Debug/net8.0-windows7.0/XP.Hardware.Detector.deps.json @@ -565,6 +565,14 @@ } } }, + "SixLabors.ImageSharp/3.1.12": { + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "assemblyVersion": "3.0.0.0", + "fileVersion": "3.1.12.0" + } + } + }, "SQLitePCLRaw.bundle_e_sqlite3/2.1.11": { "dependencies": { "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", @@ -1702,6 +1710,7 @@ "Serilog.Settings.Configuration": "10.0.0", "Serilog.Sinks.Console": "6.1.1", "Serilog.Sinks.File": "7.0.0", + "SixLabors.ImageSharp": "3.1.12", "Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408" }, "runtime": { @@ -2157,6 +2166,13 @@ "path": "sharpdx.mathematics/4.2.0", "hashPath": "sharpdx.mathematics.4.2.0.nupkg.sha512" }, + "SixLabors.ImageSharp/3.1.12": { + "type": "package", + "serviceable": true, + "sha512": "sha512-iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==", + "path": "sixlabors.imagesharp/3.1.12", + "hashPath": "sixlabors.imagesharp.3.1.12.nupkg.sha512" + }, "SQLitePCLRaw.bundle_e_sqlite3/2.1.11": { "type": "package", "serviceable": true, diff --git a/XP.Hardware.MotionControl/ViewModels/AxisControlViewModel.cs b/XP.Hardware.MotionControl/ViewModels/AxisControlViewModel.cs index 72df2e7..189a06e 100644 --- a/XP.Hardware.MotionControl/ViewModels/AxisControlViewModel.cs +++ b/XP.Hardware.MotionControl/ViewModels/AxisControlViewModel.cs @@ -4,7 +4,7 @@ using System.Windows; using Prism.Commands; using Prism.Events; using Prism.Mvvm; -using XP.Common.Controls; +using XP.Common.Controls.Joystick; using XP.Common.Logging.Interfaces; using XP.Hardware.MotionControl.Abstractions; using XP.Hardware.MotionControl.Abstractions.Enums; diff --git a/XP.Hardware.MotionControl/Views/AxisControlView.xaml b/XP.Hardware.MotionControl/Views/AxisControlView.xaml index eeefc55..2d21b8e 100644 --- a/XP.Hardware.MotionControl/Views/AxisControlView.xaml +++ b/XP.Hardware.MotionControl/Views/AxisControlView.xaml @@ -6,7 +6,7 @@ xmlns:loc="clr-namespace:XP.Common.Localization.Extensions;assembly=XP.Common" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" xmlns:prism="http://prismlibrary.com/" - xmlns:controls="clr-namespace:XP.Common.Controls;assembly=XP.Common" + xmlns:controls="clr-namespace:XP.Common.Controls.Joystick;assembly=XP.Common" prism:ViewModelLocator.AutoWireViewModel="True" mc:Ignorable="d" MinWidth="350" diff --git a/XP.Hardware.MotionControl/Views/AxisControlView.xaml.cs b/XP.Hardware.MotionControl/Views/AxisControlView.xaml.cs index b8fdcf3..d6b4ab8 100644 --- a/XP.Hardware.MotionControl/Views/AxisControlView.xaml.cs +++ b/XP.Hardware.MotionControl/Views/AxisControlView.xaml.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Input; -using XP.Common.Controls; +using XP.Common.Controls.Joystick; using XP.Hardware.MotionControl.ViewModels; namespace XP.Hardware.MotionControl.Views diff --git a/XplorePlane/App.config b/XplorePlane/App.config index f6a57ae..9825512 100644 --- a/XplorePlane/App.config +++ b/XplorePlane/App.config @@ -1,215 +1,212 @@ - + - - - - - - - - - - + + + + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +