349 lines
13 KiB
C#
349 lines
13 KiB
C#
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
|
||
}
|
||
}
|