using System.Globalization; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using OpenCvSharp; using VisionBuilder.UI.RaspberryAcquisition.Models; using VisionBuilder.UI.RaspberryAcquisition.Services; namespace VisionBuilder.UI.RaspberryAcquisition.ViewModels; public partial class MainWindowViewModel : ObservableObject, IDisposable { private const string CalibrationFile = "calibration.json"; private ConfigurableLibcameraSource? _source; private CancellationTokenSource? _previewCts; private Mat? _lastFrame; private Mat? _cameraMatrix; private Mat? _distCoeffs; [ObservableProperty] private Avalonia.Media.Imaging.Bitmap? _previewImage; [ObservableProperty] private string _statusText = "Stopped"; [ObservableProperty] private int _frameCount; [ObservableProperty] private bool _isRunning; [ObservableProperty] private bool _isCalibrated; [ObservableProperty] private string _width = "2028"; [ObservableProperty] private string _height = "1520"; [ObservableProperty] private string _shutterSpeed = "10000"; [ObservableProperty] private string _framerate = "2"; [ObservableProperty] private string _awbGainRed = "3.86"; [ObservableProperty] private string _awbGainBlue = "1.46"; [ObservableProperty] private string _saveDirectory = "/home/oem/captures"; [ObservableProperty] private string _calibrationDirectory = "/home/oem/captures"; public MainWindowViewModel() { TryLoadCalibration(); } private void TryLoadCalibration() { if (!File.Exists(CalibrationFile)) return; try { (_cameraMatrix, _distCoeffs) = CameraCalibrationService.LoadCalibration(CalibrationFile); IsCalibrated = true; StatusText = "Calibration loaded"; } catch (Exception ex) { StatusText = $"Calibration load error: {ex.Message}"; } } [RelayCommand] private void StartCapture() { if (IsRunning) return; var settings = BuildSettings(); if (settings == null) return; _source = new ConfigurableLibcameraSource(settings); _source.StatusChanged += status => Dispatcher.UIThread.Post(() => StatusText = status); _source.Start(); IsRunning = true; _previewCts = new CancellationTokenSource(); _ = PreviewLoopAsync(_previewCts.Token); } [RelayCommand] private void StopCapture() { if (!IsRunning) return; _previewCts?.Cancel(); _source?.Stop(); _source?.Dispose(); _source = null; IsRunning = false; StatusText = "Stopped"; } [RelayCommand] private void ApplySettings() { if (!IsRunning || _source == null) return; var settings = BuildSettings(); if (settings == null) return; _source.UpdateSettings(settings); } [RelayCommand] private void SaveImage() { if (_lastFrame == null || _lastFrame.Empty()) return; Directory.CreateDirectory(SaveDirectory); var filename = $"capture_{DateTime.Now:yyyyMMdd_HHmmss}.png"; var path = Path.Combine(SaveDirectory, filename); Cv2.ImWrite(path, _lastFrame); StatusText = $"Saved: {filename}"; } [RelayCommand] private void CalibrateCamera() { try { StatusText = "Calibrating..."; var data = CameraCalibrationService.Calibrate(CalibrationDirectory, new Size(9, 6)); CameraCalibrationService.SaveCalibration(data, CalibrationFile); _cameraMatrix?.Dispose(); _distCoeffs?.Dispose(); (_cameraMatrix, _distCoeffs) = CameraCalibrationService.LoadCalibration(CalibrationFile); IsCalibrated = true; StatusText = $"Calibrated (RMS: {data.RmsError:F4})"; } catch (Exception ex) { StatusText = $"Calibration error: {ex.Message}"; } } private async Task PreviewLoopAsync(CancellationToken ct) { while (!ct.IsCancellationRequested && _source != null) { try { var frame = await _source.GetImage(ct); if (_cameraMatrix != null && _distCoeffs != null) { var corrected = new Mat(); Cv2.Undistort(frame, corrected, _cameraMatrix, _distCoeffs); frame.Dispose(); frame = corrected; } _lastFrame?.Dispose(); _lastFrame = frame; var bitmap = ImageConverter.MatToAvaloniaBitmap(frame); await Dispatcher.UIThread.InvokeAsync(() => { var old = PreviewImage; PreviewImage = bitmap; FrameCount = _source?.FrameCount ?? 0; old?.Dispose(); }); } catch (OperationCanceledException) { break; } catch (Exception ex) { await Dispatcher.UIThread.InvokeAsync(() => StatusText = $"Preview error: {ex.Message}"); break; } } } private CaptureSettings? BuildSettings() { if (!int.TryParse(Width, out var w) || !int.TryParse(Height, out var h) || !int.TryParse(ShutterSpeed, out var s) || !int.TryParse(Framerate, out var f) || !double.TryParse(AwbGainRed, CultureInfo.InvariantCulture, out var r) || !double.TryParse(AwbGainBlue, CultureInfo.InvariantCulture, out var b)) { StatusText = "Error: invalid settings values"; return null; } return new CaptureSettings { Width = w, Height = h, ShutterSpeed = s, Framerate = f, AwbGainRed = r, AwbGainBlue = b }; } public void Dispose() { StopCapture(); _lastFrame?.Dispose(); _cameraMatrix?.Dispose(); _distCoeffs?.Dispose(); } }