using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LindtLeerformPlugin.Models; using LindtLeerformPlugin.Services; using OpenCvSharp; using VisionBuilder.UI.Common.Processing; namespace LindtLeerformPlugin.ViewModels; public partial class CalibrationWindowViewModel : ObservableObject, IDisposable { private readonly IImageSource _imageSource; private readonly LeerformSettings _settings; private CancellationTokenSource? _previewCts; private Mat? _lastFrame; [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 string _calibrationImageDirectory; [ObservableProperty] private double _rmsError; public CalibrationWindowViewModel(IImageSource imageSource, LeerformSettings settings) { _imageSource = imageSource; _settings = settings; _calibrationImageDirectory = settings.CalibrationImageDirectory; _isCalibrated = settings.CalibrationData is { CameraMatrix.Length: > 0 }; _rmsError = settings.CalibrationData?.RmsError ?? 0; UpdateCapturedImageCount(); } 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); _settings.CalibrationData = data; _settings.CalibrationImageDirectory = CalibrationImageDirectory; 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"; } private async Task PreviewLoopAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { var frame = await _imageSource.GetImage(ct); _lastFrame?.Dispose(); _lastFrame = frame; var bitmap = ImageConverter.MatToAvaloniaBitmap(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; } } } public void Dispose() { StopPreview(); _lastFrame?.Dispose(); } }