标定与参数获取:棋盘格标定法、OpenCV标定流程、内参矩阵与畸变系数

做实时畸变校正,第一步不是写代码,而是拿到相机的「身份证」——内参和畸变系数。没有这组参数,后面的校正全是空谈。

我见过不少团队,上来就调校正算法,折腾半天效果不对,最后发现是标定参数没算准。说白了,标定是地基,地基歪了,楼盖得再高也得塌。

棋盘格标定法:为什么是棋盘格?

棋盘格标定法,是目前最主流的相机标定方式。你想想看,为什么偏偏是棋盘格?

  • 角点检测稳定:黑白交界的角点,在图像中梯度变化明显,算法容易定位。
  • 几何关系已知:每个格子的大小是固定的,角点之间的物理距离我们事先量好了。
  • 制作成本低:一张A4纸打印出来,贴在平板上就能用。

我个人习惯用 9×6 的棋盘格,内角点就是 8×5。为什么不是 10×7?因为边缘的格子容易被裁切,留一圈余量更保险。

我的经验:打印棋盘格时,尽量用哑光纸,别用光面纸。光面纸在强光下会反光,角点检测容易失败。我曾经在户外测试时吃过这个亏,后来改用哑光纸,问题就解决了。

OpenCV标定流程:四步走

OpenCV 把标定流程封装得很干净,核心就四个步骤。我按实际项目中的顺序给你捋一遍:

  1. 准备棋盘格图像:拍摄 15-20 张不同角度、不同位置的棋盘格照片。
  2. 查找角点:用 cv::findChessboardCorners() 找到每张图中的内角点。
  3. 亚像素精化:用 cv::cornerSubPix() 把角点精度提升到亚像素级别。
  4. 执行标定:调用 cv::calibrateCamera(),得到内参矩阵和畸变系数。

这里有个坑:很多人拍完照片直接跑标定,结果误差很大。为什么?因为照片数量不够,或者角度变化太小。

避坑指南:我曾经只拍了 8 张照片就急着标定,结果重投影误差到了 0.8 像素。后来老老实实拍了 20 张,误差降到了 0.15 像素。记住:15-20 张是底线,角度要覆盖画面的四个角和中心区域。

核心代码:标定流程实现

下面这段代码,是我在项目中反复用过的模板。你直接拿去改改就能用:

// 1. 准备棋盘格尺寸
cv::Size boardSize(8, 5);   // 内角点数
float squareSize = 30.0f;   // 格子边长,单位mm

// 2. 生成物理坐标系中的角点坐标
std::vector<cv::Point3f> objectPoints;
for (int i = 0; i < boardSize.height; i++) {
    for (int j = 0; j < boardSize.width; j++) {
        objectPoints.push_back(cv::Point3f(j * squareSize, i * squareSize, 0));
    }
}

// 3. 遍历所有图像,查找角点
std::vector<std::vector<cv::Point2f>> imagePoints;
std::vector<std::vector<cv::Point3f>> objectPointsSet;

for (auto& filename : imageFiles) {
    cv::Mat img = cv::imread(filename);
    std::vector<cv::Point2f> corners;
    bool found = cv::findChessboardCorners(img, boardSize, corners);
    if (found) {
        cv::cornerSubPix(img, corners, cv::Size(5,5), cv::Size(-1,-1),
                         cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.001));
        imagePoints.push_back(corners);
        objectPointsSet.push_back(objectPoints);
    }
}

// 4. 执行标定
cv::Mat cameraMatrix, distCoeffs;
std::vector<cv::Mat> rvecs, tvecs;
double rms = cv::calibrateCamera(objectPointsSet, imagePoints,
                                 cv::Size(1920, 1080),
                                 cameraMatrix, distCoeffs,
                                 rvecs, tvecs);

std::cout << "重投影误差: " << rms << std::endl;
std::cout << "内参矩阵:\n" << cameraMatrix << std::endl;
std::cout << "畸变系数:\n" << distCoeffs << std::endl;
关键指标:重投影误差 RMS 应小于 0.3 像素。如果超过 0.5,说明标定质量有问题,需要检查照片质量或重新拍摄。

内参矩阵:相机的「出厂设置」

内参矩阵,说白了就是描述相机内部光学结构的 3×3 矩阵。它长这样:

| fx   0   cx |
| 0   fy   cy |
| 0    0    1 |
  • fx, fy:焦距,单位是像素。fx = f / dx,其中 f 是物理焦距,dx 是像素物理尺寸。
  • cx, cy:主点坐标,通常是图像中心附近。

我遇到过一个问题:同一款相机,两次标定得到的 fx 差了 2%。后来发现是标定板没贴平,微微翘起导致的。所以标定板一定要贴在刚性平板上,别用软纸板。

畸变系数:镜头天生的「缺陷」

镜头不是完美的,畸变就是它的「生理缺陷」。OpenCV 用 5 个系数来描述:

系数 含义 典型范围
k1 径向畸变一阶 -0.5 ~ 0.5
k2 径向畸变二阶 -0.5 ~ 0.5
p1 切向畸变一阶 -0.1 ~ 0.1
p2 切向畸变二阶 -0.1 ~ 0.1
k3 径向畸变三阶 -0.5 ~ 0.5

径向畸变让直线变弯,切向畸变让图像倾斜。广角镜头的 k1 通常比较大,我见过鱼眼镜头 k1 能到 -0.4 以上。

小技巧:标定完成后,用 cv::undistort() 对一张棋盘格图像做校正,然后肉眼检查边缘的直线是否变直了。这是最直观的验证方法。

标定流程全景图

下面这张图,把整个标定流程串起来了。从拍摄到参数输出,每一步都离不开:

棋盘格标定流程全景图 拍摄棋盘格图像 角点检测与精化 执行标定计算 内参矩阵 (3×3) 畸变系数 (5个) 重投影误差评估 输出:内参 + 畸变系数 → 畸变校正 关键:重投影误差 < 0.3 像素才算合格,否则需要重新拍摄或检查标定板

标定质量检查清单

标定完成后,别急着往下走。我习惯按这个清单检查一遍:

  • 重投影误差:RMS 是否小于 0.3 像素?
  • 内参合理性:fx 和 fy 是否接近?cx、cy 是否在图像中心附近?
  • 畸变系数量级:k1、k2 是否在合理范围内?如果 k1 超过 ±0.5,可能标定有问题。
  • 视觉验证:校正后的图像边缘直线是否变直?有没有出现过度校正的「枕形」或「桶形」?
我曾经踩过的坑:有一次标定结果 RMS 只有 0.12,看起来很好。但实际校正时,画面边缘出现了明显的紫色伪影。后来发现是标定板本身印刷精度不够,格子边缘模糊导致角点定位有系统偏差。所以标定板最好用高精度打印,或者买现成的陶瓷标定板。