3、相机标定基础:为什么要标定?标定板的选择与制作,张正友标定法原理简介。

说到双目立体视觉,很多人第一反应是“两个摄像头,拍两张图,算算视差,深度就出来了”。嗯,听起来挺简单,但真上手做的时候,你会发现——两个摄像头拍出来的图,根本对不上。

为什么会这样?

因为每个镜头都有畸变,两个相机的光轴也不完全平行,甚至它们的焦距、主点位置都不一样。你想想看,两个“歪”的相机,怎么可能算出准确的深度?

所以,在开始做立体匹配之前,我们必须先做一件事——相机标定

3.1 为什么要标定?

说白了,标定就是给相机“量尺寸、纠偏差”。

我刚开始做双目项目时,觉得标定这一步太麻烦,直接拿两个USB摄像头怼上去。结果呢?算出来的深度图全是噪点,连物体的轮廓都看不清。后来老老实实做了标定,效果立竿见影。

标定要解决的核心问题有三个:

  • 内参:焦距、主点、畸变系数。这些是相机本身的“出厂参数”,但实际会有偏差。
  • 外参:两个相机之间的旋转和平移关系。说白了,就是知道它们“谁在谁的什么位置”。
  • 畸变校正:镜头边缘的桶形或枕形畸变,必须去掉,否则立体匹配会出错。

一句话总结:没有标定的双目系统,就像两个近视眼没戴眼镜——看啥都是模糊的。

3.2 标定板的选择与制作

标定板,就是用来给相机“看”的参考物。常见的标定板有两种:棋盘格和圆点格。

类型 优点 缺点
棋盘格 角点检测稳定,算法成熟 需要完整的角点可见,遮挡时容易失败
圆点格 部分遮挡也能检测,适合大角度 圆心提取精度略低于角点

我个人习惯用棋盘格,因为OpenCV的findChessboardCorners函数太方便了。但要注意一点:棋盘格的格子数量必须是内角点数量,比如8x6的棋盘格,内角点是7x5。这个细节我见过不少人搞错。

制作标定板时,有几点经验:

  • 打印精度要够:用激光打印机,别用喷墨的,否则格子边缘模糊,角点检测会飘。
  • 贴在硬板上:软纸会弯曲,影响标定精度。我一般贴在亚克力板或铝板上。
  • 尺寸要合适:太小了相机看不清,太大了不好手持。A4纸大小,格子边长20-30mm比较合适。

小技巧:标定板不要做得太亮或太暗,中等灰度最好。太反光的材料(比如相纸)会引入高光,干扰角点检测。

3.3 张正友标定法原理简介

张正友标定法,是目前最主流的相机标定方法。为什么叫“张正友”?因为这是微软研究院的张正友博士在1998年提出的。这个方法牛在哪里?——只需要拍几张标定板的照片,就能算出相机的内参和外参

它的核心思路是这样的:

  1. 建立单应性矩阵:每张标定板照片,都对应一个从3D世界坐标到2D图像坐标的映射关系。这个关系用一个3x3的单应性矩阵H来表示。
  2. 利用约束求解内参:因为标定板是平面(Z=0),所以H矩阵中包含了内参信息。通过多张照片的H矩阵,可以联立方程解出内参。
  3. 考虑畸变:上面解出的内参是理想情况下的。实际镜头有畸变,所以还要用非线性优化(比如Levenberg-Marquardt算法)来修正畸变系数。

嗯,这里要注意:张正友标定法要求标定板是平面的。如果标定板弯了,那标定结果就废了。我曾经用一张海报纸做标定板,结果拍出来的角点全在一条曲线上,标定出来的畸变系数大得离谱——后来才发现是纸没贴平。

避坑指南:标定照片至少要拍10-15张,覆盖不同的角度和位置。只拍几张正面照,标定结果会很不稳定。我一般会拍20张左右,确保每个角落都覆盖到。

3.4 标定的核心流程

下面我用一个简单的流程图,把整个标定过程串起来:

拍摄标定板照片 检测角点/圆心 (findChessboardCorners) 计算单应性矩阵H (每张照片一个H) 求解内参矩阵K (利用H的约束) 求解畸变系数 (非线性优化) 输出标定结果 (内参+畸变+外参) 重投影误差过大?重新拍摄或调整参数 张正友标定法核心流程

这个流程看起来简单,但实际做的时候有几个坑:

  • 照片数量不够:少于10张,内参解出来可能不收敛。
  • 标定板角度太单一:全是正面照,畸变系数算不准。要拍一些倾斜的、旋转的。
  • 标定板离镜头太远:角点太小,检测精度下降。一般让标定板占画面1/3到1/2比较合适。

3.5 一个简单的标定代码示例

下面我用OpenCV演示一下标定的核心步骤。注意,这只是示意代码,实际项目中需要处理更多细节。

import cv2
import numpy as np

# 棋盘格内角点数量 (宽, 高)
pattern_size = (7, 5)
# 格子边长 (单位: mm)
square_size = 25.0

# 准备世界坐标系中的点
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点

# 读取图片并检测角点
images = ['calib1.jpg', 'calib2.jpg', ...]  # 你的标定图片列表
for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
    if ret:
        objpoints.append(objp)
        imgpoints.append(corners)

# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None
)

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)

个人经验:标定完成后,一定要检查重投影误差。如果误差大于0.5个像素,说明标定质量不够好。我一般会剔除误差最大的几张照片,重新标定一次。

3.6 标定结果怎么用?

标定完成后,我们得到的是:

  • 内参矩阵K:包含fx, fy, cx, cy
  • 畸变系数:k1, k2, p1, p2, k3(径向和切向畸变)
  • 外参:每个标定板照片对应的旋转向量和平移向量

对于双目系统,我们还需要额外做立体标定,也就是求出两个相机之间的旋转矩阵R和平移向量T。这个我们下一章会详细讲。

嗯,最后说一句:标定不是一次性的。如果相机被碰了、焦距调了、或者温度变化太大,都需要重新标定。我有个项目在户外跑了一天,中午太阳一晒,镜头热胀冷缩,标定参数就变了——从那以后,我每次开机都会先拍一张标定板检查一下。


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