3、相机标定基础:为什么要标定、棋盘格标定板制作、张正友标定法原理、OpenCV标定流程

大家好,我是你们这门课的老朋友。今天咱们聊一个绕不开的话题——相机标定。

说实话,我刚开始做三维重建那会儿,觉得标定这事儿特枯燥。不就是拍几张棋盘格照片嘛,有啥好学的?结果第一次自己搭双目系统,重建出来的点云全是扭曲的,像个哈哈镜。后来才明白,相机标定是三维重建的“地基”。地基没打好,上面盖的楼再漂亮也得塌。

所以,这一章咱们就把这个地基夯实了。我会从为什么标定讲起,再到棋盘格怎么做、张正友标定法到底在干啥,最后手把手带你跑通OpenCV的标定流程。

相机标定知识体系 相机标定 为什么标定? 消除畸变 · 建立几何模型 棋盘格标定板 尺寸 · 打印 · 拍摄技巧 张正友标定法 原理 · 求解流程 OpenCV标定流程 代码实现 · 参数解读 理论落地 输出:内参矩阵 + 畸变系数

3.1 为什么要标定?—— 相机也有“近视眼”

你想想看,相机镜头本质上就是个凸透镜。理想情况下,光线应该完美地汇聚到一点。但现实中的镜头,尤其是手机摄像头或者便宜的工业相机,都存在各种光学缺陷。

最常见的两个问题:

  • 径向畸变:画面边缘的直线会变弯,像鱼眼镜头那样。说白了就是“桶形畸变”或“枕形畸变”。
  • 切向畸变:镜头和成像平面不平行,导致画面一边大一边小。

除了畸变,我们还需要知道相机内部的精确参数。比如焦距是多少?主点(光轴和成像面的交点)在哪个像素位置?像素是正方形的还是长方形的?

这些参数,统称为相机内参。标定的目的,就是把这些参数算出来。有了内参,我们才能把二维图像上的像素坐标,反算回三维空间中的射线方向。

一句话总结:标定就是给相机做“视力矫正”,顺便搞清楚它的“身体构造”。

我在项目中遇到过最典型的例子:用未标定的相机做三维重建,一个1米长的物体,重建出来可能只有0.8米。你说这能忍吗?

3.2 棋盘格标定板制作——别小看这张纸

标定需要已知三维坐标的参考物。棋盘格标定板,就是最常用的参考物。

为什么用棋盘格?因为角点检测太方便了。黑白格子交界处的角点,在图像中特征极其明显,算法可以亚像素精度地定位。

制作棋盘格,我个人习惯注意以下几点:

  1. 格子数量:建议用 9×6 或 11×8 的内角点(即内部交叉点)。别用对称的,比如 8×8,否则算法分不清方向。
  2. 格子尺寸:打印出来每个格子边长 20mm-30mm 比较合适。太小了相机拍不清,太大了又占画面。
  3. 打印质量:一定要用激光打印,别用喷墨。喷墨的黑色反光严重,角点检测会飘。
  4. 贴板:贴在绝对平整的硬板上。亚克力板或者铝板都行。我曾经偷懒贴在三合板上,结果板子本身有弧度,标定出来的畸变参数全是错的。

小技巧:如果条件允许,买一块陶瓷棋盘格标定板。热稳定性好,精度高。淘宝上几十块钱一块,省心不少。

3.3 张正友标定法原理——一张棋盘搞定内参

张正友标定法,是1998年提出的。到现在二十多年了,依然是工业界最主流的标定方法。为什么这么牛?因为它只需要拍几张不同角度的棋盘格照片,就能算出内参。

核心思想其实不复杂:

我们假设棋盘格在三维空间中是一个平面(Z=0)。那么,棋盘格上的每个角点,三维坐标是已知的(比如 (0,0,0), (1,0,0), ...)。它们在图像上的像素坐标,我们也能检测到。

这就构成了一个“3D点→2D点”的对应关系。通过这个对应关系,我们可以解出一个单应性矩阵 H。H 里面混合了内参和外参的信息。

