TURBO-569:更新工程结构;将导航相机标定和校准功能迁移到XP.Camera类
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.Structure;
|
||||
using Serilog;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Drawing;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Res = XP.Camera.Calibration.Resources.CalibrationResources;
|
||||
|
||||
namespace XP.Camera.Calibration.ViewModels;
|
||||
|
||||
public class CalibrationViewModel : BindableBase
|
||||
{
|
||||
private readonly ICalibrationDialogService _dialogService;
|
||||
private readonly CalibrationProcessor _calibrator = new();
|
||||
private Image<Bgr, byte>? _currentImage;
|
||||
private static readonly ILogger _logger = Log.ForContext<CalibrationViewModel>();
|
||||
private BitmapSource? _imageSource;
|
||||
private string _statusText = Res.CalibrationStatusReady;
|
||||
private bool _showWorldCoordinates;
|
||||
|
||||
public CalibrationViewModel(ICalibrationDialogService dialogService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
CalibrationPoints = new ObservableCollection<CalibrationProcessor.CalibrationPoint>();
|
||||
|
||||
LoadImageCommand = new DelegateCommand(LoadImage);
|
||||
LoadCsvCommand = new DelegateCommand(LoadCsv);
|
||||
CalibrateCommand = new DelegateCommand(Calibrate, CanCalibrate)
|
||||
.ObservesProperty(() => CalibrationPoints.Count);
|
||||
SaveCalibrationCommand = new DelegateCommand(SaveCalibration);
|
||||
LoadCalibrationCommand = new DelegateCommand(LoadCalibration);
|
||||
}
|
||||
|
||||
public ObservableCollection<CalibrationProcessor.CalibrationPoint> CalibrationPoints { get; }
|
||||
|
||||
public BitmapSource? ImageSource
|
||||
{
|
||||
get => _imageSource;
|
||||
set => SetProperty(ref _imageSource, value);
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
public bool ShowWorldCoordinates
|
||||
{
|
||||
get => _showWorldCoordinates;
|
||||
set => SetProperty(ref _showWorldCoordinates, value);
|
||||
}
|
||||
|
||||
public DelegateCommand LoadImageCommand { get; }
|
||||
public DelegateCommand LoadCsvCommand { get; }
|
||||
public DelegateCommand CalibrateCommand { get; }
|
||||
public DelegateCommand SaveCalibrationCommand { get; }
|
||||
public DelegateCommand LoadCalibrationCommand { get; }
|
||||
|
||||
private void LoadImage()
|
||||
{
|
||||
_logger.Information("Loading image file");
|
||||
var fileName = _dialogService.ShowOpenFileDialog("图像文件|*.jpg;*.png;*.bmp;*.tif");
|
||||
if (fileName == null) return;
|
||||
|
||||
_currentImage = new Image<Bgr, byte>(fileName);
|
||||
ImageSource = MatToBitmapSource(_currentImage.Mat);
|
||||
StatusText = string.Format(Res.CalibrationStatusImageLoaded, fileName);
|
||||
RaiseEvent(ImageLoadedRequested);
|
||||
}
|
||||
|
||||
private void LoadCsv()
|
||||
{
|
||||
var fileName = _dialogService.ShowOpenFileDialog("CSV文件|*.csv|所有文件|*.*");
|
||||
if (fileName == null) return;
|
||||
|
||||
var points = _calibrator.LoadPointsFromCsv(fileName);
|
||||
CalibrationPoints.Clear();
|
||||
foreach (var pt in points)
|
||||
CalibrationPoints.Add(pt);
|
||||
|
||||
StatusText = string.Format(Res.CalibrationStatusCsvLoaded, CalibrationPoints.Count, fileName);
|
||||
}
|
||||
|
||||
private bool CanCalibrate() => CalibrationPoints.Count >= 4;
|
||||
|
||||
private void Calibrate()
|
||||
{
|
||||
if (CalibrationPoints.Count < 4)
|
||||
{
|
||||
_dialogService.ShowError(Res.CalibrationErrorMinPoints, Res.CalibrationSuccessTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_calibrator.Calibrate(new List<CalibrationProcessor.CalibrationPoint>(CalibrationPoints)))
|
||||
{
|
||||
StatusText = string.Format(Res.CalibrationStatusSuccess, CalibrationPoints.Count);
|
||||
_dialogService.ShowInfo(Res.CalibrationSuccessMessage, Res.CalibrationSuccessTitle);
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = Res.CalibrationStatusFailed;
|
||||
_dialogService.ShowError(Res.CalibrationStatusFailed, Res.CalibrationSuccessTitle);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveCalibration()
|
||||
{
|
||||
var fileName = _dialogService.ShowSaveFileDialog("标定文件|*.json", "calibration.json");
|
||||
if (fileName == null) return;
|
||||
|
||||
_calibrator.SaveCalibration(fileName, new List<CalibrationProcessor.CalibrationPoint>(CalibrationPoints));
|
||||
StatusText = string.Format(Res.CalibrationStatusSaved, fileName);
|
||||
_dialogService.ShowInfo(Res.CalibrationSaveSuccess, Res.CalibrationSuccessTitle);
|
||||
}
|
||||
|
||||
private void LoadCalibration()
|
||||
{
|
||||
var fileName = _dialogService.ShowOpenFileDialog("标定文件|*.json");
|
||||
if (fileName == null) return;
|
||||
|
||||
if (_calibrator.LoadCalibration(fileName))
|
||||
{
|
||||
StatusText = string.Format(Res.CalibrationStatusLoaded, fileName);
|
||||
_dialogService.ShowInfo(Res.CalibrationLoadSuccess, Res.CalibrationSuccessTitle);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogService.ShowError(Res.CalibrationLoadFailed, Res.CalibrationSuccessTitle);
|
||||
}
|
||||
}
|
||||
|
||||
public PointF ConvertPixelToWorld(PointF pixel) => _calibrator.PixelToWorld(pixel);
|
||||
|
||||
public Image<Bgr, byte>? CurrentImage => _currentImage;
|
||||
|
||||
public event EventHandler? ImageLoadedRequested;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user