坐标标定工具改造:新增采集服务接口及实现,支持一键采集标定点
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Threading;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.CvEnum;
|
||||
using Emgu.CV.Structure;
|
||||
using Emgu.CV.Util;
|
||||
using Serilog;
|
||||
using XP.Camera;
|
||||
using XP.Camera.Calibration;
|
||||
using XP.Hardware.MotionControl.Abstractions;
|
||||
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||
|
||||
namespace XplorePlane.Services.Calibration;
|
||||
|
||||
/// <summary>
|
||||
/// 导航相机标定采集服务实现
|
||||
/// 读取编码器坐标 + 导航相机拍图 + 图像识别标记中心(亚像素)
|
||||
/// </summary>
|
||||
public class NavigationCalibrationCaptureService : ICalibrationCaptureService
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<NavigationCalibrationCaptureService>();
|
||||
private readonly IMotionSystem _motionSystem;
|
||||
private readonly ICameraController _navCamera;
|
||||
private BitmapSource? _lastCapturedImage;
|
||||
private readonly object _captureLock = new();
|
||||
private ManualResetEventSlim? _imageReadyEvent;
|
||||
private bool _livePreviewActive;
|
||||
|
||||
public event EventHandler<LiveImageEventArgs>? LiveImageUpdated;
|
||||
|
||||
public NavigationCalibrationCaptureService(IMotionSystem motionSystem, ICameraController navCamera)
|
||||
{
|
||||
_motionSystem = motionSystem;
|
||||
_navCamera = navCamera;
|
||||
}
|
||||
|
||||
public bool IsAvailable => _navCamera.IsConnected;
|
||||
|
||||
public void StartLivePreview()
|
||||
{
|
||||
if (!IsAvailable || _livePreviewActive) return;
|
||||
_livePreviewActive = true;
|
||||
_navCamera.ImageGrabbed += OnLiveImageGrabbed;
|
||||
|
||||
// 如果相机没在采集,启动采集并触发第一帧
|
||||
if (!_navCamera.IsGrabbing)
|
||||
{
|
||||
_navCamera.StartGrabbing();
|
||||
_navCamera.ExecuteSoftwareTrigger();
|
||||
}
|
||||
|
||||
_logger.Information("标定实时预览已启动");
|
||||
}
|
||||
|
||||
public void StopLivePreview()
|
||||
{
|
||||
_livePreviewActive = false;
|
||||
_navCamera.ImageGrabbed -= OnLiveImageGrabbed;
|
||||
_logger.Information("标定实时预览已停止");
|
||||
}
|
||||
|
||||
private void OnLiveImageGrabbed(object? sender, ImageGrabbedEventArgs e)
|
||||
{
|
||||
if (!_livePreviewActive) return;
|
||||
var bmp = ConvertToBitmapSource(e);
|
||||
if (bmp != null)
|
||||
{
|
||||
_lastLiveImage = bmp;
|
||||
LiveImageUpdated?.Invoke(this, new LiveImageEventArgs(bmp));
|
||||
}
|
||||
}
|
||||
|
||||
private volatile BitmapSource? _lastLiveImage;
|
||||
|
||||
public CaptureResult? CaptureCurrentPoint()
|
||||
{
|
||||
if (!IsAvailable)
|
||||
{
|
||||
_logger.Warning("采集失败:导航相机未连接");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 1. 读取编码器坐标
|
||||
var xAxis = _motionSystem.GetLinearAxis(AxisId.StageX);
|
||||
var yAxis = _motionSystem.GetLinearAxis(AxisId.StageY);
|
||||
xAxis.UpdateStatus();
|
||||
yAxis.UpdateStatus();
|
||||
double worldX = xAxis.ActualPosition;
|
||||
double worldY = yAxis.ActualPosition;
|
||||
|
||||
// 2. 导航相机拍图
|
||||
var image = CaptureImage();
|
||||
if (image == null)
|
||||
{
|
||||
_logger.Warning("采集失败:无法获取导航相机图像");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 图像识别标记中心(亚像素)
|
||||
var grayImage = BitmapSourceToGray(image);
|
||||
var detection = DetectMarkerCenter(grayImage);
|
||||
grayImage.Dispose();
|
||||
|
||||
if (detection == null)
|
||||
{
|
||||
_logger.Warning("采集失败:未能识别标记点");
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.Information("标定点采集成功: Pixel=({Px:F1}, {Py:F1}), World=({Wx:F3}, {Wy:F3})",
|
||||
detection.Value.Center.X, detection.Value.Center.Y, worldX, worldY);
|
||||
|
||||
return new CaptureResult
|
||||
{
|
||||
PixelX = detection.Value.Center.X,
|
||||
PixelY = detection.Value.Center.Y,
|
||||
WorldX = worldX,
|
||||
WorldY = worldY,
|
||||
Image = image,
|
||||
ContourPoints = detection.Value.Contour
|
||||
};
|
||||
}
|
||||
|
||||
public BitmapSource? CaptureImage()
|
||||
{
|
||||
if (!_navCamera.IsConnected) return null;
|
||||
|
||||
// 如果实时预览在运行,直接使用最新一帧
|
||||
if (_livePreviewActive && _lastLiveImage != null)
|
||||
return _lastLiveImage;
|
||||
|
||||
lock (_captureLock)
|
||||
{
|
||||
_lastCapturedImage = null;
|
||||
_imageReadyEvent = new ManualResetEventSlim(false);
|
||||
|
||||
void OnImageGrabbed(object? sender, ImageGrabbedEventArgs e)
|
||||
{
|
||||
_lastCapturedImage = ConvertToBitmapSource(e);
|
||||
_imageReadyEvent?.Set();
|
||||
}
|
||||
|
||||
_navCamera.ImageGrabbed += OnImageGrabbed;
|
||||
|
||||
try
|
||||
{
|
||||
if (!_navCamera.IsGrabbing)
|
||||
_navCamera.StartGrabbing();
|
||||
_navCamera.ExecuteSoftwareTrigger();
|
||||
|
||||
// 等待图像到达(超时 3 秒)
|
||||
if (!_imageReadyEvent.Wait(3000))
|
||||
{
|
||||
_logger.Warning("导航相机采集超时");
|
||||
return null;
|
||||
}
|
||||
|
||||
return _lastCapturedImage;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_navCamera.ImageGrabbed -= OnImageGrabbed;
|
||||
_imageReadyEvent?.Dispose();
|
||||
_imageReadyEvent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测图像中标记点的中心(亚像素精度)
|
||||
/// 支持圆点标记:阈值分割 → 轮廓检测 → 面积过滤 → 亚像素质心
|
||||
/// </summary>
|
||||
private (PointF Center, Point[] Contour)? DetectMarkerCenter(Image<Gray, byte> grayImage)
|
||||
{
|
||||
int w = grayImage.Width, h = grayImage.Height;
|
||||
|
||||
// 高斯模糊降噪
|
||||
using var blurred = new Image<Gray, byte>(w, h);
|
||||
CvInvoke.GaussianBlur(grayImage, blurred, new Size(5, 5), 1.0);
|
||||
|
||||
// Otsu 自动阈值二值化
|
||||
using var binary = new Image<Gray, byte>(w, h);
|
||||
CvInvoke.Threshold(blurred, binary, 0, 255, ThresholdType.Otsu | ThresholdType.Binary);
|
||||
|
||||
// 轮廓检测
|
||||
using var contours = new VectorOfVectorOfPoint();
|
||||
using var hierarchy = new Mat();
|
||||
CvInvoke.FindContours(binary, contours, hierarchy, RetrType.External, ChainApproxMethod.ChainApproxSimple);
|
||||
|
||||
// 找到面积最大的轮廓(假设标记是视野中最显著的特征)
|
||||
double maxArea = 0;
|
||||
int bestIdx = -1;
|
||||
double minValidArea = w * h * 0.001; // 最小有效面积:图像面积的 0.1%
|
||||
double maxValidArea = w * h * 0.5; // 最大有效面积:图像面积的 50%
|
||||
|
||||
for (int i = 0; i < contours.Size; i++)
|
||||
{
|
||||
double area = CvInvoke.ContourArea(contours[i]);
|
||||
if (area < minValidArea || area > maxValidArea) continue;
|
||||
if (area > maxArea)
|
||||
{
|
||||
maxArea = area;
|
||||
bestIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIdx < 0) return null;
|
||||
|
||||
// 亚像素质心计算
|
||||
var moments = CvInvoke.Moments(contours[bestIdx]);
|
||||
if (moments.M00 < 1) return null;
|
||||
|
||||
double cx = moments.M10 / moments.M00;
|
||||
double cy = moments.M01 / moments.M00;
|
||||
|
||||
var contourPoints = contours[bestIdx].ToArray();
|
||||
return (new PointF((float)cx, (float)cy), contourPoints);
|
||||
}
|
||||
|
||||
private static Image<Gray, byte> BitmapSourceToGray(BitmapSource bmp)
|
||||
{
|
||||
var converted = new FormatConvertedBitmap(bmp, PixelFormats.Bgra32, null, 0);
|
||||
int w = converted.PixelWidth, h = converted.PixelHeight;
|
||||
int stride = w * 4;
|
||||
var pixels = new byte[stride * h];
|
||||
converted.CopyPixels(pixels, stride, 0);
|
||||
var gray = new Image<Gray, byte>(w, h);
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int idx = y * stride + x * 4;
|
||||
gray.Data[y, x, 0] = (byte)(pixels[idx + 2] * 0.299 + pixels[idx + 1] * 0.587 + pixels[idx] * 0.114);
|
||||
}
|
||||
return gray;
|
||||
}
|
||||
|
||||
private static BitmapSource? ConvertToBitmapSource(ImageGrabbedEventArgs e)
|
||||
{
|
||||
if (e.Width <= 0 || e.Height <= 0 || e.PixelData == null) return null;
|
||||
return PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
|
||||
}
|
||||
}
|
||||
@@ -676,14 +676,28 @@ namespace XplorePlane.ViewModels
|
||||
};
|
||||
|
||||
var calibrationDialogService = new XP.Camera.Calibration.DefaultCalibrationDialogService();
|
||||
var calibrationViewModel = new XP.Camera.Calibration.ViewModels.CalibrationViewModel(calibrationDialogService);
|
||||
|
||||
// 尝试创建采集服务(需要运动系统和导航相机)
|
||||
XP.Camera.Calibration.ICalibrationCaptureService? captureService = null;
|
||||
try
|
||||
{
|
||||
var motionSystem = Prism.Ioc.ContainerLocator.Current?.Resolve<XP.Hardware.MotionControl.Abstractions.IMotionSystem>();
|
||||
var navCamera = Prism.Ioc.ContainerLocator.Current?.Resolve<XP.Camera.ICameraController>();
|
||||
if (motionSystem != null && navCamera != null)
|
||||
captureService = new Services.Calibration.NavigationCalibrationCaptureService(motionSystem, navCamera);
|
||||
}
|
||||
catch { /* 采集服务不可用时降级为手动模式 */ }
|
||||
|
||||
var calibrationViewModel = new XP.Camera.Calibration.ViewModels.CalibrationViewModel(calibrationDialogService, captureService);
|
||||
var calibrationControl = new XP.Camera.Calibration.Controls.CalibrationControl
|
||||
{
|
||||
DataContext = calibrationViewModel
|
||||
};
|
||||
|
||||
calibrationWindow.Content = calibrationControl;
|
||||
calibrationWindow.ShowDialog();
|
||||
calibrationWindow.Closed += (s, e) => calibrationViewModel.Cleanup();
|
||||
calibrationWindow.Owner = System.Windows.Application.Current.MainWindow;
|
||||
calibrationWindow.Show();
|
||||
}
|
||||
|
||||
private void ExecuteOpenSettings()
|
||||
|
||||
Reference in New Issue
Block a user