OpenCV 方格棋盘相机标定与位姿估计实战指南(calibration + solvePnP)
导读:本文以 OpenCV 官方教程"Camera calibration with square chessboard"为骨架,围绕方格棋盘展开完整讲解:从准备标定图集、生成图像清单、运行
calibration样例获取相机内参与畸变系数,到编写代码用findChessboardCorners检测棋盘、用solvePnP求解棋盘相对相机的位姿(旋转与平移),并最终计算任意棋盘角点到相机的距离。读者学完后,将能独立完成"离线标定 + 单张图像位姿求解"的标准流程,并可将其推广到任意已知三维几何的物体上。
适用范围:本仓库对应的 OpenCV 版本要求
OpenCV >= 4.0(见 camera_calibration_square_chess.markdown 的兼容性声明);原文作者为 Victor Eruhimov。
一、任务全貌:从标定到位姿估计
本教程对应源码目录 doc/tutorials/calib3d/camera_calibration_square_chess/,属于相机标定(calib3d)系列教程之一(该系列还包含 camera_calibration_pattern 方格/圆点标定板图生成、camera_calibration 通用标定实战、real_time_pose 实时位姿估计等主题,可通过 table_of_content_calib3d.markdown 查看完整目录)。
整个工作流分两大阶段:
- 相机标定阶段:输入一组拍摄自不同角度/位姿的棋盘图像,输出相机内参矩阵(focal length、主点)、畸变系数以及用于质量评估的重投影误差。
- 位姿估计阶段:对一张新图像检测棋盘角点,结合已知的棋盘 3D 几何与上一阶段标定出的内参,用 PnP(Perspective-n-Point)求解棋盘坐标系相对相机坐标系的旋转向量
rvec与平移向量tvec,从而获得棋盘距离、姿态等度量信息。
该方法的核心思想可以推广:任何具有已知三维几何结构的刚体,只要能在图像中稳定检测到对应点,都可以套用同样的"物方点 + 像方点 + 内参 → solvePnP"流程来求位姿。
二、准备测试数据:编译样例并获取棋盘图像
2.1 启用样例编译
OpenCV 的样例代码(如 calibration、imagelist_creator)默认不随库一起编译。需要在 CMake 配置阶段打开开关:
cmake -DBUILD_EXAMPLES=ON ..
编译完成后,可执行程序会输出到构建目录的 bin 子目录(即教程中提到的 "Go to bin folder")。相关样例源码位于仓库 samples/cpp 下,其中本文涉及两个关键程序:
- samples/cpp/imagelist_creator.cpp:从命令行参数生成 XML/YAML 图像路径清单;
- samples/cpp/calibration.cpp:棋盘/圆点/ChArUco 等标定板的内参标定程序。
2.2 采集棋盘图像
原教程要求将测试图像放入 data/chess 目录。实践上,建议准备 10~20 张棋盘出现在画面不同位置、不同角度、不同尺度的照片,并且让棋盘在每张图中完整可见。光照不均时,选择亚光、平整的棋盘可显著降低误检率。仓库 samples/data 中提供了 chessboard.png 这类可用于快速验证角点检测的单张棋盘样例(见上图的示例棋盘,其实际物理方格边长以你打印时的尺寸为准)。
三、用 imagelist_creator 生成图像清单
calibration 程序不直接接收一堆图片路径,而是接收一个 XML/YAML 图像清单文件(内部是一个字符串序列)。生成方式有两种:
方式一:使用 imagelist_creator 工具(推荐,可批量生成)。假设所有棋盘图都是当前目录下的 *.png,则:
./imagelist_creator image_list.xml *.png
该工具的源码逻辑(samples/cpp/imagelist_creator.cpp)非常直观:先检测输出参数是否误指向一张已有图片(防止覆盖你的原始图片),然后用 FileStorage 以写模式打开输出文件,把所有命令行参数写入名为 images 的序列节点:
FileStorage fs(outputname, FileStorage::WRITE);
fs << "images" << "[";
for(int i = 2; i < ac; i++){
fs << string(av[i]);
}
fs << "]";
生成的 XML 文件内容形如:
<?xml version="1.0"?>
<opencv_storage>
<images>
view000.png
view001.png
view003.png
view010.png
one_extra_view.jpg
</images>
</opencv_storage>
方式二:手工编写清单文件。直接按上面的 XML 结构手写即可(YAML 亦可)。注意:calibration.cpp 在读取清单时会用 samples::findFile 对每个文件名做存在性解析(见 samples/cpp/calibration.cpp),若图片路径不在当前目录,请在清单中写入相对清单文件的路径。
四、运行 calibration 样例获取相机参数
4.1 命令行调用
标定程序支持静态图集输入、视频文件输入、实时摄像头输入三种方式。针对本教程场景(方形棋盘 + 已生成的图像清单),原教程建议的命令为(此处方格边长取 3cm,即 0.03 米):
./calibration -w=9 -h=6 -s=0.03 -o=camera.yml -op -oe image_list.xml
其中参数含义(完整参数说明见 samples/cpp/calibration.cpp 的 help 函数):
| 参数 | 含义 | 说明/默认值 |
|---|---|---|
-w |
棋盘内角点列数 | 对 10×7 格棋盘为 9(board_width) |
-h |
棋盘内角点行数 | 对 10×7 格棋盘为 6(board_height) |
-s |
方格边长 | 以用户自定义单位度量,默认 1;本教程取 0.03(米),使解出的 tvec 具有实际物理尺度 |
-o |
输出相机参数文件 | 默认 out_camera_data.yml |
-op |
把检测到的图像角点写入输出文件 | 便于离线复核 |
-oe |
把每帧外参(rvec+tvec)写入输出文件 | 6 元组矩阵形式 |
-pt |
标定板类型 | chessboard(默认)/ circles / acircles / charuco |
-n |
参与标定的帧数 | 默认 10;若不指定则用全部可用视角 |
-a |
固定纵横比 fx/fy | 需要提供该比值并配合内参估计 |
-zt |
假设切向畸变为 0 | 与 CALIB_ZERO_TANGENT_DIST 对应 |
-V |
输入为视频文件而非图像清单 | 配合 [input_data] 传视频路径 |
| 末尾位置参数 | input_data |
图像清单/视频文件;缺省时打开摄像头实时采集(此时可用 g 采集、u 切换去畸变显示、ESC/q 退出) |
当方格边长为 s = 0.03(3cm)时,程序内部会用"格边长 × 格序号"构造棋盘三维物方点(见下文第六节),因此求解出的平移向量将直接以米为单位,距离计算才有物理意义。
4.2 输出文件解读
标定成功后,程序会调用 saveCameraParams(samples/cpp/calibration.cpp)把结果写入 -o 指定的文件,关键字段包括:
%YAML:1.0
---
calibration_time: "..." # 标定时间戳
nframes: 10 # 使用的图像帧数
image_width: 1280
image_height: 720
board_width: 9
board_height: 6
square_size: 0.03 # 方格边长(用户单位)
flags: 0 # 标定选项标志
camera_matrix: !!opencv-matrix # 内参矩阵 K(fx, fy, cx, cy)
distortion_coefficients: !!opencv-matrix # 畸变系数 k1,k2,p1,p2,k3,... 等
avg_reprojection_error: 0.123 # 平均重投影误差
per_view_reprojection_errors: ... # 每帧重投影误差
extrinsic_parameters: ... # 各帧外参(-oe 时写出)
image_points: ... # 各帧图像角点(-op 时写出)
后续位姿估计代码正是通过键名 camera_matrix 与 distortion_coefficients 读取这两组核心参数。
4.3 如何判断标定质量
标定结束时程序会打印两行关键信息:
RMS error reported by calibrateCamera: ...:由calibrateCameraRO返回的内部重投影 RMS;Calibration succeeded/failed. avg reprojection error = ...:由computeReprojectionErrors计算的最终平均重投影误差(以像素为单位)。
若平均重投影误差超过 1~2 像素,通常提示:标定板未完全拍清、棋盘不平整/弯曲、图片数量不足或覆盖的姿态太少、-w/-h 内角点数量设置与真实棋盘不符等。可优先剔除误差最大的若干帧(per_view_reprojection_errors 字段中逐帧可见)后重新标定。
五、深入 findChessboardCorners:棋盘角点检测
完成标定后,进入位姿估计阶段。第一步是在待测图像中检测棋盘。
5.1 加载图像与检测
Mat img = imread(argv[1], IMREAD_GRAYSCALE);
bool found = findChessboardCorners(img, boardSize, ptvec, CALIB_CB_ADAPTIVE_THRESH);
其中:
boardSize:必须是棋盘内角点的行列数,如Size(9, 6);ptvec:输出参数,检测到的内角点按行列顺序存入vector<Point2f>;- 返回值
found为false时说明该图未找到完整棋盘,应跳过该帧。
5.2 检测标志位(源码级说明)
findChessboardCorners 的实现位于 modules/objdetect/src/calibinit.cpp,其函数声明与所有标志位定义在 modules/objdetect/include/opencv2/objdetect.hpp:
enum { CALIB_CB_ADAPTIVE_THRESH = 1, // 自适应阈值二值化
CALIB_CB_NORMALIZE_IMAGE = 2, // 先用 equalizeHist 归一化灰度
CALIB_CB_FILTER_QUADS = 4, // 按面积/周长/方形度过滤候选四边形
CALIB_CB_FAST_CHECK = 8, // 先快速预检,无棋盘则立即返回,显著提速
CALIB_CB_EXHAUSTIVE = 16, // 更彻底的搜索(精度优先)
CALIB_CB_ACCURACY = 32,
CALIB_CB_LARGER = 64,
CALIB_CB_MARKER = 128,
CALIB_CB_PLAIN = 256 // 关闭一切预处理,按原图直接检测
};
多个标志可通过按位或组合,例如 CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE。从实现代码还可以得到两条重要的输入约束(calibinit.cpp):
- 输入图像必须为 8 位单通道灰度或 3/4 通道彩色图,否则会触发类型检查错误;
- 棋盘内角点的宽高都必须 大于 2(
Size(w,h)中 w、h 均 ≥ 3),否则抛出StsOutOfRange。
实战建议:单张快速判断"有没有棋盘"时优先加 CALIB_CB_FAST_CHECK;而标定建图阶段为了召回率,常用 CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE | CALIB_CB_FILTER_QUADS;若棋盘在强光/阴影下,自适应阈值往往比固定阈值鲁棒得多。
检测到亚像素级角点后,还可以用 cornerSubPix 进一步精化坐标(calibration 样例用 -ws 控制其搜索窗口半径,默认 11 像素),以提升后续 PnP 与标定的精度。
六、构造棋盘的三维物方点坐标
findChessboardCorners 只能给出图像上的 2D 像点,要解位姿还必须知道每个角点对应的三维世界坐标。由于棋盘是平面刚体,可任意选择一个世界坐标系:令棋盘一个角位于原点,棋盘平面位于 z = 0 平面,x/y 轴沿棋盘两方向延伸。这样每个角点的三维坐标为 (j*squareSize, i*squareSize, 0)。
参考 calibration.cpp 中 calcChessboardCorners 的标准写法(samples/cpp/calibration.cpp):
static void calcChessboardCorners(Size boardSize, float squareSize,
vector<Point3f>& corners)
{
corners.resize(0);
// boardSize 为内角点数目
for( int i = 0; i < boardSize.height; i++ )
for( int j = 0; j < boardSize.width; j++ )
corners.push_back(Point3f(float(j*squareSize),
float(i*squareSize), 0));
}
注意两点:
- 物方点的行列顺序必须与
findChessboardCorners输出的像点顺序一致(OpenCV 保证二者都按"先行后列、从左到右"排列),否则 PnP 解算会错误; squareSize的单位就是最终距离/位姿的物理单位。若打印的棋盘方格实际边长为 30mm,传入0.03(米),则输出的tvec单位为米。
七、读取标定出的相机参数
标定结果以 OpenCV 的 FileStorage XML/YAML 格式保存,读取代码如下:
FileStorage fs( filename, FileStorage::READ );
Mat intrinsics, distortion;
fs["camera_matrix"] >> intrinsics;
fs["distortion_coefficients"] >> distortion;
读取到的 camera_matrix 即内参矩阵:
K = [ fx 0 cx
0 fy cy
0 0 1 ]
其中 fx/fy 为以像素为单位的焦距(与传感器分辨率强相关,切勿跨分辨率复用),(cx, cy) 为主点;distortion_coefficients 为畸变系数向量(按模型不同,依次为径向 k1、k2、k3… 与切向 p1、p2 等)。正如原教程所强调,这两组数据也可用其他任何标定工具生成,只要符合 OpenCV 的约定即可——本阶段的代码与"如何标定"解耦。
八、用 solvePnP 求解棋盘位姿与距离
8.1 位姿求解
准备好三样输入——棋盘物方点 boardPoints、当前帧检测到的像点 foundBoardCorners、内参与畸变系数——即可调用:
vector<Point3f> boardPoints;
// 用第六节的规则填充物方点数组 ...
solvePnP(Mat(boardPoints), Mat(foundBoardCorners), cameraMatrix,
distCoeffs, rvec, tvec, false);
各参数含义:
boardPoints:棋盘角点在棋盘坐标系下的 3D 坐标(vector<Point3f>);foundBoardCorners:与之一一对应的 2D 图像坐标(来自findChessboardCorners的输出);cameraMatrix、distCoeffs:第七节从文件读出的内参与畸变系数;rvec:输出,旋转向量(Rodrigues 形式,3×1),表示棋盘坐标系相对相机坐标系的旋转;tvec:输出,平移向量(3×1),表示棋盘原点在相机坐标系下的位置;- 末位参数
false(useExtrinsicGuess):不使用外参初值,直接以解析/迭代方式求解。
这一函数在 solvePnP 内部自动完成两步:先用理想针孔模型求解旋转/平移,再把检测到的图像点通过 distCoeffs 进行去畸变处理参与优化(细节可参考 modules/calib/include/opencv2/calib.hpp 中关于 solvePnP 与 solvePnPRansac 的文档注释,后者在存在离群点时更稳健)。仓库样例 samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/src/PnPProblem.cpp 中演示了相同思路的实时化封装,可与本文对照阅读。
8.2 从位姿推算棋盘到相机的距离(原教程 Q&A 详解)
原教程提出一个问题:得到位姿后,如何计算相机光心到任意棋盘角点的距离?
答案的关键在于理解 rvec 与 tvec 的几何含义:它们完整定义了世界(棋盘)坐标系 → 相机坐标系的刚体变换:
P_cam = R * P_world + t
其中 R 由 rvec 经 Rodrigues 变换得到。因此任意棋盘角点若已知其在棋盘坐标系下的坐标,先变换到相机坐标系,再求其到相机光心(相机坐标系原点)的欧氏距离即可。若角点坐标已经位于相机坐标系(例如就是 tvec 本身,即棋盘原点的位置),距离就是该向量的 L2 范数:
// 假设 'point' 是棋盘角点在相机坐标系下的三维坐标
double distance = norm(point);
即对三维点坐标 (x, y, z) 求 sqrt(x*x + y*y + z*z)。特别注意:由于 squareSize 用了带物理单位的边长(如 3cm),所得 distance 也直接落在同一物理单位(米/厘米)下——这正是第四、六节反复强调"单位一致"的原因。若要显示,可把 tvec(即棋盘原点/首个角点在相机系下的坐标)直接代入:double d = norm(tvec);。
九、用重投影误差验证位姿与标定质量
仅凭一组对应点难以判断位姿是否可靠。标准做法是重投影误差:用解出的 rvec/tvec 把物方点重新投影回图像平面,比较投影点与实测检测点的偏差。calibration.cpp 中 computeReprojectionErrors(samples/cpp/calibration.cpp)给出了规范实现:
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 )
{
vector<Point2f> imagePoints2;
int i, totalPoints = 0;
double totalErr = 0, err;
perViewErrors.resize(objectPoints.size());
for( i = 0; i < (int)objectPoints.size(); i++ )
{
projectPoints(Mat(objectPoints[i]), rvecs[i], tvecs[i],
cameraMatrix, distCoeffs, imagePoints2);
err = norm(Mat(imagePoints[i]), Mat(imagePoints2), NORM_L2);
int n = (int)objectPoints[i].size();
perViewErrors[i] = (float)std::sqrt(err*err/n); // 单帧 RMS(像素)
totalErr += err*err;
totalPoints += n;
}
return std::sqrt(totalErr/totalPoints); // 总体 RMS(像素)
}
其核心调用链为:
projectPoints(objectPoint, rvec, tvec, K, distCoeffs) → 重投影像点
norm(实测像点, 重投影像点, NORM_L2) → 单点误差(像素)
按帧求 RMS → perViewErrors;按总点数求 RMS → 整体误差
在一帧位姿估计的场景下,你可以把 rvec/tvec 装成单元素容器复用上述函数,把整体 RMS 当作该帧位姿可靠性的度量(< 1 像素为佳)。它同时是"标定参数是否与当前相机匹配"的判据——若某帧重投影误差系统性偏大,往往说明 camera_matrix 并非该相机拍摄所得。
十、完整工作流总结与仓库延伸阅读
将前文串起来,一个可运行的控制台程序的完整骨架如下:
#include <opencv2/calib.hpp>
#include <opencv2/objdetect.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
using namespace cv;
int main(int argc, char** argv)
{
// 1. 加载灰度图
Mat img = imread(argv[1], IMREAD_GRAYSCALE);
// 2. 检测棋盘(内角点尺寸需与实际棋盘一致)
Size boardSize(9, 6);
vector<Point2f> cornerPts;
bool found = findChessboardCorners(img, boardSize, cornerPts,
CALIB_CB_ADAPTIVE_THRESH);
if (!found) return -1;
// 3. 构造 3D 物方点(z=0 平面,棋盘一角在原点)
float squareSize = 0.03f; // 3 cm,物理单位由用户定义
vector<Point3f> boardPoints;
for (int i = 0; i < boardSize.height; i++)
for (int j = 0; j < boardSize.width; j++)
boardPoints.push_back(Point3f(j * squareSize, i * squareSize, 0.f));
// 4. 读取标定内参与畸变
FileStorage fs(argv[2], FileStorage::READ);
Mat K, dist;
fs["camera_matrix"] >> K;
fs["distortion_coefficients"] >> dist;
// 5. solvePnP 求解位姿,并计算棋盘原点到相机光心的距离
Mat rvec, tvec;
solvePnP(boardPoints, cornerPts, K, dist, rvec, tvec, false);
double distanceToBoardOrigin = norm(tvec); // L2 范数,单位与 squareSize 一致
return 0;
}
关键文件索引
| 用途 | 仓库路径 |
|---|---|
| 本教程源文档 | doc/tutorials/calib3d/camera_calibration_square_chess/camera_calibration_square_chess.markdown |
| 标定主样例(含参数表/位姿流程/重投影误差) | samples/cpp/calibration.cpp |
| 图像清单生成工具 | samples/cpp/imagelist_creator.cpp |
| 棋盘角点检测实现 | modules/objdetect/src/calibinit.cpp |
| 检测标志位与 API 声明 | modules/objdetect/include/opencv2/objdetect.hpp |
| 相机标定/PnP 核心 API 定义 | modules/calib/include/opencv2/calib.hpp |
| 实时位姿估计参考样例 | samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/ |
| 测试棋盘图像 | samples/data/chessboard.png |
进一步阅读建议
- 想了解如何为标定制作/打印不同标定板(方格、非对称圆点、ChArUco 等)并比较其特性,可阅读系列教程 doc/tutorials/calib3d/camera_calibration_pattern/;
- 想系统学习"图像采集 → 角点提取 → 参数优化 → 误差评估"的完整编程细节,可继续阅读 doc/tutorials/calib3d/camera_calibration/;
- 本仓库还提供
stereo_calib.cpp(双目标定)、ChArUco 标定样例 samples/cpp/tutorial_code/objectDetection/calibrate_camera_charuco.cpp 等进阶素材,可作为从单目方格棋盘扩展的下一步。
最后提醒:标定参数(尤其内参矩阵)与图像分辨率一一对应,换用不同分辨率相机或重新对焦后必须重新标定;位姿估计时传入的 squareSize 必须与真实棋盘一致,否则距离与尺寸的度量结果会整体按比例失真。
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
