Files
XplorePlane/XP.Camera/Calibration/ViewModels/ChessboardCalibrationViewModel.cs
T

245 lines
9.9 KiB
C#

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<string> _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<string>();
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<string> 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<string>(_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);
}