245 lines
9.9 KiB
C#
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);
|
|
}
|