histogram implemented
This commit is contained in:
@@ -25,7 +25,16 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
private Mat? _calibCameraMatrix;
|
||||
private Mat? _calibDistCoeffs;
|
||||
private Mat? _referenceFrame;
|
||||
private float[]? _referenceHistogram;
|
||||
|
||||
// Non-modal cell histogram tool window state. While open, clicks on cells in the
|
||||
// calibration preview update its content instead of opening a new window.
|
||||
private CellHistogramWindow? _cellDialog;
|
||||
private CellHistogramViewModel? _cellDialogVm;
|
||||
|
||||
// Transient seed for the average spline; never persisted in the recipe.
|
||||
private float[]? _averageReferenceHistogram;
|
||||
private float[]? _averageSpline;
|
||||
private readonly Dictionary<int, float[]> _cellSplines = new();
|
||||
|
||||
[ObservableProperty] private Avalonia.Media.Imaging.Bitmap? _previewImage;
|
||||
[ObservableProperty] private string _statusText = "Ready";
|
||||
@@ -49,8 +58,7 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
[ObservableProperty] private decimal? _roiHeight = 0m;
|
||||
|
||||
// Detection
|
||||
[ObservableProperty] private decimal? _tolerance = 0.30m;
|
||||
[ObservableProperty] private bool _hasReferenceHistogram;
|
||||
[ObservableProperty] private bool _hasAverageSpline;
|
||||
|
||||
// Recipe
|
||||
[ObservableProperty] private string _currentRecipeName = "default";
|
||||
@@ -190,23 +198,28 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
try
|
||||
{
|
||||
var pattern = BuildCurrentPattern();
|
||||
_referenceHistogram = _patternService.ComputeReferenceHistogram(frame, pattern);
|
||||
_averageReferenceHistogram = _patternService.ComputeReferenceHistogram(frame, pattern);
|
||||
_averageSpline = SplineCurve.CreateDefault(_averageReferenceHistogram);
|
||||
_cellSplines.Clear();
|
||||
|
||||
_referenceFrame?.Dispose();
|
||||
_referenceFrame = frame.Clone();
|
||||
|
||||
HasReferenceHistogram = true;
|
||||
StatusText = $"Reference set ({_referenceHistogram.Length} bins)";
|
||||
HasAverageSpline = true;
|
||||
StatusText = "Average spline seeded; per-cell overrides cleared.";
|
||||
RefreshPreview();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to compute reference histogram");
|
||||
Log.Error(ex, "Failed to seed average spline");
|
||||
StatusText = $"Reference error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void TestPattern()
|
||||
private void TestPattern() => RunTestPattern();
|
||||
|
||||
private void RunTestPattern()
|
||||
{
|
||||
using var frame = GetAnalysisFrame();
|
||||
if (frame == null)
|
||||
@@ -214,9 +227,9 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
StatusText = "No active frame to test";
|
||||
return;
|
||||
}
|
||||
if (_referenceHistogram == null)
|
||||
if (_averageSpline == null)
|
||||
{
|
||||
StatusText = "Set reference histogram first";
|
||||
StatusText = "Set reference first";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,7 +237,8 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
var recipe = BuildCurrentRecipe();
|
||||
var results = _patternService.EvaluateCells(frame, recipe);
|
||||
using var overlay = _patternService.RenderOverlay(frame, recipe.Pattern, results);
|
||||
var overrides = new HashSet<int>(_cellSplines.Keys);
|
||||
using var overlay = _patternService.RenderOverlay(frame, recipe.Pattern, results, overrides);
|
||||
var bitmap = ImageConverter.MatToAvaloniaBitmap(overlay);
|
||||
|
||||
var old = PreviewImage;
|
||||
@@ -260,51 +274,98 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
return _patternService.FindCellAtPoint(imageX, imageY, BuildCurrentPattern());
|
||||
}
|
||||
|
||||
public async Task ShowCellHistogramAsync(int imageX, int imageY, Avalonia.Controls.Window owner)
|
||||
public Task ShowCellHistogramAsync(int imageX, int imageY, Avalonia.Controls.Window owner)
|
||||
{
|
||||
using var frame = GetAnalysisFrame();
|
||||
if (frame == null) return;
|
||||
if (frame == null) return Task.CompletedTask;
|
||||
|
||||
var pattern = BuildCurrentPattern();
|
||||
var cellIndex = _patternService.FindCellAtPoint(imageX, imageY, pattern);
|
||||
if (cellIndex == null) return;
|
||||
if (cellIndex == null) return Task.CompletedTask;
|
||||
|
||||
if (_referenceHistogram == null)
|
||||
if (_averageSpline is not { Length: SplineCurve.KnotCount })
|
||||
{
|
||||
StatusText = "Set reference histogram first";
|
||||
return;
|
||||
StatusText = "Set reference first";
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bins = new[] { 5, 5, 5 };
|
||||
var cellHist = _patternService.ComputeCellHistogram(frame, pattern, cellIndex.Value, bins);
|
||||
var distance = _patternService.CompareHistograms(_referenceHistogram, cellHist, bins);
|
||||
|
||||
using var refChart = LeerformPatternRecognitionService.RenderHistogramChart(_referenceHistogram, bins);
|
||||
using var cellChart = LeerformPatternRecognitionService.RenderHistogramChart(cellHist, bins);
|
||||
|
||||
var refBitmap = ImageConverter.MatToAvaloniaBitmap(refChart);
|
||||
var cellBitmap = ImageConverter.MatToAvaloniaBitmap(cellChart);
|
||||
|
||||
var tolerance = (double)(Tolerance ?? 0.30m);
|
||||
var verdict = distance <= tolerance ? "GOOD" : "BAD";
|
||||
var dialogVm = new CellHistogramViewModel
|
||||
// Already open: commit the previous cell's edits and switch to the new cell.
|
||||
if (_cellDialog != null && _cellDialogVm != null)
|
||||
{
|
||||
Title = $"Cell #{cellIndex.Value}",
|
||||
ReferenceImage = refBitmap,
|
||||
CellImage = cellBitmap,
|
||||
DistanceText = $"Distance: {distance:F4} Tolerance: {tolerance:F2} Verdict: {verdict}"
|
||||
CommitDialogStateToCell(_cellDialogVm);
|
||||
LoadCellIntoDialog(_cellDialogVm, frame, pattern, cellIndex.Value);
|
||||
_cellDialog.Activate();
|
||||
RunTestPattern();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var dialogVm = new CellHistogramViewModel { Bins = new[] { 5, 5, 5 } };
|
||||
LoadCellIntoDialog(dialogVm, frame, pattern, cellIndex.Value);
|
||||
|
||||
// Re-test against the current frame on every spline drag release so the
|
||||
// calibration window's preview updates live behind the open dialog.
|
||||
dialogVm.DragCompleted = (avg, cell) =>
|
||||
{
|
||||
if (avg is { Length: SplineCurve.KnotCount })
|
||||
_averageSpline = avg;
|
||||
if (cell is { Length: SplineCurve.KnotCount })
|
||||
_cellSplines[dialogVm.CellIndex] = cell;
|
||||
else
|
||||
_cellSplines.Remove(dialogVm.CellIndex);
|
||||
RunTestPattern();
|
||||
};
|
||||
|
||||
var window = new CellHistogramWindow { DataContext = dialogVm };
|
||||
await window.ShowDialog(owner);
|
||||
_cellDialogVm = dialogVm;
|
||||
_cellDialog = new CellHistogramWindow { DataContext = dialogVm };
|
||||
_cellDialog.Closed += OnCellDialogClosed;
|
||||
_cellDialog.Show(owner);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to show cell histogram");
|
||||
StatusText = $"Histogram error: {ex.Message}";
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void LoadCellIntoDialog(CellHistogramViewModel vm, Mat frame, CellPattern pattern, int cellIndex)
|
||||
{
|
||||
var bins = vm.Bins is { Length: 3 } ? vm.Bins : new[] { 5, 5, 5 };
|
||||
var cellHist = _patternService.ComputeCellHistogram(frame, pattern, cellIndex, bins);
|
||||
var avgSplineCopy = (float[])(_averageSpline ?? new float[SplineCurve.KnotCount]).Clone();
|
||||
var cellSplineCopy = _cellSplines.TryGetValue(cellIndex, out var existing) && existing is { Length: SplineCurve.KnotCount }
|
||||
? (float[])existing.Clone()
|
||||
: null;
|
||||
|
||||
vm.CellIndex = cellIndex;
|
||||
vm.Title = $"Cell #{cellIndex}";
|
||||
vm.CellHistogram = cellHist;
|
||||
vm.AverageSpline = avgSplineCopy;
|
||||
vm.CellSpline = cellSplineCopy;
|
||||
}
|
||||
|
||||
private void CommitDialogStateToCell(CellHistogramViewModel vm)
|
||||
{
|
||||
if (vm.AverageSpline is { Length: SplineCurve.KnotCount })
|
||||
_averageSpline = vm.AverageSpline;
|
||||
|
||||
if (vm.CellSpline is { Length: SplineCurve.KnotCount })
|
||||
_cellSplines[vm.CellIndex] = vm.CellSpline;
|
||||
else
|
||||
_cellSplines.Remove(vm.CellIndex);
|
||||
}
|
||||
|
||||
private void OnCellDialogClosed(object? sender, EventArgs e)
|
||||
{
|
||||
if (_cellDialogVm != null)
|
||||
CommitDialogStateToCell(_cellDialogVm);
|
||||
if (_cellDialog != null)
|
||||
_cellDialog.Closed -= OnCellDialogClosed;
|
||||
_cellDialog = null;
|
||||
_cellDialogVm = null;
|
||||
RunTestPattern();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -349,17 +410,30 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
RoiY = recipe.Pattern.RoiY;
|
||||
RoiWidth = recipe.Pattern.RoiWidth;
|
||||
RoiHeight = recipe.Pattern.RoiHeight;
|
||||
Tolerance = (decimal)recipe.Tolerance;
|
||||
|
||||
_referenceHistogram = recipe.ReferenceHistogram is { Length: > 0 }
|
||||
? recipe.ReferenceHistogram
|
||||
_averageReferenceHistogram = null;
|
||||
_averageSpline = recipe.AverageSpline is { Length: SplineCurve.KnotCount }
|
||||
? recipe.AverageSpline
|
||||
: null;
|
||||
HasReferenceHistogram = _referenceHistogram != null;
|
||||
|
||||
_cellSplines.Clear();
|
||||
if (recipe.CellSplines != null)
|
||||
{
|
||||
foreach (var kv in recipe.CellSplines)
|
||||
{
|
||||
if (kv.Value is { Length: SplineCurve.KnotCount })
|
||||
_cellSplines[kv.Key] = kv.Value;
|
||||
}
|
||||
}
|
||||
|
||||
HasAverageSpline = _averageSpline != null;
|
||||
|
||||
_referenceFrame?.Dispose();
|
||||
_referenceFrame = LeerformRecipeStore.DecodeThumbnail(recipe);
|
||||
|
||||
StatusText = $"Loaded recipe '{recipeName}'";
|
||||
StatusText = HasAverageSpline
|
||||
? $"Loaded recipe '{recipeName}'"
|
||||
: $"Loaded recipe '{recipeName}' — no spline; click 'Set Reference'.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -384,8 +458,8 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
RecipeName = CurrentRecipeName,
|
||||
Pattern = BuildCurrentPattern(),
|
||||
HistogramBins = [5, 5, 5],
|
||||
ReferenceHistogram = _referenceHistogram ?? [],
|
||||
Tolerance = (double)(Tolerance ?? 0.30m)
|
||||
AverageSpline = _averageSpline ?? [],
|
||||
CellSplines = new Dictionary<int, float[]>(_cellSplines)
|
||||
};
|
||||
|
||||
private async Task PreviewLoopAsync(CancellationToken ct)
|
||||
@@ -457,6 +531,11 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
owned = true;
|
||||
}
|
||||
_patternService.DrawPattern(displayFrame, pattern, GridColor);
|
||||
if (_cellSplines.Count > 0)
|
||||
{
|
||||
var overrides = new HashSet<int>(_cellSplines.Keys);
|
||||
_patternService.DrawOverrideMarkers(displayFrame, pattern, overrides);
|
||||
}
|
||||
}
|
||||
|
||||
var bitmap = ImageConverter.MatToAvaloniaBitmap(displayFrame);
|
||||
@@ -484,11 +563,28 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pattern geometry changed — cell indices are no longer valid, so any per-cell
|
||||
/// overrides and the seeded average spline must be discarded. The user has to
|
||||
/// click 'Set Reference' again before testing.
|
||||
/// </summary>
|
||||
private void InvalidateSplinesForPatternChange()
|
||||
{
|
||||
if (_cellSplines.Count == 0 && _averageSpline == null)
|
||||
return;
|
||||
|
||||
_cellSplines.Clear();
|
||||
_averageSpline = null;
|
||||
_averageReferenceHistogram = null;
|
||||
HasAverageSpline = false;
|
||||
StatusText = "Pattern changed — splines cleared. Click 'Set Reference'.";
|
||||
}
|
||||
|
||||
partial void OnApplyCalibrationChanged(bool value) => RefreshPreview();
|
||||
partial void OnRowsChanged(decimal? value) => RefreshPreview();
|
||||
partial void OnColsChanged(decimal? value) => RefreshPreview();
|
||||
partial void OnSelectedCellShapeChanged(CellShape value) => RefreshPreview();
|
||||
partial void OnPaddingChanged(decimal? value) => RefreshPreview();
|
||||
partial void OnRowsChanged(decimal? value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
|
||||
partial void OnColsChanged(decimal? value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
|
||||
partial void OnSelectedCellShapeChanged(CellShape value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
|
||||
partial void OnPaddingChanged(decimal? value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
|
||||
partial void OnRoiXChanged(decimal? value) => RefreshPreview();
|
||||
partial void OnRoiYChanged(decimal? value) => RefreshPreview();
|
||||
partial void OnRoiWidthChanged(decimal? value) => RefreshPreview();
|
||||
@@ -504,6 +600,13 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
_singleCameraVm.PropertyChanged -= OnSingleCameraVmPropertyChanged;
|
||||
if (_cellDialog != null)
|
||||
{
|
||||
_cellDialog.Closed -= OnCellDialogClosed;
|
||||
_cellDialog.Close();
|
||||
_cellDialog = null;
|
||||
_cellDialogVm = null;
|
||||
}
|
||||
StopPreview();
|
||||
_lastFrame?.Dispose();
|
||||
_referenceFrame?.Dispose();
|
||||
|
||||
@@ -1,11 +1,72 @@
|
||||
using Avalonia.Media.Imaging;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LindtLeerformPlugin.Services;
|
||||
|
||||
namespace LindtLeerformPlugin.ViewModels;
|
||||
|
||||
public class CellHistogramViewModel
|
||||
public partial class CellHistogramViewModel : ObservableObject
|
||||
{
|
||||
public string Title { get; set; } = "Cell Histogram";
|
||||
public Bitmap? ReferenceImage { get; set; }
|
||||
public Bitmap? CellImage { get; set; }
|
||||
public string DistanceText { get; set; } = string.Empty;
|
||||
[ObservableProperty] private string _title = "Cell Histogram";
|
||||
public int CellIndex { get; set; }
|
||||
public int[] Bins { get; init; } = [5, 5, 5];
|
||||
|
||||
[ObservableProperty] private float[]? _cellHistogram;
|
||||
[ObservableProperty] private float[]? _cellSpline;
|
||||
[ObservableProperty] private float[] _averageSpline = new float[SplineCurve.KnotCount];
|
||||
[ObservableProperty] private string _verdictText = string.Empty;
|
||||
|
||||
public bool HasOverride => CellSpline is { Length: SplineCurve.KnotCount };
|
||||
|
||||
/// <summary>
|
||||
/// Invoked by the view when the user releases the mouse after dragging a spline handle.
|
||||
/// Lets the calibration window re-test and refresh its preview while the dialog stays open.
|
||||
/// </summary>
|
||||
public Action<float[], float[]?>? DragCompleted { get; set; }
|
||||
|
||||
public void RaiseDragCompleted() => DragCompleted?.Invoke(AverageSpline, CellSpline);
|
||||
|
||||
partial void OnCellSplineChanged(float[]? value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasOverride));
|
||||
UpdateVerdict();
|
||||
}
|
||||
|
||||
partial void OnAverageSplineChanged(float[] value) => UpdateVerdict();
|
||||
partial void OnCellHistogramChanged(float[]? value) => UpdateVerdict();
|
||||
|
||||
[RelayCommand]
|
||||
private void CreateOverride()
|
||||
{
|
||||
// Seed from THIS cell's histogram so the user gets a useful starting envelope.
|
||||
CellSpline = SplineCurve.CreateDefault(CellHistogram);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveOverride()
|
||||
{
|
||||
CellSpline = null;
|
||||
}
|
||||
|
||||
private void UpdateVerdict()
|
||||
{
|
||||
var hist = CellHistogram;
|
||||
if (hist == null || hist.Length == 0)
|
||||
{
|
||||
VerdictText = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
var spline = CellSpline is { Length: SplineCurve.KnotCount } ? CellSpline : AverageSpline;
|
||||
if (spline is not { Length: SplineCurve.KnotCount })
|
||||
{
|
||||
VerdictText = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
var (isGood, exceed) = LindtLeerformPlugin.Services.LeerformPatternRecognitionService
|
||||
.EvaluateAgainstSpline(hist, spline);
|
||||
var label = isGood ? "GOOD" : "BAD";
|
||||
var splineLabel = HasOverride ? "cell spline" : "average spline";
|
||||
VerdictText = $"Verdict: {label} Max exceedance: {exceed:F4} Using: {splineLabel}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user