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 ICalibrationCaptureService? _captureService; private readonly CalibrationProcessor _calibrator = new(); private Image? _currentImage; private static readonly ILogger _logger = Log.ForContext(); private BitmapSource? _imageSource; private BitmapSource? _frozenImage; private string _statusText = Res.CalibrationStatusReady; private bool _showWorldCoordinates; private bool _isLiveView = true; public CalibrationViewModel(ICalibrationDialogService dialogService, ICalibrationCaptureService? captureService = null) { _dialogService = dialogService; _captureService = captureService; CalibrationPoints = new ObservableCollection(); LoadImageCommand = new DelegateCommand(LoadImage); LoadCsvCommand = new DelegateCommand(LoadCsv); CalibrateCommand = new DelegateCommand(Calibrate, CanCalibrate) .ObservesProperty(() => CalibrationPoints.Count); SaveCalibrationCommand = new DelegateCommand(SaveCalibration); LoadCalibrationCommand = new DelegateCommand(LoadCalibration); CapturePointCommand = new DelegateCommand(CapturePoint, CanCapturePoint); DeleteSelectedPointCommand = new DelegateCommand(DeleteSelectedPoint, () => SelectedPoint != null) .ObservesProperty(() => SelectedPoint); // 启动实时预览 if (_captureService != null) { _captureService.LiveImageUpdated += OnLiveImageUpdated; _captureService.StartLivePreview(); } } public ObservableCollection CalibrationPoints { get; } public BitmapSource? ImageSource { get => _imageSource; set => SetProperty(ref _imageSource, value); } private BitmapSource? _overlayImage; /// 叠加层图像(显示检测到的轮廓和中心点) public BitmapSource? OverlayImage { get => _overlayImage; set => SetProperty(ref _overlayImage, value); } public string StatusText { get => _statusText; set => SetProperty(ref _statusText, value); } public bool ShowWorldCoordinates { get => _showWorldCoordinates; set => SetProperty(ref _showWorldCoordinates, value); } public bool IsLiveView { get => _isLiveView; set { if (SetProperty(ref _isLiveView, value)) { RaisePropertyChanged(nameof(LiveViewButtonText)); if (value) { // 切回实时:恢复实时预览 _captureService?.StartLivePreview(); } else { // 切到当前:冻结当前帧 _frozenImage = _imageSource; _captureService?.StopLivePreview(); } } } } public string LiveViewButtonText => _isLiveView ? "⏸ 冻结" : "▶ 实时"; public DelegateCommand LoadImageCommand { get; } public DelegateCommand LoadCsvCommand { get; } public DelegateCommand CalibrateCommand { get; } public DelegateCommand SaveCalibrationCommand { get; } public DelegateCommand LoadCalibrationCommand { get; } public DelegateCommand CapturePointCommand { get; } public DelegateCommand DeleteSelectedPointCommand { get; } /// 是否支持采集模式(有采集服务注入) public bool IsCaptureAvailable => _captureService?.IsAvailable == true; private CalibrationProcessor.CalibrationPoint? _selectedPoint; public CalibrationProcessor.CalibrationPoint? SelectedPoint { get => _selectedPoint; set => SetProperty(ref _selectedPoint, value); } private void LoadImage() { _logger.Information("Loading image file"); var fileName = _dialogService.ShowOpenFileDialog("图像文件|*.jpg;*.png;*.bmp;*.tif"); if (fileName == null) return; _currentImage = new Image(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(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(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); } } private bool CanCapturePoint() => _captureService?.IsAvailable == true; private void CapturePoint() { if (_captureService == null) return; try { var result = _captureService.CaptureCurrentPoint(); if (result == null) { StatusText = "采集失败:未能识别标记点,请确认标记在视野内"; return; } CalibrationPoints.Add(new CalibrationProcessor.CalibrationPoint { PixelX = result.PixelX, PixelY = result.PixelY, WorldX = result.WorldX, WorldY = result.WorldY }); // 更新图像显示 if (result.Image != null) { ImageSource = result.Image; } // 绘制检测结果叠加层(轮廓 + 中心点) DrawDetectionOverlay(result); StatusText = $"已采集第 {CalibrationPoints.Count} 个点: 像素({result.PixelX:F1}, {result.PixelY:F1}) → 物理({result.WorldX:F3}, {result.WorldY:F3})"; _logger.Information("标定点采集: Pixel=({PixelX:F1}, {PixelY:F1}), World=({WorldX:F3}, {WorldY:F3})", result.PixelX, result.PixelY, result.WorldX, result.WorldY); } catch (Exception ex) { StatusText = $"采集异常: {ex.Message}"; _logger.Error(ex, "标定点采集失败"); } } private void DeleteSelectedPoint() { if (SelectedPoint != null && CalibrationPoints.Contains(SelectedPoint)) { CalibrationPoints.Remove(SelectedPoint); SelectedPoint = null; StatusText = $"已删除,剩余 {CalibrationPoints.Count} 个标定点"; } } public PointF ConvertPixelToWorld(PointF pixel) => _calibrator.PixelToWorld(pixel); public Image? CurrentImage => _currentImage; public event EventHandler? ImageLoadedRequested; private void RaiseEvent(EventHandler? handler) => handler?.Invoke(this, EventArgs.Empty); private void OnLiveImageUpdated(object? sender, LiveImageEventArgs e) { if (!_isLiveView) return; System.Windows.Application.Current?.Dispatcher?.BeginInvoke(() => { ImageSource = e.Image; }); } /// /// 绘制检测结果叠加层(轮廓 + 中心十字 + 坐标文字) /// private void DrawDetectionOverlay(CaptureResult result) { if (result.Image == null) return; int w = result.Image.PixelWidth; int h = result.Image.PixelHeight; // 创建透明叠加层 var visual = new System.Windows.Media.DrawingVisual(); using (var dc = visual.RenderOpen()) { // 绘制轮廓 if (result.ContourPoints != null && result.ContourPoints.Length > 2) { var pen = new System.Windows.Media.Pen(System.Windows.Media.Brushes.Lime, 2); var geometry = new System.Windows.Media.StreamGeometry(); using (var ctx = geometry.Open()) { ctx.BeginFigure(new System.Windows.Point(result.ContourPoints[0].X, result.ContourPoints[0].Y), false, true); for (int i = 1; i < result.ContourPoints.Length; i++) ctx.LineTo(new System.Windows.Point(result.ContourPoints[i].X, result.ContourPoints[i].Y), true, false); } geometry.Freeze(); dc.DrawGeometry(null, pen, geometry); } // 绘制中心十字 double cx = result.PixelX; double cy = result.PixelY; double crossSize = Math.Max(10, Math.Max(w, h) / 80.0); var crossPen = new System.Windows.Media.Pen(System.Windows.Media.Brushes.Red, 2); dc.DrawLine(crossPen, new System.Windows.Point(cx - crossSize, cy), new System.Windows.Point(cx + crossSize, cy)); dc.DrawLine(crossPen, new System.Windows.Point(cx, cy - crossSize), new System.Windows.Point(cx, cy + crossSize)); // 绘制坐标文字 var text = new System.Windows.Media.FormattedText( $"({result.PixelX:F1}, {result.PixelY:F1})", System.Globalization.CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight, new System.Windows.Media.Typeface("Segoe UI"), Math.Max(12, Math.Max(w, h) / 60.0), System.Windows.Media.Brushes.Yellow, 1.0); dc.DrawText(text, new System.Windows.Point(cx + crossSize + 4, cy - text.Height / 2)); } var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(w, h, 96, 96, System.Windows.Media.PixelFormats.Pbgra32); rtb.Render(visual); rtb.Freeze(); OverlayImage = rtb; } /// /// 停止实时预览并清理资源(窗口关闭时调用) /// public void Cleanup() { if (_captureService != null) { _captureService.StopLivePreview(); _captureService.LiveImageUpdated -= OnLiveImageUpdated; } } 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); }