相机标定基础:张正友标定法原理、棋盘格标定板制作、OpenCV标定流程与代码实现

各位同学,欢迎来到《多相机视觉融合控制实战》的第一章。

说实话,做多相机融合,第一步不是写代码,也不是调算法。第一步是什么?是让每一只“眼睛”都看得准。如果相机本身就有畸变、焦距不准、光心偏移,那后面所有融合都是空中楼阁。所以,咱们今天就从相机标定开始聊起。

为什么非要标定?

你想想看,一个镜头拍出来的图像,边缘是不是有点弯?尤其是广角镜头,桶形畸变特别明显。这不是镜头坏了,是物理规律决定的。相机标定,说白了就是给镜头“配眼镜”——算出它的内参(焦距、光心)、畸变系数,还有外参(相机在世界坐标系里的位置和姿态)。

我刚开始做视觉的时候,觉得标定挺麻烦,直接用厂家给的参数。结果呢?融合出来的点云全是歪的。后来老老实实标定了一遍,效果立竿见影。嗯,从那以后我再也不敢偷懒了。

核心结论: 标定是视觉系统的“校准仪式”,不做标定,后面所有工作都是白费。

张正友标定法:为什么它成了行业标准?

张正友标定法,1998年提出,到现在二十多年了,依然是OpenCV、MATLAB标定工具箱的底层算法。为什么这么能打?因为它只需要拍几张棋盘格照片,不需要昂贵的精密设备。

它的核心思路是这样的:

  • 你拍一张棋盘格照片,棋盘格上的角点坐标你是知道的(比如每个格子30mm)。
  • 相机把棋盘格投影到成像平面上,像素坐标你也知道。
  • 通过单应性矩阵,建立世界坐标和像素坐标的映射关系。
  • 多张照片,多个角度,联立求解内参、外参和畸变系数。

说白了,就是用数学方法,从已知的棋盘格尺寸反推相机的“脾气”。

我的经验: 张正友法对棋盘格平面度要求很高。我曾经用一张贴在纸板上的棋盘格,纸板有点弯,结果标定出来的畸变系数明显偏大。后来换了玻璃基板的棋盘格,问题就解决了。

棋盘格标定板制作:别在这上面省钱

棋盘格标定板,看起来简单,但坑不少。我见过有人用A4纸打印一张棋盘格,贴在墙上就开始标定。结果呢?角点检测总是失败,因为纸张不平整,反光也严重。

制作标定板,我建议你注意以下几点:

  • 材质: 最好用亚克力或玻璃基板,表面平整,不易变形。
  • 尺寸: 棋盘格边长10mm-30mm比较常见。太大或太小都不好。太大,一张照片里拍不全;太小,角点检测精度下降。
  • 格子数: 一般用9×6或10×7的内角点(注意,是内角点,不是格子数)。比如9×6的棋盘格,实际有10×7个格子。
  • 打印精度: 用高精度打印机,确保每个格子尺寸一致。我见过打印出来格子边长差了0.5mm的,标定结果直接废了。
参数 推荐值 说明
棋盘格边长 10-30 mm 根据镜头视场角调整
内角点数量 9×6 或 10×7 奇数×偶数,方便检测
基板材质 亚克力/玻璃 避免纸张弯曲
打印精度 ≥600 dpi 确保格子尺寸一致
避坑指南: 我曾经用一张反光严重的覆膜棋盘格,在强光下标定,结果角点检测总是把高光点误判为角点。后来换了哑光材质,问题迎刃而解。

OpenCV标定流程:一步步来

好了,理论说完了,咱们上代码。OpenCV的标定流程其实很清晰,我把它拆成几步:

  1. 准备棋盘格角点坐标: 生成世界坐标系下的棋盘格角点坐标(假设Z=0)。
  2. 检测图像中的角点:cv::findChessboardCorners() 找到每张照片里的角点像素坐标。
  3. 亚像素精细化:cv::cornerSubPix() 把角点精度提升到亚像素级别。
  4. 执行标定: 调用 cv::calibrateCamera(),输入世界坐标和像素坐标,输出内参、畸变系数、旋转和平移向量。
  5. 评估结果: 计算重投影误差,看看标定精度怎么样。

下面是一个完整的标定代码示例,我加了详细的注释:

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

int main() {
    // 1. 设置棋盘格参数
    cv::Size boardSize(9, 6);  // 内角点数量
    float squareSize = 20.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.0f));
        }
    }

    // 3. 准备存储容器
    std::vector<std::vector<cv::Point3f>> objectPointsVec;
    std::vector<std::vector<cv::Point2f>> imagePointsVec;
    std::vector<cv::String> imagePaths;
    cv::glob("calib_images/*.jpg", imagePaths);  // 读取所有标定图片

    // 4. 遍历每张图片,检测角点
    for (const auto& path : imagePaths) {
        cv::Mat img = cv::imread(path);
        if (img.empty()) continue;

        std::vector<cv::Point2f> corners;
        bool found = cv::findChessboardCorners(img, boardSize, corners,
            cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);

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

            objectPointsVec.push_back(objectPoints);
            imagePointsVec.push_back(corners);

            // 可视化角点(可选)
            cv::drawChessboardCorners(img, boardSize, corners, found);
            cv::imshow("Corners", img);
            cv::waitKey(100);
        }
    }

    // 5. 执行标定
    cv::Mat cameraMatrix, distCoeffs;
    std::vector<cv::Mat> rvecs, tvecs;
    double reprojectionError = cv::calibrateCamera(objectPointsVec, imagePointsVec,
        cv::Size(1920, 1080), cameraMatrix, distCoeffs, rvecs, tvecs);

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

    // 7. 保存结果
    cv::FileStorage fs("calibration_result.yml", cv::FileStorage::WRITE);
    fs << "cameraMatrix" << cameraMatrix;
    fs << "distCoeffs" << distCoeffs;
    fs.release();

    return 0;
}
我的习惯: 标定图片至少拍15-20张,覆盖不同角度和距离。尤其是棋盘格要出现在画面的四个角落和中心,这样畸变系数才能算得准。

标定结果怎么看?

标定完成后,你会得到几个关键参数:

  • 内参矩阵: 包含fx, fy(焦距,单位像素),cx, cy(光心坐标)。
  • 畸变系数: 通常有5个参数(k1, k2, p1, p2, k3),分别对应径向畸变和切向畸变。
  • 重投影误差: 这个值越小越好。一般小于0.5像素就算不错了。如果超过1像素,说明标定质量有问题,需要重新拍图。

我见过有人标定完,重投影误差0.05像素,高兴得不行。其实0.05和0.3在实际应用中差别不大,只要小于0.5,基本够用。别太纠结这个数字。

知识体系总览

下面这张图,是我自己画的本章知识结构。你可以把它当作一个“地图”,随时回来看看:

相机标定知识体系 张正友标定法原理 棋盘格标定板制作 OpenCV标定流程 单应性矩阵 → 内参 → 畸变 多张照片联立求解 材质:亚克力/玻璃 尺寸:10-30mm边长 内角点:9×6或10×7 角点检测 → 亚像素 → 标定 重投影误差评估 结果保存与验证 核心目标:让每只“眼睛”都看得准

这张图把本章的三个核心模块串起来了:原理是基础,硬件是工具,软件是手段。三者缺一不可。

最后说一句: 标定这件事,看起来简单,但细节决定成败。多拍几张照片,多检查几次角点检测结果,多验证一下重投影误差。这些功夫花下去,后面做多相机融合的时候,你会感谢自己的。

公众号:蓝海资料掘金营,微信deep3321