第2章 相机内参标定:张正友标定法原理、棋盘格角点检测、内参矩阵与畸变系数求解

2.1 张正友标定法:为什么它成了行业标配?

说到相机内参标定,我第一个想到的就是张正友标定法。说实话,这个方法在视觉SLAM和自动驾驶领域,基本是「人手一份」的存在。为什么?因为它太实用了。

你想想看,传统的标定方法需要高精度的三维标定物,比如两个垂直的平面,或者一个已知尺寸的立方体。这些东西加工成本高,而且携带不方便。张正友的方法只需要一个打印的棋盘格,贴在平板上就能干活。我在项目初期,经常就是拿A4纸打印一张棋盘格,用双面胶贴在硬纸板上就开始标定了。

它的核心思想其实很简单:

  • 我们不知道棋盘格在空间中的具体位置(外参未知)
  • 但我们知道棋盘格上所有角点之间的相对位置(内参已知)
  • 通过多张不同角度的照片,利用约束关系求解内参

说白了,就是用「已知的平面几何关系」去反推「未知的相机参数」。我刚开始做标定时,总觉得这方法有点「绕」,后来亲手推导了一遍公式,才明白它的巧妙之处。

核心优势:只需要一个平面棋盘格,拍摄10-20张不同角度的图像,就能同时求解内参和外参。不需要昂贵的设备,不需要复杂的操作。

2.2 棋盘格角点检测:从图像到亚像素精度

角点检测是标定的第一步,也是最容易出问题的一步。我记得有一次,标定出来的内参矩阵总是「飘」,重投影误差高达0.8个像素。排查了半天,发现是棋盘格打印时边缘裁切不齐,导致角点检测偏差。

OpenCV里用的是cv::findChessboardCorners(),它的流程大致是这样的:

  1. 图像预处理:转灰度图,自适应阈值二值化
  2. 轮廓提取:找到棋盘格的黑白交界区域
  3. 四边形检测:筛选出符合棋盘格形状的四边形
  4. 角点定位:在四边形顶点处进行亚像素精化

这里有个坑,我踩过好几次——光照不均匀。如果棋盘格一半亮一半暗,自适应阈值可能失效。我的建议是:拍摄时尽量保证光照均匀,或者用漫反射光源。

个人经验:亚像素精化这一步很关键。OpenCV的cv::cornerSubPix()可以进一步提高角点精度。我一般设置迭代次数为30,终止误差为0.001像素。别小看这0.1像素的差异,标定结果能差不少。

2.3 内参矩阵:相机到底在「看」什么?

内参矩阵,说白了就是描述「三维世界中的点,是怎么映射到二维图像上的」。它的形式是3x3的矩阵:

K = [fx  0  cx
     0  fy  cy
     0   0   1]

其中:

  • fx、fy:焦距在x、y方向上的像素长度
  • cx、cy:主点坐标,通常是图像中心

我刚开始做标定时,一直有个疑问:为什么焦距要用像素来表示?后来想明白了——因为图像传感器上的像素是离散的,我们需要知道「一个毫米对应多少个像素」。这个比例因子,就是焦距的物理意义。

举个例子:假设你的相机传感器像素尺寸是3.45μm,焦距是8mm,那么fx = 8 / 0.00345 ≈ 2318。这个数值越大,说明相机「看得越远」,但视场角越小。

2.4 畸变系数:镜头不是完美的

没有镜头是完美的。我见过最夸张的畸变,是在一个广角镜头上,画面边缘的直线都弯成了弧形。畸变主要分两类:

畸变类型 数学表示 典型表现
径向畸变 k1, k2, k3 画面边缘的直线弯曲(桶形/枕形)
切向畸变 p1, p2 镜头与传感器不平行导致的倾斜

张正友标定法可以同时求解5个畸变系数(k1, k2, k3, p1, p2)。不过我个人建议,对于大多数应用场景,只标定k1, k2, p1, p2就够了。k3只有在鱼眼镜头或者畸变特别严重时才需要。

避坑指南:我曾经在标定一个广角相机时,把k3也加进去了,结果标定出来的畸变矫正图反而出现了「波浪形」扭曲。后来发现是数据量不够,k3过拟合了。记住:参数越多,需要的标定图像也越多。

2.5 标定流程:从拍摄到结果

我一般按这个步骤来:

  1. 准备棋盘格:打印一张7x10或8x11的棋盘格,贴在平板上
  2. 拍摄图像:手持棋盘格,在相机视场内变换姿态,拍摄15-20张
  3. 角点检测:用OpenCV的findChessboardCorners()检测所有角点
  4. 标定求解:调用calibrateCamera(),得到内参矩阵和畸变系数
  5. 评估结果:检查重投影误差,一般应小于0.5像素

这里有个小技巧:拍摄时尽量让棋盘格出现在画面的不同位置——左上、右下、中间、靠近边缘。这样标定出来的畸变系数会更准确。我见过有人只拍了10张,而且棋盘格都在画面中央,结果边缘的畸变完全没标定出来。

2.6 知识体系:一张图看懂

下面这张图,是我自己总结的标定知识体系。你可以把它当作一个「思维导图」来看:

相机内参标定 张正友标定法 棋盘格角点检测 内参矩阵与畸变 平面棋盘格 多角度拍摄 闭合解+非线性优化 图像预处理(灰度+二值化) 四边形检测与筛选 亚像素精化 内参矩阵 K (fx,fy,cx,cy) 径向畸变 (k1,k2,k3) 切向畸变 (p1,p2) 输出:内参矩阵 + 畸变系数 → 用于图像矫正与3D重建

2.7 代码实战:OpenCV标定示例

下面是我常用的标定代码片段。嗯,这里要注意:代码里的棋盘格尺寸(pattern_size)一定要和实际打印的一致,否则标定出来的焦距会偏大或偏小。

import cv2
import numpy as np

# 棋盘格尺寸(内角点数量)
pattern_size = (9, 6)  # 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

# 存储所有图像的点
obj_points = []  # 世界坐标系点
img_points = []  # 图像坐标系点

# 读取图像并检测角点
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:
        obj_points.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)
        img_points.append(corners2)

# 标定
ret, K, dist, rvecs, tvecs = cv2.calibrateCamera(
    obj_points, img_points, gray.shape[::-1], None, None
)

print("内参矩阵:\n", K)
print("畸变系数:\n", dist)
print("重投影误差:", ret)

我的习惯:标定完成后,我会用cv2.undistort()对一张测试图像做畸变矫正,看看棋盘格的直线是不是真的变直了。如果边缘还有弯曲,说明标定不够好,需要重新拍一组图像。

好了,这一章的内容就到这里。标定这件事,说难不难,说简单也不简单。关键是要理解背后的原理,然后多动手实践。你如果按照上面的步骤走一遍,应该能拿到不错的结果。

可可精品资料库