然后,利用旋转矩阵的正交约束,我们可以从多个 H 中剥离出内参矩阵 K。具体推导我就不展开了,但你可以理解为:

  • 每张照片提供一个 H
  • 多个 H 联立求解,得到内参 K
  • 有了 K,再反算每张照片的外参(旋转和平移)
  • 最后用非线性优化,把畸变系数也拟合出来

嗯,这里要注意:张正友法假设镜头畸变模型是多项式模型(通常是 2 个径向参数 + 2 个切向参数)。对于大多数镜头,这个模型已经够用了。但如果是广角或鱼眼镜头,需要更复杂的模型。

3.4 OpenCV标定流程——跑通你的第一个标定程序

理论说完了,咱们直接上代码。OpenCV 把张正友法封装得非常好,核心函数就几个。

我习惯的标定流程分五步:

  1. 准备棋盘格:打印并拍摄 15-20 张不同角度的照片。角度要丰富,有倾斜、有旋转、有远近。
  2. 检测角点:用 cv::findChessboardCorners() 找到所有内角点。
  3. 亚像素精化:用 cv::cornerSubPix() 把角点精度提高到亚像素级别。
  4. 执行标定:调用 cv::calibrateCamera(),传入世界坐标和图像坐标,得到内参和畸变系数。
  5. 评估结果:计算重投影误差,一般小于 0.5 像素就算不错。

下面是一个完整的 C++ 示例代码(Python 版本也类似):

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

int main() {
    // 1. 准备棋盘格参数
    cv::Size boardSize(9, 6);   // 内角点数量
    float squareSize = 25.0f;   // 格子边长,单位mm

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

    // 3. 读取所有标定图片
    std::vector<std::vector<cv::Point3f>> objectPoints;
    std::vector<std::vector<cv::Point2f>> imagePoints;
    cv::Size imageSize;

    std::vector<cv::String> files;
    cv::glob("calib_images/*.jpg", files);
    for (auto &file : files) {
        cv::Mat img = cv::imread(file);
        if (img.empty()) continue;
        imageSize = img.size();

        std::vector<cv::Point2f> corners;
        bool found = cv::findChessboardCorners(img, boardSize, corners);
        if (found) {
            // 亚像素精化
            cv::Mat gray;
            cv::cvtColor(img, gray, cv::COLOR_BGR2GRAY);
            cv::cornerSubPix(gray, corners, cv::Size(11,11), cv::Size(-1,-1),
                             cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.001));
            objectPoints.push_back(objPoints);
            imagePoints.push_back(corners);
        }
    }

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

    // 5. 输出结果
    std::cout << "重投影误差: " << rms << " 像素" << std::endl;
    std::cout << "内参矩阵:\n" << cameraMatrix << std::endl;
    std::cout << "畸变系数:\n" << distCoeffs << std::endl;

    return 0;
}

避坑指南:我曾经拍标定照片时,所有照片都让棋盘格位于画面中央。结果标定出来的畸变参数在边缘区域完全不准。后来才意识到,必须让棋盘格出现在画面的各个角落——左上、右下、边缘都要覆盖到。这样才能充分约束畸变模型。

标定完成后,你会得到两个核心输出:

输出 含义 示例值
内参矩阵 K fx, fy, cx, cy [fx=825.3, fy=824.7, cx=640.5, cy=480.2]
畸变系数 k1, k2, p1, p2, k3 [k1=-0.32, k2=0.12, p1=0.001, p2=-0.002, k3=0.0]

拿到这些参数后,你就可以用 cv::undistort() 对任意图像做畸变矫正了。矫正后的图像,直线是直的,三维重建的精度才有保障。

好了,这一章的内容就到这儿。标定是三维重建的第一步,也是最重要的一步。别嫌麻烦,多拍几张照片,多检查几遍重投影误差。后面咱们做立体匹配、点云融合,全靠这组参数撑着呢。


专注资料整理