617 lines
20 KiB
C#
617 lines
20 KiB
C#
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;
|
|
|
|
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;
|
|
|
|
// 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;
|
|
[ObservableProperty] private bool _isPreviewRunning;
|
|
[ObservableProperty] private bool _isCalibrated;
|
|
[ObservableProperty] private bool _applyCalibration;
|
|
[ObservableProperty] private string _calibrationImageDirectory;
|
|
[ObservableProperty] private double _rmsError;
|
|
|
|
// 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 bool _hasAverageSpline;
|
|
|
|
// 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;
|
|
|
|
var calibrationData = CalibrationDataStore.Load();
|
|
_isCalibrated = calibrationData is { CameraMatrix.Length: > 0 };
|
|
_rmsError = calibrationData?.RmsError ?? 0;
|
|
|
|
if (_isCalibrated)
|
|
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()
|
|
{
|
|
if (Directory.Exists(CalibrationImageDirectory))
|
|
CapturedImageCount = Directory.GetFiles(CalibrationImageDirectory, "*.png").Length;
|
|
else
|
|
CapturedImageCount = 0;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void StartPreview()
|
|
{
|
|
if (IsPreviewRunning) return;
|
|
|
|
IsPreviewRunning = true;
|
|
_previewCts = new CancellationTokenSource();
|
|
_ = PreviewLoopAsync(_previewCts.Token);
|
|
StatusText = "Preview running";
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void StopPreview()
|
|
{
|
|
if (!IsPreviewRunning) return;
|
|
|
|
_previewCts?.Cancel();
|
|
IsPreviewRunning = false;
|
|
StatusText = "Preview stopped";
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void CaptureImage()
|
|
{
|
|
if (_lastFrame == null || _lastFrame.Empty()) return;
|
|
|
|
Directory.CreateDirectory(CalibrationImageDirectory);
|
|
var filename = $"calib_{DateTime.Now:yyyyMMdd_HHmmss_fff}.png";
|
|
var path = Path.Combine(CalibrationImageDirectory, filename);
|
|
Cv2.ImWrite(path, _lastFrame);
|
|
UpdateCapturedImageCount();
|
|
StatusText = $"Captured: {filename}";
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void RunCalibration()
|
|
{
|
|
try
|
|
{
|
|
StatusText = "Calibrating...";
|
|
var patternSize = new Size(_settings.CheckerboardCols, _settings.CheckerboardRows);
|
|
var data = CameraCalibrationService.Calibrate(CalibrationImageDirectory, patternSize);
|
|
|
|
CalibrationDataStore.Save(data);
|
|
_settings.CalibrationImageDirectory = CalibrationImageDirectory;
|
|
|
|
LoadCalibrationMats(data);
|
|
IsCalibrated = true;
|
|
RmsError = data.RmsError;
|
|
StatusText = $"Calibrated (RMS: {data.RmsError:F4})";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusText = $"Calibration error: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void ClearCalibrationImages()
|
|
{
|
|
if (!Directory.Exists(CalibrationImageDirectory)) return;
|
|
|
|
foreach (var file in Directory.GetFiles(CalibrationImageDirectory, "*.png"))
|
|
File.Delete(file);
|
|
|
|
UpdateCapturedImageCount();
|
|
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();
|
|
|
|
private void RunTestPattern()
|
|
{
|
|
using var frame = GetAnalysisFrame();
|
|
if (frame == null)
|
|
{
|
|
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);
|
|
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 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()
|
|
{
|
|
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;
|
|
|
|
_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;
|
|
|
|
_referenceFrame?.Dispose();
|
|
_referenceFrame = LeerformRecipeStore.DecodeThumbnail(recipe);
|
|
|
|
StatusText = HasAverageSpline
|
|
? $"Loaded recipe '{recipeName}'"
|
|
: $"Loaded recipe '{recipeName}' — no spline; click 'Set Reference'.";
|
|
}
|
|
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],
|
|
AverageSpline = _averageSpline ?? [],
|
|
CellSplines = new Dictionary<int, float[]>(_cellSplines)
|
|
};
|
|
|
|
private async Task PreviewLoopAsync(CancellationToken ct)
|
|
{
|
|
while (!ct.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var frame = await _imageSource.GetImage(ct);
|
|
|
|
_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(() =>
|
|
{
|
|
var old = PreviewImage;
|
|
PreviewImage = bitmap;
|
|
old?.Dispose();
|
|
});
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await Dispatcher.UIThread.InvokeAsync(() =>
|
|
StatusText = $"Preview error: {ex.Message}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (_cellSplines.Count > 0)
|
|
{
|
|
var overrides = new HashSet<int>(_cellSplines.Keys);
|
|
_patternService.DrawOverrideMarkers(displayFrame, pattern, overrides);
|
|
}
|
|
}
|
|
|
|
var bitmap = ImageConverter.MatToAvaloniaBitmap(displayFrame);
|
|
|
|
if (owned)
|
|
displayFrame.Dispose();
|
|
|
|
return bitmap;
|
|
}
|
|
|
|
private void RefreshPreview()
|
|
{
|
|
if (_lastFrame == null || _lastFrame.Empty()) return;
|
|
|
|
try
|
|
{
|
|
var bitmap = RenderFrame(_lastFrame);
|
|
var old = PreviewImage;
|
|
PreviewImage = bitmap;
|
|
old?.Dispose();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatusText = $"Preview error: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
/// <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 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();
|
|
_calibDistCoeffs?.Dispose();
|
|
(_calibCameraMatrix, _calibDistCoeffs) = CameraCalibrationService.LoadCalibrationMats(data);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_singleCameraVm.PropertyChanged -= OnSingleCameraVmPropertyChanged;
|
|
if (_cellDialog != null)
|
|
{
|
|
_cellDialog.Closed -= OnCellDialogClosed;
|
|
_cellDialog.Close();
|
|
_cellDialog = null;
|
|
_cellDialogVm = null;
|
|
}
|
|
StopPreview();
|
|
_lastFrame?.Dispose();
|
|
_referenceFrame?.Dispose();
|
|
_calibCameraMatrix?.Dispose();
|
|
_calibDistCoeffs?.Dispose();
|
|
}
|
|
}
|