通用基础设施XP.Common新增 ImageHistogramControl 图像灰度直方图通用控件(使用SixLabors.ImageSharp 3.1.12),支持 Image<Rgba32> 和 byte[] 输入,支持多线程调用,Telerik RadChartView 渲染。
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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 BarSeries _barSeries;
|
||||
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="barSeries">柱状图系列 | Bar series</param>
|
||||
/// <param name="xAxis">X 轴 | X axis</param>
|
||||
public ChartRenderer(RadCartesianChart chart, BarSeries barSeries, LinearAxis xAxis)
|
||||
{
|
||||
_chart = chart ?? throw new ArgumentNullException(nameof(chart));
|
||||
_barSeries = barSeries ?? throw new ArgumentNullException(nameof(barSeries));
|
||||
_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;
|
||||
|
||||
// 构建数据点 | 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
|
||||
_barSeries.ItemsSource = dataPoints;
|
||||
|
||||
// 设置 Y 轴范围 | Set Y axis range
|
||||
UpdateYAxis(displayData, isLogarithmic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空图表,恢复初始状态 | Clear chart, restore initial state
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_barSeries.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 (_barSeries.ItemsSource is ICollection<HistogramDataPoint> collection)
|
||||
return collection.Count;
|
||||
if (_barSeries.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;
|
||||
|
||||
SetYAxisRange(0, maxValue, isLogarithmic);
|
||||
}
|
||||
|
||||
/// <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,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,36 @@
|
||||
<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: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">
|
||||
<Grid>
|
||||
<telerik:RadCartesianChart x:Name="HistogramChart">
|
||||
<!-- X 轴:灰度级别 | X Axis: Gray Level -->
|
||||
<telerik:RadCartesianChart.HorizontalAxis>
|
||||
<telerik:LinearAxis x:Name="XAxis"
|
||||
Minimum="0"
|
||||
Maximum="255"
|
||||
Title="灰度级别"/>
|
||||
</telerik:RadCartesianChart.HorizontalAxis>
|
||||
|
||||
<!-- Y 轴:像素频次(默认线性)| Y Axis: Pixel Frequency (default linear) -->
|
||||
<telerik:RadCartesianChart.VerticalAxis>
|
||||
<telerik:LinearAxis x:Name="YAxis"
|
||||
Minimum="0"
|
||||
Maximum="1"
|
||||
Title="频次"/>
|
||||
</telerik:RadCartesianChart.VerticalAxis>
|
||||
|
||||
<!-- 柱状图系列 | Bar Series -->
|
||||
<telerik:RadCartesianChart.Series>
|
||||
<telerik:BarSeries x:Name="HistogramBarSeries"
|
||||
ValueBinding="Frequency"
|
||||
CategoryBinding="GrayLevel"
|
||||
ShowLabels="False"/>
|
||||
</telerik:RadCartesianChart.Series>
|
||||
</telerik:RadCartesianChart>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,331 @@
|
||||
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();
|
||||
|
||||
// 清空图表 | Clear chart
|
||||
if (_chartRenderer != null)
|
||||
{
|
||||
Dispatcher.InvokeAsync(() => _chartRenderer.Clear());
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user