diff --git a/XP.Camera/BaslerCameraController.cs b/XP.Camera/Basler/BaslerCameraController.cs
similarity index 100%
rename from XP.Camera/BaslerCameraController.cs
rename to XP.Camera/Basler/BaslerCameraController.cs
diff --git a/XP.Camera/Calibration/CalibrationLocalizedStrings.cs b/XP.Camera/Calibration/CalibrationLocalizedStrings.cs
new file mode 100644
index 0000000..a914b9c
--- /dev/null
+++ b/XP.Camera/Calibration/CalibrationLocalizedStrings.cs
@@ -0,0 +1,8 @@
+using XP.Camera.Calibration.Resources;
+
+namespace XP.Camera.Calibration;
+
+public class CalibrationLocalizedStrings
+{
+ public CalibrationResources Resources { get; } = new CalibrationResources();
+}
diff --git a/XP.Camera/Calibration/CalibrationProcessor.cs b/XP.Camera/Calibration/CalibrationProcessor.cs
new file mode 100644
index 0000000..9ec1efb
--- /dev/null
+++ b/XP.Camera/Calibration/CalibrationProcessor.cs
@@ -0,0 +1,201 @@
+// ============================================================================
+// 文件名: CalibrationProcessor.cs
+// 描述: 标定处理器,实现图像坐标系到世界坐标系的转换
+// 功能:
+// - 基于多点标定计算透视变换矩阵(支持4点及以上)
+// - 像素坐标到世界坐标的转换
+// - 标定数据的保存和加载(JSON格式)
+// - 从CSV文件导入标定点数据
+// 算法: 使用DLT(Direct Linear Transformation)方法求解单应性矩阵
+// ============================================================================
+
+using Emgu.CV;
+using System.IO;
+using System.Text.Json;
+
+namespace XP.Camera.Calibration;
+
+///
+/// 九点标定处理器
+///
+public class CalibrationProcessor
+{
+ public class CalibrationPoint
+ {
+ public double PixelX { get; set; }
+ public double PixelY { get; set; }
+ public double WorldX { get; set; }
+ public double WorldY { get; set; }
+ }
+
+ public class CalibrationData
+ {
+ public List Points { get; set; } = new List();
+ public double[] TransformMatrix { get; set; } = new double[9];
+ public DateTime CalibrationTime { get; set; }
+ }
+
+ private Matrix? _transformMatrix;
+
+ ///
+ /// 执行九点标定
+ ///
+ public bool Calibrate(List points)
+ {
+ if (points.Count < 4)
+ return false;
+
+ int n = points.Count;
+ var A = new Matrix(2 * n, 8);
+ var b = new Matrix(2 * n, 1);
+
+ for (int i = 0; i < n; i++)
+ {
+ double u = points[i].PixelX;
+ double v = points[i].PixelY;
+ double x = points[i].WorldX;
+ double y = points[i].WorldY;
+
+ A[2 * i, 0] = u;
+ A[2 * i, 1] = v;
+ A[2 * i, 2] = 1;
+ A[2 * i, 3] = 0;
+ A[2 * i, 4] = 0;
+ A[2 * i, 5] = 0;
+ A[2 * i, 6] = -x * u;
+ A[2 * i, 7] = -x * v;
+ b[2 * i, 0] = x;
+
+ A[2 * i + 1, 0] = 0;
+ A[2 * i + 1, 1] = 0;
+ A[2 * i + 1, 2] = 0;
+ A[2 * i + 1, 3] = u;
+ A[2 * i + 1, 4] = v;
+ A[2 * i + 1, 5] = 1;
+ A[2 * i + 1, 6] = -y * u;
+ A[2 * i + 1, 7] = -y * v;
+ b[2 * i + 1, 0] = y;
+ }
+
+ var h = new Matrix(8, 1);
+ CvInvoke.Solve(A, b, h, Emgu.CV.CvEnum.DecompMethod.Svd);
+
+ _transformMatrix = new Matrix(3, 3);
+ _transformMatrix[0, 0] = h[0, 0];
+ _transformMatrix[0, 1] = h[1, 0];
+ _transformMatrix[0, 2] = h[2, 0];
+ _transformMatrix[1, 0] = h[3, 0];
+ _transformMatrix[1, 1] = h[4, 0];
+ _transformMatrix[1, 2] = h[5, 0];
+ _transformMatrix[2, 0] = h[6, 0];
+ _transformMatrix[2, 1] = h[7, 0];
+ _transformMatrix[2, 2] = 1.0;
+
+ return true;
+ }
+
+ ///
+ /// 像素坐标转世界坐标
+ ///
+ public System.Drawing.PointF PixelToWorld(System.Drawing.PointF pixel)
+ {
+ if (_transformMatrix == null)
+ return pixel;
+
+ double u = pixel.X;
+ double v = pixel.Y;
+
+ double w = _transformMatrix[2, 0] * u + _transformMatrix[2, 1] * v + _transformMatrix[2, 2];
+ double x = (_transformMatrix[0, 0] * u + _transformMatrix[0, 1] * v + _transformMatrix[0, 2]) / w;
+ double y = (_transformMatrix[1, 0] * u + _transformMatrix[1, 1] * v + _transformMatrix[1, 2]) / w;
+
+ return new System.Drawing.PointF((float)x, (float)y);
+ }
+
+ ///
+ /// 保存标定文件
+ ///
+ public void SaveCalibration(string filePath, List points)
+ {
+ var data = new CalibrationData
+ {
+ Points = points,
+ TransformMatrix = new double[9],
+ CalibrationTime = DateTime.Now
+ };
+
+ if (_transformMatrix != null)
+ {
+ for (int i = 0; i < 3; i++)
+ for (int j = 0; j < 3; j++)
+ data.TransformMatrix[i * 3 + j] = _transformMatrix[i, j];
+ }
+
+ var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
+ File.WriteAllText(filePath, json);
+ }
+
+ ///
+ /// 加载标定文件
+ ///
+ public bool LoadCalibration(string filePath)
+ {
+ if (!File.Exists(filePath))
+ return false;
+
+ try
+ {
+ var json = File.ReadAllText(filePath);
+ var data = JsonSerializer.Deserialize(json);
+
+ if (data == null || data.TransformMatrix == null || data.TransformMatrix.Length != 9)
+ return false;
+
+ _transformMatrix = new Matrix(3, 3);
+ for (int i = 0; i < 3; i++)
+ for (int j = 0; j < 3; j++)
+ _transformMatrix[i, j] = data.TransformMatrix[i * 3 + j];
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// 从CSV文件加载标定点
+ ///
+ public List LoadPointsFromCsv(string filePath)
+ {
+ var points = new List();
+ if (!File.Exists(filePath))
+ return points;
+
+ var lines = File.ReadAllLines(filePath);
+ for (int i = 0; i < lines.Length; i++)
+ {
+ if (i == 0 && (lines[i].Contains("PixelX") || lines[i].Contains("像素"))) continue;
+
+ var parts = lines[i].Split(',');
+ if (parts.Length >= 4)
+ {
+ if (double.TryParse(parts[0].Trim(), out double px) &&
+ double.TryParse(parts[1].Trim(), out double py) &&
+ double.TryParse(parts[2].Trim(), out double wx) &&
+ double.TryParse(parts[3].Trim(), out double wy))
+ {
+ points.Add(new CalibrationPoint
+ {
+ PixelX = px,
+ PixelY = py,
+ WorldX = wx,
+ WorldY = wy
+ });
+ }
+ }
+ }
+ return points;
+ }
+}
diff --git a/XP.Camera/Calibration/ChessboardCalibrator.cs b/XP.Camera/Calibration/ChessboardCalibrator.cs
new file mode 100644
index 0000000..613d83d
--- /dev/null
+++ b/XP.Camera/Calibration/ChessboardCalibrator.cs
@@ -0,0 +1,221 @@
+// ============================================================================
+// 文件名: ChessboardCalibrator.cs
+// 描述: 棋盘格标定器,实现基于棋盘格的相机内参标定
+// 功能:
+// - 从多张棋盘格图像中检测角点
+// - 计算相机内参矩阵和畸变系数
+// - 图像去畸变处理
+// - 计算重投影误差评估标定质量
+// - 标定结果的保存和加载(JSON格式)
+// 算法: 使用 Zhang's 标定方法进行相机标定
+// ============================================================================
+
+using Emgu.CV;
+using Emgu.CV.CvEnum;
+using Emgu.CV.Structure;
+using Emgu.CV.Util;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+
+namespace XP.Camera.Calibration;
+
+public class ChessboardCalibrator
+{
+ public class CalibrationResult
+ {
+ public double[][] CameraMatrix { get; set; } = new double[3][];
+ public double[] DistortionCoeffs { get; set; } = Array.Empty();
+ public double ReprojectionError { get; set; }
+ public DateTime CalibrationTime { get; set; }
+ }
+
+ private Mat? _cameraMatrix;
+ private Mat? _distCoeffs;
+ private double _reprojectionError;
+ private List _perImageErrors = new List();
+
+ public double ReprojectionError => _reprojectionError;
+ public List PerImageErrors => _perImageErrors;
+
+ // 进度报告委托
+ public delegate void ProgressReportHandler(int current, int total, string message);
+
+ public event ProgressReportHandler? ProgressChanged;
+
+ public bool CalibrateFromImages(List imagePaths, int boardWidth, int boardHeight, float squareSize, out string errorMsg)
+ {
+ errorMsg = "";
+ var objectPoints = new VectorOfVectorOfPoint3D32F();
+ var imagePoints = new VectorOfVectorOfPointF();
+ var imageSize = new System.Drawing.Size();
+
+ var objp = new MCvPoint3D32f[boardWidth * boardHeight];
+ for (int i = 0; i < boardHeight; i++)
+ for (int j = 0; j < boardWidth; j++)
+ objp[i * boardWidth + j] = new MCvPoint3D32f(j * squareSize, i * squareSize, 0);
+
+ int validImages = 0;
+ int totalImages = imagePaths.Count;
+
+ // 第一阶段:检测角点
+ for (int idx = 0; idx < totalImages; idx++)
+ {
+ var path = imagePaths[idx];
+ ProgressChanged?.Invoke(idx + 1, totalImages * 2, $"检测角点 ({idx + 1}/{totalImages})");
+
+ var img = CvInvoke.Imread(path, ImreadModes.Grayscale);
+ if (img.IsEmpty) continue;
+
+ imageSize = img.Size;
+ var corners = new VectorOfPointF();
+
+ if (CvInvoke.FindChessboardCorners(img, new System.Drawing.Size(boardWidth, boardHeight), corners))
+ {
+ // 亚像素级角点精化
+ CvInvoke.CornerSubPix(img, corners, new System.Drawing.Size(5, 5), new System.Drawing.Size(-1, -1),
+ new MCvTermCriteria(30, 0.001));
+
+ objectPoints.Push(new VectorOfPoint3D32F(objp));
+ imagePoints.Push(corners);
+ validImages++;
+ }
+ img.Dispose();
+ }
+
+ if (validImages < 3)
+ {
+ errorMsg = $"有效图像不足,需要至少3张,当前{validImages}张";
+ ProgressChanged?.Invoke(0, 100, "标定失败");
+ return false;
+ }
+
+ // 第二阶段:执行标定
+ ProgressChanged?.Invoke(totalImages, totalImages * 2, "执行相机标定...");
+
+ _cameraMatrix = new Mat();
+ _distCoeffs = new Mat();
+ var rvecs = new VectorOfMat();
+ var tvecs = new VectorOfMat();
+
+ _reprojectionError = CvInvoke.CalibrateCamera(objectPoints, imagePoints, imageSize, _cameraMatrix, _distCoeffs,
+ rvecs, tvecs, CalibType.Default, new MCvTermCriteria(30, 1e-6));
+
+ // 第三阶段:计算每张图像的重投影误差
+ _perImageErrors.Clear();
+ for (int i = 0; i < objectPoints.Size; i++)
+ {
+ ProgressChanged?.Invoke(totalImages + i + 1, totalImages * 2, $"计算重投影误差 ({i + 1}/{objectPoints.Size})");
+
+ var projectedPoints = new VectorOfPointF();
+ CvInvoke.ProjectPoints(objectPoints[i], rvecs[i], tvecs[i], _cameraMatrix, _distCoeffs, projectedPoints);
+
+ double error = 0;
+ for (int j = 0; j < projectedPoints.Size; j++)
+ {
+ var dx = imagePoints[i][j].X - projectedPoints[j].X;
+ var dy = imagePoints[i][j].Y - projectedPoints[j].Y;
+ error += Math.Sqrt(dx * dx + dy * dy);
+ }
+ _perImageErrors.Add(error / projectedPoints.Size);
+ }
+
+ ProgressChanged?.Invoke(totalImages * 2, totalImages * 2, "标定完成");
+ return true;
+ }
+
+ public Mat UndistortImage(Mat inputImage)
+ {
+ if (_cameraMatrix == null || _distCoeffs == null)
+ return inputImage;
+
+ var output = new Mat();
+ CvInvoke.Undistort(inputImage, output, _cameraMatrix, _distCoeffs);
+ return output;
+ }
+
+ public Mat? DrawChessboardCorners(string imagePath, int boardWidth, int boardHeight)
+ {
+ var img = CvInvoke.Imread(imagePath);
+ if (img.IsEmpty) return null;
+
+ var gray = new Mat();
+ CvInvoke.CvtColor(img, gray, ColorConversion.Bgr2Gray);
+ var corners = new VectorOfPointF();
+
+ if (CvInvoke.FindChessboardCorners(gray, new System.Drawing.Size(boardWidth, boardHeight), corners))
+ {
+ CvInvoke.CornerSubPix(gray, corners, new System.Drawing.Size(11, 11), new System.Drawing.Size(-1, -1),
+ new MCvTermCriteria(30, 0.001));
+ CvInvoke.DrawChessboardCorners(img, new System.Drawing.Size(boardWidth, boardHeight), corners, true);
+ }
+
+ gray.Dispose();
+ return img;
+ }
+
+ public void SaveCalibration(string filePath)
+ {
+ if (_cameraMatrix == null || _distCoeffs == null)
+ throw new InvalidOperationException("请先执行标定");
+
+ var result = new CalibrationResult
+ {
+ CameraMatrix = new double[3][],
+ DistortionCoeffs = new double[_distCoeffs.Rows],
+ ReprojectionError = _reprojectionError,
+ CalibrationTime = DateTime.Now
+ };
+
+ for (int i = 0; i < 3; i++)
+ {
+ result.CameraMatrix[i] = new double[3];
+ for (int j = 0; j < 3; j++)
+ {
+ double[] data = new double[1];
+ Marshal.Copy(_cameraMatrix.DataPointer + (i * 3 + j) * 8, data, 0, 1);
+ result.CameraMatrix[i][j] = data[0];
+ }
+ }
+
+ for (int i = 0; i < _distCoeffs.Rows; i++)
+ {
+ double[] data = new double[1];
+ Marshal.Copy(_distCoeffs.DataPointer + i * 8, data, 0, 1);
+ result.DistortionCoeffs[i] = data[0];
+ }
+
+ var json = JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
+ File.WriteAllText(filePath, json);
+ }
+
+ public bool LoadCalibration(string filePath)
+ {
+ if (!File.Exists(filePath)) return false;
+
+ try
+ {
+ var json = File.ReadAllText(filePath);
+ var result = JsonSerializer.Deserialize(json);
+
+ if (result == null) return false;
+
+ _reprojectionError = result.ReprojectionError;
+
+ _cameraMatrix = new Mat(3, 3, DepthType.Cv64F, 1);
+ for (int i = 0; i < 3; i++)
+ for (int j = 0; j < 3; j++)
+ Marshal.Copy(new[] { result.CameraMatrix[i][j] }, 0, _cameraMatrix.DataPointer + (i * 3 + j) * 8, 1);
+
+ _distCoeffs = new Mat(result.DistortionCoeffs.Length, 1, DepthType.Cv64F, 1);
+ for (int i = 0; i < result.DistortionCoeffs.Length; i++)
+ Marshal.Copy(new[] { result.DistortionCoeffs[i] }, 0, _distCoeffs.DataPointer + i * 8, 1);
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/XP.Camera/Calibration/Controls/CalibrationControl.xaml b/XP.Camera/Calibration/Controls/CalibrationControl.xaml
new file mode 100644
index 0000000..a7bc653
--- /dev/null
+++ b/XP.Camera/Calibration/Controls/CalibrationControl.xaml
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XP.Camera/Calibration/Controls/CalibrationControl.xaml.cs b/XP.Camera/Calibration/Controls/CalibrationControl.xaml.cs
new file mode 100644
index 0000000..84a72cf
--- /dev/null
+++ b/XP.Camera/Calibration/Controls/CalibrationControl.xaml.cs
@@ -0,0 +1,104 @@
+using System.Drawing;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using XP.Camera.Calibration.ViewModels;
+using WpfBrushes = System.Windows.Media.Brushes;
+using WpfColor = System.Windows.Media.Color;
+
+namespace XP.Camera.Calibration.Controls;
+
+public partial class CalibrationControl : UserControl
+{
+ private CalibrationViewModel? _viewModel;
+
+ public CalibrationControl()
+ {
+ InitializeComponent();
+ Loaded += CalibrationControl_Loaded;
+ }
+
+ private void CalibrationControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (DataContext is CalibrationViewModel viewModel)
+ {
+ _viewModel = viewModel;
+
+ _viewModel.ImageLoadedRequested += (s, e) =>
+ {
+ imageCanvas.ReferenceImage = _viewModel.ImageSource;
+ imageCanvas.RoiCanvas.Children.Clear();
+ };
+
+ imageCanvas.CanvasRightMouseUp += ImageCanvas_RightMouseUp;
+ imageCanvas.CanvasMouseWheel += ImageCanvas_MouseWheel;
+ }
+ }
+
+ private void ImageCanvas_MouseWheel(object? sender, MouseWheelEventArgs e)
+ {
+ if (_viewModel?.CurrentImage == null) return;
+
+ double zoom = e.Delta > 0 ? 1.1 : 0.9;
+ imageCanvas.ZoomScale *= zoom;
+ imageCanvas.ZoomScale = Math.Max(0.1, Math.Min(imageCanvas.ZoomScale, 10));
+ }
+
+ private void ImageCanvas_RightMouseUp(object? sender, MouseButtonEventArgs e)
+ {
+ if (_viewModel?.CurrentImage == null) return;
+
+ var pos = e.GetPosition(imageCanvas.RoiCanvas);
+ float imageX = (float)pos.X;
+ float imageY = (float)pos.Y;
+
+ if (imageX >= 0 && imageX < _viewModel.CurrentImage.Width &&
+ imageY >= 0 && imageY < _viewModel.CurrentImage.Height)
+ {
+ var pixelPoint = new PointF(imageX, imageY);
+ var worldPoint = _viewModel.ConvertPixelToWorld(pixelPoint);
+
+ _viewModel.StatusText = $"像素坐标: ({imageX:F2}, {imageY:F2})\n世界坐标: ({worldPoint.X:F2}, {worldPoint.Y:F2})";
+
+ DrawMarkerOnCanvas(imageX, imageY, worldPoint);
+ }
+ }
+
+ private void DrawMarkerOnCanvas(float imageX, float imageY, PointF worldPoint)
+ {
+ imageCanvas.RoiCanvas.Children.Clear();
+
+ var ellipse = new System.Windows.Shapes.Ellipse
+ {
+ Width = 10, Height = 10,
+ Stroke = WpfBrushes.Red, StrokeThickness = 2,
+ Fill = WpfBrushes.Transparent
+ };
+ Canvas.SetLeft(ellipse, imageX - 5);
+ Canvas.SetTop(ellipse, imageY - 5);
+ imageCanvas.RoiCanvas.Children.Add(ellipse);
+
+ var pixelText = new TextBlock
+ {
+ Text = $"P:({imageX:F0},{imageY:F0})",
+ Foreground = WpfBrushes.Red, FontSize = 12,
+ Background = new System.Windows.Media.SolidColorBrush(WpfColor.FromArgb(180, 255, 255, 255))
+ };
+ Canvas.SetLeft(pixelText, imageX + 10);
+ Canvas.SetTop(pixelText, imageY - 20);
+ imageCanvas.RoiCanvas.Children.Add(pixelText);
+
+ if (_viewModel?.ShowWorldCoordinates == true)
+ {
+ var worldText = new TextBlock
+ {
+ Text = $"W:({worldPoint.X:F2},{worldPoint.Y:F2})",
+ Foreground = WpfBrushes.Blue, FontSize = 12,
+ Background = new System.Windows.Media.SolidColorBrush(WpfColor.FromArgb(180, 255, 255, 255))
+ };
+ Canvas.SetLeft(worldText, imageX + 10);
+ Canvas.SetTop(worldText, imageY + 5);
+ imageCanvas.RoiCanvas.Children.Add(worldText);
+ }
+ }
+}
diff --git a/XP.Camera/Calibration/Controls/ChessboardCalibrationControl.xaml b/XP.Camera/Calibration/Controls/ChessboardCalibrationControl.xaml
new file mode 100644
index 0000000..dca8b2d
--- /dev/null
+++ b/XP.Camera/Calibration/Controls/ChessboardCalibrationControl.xaml
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XP.Camera/Calibration/Controls/ChessboardCalibrationControl.xaml.cs b/XP.Camera/Calibration/Controls/ChessboardCalibrationControl.xaml.cs
new file mode 100644
index 0000000..d7425ab
--- /dev/null
+++ b/XP.Camera/Calibration/Controls/ChessboardCalibrationControl.xaml.cs
@@ -0,0 +1,46 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using XP.Camera.Calibration.ViewModels;
+
+namespace XP.Camera.Calibration.Controls;
+
+public partial class ChessboardCalibrationControl : UserControl
+{
+ private ChessboardCalibrationViewModel? _viewModel;
+
+ public ChessboardCalibrationControl()
+ {
+ InitializeComponent();
+ Loaded += ChessboardCalibrationControl_Loaded;
+ }
+
+ private void ChessboardCalibrationControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (DataContext is ChessboardCalibrationViewModel viewModel)
+ {
+ _viewModel = viewModel;
+
+ _viewModel.ImageLoadedRequested += (s, e) =>
+ {
+ imageCanvas.ReferenceImage = _viewModel.ImageSource;
+ };
+
+ _viewModel.ImageClearedRequested += (s, e) =>
+ {
+ imageCanvas.ReferenceImage = null;
+ };
+
+ imageCanvas.CanvasMouseWheel += ImageCanvas_MouseWheel;
+ }
+ }
+
+ private void ImageCanvas_MouseWheel(object? sender, MouseWheelEventArgs e)
+ {
+ if (_viewModel?.ImageSource == null) return;
+
+ double zoom = e.Delta > 0 ? 1.1 : 0.9;
+ imageCanvas.ZoomScale *= zoom;
+ imageCanvas.ZoomScale = Math.Max(0.1, Math.Min(imageCanvas.ZoomScale, 10));
+ }
+}
diff --git a/XP.Camera/Calibration/Controls/ImageCanvasControl.xaml b/XP.Camera/Calibration/Controls/ImageCanvasControl.xaml
new file mode 100644
index 0000000..27940d6
--- /dev/null
+++ b/XP.Camera/Calibration/Controls/ImageCanvasControl.xaml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XP.Camera/Calibration/Controls/ImageCanvasControl.xaml.cs b/XP.Camera/Calibration/Controls/ImageCanvasControl.xaml.cs
new file mode 100644
index 0000000..e3d2b61
--- /dev/null
+++ b/XP.Camera/Calibration/Controls/ImageCanvasControl.xaml.cs
@@ -0,0 +1,229 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+
+namespace XP.Camera.Calibration.Controls;
+
+///
+/// 图像画布控件 - 提供图像显示、缩放、平移功能
+///
+public partial class ImageCanvasControl : UserControl
+{
+ private Point mouseDownPoint = new Point();
+
+ #region Dependency Properties
+
+ public static readonly DependencyProperty ZoomScaleProperty =
+ DependencyProperty.Register("ZoomScale", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(1.0));
+
+ public static readonly DependencyProperty ZoomCenterProperty =
+ DependencyProperty.Register("ZoomCenter", typeof(Point), typeof(ImageCanvasControl), new PropertyMetadata(new Point()));
+
+ public static readonly DependencyProperty PanningOffsetXProperty =
+ DependencyProperty.Register("PanningOffsetX", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(0.0));
+
+ public static readonly DependencyProperty PanningOffsetYProperty =
+ DependencyProperty.Register("PanningOffsetY", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(0.0));
+
+ public static readonly DependencyProperty ReferenceImageProperty =
+ DependencyProperty.Register("ReferenceImage", typeof(BitmapSource), typeof(ImageCanvasControl),
+ new UIPropertyMetadata(null, ReferenceImageChanged));
+
+ public static readonly DependencyProperty ImageScaleFactorProperty =
+ DependencyProperty.Register("ImageScaleFactor", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(1.0));
+
+ public static readonly DependencyProperty MaxImageWidthProperty =
+ DependencyProperty.Register("MaxImageWidth", typeof(int), typeof(ImageCanvasControl), new PropertyMetadata(0));
+
+ public static readonly DependencyProperty MaxImageHeightProperty =
+ DependencyProperty.Register("MaxImageHeight", typeof(int), typeof(ImageCanvasControl), new PropertyMetadata(0));
+
+ public static readonly DependencyProperty EnablePanningProperty =
+ DependencyProperty.Register("EnablePanning", typeof(bool), typeof(ImageCanvasControl), new PropertyMetadata(true));
+
+ #endregion
+
+ #region Properties
+
+ public double ZoomScale
+ {
+ get => (double)GetValue(ZoomScaleProperty);
+ set => SetValue(ZoomScaleProperty, value);
+ }
+
+ public Point ZoomCenter
+ {
+ get => (Point)GetValue(ZoomCenterProperty);
+ set => SetValue(ZoomCenterProperty, value);
+ }
+
+ public double PanningOffsetX
+ {
+ get => (double)GetValue(PanningOffsetXProperty);
+ set => SetValue(PanningOffsetXProperty, value);
+ }
+
+ public double PanningOffsetY
+ {
+ get => (double)GetValue(PanningOffsetYProperty);
+ set => SetValue(PanningOffsetYProperty, value);
+ }
+
+ public BitmapSource? ReferenceImage
+ {
+ get => (BitmapSource?)GetValue(ReferenceImageProperty);
+ set => SetValue(ReferenceImageProperty, value);
+ }
+
+ public double ImageScaleFactor
+ {
+ get => (double)GetValue(ImageScaleFactorProperty);
+ set => SetValue(ImageScaleFactorProperty, value);
+ }
+
+ public int MaxImageWidth
+ {
+ get => (int)GetValue(MaxImageWidthProperty);
+ set => SetValue(MaxImageWidthProperty, value);
+ }
+
+ public int MaxImageHeight
+ {
+ get => (int)GetValue(MaxImageHeightProperty);
+ set => SetValue(MaxImageHeightProperty, value);
+ }
+
+ public bool EnablePanning
+ {
+ get => (bool)GetValue(EnablePanningProperty);
+ set => SetValue(EnablePanningProperty, value);
+ }
+
+ private Canvas roiCanvas = new Canvas();
+ public Canvas RoiCanvas
+ {
+ get => roiCanvas;
+ set => roiCanvas = value;
+ }
+
+ #endregion
+
+ #region Events
+
+ public event EventHandler? CanvasRightMouseUp;
+ public event EventHandler? CanvasRightMouseDown;
+ public event EventHandler? CanvasLeftMouseDown;
+ public event EventHandler? CanvasMouseMove;
+ public event EventHandler? CanvasMouseWheel;
+
+ #endregion
+
+ public ImageCanvasControl()
+ {
+ InitializeComponent();
+ }
+
+ #region Private Methods
+
+ private static void ReferenceImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ (d as ImageCanvasControl)?.OnReferenceImageChanged(e.NewValue as BitmapSource);
+ }
+
+ private void OnReferenceImageChanged(BitmapSource? bitmapSource)
+ {
+ if (bitmapSource != null)
+ {
+ ImageBrush brush = new ImageBrush { ImageSource = bitmapSource, Stretch = Stretch.Uniform };
+ RoiCanvas.Background = brush;
+ RoiCanvas.Height = bitmapSource.Height;
+ RoiCanvas.Width = bitmapSource.Width;
+ }
+ else
+ {
+ RoiCanvas.Height = MaxImageHeight > 0 ? MaxImageHeight : 600;
+ RoiCanvas.Width = MaxImageWidth > 0 ? MaxImageWidth : 800;
+ RoiCanvas.Background = Brushes.LightGray;
+ }
+ }
+
+ private double CalculateScaleFactor()
+ {
+ if (ActualWidth <= 0) return 1;
+ double scaleFactor = Math.Max(RoiCanvas.Width / ActualWidth, RoiCanvas.Height / ActualHeight);
+ if (scaleFactor < 0)
+ scaleFactor = Math.Min(RoiCanvas.Width / ActualWidth, RoiCanvas.Height / ActualHeight);
+ return scaleFactor;
+ }
+
+ #endregion
+
+ #region Event Handlers
+
+ private void Canvas_MouseEnter(object sender, MouseEventArgs e)
+ {
+ if (e.LeftButton == MouseButtonState.Pressed)
+ mouseDownPoint = e.GetPosition(RoiCanvas);
+ }
+
+ private void Canvas_MouseMove(object sender, MouseEventArgs e)
+ {
+ CanvasMouseMove?.Invoke(sender, e);
+ if (EnablePanning && e.LeftButton == MouseButtonState.Pressed)
+ {
+ Point mousePoint = e.GetPosition(RoiCanvas);
+ double mouseMoveLength = Point.Subtract(mousePoint, mouseDownPoint).Length;
+ if (mouseMoveLength > (10 * CalculateScaleFactor()) / ZoomScale)
+ {
+ PanningOffsetX += mousePoint.X - mouseDownPoint.X;
+ PanningOffsetY += mousePoint.Y - mouseDownPoint.Y;
+ }
+ }
+ }
+
+ private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { }
+
+ private void Canvas_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ CanvasRightMouseUp?.Invoke(sender, e);
+ }
+
+ private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ mouseDownPoint = e.GetPosition(RoiCanvas);
+ CanvasLeftMouseDown?.Invoke(sender, e);
+ if (EnablePanning && e.ClickCount == 2)
+ {
+ ResetView();
+ e.Handled = true;
+ }
+ }
+
+ private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ CanvasRightMouseDown?.Invoke(sender, e);
+ e.Handled = true;
+ }
+
+ private void Adorner_MouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ CanvasMouseWheel?.Invoke(sender, e);
+ }
+
+ private void ContentPresenter_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ ImageScaleFactor = CalculateScaleFactor();
+ }
+
+ private void ResetView()
+ {
+ ZoomScale = 1.0;
+ PanningOffsetX = 0.0;
+ PanningOffsetY = 0.0;
+ ZoomCenter = new Point(0, 0);
+ }
+
+ #endregion
+}
diff --git a/XP.Camera/Calibration/DefaultCalibrationDialogService.cs b/XP.Camera/Calibration/DefaultCalibrationDialogService.cs
new file mode 100644
index 0000000..13e456d
--- /dev/null
+++ b/XP.Camera/Calibration/DefaultCalibrationDialogService.cs
@@ -0,0 +1,50 @@
+using Microsoft.Win32;
+using System.Windows;
+using XP.Common.GeneralForm.Views;
+
+namespace XP.Camera.Calibration;
+
+///
+/// 默认对话框服务实现,使用 HexMessageBox 自定义消息框。
+/// 外部项目可直接使用,无需额外依赖。
+///
+public class DefaultCalibrationDialogService : ICalibrationDialogService
+{
+ public void ShowMessage(string message, string title)
+ {
+ HexMessageBox.Show(message, MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ public void ShowError(string message, string title)
+ {
+ HexMessageBox.Show(message, MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+
+ public void ShowInfo(string message, string title)
+ {
+ HexMessageBox.Show(message, MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ public bool ShowConfirm(string message, string title)
+ {
+ return HexMessageBox.Show(message, MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes;
+ }
+
+ public string? ShowOpenFileDialog(string filter)
+ {
+ var dialog = new OpenFileDialog { Filter = filter };
+ return dialog.ShowDialog() == true ? dialog.FileName : null;
+ }
+
+ public string[]? ShowOpenMultipleFilesDialog(string filter)
+ {
+ var dialog = new OpenFileDialog { Filter = filter, Multiselect = true };
+ return dialog.ShowDialog() == true ? dialog.FileNames : null;
+ }
+
+ public string? ShowSaveFileDialog(string filter, string? defaultFileName = null)
+ {
+ var dialog = new SaveFileDialog { Filter = filter, FileName = defaultFileName ?? string.Empty };
+ return dialog.ShowDialog() == true ? dialog.FileName : null;
+ }
+}
diff --git a/XP.Camera/Calibration/IDialogService.cs b/XP.Camera/Calibration/IDialogService.cs
new file mode 100644
index 0000000..d6a9a5d
--- /dev/null
+++ b/XP.Camera/Calibration/IDialogService.cs
@@ -0,0 +1,15 @@
+namespace XP.Camera.Calibration;
+
+///
+/// 对话框服务接口,用于标定模块的文件选择和消息提示。
+///
+public interface ICalibrationDialogService
+{
+ void ShowMessage(string message, string title);
+ void ShowError(string message, string title);
+ void ShowInfo(string message, string title);
+ bool ShowConfirm(string message, string title);
+ string? ShowOpenFileDialog(string filter);
+ string[]? ShowOpenMultipleFilesDialog(string filter);
+ string? ShowSaveFileDialog(string filter, string? defaultFileName = null);
+}
diff --git a/XP.Camera/Calibration/Resources/CalibrationResources.Designer.cs b/XP.Camera/Calibration/Resources/CalibrationResources.Designer.cs
new file mode 100644
index 0000000..18f6cc4
--- /dev/null
+++ b/XP.Camera/Calibration/Resources/CalibrationResources.Designer.cs
@@ -0,0 +1,104 @@
+//------------------------------------------------------------------------------
+//
+// 此代码由工具生成。
+// 如果重新生成代码,将丢失对此文件所做的更改。
+//
+//------------------------------------------------------------------------------
+
+namespace XP.Camera.Calibration.Resources {
+ using System;
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class CalibrationResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal CalibrationResources() { }
+
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("XP.Camera.Calibration.Resources.CalibrationResources", typeof(CalibrationResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get { return resourceCulture; }
+ set { resourceCulture = value; }
+ }
+
+ // 九点标定
+ public static string CalibrationToolTitle => ResourceManager.GetString("CalibrationToolTitle", resourceCulture) ?? "";
+ public static string CalibrationLoadImage => ResourceManager.GetString("CalibrationLoadImage", resourceCulture) ?? "";
+ public static string CalibrationLoadCsv => ResourceManager.GetString("CalibrationLoadCsv", resourceCulture) ?? "";
+ public static string CalibrationExecute => ResourceManager.GetString("CalibrationExecute", resourceCulture) ?? "";
+ public static string CalibrationSave => ResourceManager.GetString("CalibrationSave", resourceCulture) ?? "";
+ public static string CalibrationLoad => ResourceManager.GetString("CalibrationLoad", resourceCulture) ?? "";
+ public static string CalibrationShowWorld => ResourceManager.GetString("CalibrationShowWorld", resourceCulture) ?? "";
+ public static string CalibrationPointList => ResourceManager.GetString("CalibrationPointList", resourceCulture) ?? "";
+ public static string CalibrationPixelX => ResourceManager.GetString("CalibrationPixelX", resourceCulture) ?? "";
+ public static string CalibrationPixelY => ResourceManager.GetString("CalibrationPixelY", resourceCulture) ?? "";
+ public static string CalibrationWorldX => ResourceManager.GetString("CalibrationWorldX", resourceCulture) ?? "";
+ public static string CalibrationWorldY => ResourceManager.GetString("CalibrationWorldY", resourceCulture) ?? "";
+ public static string CalibrationStatusReady => ResourceManager.GetString("CalibrationStatusReady", resourceCulture) ?? "";
+ public static string CalibrationStatusImageLoaded => ResourceManager.GetString("CalibrationStatusImageLoaded", resourceCulture) ?? "";
+ public static string CalibrationStatusCsvLoaded => ResourceManager.GetString("CalibrationStatusCsvLoaded", resourceCulture) ?? "";
+ public static string CalibrationStatusSuccess => ResourceManager.GetString("CalibrationStatusSuccess", resourceCulture) ?? "";
+ public static string CalibrationStatusFailed => ResourceManager.GetString("CalibrationStatusFailed", resourceCulture) ?? "";
+ public static string CalibrationStatusSaved => ResourceManager.GetString("CalibrationStatusSaved", resourceCulture) ?? "";
+ public static string CalibrationStatusLoaded => ResourceManager.GetString("CalibrationStatusLoaded", resourceCulture) ?? "";
+ public static string CalibrationCoordinates => ResourceManager.GetString("CalibrationCoordinates", resourceCulture) ?? "";
+ public static string CalibrationErrorMinPoints => ResourceManager.GetString("CalibrationErrorMinPoints", resourceCulture) ?? "";
+ public static string CalibrationSuccessTitle => ResourceManager.GetString("CalibrationSuccessTitle", resourceCulture) ?? "";
+ public static string CalibrationSuccessMessage => ResourceManager.GetString("CalibrationSuccessMessage", resourceCulture) ?? "";
+ public static string CalibrationSaveSuccess => ResourceManager.GetString("CalibrationSaveSuccess", resourceCulture) ?? "";
+ public static string CalibrationLoadSuccess => ResourceManager.GetString("CalibrationLoadSuccess", resourceCulture) ?? "";
+ public static string CalibrationLoadFailed => ResourceManager.GetString("CalibrationLoadFailed", resourceCulture) ?? "";
+
+ // 棋盘格标定
+ public static string ChessboardToolTitle => ResourceManager.GetString("ChessboardToolTitle", resourceCulture) ?? "";
+ public static string ChessboardAddImages => ResourceManager.GetString("ChessboardAddImages", resourceCulture) ?? "";
+ public static string ChessboardClearImages => ResourceManager.GetString("ChessboardClearImages", resourceCulture) ?? "";
+ public static string ChessboardCalibrate => ResourceManager.GetString("ChessboardCalibrate", resourceCulture) ?? "";
+ public static string ChessboardSave => ResourceManager.GetString("ChessboardSave", resourceCulture) ?? "";
+ public static string ChessboardLoad => ResourceManager.GetString("ChessboardLoad", resourceCulture) ?? "";
+ public static string ChessboardUndistort => ResourceManager.GetString("ChessboardUndistort", resourceCulture) ?? "";
+ public static string ChessboardParameters => ResourceManager.GetString("ChessboardParameters", resourceCulture) ?? "";
+ public static string ChessboardWidth => ResourceManager.GetString("ChessboardWidth", resourceCulture) ?? "";
+ public static string ChessboardHeight => ResourceManager.GetString("ChessboardHeight", resourceCulture) ?? "";
+ public static string ChessboardSquareSize => ResourceManager.GetString("ChessboardSquareSize", resourceCulture) ?? "";
+ public static string ChessboardImageList => ResourceManager.GetString("ChessboardImageList", resourceCulture) ?? "";
+ public static string ChessboardStatusInfo => ResourceManager.GetString("ChessboardStatusInfo", resourceCulture) ?? "";
+ public static string ChessboardStatusReady => ResourceManager.GetString("ChessboardStatusReady", resourceCulture) ?? "";
+ public static string ChessboardStatusAdded => ResourceManager.GetString("ChessboardStatusAdded", resourceCulture) ?? "";
+ public static string ChessboardStatusCleared => ResourceManager.GetString("ChessboardStatusCleared", resourceCulture) ?? "";
+ public static string ChessboardStatusCalibrating => ResourceManager.GetString("ChessboardStatusCalibrating", resourceCulture) ?? "";
+ public static string ChessboardStatusSuccess => ResourceManager.GetString("ChessboardStatusSuccess", resourceCulture) ?? "";
+ public static string ChessboardStatusFailed => ResourceManager.GetString("ChessboardStatusFailed", resourceCulture) ?? "";
+ public static string ChessboardStatusSaved => ResourceManager.GetString("ChessboardStatusSaved", resourceCulture) ?? "";
+ public static string ChessboardStatusLoaded => ResourceManager.GetString("ChessboardStatusLoaded", resourceCulture) ?? "";
+ public static string ChessboardStatusUndistorted => ResourceManager.GetString("ChessboardStatusUndistorted", resourceCulture) ?? "";
+ public static string ChessboardStatusImageError => ResourceManager.GetString("ChessboardStatusImageError", resourceCulture) ?? "";
+ public static string ChessboardProgressPreparing => ResourceManager.GetString("ChessboardProgressPreparing", resourceCulture) ?? "";
+ public static string ChessboardProgressDetecting => ResourceManager.GetString("ChessboardProgressDetecting", resourceCulture) ?? "";
+ public static string ChessboardProgressCalibrating => ResourceManager.GetString("ChessboardProgressCalibrating", resourceCulture) ?? "";
+ public static string ChessboardProgressCalculating => ResourceManager.GetString("ChessboardProgressCalculating", resourceCulture) ?? "";
+ public static string ChessboardProgressComplete => ResourceManager.GetString("ChessboardProgressComplete", resourceCulture) ?? "";
+ public static string ChessboardProgressFailed => ResourceManager.GetString("ChessboardProgressFailed", resourceCulture) ?? "";
+ public static string ChessboardErrorMinImages => ResourceManager.GetString("ChessboardErrorMinImages", resourceCulture) ?? "";
+ public static string ChessboardErrorInsufficientValid => ResourceManager.GetString("ChessboardErrorInsufficientValid", resourceCulture) ?? "";
+ public static string ChessboardSaveSuccess => ResourceManager.GetString("ChessboardSaveSuccess", resourceCulture) ?? "";
+ public static string ChessboardLoadSuccess => ResourceManager.GetString("ChessboardLoadSuccess", resourceCulture) ?? "";
+ public static string ChessboardCalibrationComplete => ResourceManager.GetString("ChessboardCalibrationComplete", resourceCulture) ?? "";
+ public static string ChessboardImageError => ResourceManager.GetString("ChessboardImageError", resourceCulture) ?? "";
+ }
+}
diff --git a/XP.Camera/Calibration/Resources/CalibrationResources.en-US.resx b/XP.Camera/Calibration/Resources/CalibrationResources.en-US.resx
new file mode 100644
index 0000000..033481e
--- /dev/null
+++ b/XP.Camera/Calibration/Resources/CalibrationResources.en-US.resx
@@ -0,0 +1,259 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ Nine-Point Calibration Tool
+
+
+ Load Image
+
+
+ Load from CSV
+
+
+ Calibrate
+
+
+ Save Calibration
+
+
+ Load Calibration
+
+
+ Show World Coordinates
+
+
+ Calibration Points
+
+
+ Pixel X
+
+
+ Pixel Y
+
+
+ World X
+
+
+ World Y
+
+
+ Ready
+
+
+ Status: Image loaded
+{0}
+Right-click on image to view coordinate conversion
+
+
+ Status: Loaded {0} calibration points from CSV
+{1}
+
+
+ Status: Calibration successful! Using {0} points
+
+
+ Status: Calibration failed
+
+
+ Status: Calibration saved to
+{0}
+
+
+ Status: Calibration loaded from
+{0}
+
+
+ Pixel coordinates: ({0:F2}, {1:F2})
+World coordinates: ({2:F2}, {3:F2})
+
+
+ At least 4 calibration points required!
+
+
+ Success
+
+
+ Calibration completed!
+
+
+ Save successful!
+
+
+ Load successful!
+
+
+ Load failed!
+
+
+
+ Chessboard Calibration Tool
+
+
+ Add Images
+
+
+ Clear List
+
+
+ Calibrate
+
+
+ Save Calibration
+
+
+ Load Calibration
+
+
+ Undistort Image
+
+
+ Chessboard Parameters
+
+
+ Inner Corners Width:
+
+
+ Inner Corners Height:
+
+
+ Square Size (mm):
+
+
+ Calibration Images
+
+
+ Status Information
+
+
+ Ready
+
+
+ Added {0} images
+
+
+ Image list cleared
+
+
+ Calibrating, please wait...
+
+
+ Calibration successful!
+Overall reprojection error: {0:F4} pixels
+
+{1}
+
+
+ Calibration failed: {0}
+
+
+ Calibration saved:
+{0}
+
+
+ Calibration loaded:
+{0}
+
+
+ Image undistorted:
+{0}
+
+
+ Image {0}
+Reprojection error: {1:F4} pixels
+
+
+ Preparing calibration...
+
+
+ Detecting corners ({0}/{1})
+
+
+ Performing camera calibration...
+
+
+ Calculating reprojection errors ({0}/{1})
+
+
+ Calibration complete
+
+
+ Calibration failed
+
+
+ At least 3 images required!
+
+
+ Insufficient valid images, need at least 3, current {0}
+
+
+ Save successful!
+
+
+ Load successful!
+
+
+ Calibration completed!
+
+
+ Image{0}: {1:F4} pixels
+
+
diff --git a/XP.Camera/Calibration/Resources/CalibrationResources.resx b/XP.Camera/Calibration/Resources/CalibrationResources.resx
new file mode 100644
index 0000000..4a91b42
--- /dev/null
+++ b/XP.Camera/Calibration/Resources/CalibrationResources.resx
@@ -0,0 +1,259 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ 九点标定工具
+
+
+ 加载图像
+
+
+ 从CSV加载
+
+
+ 执行标定
+
+
+ 保存标定
+
+
+ 加载标定
+
+
+ 显示世界坐标
+
+
+ 标定点列表
+
+
+ 像素X
+
+
+ 像素Y
+
+
+ 世界X
+
+
+ 世界Y
+
+
+ 就绪
+
+
+ 状态:图像已加载
+{0}
+右键点击图像查看坐标转换
+
+
+ 状态:已从CSV加载 {0} 个标定点
+{1}
+
+
+ 状态:标定成功!使用 {0} 个点
+
+
+ 状态:标定失败
+
+
+ 状态:标定文件已保存到
+{0}
+
+
+ 状态:标定文件已加载
+{0}
+
+
+ 像素坐标: ({0:F2}, {1:F2})
+世界坐标: ({2:F2}, {3:F2})
+
+
+ 至少需要4个标定点!
+
+
+ 成功
+
+
+ 标定完成!
+
+
+ 保存成功!
+
+
+ 加载成功!
+
+
+ 加载失败!
+
+
+
+ 棋盘格畸变校正工具
+
+
+ 添加图像
+
+
+ 清空列表
+
+
+ 执行标定
+
+
+ 保存标定
+
+
+ 加载标定
+
+
+ 校正图像
+
+
+ 棋盘格参数
+
+
+ 内角点宽度:
+
+
+ 内角点高度:
+
+
+ 方格尺寸(mm):
+
+
+ 标定图像列表
+
+
+ 状态信息
+
+
+ 就绪
+
+
+ 已添加 {0} 张图像
+
+
+ 已清空图像列表
+
+
+ 正在标定,请稍候...
+
+
+ 标定成功!
+总体重投影误差: {0:F4} 像素
+
+{1}
+
+
+ 标定失败: {0}
+
+
+ 标定已保存:
+{0}
+
+
+ 标定已加载:
+{0}
+
+
+ 已校正图像:
+{0}
+
+
+ 图像 {0}
+重投影误差: {1:F4} 像素
+
+
+ 准备标定...
+
+
+ 检测角点 ({0}/{1})
+
+
+ 执行相机标定...
+
+
+ 计算重投影误差 ({0}/{1})
+
+
+ 标定完成
+
+
+ 标定失败
+
+
+ 至少需要3张图像!
+
+
+ 有效图像不足,需要至少3张,当前{0}张
+
+
+ 保存成功!
+
+
+ 加载成功!
+
+
+ 标定完成!
+
+
+ 图像{0}: {1:F4} 像素
+
+
diff --git a/XP.Camera/Calibration/ViewModels/CalibrationViewModel.cs b/XP.Camera/Calibration/ViewModels/CalibrationViewModel.cs
new file mode 100644
index 0000000..90c7acd
--- /dev/null
+++ b/XP.Camera/Calibration/ViewModels/CalibrationViewModel.cs
@@ -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? _currentImage;
+ private static readonly ILogger _logger = Log.ForContext();
+ private BitmapSource? _imageSource;
+ private string _statusText = Res.CalibrationStatusReady;
+ private bool _showWorldCoordinates;
+
+ public CalibrationViewModel(ICalibrationDialogService dialogService)
+ {
+ _dialogService = dialogService;
+ 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);
+ }
+
+ public ObservableCollection 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(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);
+ }
+ }
+
+ 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 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);
+}
diff --git a/XP.Camera/Calibration/ViewModels/ChessboardCalibrationViewModel.cs b/XP.Camera/Calibration/ViewModels/ChessboardCalibrationViewModel.cs
new file mode 100644
index 0000000..d06f443
--- /dev/null
+++ b/XP.Camera/Calibration/ViewModels/ChessboardCalibrationViewModel.cs
@@ -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 _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();
+
+ 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 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(_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);
+}
diff --git a/XP.Camera/PixelConverter.cs b/XP.Camera/Converters/PixelConverter.cs
similarity index 100%
rename from XP.Camera/PixelConverter.cs
rename to XP.Camera/Converters/PixelConverter.cs
diff --git a/XP.Camera/CameraExceptions.cs b/XP.Camera/Core/CameraExceptions.cs
similarity index 100%
rename from XP.Camera/CameraExceptions.cs
rename to XP.Camera/Core/CameraExceptions.cs
diff --git a/XP.Camera/CameraFactory.cs b/XP.Camera/Core/CameraFactory.cs
similarity index 100%
rename from XP.Camera/CameraFactory.cs
rename to XP.Camera/Core/CameraFactory.cs
diff --git a/XP.Camera/CameraModels.cs b/XP.Camera/Core/CameraModels.cs
similarity index 100%
rename from XP.Camera/CameraModels.cs
rename to XP.Camera/Core/CameraModels.cs
diff --git a/XP.Camera/ICameraController.cs b/XP.Camera/Core/ICameraController.cs
similarity index 100%
rename from XP.Camera/ICameraController.cs
rename to XP.Camera/Core/ICameraController.cs
diff --git a/XP.Camera/README.md b/XP.Camera/README.md
index c72208a..526db80 100644
--- a/XP.Camera/README.md
+++ b/XP.Camera/README.md
@@ -1,6 +1,6 @@
# XP.Camera 使用说明
-基于 .NET 8 WPF 的工业相机控制类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
+基于 .NET 8 WPF 的工业相机控制与标定类库,采用工厂模式 + 统一接口设计,支持多品牌相机扩展。当前已实现 Basler pylon SDK 驱动。
## 环境要求
@@ -12,16 +12,36 @@
```
XP.Camera/
-├── ICameraController.cs # 控制器接口 + 工厂接口
-├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
-├── BaslerCameraController.cs # Basler 实现
-├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
-├── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
-├── PixelConverter.cs # 像素数据 → WPF BitmapSource 转换工具
+├── Core/ # 相机核心抽象
+│ ├── ICameraController.cs # 控制器接口 + 工厂接口
+│ ├── CameraFactory.cs # 统一工厂(根据品牌创建控制器)
+│ ├── CameraModels.cs # CameraInfo、ImageGrabbedEventArgs、GrabErrorEventArgs
+│ └── CameraExceptions.cs # CameraException、ConnectionLostException、DeviceNotFoundException
+├── Basler/ # Basler 品牌实现
+│ └── BaslerCameraController.cs # Basler pylon SDK 实现
+├── Converters/ # 数据转换工具
+│ └── PixelConverter.cs # 像素数据 → WPF BitmapSource 转换
+├── Calibration/ # 相机标定模块
+│ ├── CalibrationProcessor.cs # 九点标定(DLT 单应性矩阵,像素→世界坐标)
+│ ├── ChessboardCalibrator.cs # 棋盘格标定(Zhang's 方法,内参 + 畸变校正)
+│ ├── IDialogService.cs # ICalibrationDialogService 接口
+│ ├── DefaultCalibrationDialogService.cs # 默认实现(标准 WPF MessageBox)
+│ ├── CalibrationLocalizedStrings.cs # XAML 本地化绑定辅助
+│ ├── Controls/ # 标定 UI 控件(UserControl)
+│ │ ├── CalibrationControl.xaml/.cs # 九点标定界面
+│ │ ├── ChessboardCalibrationControl.xaml/.cs # 棋盘格标定界面
+│ │ └── ImageCanvasControl.xaml/.cs # 图像画布(缩放/平移)
+│ ├── ViewModels/ # 标定视图模型
+│ │ ├── CalibrationViewModel.cs
+│ │ └── ChessboardCalibrationViewModel.cs
+│ └── Resources/ # 本地化资源
+│ ├── CalibrationResources.resx # 中文(默认)
+│ ├── CalibrationResources.en-US.resx # 英文
+│ └── CalibrationResources.Designer.cs
└── XP.Camera.csproj
```
-所有类型统一在 `XP.Camera` 命名空间下。
+所有相机核心类型在 `XP.Camera` 命名空间下,标定模块在 `XP.Camera.Calibration` 命名空间下。
## 项目引用
@@ -48,27 +68,12 @@ Console.WriteLine($"已连接: {info.ModelName} (SN: {info.SerialNumber})");
在 Prism / DI 容器中注册:
```csharp
-// App.xaml.cs
var config = AppConfig.Load();
containerRegistry.RegisterSingleton();
containerRegistry.RegisterSingleton(() =>
new CameraFactory().CreateController(config.CameraType));
```
-ViewModel 中注入使用:
-
-```csharp
-public class MyViewModel
-{
- private readonly ICameraController _camera;
-
- public MyViewModel(ICameraController camera)
- {
- _camera = camera;
- }
-}
-```
-
相机品牌通过配置文件 `config.json` 指定:
```json
@@ -82,60 +87,85 @@ public class MyViewModel
```csharp
_camera.ImageGrabbed += (s, e) =>
{
- // PixelConverter 返回已 Freeze 的 BitmapSource,可跨线程传递
var bitmap = PixelConverter.ToBitmapSource(
e.PixelData, e.Width, e.Height, e.PixelFormat);
-
- Application.Current.Dispatcher.Invoke(() =>
- {
- CameraImageSource = bitmap;
- });
+ Application.Current.Dispatcher.Invoke(() => CameraImageSource = bitmap);
};
```
-XAML 绑定:
-
-```xml
-
-```
-
### 4. 软件触发采集流程
```csharp
camera.Open();
camera.SetExposureTime(10000); // 10ms
camera.StartGrabbing();
-
-// 每次需要采集时调用(结果通过 ImageGrabbed 事件返回)
camera.ExecuteSoftwareTrigger();
-
camera.StopGrabbing();
camera.Close();
```
-### 5. 实时连续采集(链式触发)
+### 5. 使用标定模块
-收到上一帧后立即触发下一帧,自动适配任何帧率:
+标定模块完全自包含,可独立使用,无需外部依赖。
+
+#### 棋盘格标定(相机内参 + 畸变校正)
```csharp
-private volatile bool _liveViewRunning;
+using XP.Camera.Calibration;
+using XP.Camera.Calibration.ViewModels;
+using XP.Camera.Calibration.Controls;
-_camera.ImageGrabbed += (s, e) =>
+// 使用默认对话框服务(标准 WPF MessageBox)
+var dialogService = new DefaultCalibrationDialogService();
+var viewModel = new ChessboardCalibrationViewModel(dialogService);
+
+var window = new Window
{
- var bitmap = PixelConverter.ToBitmapSource(e.PixelData, e.Width, e.Height, e.PixelFormat);
- Application.Current.Dispatcher.Invoke(() => CameraImageSource = bitmap);
-
- if (_liveViewRunning)
- _camera.ExecuteSoftwareTrigger(); // 链式触发下一帧
+ Title = "棋盘格标定",
+ Width = 1600, Height = 900,
+ Content = new ChessboardCalibrationControl { DataContext = viewModel }
};
+window.ShowDialog();
+```
-// 启动实时
-_camera.StartGrabbing();
-_liveViewRunning = true;
-_camera.ExecuteSoftwareTrigger(); // 触发第一帧
+#### 九点标定(像素→世界坐标)
-// 停止实时
-_liveViewRunning = false;
+```csharp
+var dialogService = new DefaultCalibrationDialogService();
+var viewModel = new CalibrationViewModel(dialogService);
+
+var window = new Window
+{
+ Title = "九点标定",
+ Width = 1400, Height = 850,
+ Content = new CalibrationControl { DataContext = viewModel }
+};
+window.ShowDialog();
+```
+
+#### 纯算法调用(不使用 UI)
+
+```csharp
+// 棋盘格标定
+var calibrator = new ChessboardCalibrator();
+calibrator.CalibrateFromImages(imagePaths, boardWidth: 11, boardHeight: 8, squareSize: 15f, out string error);
+calibrator.SaveCalibration("camera_calibration.json");
+
+// 九点标定
+var processor = new CalibrationProcessor();
+processor.Calibrate(points);
+var worldPoint = processor.PixelToWorld(new PointF(100, 200));
+```
+
+#### 自定义对话框服务
+
+如需自定义弹框样式,实现 `ICalibrationDialogService` 接口即可:
+
+```csharp
+public class MyDialogService : ICalibrationDialogService
+{
+ // 实现所有接口方法,使用自定义 UI 组件...
+}
```
## 核心接口
@@ -149,94 +179,37 @@ _liveViewRunning = false;
| `StartGrabbing()` | 以软件触发模式启动采集 |
| `ExecuteSoftwareTrigger()` | 触发一帧采集 |
| `StopGrabbing()` | 停止采集 |
+| `Get/SetExposureTime` | 曝光时间(微秒) |
+| `Get/SetGain` | 增益值 |
+| `Get/SetWidth/Height` | 图像尺寸 |
+| `Get/SetPixelFormat` | 像素格式(Mono8 / BGR8 / BGRA8) |
-### 参数读写
+### 事件
-| 方法 | 说明 |
+| 事件 | 说明 |
|------|------|
-| `Get/SetExposureTime(double)` | 曝光时间(微秒) |
-| `Get/SetGain(double)` | 增益值 |
-| `Get/SetWidth(int)` | 图像宽度(自动校正到有效值) |
-| `Get/SetHeight(int)` | 图像高度(自动校正到有效值) |
-| `Get/SetPixelFormat(string)` | 像素格式(Mono8 / BGR8 / BGRA8) |
+| `ImageGrabbed` | 成功采集一帧图像 |
+| `GrabError` | 图像采集失败 |
+| `ConnectionLost` | 相机连接意外断开 |
-### ICameraFactory
-
-| 方法 | 说明 |
-|------|------|
-| `CreateController(string cameraType)` | 根据品牌名创建控制器 |
-
-当前支持的 `cameraType` 值:`"Basler"`
-
-## 事件
-
-| 事件 | 说明 | 触发线程 |
-|------|------|----------|
-| `ImageGrabbed` | 成功采集一帧图像 | StreamGrabber 回调线程 |
-| `GrabError` | 图像采集失败 | StreamGrabber 回调线程 |
-| `ConnectionLost` | 相机连接意外断开 | pylon SDK 事件线程 |
-
-> 所有事件均在非 UI 线程触发。更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
-> `PixelConverter.ToBitmapSource()` 返回的 BitmapSource 已调用 `Freeze()`,可直接跨线程传递。
+> 所有事件均在非 UI 线程触发,更新 WPF 界面时需通过 `Dispatcher.Invoke` 调度。
## 异常处理
-```csharp
-try
-{
- camera.Open();
-}
-catch (DeviceNotFoundException)
-{
- // 无可用相机设备
-}
-catch (CameraException ex)
-{
- // 其他相机错误,ex.InnerException 包含原始 SDK 异常
-}
-```
-
| 异常类型 | 场景 |
|---------|------|
| `DeviceNotFoundException` | 无可用相机 |
| `ConnectionLostException` | 相机物理断开 |
| `CameraException` | SDK 操作失败(基类) |
-| `InvalidOperationException` | 未连接时访问参数,未采集时触发 |
-| `TimeoutException` | 软件触发等待超时 |
## 扩展其他品牌相机
-1. 实现 `ICameraController` 接口:
-
-```csharp
-public class HikvisionCameraController : ICameraController
-{
- // 实现所有接口方法...
-}
-```
-
-2. 在 `CameraFactory.cs` 中注册:
-
-```csharp
-public ICameraController CreateController(string cameraType)
-{
- return cameraType switch
- {
- "Basler" => new BaslerCameraController(),
- "Hikvision" => new HikvisionCameraController(),
- _ => throw new NotSupportedException($"不支持的相机品牌: {cameraType}")
- };
-}
-```
-
-3. 配置文件切换品牌即可,业务代码无需修改。
+1. 在 `Basler/` 同级创建新文件夹,实现 `ICameraController` 接口
+2. 在 `CameraFactory.cs` 中注册新品牌
+3. 配置文件切换品牌即可,业务代码无需修改
## 线程安全
-- 所有公共方法(Open / Close / StartGrabbing / StopGrabbing / ExecuteSoftwareTrigger / 参数读写)均线程安全
+- 所有公共方法均线程安全
- 事件回调不持有内部锁,不会导致死锁
- `Open()` / `Close()` 幂等,重复调用安全
-
-## 日志
-
-使用 Serilog 静态 API(`Log.ForContext()`),与宿主应用共享同一个日志管道。宿主应用只需在启动时配置 `Log.Logger` 即可。
diff --git a/XP.Camera/XP.Camera.csproj b/XP.Camera/XP.Camera.csproj
index 2b53a87..29d059d 100644
--- a/XP.Camera/XP.Camera.csproj
+++ b/XP.Camera/XP.Camera.csproj
@@ -13,7 +13,24 @@
..\ExternalLibraries\Basler.Pylon.dll
+
+
+
+
+
+
+
+
+
+
+
+ PublicResXFileCodeGenerator
+ CalibrationResources.Designer.cs
+
+
+ CalibrationResources.resx
+