OpenCV 相机标定实战详解:畸变模型、calibrateCameraRO 全流程与标定结果解析
本篇指南围绕 OpenCV 的相机标定(camera calibration)展开:从径向/切向畸变的数学模型讲起,完整走读 OpenCV 官方示例程序的配置文件、主循环与核心标定调用 cv::calibrateCameraRO,并结合仓库内的真实标定输出文件,帮你掌握“采集标定图 → 检测角点 → 求解内参与畸变系数 → 保存并去畸变”的完整工作流。读完本文,你可以独立完成一次相机标定,正确解读输出文件中的相机矩阵、畸变系数与重投影误差,并将结果用于后续的去畸变与像素到真实世界坐标的换算。适用环境为 OpenCV >= 4.0(对应仓库当前版本线)。
1. 标定理论基础:两种畸变与相机矩阵
廉价针孔相机的普及以显著的画面畸变为代价。好消息是,畸变参数是常量——通过标定求出这些常量后,配合一次重映射(remap)即可校正畸变;同时标定还能建立相机固有单位(像素)与真实世界单位(例如毫米)之间的换算关系,这是三维视觉应用的前提。
1.1 径向畸变(Radial Distortion)
OpenCV 的针孔模型同时考虑径向与切向两种畸变。径向畸变公式为:
即未畸变像素点 在畸变图像上的位置为 。径向畸变在画面上的直观表现就是“桶形”(barrel)或“鱼眼”(fish-eye)效果。
1.2 切向畸变(Tangential Distortion)
切向畸变源于取像镜头与成像平面不完全平行,其公式为:
综合两种畸变,共有 5 个畸变参数,在 OpenCV 中以一个 5 列(或 5×1)的行向量表示:
1.3 相机矩阵:像素与世界单位的换算
单位换算采用如下投影公式:
其中 的出现是因为采用了齐次坐标系(且 )。待求参数为 、(相机焦距)与 (以像素表示的光心,即主点)。若两个轴共用一个焦距并给定纵横比 (通常为 1),则 ,公式中只剩单个焦距 。包含这四个参数的矩阵即相机矩阵(camera matrix)。
一个容易被忽略的细节:畸变系数与分辨率无关——同一相机在不同分辨率下畸变系数不变;而相机矩阵参数需要根据当前分辨率从标定时的分辨率进行等比缩放。
1.4 支持的标定物与图像数量要求
求解上述两个矩阵的过程就是标定,其计算基于基础几何方程,方程形式取决于所选标定物。当前 OpenCV 支持以下四类标定图案:
- 经典黑白棋盘格(chessboard)
- ChArUco 棋盘格(棋盘格上贴 ArUco 标记,角点由标记匹配得到)
- 对称圆点阵列(symmetrical circles grid)
- 非对称圆点阵列(asymmetrical circles grid)
实际操作是用相机拍摄多张标定图案图像,让 OpenCV 在其中检测图案,每成功检测到一次图案就得到一组新方程。要解出方程组,需要不少于某个阈值的快照才能构成适定问题——棋盘格要求的张数多于圆点图案。理论上棋盘格最少需要 2 张快照,但实际输入图像存在大量噪声,为获得良好结果,实践中建议至少采集 10 张、姿态各异的合格快照。
2. 示例程序:能力、位置与命令行接口
官方示例程序(camera_calibration)具备以下能力:
- 求解畸变矩阵
- 求解相机矩阵
- 支持相机(camera)、视频文件、图像列表三种输入
- 从 XML/YAML 配置文件读取设置
- 将标定结果保存为 XML/YAML 文件
- 计算重投影误差(re-projection error)
源码位于 camera_calibration.cpp,同目录下还有配套文件:
- 示例配置文件:in_VID5.xml
- 图像列表文件:VID5.xml
- 真实标定输出样例:out_camera_data.yml
程序只有一个必需参数——配置文件名;不传时默认尝试打开 default.xml。用 -h 可查看完整用法说明。从 camera_calibration.cpp#L264-L269 的命令行定义可以看到全部参数:
const String keys
= "{help h usage ? | | print this message }"
"{@settings |default.xml| input setting file }"
"{d | | actual distance between top-left and top-right corners of "
"the calibration grid }"
"{winSize | 11 | Half of search window for cornerSubPix }";
@settings:配置文件路径,默认default.xml;-d=<number>:标定网格左上角与右上角角点之间的实测距离,用于标定板不精确时的补偿标定(见第 5 节);--winSize=<number>:cornerSubPix亚像素精化的搜索窗口半长,默认 11。
2.1 第一步:读取设置
程序启动后先从配置文件读入设置(使用 OpenCV 的 FileStorage 输入操作),随后执行 Settings::validate() 做合法性校验,只有全部输入合法时 goodInput 才为 true:
//! [file_read]
Settings s;
const string inputSettingsFile = parser.get<string>(0);
FileStorage fs(inputSettingsFile, FileStorage::READ); // Read the settings
if (!fs.isOpened())
{
cout << "Could not open the configuration file: \"" << inputSettingsFile << "\"" << endl;
parser.printMessage();
return -1;
}
fs["Settings"] >> s;
fs.release(); // close Settings file
//! [file_read]
文件读写机制的详细背景可参考仓库文档 XML/YAML 文件输入输出教程(@ref tutorial_file_input_output_with_xml_yml 指向的教程)。
3. 配置文件详解:所有标定参数一览
配置文件的完整字段可从 in_VID5.xml 逐条对照,Settings 类的 read/validate 实现 则给出每项的解析与校验逻辑:
| 配置项 | 类型 | 说明 |
|---|---|---|
BoardSize_Width / BoardSize_Height |
int | 标定板尺寸。注意含义随图案类型变化:棋盘格是内角点行列数;圆点阵列是圆点行列数;ChArUco 以方格数定义 |
Square_Size |
float | 方格边长(任意自定义单位,如毫米),必须大于 10e-6 |
Marker_Size |
float | ChArUco 板中标记尺寸,用于写入输出文件 |
Calibrate_Pattern |
string | CHESSBOARD / CHARUCOBOARD / CIRCLES_GRID / ASYMMETRIC_CIRCLES_GRID 四选一 |
ArUco_Dict_Name |
string | ChArUco 使用的 ArUco 预定义字典名,如 DICT_4X4_50、DICT_ARUCO_ORIGINAL、DICT_APRILTAG_36h10 等 |
ArUco_Dict_File_Name |
string | 自定义 ArUco 字典文件路径;留空则使用 ArUco_Dict_Name 指定的预定义字典 |
Calibrate_NrOfFrameToUse |
int | 参与标定的帧数上限 |
Calibrate_FixAspectRatio |
0/1 | 固定 fx/fy 比例(CALIB_FIX_ASPECT_RATIO),非零值作为比例值 |
Calibrate_AssumeZeroTangentialDistortion |
0/1 | 假设切向畸变为零(CALIB_ZERO_TANGENT_DIST) |
Calibrate_FixPrincipalPointAtTheCenter |
0/1 | 全局优化中固定主点(CALIB_FIX_PRINCIPAL_POINT) |
Fix_K1 ~ Fix_K5 |
0/1 | 将对应径向畸变系数固定为零(CALIB_FIX_K1…) |
Calibrate_UseFisheyeModel |
0/1 | 启用鱼眼模型(cv::fisheye::calibrate) |
Input |
string | 输入源:相机 ID(如 "1")、视频路径或图像列表 XML/YAML 路径 |
Input_FlipAroundHorizontalAxis |
0/1 | 沿水平轴翻转输入图像 |
Input_Delay |
int | 相机/视频输入时帧间延迟(毫秒) |
Write_outputFileName |
string | 输出文件路径,扩展名决定写 XML 还是 YAML |
Write_DetectedFeaturePoints |
0/1 | 是否把检测到的特征点写入输出文件 |
Write_extrinsicParameters |
0/1 | 是否写入每帧外参(rvec + tvec) |
Write_gridPoints |
0/1 | 是否写入细化后的 3D 靶标网格点 |
Show_UndistortedImage |
0/1 | 标定后是否展示去畸变图像 |
示例配置 的核心片段:
<opencv_storage>
<Settings>
<!-- 内角点数(棋盘格) -->
<BoardSize_Width>9</BoardSize_Width>
<BoardSize_Height>6</BoardSize_Height>
<!-- 方格边长(自定义度量单位) -->
<Square_Size>50</Square_Size>
<Calibrate_Pattern>"CHESSBOARD"</Calibrate_Pattern>
<Input>"images/CameraCalibration/VID5/VID5.xml"</Input>
<Input_FlipAroundHorizontalAxis>0</Input_FlipAroundHorizontalAxis>
<Input_Delay>100</Input_Delay>
<Calibrate_NrOfFrameToUse>25</Calibrate_NrOfFrameToUse>
<Calibrate_FixAspectRatio> 1 </Calibrate_FixAspectRatio>
<Calibrate_AssumeZeroTangentialDistortion>1</Calibrate_AssumeZeroTangentialDistortion>
<Calibrate_FixPrincipalPointAtTheCenter> 1 </Calibrate_FixPrincipalPointAtTheCenter>
<Write_outputFileName>"out_camera_data.xml"</Write_outputFileName>
<Show_UndistortedImage>1</Show_UndistortedImage>
<Calibrate_UseFisheyeModel>0</Calibrate_UseFisheyeModel>
</Settings>
</opencv_storage>
3.1 三种输入类型与校验逻辑
Settings::validate() 的解析规则值得注意(camera_calibration.cpp#L104-L135):
Input以数字开头 → 视为相机 ID,调用VideoCapture::open(cameraID);Input是.xml/.yaml/.yml扩展名且能被readStringList解析成字符串序列 → 视为图像列表,并自动把nrFrames截断为不超过列表长度;- 其余情况视为视频文件路径。
同时,四个布尔开关被装配成标定 flags(camera_calibration.cpp#L137-L155):
flag = 0;
if(calibFixPrincipalPoint) flag |= CALIB_FIX_PRINCIPAL_POINT;
if(calibZeroTangentDist) flag |= CALIB_ZERO_TANGENT_DIST;
if(aspectRatio) flag |= CALIB_FIX_ASPECT_RATIO;
if(fixK1) flag |= CALIB_FIX_K1;
// … fixK2 ~ fixK5 同理
if (useFisheye) {
// 鱼眼模型使用独立的枚举,直接覆写 flags
flag = CALIB_FIX_SKEW | CALIB_RECOMPUTE_EXTRINSIC;
// 再按需叠加 CALIB_FIX_K1~K4、CALIB_FIX_PRINCIPAL_POINT
}
3.2 图像列表配置文件
选择图像列表输入时,需要单独建一个枚举图像的配置文件。关键约束:图像路径必须使用绝对路径,或相对于程序工作目录的相对路径。 教程原文给出的 9×6 棋盘格示例(8 张图):
<?xml version="1.0"?>
<opencv_storage>
<images>
images/CameraCalibration/VID5/xx1.jpg
images/CameraCalibration/VID5/xx2.jpg
images/CameraCalibration/VID5/xx3.jpg
images/CameraCalibration/VID5/xx4.jpg
images/CameraCalibration/VID5/xx5.jpg
images/CameraCalibration/VID5/xx6.jpg
images/CameraCalibration/VID5/xx7.jpg
images/CameraCalibration/VID5/xx8.jpg
</images>
</opencv_storage>
仓库内可直接参考 VID5.xml。标定物图案的生成方法见配套教程 相机标定图案生成。
4. 主循环逐段走读
4.1 输入循环与状态机
主循环维护三态:DETECTION = 0(检测模式)、CAPTURING = 1(采集模式)、CALIBRATED = 2(已标定)。图像列表输入直接以 CAPTURING 起步,相机输入则从 DETECTION 起步(camera_calibration.cpp#L257、#L369):
//! [get_input]
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 = DETECTION;
}
if(view.empty()) // 图像耗尽:若尚未达标但已有采集,则就标定一次
{
if( mode != CALIBRATED && !imagePoints.empty() )
runCalibrationAndSave(s, imageSize, cameraMatrix, distCoeffs, imagePoints, grid_width,
release_object);
break;
}
//! [get_input]
若需要翻转输入图像,在取帧后立即 flip(view, view, 0) 处理。
4.2 图案检测:四种图案分派
//! [find_pattern]
vector<Point2f> pointBuf;
bool found;
int chessBoardFlags = CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE;
if(!s.useFisheye)
chessBoardFlags |= CALIB_CB_FAST_CHECK; // 高畸变(如鱼眼)下快速检测会误判,故排除
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, CALIB_CB_ASYMMETRIC_GRID );
break;
default:
found = false;
}
//! [find_pattern]
(对应 camera_calibration.cpp#L404-L435。)cv::findChessboardCorners 与 cv::findCirclesGrid 返回布尔值表示是否找到图案,只有返回 true 的图像才计入标定;而 CharucoDetector::detectBoard 允许检测到部分可见的图案,返回可见内角点的坐标与 id。
关于 boardSize 的语义差异(原文特别强调的易错点):
- 所有棋盘格相关算法期望的是内角点数(width/height 均为内角点数);
- 圆点阵列的 boardSize 就是两个方向上的圆点总数;
- ChArUco 板尺寸以方格数定义,而检测结果是一组内角点,因此检测点数在两个方向上都比方格数小 1。
检测到的点会写入 pointBuf 向量,作为方程组的图像侧数据。
4.3 亚像素精化与采集节流
方格图像上的角点位置只是近似值,调用 cv::cornerSubPix 可显著提升坐标精度,从而获得更好的标定结果(camera_calibration.cpp#L441-L447):
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 ));
}
其中 winSize 即命令行 --winSize=<number> 传入值(默认 11),控制搜索窗口边长。
对实时相机输入,只有距离上次采集经过了 Input_Delay 时才真正入库(camera_calibration.cpp#L449-L455)。原因值得记住:相似图像产生相似方程,相似方程过多会使标定成为不适定问题并导致失败,延迟采集是为了让用户有时间移动棋盘、获得姿态多样的样本。最后,检测到的点会通过 cv::drawChessboardCorners 绘制到输入图上,作为可视化反馈。
4.4 状态显示与键盘控制
屏幕上实时显示当前状态文本(采集进度 n/N、Calibrated 等);标定完成后若开启 Show_UndistortedImage,则用 cv::undistort 实时校正显示(鱼眼模型走 cv::fisheye::undistortImage 路径,并先用 fisheye::estimateNewCameraMatrixForUndistortRectify 计算新相机矩阵):
//! [output_undistorted]
if( mode == CALIBRATED && s.showUndistorted )
{
Mat temp = view.clone();
if (s.useFisheye)
{
Mat newCamMat;
fisheye::estimateNewCameraMatrixForUndistortRectify(cameraMatrix, distCoeffs, imageSize,
Matx33d::eye(), newCamMat, 1);
cv::fisheye::undistortImage(temp, view, cameraMatrix, distCoeffs, newCamMat);
}
else
undistort(temp, view, cameraMatrix, distCoeffs);
}
//! [output_undistorted]
随后 imshow 显示并等待按键(camera_calibration.cpp#L502-L517):
u:切换是否显示去畸变结果(仅在CALIBRATED状态生效);g:清空imagePoints并回到CAPTURING,重新开始采集;ESC:退出程序。
4.5 图像列表的批量去畸变
对图像列表输入,无法在循环内逐帧去畸变,因此放在主循环结束后批量处理(camera_calibration.cpp#L522-L553)。这里用展开形式替代 cv::undistort:cv::undistort 内部其实是先调用 cv::initUndistortRectifyMap 计算变换映射表、再调用 cv::remap 执行重映射。由于成功标定后映射表只需计算一次,展开成两步可以对整批图像复用 map1/map2,从而加速:
//! [show_results]
if( s.inputType == Settings::IMAGE_LIST && s.showUndistorted && !cameraMatrix.empty())
{
Mat view, rview, map1, map2;
initUndistortRectifyMap(
cameraMatrix, distCoeffs, Mat(),
getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, 1, imageSize, 0), imageSize,
CV_16SC2, map1, map2);
for(size_t i = 0; i < s.imageList.size(); i++ )
{
view = imread(s.imageList[i], IMREAD_COLOR);
if(view.empty())
continue;
remap(view, rview, map1, map2, INTER_LINEAR);
imshow("Image View", rview);
char c = (char)waitKey();
if( c == ESC_KEY || c == 'q' || c == 'Q' )
break;
}
}
//! [show_results]
5. 标定核心:calibrateCameraRO 的参数与对象点构造
标定每部相机只需做一次,成功后保存结果、供后续程序直接加载。示例把“标定”与“保存”拆为两个过程:先调用 runCalibrationAndSave,成功后将参数写入 OpenCV 风格 XML 或 YAML 文件(扩展名由配置文件 Write_outputFileName 决定):
//! [run_and_save]
bool runCalibrationAndSave(Settings& s, Size imageSize, Mat& cameraMatrix, Mat& distCoeffs,
vector<vector<Point2f> > imagePoints, float grid_width, bool release_object)
{
vector<Mat> rvecs, tvecs;
vector<float> reprojErrs;
double totalAvgErr = 0;
vector<Point3f> newObjPoints;
bool ok = runCalibration(s, imageSize, cameraMatrix, distCoeffs, imagePoints, rvecs, tvecs, reprojErrs,
totalAvgErr, newObjPoints, grid_width, release_object);
cout << (ok ? "Calibration succeeded" : "Calibration failed")
<< ". avg re projection error = " << totalAvgErr << endl;
if (ok)
saveCameraParams(s, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, reprojErrs, imagePoints,
totalAvgErr, newObjPoints);
return ok;
}
//! [run_and_save]
实际求解由 cv::calibrateCameraRO 完成(标准路径),鱼眼走 cv::fisheye::calibrate。其参数含义(对照 camera_calibration.cpp#L628-L702):
- 对象点(object points):
Point3f向量的向量,每张输入图像对应一组,描述标定图案在世界坐标中的理想布局。平面图案(如棋盘格)所有 Z 坐标设为 0。由于所有图像共用同一图案,只需计算一次再复制。示例中用calcBoardCornerPositions生成角点(camera_calibration.cpp#L594-L626):
//! [board_corners]
static void calcBoardCornerPositions(Size boardSize, float squareSize, vector<Point3f>& corners,
Settings::Pattern patternType)
{
corners.clear();
switch(patternType)
{
case Settings::CHESSBOARD:
case Settings::CIRCLES_GRID:
for (int i = 0; i < boardSize.height; ++i)
for (int j = 0; j < boardSize.width; ++j)
corners.push_back(Point3f(j*squareSize, i*squareSize, 0));
break;
case Settings::CHARUCOBOARD:
for (int i = 0; i < boardSize.height - 1; ++i)
for (int j = 0; j < boardSize.width - 1; ++j)
corners.push_back(Point3f(j*squareSize, i*squareSize, 0));
break;
case Settings::ASYMMETRIC_CIRCLES_GRID:
for (int i = 0; i < boardSize.height; i++)
for (int j = 0; j < boardSize.width; j++)
corners.push_back(Point3f((2 * j + i % 2)*squareSize, i*squareSize, 0));
break;
}
}
//! [board_corners]
注意非对称圆点阵列 x 坐标公式 `(2*j + i%2)*squareSize` 正是“奇偶行错位一半间距”的几何特征。然后复制给每帧:
vector<vector<Point3f> > objectPoints(1);
calcBoardCornerPositions(s.boardSize, s.squareSize, objectPoints[0], s.calibrationPattern);
objectPoints[0][s.boardSize.width - 1].x = objectPoints[0][0].x + grid_width;
newObjPoints = objectPoints[0];
objectPoints.resize(imagePoints.size(), objectPoints[0]);
**标定板不精确时的修正**:纸质打印的棋盘格通常精度不足,此时可传入命令行 `-d=<number>` 启用修正方法(源自 Strobl 等人的 ICCV 2011 工作):`-d` 的值是**用尺子或游标卡尺精确测量**的网格左上角 (0,0,0) 到右上角 `s.squareSize*(s.boardSize.width-1), 0, 0` 两点之间的真实距离(即上式中 `grid_width`),用于覆盖理想网格宽;标定完成后 `newObjPoints` 会被更新为细化后的对象点 3D 坐标(配合 `Write_gridPoints` 可写入输出文件)。
-
图像点(image points):
Point2f向量的向量,每张图像对应一组检测到的角点/圆心坐标,即前面第 4 节中imagePoints收集的内容。 -
图像尺寸:取自相机、视频文件或图像的
Size。 -
固定点索引
iFixedPoint:设为 -1 表示请求标准标定方法;若启用“对象释放”修正方法(即传了-d),则设为标定板网格右上角点的索引:
int iFixedPoint = -1;
if (release_object)
iFixedPoint = s.boardSize.width - 1;
- 相机矩阵(输出):初始化为 3×3 单位阵;若使用固定纵横比选项,需要预设 :
//! [fixed_aspect]
cameraMatrix = Mat::eye(3, 3, CV_64F);
if( !s.useFisheye && s.flag & CALIB_FIX_ASPECT_RATIO )
cameraMatrix.at<double>(0,0) = s.aspectRatio;
//! [fixed_aspect]
- 畸变系数矩阵(输出):初始化为零。标准模型分配 8×1(容纳 k1~k5 + p1,p2),鱼眼模型分配 4×1:
if (s.useFisheye)
distCoeffs = Mat::zeros(4, 1, CV_64F);
else
distCoeffs = Mat::zeros(8, 1, CV_64F);
-
旋转/平移向量(输出):对每个视角,函数计算出把对象点(模型坐标系)变换到图像点(相机/世界坐标系)的旋转与平移向量;第 7、8 个参数即承载这两组向量的输出容器,第 i 个位置对应第 i 组对象点到第 i 组图像点的变换。
-
更新后的对象点(输出):标准标定方法下该参数被忽略;启用
-d修正时则输出细化后的 3D 网格点。 -
flags:标定标志位,如固定焦距纵横比、假设零切向畸变、固定主点、固定部分畸变系数等。示例额外叠加
CALIB_USE_LU以获得更快的标定速度。完整调用:
rms = calibrateCameraRO(objectPoints, imagePoints, imageSize, iFixedPoint,
cameraMatrix, distCoeffs, rvecs, tvecs, newObjPoints,
s.flag | CALIB_USE_LU);
cv::calibrateCameraRO 的接口声明位于 calib.hpp#L765-L778,实现位于 calibration.cpp#L2899,需要深入了解 LU 分解路径与固定点处理时可直接阅读该实现。
5.1 重投影误差
函数返回值 rms 是平均重投影误差,是衡量所求参数精度的良好估计,应尽可能接近 0。其计算方式:给定内参、畸变、旋转与平移参数后,对单个视角先用 cv::projectPoints 把对象点投影到图像平面,再计算投影结果与角点/圆点检测算法所得坐标之间的绝对范数;对全部标定图像求算术平均即得平均误差。示例的完整实现(camera_calibration.cpp#L560-L591)同时输出每视图误差:
//! [compute_errors]
static double computeReprojectionErrors( const vector<vector<Point3f> >& objectPoints,
const vector<vector<Point2f> >& imagePoints,
const vector<Mat>& rvecs, const vector<Mat>& tvecs,
const Mat& cameraMatrix, const Mat& distCoeffs,
vector<float>& perViewErrors, bool fisheye)
{
vector<Point2f> imagePoints2;
size_t totalPoints = 0;
double totalErr = 0, err;
perViewErrors.resize(objectPoints.size());
for(size_t i = 0; i < objectPoints.size(); ++i )
{
if (fisheye)
fisheye::projectPoints(objectPoints[i], imagePoints2, rvecs[i], tvecs[i],
cameraMatrix, distCoeffs);
else
projectPoints(objectPoints[i], rvecs[i], tvecs[i], cameraMatrix, distCoeffs, imagePoints2);
err = norm(imagePoints[i], imagePoints2, NORM_L2);
size_t n = objectPoints[i].size();
perViewErrors[i] = (float) std::sqrt(err*err/n);
totalErr += err*err;
totalPoints += n;
}
return std::sqrt(totalErr/totalPoints);
}
//! [compute_errors]
6. 结果保存:输出文件字段与真实样例
saveCameraParams(camera_calibration.cpp#L705-L821)按配置写入输出文件,字段包括:标定时间 calibration_time、使用帧数 nr_of_frames、图像宽高、棋盘宽高、方格/标记尺寸、固定纵横比值、flags 数值及可读注释(如 +fix_aspectRatio +zero_tangent_dist)、fisheye_model 标记、camera_matrix、distortion_coefficients、avg_reprojection_error,以及可选的 per_view_reprojection_errors、外参(每视图旋转+平移的 6 元组矩阵 extrinsic_parameters)、image_points、grid_points。
仓库中保留了一份真实输出样例 out_camera_data.yml,对应一次 8 帧、640×480、9×6 棋盘格(方格 50)的标定:
%YAML:1.0
calibration_Time: "08/19/11 20:44:38"
nrOfFrames: 8
image_Width: 640
image_Height: 480
board_Width: 9
board_Height: 6
square_Size: 50.
# flags: +fix_aspectRatio +fix_principal_point +zero_tangent_dist
flagValue: 14
Camera_Matrix: !!opencv-matrix
rows: 3
cols: 3
dt: d
data: [ 6.5746697810243404e+002, 0., 3.1950000000000000e+002, 0.,
6.5746697810243404e+002, 2.3950000000000000e+002, 0., 0., 1. ]
Distortion_Coefficients: !!opencv-matrix
rows: 5
cols: 1
dt: d
data: [ -4.1802327018241026e-001, 5.0715243805833121e-001, 0., 0.,
-5.7843596847939704e-001 ]
Avg_Reprojection_Error: 3.8441346462381665e-001
对应的 XML 形式输出即教程原文给出的结果片段(k4/k5 被 Fix_K4/Fix_K5 固定为 0,故切向畸变 p1=p2=0 由 AssumeZeroTangentialDistortion 生效):
<camera_matrix type_id="opencv-matrix">
<rows>3</rows>
<cols>3</cols>
<dt>d</dt>
<data>
6.5746697944293521e+002 0. 3.1950000000000000e+002 0.
6.5746697944293521e+002 2.3950000000000000e+002 0. 0. 1.</data></camera_matrix>
<distortion_coefficients type_id="opencv-matrix">
<rows>5</rows>
<cols>1</cols>
<dt>d</dt>
<data>
-4.1802327176423804e-001 5.0715244063187526e-001 0. 0.
-5.7843597214487474e-001</data></distortion_coefficients>
拿到这两个矩阵后,把它们作为常量写入你的程序,调用 cv::initUndistortRectifyMap 与 cv::remap 即可持续去除廉价低质量相机的畸变,获得无畸变输入。
7. 实战案例:棋盘格与非对称圆点阵列
案例一:9×6 棋盘格 + 图像列表。 使用 AXIS IP 相机对棋盘格拍摄多组快照并保存,配合第 3 节的图像列表配置文件运行程序。下图分别是运行时检测到的棋盘格角点(左,红色连线与角点标注),以及开启去畸变后的效果(右):
案例二:4×11 非对称圆点阵列 + 实时相机。 将输入宽度设为 4、高度设为 11,输入源指定为相机 ID("1"),下图为检测成功的图案效果(红圈标注圆心,绿色为连线区域):
两种情况下,指定的输出 XML/YAML 文件中都会包含相机矩阵与畸变系数矩阵,即可按第 6 节的方式用于后续去畸变。
8. 小结
- 标定求解的是相机矩阵()与畸变系数()两个矩阵,其理论模型在第 1 节给出;
- 配置文件是唯一入口,所有标定行为(图案类型、约束 flags、输入源、输出项)由 in_VID5.xml 所示字段驱动,
Settings::validate()负责校验与 flag 装配; - 主循环是“取帧 → 检测图案 → 亚像素精化 → 入库 → 满帧/耗尽触发标定”的状态机,
cv::cornerSubPix的winSize与采集延迟直接影响采样质量; - 核心调用
cv::calibrateCameraRO支持标准标定与-d参数触发的标定板不精确修正两种模式,返回平均重投影误差作为精度指标; - 标定一次、长期复用:结果写入 XML/YAML 后,用
cv::initUndistortRectifyMap+cv::remap(映射表计算一次、重映射多次)即为线上推理提供去畸变图像。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


