3、相机标定基础:为什么要标定、棋盘格标定板制作、张正友标定法原理、OpenCV标定流程
大家好,我是你们这门课的老朋友。今天咱们聊一个绕不开的话题——相机标定。
说实话,我刚开始做三维重建那会儿,觉得标定这事儿特枯燥。不就是拍几张棋盘格照片嘛,有啥好学的?结果第一次自己搭双目系统,重建出来的点云全是扭曲的,像个哈哈镜。后来才明白,相机标定是三维重建的“地基”。地基没打好,上面盖的楼再漂亮也得塌。
所以,这一章咱们就把这个地基夯实了。我会从为什么标定讲起,再到棋盘格怎么做、张正友标定法到底在干啥,最后手把手带你跑通OpenCV的标定流程。
3.1 为什么要标定?—— 相机也有“近视眼”
你想想看,相机镜头本质上就是个凸透镜。理想情况下,光线应该完美地汇聚到一点。但现实中的镜头,尤其是手机摄像头或者便宜的工业相机,都存在各种光学缺陷。
最常见的两个问题:
- 径向畸变:画面边缘的直线会变弯,像鱼眼镜头那样。说白了就是“桶形畸变”或“枕形畸变”。
- 切向畸变:镜头和成像平面不平行,导致画面一边大一边小。
除了畸变,我们还需要知道相机内部的精确参数。比如焦距是多少?主点(光轴和成像面的交点)在哪个像素位置?像素是正方形的还是长方形的?
这些参数,统称为相机内参。标定的目的,就是把这些参数算出来。有了内参,我们才能把二维图像上的像素坐标,反算回三维空间中的射线方向。
一句话总结:标定就是给相机做“视力矫正”,顺便搞清楚它的“身体构造”。
我在项目中遇到过最典型的例子:用未标定的相机做三维重建,一个1米长的物体,重建出来可能只有0.8米。你说这能忍吗?
3.2 棋盘格标定板制作——别小看这张纸
标定需要已知三维坐标的参考物。棋盘格标定板,就是最常用的参考物。
为什么用棋盘格?因为角点检测太方便了。黑白格子交界处的角点,在图像中特征极其明显,算法可以亚像素精度地定位。
制作棋盘格,我个人习惯注意以下几点:
- 格子数量:建议用 9×6 或 11×8 的内角点(即内部交叉点)。别用对称的,比如 8×8,否则算法分不清方向。
- 格子尺寸:打印出来每个格子边长 20mm-30mm 比较合适。太小了相机拍不清,太大了又占画面。
- 打印质量:一定要用激光打印,别用喷墨。喷墨的黑色反光严重,角点检测会飘。
- 贴板:贴在绝对平整的硬板上。亚克力板或者铝板都行。我曾经偷懒贴在三合板上,结果板子本身有弧度,标定出来的畸变参数全是错的。
小技巧:如果条件允许,买一块陶瓷棋盘格标定板。热稳定性好,精度高。淘宝上几十块钱一块,省心不少。
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 把张正友法封装得非常好,核心函数就几个。
我习惯的标定流程分五步:
- 准备棋盘格:打印并拍摄 15-20 张不同角度的照片。角度要丰富,有倾斜、有旋转、有远近。
- 检测角点:用
cv::findChessboardCorners()找到所有内角点。 - 亚像素精化:用
cv::cornerSubPix()把角点精度提高到亚像素级别。 - 执行标定:调用
cv::calibrateCamera(),传入世界坐标和图像坐标,得到内参和畸变系数。 - 评估结果:计算重投影误差,一般小于 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() 对任意图像做畸变矫正了。矫正后的图像,直线是直的,三维重建的精度才有保障。
好了,这一章的内容就到这儿。标定是三维重建的第一步,也是最重要的一步。别嫌麻烦,多拍几张照片,多检查几遍重投影误差。后面咱们做立体匹配、点云融合,全靠这组参数撑着呢。