相机标定基础:什么是相机标定、为什么需要标定、标定板的选择、标定流程概述

大家好,欢迎来到《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厘米以内。差距就这么大。

具体来说,标定的作用包括:

  1. 去畸变:把桶形/枕形畸变矫正回来
  2. 建立像素-物理映射:知道一个像素对应真实世界多少毫米
  3. 多相机对齐:双目/多目系统中,让左右视图的极线对齐
  4. 三维重建基础:没有标定,三角测量就是空谈

避坑指南:我曾经以为买来的工业相机出厂时已经标定好了,结果发现畸变系数全是0。后来才知道,出厂标定只保证光学中心大致在图像中心,具体参数需要你自己标。千万别偷懒!

三、标定板的选择

标定板是标定的“尺子”。选对了事半功倍,选错了……嗯,你会怀疑人生的。

3.1 棋盘格标定板

这是最经典、最常用的标定板。为什么?因为角点检测极其稳定

  • 优点:角点位置精确到亚像素级别,OpenCV的findChessboardCorners()非常成熟
  • 缺点:需要完全可见,部分遮挡就检测失败;对光照敏感
  • 适用场景:室内、可控光照、单目/双目标定

我个人习惯用9×6或12×9的棋盘格,内角点数量适中,计算量不大,精度也够。

3.2 圆点阵标定板

圆点阵是另一种常见选择,尤其适合大视场角或鱼眼相机

  • 优点:圆心检测对畸变不敏感,部分遮挡也能工作
  • 缺点:透视投影下圆会变成椭圆,圆心定位有偏差(需要椭圆拟合补偿)
  • 适用场景:鱼眼相机、大广角镜头、户外标定

我的经验:如果你用圆点阵,记得把标定板打印成非对称圆点阵(比如11×4,但圆点错开排列)。这样OpenCV的findCirclesGrid()能自动确定方向,不用手动指定。

3.3 两种标定板的对比

特性 棋盘格 圆点阵
角点/圆心精度 亚像素级(高) 亚像素级(中)
抗畸变能力 弱(角点偏移明显) 强(圆心偏移小)
部分遮挡鲁棒性 较好
检测速度 中等
OpenCV支持 非常成熟 成熟

四、标定流程概述

标定流程其实不复杂,但细节决定成败。我把它拆成5步:

  1. 准备标定板:打印棋盘格或圆点阵,贴在平整的硬板上。尺寸要量准,误差控制在0.1mm以内。
  2. 采集图像:从不同角度拍摄15-30张标定板图像。注意:覆盖整个视场(四个角、中心、倾斜、旋转)。
  3. 检测特征点:用OpenCV的findChessboardCorners()findCirclesGrid()提取角点/圆心。
  4. 计算参数:调用calibrateCamera(),输入世界坐标和像素坐标,得到内参、畸变系数、外参。
  5. 评估结果:看重投影误差(小于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,手把手带你跑通第一个标定程序。到时候你会发现,原来标定也可以这么简单。

嗯,今天就到这里。有问题欢迎在评论区交流,咱们下期见。