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

417 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 → 100000350000 → 500000780000 → 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; }
}
}