cell histogram
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia.Threading;
|
||||
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;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
|
||||
namespace LindtLeerformPlugin.ViewModels;
|
||||
@@ -11,10 +16,16 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IImageSource _imageSource;
|
||||
private readonly LeerformSettings _settings;
|
||||
private readonly SingleCameraVM _singleCameraVm;
|
||||
private readonly LeerformPatternRecognitionService _patternService;
|
||||
private readonly LeerformRecipeStore _recipeStore;
|
||||
|
||||
private CancellationTokenSource? _previewCts;
|
||||
private Mat? _lastFrame;
|
||||
private Mat? _calibCameraMatrix;
|
||||
private Mat? _calibDistCoeffs;
|
||||
private Mat? _referenceFrame;
|
||||
private float[]? _referenceHistogram;
|
||||
|
||||
[ObservableProperty] private Avalonia.Media.Imaging.Bitmap? _previewImage;
|
||||
[ObservableProperty] private string _statusText = "Ready";
|
||||
@@ -25,10 +36,39 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
[ObservableProperty] private string _calibrationImageDirectory;
|
||||
[ObservableProperty] private double _rmsError;
|
||||
|
||||
public CalibrationWindowViewModel(IImageSource imageSource, LeerformSettings settings)
|
||||
// Pattern
|
||||
[ObservableProperty] private decimal? _rows = 4m;
|
||||
[ObservableProperty] private decimal? _cols = 5m;
|
||||
[ObservableProperty] private CellShape _selectedCellShape = CellShape.Round;
|
||||
[ObservableProperty] private decimal? _padding = 5m;
|
||||
|
||||
// ROI
|
||||
[ObservableProperty] private decimal? _roiX = 0m;
|
||||
[ObservableProperty] private decimal? _roiY = 0m;
|
||||
[ObservableProperty] private decimal? _roiWidth = 0m;
|
||||
[ObservableProperty] private decimal? _roiHeight = 0m;
|
||||
|
||||
// Detection
|
||||
[ObservableProperty] private decimal? _tolerance = 0.30m;
|
||||
[ObservableProperty] private bool _hasReferenceHistogram;
|
||||
|
||||
// Recipe
|
||||
[ObservableProperty] private string _currentRecipeName = "default";
|
||||
|
||||
public CellShape[] AvailableCellShapes { get; } = Enum.GetValues<CellShape>();
|
||||
|
||||
public CalibrationWindowViewModel(
|
||||
IImageSource imageSource,
|
||||
LeerformSettings settings,
|
||||
SingleCameraVM singleCameraVm,
|
||||
LeerformPatternRecognitionService patternService,
|
||||
LeerformRecipeStore recipeStore)
|
||||
{
|
||||
_imageSource = imageSource;
|
||||
_settings = settings;
|
||||
_singleCameraVm = singleCameraVm;
|
||||
_patternService = patternService;
|
||||
_recipeStore = recipeStore;
|
||||
|
||||
_calibrationImageDirectory = settings.CalibrationImageDirectory;
|
||||
|
||||
@@ -40,6 +80,24 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
LoadCalibrationMats(calibrationData!);
|
||||
|
||||
UpdateCapturedImageCount();
|
||||
|
||||
_currentRecipeName = _singleCameraVm.SelectedRecipe?.RecipeName ?? "default";
|
||||
_singleCameraVm.PropertyChanged += OnSingleCameraVmPropertyChanged;
|
||||
|
||||
TryLoadRecipe(_currentRecipeName);
|
||||
}
|
||||
|
||||
private void OnSingleCameraVmPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName != nameof(SingleCameraVM.SelectedRecipe))
|
||||
return;
|
||||
|
||||
var name = _singleCameraVm.SelectedRecipe?.RecipeName ?? "default";
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
CurrentRecipeName = name;
|
||||
TryLoadRecipe(name);
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateCapturedImageCount()
|
||||
@@ -119,6 +177,217 @@ 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();
|
||||
_referenceHistogram = _patternService.ComputeReferenceHistogram(frame, pattern);
|
||||
|
||||
_referenceFrame?.Dispose();
|
||||
_referenceFrame = frame.Clone();
|
||||
|
||||
HasReferenceHistogram = true;
|
||||
StatusText = $"Reference set ({_referenceHistogram.Length} bins)";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to compute reference histogram");
|
||||
StatusText = $"Reference error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void TestPattern()
|
||||
{
|
||||
using var frame = GetAnalysisFrame();
|
||||
if (frame == null)
|
||||
{
|
||||
StatusText = "No active frame to test";
|
||||
return;
|
||||
}
|
||||
if (_referenceHistogram == null)
|
||||
{
|
||||
StatusText = "Set reference histogram first";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var recipe = BuildCurrentRecipe();
|
||||
var results = _patternService.EvaluateCells(frame, recipe);
|
||||
using var overlay = _patternService.RenderOverlay(frame, recipe.Pattern, results);
|
||||
var bitmap = ImageConverter.MatToAvaloniaBitmap(overlay);
|
||||
|
||||
var old = PreviewImage;
|
||||
PreviewImage = bitmap;
|
||||
old?.Dispose();
|
||||
|
||||
var bad = results.Count(r => !r.IsGood);
|
||||
StatusText = $"Test: {results.Count - bad} good / {bad} bad";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to test pattern");
|
||||
StatusText = $"Test error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private Mat? GetAnalysisFrame()
|
||||
{
|
||||
if (_lastFrame == null || _lastFrame.Empty())
|
||||
return null;
|
||||
|
||||
var frame = new Mat();
|
||||
if (_calibCameraMatrix != null && _calibDistCoeffs != null)
|
||||
Cv2.Undistort(_lastFrame, frame, _calibCameraMatrix, _calibDistCoeffs);
|
||||
else
|
||||
_lastFrame.CopyTo(frame);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
public int? HitTestCell(int imageX, int imageY)
|
||||
{
|
||||
return _patternService.FindCellAtPoint(imageX, imageY, BuildCurrentPattern());
|
||||
}
|
||||
|
||||
public async Task ShowCellHistogramAsync(int imageX, int imageY, Avalonia.Controls.Window owner)
|
||||
{
|
||||
using var frame = GetAnalysisFrame();
|
||||
if (frame == null) return;
|
||||
|
||||
var pattern = BuildCurrentPattern();
|
||||
var cellIndex = _patternService.FindCellAtPoint(imageX, imageY, pattern);
|
||||
if (cellIndex == null) return;
|
||||
|
||||
if (_referenceHistogram == null)
|
||||
{
|
||||
StatusText = "Set reference histogram first";
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
Title = $"Cell #{cellIndex.Value}",
|
||||
ReferenceImage = refBitmap,
|
||||
CellImage = cellBitmap,
|
||||
DistanceText = $"Distance: {distance:F4} Tolerance: {tolerance:F2} Verdict: {verdict}"
|
||||
};
|
||||
|
||||
var window = new CellHistogramWindow { DataContext = dialogVm };
|
||||
await window.ShowDialog(owner);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to show cell histogram");
|
||||
StatusText = $"Histogram error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveRecipe()
|
||||
{
|
||||
try
|
||||
{
|
||||
var recipe = BuildCurrentRecipe();
|
||||
recipe.RecipeName = CurrentRecipeName;
|
||||
|
||||
if (_referenceFrame != null && !_referenceFrame.Empty())
|
||||
{
|
||||
using var thumb = new Mat();
|
||||
Cv2.Resize(_referenceFrame, thumb, new Size(128, 128), 0, 0, InterpolationFlags.Area);
|
||||
var bytes = thumb.ImEncode(".png");
|
||||
recipe.ThumbnailBase64 = Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
_recipeStore.Save(recipe);
|
||||
StatusText = $"Saved recipe '{recipe.RecipeName}'";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to save recipe");
|
||||
StatusText = $"Save error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void TryLoadRecipe(string recipeName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var recipe = _recipeStore.Load(recipeName);
|
||||
if (recipe == null)
|
||||
return;
|
||||
|
||||
Rows = recipe.Pattern.Rows;
|
||||
Cols = recipe.Pattern.Cols;
|
||||
SelectedCellShape = recipe.Pattern.Shape;
|
||||
Padding = recipe.Pattern.Padding;
|
||||
RoiX = recipe.Pattern.RoiX;
|
||||
RoiY = recipe.Pattern.RoiY;
|
||||
RoiWidth = recipe.Pattern.RoiWidth;
|
||||
RoiHeight = recipe.Pattern.RoiHeight;
|
||||
Tolerance = (decimal)recipe.Tolerance;
|
||||
|
||||
_referenceHistogram = recipe.ReferenceHistogram is { Length: > 0 }
|
||||
? recipe.ReferenceHistogram
|
||||
: null;
|
||||
HasReferenceHistogram = _referenceHistogram != null;
|
||||
|
||||
_referenceFrame?.Dispose();
|
||||
_referenceFrame = LeerformRecipeStore.DecodeThumbnail(recipe);
|
||||
|
||||
StatusText = $"Loaded recipe '{recipeName}'";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to load recipe '{Recipe}'", recipeName);
|
||||
}
|
||||
}
|
||||
|
||||
private CellPattern BuildCurrentPattern() => new()
|
||||
{
|
||||
Rows = (int)(Rows ?? 1m),
|
||||
Cols = (int)(Cols ?? 1m),
|
||||
Shape = SelectedCellShape,
|
||||
Padding = (int)(Padding ?? 0m),
|
||||
RoiX = (int)(RoiX ?? 0m),
|
||||
RoiY = (int)(RoiY ?? 0m),
|
||||
RoiWidth = (int)(RoiWidth ?? 0m),
|
||||
RoiHeight = (int)(RoiHeight ?? 0m)
|
||||
};
|
||||
|
||||
private LeerformRecipe BuildCurrentRecipe() => new()
|
||||
{
|
||||
RecipeName = CurrentRecipeName,
|
||||
Pattern = BuildCurrentPattern(),
|
||||
HistogramBins = [5, 5, 5],
|
||||
ReferenceHistogram = _referenceHistogram ?? [],
|
||||
Tolerance = (double)(Tolerance ?? 0.30m)
|
||||
};
|
||||
|
||||
private async Task PreviewLoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
@@ -130,6 +399,19 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
_lastFrame?.Dispose();
|
||||
_lastFrame = frame;
|
||||
|
||||
if ((RoiWidth ?? 0m) <= 0m || (RoiHeight ?? 0m) <= 0m)
|
||||
{
|
||||
var w = frame.Cols;
|
||||
var h = frame.Rows;
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
RoiX = 0;
|
||||
RoiY = 0;
|
||||
RoiWidth = w;
|
||||
RoiHeight = h;
|
||||
});
|
||||
}
|
||||
|
||||
var bitmap = RenderFrame(frame);
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
@@ -152,24 +434,40 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Scalar GridColor = new(0, 255, 255); // BGR yellow
|
||||
|
||||
private Avalonia.Media.Imaging.Bitmap RenderFrame(Mat frame)
|
||||
{
|
||||
var displayFrame = frame;
|
||||
var owned = false;
|
||||
|
||||
if (ApplyCalibration && _calibCameraMatrix != null && _calibDistCoeffs != null)
|
||||
{
|
||||
displayFrame = new Mat();
|
||||
Cv2.Undistort(frame, displayFrame, _calibCameraMatrix, _calibDistCoeffs);
|
||||
owned = true;
|
||||
}
|
||||
|
||||
var pattern = BuildCurrentPattern();
|
||||
if (pattern.Rows > 0 && pattern.Cols > 0 && pattern.RoiWidth > 0 && pattern.RoiHeight > 0)
|
||||
{
|
||||
if (!owned)
|
||||
{
|
||||
displayFrame = frame.Clone();
|
||||
owned = true;
|
||||
}
|
||||
_patternService.DrawPattern(displayFrame, pattern, GridColor);
|
||||
}
|
||||
|
||||
var bitmap = ImageConverter.MatToAvaloniaBitmap(displayFrame);
|
||||
|
||||
if (displayFrame != frame)
|
||||
if (owned)
|
||||
displayFrame.Dispose();
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
partial void OnApplyCalibrationChanged(bool value)
|
||||
private void RefreshPreview()
|
||||
{
|
||||
if (_lastFrame == null || _lastFrame.Empty()) return;
|
||||
|
||||
@@ -186,6 +484,16 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
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 OnRoiXChanged(decimal? value) => RefreshPreview();
|
||||
partial void OnRoiYChanged(decimal? value) => RefreshPreview();
|
||||
partial void OnRoiWidthChanged(decimal? value) => RefreshPreview();
|
||||
partial void OnRoiHeightChanged(decimal? value) => RefreshPreview();
|
||||
|
||||
private void LoadCalibrationMats(Models.CalibrationData data)
|
||||
{
|
||||
_calibCameraMatrix?.Dispose();
|
||||
@@ -195,8 +503,10 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_singleCameraVm.PropertyChanged -= OnSingleCameraVmPropertyChanged;
|
||||
StopPreview();
|
||||
_lastFrame?.Dispose();
|
||||
_referenceFrame?.Dispose();
|
||||
_calibCameraMatrix?.Dispose();
|
||||
_calibDistCoeffs?.Dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user