417 lines
17 KiB
C#
417 lines
17 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Windows;
|
||
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 ScatterPointSeries _peakSeries;
|
||
private readonly ScatterLineSeries _bgaLowThresholdSeries;
|
||
private readonly ScatterLineSeries _bgaHighThresholdSeries;
|
||
private readonly ScatterLineSeries _voidLowThresholdSeries;
|
||
private readonly ScatterLineSeries _voidHighThresholdSeries;
|
||
private readonly LinearAxis _xAxis;
|
||
private (double? BgaLow, double? BgaHigh, double? VoidLow, double? VoidHigh) _thresholds;
|
||
|
||
/// <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,
|
||
ScatterPointSeries peakSeries,
|
||
ScatterLineSeries bgaLowThresholdSeries,
|
||
ScatterLineSeries bgaHighThresholdSeries,
|
||
ScatterLineSeries voidLowThresholdSeries,
|
||
ScatterLineSeries voidHighThresholdSeries,
|
||
LinearAxis xAxis)
|
||
{
|
||
_chart = chart ?? throw new ArgumentNullException(nameof(chart));
|
||
_areaSeries = areaSeries ?? throw new ArgumentNullException(nameof(areaSeries));
|
||
_peakSeries = peakSeries ?? throw new ArgumentNullException(nameof(peakSeries));
|
||
_bgaLowThresholdSeries = bgaLowThresholdSeries ?? throw new ArgumentNullException(nameof(bgaLowThresholdSeries));
|
||
_bgaHighThresholdSeries = bgaHighThresholdSeries ?? throw new ArgumentNullException(nameof(bgaHighThresholdSeries));
|
||
_voidLowThresholdSeries = voidLowThresholdSeries ?? throw new ArgumentNullException(nameof(voidLowThresholdSeries));
|
||
_voidHighThresholdSeries = voidHighThresholdSeries ?? throw new ArgumentNullException(nameof(voidHighThresholdSeries));
|
||
_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;
|
||
_peakSeries.ItemsSource = DetectPeaks(displayData, histogram.Length == 65536);
|
||
|
||
// 设置 Y 轴范围 | Set Y axis range
|
||
UpdateYAxis(displayData, isLogarithmic);
|
||
UpdateThresholdLines(displayData.Max());
|
||
}
|
||
|
||
public void SetThresholds(double? bgaLow, double? bgaHigh, double? voidLow, double? voidHigh)
|
||
{
|
||
_thresholds = (bgaLow, bgaHigh, voidLow, voidHigh);
|
||
var max = _areaSeries.ItemsSource is IEnumerable<HistogramDataPoint> points
|
||
? points.Select(point => point.Frequency).DefaultIfEmpty(1).Max()
|
||
: 1;
|
||
UpdateThresholdLines(max);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清空图表,恢复初始状态 | Clear chart, restore initial state
|
||
/// </summary>
|
||
public void Clear()
|
||
{
|
||
_thresholds = (null, null, null, null);
|
||
_areaSeries.ItemsSource = null;
|
||
_peakSeries.ItemsSource = null;
|
||
_bgaLowThresholdSeries.ItemsSource = null;
|
||
_bgaHighThresholdSeries.ItemsSource = null;
|
||
_voidLowThresholdSeries.ItemsSource = null;
|
||
_voidHighThresholdSeries.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);
|
||
}
|
||
|
||
private void UpdateThresholdLines(long maximumFrequency)
|
||
{
|
||
var max = Math.Max(1, maximumFrequency);
|
||
_bgaLowThresholdSeries.ItemsSource = CreateVerticalLine(_thresholds.BgaLow, max);
|
||
_bgaHighThresholdSeries.ItemsSource = CreateVerticalLine(_thresholds.BgaHigh, max);
|
||
_voidLowThresholdSeries.ItemsSource = CreateVerticalLine(_thresholds.VoidLow, max);
|
||
_voidHighThresholdSeries.ItemsSource = CreateVerticalLine(_thresholds.VoidHigh, max);
|
||
}
|
||
|
||
private static IReadOnlyList<HistogramDataPoint>? CreateVerticalLine(double? grayLevel, long maximumFrequency)
|
||
{
|
||
if (!grayLevel.HasValue || grayLevel.Value < 0)
|
||
return null;
|
||
|
||
return new[]
|
||
{
|
||
new HistogramDataPoint { GrayLevel = grayLevel.Value, Frequency = 0 },
|
||
new HistogramDataPoint { GrayLevel = grayLevel.Value, Frequency = maximumFrequency }
|
||
};
|
||
}
|
||
|
||
/// <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>
|
||
/// 检测需要标注的明显局部峰值。忽略低于最高峰 5% 的噪声,并限制标注数量,
|
||
/// 避免直方图在平坦区域产生大量重叠文字。
|
||
/// </summary>
|
||
private static IReadOnlyList<HistogramPeakDataPoint> DetectPeaks(long[] displayData, bool is16Bit)
|
||
{
|
||
if (displayData == null || displayData.Length < 3)
|
||
return Array.Empty<HistogramPeakDataPoint>();
|
||
|
||
long maximum = displayData.Max();
|
||
if (maximum <= 0)
|
||
return Array.Empty<HistogramPeakDataPoint>();
|
||
|
||
long minimumPeak = Math.Max(1, (long)Math.Ceiling(maximum * 0.05));
|
||
var candidates = new List<(int Index, long Frequency)>();
|
||
|
||
for (int i = 1; i < displayData.Length - 1; i++)
|
||
{
|
||
long value = displayData[i];
|
||
if (value < minimumPeak || value < displayData[i - 1] || value < displayData[i + 1])
|
||
continue;
|
||
|
||
// 平台峰取平台中心,避免连续相同频次产生多个标签。
|
||
int start = i;
|
||
int end = i;
|
||
while (start > 0 && displayData[start - 1] == value) start--;
|
||
while (end < displayData.Length - 1 && displayData[end + 1] == value) end++;
|
||
int center = (start + end) / 2;
|
||
if (i != center)
|
||
continue;
|
||
|
||
candidates.Add((center, value));
|
||
i = end;
|
||
}
|
||
|
||
// 优先保留高峰,最多显示 6 个标签;最终按灰度位置排序以保持阅读顺序。
|
||
var selected = candidates
|
||
.OrderByDescending(item => item.Frequency)
|
||
.Take(6)
|
||
.OrderBy(item => item.Index)
|
||
.ToArray();
|
||
|
||
return selected
|
||
.Select((item, index) => new HistogramPeakDataPoint
|
||
{
|
||
GrayLevel = is16Bit
|
||
? item.Index * AggregationFactor + AggregationFactor / 2.0
|
||
: item.Index,
|
||
Frequency = item.Frequency,
|
||
Label = FormatGrayLevel(is16Bit
|
||
? item.Index * AggregationFactor + AggregationFactor / 2.0
|
||
: item.Index),
|
||
LabelMargin = item.Frequency >= maximum * 0.75
|
||
? new Thickness(0, 4 + (index % 2) * 14, 0, 0)
|
||
: new Thickness(0, -18 - (index % 2) * 14, 0, 0)
|
||
})
|
||
.ToArray();
|
||
}
|
||
|
||
private static string FormatGrayLevel(double value) => $"{Math.Round(value):0}";
|
||
|
||
/// <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; }
|
||
}
|
||
|
||
/// <summary>直方图峰值标注数据 | Histogram peak label data</summary>
|
||
internal sealed class HistogramPeakDataPoint
|
||
{
|
||
public double GrayLevel { get; set; }
|
||
public long Frequency { get; set; }
|
||
public string Label { get; set; } = string.Empty;
|
||
public Thickness LabelMargin { get; set; }
|
||
}
|
||
}
|