已合并 PR 90: 探测器直方图和优化合并至开发分支
1、重构探测器Hardware.Detector模块,统一设备调用接口,支持多探测器兼容,优化设备连接状态判断逻辑,新增校正帧数可配置功能。 2、优化Varex探测器校正流程,修复内存缓冲区对齐问题,增加指针、分辨率有效性校验,校正期间屏蔽帧回调、自动启停采集,规避SDK冲突与程序崩溃问题。 3、开发通用图像灰度直方图控件,优化资源释放逻辑。
This commit is contained in:
@@ -71,7 +71,6 @@
|
||||
<add key="Detector:Port" value="5000" />
|
||||
<add key="Detector:SavePath" value="D:\XplorePlane\DetectorImages" />
|
||||
<add key="Detector:AutoSave" value="true" />
|
||||
<!-- Varex 探测器专属配置 | Varex Detector Specific Configuration -->
|
||||
<!-- Binning 模式: Bin1x1, Bin2x2, Bin4x4 | Binning mode: Bin1x1, Bin2x2, Bin4x4 -->
|
||||
<add key="Detector:Varex:BinningMode" value="Bin1x1" />
|
||||
<!-- 增益模式: Low, High | Gain mode: Low, High -->
|
||||
@@ -83,14 +82,13 @@
|
||||
<add key="Detector:Varex:ROI_Y" value="0" />
|
||||
<add key="Detector:Varex:ROI_Width" value="2880" />
|
||||
<add key="Detector:Varex:ROI_Height" value="2880" />
|
||||
<!-- iRay 探测器专属配置 | iRay Detector Specific Configuration -->
|
||||
<!-- 采集模式: Continuous, SingleFrame | Acquisition mode: Continuous, SingleFrame -->
|
||||
<add key="Detector:IRay:AcquisitionMode" value="Continuous" />
|
||||
<!-- 默认增益值 | Default gain value -->
|
||||
<add key="Detector:IRay:DefaultGain" value="1.0" />
|
||||
<!-- 校正配置 | Correction Configuration -->
|
||||
<add key="Detector:Correction:DarkFrameCount" value="10" />
|
||||
<add key="Detector:Correction:GainFrameCount" value="10" />
|
||||
<add key="Detector:Correction:DarkFrameCount" value="64" />
|
||||
<add key="Detector:Correction:GainFrameCount" value="64" />
|
||||
<add key="Detector:Correction:SaveCorrectionData" value="true" />
|
||||
<!-- 操作超时配置 | Operation Timeout Configuration -->
|
||||
<add key="Detector:InitializationTimeout" value="30000" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// RadChartView 渲染适配器 | RadChartView rendering adapter
|
||||
/// 负责将直方图频次数据渲染到 Telerik RadCartesianChart 控件
|
||||
/// </summary>
|
||||
internal sealed class ChartRenderer
|
||||
{
|
||||
private readonly RadCartesianChart _chart;
|
||||
private readonly ScatterAreaSeries _areaSeries;
|
||||
private readonly LinearAxis _xAxis;
|
||||
|
||||
/// <summary>
|
||||
/// 16 位数据聚合因子 | 16-bit data aggregation factor
|
||||
/// </summary>
|
||||
private const int AggregationFactor = 256;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,接收 RadCartesianChart 实例 | Constructor, receives RadCartesianChart instance
|
||||
/// </summary>
|
||||
/// <param name="chart">图表控件实例 | Chart control instance</param>
|
||||
/// <param name="areaSeries">面积图系列 | Area series</param>
|
||||
/// <param name="xAxis">X 轴 | X axis</param>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新直方图数据 | Update histogram data
|
||||
/// </summary>
|
||||
/// <param name="histogram">频次数组(256 或 65536 长度)| Frequency array (256 or 65536 length)</param>
|
||||
/// <param name="isLogarithmic">是否使用对数 Y 轴 | Whether to use logarithmic Y axis</param>
|
||||
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<HistogramDataPoint>();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空图表,恢复初始状态 | Clear chart, restore initial state
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前数据点数量 | Get current data point count
|
||||
/// </summary>
|
||||
public int DataPointCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_areaSeries.ItemsSource is ICollection<HistogramDataPoint> collection)
|
||||
return collection.Count;
|
||||
if (_areaSeries.ItemsSource is IEnumerable<HistogramDataPoint> enumerable)
|
||||
return enumerable.Count();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 65536 长度的频次数组聚合为 256 个柱体 | Aggregate 65536-length array to 256 bars
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新 Y 轴范围 | Update Y axis range
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将步长取整为"好看"的数值(1, 2, 5 的倍数 × 10^n)| Round step to "nice" value (multiples of 1, 2, 5 × 10^n)
|
||||
/// 例如:123456 → 100000,350000 → 500000,780000 → 1000000
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Y 轴范围和刻度类型 | Set Y axis range and scale type
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直方图数据点模型 | Histogram data point model
|
||||
/// </summary>
|
||||
internal class HistogramDataPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// 灰度级别(X 轴值)| Gray level (X axis value)
|
||||
/// </summary>
|
||||
public double GrayLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 像素频次(Y 轴值)| Pixel frequency (Y axis value)
|
||||
/// </summary>
|
||||
public long Frequency { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.Common.Controls.ImageHistogram
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧率限流器,确保计算频率不超过 MaxFrameRate | Frame rate throttler
|
||||
/// 支持从任意线程调用,使用 lock 保护内部状态
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 最大刷新帧率(fps),有效范围 1-60,超出范围自动钳位 | Max frame rate (fps), valid range 1-60, auto-clamped
|
||||
/// </summary>
|
||||
public int MaxFrameRate
|
||||
{
|
||||
get => _maxFrameRate;
|
||||
set => _maxFrameRate = Math.Clamp(value, 1, 60);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前帧间隔(毫秒)| Get current frame interval (ms)
|
||||
/// </summary>
|
||||
private double FrameIntervalMs => 1000.0 / _maxFrameRate;
|
||||
|
||||
/// <summary>
|
||||
/// 提交一帧计算动作 | Submit a frame compute action
|
||||
/// 若未超过帧率限制则立即执行,否则缓存最新帧并延迟触发
|
||||
/// </summary>
|
||||
/// <param name="computeAction">计算动作 | Compute action</param>
|
||||
/// <returns>是否被立即接受处理 | Whether it was immediately accepted</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行计算动作(异步,完成后检查待处理帧)| Execute compute action asynchronously
|
||||
/// </summary>
|
||||
private void ExecuteAction(Action action)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 异常不外抛 | Do not propagate exceptions
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnActionCompleted();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算动作完成后的回调 | Callback after compute action completes
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安排延迟执行(等待帧间隔到期后处理最新帧)| Schedule delayed execution
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消所有待处理任务 | Cancel all pending tasks
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_pendingAction = null;
|
||||
_delayCts?.Cancel();
|
||||
_delayCts?.Dispose();
|
||||
_delayCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放所有资源 | Dispose all resources
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_pendingAction = null;
|
||||
_delayCts?.Cancel();
|
||||
_delayCts?.Dispose();
|
||||
_delayCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace XP.Common.Controls.ImageHistogram
|
||||
{
|
||||
/// <summary>
|
||||
/// 频次标签转换器:将大数值转为 K/M 缩写格式 | Frequency label converter: converts large values to K/M abbreviation format
|
||||
/// 例如:500000 → "500K",1500000 → "1.5M",800 → "800"
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 直方图后台计算引擎 | Histogram background computation engine
|
||||
/// 负责在后台线程中执行灰度值遍历和统计计算
|
||||
/// </summary>
|
||||
internal sealed class HistogramEngine : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 单帧计算超时时间(毫秒)| Single frame computation timeout (ms)
|
||||
/// </summary>
|
||||
private const int ComputeTimeoutMs = 5000;
|
||||
|
||||
private CancellationTokenSource? _timeoutCts;
|
||||
private readonly object _lock = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 从 Image<Rgba32> 计算灰度直方图 | Compute histogram from Image<Rgba32>
|
||||
/// 使用 ITU-R BT.601 亮度公式:Gray = 0.299R + 0.587G + 0.114B
|
||||
/// </summary>
|
||||
/// <param name="image">输入图像 | Input image</param>
|
||||
/// <param name="ct">取消令牌 | Cancellation token</param>
|
||||
/// <returns>256 长度的频次数组,失败返回 null | 256-length frequency array, null on failure</returns>
|
||||
public Task<long[]?> ComputeAsync(Image<Rgba32> image, CancellationToken ct)
|
||||
{
|
||||
if (image == null)
|
||||
return Task.FromResult<long[]?>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从原始字节数组计算灰度直方图 | Compute histogram from raw byte array
|
||||
/// </summary>
|
||||
/// <param name="rawData">原始像素数据 | Raw pixel data</param>
|
||||
/// <param name="width">图像宽度 | Image width</param>
|
||||
/// <param name="height">图像高度 | Image height</param>
|
||||
/// <param name="bitDepth">位深度(8 或 16)| Bit depth (8 or 16)</param>
|
||||
/// <param name="ct">取消令牌 | Cancellation token</param>
|
||||
/// <returns>频次数组(8位:256长度,16位:65536长度),失败返回 null | Frequency array, null on failure</returns>
|
||||
public Task<long[]?> ComputeAsync(byte[] rawData, int width, int height, int bitDepth, CancellationToken ct)
|
||||
{
|
||||
// 参数有效性验证 | Parameter validation
|
||||
if (rawData == null || width <= 0 || height <= 0)
|
||||
return Task.FromResult<long[]?>(null);
|
||||
|
||||
if (bitDepth != 8 && bitDepth != 16)
|
||||
return Task.FromResult<long[]?>(null);
|
||||
|
||||
int expectedLength = bitDepth == 8 ? width * height : width * height * 2;
|
||||
if (rawData.Length != expectedLength)
|
||||
return Task.FromResult<long[]?>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 8 位灰度直方图 | Compute 8-bit grayscale histogram
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 16 位灰度直方图 | Compute 16-bit grayscale histogram
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带超时的链接取消令牌 | Create linked cancellation token with timeout
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源 | Dispose resources
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_timeoutCts?.Cancel();
|
||||
_timeoutCts?.Dispose();
|
||||
_timeoutCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<UserControl x:Class="XP.Common.Controls.ImageHistogram.ImageHistogramControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions"
|
||||
xmlns:local="clr-namespace:XP.Common.Controls.ImageHistogram"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="200" d:DesignWidth="400">
|
||||
<UserControl.Resources>
|
||||
<local:FrequencyLabelConverter x:Key="FreqConverter"/>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<!-- 图表控件 | Chart control -->
|
||||
<telerik:RadCartesianChart x:Name="HistogramChart" Padding="0">
|
||||
<!-- 禁用 Telerik 自带的无数据提示 | Disable Telerik built-in empty content -->
|
||||
<telerik:RadCartesianChart.EmptyContent>
|
||||
<TextBlock/>
|
||||
</telerik:RadCartesianChart.EmptyContent>
|
||||
<!-- X 轴:灰度级别(缩小字体,控制刻度数量,K/M 缩写)| X Axis: Gray Level -->
|
||||
<telerik:RadCartesianChart.HorizontalAxis>
|
||||
<telerik:LinearAxis x:Name="XAxis"
|
||||
Minimum="0"
|
||||
Maximum="255"
|
||||
MajorStep="64"
|
||||
FontSize="9">
|
||||
<telerik:LinearAxis.LabelTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={StaticResource FreqConverter}}"/>
|
||||
</DataTemplate>
|
||||
</telerik:LinearAxis.LabelTemplate>
|
||||
</telerik:LinearAxis>
|
||||
</telerik:RadCartesianChart.HorizontalAxis>
|
||||
|
||||
<!-- Y 轴:像素频次(K/M 缩写标签)| Y Axis: Pixel Frequency (K/M abbreviation labels) -->
|
||||
<telerik:RadCartesianChart.VerticalAxis>
|
||||
<telerik:LinearAxis x:Name="YAxis"
|
||||
Minimum="0"
|
||||
Maximum="1"
|
||||
FontSize="9">
|
||||
<telerik:LinearAxis.LabelTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={StaticResource FreqConverter}}"/>
|
||||
</DataTemplate>
|
||||
</telerik:LinearAxis.LabelTemplate>
|
||||
</telerik:LinearAxis>
|
||||
</telerik:RadCartesianChart.VerticalAxis>
|
||||
|
||||
<!-- 面积图系列(适合密集直方图数据)| Area Series (suitable for dense histogram data) -->
|
||||
<telerik:RadCartesianChart.Series>
|
||||
<telerik:ScatterAreaSeries x:Name="HistogramBarSeries"
|
||||
XValueBinding="GrayLevel"
|
||||
YValueBinding="Frequency"
|
||||
Fill="#7F2196F3"
|
||||
Stroke="#FF2196F3"
|
||||
StrokeThickness="1"/>
|
||||
</telerik:RadCartesianChart.Series>
|
||||
</telerik:RadCartesianChart>
|
||||
|
||||
<!-- 无数据提示文本(叠加在图表上方)| No data placeholder text (overlaid on chart) -->
|
||||
<TextBlock x:Name="NoDataPlaceholder"
|
||||
Text="{loc:Localization Histogram_NoData}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="#9E9E9E"
|
||||
Visibility="Visible"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 图像灰度直方图通用控件 | Image grayscale histogram control
|
||||
/// 支持单帧静态图像和高频流式图像输入,使用 Telerik RadChartView 进行可视化渲染
|
||||
/// </summary>
|
||||
public partial class ImageHistogramControl : UserControl
|
||||
{
|
||||
#region 依赖属性 | Dependency Properties
|
||||
|
||||
/// <summary>
|
||||
/// 最大刷新帧率依赖属性 | MaxFrameRate dependency property
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty MaxFrameRateProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(MaxFrameRate),
|
||||
typeof(int),
|
||||
typeof(ImageHistogramControl),
|
||||
new PropertyMetadata(15, OnMaxFrameRateChanged, CoerceMaxFrameRate));
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用对数 Y 轴依赖属性 | IsLogarithmic dependency property
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty IsLogarithmicProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(IsLogarithmic),
|
||||
typeof(bool),
|
||||
typeof(ImageHistogramControl),
|
||||
new PropertyMetadata(false));
|
||||
|
||||
/// <summary>
|
||||
/// 最大刷新帧率(fps),有效范围 1-60,默认 15 | Max frame rate (fps), valid range 1-60, default 15
|
||||
/// </summary>
|
||||
public int MaxFrameRate
|
||||
{
|
||||
get => (int)GetValue(MaxFrameRateProperty);
|
||||
set => SetValue(MaxFrameRateProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用对数 Y 轴,默认 false | Whether to use logarithmic Y axis, default false
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
public ImageHistogramControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 初始化内部组件 | Initialize internal components
|
||||
_frameThrottler = new FrameThrottler();
|
||||
_histogramEngine = new HistogramEngine();
|
||||
|
||||
// 尝试解析日志服务 | Try to resolve logger service
|
||||
try
|
||||
{
|
||||
var loggerService = ContainerLocator.Current?.Resolve<ILoggerService>();
|
||||
_logger = loggerService?.ForModule<ImageHistogramControl>();
|
||||
}
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 传入 ImageSharp 图像对象,计算并显示灰度直方图 | Update histogram from ImageSharp image
|
||||
/// </summary>
|
||||
/// <param name="image">ImageSharp 图像对象 | ImageSharp image object</param>
|
||||
public void UpdateImage(Image<Rgba32> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 传入原始像素数组,计算并显示灰度直方图 | Update histogram from raw byte array
|
||||
/// </summary>
|
||||
/// <param name="rawData">原始像素数据 | Raw pixel data</param>
|
||||
/// <param name="width">图像宽度 | Image width</param>
|
||||
/// <param name="height">图像高度 | Image height</param>
|
||||
/// <param name="bitDepth">位深度(8 或 16)| Bit depth (8 or 16)</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空直方图显示,恢复初始空白状态 | Clear histogram display, restore initial blank state
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 通过帧率限流器提交计算任务 | Submit computation through frame throttler
|
||||
/// </summary>
|
||||
private void SubmitComputation(Func<System.Threading.Tasks.Task<long[]?>> 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建取消令牌(取消上一个)| Get or create cancellation token (cancel previous)
|
||||
/// </summary>
|
||||
private CancellationToken GetOrCreateCancellationToken()
|
||||
{
|
||||
lock (_ctsLock)
|
||||
{
|
||||
_currentCts?.Cancel();
|
||||
_currentCts?.Dispose();
|
||||
_currentCts = new CancellationTokenSource();
|
||||
return _currentCts.Token;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消当前计算 | Cancel current computation
|
||||
/// </summary>
|
||||
private void CancelCurrentComputation()
|
||||
{
|
||||
lock (_ctsLock)
|
||||
{
|
||||
_currentCts?.Cancel();
|
||||
_currentCts?.Dispose();
|
||||
_currentCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unloaded 事件处理:释放所有资源 | Unloaded event handler: release all resources
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.Controls
|
||||
namespace XP.Common.Controls.Joystick
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟摇杆核心计算逻辑(纯函数,无副作用)| Virtual joystick core calculation logic (pure functions, no side effects)
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace XP.Common.Controls
|
||||
namespace XP.Common.Controls.Joystick
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟摇杆轴模式枚举 | Virtual joystick axis mode enumeration
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace XP.Common.Controls
|
||||
namespace XP.Common.Controls.Joystick
|
||||
{
|
||||
/// <summary>
|
||||
/// 鼠标按键类型枚举 | Mouse button type enumeration
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟摇杆 UserControl,提供圆形区域内的鼠标拖拽操控能力 | Virtual joystick UserControl providing mouse drag interaction within a circular area
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
<UserControl x:Class="XP.Common.Controls.VirtualJoystick"
|
||||
<UserControl x:Class="XP.Common.Controls.Joystick.VirtualJoystick"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:XP.Common.Controls"
|
||||
xmlns:local="clr-namespace:XP.Common.Controls.Joystick"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="200" d:DesignHeight="200"
|
||||
Cursor="Hand">
|
||||
@@ -1887,7 +1887,6 @@ Reprojection error: {1:F4} pixels</value>
|
||||
<data name="ChessboardImageError" xml:space="preserve">
|
||||
<value>Image{0}: {1:F4} pixels</value>
|
||||
</data>
|
||||
|
||||
<!-- EdgeLineFitProcessor -->
|
||||
<data name="EdgeLineFitProcessor_Name" xml:space="preserve">
|
||||
<value>Edge Find Line Fit</value>
|
||||
@@ -2005,4 +2004,8 @@ Reprojection error: {1:F4} pixels</value>
|
||||
<data name="EdgeCircleFitProcessor_Thickness_Desc" xml:space="preserve">
|
||||
<value>Drawing thickness for result visualization</value>
|
||||
</data>
|
||||
<data name="Histogram_NoData" xml:space="preserve">
|
||||
<value>Histogram — No data</value>
|
||||
<comment>ImageHistogramControl - Placeholder text when no image data</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1920,7 +1920,6 @@
|
||||
<data name="ChessboardImageError" xml:space="preserve">
|
||||
<value>图像{0}: {1:F4} 像素</value>
|
||||
</data>
|
||||
|
||||
<!-- EdgeLineFitProcessor -->
|
||||
<data name="EdgeLineFitProcessor_Name" xml:space="preserve">
|
||||
<value>边缘查找拟合直线</value>
|
||||
@@ -2038,4 +2037,8 @@
|
||||
<data name="EdgeCircleFitProcessor_Thickness_Desc" xml:space="preserve">
|
||||
<value>绘制结果的线条粗细</value>
|
||||
</data>
|
||||
<data name="Histogram_NoData" xml:space="preserve">
|
||||
<value>直方图 — 暂无数据</value>
|
||||
<comment>ImageHistogramControl - 无图像输入时的提示文本 | Placeholder text when no image data</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1881,7 +1881,6 @@
|
||||
<data name="ChessboardImageError" xml:space="preserve">
|
||||
<value>图像{0}: {1:F4} 像素</value>
|
||||
</data>
|
||||
|
||||
<!-- EdgeLineFitProcessor -->
|
||||
<data name="EdgeLineFitProcessor_Name" xml:space="preserve">
|
||||
<value>边缘查找拟合直线</value>
|
||||
@@ -1999,4 +1998,8 @@
|
||||
<data name="EdgeCircleFitProcessor_Thickness_Desc" xml:space="preserve">
|
||||
<value>绘制结果的线条粗细</value>
|
||||
</data>
|
||||
<data name="Histogram_NoData" xml:space="preserve">
|
||||
<value>直方图 — 暂无数据</value>
|
||||
<comment>ImageHistogramControl - 无图像输入时的提示文本 | Placeholder text when no image data</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1881,4 +1881,8 @@
|
||||
<data name="ChessboardImageError" xml:space="preserve">
|
||||
<value>图像{0}: {1:F4} 像素</value>
|
||||
</data>
|
||||
<data name="Histogram_NoData" xml:space="preserve">
|
||||
<value>直方圖 — 暫無資料</value>
|
||||
<comment>ImageHistogramControl - 無圖像輸入時的提示文字 | Placeholder text when no image data</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -27,6 +27,7 @@
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
<PackageReference Include="Telerik.UI.for.Wpf.NetCore.Xaml" Version="2024.1.408" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -386,6 +386,44 @@ namespace XP.Hardware.Detector.Abstractions
|
||||
/// </summary>
|
||||
public abstract DetectorInfo GetInfo();
|
||||
|
||||
/// <summary>
|
||||
/// 应用探测器参数 | Apply detector parameters
|
||||
/// </summary>
|
||||
public async Task<DetectorResult> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取校正能力描述(子类可重写)| Get correction capabilities (subclass can override)
|
||||
/// </summary>
|
||||
public virtual CorrectionCapabilities GetCorrectionCapabilities()
|
||||
{
|
||||
return new CorrectionCapabilities();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显式失效校正数据(子类可重写)| Explicitly invalidate correction data (subclass can override)
|
||||
/// </summary>
|
||||
public virtual void InvalidateCorrectionData()
|
||||
{
|
||||
// 默认空实现,子类按需重写 | Default empty implementation, subclass overrides as needed
|
||||
}
|
||||
|
||||
// 模板方法,由子类实现 | Template methods, implemented by derived classes
|
||||
protected abstract Task<DetectorResult> InitializeInternalAsync(CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> StartAcquisitionInternalAsync(CancellationToken cancellationToken);
|
||||
@@ -395,6 +433,7 @@ namespace XP.Hardware.Detector.Abstractions
|
||||
protected abstract Task<DetectorResult> GainCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> AutoCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> BadPixelCorrectionInternalAsync(CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> ApplyParametersInternalAsync(int binningIndex, int pga, decimal frameRate, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 更新状态并发布事件 | Update status and publish event
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace XP.Hardware.Detector.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// 校正能力描述 | Correction capabilities description
|
||||
/// 描述探测器支持的校正行为和参数范围,不同探测器可返回不同配置
|
||||
/// </summary>
|
||||
public class CorrectionCapabilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否需要在校正前停止采集 | Whether to stop acquisition before correction
|
||||
/// </summary>
|
||||
public bool RequiresStopBeforeCorrection { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否需要在暗场校正前应用参数 | Whether to apply parameters before dark correction
|
||||
/// </summary>
|
||||
public bool RequiresParameterApplyBeforeDark { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 亮场校正后是否自动执行坏像素校正 | Auto bad pixel correction after gain correction
|
||||
/// </summary>
|
||||
public bool AutoBadPixelAfterGain { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 停止采集后等待时间(ms)| Post-stop delay (ms)
|
||||
/// </summary>
|
||||
public int PostStopDelayMs { get; set; } = 500;
|
||||
|
||||
/// <summary>
|
||||
/// 暗场校正帧数(从配置文件加载)| Dark correction frame count (loaded from config)
|
||||
/// </summary>
|
||||
public int DarkFrameCount { get; set; } = 64;
|
||||
|
||||
/// <summary>
|
||||
/// 亮场校正帧数(从配置文件加载)| Gain correction frame count (loaded from config)
|
||||
/// </summary>
|
||||
public int GainFrameCount { get; set; } = 64;
|
||||
|
||||
/// <summary>
|
||||
/// 校正帧数最小值 | Correction frame count minimum
|
||||
/// </summary>
|
||||
public int FrameCountMin { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 校正帧数最大值 | Correction frame count maximum
|
||||
/// </summary>
|
||||
public int FrameCountMax { get; set; } = 128;
|
||||
}
|
||||
}
|
||||
@@ -85,5 +85,26 @@ namespace XP.Hardware.Detector.Abstractions
|
||||
/// </summary>
|
||||
/// <returns>探测器信息 | Detector information</returns>
|
||||
DetectorInfo GetInfo();
|
||||
|
||||
/// <summary>
|
||||
/// 应用探测器参数(Binning/PGA/帧率)| Apply detector parameters (Binning/PGA/FrameRate)
|
||||
/// </summary>
|
||||
/// <param name="binningIndex">Binning 索引 | Binning index</param>
|
||||
/// <param name="pga">PGA 灵敏度值 | PGA sensitivity value</param>
|
||||
/// <param name="frameRate">帧率 | Frame rate</param>
|
||||
/// <param name="cancellationToken">取消令牌 | Cancellation token</param>
|
||||
/// <returns>操作结果 | Operation result</returns>
|
||||
Task<DetectorResult> ApplyParametersAsync(int binningIndex, int pga, decimal frameRate, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取校正能力描述 | Get correction capabilities
|
||||
/// </summary>
|
||||
/// <returns>校正能力描述 | Correction capabilities</returns>
|
||||
CorrectionCapabilities GetCorrectionCapabilities();
|
||||
|
||||
/// <summary>
|
||||
/// 显式失效校正数据(参数变更后调用)| Explicitly invalidate correction data (called after parameter change)
|
||||
/// </summary>
|
||||
void InvalidateCorrectionData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -34,6 +34,16 @@ namespace XP.Hardware.Detector.Config
|
||||
/// </summary>
|
||||
public bool AutoSave { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 暗场校正帧数(1-128,默认 64)| Dark correction frame count (1-128, default 64)
|
||||
/// </summary>
|
||||
public int DarkCorrectionFrameCount { get; set; } = 64;
|
||||
|
||||
/// <summary>
|
||||
/// 亮场校正帧数(1-128,默认 64)| Gain correction frame count (1-128, default 64)
|
||||
/// </summary>
|
||||
public int GainCorrectionFrameCount { get; set; } = 64;
|
||||
|
||||
/// <summary>
|
||||
/// 获取支持的 Binning 选项(显示名称 → 索引)| Get supported binning options (display name → index)
|
||||
/// 子类可重写以提供不同的选项列表
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace XP.Hardware.Detector.Config
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
public override List<BinningOption> 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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,6 +111,15 @@ namespace XP.Hardware.Detector.Implementations
|
||||
throw new NotImplementedException("iRay 探测器坏像素校正尚未实现 | iRay detector bad pixel correction not implemented yet");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用参数(内部实现)| Apply parameters (internal implementation)
|
||||
/// </summary>
|
||||
protected override Task<DetectorResult> 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取探测器信息 | Get detector information
|
||||
/// </summary>
|
||||
|
||||
@@ -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<byte>((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<byte>((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
|
||||
|
||||
/// <summary>
|
||||
/// 应用参数内部实现 | Apply parameters internal implementation
|
||||
/// </summary>
|
||||
protected override Task<DetectorResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Varex 校正能力描述 | Get Varex correction capabilities
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 失效校正数据(释放校正缓冲区)| Invalidate correction data (free correction buffers)
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Binning 模式 | Set binning mode
|
||||
/// Binning 变化后校正数据失效,需要重新校正 | Correction data becomes invalid after binning change, recalibration needed
|
||||
/// </summary>
|
||||
public Task<DetectorResult> 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
|
||||
|
||||
/// <summary>
|
||||
/// 设置增益模式 | Set gain mode
|
||||
/// PGA 变化后校正数据在物理意义上失效(噪声特性不同),需要重新校正
|
||||
/// After PGA change, correction data is physically invalid (different noise characteristics), recalibration needed
|
||||
/// </summary>
|
||||
public Task<DetectorResult> 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);
|
||||
|
||||
@@ -145,4 +145,64 @@
|
||||
<data name="Detector_StatusLabel" xml:space="preserve">
|
||||
<value>Status:</value>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_ConfirmTitle" xml:space="preserve">
|
||||
<value>Dark Correction Confirmation</value>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_ConfirmMessage" xml:space="preserve">
|
||||
<value>Please confirm the X-ray source is OFF before starting dark correction.</value>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_Title" xml:space="preserve">
|
||||
<value>Dark Correction</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmObjectTitle" xml:space="preserve">
|
||||
<value>Light Correction Confirmation</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmObjectMessage" xml:space="preserve">
|
||||
<value>Please confirm the object has been removed from the detector field of view.</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmRayMessage" xml:space="preserve">
|
||||
<value>Please confirm the X-ray source is ON and stable before starting light correction.</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_Title" xml:space="preserve">
|
||||
<value>Light Correction</value>
|
||||
</data>
|
||||
<data name="Detector_BadPixelCorrection_Title" xml:space="preserve">
|
||||
<value>Bad Pixel Correction</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_StoppingAcquisition" xml:space="preserve">
|
||||
<value>Stopping acquisition...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_ApplyingParameters" xml:space="preserve">
|
||||
<value>Applying parameters...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_AcquiringDarkData" xml:space="preserve">
|
||||
<value>Acquiring dark field data (64 frames), please wait...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_DarkCorrectionDone" xml:space="preserve">
|
||||
<value>Dark correction completed</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_AcquiringLightData" xml:space="preserve">
|
||||
<value>Acquiring light field data (64 frames), please wait...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_BadPixelCorrecting" xml:space="preserve">
|
||||
<value>Executing bad pixel correction...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_LightAndBadPixelDone" xml:space="preserve">
|
||||
<value>Light and bad pixel correction completed</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_LightDoneBadPixelFailed" xml:space="preserve">
|
||||
<value>Light correction done, bad pixel correction failed</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_DetectingBadPixels" xml:space="preserve">
|
||||
<value>Detecting bad pixels...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_BadPixelDone" xml:space="preserve">
|
||||
<value>Bad pixel correction completed</value>
|
||||
</data>
|
||||
<data name="Detector_ParameterMismatch_Title" xml:space="preserve">
|
||||
<value>Parameter Mismatch</value>
|
||||
</data>
|
||||
<data name="Detector_ParameterMismatch_Message" xml:space="preserve">
|
||||
<value>Current parameters differ from dark correction. Please redo dark correction.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -169,4 +169,84 @@
|
||||
<value>状态:</value>
|
||||
<comment>DetectorImageWindow - 探测器状态标签 | Detector status label</comment>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_ConfirmTitle" xml:space="preserve">
|
||||
<value>暗场校正确认</value>
|
||||
<comment>暗场校正确认对话框标题 | Dark correction confirmation dialog title</comment>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_ConfirmMessage" xml:space="preserve">
|
||||
<value>请确认射线源已关闭,即将开始暗场校正。</value>
|
||||
<comment>暗场校正确认对话框消息 | Dark correction confirmation dialog message</comment>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_Title" xml:space="preserve">
|
||||
<value>暗场校正</value>
|
||||
<comment>暗场校正进度窗口标题 | Dark correction progress window title</comment>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmObjectTitle" xml:space="preserve">
|
||||
<value>亮场校正确认</value>
|
||||
<comment>亮场校正确认对话框标题 | Light correction confirmation dialog title</comment>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmObjectMessage" xml:space="preserve">
|
||||
<value>请确认物体已移出探测器视野。</value>
|
||||
<comment>亮场校正物体确认消息 | Light correction object removal confirmation message</comment>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmRayMessage" xml:space="preserve">
|
||||
<value>请确认射线源已开启且稳定,即将开始亮场校正。</value>
|
||||
<comment>亮场校正射线源确认消息 | Light correction ray source confirmation message</comment>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_Title" xml:space="preserve">
|
||||
<value>亮场校正</value>
|
||||
<comment>亮场校正进度窗口标题 | Light correction progress window title</comment>
|
||||
</data>
|
||||
<data name="Detector_BadPixelCorrection_Title" xml:space="preserve">
|
||||
<value>坏像素校正</value>
|
||||
<comment>坏像素校正进度窗口标题 | Bad pixel correction progress window title</comment>
|
||||
</data>
|
||||
<data name="Detector_Progress_StoppingAcquisition" xml:space="preserve">
|
||||
<value>正在停止采集...</value>
|
||||
<comment>进度提示:停止采集 | Progress: stopping acquisition</comment>
|
||||
</data>
|
||||
<data name="Detector_Progress_ApplyingParameters" xml:space="preserve">
|
||||
<value>正在应用参数...</value>
|
||||
<comment>进度提示:应用参数 | Progress: applying parameters</comment>
|
||||
</data>
|
||||
<data name="Detector_Progress_AcquiringDarkData" xml:space="preserve">
|
||||
<value>正在采集暗场数据(64帧),请等待...</value>
|
||||
<comment>进度提示:采集暗场数据 | Progress: acquiring dark field data</comment>
|
||||
</data>
|
||||
<data name="Detector_Progress_DarkCorrectionDone" xml:space="preserve">
|
||||
<value>暗场校正完成</value>
|
||||
<comment>进度提示:暗场校正完成 | Progress: dark correction done</comment>
|
||||
</data>
|
||||
<data name="Detector_Progress_AcquiringLightData" xml:space="preserve">
|
||||
<value>正在采集亮场数据(64帧),请等待...</value>
|
||||
<comment>进度提示:采集亮场数据 | Progress: acquiring light field data</comment>
|
||||
</data>
|
||||
<data name="Detector_Progress_BadPixelCorrecting" xml:space="preserve">
|
||||
<value>正在执行坏像素校正...</value>
|
||||
<comment>进度提示:坏像素校正中 | Progress: bad pixel correcting</comment>
|
||||
</data>
|
||||
<data name="Detector_Progress_LightAndBadPixelDone" xml:space="preserve">
|
||||
<value>亮场校正及坏像素校正完成</value>
|
||||
<comment>进度提示:亮场和坏像素校正完成 | Progress: light and bad pixel correction done</comment>
|
||||
</data>
|
||||
<data name="Detector_Progress_LightDoneBadPixelFailed" xml:space="preserve">
|
||||
<value>亮场校正完成,但坏像素校正失败</value>
|
||||
<comment>进度提示:亮场完成但坏像素失败 | Progress: light done but bad pixel failed</comment>
|
||||
</data>
|
||||
<data name="Detector_Progress_DetectingBadPixels" xml:space="preserve">
|
||||
<value>正在检测坏像素...</value>
|
||||
<comment>进度提示:检测坏像素 | Progress: detecting bad pixels</comment>
|
||||
</data>
|
||||
<data name="Detector_Progress_BadPixelDone" xml:space="preserve">
|
||||
<value>坏像素校正完成</value>
|
||||
<comment>进度提示:坏像素校正完成 | Progress: bad pixel correction done</comment>
|
||||
</data>
|
||||
<data name="Detector_ParameterMismatch_Title" xml:space="preserve">
|
||||
<value>参数不一致</value>
|
||||
<comment>参数不一致对话框标题 | Parameter mismatch dialog title</comment>
|
||||
</data>
|
||||
<data name="Detector_ParameterMismatch_Message" xml:space="preserve">
|
||||
<value>当前参数与暗场校正时不一致,请重新进行暗场校正。</value>
|
||||
<comment>参数不一致对话框消息 | Parameter mismatch dialog message</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -145,4 +145,64 @@
|
||||
<data name="Detector_StatusLabel" xml:space="preserve">
|
||||
<value>状态:</value>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_ConfirmTitle" xml:space="preserve">
|
||||
<value>暗场校正确认</value>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_ConfirmMessage" xml:space="preserve">
|
||||
<value>请确认射线源已关闭,即将开始暗场校正。</value>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_Title" xml:space="preserve">
|
||||
<value>暗场校正</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmObjectTitle" xml:space="preserve">
|
||||
<value>亮场校正确认</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmObjectMessage" xml:space="preserve">
|
||||
<value>请确认物体已移出探测器视野。</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmRayMessage" xml:space="preserve">
|
||||
<value>请确认射线源已开启且稳定,即将开始亮场校正。</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_Title" xml:space="preserve">
|
||||
<value>亮场校正</value>
|
||||
</data>
|
||||
<data name="Detector_BadPixelCorrection_Title" xml:space="preserve">
|
||||
<value>坏像素校正</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_StoppingAcquisition" xml:space="preserve">
|
||||
<value>正在停止采集...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_ApplyingParameters" xml:space="preserve">
|
||||
<value>正在应用参数...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_AcquiringDarkData" xml:space="preserve">
|
||||
<value>正在采集暗场数据(64帧),请等待...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_DarkCorrectionDone" xml:space="preserve">
|
||||
<value>暗场校正完成</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_AcquiringLightData" xml:space="preserve">
|
||||
<value>正在采集亮场数据(64帧),请等待...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_BadPixelCorrecting" xml:space="preserve">
|
||||
<value>正在执行坏像素校正...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_LightAndBadPixelDone" xml:space="preserve">
|
||||
<value>亮场校正及坏像素校正完成</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_LightDoneBadPixelFailed" xml:space="preserve">
|
||||
<value>亮场校正完成,但坏像素校正失败</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_DetectingBadPixels" xml:space="preserve">
|
||||
<value>正在检测坏像素...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_BadPixelDone" xml:space="preserve">
|
||||
<value>坏像素校正完成</value>
|
||||
</data>
|
||||
<data name="Detector_ParameterMismatch_Title" xml:space="preserve">
|
||||
<value>参数不一致</value>
|
||||
</data>
|
||||
<data name="Detector_ParameterMismatch_Message" xml:space="preserve">
|
||||
<value>当前参数与暗场校正时不一致,请重新进行暗场校正。</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -145,4 +145,64 @@
|
||||
<data name="Detector_StatusLabel" xml:space="preserve">
|
||||
<value>狀態:</value>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_ConfirmTitle" xml:space="preserve">
|
||||
<value>暗場校正確認</value>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_ConfirmMessage" xml:space="preserve">
|
||||
<value>請確認射線源已關閉,即將開始暗場校正。</value>
|
||||
</data>
|
||||
<data name="Detector_DarkCorrection_Title" xml:space="preserve">
|
||||
<value>暗場校正</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmObjectTitle" xml:space="preserve">
|
||||
<value>亮場校正確認</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmObjectMessage" xml:space="preserve">
|
||||
<value>請確認物體已移出探測器視野。</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_ConfirmRayMessage" xml:space="preserve">
|
||||
<value>請確認射線源已開啟且穩定,即將開始亮場校正。</value>
|
||||
</data>
|
||||
<data name="Detector_LightCorrection_Title" xml:space="preserve">
|
||||
<value>亮場校正</value>
|
||||
</data>
|
||||
<data name="Detector_BadPixelCorrection_Title" xml:space="preserve">
|
||||
<value>壞像素校正</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_StoppingAcquisition" xml:space="preserve">
|
||||
<value>正在停止採集...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_ApplyingParameters" xml:space="preserve">
|
||||
<value>正在套用參數...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_AcquiringDarkData" xml:space="preserve">
|
||||
<value>正在採集暗場資料(64幀),請等待...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_DarkCorrectionDone" xml:space="preserve">
|
||||
<value>暗場校正完成</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_AcquiringLightData" xml:space="preserve">
|
||||
<value>正在採集亮場資料(64幀),請等待...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_BadPixelCorrecting" xml:space="preserve">
|
||||
<value>正在執行壞像素校正...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_LightAndBadPixelDone" xml:space="preserve">
|
||||
<value>亮場校正及壞像素校正完成</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_LightDoneBadPixelFailed" xml:space="preserve">
|
||||
<value>亮場校正完成,但壞像素校正失敗</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_DetectingBadPixels" xml:space="preserve">
|
||||
<value>正在檢測壞像素...</value>
|
||||
</data>
|
||||
<data name="Detector_Progress_BadPixelDone" xml:space="preserve">
|
||||
<value>壞像素校正完成</value>
|
||||
</data>
|
||||
<data name="Detector_ParameterMismatch_Title" xml:space="preserve">
|
||||
<value>參數不一致</value>
|
||||
</data>
|
||||
<data name="Detector_ParameterMismatch_Message" xml:space="preserve">
|
||||
<value>當前參數與暗場校正時不一致,請重新進行暗場校正。</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前探测器的校正能力描述 | Get correction capabilities of current detector
|
||||
/// 未初始化时返回基于配置文件的默认值
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取探测器实例或抛出异常 | Get detector instance or throw exception
|
||||
/// </summary>
|
||||
|
||||
@@ -130,5 +130,11 @@ namespace XP.Hardware.Detector.Services
|
||||
/// </summary>
|
||||
/// <returns>探测器配置,未初始化时返回 null | Detector config, null if not initialized</returns>
|
||||
DetectorConfig GetCurrentConfig();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前探测器的校正能力描述 | Get correction capabilities of current detector
|
||||
/// </summary>
|
||||
/// <returns>校正能力描述,未初始化时返回默认值 | Correction capabilities, default if not initialized</returns>
|
||||
CorrectionCapabilities GetCorrectionCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 探测器状态变更回调,用于扫描期间自动锁定/解锁参数 | Detector status changed callback
|
||||
/// 探测器状态变更回调,用于同步连接状态 | Detector status changed callback for connection status sync
|
||||
/// 注意:参数锁定由外部扫描流程通过 LockParameters()/UnlockParameters() 显式控制,
|
||||
/// 普通预览采集不应锁定配置页面按钮
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -479,28 +478,60 @@ namespace XP.Hardware.Detector.ViewModels
|
||||
RaisePropertyChanged(nameof(IsParametersEditable));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取校正帧数(从配置加载)| Get correction frame count (loaded from config)
|
||||
/// </summary>
|
||||
private CorrectionCapabilities GetCorrectionCaps() => _detectorService.GetCorrectionCapabilities();
|
||||
|
||||
/// <summary>
|
||||
/// 执行暗场校正 | Execute dark correction
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
@@ -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">
|
||||
|
||||
<Grid Margin="15">
|
||||
<Grid Margin="15 15 15 5">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="10"/>
|
||||
@@ -22,6 +23,8 @@
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="10"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 第1行:灵敏度 + 帧率 | Row 1: Sensitivity (PGA) + Frame rate -->
|
||||
@@ -94,6 +97,7 @@
|
||||
<telerik:RadButton Grid.Column="1"
|
||||
Content="{loc:Localization Detector_ApplyParametersButton}"
|
||||
Command="{Binding ApplyParametersCommand}"
|
||||
IsEnabled="{Binding IsParametersEditable}"
|
||||
Height="32" HorizontalAlignment="Stretch"
|
||||
telerik:StyleManager.Theme="Crystal" Margin="4 0 0 0"/>
|
||||
</Grid>
|
||||
@@ -112,6 +116,7 @@
|
||||
<telerik:RadButton Grid.Column="0"
|
||||
Content="{loc:Localization Detector_DarkCorrectionButton}"
|
||||
Command="{Binding DarkCorrectionCommand}"
|
||||
IsEnabled="{Binding IsParametersEditable}"
|
||||
Height="32" Margin="0 0 4 0" HorizontalAlignment="Stretch"
|
||||
telerik:StyleManager.Theme="Crystal">
|
||||
<telerik:RadButton.Background>
|
||||
@@ -126,6 +131,7 @@
|
||||
<telerik:RadButton Grid.Column="1"
|
||||
Content="{loc:Localization Detector_LightCorrectionButton}"
|
||||
Command="{Binding LightCorrectionCommand}"
|
||||
IsEnabled="{Binding IsParametersEditable}"
|
||||
Height="32" Margin="4 0 0 0" HorizontalAlignment="Stretch"
|
||||
telerik:StyleManager.Theme="Crystal">
|
||||
<telerik:RadButton.Background>
|
||||
@@ -136,5 +142,13 @@
|
||||
</telerik:RadButton.Background>
|
||||
</telerik:RadButton>
|
||||
</Grid>
|
||||
|
||||
<!-- 第6行:图像灰度直方图(填充剩余空间)| Row 6: Image grayscale histogram (fill remaining space) -->
|
||||
<hist:ImageHistogramControl x:Name="HistogramControl"
|
||||
Grid.Row="10"
|
||||
MaxFrameRate="10"
|
||||
IsLogarithmic="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 面阵探测器配置视图 | Area detector configuration view
|
||||
/// 订阅探测器图像采集事件,将图像数据传递给直方图控件
|
||||
/// </summary>
|
||||
public partial class DetectorConfigView : UserControl
|
||||
{
|
||||
private IEventAggregator _eventAggregator;
|
||||
private SubscriptionToken _imageSubscriptionToken;
|
||||
|
||||
public DetectorConfigView()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnLoaded;
|
||||
Unloaded += OnUnloaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载时订阅图像采集事件 | Subscribe to image captured event on load
|
||||
/// </summary>
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_eventAggregator = ContainerLocator.Current?.Resolve<IEventAggregator>();
|
||||
if (_eventAggregator != null)
|
||||
{
|
||||
_imageSubscriptionToken = _eventAggregator.GetEvent<ImageCapturedEvent>()
|
||||
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 事件聚合器不可用时静默降级 | Silent degradation when event aggregator unavailable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 卸载时取消订阅 | Unsubscribe on unload
|
||||
/// </summary>
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_eventAggregator != null && _imageSubscriptionToken != null)
|
||||
{
|
||||
_eventAggregator.GetEvent<ImageCapturedEvent>().Unsubscribe(_imageSubscriptionToken);
|
||||
_imageSubscriptionToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 图像采集回调:将 ushort[] 转为 byte[] 后传给直方图控件 | Image captured callback
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
<Grid>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
+202
-205
@@ -1,215 +1,212 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<appSettings>
|
||||
<!-- 语言配置 可选值: ZhCN, ZhTW, EnUS| Language Configuration -->
|
||||
<add key="Language" value="ZhCN" />
|
||||
<add key="XpData:RootPath" value="D:\XPData" />
|
||||
<add key="UserManual" value="UserManual.pdf" />
|
||||
<add key="DeviceId" value="PlanerCT001" />
|
||||
<configSections>
|
||||
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<appSettings>
|
||||
<!-- 语言配置 可选值: ZhCN, ZhTW, EnUS| Language Configuration -->
|
||||
<add key="Language" value="ZhCN" />
|
||||
<add key="XpData:RootPath" value="D:\XPData" />
|
||||
<add key="UserManual" value="UserManual.pdf" />
|
||||
<add key="DeviceId" value="PlanerCT001" />
|
||||
|
||||
<!-- 授权配置 | License configuration -->
|
||||
<add key="License:LicenseMode" value="885" />
|
||||
<add key="License:ModuleId" value="4" />
|
||||
<add key="License:UseSma" value="false" />
|
||||
<add key="License:LicenseState" value="20" />
|
||||
<!-- 授权配置 | License configuration -->
|
||||
<add key="License:LicenseMode" value="885" />
|
||||
<add key="License:ModuleId" value="4" />
|
||||
<add key="License:UseSma" value="false" />
|
||||
<add key="License:LicenseState" value="20" />
|
||||
|
||||
<!-- Serilog日志配置 -->
|
||||
<add key="Serilog:LogPath" value="D:\XplorePlane\Logs" />
|
||||
<add key="Serilog:MinimumLevel" value="Debug" />
|
||||
<add key="Serilog:EnableConsole" value="true" />
|
||||
<add key="Serilog:RollingInterval" value="Day" />
|
||||
<add key="Serilog:FileSizeLimitMB" value="100" />
|
||||
<add key="Serilog:RetainedFileCountLimit" value="365" />
|
||||
<!-- Serilog日志配置 -->
|
||||
<add key="Serilog:LogPath" value="D:\XplorePlane\Logs" />
|
||||
<add key="Serilog:MinimumLevel" value="Debug" />
|
||||
<add key="Serilog:EnableConsole" value="true" />
|
||||
<add key="Serilog:RollingInterval" value="Day" />
|
||||
<add key="Serilog:FileSizeLimitMB" value="100" />
|
||||
<add key="Serilog:RetainedFileCountLimit" value="365" />
|
||||
|
||||
<!-- 数据库SQLite配置 -->
|
||||
<add key="Sqlite:DbFilePath" value="D:\XplorePlane\DataBase\XP.db" />
|
||||
<add key="Sqlite:ConnectionTimeout" value="10" />
|
||||
<add key="Sqlite:CreateIfNotExists" value="true" />
|
||||
<!-- 是否启用SQLite WAL模式(提升并发读写性能,默认true) -->
|
||||
<add key="Sqlite:EnableWalMode" value="true" />
|
||||
<!-- 是否开启SQL操作日志(记录所有执行的SQL语句,默认false) -->
|
||||
<add key="Sqlite:EnableSqlLogging" value="false" />
|
||||
<!-- 数据库SQLite配置 -->
|
||||
<add key="Sqlite:DbFilePath" value="D:\XplorePlane\DataBase\XP.db" />
|
||||
<add key="Sqlite:ConnectionTimeout" value="10" />
|
||||
<add key="Sqlite:CreateIfNotExists" value="true" />
|
||||
<!-- 是否启用SQLite WAL模式(提升并发读写性能,默认true) -->
|
||||
<add key="Sqlite:EnableWalMode" value="true" />
|
||||
<!-- 是否开启SQL操作日志(记录所有执行的SQL语句,默认false) -->
|
||||
<add key="Sqlite:EnableSqlLogging" value="false" />
|
||||
|
||||
<!-- 射线源配置 -->
|
||||
<!-- 射线源类型 | Ray Source Type -->
|
||||
<!-- 可选值: Comet225 | Available values: Comet225 -->
|
||||
<add key="RaySource:SourceType" value="Comet225" />
|
||||
<add key="RaySource:SerialNumber" value="SN08602861" />
|
||||
<add key="RaySource:TotalLifeThreshold" value="10" />
|
||||
<!-- PVI通讯参数 | PVI Communication Parameters -->
|
||||
<add key="RaySource:PlcIpAddress" value="192.168.12.10" />
|
||||
<add key="RaySource:PlcPort" value="11159" />
|
||||
<add key="RaySource:PortNumber" value="11" />
|
||||
<add key="RaySource:StationNumber" value="1" />
|
||||
<add key="RaySource:CpuName" value="cpu" />
|
||||
<add key="RaySource:ConnectionTimeout" value="30000" />
|
||||
<!-- 硬件参数范围 | Hardware Parameter Ranges -->
|
||||
<add key="RaySource:MinVoltage" value="20" />
|
||||
<add key="RaySource:MaxVoltage" value="225" />
|
||||
<add key="RaySource:MinCurrent" value="10" />
|
||||
<add key="RaySource:MaxCurrent" value="1440" />
|
||||
<!-- 外部程序配置 | External Program Configuration -->
|
||||
<add key="RaySource:AdvanceExePath" value="C:\Program Files (x86)\Feinfocus\FXEControl_3.1.1.65\FXEControl.exe" />
|
||||
<!-- 操作超时配置 | Operation Timeout Configuration -->
|
||||
<add key="RaySource:RaySource:InitializationTimeout" value="30000" />
|
||||
<add key="RaySource:WarmUpTimeout" value="600000" />
|
||||
<add key="RaySource:StartUpTimeout" value="1800000" />
|
||||
<add key="RaySource:AutoCenterTimeout" value="1200000" />
|
||||
<add key="RaySource:FilamentAdjustTimeout" value="1200000" />
|
||||
<add key="RaySource:GeneralOperationTimeout" value="100000" />
|
||||
<!-- 射线源配置 -->
|
||||
<!-- 射线源类型 | Ray Source Type -->
|
||||
<!-- 可选值: Comet225 | Available values: Comet225 -->
|
||||
<add key="RaySource:SourceType" value="Comet225" />
|
||||
<add key="RaySource:SerialNumber" value="SN08602861" />
|
||||
<add key="RaySource:TotalLifeThreshold" value="10" />
|
||||
<!-- PVI通讯参数 | PVI Communication Parameters -->
|
||||
<add key="RaySource:PlcIpAddress" value="192.168.12.10" />
|
||||
<add key="RaySource:PlcPort" value="11159" />
|
||||
<add key="RaySource:PortNumber" value="11" />
|
||||
<add key="RaySource:StationNumber" value="1" />
|
||||
<add key="RaySource:CpuName" value="cpu" />
|
||||
<add key="RaySource:ConnectionTimeout" value="30000" />
|
||||
<!-- 硬件参数范围 | Hardware Parameter Ranges -->
|
||||
<add key="RaySource:MinVoltage" value="20" />
|
||||
<add key="RaySource:MaxVoltage" value="225" />
|
||||
<add key="RaySource:MinCurrent" value="10" />
|
||||
<add key="RaySource:MaxCurrent" value="1440" />
|
||||
<!-- 外部程序配置 | External Program Configuration -->
|
||||
<add key="RaySource:AdvanceExePath" value="C:\Program Files (x86)\Feinfocus\FXEControl_3.1.1.65\FXEControl.exe" />
|
||||
<!-- 操作超时配置 | Operation Timeout Configuration -->
|
||||
<add key="RaySource:RaySource:InitializationTimeout" value="30000" />
|
||||
<add key="RaySource:WarmUpTimeout" value="600000" />
|
||||
<add key="RaySource:StartUpTimeout" value="1800000" />
|
||||
<add key="RaySource:AutoCenterTimeout" value="1200000" />
|
||||
<add key="RaySource:FilamentAdjustTimeout" value="1200000" />
|
||||
<add key="RaySource:GeneralOperationTimeout" value="100000" />
|
||||
|
||||
<!-- 探测器配置 -->
|
||||
<!-- 探测器类型 | Detector Type -->
|
||||
<!-- 可选值: Varex, IRay, Hamamatsu, Simulated | Available values: Varex, IRay, Hamamatsu, Simulated -->
|
||||
<!-- 切换为 Simulated 可在无硬件环境下验证图像采集链路 | Switch to Simulated to verify image chain without hardware -->
|
||||
<add key="Detector:Type" value="Simulated" />
|
||||
<!-- 通用配置 | Common Configuration -->
|
||||
<add key="Detector:IP" value="192.168.1.200" />
|
||||
<add key="Detector:Port" value="5000" />
|
||||
<add key="Detector:SavePath" value="D:\XplorePlane\DetectorImages" />
|
||||
<add key="Detector:AutoSave" value="true" />
|
||||
<!-- Simulated 探测器专属配置 | Simulated Detector Specific Configuration -->
|
||||
<add key="Detector:Simulated:Width" value="256" />
|
||||
<add key="Detector:Simulated:Height" value="256" />
|
||||
<add key="Detector:Simulated:FrameRateFps" value="5" />
|
||||
<!-- Varex 探测器专属配置 | Varex Detector Specific Configuration -->
|
||||
<!-- Binning 模式: Bin1x1, Bin2x2, Bin4x4 | Binning mode: Bin1x1, Bin2x2, Bin4x4 -->
|
||||
<add key="Detector:Varex:BinningMode" value="Bin1x1" />
|
||||
<!-- 增益模式: Low, High | Gain mode: Low, High -->
|
||||
<add key="Detector:Varex:GainMode" value="High" />
|
||||
<!-- 曝光时间(毫秒)| Exposure time (milliseconds) -->
|
||||
<add key="Detector:Varex:ExposureTime" value="100" />
|
||||
<!-- ROI 区域 | ROI Region -->
|
||||
<add key="Detector:Varex:ROI_X" value="0" />
|
||||
<add key="Detector:Varex:ROI_Y" value="0" />
|
||||
<add key="Detector:Varex:ROI_Width" value="2880" />
|
||||
<add key="Detector:Varex:ROI_Height" value="2880" />
|
||||
<!-- iRay 探测器专属配置 | iRay Detector Specific Configuration -->
|
||||
<!-- 采集模式: Continuous, SingleFrame | Acquisition mode: Continuous, SingleFrame -->
|
||||
<add key="Detector:IRay:AcquisitionMode" value="Continuous" />
|
||||
<!-- 默认增益值 | Default gain value -->
|
||||
<add key="Detector:IRay:DefaultGain" value="1.0" />
|
||||
<!-- 校正配置 | Correction Configuration -->
|
||||
<add key="Detector:Correction:DarkFrameCount" value="10" />
|
||||
<add key="Detector:Correction:GainFrameCount" value="10" />
|
||||
<add key="Detector:Correction:SaveCorrectionData" value="true" />
|
||||
<!-- 操作超时配置 | Operation Timeout Configuration -->
|
||||
<add key="Detector:InitializationTimeout" value="30000" />
|
||||
<add key="Detector:AcquisitionTimeout" value="10000" />
|
||||
<add key="Detector:CorrectionTimeout" value="60000" />
|
||||
<!-- 主界面实时图像与探测器帧流水线 -->
|
||||
<add key="MainViewport:RealtimeEnabledDefault" value="true" />
|
||||
<add key="DetectorPipeline:AcquireQueueCapacity" value="16" />
|
||||
<add key="DetectorPipeline:ProcessQueueCapacity" value="8" />
|
||||
<add key="DetectorPipeline:ProcessEveryNFrames" value="1" />
|
||||
<!-- 探测器配置 -->
|
||||
<!-- 探测器类型 | Detector Type -->
|
||||
<!-- 可选值: Varex, IRay, Hamamatsu | Available values: Varex, IRay, Hamamatsu -->
|
||||
<add key="Detector:Type" value="Varex" />
|
||||
<!-- 通用配置 | Common Configuration -->
|
||||
<add key="Detector:IP" value="192.168.1.200" />
|
||||
<add key="Detector:Port" value="5000" />
|
||||
<add key="Detector:SavePath" value="D:\XplorePlane\DetectorImages" />
|
||||
<add key="Detector:AutoSave" value="true" />
|
||||
<!-- Simulated 探测器专属配置 | Simulated Detector Specific Configuration -->
|
||||
<add key="Detector:Simulated:Width" value="256" />
|
||||
<add key="Detector:Simulated:Height" value="256" />
|
||||
<add key="Detector:Simulated:FrameRateFps" value="5" />
|
||||
<!-- Binning 模式: Bin1x1, Bin2x2, Bin4x4 | Binning mode: Bin1x1, Bin2x2, Bin4x4 -->
|
||||
<add key="Detector:Varex:BinningMode" value="Bin1x1" />
|
||||
<!-- 增益模式: Low, High | Gain mode: Low, High -->
|
||||
<add key="Detector:Varex:GainMode" value="High" />
|
||||
<!-- 曝光时间(毫秒)| Exposure time (milliseconds) -->
|
||||
<add key="Detector:Varex:ExposureTime" value="100" />
|
||||
<!-- ROI 区域 | ROI Region -->
|
||||
<add key="Detector:Varex:ROI_X" value="0" />
|
||||
<add key="Detector:Varex:ROI_Y" value="0" />
|
||||
<add key="Detector:Varex:ROI_Width" value="2880" />
|
||||
<add key="Detector:Varex:ROI_Height" value="2880" />
|
||||
<!-- 采集模式: Continuous, SingleFrame | Acquisition mode: Continuous, SingleFrame -->
|
||||
<add key="Detector:IRay:AcquisitionMode" value="Continuous" />
|
||||
<!-- 默认增益值 | Default gain value -->
|
||||
<add key="Detector:IRay:DefaultGain" value="1.0" />
|
||||
<!-- 校正配置 | Correction Configuration -->
|
||||
<add key="Detector:Correction:DarkFrameCount" value="64" />
|
||||
<add key="Detector:Correction:GainFrameCount" value="64" />
|
||||
<add key="Detector:Correction:SaveCorrectionData" value="true" />
|
||||
<!-- 操作超时配置 | Operation Timeout Configuration -->
|
||||
<add key="Detector:InitializationTimeout" value="30000" />
|
||||
<add key="Detector:AcquisitionTimeout" value="10000" />
|
||||
<add key="Detector:CorrectionTimeout" value="60000" />
|
||||
<!-- 主界面实时图像与探测器帧流水线 -->
|
||||
<add key="MainViewport:RealtimeEnabledDefault" value="true" />
|
||||
<add key="DetectorPipeline:AcquireQueueCapacity" value="16" />
|
||||
<add key="DetectorPipeline:ProcessQueueCapacity" value="8" />
|
||||
<add key="DetectorPipeline:ProcessEveryNFrames" value="1" />
|
||||
|
||||
<!-- Dump 配置 | Dump Configuration -->
|
||||
<add key="Dump:StoragePath" value="D:\XplorePlane\Dump" />
|
||||
<add key="Dump:EnableScheduledDump" value="false" />
|
||||
<add key="Dump:ScheduledIntervalMinutes" value="60" />
|
||||
<add key="Dump:MiniDumpSizeLimitMB" value="100" />
|
||||
<add key="Dump:RetentionDays" value="7" />
|
||||
<!-- Dump 配置 | Dump Configuration -->
|
||||
<add key="Dump:StoragePath" value="D:\XplorePlane\Dump" />
|
||||
<add key="Dump:EnableScheduledDump" value="false" />
|
||||
<add key="Dump:ScheduledIntervalMinutes" value="60" />
|
||||
<add key="Dump:MiniDumpSizeLimitMB" value="100" />
|
||||
<add key="Dump:RetentionDays" value="7" />
|
||||
|
||||
<!-- PLC 配置 | PLC Configuration -->
|
||||
<add key="Plc:IpAddress" value="192.168.0.1" />
|
||||
<add key="Plc:Port" value="102" />
|
||||
<add key="Plc:Rack" value="0" />
|
||||
<add key="Plc:Slot" value="1" />
|
||||
<!-- PlcType可选值: S200Smart, S300, S400, S1200, S1500 | PlcType Available values: S200Smart, S300, S400, S1200, S1500 -->
|
||||
<add key="Plc:PlcType" value="S1200" />
|
||||
<!-- 数据块配置 | Data Block Configuration -->
|
||||
<add key="Plc:ReadDbBlock" value="DB31" />
|
||||
<add key="Plc:WriteDbBlock" value="DB31" />
|
||||
<add key="Plc:ReadStartAddress" value="0" />
|
||||
<add key="Plc:ReadLength" value="200" />
|
||||
<!-- 批量读取周期(毫秒)| Bulk read interval (ms) -->
|
||||
<add key="Plc:BulkReadIntervalMs" value="250" />
|
||||
<!-- 超时配置 | Timeout Configuration -->
|
||||
<add key="Plc:ConnectTimeoutMs" value="3000" />
|
||||
<add key="Plc:ReadTimeoutMs" value="1000" />
|
||||
<add key="Plc:WriteTimeoutMs" value="1000" />
|
||||
<!-- 自动重连 | Auto Reconnection -->
|
||||
<add key="Plc:bReConnect" value="true" />
|
||||
<!-- PLC 配置 | PLC Configuration -->
|
||||
<add key="Plc:IpAddress" value="192.168.0.1" />
|
||||
<add key="Plc:Port" value="102" />
|
||||
<add key="Plc:Rack" value="0" />
|
||||
<add key="Plc:Slot" value="1" />
|
||||
<!-- PlcType可选值: S200Smart, S300, S400, S1200, S1500 | PlcType Available values: S200Smart, S300, S400, S1200, S1500 -->
|
||||
<add key="Plc:PlcType" value="S1200" />
|
||||
<!-- 数据块配置 | Data Block Configuration -->
|
||||
<add key="Plc:ReadDbBlock" value="DB31" />
|
||||
<add key="Plc:WriteDbBlock" value="DB31" />
|
||||
<add key="Plc:ReadStartAddress" value="0" />
|
||||
<add key="Plc:ReadLength" value="200" />
|
||||
<!-- 批量读取周期(毫秒)| Bulk read interval (ms) -->
|
||||
<add key="Plc:BulkReadIntervalMs" value="250" />
|
||||
<!-- 超时配置 | Timeout Configuration -->
|
||||
<add key="Plc:ConnectTimeoutMs" value="3000" />
|
||||
<add key="Plc:ReadTimeoutMs" value="1000" />
|
||||
<add key="Plc:WriteTimeoutMs" value="1000" />
|
||||
<!-- 自动重连 | Auto Reconnection -->
|
||||
<add key="Plc:bReConnect" value="true" />
|
||||
|
||||
<!-- 直线轴配置(单位:mm)| Linear axis config (unit: mm) -->
|
||||
<add key="MotionControl:SourceZ:Min" value="-500" />
|
||||
<add key="MotionControl:SourceZ:Max" value="500" />
|
||||
<add key="MotionControl:SourceZ:Origin" value="0" />
|
||||
<add key="MotionControl:DetectorZ:Min" value="-600" />
|
||||
<add key="MotionControl:DetectorZ:Max" value="600" />
|
||||
<add key="MotionControl:DetectorZ:Origin" value="600" />
|
||||
<add key="MotionControl:StageX:Min" value="-150" />
|
||||
<add key="MotionControl:StageX:Max" value="150" />
|
||||
<add key="MotionControl:StageX:Origin" value="0" />
|
||||
<add key="MotionControl:StageY:Min" value="-150" />
|
||||
<add key="MotionControl:StageY:Max" value="150" />
|
||||
<add key="MotionControl:StageY:Origin" value="0" />
|
||||
<!-- 旋转轴配置(单位:度)| Rotary axis config (unit: degrees) -->
|
||||
<add key="MotionControl:DetectorSwing:Min" value="-45" />
|
||||
<add key="MotionControl:DetectorSwing:Max" value="45" />
|
||||
<add key="MotionControl:DetectorSwing:Origin" value="0" />
|
||||
<add key="MotionControl:DetectorSwing:Enabled" value="true" />
|
||||
<add key="MotionControl:StageRotation:Min" value="-360" />
|
||||
<add key="MotionControl:StageRotation:Max" value="360" />
|
||||
<add key="MotionControl:StageRotation:Origin" value="0" />
|
||||
<add key="MotionControl:StageRotation:Enabled" value="true" />
|
||||
<add key="MotionControl:FixtureRotation:Min" value="-90" />
|
||||
<add key="MotionControl:FixtureRotation:Max" value="90" />
|
||||
<add key="MotionControl:FixtureRotation:Origin" value="0" />
|
||||
<add key="MotionControl:FixtureRotation:Enabled" value="false" />
|
||||
<!-- 几何原点(mm)| Geometry origins (mm) -->
|
||||
<add key="MotionControl:Geometry:SourceZOrigin" value="0" />
|
||||
<add key="MotionControl:Geometry:DetectorZOrigin" value="600" />
|
||||
<add key="MotionControl:Geometry:StageRotationCenterZ" value="300" />
|
||||
<!-- 探测器摆动几何参数(mm)| Detector swing geometry parameters (mm) -->
|
||||
<!-- SwingPivotOffset: Pivot 相对于 DetectorZ 绝对坐标的 Z 方向偏移,正值表示 Pivot 在 DetectorZ_abs 上方 -->
|
||||
<add key="MotionControl:Geometry:SwingPivotOffset" value="80" />
|
||||
<!-- SwingRadius: Pivot 到探测器感光面中心的距离,为 0 时退化为无摆动模型 -->
|
||||
<add key="MotionControl:Geometry:SwingRadius" value="200" />
|
||||
<!-- 运行参数 | Runtime parameters -->
|
||||
<add key="MotionControl:PollingInterval" value="500" />
|
||||
<add key="MotionControl:DefaultVelocity" value="500" />
|
||||
<!-- 射线源与探测器Z轴联动配置 | Source-Detector Z-axis linkage configuration -->
|
||||
<add key="MotionControl:SourceDetectorZLinkage:Enabled" value="true" />
|
||||
<add key="MotionControl:SourceDetectorZLinkage:TriggerThreshold" value="1.0" />
|
||||
<add key="MotionControl:SourceDetectorZLinkage:SpeedPercent" value="100" />
|
||||
<!-- 直线轴配置(单位:mm)| Linear axis config (unit: mm) -->
|
||||
<add key="MotionControl:SourceZ:Min" value="-500" />
|
||||
<add key="MotionControl:SourceZ:Max" value="500" />
|
||||
<add key="MotionControl:SourceZ:Origin" value="0" />
|
||||
<add key="MotionControl:DetectorZ:Min" value="-600" />
|
||||
<add key="MotionControl:DetectorZ:Max" value="600" />
|
||||
<add key="MotionControl:DetectorZ:Origin" value="600" />
|
||||
<add key="MotionControl:StageX:Min" value="-150" />
|
||||
<add key="MotionControl:StageX:Max" value="150" />
|
||||
<add key="MotionControl:StageX:Origin" value="0" />
|
||||
<add key="MotionControl:StageY:Min" value="-150" />
|
||||
<add key="MotionControl:StageY:Max" value="150" />
|
||||
<add key="MotionControl:StageY:Origin" value="0" />
|
||||
<!-- 旋转轴配置(单位:度)| Rotary axis config (unit: degrees) -->
|
||||
<add key="MotionControl:DetectorSwing:Min" value="-45" />
|
||||
<add key="MotionControl:DetectorSwing:Max" value="45" />
|
||||
<add key="MotionControl:DetectorSwing:Origin" value="0" />
|
||||
<add key="MotionControl:DetectorSwing:Enabled" value="true" />
|
||||
<add key="MotionControl:StageRotation:Min" value="-360" />
|
||||
<add key="MotionControl:StageRotation:Max" value="360" />
|
||||
<add key="MotionControl:StageRotation:Origin" value="0" />
|
||||
<add key="MotionControl:StageRotation:Enabled" value="true" />
|
||||
<add key="MotionControl:FixtureRotation:Min" value="-90" />
|
||||
<add key="MotionControl:FixtureRotation:Max" value="90" />
|
||||
<add key="MotionControl:FixtureRotation:Origin" value="0" />
|
||||
<add key="MotionControl:FixtureRotation:Enabled" value="false" />
|
||||
<!-- 几何原点(mm)| Geometry origins (mm) -->
|
||||
<add key="MotionControl:Geometry:SourceZOrigin" value="0" />
|
||||
<add key="MotionControl:Geometry:DetectorZOrigin" value="600" />
|
||||
<add key="MotionControl:Geometry:StageRotationCenterZ" value="300" />
|
||||
<!-- 探测器摆动几何参数(mm)| Detector swing geometry parameters (mm) -->
|
||||
<!-- SwingPivotOffset: Pivot 相对于 DetectorZ 绝对坐标的 Z 方向偏移,正值表示 Pivot 在 DetectorZ_abs 上方 -->
|
||||
<add key="MotionControl:Geometry:SwingPivotOffset" value="80" />
|
||||
<!-- SwingRadius: Pivot 到探测器感光面中心的距离,为 0 时退化为无摆动模型 -->
|
||||
<add key="MotionControl:Geometry:SwingRadius" value="200" />
|
||||
<!-- 运行参数 | Runtime parameters -->
|
||||
<add key="MotionControl:PollingInterval" value="500" />
|
||||
<add key="MotionControl:DefaultVelocity" value="500" />
|
||||
<!-- 射线源与探测器Z轴联动配置 | Source-Detector Z-axis linkage configuration -->
|
||||
<add key="MotionControl:SourceDetectorZLinkage:Enabled" value="true" />
|
||||
<add key="MotionControl:SourceDetectorZLinkage:TriggerThreshold" value="1.0" />
|
||||
<add key="MotionControl:SourceDetectorZLinkage:SpeedPercent" value="100" />
|
||||
|
||||
<!-- 报告输出文件夹路径(绝对路径)| Report output directory (absolute path) -->
|
||||
<add key="Report:OutputDirectory" value="D:\XplorePlane\Report" />
|
||||
<!-- 报告模板文件路径(相对于应用程序目录或绝对路径)| Template file path (relative to app dir or absolute) -->
|
||||
<add key="Report:TemplatePath" value="Templates\StandardReportTemplate.json" />
|
||||
<!-- 输出文件名模式 | File name pattern -->
|
||||
<add key="Report:FileNamePattern" value="{Date}_{ProductName}_{WorkpieceSN}_{ReportId}" />
|
||||
<!-- 文件名重复时是否自动累加序号 | Auto-increment suffix when file name duplicates-->
|
||||
<add key="Report:AutoIncrementOnDuplicate" value="true" />
|
||||
<!-- 生成后是否自动打开 PDF 阅读器 | Auto-open PDF viewer after generation (true/false) -->
|
||||
<add key="Report:AutoOpenAfterGenerate" value="false" />
|
||||
<!-- 默认页面尺寸 | Default page size (A4) -->
|
||||
<add key="Report:DefaultPageSize" value="A4" />
|
||||
<!-- 默认页面方向 | Default orientation (Portrait / Landscape) -->
|
||||
<add key="Report:DefaultOrientation" value="Portrait" />
|
||||
<!-- 页面边距(mm),有效范围 0-100 | Page margins (mm), valid range 0-100 -->
|
||||
<add key="Report:MarginTop" value="20" />
|
||||
<add key="Report:MarginBottom" value="20" />
|
||||
<add key="Report:MarginLeft" value="20" />
|
||||
<add key="Report:MarginRight" value="20" />
|
||||
<!-- 报告中显示的公司名称 | Company name displayed in report -->
|
||||
<add key="Report:CompanyName" value="海克斯康制造智能技术(青岛)有限公司" />
|
||||
<!-- 公司 Logo 图片路径(可选,为空则不显示)| Company logo path (optional, empty = no logo) -->
|
||||
<add key="Report:CompanyLogo" value="Templates\Logo.png" />
|
||||
<!-- 软件信息配置 | Software Information Configuration -->
|
||||
<add key="Report:SoftwareName" value="XplorePlane" />
|
||||
<add key="Report:SoftwareLogo" value="Templates\Logo2.png" />
|
||||
</appSettings>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
<runtime>
|
||||
<!-- 允许捕获非托管异常(如 AccessViolationException)以便生成 Dump | Allow catching unmanaged exceptions (e.g. AccessViolationException) for dump generation -->
|
||||
<legacyCorruptedStateExceptionsPolicy enabled="true" />
|
||||
</runtime>
|
||||
</configuration>
|
||||
<!-- 报告输出文件夹路径(绝对路径)| Report output directory (absolute path) -->
|
||||
<add key="Report:OutputDirectory" value="D:\XplorePlane\Report" />
|
||||
<!-- 报告模板文件路径(相对于应用程序目录或绝对路径)| Template file path (relative to app dir or absolute) -->
|
||||
<add key="Report:TemplatePath" value="Templates\StandardReportTemplate.json" />
|
||||
<!-- 输出文件名模式 | File name pattern -->
|
||||
<add key="Report:FileNamePattern" value="{Date}_{ProductName}_{WorkpieceSN}_{ReportId}" />
|
||||
<!-- 文件名重复时是否自动累加序号 | Auto-increment suffix when file name duplicates-->
|
||||
<add key="Report:AutoIncrementOnDuplicate" value="true" />
|
||||
<!-- 生成后是否自动打开 PDF 阅读器 | Auto-open PDF viewer after generation (true/false) -->
|
||||
<add key="Report:AutoOpenAfterGenerate" value="false" />
|
||||
<!-- 默认页面尺寸 | Default page size (A4) -->
|
||||
<add key="Report:DefaultPageSize" value="A4" />
|
||||
<!-- 默认页面方向 | Default orientation (Portrait / Landscape) -->
|
||||
<add key="Report:DefaultOrientation" value="Portrait" />
|
||||
<!-- 页面边距(mm),有效范围 0-100 | Page margins (mm), valid range 0-100 -->
|
||||
<add key="Report:MarginTop" value="20" />
|
||||
<add key="Report:MarginBottom" value="20" />
|
||||
<add key="Report:MarginLeft" value="20" />
|
||||
<add key="Report:MarginRight" value="20" />
|
||||
<!-- 报告中显示的公司名称 | Company name displayed in report -->
|
||||
<add key="Report:CompanyName" value="海克斯康制造智能技术(青岛)有限公司" />
|
||||
<!-- 公司 Logo 图片路径(可选,为空则不显示)| Company logo path (optional, empty = no logo) -->
|
||||
<add key="Report:CompanyLogo" value="Templates\Logo.png" />
|
||||
<!-- 软件信息配置 | Software Information Configuration -->
|
||||
<add key="Report:SoftwareName" value="XplorePlane" />
|
||||
<add key="Report:SoftwareLogo" value="Templates\Logo2.png" />
|
||||
</appSettings>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
<runtime>
|
||||
<!-- 允许捕获非托管异常(如 AccessViolationException)以便生成 Dump | Allow catching unmanaged exceptions (e.g. AccessViolationException) for dump generation -->
|
||||
<legacyCorruptedStateExceptionsPolicy enabled="true" />
|
||||
</runtime>
|
||||
</configuration>
|
||||
|
||||
Reference in New Issue
Block a user