相机标定基础:什么是相机标定、为什么需要标定、标定板的选择、标定流程概述
大家好,欢迎来到《OpenCV相机标定与三维重建实战》的第二讲。
今天咱们聊点实在的——相机标定。说实话,我刚开始接触这个领域的时候,觉得标定就是个“玄学”。明明相机买回来就能拍照,为什么还要多此一举去标定?后来踩了不少坑才明白,标定是三维视觉的基石,没有它,后面的重建、测距、SLAM全是空中楼阁。
一、什么是相机标定?
相机标定,说白了就是找到相机内部和外部的一些“隐藏参数”。
你想想看,相机把三维世界拍成二维照片,这个过程是有数学模型的。但这个模型里有很多未知数:
- 内参:焦距、主点坐标、畸变系数(径向畸变、切向畸变)
- 外参:相机在世界坐标系中的位置和朝向(旋转矩阵 + 平移向量)
标定就是通过拍摄已知尺寸的标定板,反推出这些参数。嗯,这里要注意,标定不是“校准”相机硬件,而是“测量”相机的成像特性。
核心公式(针孔相机模型):
s * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T
其中:
(u, v) —— 像素坐标
(X, Y, Z) —— 世界坐标
K —— 内参矩阵
[R | t] —— 外参矩阵
s —— 尺度因子
二、为什么需要标定?
这个问题我问过很多新人,答案五花八门。我个人习惯用一个比喻来解释:相机标定就像给相机配一副“度数准确的眼镜”。
没有标定,你拍出来的照片是这样的:
- 直线是弯的(径向畸变)
- 画面边缘被拉伸(切向畸变)
- 实际距离和像素距离对不上(焦距不准)
我在项目中遇到过最典型的场景——用双目相机做测距。没标定之前,测10米外的物体,误差能到半米。标定之后,误差直接降到2厘米以内。差距就这么大。
具体来说,标定的作用包括:
- 去畸变:把桶形/枕形畸变矫正回来
- 建立像素-物理映射:知道一个像素对应真实世界多少毫米
- 多相机对齐:双目/多目系统中,让左右视图的极线对齐
- 三维重建基础:没有标定,三角测量就是空谈
避坑指南:我曾经以为买来的工业相机出厂时已经标定好了,结果发现畸变系数全是0。后来才知道,出厂标定只保证光学中心大致在图像中心,具体参数需要你自己标。千万别偷懒!
三、标定板的选择
标定板是标定的“尺子”。选对了事半功倍,选错了……嗯,你会怀疑人生的。
3.1 棋盘格标定板
这是最经典、最常用的标定板。为什么?因为角点检测极其稳定。
- 优点:角点位置精确到亚像素级别,OpenCV的
findChessboardCorners()非常成熟 - 缺点:需要完全可见,部分遮挡就检测失败;对光照敏感
- 适用场景:室内、可控光照、单目/双目标定
我个人习惯用9×6或12×9的棋盘格,内角点数量适中,计算量不大,精度也够。
3.2 圆点阵标定板
圆点阵是另一种常见选择,尤其适合大视场角或鱼眼相机。
- 优点:圆心检测对畸变不敏感,部分遮挡也能工作
- 缺点:透视投影下圆会变成椭圆,圆心定位有偏差(需要椭圆拟合补偿)
- 适用场景:鱼眼相机、大广角镜头、户外标定
我的经验:如果你用圆点阵,记得把标定板打印成非对称圆点阵(比如11×4,但圆点错开排列)。这样OpenCV的findCirclesGrid()能自动确定方向,不用手动指定。
3.3 两种标定板的对比
| 特性 | 棋盘格 | 圆点阵 |
|---|---|---|
| 角点/圆心精度 | 亚像素级(高) | 亚像素级(中) |
| 抗畸变能力 | 弱(角点偏移明显) | 强(圆心偏移小) |
| 部分遮挡鲁棒性 | 差 | 较好 |
| 检测速度 | 快 | 中等 |
| OpenCV支持 | 非常成熟 | 成熟 |
四、标定流程概述
标定流程其实不复杂,但细节决定成败。我把它拆成5步:
- 准备标定板:打印棋盘格或圆点阵,贴在平整的硬板上。尺寸要量准,误差控制在0.1mm以内。
- 采集图像:从不同角度拍摄15-30张标定板图像。注意:覆盖整个视场(四个角、中心、倾斜、旋转)。
- 检测特征点:用OpenCV的
findChessboardCorners()或findCirclesGrid()提取角点/圆心。 - 计算参数:调用
calibrateCamera(),输入世界坐标和像素坐标,得到内参、畸变系数、外参。 - 评估结果:看重投影误差(小于0.5像素算合格,小于0.1像素算优秀)。
核心代码片段(OpenCV Python):
import cv2
import numpy as np
# 准备世界坐标(假设棋盘格内角点9×6,格子边长25mm)
pattern_size = (9, 6)
square_size = 25.0 # mm
objp = np.zeros((pattern_size[0]*pattern_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
objp *= square_size
# 存储所有图像的世界坐标和像素坐标
objpoints = [] # 3D点
imgpoints = [] # 2D点
# 遍历图像
for fname in image_files:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
objpoints.append(objp)
# 亚像素精细化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)
我曾经踩过的坑:采集图像时只拍了正对着相机的角度,结果标定出来的畸变系数全是0。后来才知道,标定板必须覆盖视场的边缘和角落,否则畸变参数无法正确估计。建议至少拍20张,其中5张在边缘、5张倾斜30度以上。
五、总结
相机标定,说白了就是用已知的“尺子”去测量相机的“眼睛”。棋盘格适合大多数场景,圆点阵适合鱼眼镜头。流程不复杂,但采集数据时一定要耐心——角度要全、光照要匀、标定板要平。
下一讲,我们会深入OpenCV的标定API,手把手带你跑通第一个标定程序。到时候你会发现,原来标定也可以这么简单。
嗯,今天就到这里。有问题欢迎在评论区交流,咱们下期见。