profile influences configuration
This commit is contained in:
EugeneTes
2026-05-15 11:54:36 +02:00
parent 2be13501fa
commit 8704d0b5ac
42 changed files with 2467 additions and 1023 deletions

View File

@@ -4,7 +4,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LindtLeerformPlugin.Models;
using LindtLeerformPlugin.Services;
using LindtLeerformPlugin.Views;
using OpenCvSharp;
using Serilog;
using VisionBuilder.UI.Common;
@@ -26,16 +25,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
private Mat? _calibDistCoeffs;
private Mat? _referenceFrame;
// 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";
[ObservableProperty] private int _capturedImageCount;
@@ -57,8 +46,11 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
[ObservableProperty] private decimal? _roiWidth = 0m;
[ObservableProperty] private decimal? _roiHeight = 0m;
// Detection
[ObservableProperty] private bool _hasAverageSpline;
// Detection thresholds (LAB-based, see LeerformPatternRecognitionService.DetectChocolateMask)
[ObservableProperty] private decimal? _aThreshold = 140m;
[ObservableProperty] private decimal? _lThreshold = 100m;
[ObservableProperty] private decimal? _minBlobArea = 100m;
[ObservableProperty] private decimal? _maxBlobArea = 5000m;
// Recipe
[ObservableProperty] private string _currentRecipeName = "default";
@@ -185,37 +177,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
StatusText = "Calibration images cleared";
}
[RelayCommand]
private void SetReferenceHistogram()
{
using var frame = GetAnalysisFrame();
if (frame == null)
{
StatusText = "No active frame to use as reference";
return;
}
try
{
var pattern = BuildCurrentPattern();
_averageReferenceHistogram = _patternService.ComputeReferenceHistogram(frame, pattern);
_averageSpline = SplineCurve.CreateDefault(_averageReferenceHistogram);
_cellSplines.Clear();
_referenceFrame?.Dispose();
_referenceFrame = frame.Clone();
HasAverageSpline = true;
StatusText = "Average spline seeded; per-cell overrides cleared.";
RefreshPreview();
}
catch (Exception ex)
{
Log.Error(ex, "Failed to seed average spline");
StatusText = $"Reference error: {ex.Message}";
}
}
[RelayCommand]
private void TestPattern() => RunTestPattern();
@@ -227,24 +188,21 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
StatusText = "No active frame to test";
return;
}
if (_averageSpline == null)
{
StatusText = "Set reference first";
return;
}
try
{
var recipe = BuildCurrentRecipe();
var results = _patternService.EvaluateCells(frame, recipe);
var overrides = new HashSet<int>(_cellSplines.Keys);
using var overlay = _patternService.RenderOverlay(frame, recipe.Pattern, results, overrides);
using var overlay = _patternService.RenderOverlay(frame, recipe.Pattern, results);
var bitmap = ImageConverter.MatToAvaloniaBitmap(overlay);
var old = PreviewImage;
PreviewImage = bitmap;
old?.Dispose();
_referenceFrame?.Dispose();
_referenceFrame = frame.Clone();
var bad = results.Count(r => !r.IsGood);
StatusText = $"Test: {results.Count - bad} good / {bad} bad";
}
@@ -269,105 +227,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
return frame;
}
public int? HitTestCell(int imageX, int imageY)
{
return _patternService.FindCellAtPoint(imageX, imageY, BuildCurrentPattern());
}
public Task ShowCellHistogramAsync(int imageX, int imageY, Avalonia.Controls.Window owner)
{
using var frame = GetAnalysisFrame();
if (frame == null) return Task.CompletedTask;
var pattern = BuildCurrentPattern();
var cellIndex = _patternService.FindCellAtPoint(imageX, imageY, pattern);
if (cellIndex == null) return Task.CompletedTask;
if (_averageSpline is not { Length: SplineCurve.KnotCount })
{
StatusText = "Set reference first";
return Task.CompletedTask;
}
try
{
// Already open: commit the previous cell's edits and switch to the new cell.
if (_cellDialog != null && _cellDialogVm != null)
{
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();
};
_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]
private void SaveRecipe()
{
@@ -411,29 +270,15 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
RoiWidth = recipe.Pattern.RoiWidth;
RoiHeight = recipe.Pattern.RoiHeight;
_averageReferenceHistogram = null;
_averageSpline = recipe.AverageSpline is { Length: SplineCurve.KnotCount }
? recipe.AverageSpline
: 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;
AThreshold = recipe.AThreshold;
LThreshold = recipe.LThreshold;
MinBlobArea = recipe.MinBlobArea;
MaxBlobArea = recipe.MaxBlobArea;
_referenceFrame?.Dispose();
_referenceFrame = LeerformRecipeStore.DecodeThumbnail(recipe);
StatusText = HasAverageSpline
? $"Loaded recipe '{recipeName}'"
: $"Loaded recipe '{recipeName}' — no spline; click 'Set Reference'.";
StatusText = $"Loaded recipe '{recipeName}'";
}
catch (Exception ex)
{
@@ -457,9 +302,10 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
{
RecipeName = CurrentRecipeName,
Pattern = BuildCurrentPattern(),
HistogramBins = [5, 5, 5],
AverageSpline = _averageSpline ?? [],
CellSplines = new Dictionary<int, float[]>(_cellSplines)
AThreshold = (int)(AThreshold ?? 140m),
LThreshold = (int)(LThreshold ?? 100m),
MinBlobArea = (int)(MinBlobArea ?? 100m),
MaxBlobArea = (int)(MaxBlobArea ?? 5000m)
};
private async Task PreviewLoopAsync(CancellationToken ct)
@@ -531,11 +377,6 @@ 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);
@@ -563,32 +404,19 @@ 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) { 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 OnRowsChanged(decimal? value) => RefreshPreview();
partial void OnColsChanged(decimal? value) => RefreshPreview();
partial void OnSelectedCellShapeChanged(CellShape value) => RefreshPreview();
partial void OnPaddingChanged(decimal? value) => RefreshPreview();
partial void OnRoiXChanged(decimal? value) => RefreshPreview();
partial void OnRoiYChanged(decimal? value) => RefreshPreview();
partial void OnRoiWidthChanged(decimal? value) => RefreshPreview();
partial void OnRoiHeightChanged(decimal? value) => RefreshPreview();
partial void OnAThresholdChanged(decimal? value) => RefreshPreview();
partial void OnLThresholdChanged(decimal? value) => RefreshPreview();
partial void OnMinBlobAreaChanged(decimal? value) => RefreshPreview();
partial void OnMaxBlobAreaChanged(decimal? value) => RefreshPreview();
private void LoadCalibrationMats(Models.CalibrationData data)
{
@@ -600,13 +428,6 @@ 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();