坐标标定工具改造:新增采集服务接口及实现,支持一键采集标定点

This commit is contained in:
李伟
2026-05-27 09:22:44 +08:00
parent 030433cc92
commit bc626a0ca8
4 changed files with 523 additions and 3 deletions
@@ -0,0 +1,73 @@
using System.Drawing;
using System.Windows.Media.Imaging;
namespace XP.Camera.Calibration;
/// <summary>
/// 标定采集服务接口
/// 提供"一键采集"能力:读取编码器坐标 + 拍图 + 识别标记中心
/// </summary>
public interface ICalibrationCaptureService
{
/// <summary>是否可用(相机已连接、运动系统就绪)</summary>
bool IsAvailable { get; }
/// <summary>
/// 采集当前标定点
/// </summary>
/// <returns>采集结果,失败时返回 null</returns>
CaptureResult? CaptureCurrentPoint();
/// <summary>
/// 获取当前导航相机图像
/// </summary>
BitmapSource? CaptureImage();
/// <summary>
/// 启动实时预览(将相机实时画面推送到 LiveImageUpdated 事件)
/// </summary>
void StartLivePreview();
/// <summary>
/// 停止实时预览
/// </summary>
void StopLivePreview();
/// <summary>
/// 实时画面更新事件
/// </summary>
event EventHandler<LiveImageEventArgs>? LiveImageUpdated;
}
/// <summary>
/// 实时画面事件参数
/// </summary>
public class LiveImageEventArgs : EventArgs
{
public BitmapSource Image { get; }
public LiveImageEventArgs(BitmapSource image) => Image = image;
}
/// <summary>
/// 单次采集结果
/// </summary>
public class CaptureResult
{
/// <summary>标记中心像素坐标 X(亚像素)</summary>
public double PixelX { get; set; }
/// <summary>标记中心像素坐标 Y(亚像素)</summary>
public double PixelY { get; set; }
/// <summary>平台编码器坐标 X (mm)</summary>
public double WorldX { get; set; }
/// <summary>平台编码器坐标 Y (mm)</summary>
public double WorldY { get; set; }
/// <summary>采集的图像</summary>
public BitmapSource? Image { get; set; }
/// <summary>检测到的标记轮廓点集</summary>
public System.Drawing.Point[]? ContourPoints { get; set; }
}
@@ -11,16 +11,20 @@ namespace XP.Camera.Calibration.ViewModels;
public class CalibrationViewModel : BindableBase
{
private readonly ICalibrationDialogService _dialogService;
private readonly ICalibrationCaptureService? _captureService;
private readonly CalibrationProcessor _calibrator = new();
private Image<Bgr, byte>? _currentImage;
private static readonly ILogger _logger = Log.ForContext<CalibrationViewModel>();
private BitmapSource? _imageSource;
private BitmapSource? _frozenImage;
private string _statusText = Res.CalibrationStatusReady;
private bool _showWorldCoordinates;
private bool _isLiveView = true;
public CalibrationViewModel(ICalibrationDialogService dialogService)
public CalibrationViewModel(ICalibrationDialogService dialogService, ICalibrationCaptureService? captureService = null)
{
_dialogService = dialogService;
_captureService = captureService;
CalibrationPoints = new ObservableCollection<CalibrationProcessor.CalibrationPoint>();
LoadImageCommand = new DelegateCommand(LoadImage);
@@ -29,6 +33,16 @@ public class CalibrationViewModel : BindableBase
.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<CalibrationProcessor.CalibrationPoint> CalibrationPoints { get; }
@@ -39,6 +53,14 @@ public class CalibrationViewModel : BindableBase
set => SetProperty(ref _imageSource, value);
}
private BitmapSource? _overlayImage;
/// <summary>叠加层图像(显示检测到的轮廓和中心点)</summary>
public BitmapSource? OverlayImage
{
get => _overlayImage;
set => SetProperty(ref _overlayImage, value);
}
public string StatusText
{
get => _statusText;
@@ -51,11 +73,48 @@ public class CalibrationViewModel : BindableBase
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; }
/// <summary>是否支持采集模式(有采集服务注入)</summary>
public bool IsCaptureAvailable => _captureService?.IsAvailable == true;
private CalibrationProcessor.CalibrationPoint? _selectedPoint;
public CalibrationProcessor.CalibrationPoint? SelectedPoint
{
get => _selectedPoint;
set => SetProperty(ref _selectedPoint, value);
}
private void LoadImage()
{
@@ -130,6 +189,59 @@ public class CalibrationViewModel : BindableBase
}
}
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<Bgr, byte>? CurrentImage => _currentImage;
@@ -138,6 +250,82 @@ public class CalibrationViewModel : BindableBase
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;
});
}
/// <summary>
/// 绘制检测结果叠加层(轮廓 + 中心十字 + 坐标文字)
/// </summary>
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;
}
/// <summary>
/// 停止实时预览并清理资源(窗口关闭时调用)
/// </summary>
public void Cleanup()
{
if (_captureService != null)
{
_captureService.StopLivePreview();
_captureService.LiveImageUpdated -= OnLiveImageUpdated;
}
}
private static BitmapSource MatToBitmapSource(Mat mat)
{
using var bitmap = mat.ToBitmap();