跳到主要内容使用 OpenCV 进行相机校准与畸变校正 | 极客日志C++AI算法
使用 OpenCV 进行相机校准与畸变校正
使用 OpenCV 进行相机校准的完整流程。内容涵盖相机畸变的理论基础,包括径向畸变和切向畸变的数学模型及修正公式。文章阐述了相机矩阵与内参的含义,并对比了棋盘格、ChArUco 及圆形图案等多种校准标的物的特点。核心部分提供了基于 C++ 的 OpenCV 代码实现,展示了如何读取配置、采集图像序列、检测特征点、执行 calibrateCamera 函数以及保存和加载校准参数。此外,还包含了图像去畸变的具体操作步骤、批量处理技巧以及实践中的关键注意事项,旨在帮助开发者建立准确的相机模型以应用于计算机视觉项目。
霸天1 浏览 相机已经存在了很长时间。然而,随着 20 世纪后期廉价针孔相机的推出,它们在我们的日常生活中变得司空见惯。不幸的是,这种廉价是有代价的:严重的失真。幸运的是,这些失真是常数,通过校准和一些重新映射,我们可以纠正这一点。此外,通过校准,您还可以确定相机的自然单位(像素)与现实世界单位(例如毫米)之间的关系。
理论
对于畸变,OpenCV 考虑了径向和切向因素。理解这些数学模型是进行准确校准的基础。
径向畸变
径向畸变是由于透镜形状引起的,光线在远离光轴的地方弯曲得更厉害。它通常表现为'桶形'或'枕形'畸变。OpenCV 使用以下多项式公式来描述径向畸变:
x_distorted = x * (1 + k1r^2 + k2r^4 + k3r^6)
y_distored = y * (1 + k1r^2 + k2r^4 + k3r^6)
其中 (x, y) 是归一化坐标,r^2 = x^2 + y^2,k1, k2, k3 是径向畸变系数。
切向畸变
切向畸变的发生是因为拍摄图像的镜头与成像平面不完全平行。这可以通过以下公式表示:
x_distorted = x + [2p1xy + p2(r^2 + 2x^2)]
y_distorted = y + [p1(r^2 + 2y^2) + 2p2xy]
因此,我们有五个失真参数,在 OpenCV 中,这些参数通常表示为具有 5 列的一行矩阵(或者根据模型不同为 8 列)。
相机矩阵与单位转换
为了将像素坐标转换为物理坐标,我们使用相机内参矩阵。公式如下:
u = fx * (X/Z) + cx
v = fy * (Y/Z) + cy
在这里,fx 和 fy 是相机焦距(以像素为单位),(cx, cy) 是以像素坐标表示的光学中心。如果对于两个轴,使用具有给定纵横比的公共焦距,则 fy = fx * a。包含这四个参数的矩阵称为相机矩阵。
虽然无论使用何种相机分辨率,失真系数都是相同的,但这些失真系数应与校准分辨率的当前分辨率一起缩放。确定这两个矩阵的过程就是校准。这些参数的计算是通过基本的几何方程完成的。
校准对象
使用的方程式取决于所选的校准对象。目前 OpenCV 支持三种类型的对象进行校准:
- 经典黑白棋盘格
- ChArUco 板图案
- 对称圆形图案
- 不对称圆形图案
基本上,您需要用相机拍摄这些模式的快照,并让 OpenCV 找到它们。每个找到的模式都会产生一个新方程。要求解方程,您至少需要预定数量的模式快照来形成一个适配方程组。这个数字对于棋盘模式来说较高,而对于圆盘模式来说,这个数字较小。例如,从理论上讲,棋盘模式至少需要两个快照。然而,在实践中,我们的输入图像中存在大量的噪点,因此为了获得良好的效果,您可能需要至少 10 张不同位置的输入模式的良好快照。
目标示例应用
- 确定失真矩阵
- 确定相机矩阵
- 从相机、视频和图像文件列表中获取输入
- 从 XML/YAML 文件读取配置
- 将结果保存到 XML/YAML 文件中
- 计算重投影误差
源代码实现逻辑
您可以在 OpenCV 源代码库的文件夹中找到参考源代码。对于程序的用法,请使用参数运行它。该程序有一个基本参数:其配置文件的名称。如果没有给出,那么它将尝试打开名为'default.xml'的那个。
在配置文件中,您可以选择使用相机作为输入、视频文件或图像列表。如果选择最后一个,则需要创建一个配置文件,在其中枚举要使用的图像。要记住的重要部分是,需要使用应用程序工作目录中的绝对路径或相对路径来指定图像。
配置读取
应用程序从配置文件中读取设置后启动。虽然这是其中的一个重要部分,但它与本教程的主题无关:相机校准。因此,我选择不在此处发布该部分的代码。有关如何执行此操作的技术背景,请参阅官方文档。
const string inputSettingsFile = parser.get(0);
FileStorage fs(inputSettingsFile, FileStorage::READ);
if (!fs.isOpened()) {
cout << "无法打开配置文件:" << inputSettingsFile << endl;
parser.printMessage();
return -1;
}
Settings s;
fs["设置"] >> s;
fs.release();
为此,我使用了简单的 OpenCV 类输入操作。读取文件后,我有一个额外的后处理功能来检查输入的有效性。只有当所有输入都良好时,goodInput 变量才会为 true。
主循环与校准流程
在此之后,我们有一个大循环,我们在其中执行以下操作:从图像列表、相机或视频文件中获取下一张图像。如果这失败了,或者我们有足够的图像,那么我们就会运行校准过程。在图像的情况下,我们跳出循环,否则,通过从检测模式更改为校准模式,剩余的帧将不会失真(如果设置了选项)。
for (;;) {
Mat view;
bool blinkOutput = false;
view = s.nextImage();
if (mode == CAPTURING && imagePoints.size() >= (size_t)s.nrFrames) {
if (runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints, grid_width, release_object))
mode = CALIBRATED;
else
mode = DETECTING;
}
if (view.empty()) {
if (mode != CALIBRATED && !imagePoints.empty())
runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints, grid_width, release_object);
break;
}
if (s.flip)
flip(view, view, 1);
vector<Point2f> pointBuf;
bool found = false;
int chessBoardFlags = 0;
if (!s.useFisheye) {
chessBoardFlags |= cv::CALIB_FIX_ASPECT_RATIO;
}
switch (s.calibrationPattern) {
case Settings::CHESSBOARD:
found = findChessboardCorners(view, s.boardSize, pointBuf, chessBoardFlags);
break;
case Settings::CHARUCOBOARD:
ch_detector.detectBoard(view, pointBuf, markerIds);
found = pointBuf.size() == (size_t)((s.boardSize.height - 1)*(s.boardSize.width - 1));
break;
case Settings::CIRCLES_GRID:
found = findCirclesGrid(view, s.boardSize, pointBuf);
break;
case Settings::ASYMMETRIC_CIRCLES_GRID:
found = findCirclesGrid(view, s.boardSize, pointBuf, cv::CIRCLES_GRID_ASYMMETRIC_SYMMETRY_AXIS);
break;
default:
found = false;
break;
}
if (found) {
if (s.calibrationPattern == Settings::CHESSBOARD) {
Mat viewGray;
cvtColor(view, viewGray, COLOR_BGR2GRAY);
cornerSubPix(viewGray, pointBuf, Size(winSize, winSize), Size(-1, -1), TermCriteria(TermCriteria::EPS+TermCriteria::COUNT, 30, 0.0001));
}
if (mode == CAPTURING && (!s.inputCapture.isOpened() || clock() - prevTimestamp > s.delay*1e-3*CLOCKS_PER_SEC)) {
imagePoints.push_back(pointBuf);
prevTimestamp = clock();
blinkOutput = s.inputCapture.isOpened();
}
if (s.calibrationPattern == Settings::CHARUCOBOARD)
drawChessboardCorners(view, Size(s.boardSize.width-1, s.boardSize.height-1), Mat(pointBuf), found);
else
drawChessboardCorners(view, s.boardSize, Mat(pointBuf), found);
}
}
显示状态与用户交互
此部分显示图像上的文本输出。我们显示当前捕获的帧数、总目标帧数以及校准状态。
string msg = (mode == CAPTURING) ? "Capturing..." :
(mode == CALIBRATED) ? "Calibrated" : "Press 'g' to start";
putText(view, msg, Point(view.cols - 200, 50), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 255, 0), 2);
char key = (char)(waitKey(30));
if (key == ESC_KEY) break;
if (key == 'u' && mode == CALIBRATED) s.showUndistorted = !s.showUndistorted;
if (s.inputCapture.isOpened() && key == 'g') {
mode = CAPTURING;
imagePoints.clear();
}
图像去畸变
如果我们运行校准并得到带有失真系数的相机矩阵,我们可能需要使用 undistort 函数校正图像。
if (mode == CALIBRATED && s.showUndistorted) {
Mat temp = view.clone();
if (s.useFisheye) {
Mat newCamMat;
fisheye::undistortImage(temp, view, cameraMatrix, distCoeffs, newCamMat);
} else {
undistort(temp, view, cameraMatrix, distCoeffs);
}
}
然后我们显示图像并等待输入键,如果是 u,我们切换失真消除,如果是 g,我们再次开始检测过程,最后对于 ESC 键,我们退出应用程序。
批量处理图像列表
使用图像列表时,无法消除循环内部的失真。因此,您必须在循环之后执行此操作。现在,我将利用这一点来扩展 cv::undistort 函数,该函数实际上首先调用 getOptimalNewCameraMatrix 来查找变换矩阵,然后使用 remap 函数执行变换。因为,在成功校准图计算后,只需执行一次,因此通过使用此扩展表单,您可以加快应用速度。
if (s.inputType == Settings::IMAGE_LIST && s.showUndistorted && !cameraMatrix.empty()) {
Mat view, rview, map1, map2;
if (s.useFisheye) {
Mat newCamMat;
fisheye::estimateNewCameraMatrixForUndistortRectify(cameraMatrix, distCoeffs, imageSize, Matx33d::eye(), newCamMat, 1);
fisheye::initUndistortRectifyMap(cameraMatrix, distCoeffs, Matx33d::eye(), newCamMat, imageSize, CV_16SC2, map1, map2);
} else {
getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, imageSize, &newCamMat);
initUndistortRectifyMap(cameraMatrix, distCoeffs, Mat(), newCamMat, imageSize, CV_16SC2, map1, map2);
}
for (size_t i = 0; i < s.imageList.size(); i++) {
view = imread(s.imageList[i]);
if (view.empty()) continue;
remap(view, rview, map1, map2, INTER_LINEAR);
imshow("Undistorted View", rview);
char c = (char)(waitKey(0));
if (c == ESC_KEY || c == 'q' || c == 'Q') break;
}
}
校准和保存
由于每台相机只需进行一次校准,因此在校准成功后保存校准是有意义的。这样,以后您就可以将这些值加载到程序中。因此,我们首先进行校准,如果校准成功,我们将结果保存到 OpenCV 样式的 XML 或 YAML 文件中,具体取决于您在配置文件中提供的扩展名。
bool runCalibrationAndSave(Settings& s, Size imageSize, Mat& cameraMatrix, Mat& distCoeffs,
vector<vector<Point2f>> imagePoints, float grid_width, bool release_object)
{
vector<Vec3f> rvecs, tvecs;
vector<double> reprojErrs;
double totalAvgErr = 0;
vector<vector<Point3f>> newObjPoints;
bool ok = calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, reprojErrs, totalAvgErr, newObjPoints, grid_width, release_object);
cout << (ok ? "Calibration succeeds" : "Calibration fails")
<< ". avg re projection error = " << totalAvgErr << endl;
if (ok)
saveCameraParams(s, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, reprojErrs, imagePoints, totalAvgErr, newObjPoints);
return ok;
}
我们在 calibrateCamera 函数的帮助下进行校准。它具有以下参数:
-
objectPoints: 指向 Point3f 向量的向量,对于每个输入图像,它描述了图案的外观。如果我们有一个平面图案(如棋盘),那么我们可以简单地将所有 Z 坐标设置为零。这是存在这些重要点的点的集合。因为,我们对所有输入图像使用单一模式,因此我们只需计算一次,然后将其乘以所有其他输入视图。我们使用 calcBoardCornerPositions 函数计算角点。
-
imagePoints: 这是 Point2f 向量的向量,对于每个输入图像,它包含重要点的坐标(棋盘的角和圆图案的圆心)。我们已经从 findChessboardCorners 或 findCirclesGrid 函数中收集了它。
-
imageSize: 从相机、视频文件或图像获取的图像的大小。
-
iFixedPoint: 要固定的对象点的索引。我们将其设置为 -1 以请求标准校准方法。
-
cameraMatrix: 相机矩阵。如果我们使用固定纵横比选项,我们需要设置 fx/fy。
-
distCoeffs: 失真系数矩阵。使用零初始化。
-
flags: 最后一个参数是标志。您需要在此处指定选项,例如固定焦距的纵横比、假设切向失真为零或固定主点。在这里,我们使用 CALIB_USE_LU 来获得更快的校准速度。
该函数返回平均重新投影误差。这个数字可以很好地估计所找到参数的精度。这应该尽可能接近于零。给定固有矩阵、失真矩阵、旋转矩阵和平移矩阵,我们可以通过使用 projectPoints 首先将对象点转换为图像点来计算一个视图的误差。然后,我们计算变换得到的绝对范数与角/圆查找算法之间的绝对范数。为了找到平均误差,我们计算了所有校准图像计算的误差的算术平均值。
实践建议与注意事项
- 打印质量:确保校准板打印清晰,边缘锐利。使用高质量纸张,避免拉伸变形。
- 光照条件:保持光照均匀,避免过曝或阴影遮挡特征点。
- 多角度采集:确保校准板在不同角度、距离和位置下被拍摄,覆盖整个视场。
- 重复性:同一场景多拍几张,取平均值以减少随机误差。
- 验证:校准完成后,务必使用未参与校准的图像进行验证,观察重投影误差是否可接受(通常小于 0.5 像素)。
通过遵循上述步骤和代码逻辑,您可以有效地完成相机的标定工作,为后续的三维重建、测量或增强现实应用打下坚实基础。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- RSA密钥对生成器
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
- Mermaid 预览与可视化编辑
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online