Files
XplorePlane/XP.Common/Controls/ImageHistogram/ImageHistogramControl.xaml.cs
T
2026-08-04 14:14:24 +08:00

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
}
}