using Emgu.CV; using System.Collections.ObjectModel; using System.IO; using System.Windows.Media.Imaging; using Res = XP.Camera.Calibration.Resources.CalibrationResources; namespace XP.Camera.Calibration.ViewModels; public class ChessboardCalibrationViewModel : BindableBase { private readonly ICalibrationDialogService _dialogService; private readonly ChessboardCalibrator _calibrator = new(); private readonly ObservableCollection _imagePaths = new(); private BitmapSource? _imageSource; private string _statusText = Res.ChessboardStatusReady; private int _chessboardWidth = 11; private int _chessboardHeight = 8; private float _squareSize = 15; private int _selectedImageIndex = -1; private bool _isCalibrating = false; private double _progressValue = 0; private string _progressText = ""; public ChessboardCalibrationViewModel(ICalibrationDialogService dialogService) { _dialogService = dialogService; ImageFileNames = new ObservableCollection(); AddImagesCommand = new DelegateCommand(AddImages); ClearImagesCommand = new DelegateCommand(ClearImages, CanClearImages) .ObservesProperty(() => ImageFileNames.Count); CalibrateCommand = new DelegateCommand(async () => await CalibrateAsync(), CanCalibrate) .ObservesProperty(() => ImageFileNames.Count) .ObservesProperty(() => IsCalibrating); SaveCalibrationCommand = new DelegateCommand(SaveCalibration); LoadCalibrationCommand = new DelegateCommand(LoadCalibration); UndistortImageCommand = new DelegateCommand(UndistortImage); _calibrator.ProgressChanged += OnCalibrationProgressChanged; } public ObservableCollection ImageFileNames { get; } public BitmapSource? ImageSource { get => _imageSource; set => SetProperty(ref _imageSource, value); } public string StatusText { get => _statusText; set => SetProperty(ref _statusText, value); } public int ChessboardWidth { get => _chessboardWidth; set => SetProperty(ref _chessboardWidth, value); } public int ChessboardHeight { get => _chessboardHeight; set => SetProperty(ref _chessboardHeight, value); } public float SquareSize { get => _squareSize; set => SetProperty(ref _squareSize, value); } public int SelectedImageIndex { get => _selectedImageIndex; set { if (SetProperty(ref _selectedImageIndex, value) && value >= 0) LoadSelectedImage(value); } } public bool IsCalibrating { get => _isCalibrating; set => SetProperty(ref _isCalibrating, value); } public double ProgressValue { get => _progressValue; set => SetProperty(ref _progressValue, value); } public string ProgressText { get => _progressText; set => SetProperty(ref _progressText, value); } public DelegateCommand AddImagesCommand { get; } public DelegateCommand ClearImagesCommand { get; } public DelegateCommand CalibrateCommand { get; } public DelegateCommand SaveCalibrationCommand { get; } public DelegateCommand LoadCalibrationCommand { get; } public DelegateCommand UndistortImageCommand { get; } private void AddImages() { var fileNames = _dialogService.ShowOpenMultipleFilesDialog("图像文件|*.jpg;*.png;*.bmp;*.tif"); if (fileNames == null) return; foreach (var file in fileNames) { _imagePaths.Add(file); ImageFileNames.Add(Path.GetFileName(file)); } StatusText = string.Format(Res.ChessboardStatusAdded, _imagePaths.Count); } private bool CanClearImages() => ImageFileNames.Count > 0; private void ClearImages() { _imagePaths.Clear(); ImageFileNames.Clear(); ImageSource = null; StatusText = Res.ChessboardStatusCleared; RaiseEvent(ImageClearedRequested); } private bool CanCalibrate() => ImageFileNames.Count >= 3 && !IsCalibrating; private void OnCalibrationProgressChanged(int current, int total, string message) { System.Windows.Application.Current.Dispatcher.Invoke(() => { ProgressValue = (double)current / total * 100; if (message.Contains("检测角点")) { var match = System.Text.RegularExpressions.Regex.Match(message, @"\((\d+)/(\d+)\)"); ProgressText = match.Success ? string.Format(Res.ChessboardProgressDetecting, match.Groups[1].Value, match.Groups[2].Value) : message; } else if (message.Contains("执行相机标定")) ProgressText = Res.ChessboardProgressCalibrating; else if (message.Contains("计算重投影误差")) { var match = System.Text.RegularExpressions.Regex.Match(message, @"\((\d+)/(\d+)\)"); ProgressText = match.Success ? string.Format(Res.ChessboardProgressCalculating, match.Groups[1].Value, match.Groups[2].Value) : message; } else if (message.Contains("标定完成")) ProgressText = Res.ChessboardProgressComplete; else if (message.Contains("标定失败")) ProgressText = Res.ChessboardProgressFailed; else ProgressText = message; }); } private async System.Threading.Tasks.Task CalibrateAsync() { if (_imagePaths.Count < 3) { _dialogService.ShowError(Res.ChessboardErrorMinImages, Res.ChessboardCalibrationComplete); return; } IsCalibrating = true; ProgressValue = 0; ProgressText = Res.ChessboardProgressPreparing; StatusText = Res.ChessboardStatusCalibrating; try { await System.Threading.Tasks.Task.Run(() => { if (_calibrator.CalibrateFromImages(new List(_imagePaths), ChessboardWidth, ChessboardHeight, SquareSize, out string error)) { System.Windows.Application.Current.Dispatcher.Invoke(() => { var imageErrors = new System.Text.StringBuilder(); for (int i = 0; i < _calibrator.PerImageErrors.Count; i++) imageErrors.AppendLine(string.Format(Res.ChessboardImageError, i + 1, _calibrator.PerImageErrors[i])); StatusText = string.Format(Res.ChessboardStatusSuccess, _calibrator.ReprojectionError, imageErrors.ToString()); _dialogService.ShowInfo(Res.ChessboardCalibrationComplete, Res.ChessboardSaveSuccess); }); } else { System.Windows.Application.Current.Dispatcher.Invoke(() => { StatusText = string.Format(Res.ChessboardStatusFailed, error); _dialogService.ShowError(error, Res.ChessboardCalibrationComplete); }); } }); } finally { IsCalibrating = false; ProgressValue = 0; } } private void SaveCalibration() { var fileName = _dialogService.ShowSaveFileDialog("标定文件|*.json", "camera_calibration.json"); if (fileName == null) return; try { _calibrator.SaveCalibration(fileName); StatusText = string.Format(Res.ChessboardStatusSaved, fileName); _dialogService.ShowInfo(Res.ChessboardSaveSuccess, Res.ChessboardCalibrationComplete); } catch (Exception ex) { _dialogService.ShowError($"保存失败: {ex.Message}", Res.ChessboardCalibrationComplete); } } private void LoadCalibration() { var fileName = _dialogService.ShowOpenFileDialog("标定文件|*.json"); if (fileName == null) return; if (_calibrator.LoadCalibration(fileName)) { StatusText = string.Format(Res.ChessboardStatusLoaded, fileName); _dialogService.ShowInfo(Res.ChessboardLoadSuccess, Res.ChessboardCalibrationComplete); } else { _dialogService.ShowError(Res.CalibrationLoadFailed, Res.ChessboardCalibrationComplete); } } private void UndistortImage() { var fileName = _dialogService.ShowOpenFileDialog("图像文件|*.jpg;*.png;*.bmp;*.tif"); if (fileName == null) return; using var image = CvInvoke.Imread(fileName); var undistorted = _calibrator.UndistortImage(image); ImageSource = MatToBitmapSource(undistorted); StatusText = string.Format(Res.ChessboardStatusUndistorted, Path.GetFileName(fileName)); RaiseEvent(ImageLoadedRequested); } private void LoadSelectedImage(int index) { if (index < 0 || index >= _imagePaths.Count) return; var img = _calibrator.DrawChessboardCorners(_imagePaths[index], ChessboardWidth, ChessboardHeight); if (img != null) { ImageSource = MatToBitmapSource(img); RaiseEvent(ImageLoadedRequested); } if (_calibrator.PerImageErrors.Count > index) StatusText = string.Format(Res.ChessboardStatusImageError, index + 1, _calibrator.PerImageErrors[index]); } public event EventHandler? ImageLoadedRequested; public event EventHandler? ImageClearedRequested; private void RaiseEvent(EventHandler? handler) => handler?.Invoke(this, EventArgs.Empty); private static BitmapSource MatToBitmapSource(Mat mat) { using var bitmap = mat.ToBitmap(); var hBitmap = bitmap.GetHbitmap(); try { return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap( hBitmap, IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); } finally { DeleteObject(hBitmap); } } [System.Runtime.InteropServices.DllImport("gdi32.dll")] private static extern bool DeleteObject(IntPtr hObject); }