相机标定基础:张正友标定法原理、棋盘格标定板制作、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的标定流程其实很清晰,我把它拆成几步:
- 准备棋盘格角点坐标: 生成世界坐标系下的棋盘格角点坐标(假设Z=0)。
- 检测图像中的角点: 用
cv::findChessboardCorners()找到每张照片里的角点像素坐标。 - 亚像素精细化: 用
cv::cornerSubPix()把角点精度提升到亚像素级别。 - 执行标定: 调用
cv::calibrateCamera(),输入世界坐标和像素坐标,输出内参、畸变系数、旋转和平移向量。 - 评估结果: 计算重投影误差,看看标定精度怎么样。
下面是一个完整的标定代码示例,我加了详细的注释:
#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;
}
标定结果怎么看?
标定完成后,你会得到几个关键参数:
- 内参矩阵: 包含fx, fy(焦距,单位像素),cx, cy(光心坐标)。
- 畸变系数: 通常有5个参数(k1, k2, p1, p2, k3),分别对应径向畸变和切向畸变。
- 重投影误差: 这个值越小越好。一般小于0.5像素就算不错了。如果超过1像素,说明标定质量有问题,需要重新拍图。
我见过有人标定完,重投影误差0.05像素,高兴得不行。其实0.05和0.3在实际应用中差别不大,只要小于0.5,基本够用。别太纠结这个数字。
知识体系总览
下面这张图,是我自己画的本章知识结构。你可以把它当作一个“地图”,随时回来看看:
这张图把本章的三个核心模块串起来了:原理是基础,硬件是工具,软件是手段。三者缺一不可。
公众号:蓝海资料掘金营,微信deep3321