feat: show histogram gray peaks and threshold lines

This commit is contained in:
zhengxuan.zhang
2026-08-04 14:14:24 +08:00
parent 09747e5f23
commit 68b680027b
4 changed files with 114 additions and 24 deletions
@@ -15,8 +15,13 @@ namespace XP.Common.Controls.ImageHistogram
{
private readonly RadCartesianChart _chart;
private readonly ScatterAreaSeries _areaSeries;
private readonly ScatterPointSeries _peakSeries;
private readonly LinearAxis _xAxis;
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
@@ -31,13 +36,21 @@ namespace XP.Common.Controls.ImageHistogram
/// <param name="xAxis">X 轴 | X axis</param>
public ChartRenderer(
RadCartesianChart chart,
ScatterAreaSeries areaSeries,
ScatterPointSeries peakSeries,
LinearAxis xAxis)
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));
_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));
}
@@ -116,16 +129,31 @@ namespace XP.Common.Controls.ImageHistogram
_peakSeries.ItemsSource = DetectPeaks(displayData, histogram.Length == 65536);
// 设置 Y 轴范围 | Set Y axis range
UpdateYAxis(displayData, isLogarithmic);
}
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()
{
_areaSeries.ItemsSource = null;
_peakSeries.ItemsSource = null;
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;
@@ -133,7 +161,28 @@ namespace XP.Common.Controls.ImageHistogram
// 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
@@ -218,7 +267,9 @@ namespace XP.Common.Controls.ImageHistogram
? item.Index * AggregationFactor + AggregationFactor / 2.0
: item.Index,
Frequency = item.Frequency,
Label = FormatFrequency(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)
@@ -226,14 +277,7 @@ namespace XP.Common.Controls.ImageHistogram
.ToArray();
}
private static string FormatFrequency(long value)
{
if (value >= 1_000_000)
return $"{value / 1_000_000d:0.#}M";
if (value >= 1_000)
return $"{value / 1_000d:0.#}K";
return value.ToString();
}
private static string FormatGrayLevel(double value) => $"{Math.Round(value):0}";
/// <summary>
/// 更新 Y 轴范围 | Update Y axis range
@@ -65,6 +65,10 @@
</DataTemplate>
</telerik:ScatterPointSeries.PointTemplate>
</telerik:ScatterPointSeries>
<telerik:ScatterLineSeries x:Name="BgaLowThresholdSeries" XValueBinding="GrayLevel" YValueBinding="Frequency" Stroke="#FFFF7043" StrokeThickness="1" IsHitTestVisible="False" />
<telerik:ScatterLineSeries x:Name="BgaHighThresholdSeries" XValueBinding="GrayLevel" YValueBinding="Frequency" Stroke="#FFE53935" StrokeThickness="1" IsHitTestVisible="False" />
<telerik:ScatterLineSeries x:Name="VoidLowThresholdSeries" XValueBinding="GrayLevel" YValueBinding="Frequency" Stroke="#FFFFA000" StrokeThickness="1" IsHitTestVisible="False" />
<telerik:ScatterLineSeries x:Name="VoidHighThresholdSeries" XValueBinding="GrayLevel" YValueBinding="Frequency" Stroke="#FF8E24AA" StrokeThickness="1" IsHitTestVisible="False" />
</telerik:RadCartesianChart.Series>
</telerik:RadCartesianChart>
<TextBlock x:Name="NoDataPlaceholder"
@@ -207,7 +207,10 @@ namespace XP.Common.Controls.ImageHistogram
private void OnLoaded(object sender, RoutedEventArgs e)
{
_chartRenderer = new ChartRenderer(HistogramChart, HistogramBarSeries, PeakLabelSeries, XAxis);
_chartRenderer = new ChartRenderer(
HistogramChart, HistogramBarSeries, PeakLabelSeries,
BgaLowThresholdSeries, BgaHighThresholdSeries,
VoidLowThresholdSeries, VoidHighThresholdSeries, XAxis);
}
#endregion
@@ -231,7 +234,7 @@ namespace XP.Common.Controls.ImageHistogram
}
}
public void UpdateImage(byte[] rawData, int width, int height, int bitDepth)
public void UpdateImage(byte[] rawData, int width, int height, int bitDepth)
{
try
{
@@ -364,6 +367,12 @@ namespace XP.Common.Controls.ImageHistogram
_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;
@@ -1,4 +1,5 @@
using System.Windows;
using XP.Common.Controls.ImageHistogram;
using XP.ImageProcessing.RoiControl.Controls;
using XplorePlane.Services.Main.Viewport;
using XplorePlane.ViewModels.Cnc;
@@ -20,6 +21,12 @@ namespace XplorePlane.Views.ImageProcessing
Loaded += (s, e) =>
{
if (DataContext is BgaDetectionViewModel loadedVm)
{
loadedVm.PropertyChanged += BgaVmPropertyChanged;
UpdateHistogramThresholds(loadedVm);
}
var mainWin = Owner as MainWindow;
if (mainWin != null)
{
@@ -40,10 +47,36 @@ namespace XplorePlane.Views.ImageProcessing
Closed += (s, e) =>
{
if (DataContext is BgaDetectionViewModel vm)
{
vm.PropertyChanged -= BgaVmPropertyChanged;
FindChild<ImageHistogramControl>(Owner)?.SetThresholds(null, null, null, null);
vm.RestoreContextMenu();
}
};
}
private void BgaVmPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (sender is BgaDetectionViewModel vm &&
(e.PropertyName == nameof(BgaDetectionViewModel.BgaThresholdLow) ||
e.PropertyName == nameof(BgaDetectionViewModel.BgaThresholdHigh) ||
e.PropertyName == nameof(BgaDetectionViewModel.MinThreshold) ||
e.PropertyName == nameof(BgaDetectionViewModel.MaxThreshold)))
{
UpdateHistogramThresholds(vm);
}
}
private void UpdateHistogramThresholds(BgaDetectionViewModel vm)
{
if (Owner is MainWindow mainWindow)
{
FindChild<ImageHistogramControl>(mainWindow)?.SetThresholds(
vm.BgaThresholdLow, vm.BgaThresholdHigh,
vm.MinThreshold, vm.MaxThreshold);
}
}
private void Close_Click(object sender, RoutedEventArgs e) => Close();
private static T FindChild<T>(DependencyObject parent) where T : DependencyObject