Files
XplorePlane/XP.Camera/Calibration/ChessboardCalibrator.cs
T

222 lines
7.9 KiB
C#

// ============================================================================
// 文件名: 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<double>();
public double ReprojectionError { get; set; }
public DateTime CalibrationTime { get; set; }
}
private Mat? _cameraMatrix;
private Mat? _distCoeffs;
private double _reprojectionError;
private List<double> _perImageErrors = new List<double>();
public double ReprojectionError => _reprojectionError;
public List<double> PerImageErrors => _perImageErrors;
// 进度报告委托
public delegate void ProgressReportHandler(int current, int total, string message);
public event ProgressReportHandler? ProgressChanged;
public bool CalibrateFromImages(List<string> 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<CalibrationResult>(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;
}
}
}