首页
/ OpenCV 方格棋盘相机标定与位姿估计实战指南(calibration + solvePnP)

OpenCV 方格棋盘相机标定与位姿估计实战指南(calibration + solvePnP)

2026-09-07 14:43:20作者:俞予舒Fleming

导读:本文以 OpenCV 官方教程"Camera calibration with square chessboard"为骨架,围绕方格棋盘展开完整讲解:从准备标定图集、生成图像清单、运行 calibration 样例获取相机内参与畸变系数,到编写代码用 findChessboardCorners 检测棋盘、用 solvePnP 求解棋盘相对相机的位姿(旋转与平移),并最终计算任意棋盘角点到相机的距离。读者学完后,将能独立完成"离线标定 + 单张图像位姿求解"的标准流程,并可将其推广到任意已知三维几何的物体上。

适用范围:本仓库对应的 OpenCV 版本要求 OpenCV >= 4.0(见 camera_calibration_square_chess.markdown 的兼容性声明);原文作者为 Victor Eruhimov。

OpenCV 样例数据中的方格棋盘图,用作角点检测与位姿估计的测试输入

一、任务全貌:从标定到位姿估计

本教程对应源码目录 doc/tutorials/calib3d/camera_calibration_square_chess/,属于相机标定(calib3d)系列教程之一(该系列还包含 camera_calibration_pattern 方格/圆点标定板图生成、camera_calibration 通用标定实战、real_time_pose 实时位姿估计等主题,可通过 table_of_content_calib3d.markdown 查看完整目录)。

整个工作流分两大阶段:

  1. 相机标定阶段:输入一组拍摄自不同角度/位姿的棋盘图像,输出相机内参矩阵(focal length、主点)、畸变系数以及用于质量评估的重投影误差。
  2. 位姿估计阶段:对一张新图像检测棋盘角点,结合已知的棋盘 3D 几何与上一阶段标定出的内参,用 PnP(Perspective-n-Point)求解棋盘坐标系相对相机坐标系的旋转向量 rvec 与平移向量 tvec,从而获得棋盘距离、姿态等度量信息。

该方法的核心思想可以推广:任何具有已知三维几何结构的刚体,只要能在图像中稳定检测到对应点,都可以套用同样的"物方点 + 像方点 + 内参 → solvePnP"流程来求位姿。

二、准备测试数据:编译样例并获取棋盘图像

2.1 启用样例编译

OpenCV 的样例代码(如 calibrationimagelist_creator)默认不随库一起编译。需要在 CMake 配置阶段打开开关:

cmake -DBUILD_EXAMPLES=ON ..

编译完成后,可执行程序会输出到构建目录的 bin 子目录(即教程中提到的 "Go to bin folder")。相关样例源码位于仓库 samples/cpp 下,其中本文涉及两个关键程序:

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.cpphelp 函数):

参数 含义 说明/默认值
-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 输出文件解读

标定成功后,程序会调用 saveCameraParamssamples/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_matrixdistortion_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>
  • 返回值 foundfalse 时说明该图未找到完整棋盘,应跳过该帧。

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):

  1. 输入图像必须为 8 位单通道灰度或 3/4 通道彩色图,否则会触发类型检查错误;
  2. 棋盘内角点的宽高都必须 大于 2Size(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.cppcalcChessboardCorners 的标准写法(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 的输出);
  • cameraMatrixdistCoeffs:第七节从文件读出的内参与畸变系数;
  • rvec:输出,旋转向量(Rodrigues 形式,3×1),表示棋盘坐标系相对相机坐标系的旋转;
  • tvec:输出,平移向量(3×1),表示棋盘原点在相机坐标系下的位置;
  • 末位参数 falseuseExtrinsicGuess):不使用外参初值,直接以解析/迭代方式求解。

这一函数在 solvePnP 内部自动完成两步:先用理想针孔模型求解旋转/平移,再把检测到的图像点通过 distCoeffs 进行去畸变处理参与优化(细节可参考 modules/calib/include/opencv2/calib.hpp 中关于 solvePnPsolvePnPRansac 的文档注释,后者在存在离群点时更稳健)。仓库样例 samples/cpp/tutorial_code/calib3d/real_time_pose_estimation/src/PnPProblem.cpp 中演示了相同思路的实时化封装,可与本文对照阅读。

8.2 从位姿推算棋盘到相机的距离(原教程 Q&A 详解)

原教程提出一个问题:得到位姿后,如何计算相机光心到任意棋盘角点的距离?

答案的关键在于理解 rvectvec 的几何含义:它们完整定义了世界(棋盘)坐标系 → 相机坐标系的刚体变换:

P_cam = R * P_world + t

其中 Rrvec 经 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.cppcomputeReprojectionErrorssamples/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

进一步阅读建议

最后提醒:标定参数(尤其内参矩阵)与图像分辨率一一对应,换用不同分辨率相机或重新对焦后必须重新标定;位姿估计时传入的 squareSize 必须与真实棋盘一致,否则距离与尺寸的度量结果会整体按比例失真。

登录后查看全文
热门项目推荐
相关项目推荐