396 lines
14 KiB
C#
396 lines
14 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
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 枚举 | Enums
|
|
|
|
/// <summary>
|
|
/// 直方图在父容器中的显示位置(4个角落)
|
|
/// </summary>
|
|
public enum CornerPosition
|
|
{
|
|
TopLeft,
|
|
TopRight,
|
|
BottomLeft,
|
|
BottomRight
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 依赖属性 | Dependency Properties
|
|
|
|
public static readonly DependencyProperty MaxFrameRateProperty =
|
|
DependencyProperty.Register(
|
|
nameof(MaxFrameRate),
|
|
typeof(int),
|
|
typeof(ImageHistogramControl),
|
|
new PropertyMetadata(15, OnMaxFrameRateChanged, CoerceMaxFrameRate));
|
|
|
|
public static readonly DependencyProperty IsLogarithmicProperty =
|
|
DependencyProperty.Register(
|
|
nameof(IsLogarithmic),
|
|
typeof(bool),
|
|
typeof(ImageHistogramControl),
|
|
new PropertyMetadata(false));
|
|
|
|
/// <summary>
|
|
/// 显示位置(4个角落)| Display corner position
|
|
/// </summary>
|
|
public static readonly DependencyProperty DisplayPositionProperty =
|
|
DependencyProperty.Register(
|
|
nameof(DisplayPosition),
|
|
typeof(CornerPosition),
|
|
typeof(ImageHistogramControl),
|
|
new PropertyMetadata(CornerPosition.BottomRight, OnDisplayPositionChanged));
|
|
|
|
/// <summary>
|
|
/// 是否显示 | Is histogram visible
|
|
/// </summary>
|
|
public static readonly DependencyProperty IsVisibleProperty =
|
|
DependencyProperty.Register(
|
|
nameof(IsVisible),
|
|
typeof(bool),
|
|
typeof(ImageHistogramControl),
|
|
new PropertyMetadata(false, OnIsVisibleChanged));
|
|
|
|
public int MaxFrameRate
|
|
{
|
|
get => (int)GetValue(MaxFrameRateProperty);
|
|
set => SetValue(MaxFrameRateProperty, value);
|
|
}
|
|
|
|
public bool IsLogarithmic
|
|
{
|
|
get => (bool)GetValue(IsLogarithmicProperty);
|
|
set => SetValue(IsLogarithmicProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取或设置显示位置(左上/右上/左下/右下)
|
|
/// </summary>
|
|
public CornerPosition DisplayPosition
|
|
{
|
|
get => (CornerPosition)GetValue(DisplayPositionProperty);
|
|
set => SetValue(DisplayPositionProperty, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取或设置是否显示直方图
|
|
/// </summary>
|
|
public bool IsVisible
|
|
{
|
|
get => (bool)GetValue(IsVisibleProperty);
|
|
set => SetValue(IsVisibleProperty, 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}",
|
|
value, clamped);
|
|
}
|
|
return clamped;
|
|
}
|
|
|
|
private static void OnDisplayPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (d is ImageHistogramControl control)
|
|
control.UpdatePosition();
|
|
}
|
|
|
|
private static void OnIsVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
{
|
|
if (d is ImageHistogramControl control)
|
|
control.Visibility = (bool)e.NewValue ? Visibility.Visible : Visibility.Collapsed;
|
|
}
|
|
|
|
#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();
|
|
private bool _isExpanded;
|
|
private const double CompactWidth = 200;
|
|
private const double CompactHeight = 150;
|
|
private const double ExpandedWidth = 420;
|
|
private const double ExpandedHeight = 300;
|
|
|
|
#endregion
|
|
|
|
#region 构造函数 | Constructor
|
|
|
|
public ImageHistogramControl()
|
|
{
|
|
InitializeComponent();
|
|
|
|
UpdatePosition();
|
|
Visibility = IsVisible ? Visibility.Visible : Visibility.Collapsed;
|
|
SizeChanged += (s, e) => UpdatePosition();
|
|
|
|
_frameThrottler = new FrameThrottler();
|
|
_histogramEngine = new HistogramEngine();
|
|
Width = CompactWidth;
|
|
Height = CompactHeight;
|
|
IsHitTestVisible = true;
|
|
|
|
try
|
|
{
|
|
var loggerService = ContainerLocator.Current?.Resolve<ILoggerService>();
|
|
_logger = loggerService?.ForModule<ImageHistogramControl>();
|
|
}
|
|
catch
|
|
{
|
|
_logger = null;
|
|
}
|
|
|
|
Loaded += OnLoaded;
|
|
Unloaded += OnUnloaded;
|
|
MouseDoubleClick += OnMouseDoubleClick;
|
|
MouseWheel += OnMouseWheel;
|
|
}
|
|
|
|
private void UpdatePosition()
|
|
{
|
|
switch (DisplayPosition)
|
|
{
|
|
case CornerPosition.TopLeft:
|
|
HorizontalAlignment = HorizontalAlignment.Left;
|
|
VerticalAlignment = VerticalAlignment.Top;
|
|
break;
|
|
case CornerPosition.TopRight:
|
|
HorizontalAlignment = HorizontalAlignment.Right;
|
|
VerticalAlignment = VerticalAlignment.Top;
|
|
break;
|
|
case CornerPosition.BottomLeft:
|
|
HorizontalAlignment = HorizontalAlignment.Left;
|
|
VerticalAlignment = VerticalAlignment.Bottom;
|
|
break;
|
|
case CornerPosition.BottomRight:
|
|
HorizontalAlignment = HorizontalAlignment.Right;
|
|
VerticalAlignment = VerticalAlignment.Bottom;
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
|
{
|
|
_chartRenderer = new ChartRenderer(
|
|
HistogramChart, HistogramBarSeries, PeakLabelSeries,
|
|
BgaLowThresholdSeries, BgaHighThresholdSeries,
|
|
VoidLowThresholdSeries, VoidHighThresholdSeries, XAxis);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 公共 API | Public API
|
|
|
|
public void UpdateImage(Image<Rgba32> image)
|
|
{
|
|
try
|
|
{
|
|
if (image == null)
|
|
{
|
|
_logger?.Warn("UpdateImage 收到 null 图像,已忽略");
|
|
return;
|
|
}
|
|
SubmitComputation(() => _histogramEngine.ComputeAsync(image, GetOrCreateCancellationToken()));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.Error(ex, "UpdateImage(Image) 异常:{Message}", ex.Message);
|
|
}
|
|
}
|
|
|
|
public void UpdateImage(byte[] rawData, int width, int height, int bitDepth)
|
|
{
|
|
try
|
|
{
|
|
if (rawData == null)
|
|
{
|
|
_logger?.Warn("UpdateImage 收到 null rawData,已忽略");
|
|
return;
|
|
}
|
|
if (width <= 0 || height <= 0)
|
|
{
|
|
_logger?.Warn("UpdateImage 参数无效:width={Width}, height={Height}", width, height);
|
|
return;
|
|
}
|
|
if (bitDepth != 8 && bitDepth != 16)
|
|
{
|
|
_logger?.Warn("UpdateImage 参数无效:bitDepth={BitDepth},仅支持 8 或 16", bitDepth);
|
|
return;
|
|
}
|
|
int expectedLength = bitDepth == 8 ? width * height : width * height * 2;
|
|
if (rawData.Length != expectedLength)
|
|
{
|
|
_logger?.Warn("UpdateImage 参数无效:rawData.Length={Length}, 预期={Expected}", rawData.Length, expectedLength);
|
|
return;
|
|
}
|
|
SubmitComputation(() => _histogramEngine.ComputeAsync(rawData, width, height, bitDepth, GetOrCreateCancellationToken()));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.Error(ex, "UpdateImage(byte[]) 异常:{Message}", ex.Message);
|
|
}
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
try
|
|
{
|
|
CancelCurrentComputation();
|
|
_frameThrottler.Cancel();
|
|
var renderer = _chartRenderer;
|
|
if (renderer != null)
|
|
{
|
|
Dispatcher.InvokeAsync(() =>
|
|
{
|
|
try
|
|
{
|
|
renderer.Clear();
|
|
NoDataPlaceholder.Visibility = Visibility.Visible;
|
|
}
|
|
catch { }
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.Error(ex, "Clear() 异常:{Message}", ex.Message);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 私有方法 | Private Methods
|
|
|
|
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;
|
|
Dispatcher.InvokeAsync(() =>
|
|
{
|
|
try
|
|
{
|
|
var isLog = IsLogarithmic;
|
|
_chartRenderer?.UpdateData(histogram, isLog);
|
|
NoDataPlaceholder.Visibility = Visibility.Collapsed;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.Error(ex, "图表更新异常:{Message}", ex.Message);
|
|
}
|
|
});
|
|
}
|
|
else if (t.IsFaulted)
|
|
{
|
|
_logger?.Error(t.Exception, "直方图计算异常:{Message}",
|
|
t.Exception?.InnerException?.Message ?? "Unknown");
|
|
}
|
|
}, System.Threading.Tasks.TaskScheduler.Default);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.Error(ex, "提交计算任务异常:{Message}", ex.Message);
|
|
}
|
|
});
|
|
}
|
|
|
|
private CancellationToken GetOrCreateCancellationToken()
|
|
{
|
|
lock (_ctsLock)
|
|
{
|
|
_currentCts?.Cancel();
|
|
_currentCts?.Dispose();
|
|
_currentCts = new CancellationTokenSource();
|
|
return _currentCts.Token;
|
|
}
|
|
}
|
|
|
|
private void CancelCurrentComputation()
|
|
{
|
|
lock (_ctsLock)
|
|
{
|
|
_currentCts?.Cancel();
|
|
_currentCts?.Dispose();
|
|
_currentCts = null;
|
|
}
|
|
}
|
|
|
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
|
{
|
|
CancelCurrentComputation();
|
|
_frameThrottler.Cancel();
|
|
_frameThrottler.Dispose();
|
|
_histogramEngine.Dispose();
|
|
_chartRenderer = null;
|
|
}
|
|
|
|
/// <summary>设置 BGA 定位和空洞分割的灰度阈值线。</summary>
|
|
public void SetThresholds(double? bgaLow, double? bgaHigh, double? voidLow, double? voidHigh)
|
|
{
|
|
Dispatcher.InvokeAsync(() => _chartRenderer?.SetThresholds(bgaLow, bgaHigh, voidLow, voidHigh));
|
|
}
|
|
|
|
private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
|
|
{
|
|
_isExpanded = !_isExpanded;
|
|
Width = _isExpanded ? ExpandedWidth : CompactWidth;
|
|
Height = _isExpanded ? ExpandedHeight : CompactHeight;
|
|
e.Handled = true;
|
|
}
|
|
|
|
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
|
|
{
|
|
double factor = e.Delta > 0 ? 1.1 : 1 / 1.1;
|
|
Width = Math.Clamp(Width * factor, CompactWidth, 600);
|
|
Height = Math.Clamp(Height * factor, CompactHeight, 420);
|
|
_isExpanded = Width > CompactWidth || Height > CompactHeight;
|
|
e.Handled = true;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|