4. 相机内参标定:张正友标定法原理、棋盘格角点检测、内参矩阵与畸变系数的计算

相机标定,说白了就是给相机做一次「视力检查」。

你想想看,深度相机和彩色相机要融合,首先得知道它们各自看世界的方式。每台相机的镜头都有点小脾气——有的画面边缘会扭曲,有的焦距不太准。这些偏差如果不修正,后面做对齐融合就是白费功夫。

我个人习惯用张正友标定法。为什么?因为它只需要打印一张棋盘格,拍几张照片就能搞定。我在项目中遇到过不少标定方法,有的太复杂,有的精度不够。张正友的方法,嗯,是工业界公认的「黄金标准」。

4.1 张正友标定法的核心思想

张正友标定法,本质上是在做一件事:建立3D世界点和2D图像点之间的映射关系

棋盘格上的角点,位置是已知的。相机拍到的角点,位置是测量出来的。通过这两组点之间的对应关系,就能反推出相机的内部参数。

为什么会这样?因为棋盘格是平面,它的几何结构简单。我们只需要知道棋盘格上每个格子的物理尺寸(比如每个格子是30mm×30mm),就能算出所有角点的3D坐标。然后和图像上检测到的2D坐标做匹配,解方程,参数就出来了。

核心公式(简化版):

s · m = K · [R|t] · M

其中:

  • M 是棋盘格上的3D点
  • m 是图像上的2D点
  • K 是内参矩阵(我们想求的)
  • [R|t] 是外参(相机相对于棋盘格的位姿)
  • s 是尺度因子

说白了,张正友的贡献在于:他证明了只需要一个平面棋盘格,就能把内参和外参解耦出来。这在当时是个大突破。

4.2 棋盘格角点检测

角点检测是标定的第一步,也是最容易出问题的一步。

我记得第一次做标定时,拍了20张照片,结果角点检测只成功了3张。后来才发现是光照不均匀导致的。

检测流程:

  1. 图像预处理:转灰度图,做直方图均衡化
  2. 粗定位:用Harris角点检测器找到候选角点
  3. 亚像素精化:在粗定位附近做亚像素级别的优化
  4. 排序与匹配:按棋盘格的行列顺序排列角点

我的经验:

拍摄棋盘格时,尽量让棋盘格占画面面积的1/3到1/2。太小了角点检测不准,太大了容易超出视野。另外,棋盘格最好贴在硬纸板上,不要有褶皱。我曾经用一张皱巴巴的打印纸做标定,结果畸变系数算出来全是错的。

OpenCV里提供了现成的函数:

import cv2
import numpy as np

# 棋盘格尺寸(内角点数量)
pattern_size = (9, 6)  # 9列6行内角点

# 准备3D点
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)

# 存储所有图像的点
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:
        # 亚像素精化
        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
        corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
        
        objpoints.append(objp)
        imgpoints.append(corners2)
        
        # 可视化
        cv2.drawChessboardCorners(img, pattern_size, corners2, ret)

注意:

棋盘格的内角点数量是 (列数-1) × (行数-1)。比如9×10的棋盘格,内角点是8×9。我见过有人搞混这个,结果标定出来的参数完全不能用。

4.3 内参矩阵的计算

内参矩阵K,包含了相机的焦距和主点位置:

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

其中:

  • fx, fy:x和y方向的焦距(单位是像素)
  • cx, cy:主点坐标(通常是图像中心附近)

计算过程是这样的:

  1. 用所有检测到的角点,建立超定方程组
  2. 用最小二乘法求解
  3. 再用最大似然估计做一次优化

你想想看,每张照片提供了几十个角点,20张照片就是上千个方程。解这个方程组,内参就出来了。

OpenCV里一句话搞定:

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

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

4.4 畸变系数的计算

畸变主要分两种:

畸变类型 表现 参数
径向畸变 画面边缘的直线变弯(桶形/枕形) k1, k2, k3
切向畸变 镜头和成像平面不平行 p1, p2

畸变矫正的数学公式:

x_corrected = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + 2*p1*x*y + p2*(r^2 + 2*x^2)
y_corrected = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + p1*(r^2 + 2*y^2) + 2*p2*x*y

其中 r 是像素点到主点的距离。

避坑指南:

我曾经只用了5张照片做标定,结果畸变系数k3算出来是负数,矫正后画面反而更扭曲了。后来才知道,照片数量至少要10-15张,而且要有不同的角度和距离。特别是棋盘格要出现在画面的四个角落,这样才能准确估计边缘的畸变。

4.5 标定结果验证

标定完别急着用,先验证一下:

# 计算重投影误差
mean_error = 0
for i in range(len(objpoints)):
    imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
    error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
    mean_error += error

print("平均重投影误差:", mean_error / len(objpoints))

重投影误差小于0.5像素,说明标定质量不错。如果超过1个像素,建议重新拍一组照片。

我的习惯:

标定完成后,我会拍一张棋盘格放在画面正中央的照片,用标定结果做畸变矫正。如果矫正后的棋盘格边缘是笔直的直线,说明标定没问题。如果还有弯曲,嗯,那就得重新来了。

4.6 本章知识体系

下面这张图,把整个标定流程串起来了:

相机内参标定流程 步骤1:准备棋盘格 打印并贴在硬板上 步骤2:拍摄多张照片 10-20张不同角度 步骤3:角点检测 亚像素精化 步骤4:标定计算 求解内参矩阵K和畸变系数 步骤5:验证标定结果 重投影误差 < 0.5像素 步骤6:输出参数 内参矩阵 + 畸变系数 ⚠ 常见问题与处理 角点检测失败 → 调整光照/增加对比度 重投影误差大 → 增加照片数量/重新拍摄 失败 重拍 标定完成后,保存内参矩阵和畸变系数,用于后续的深度图与彩色图对齐融合

这张图把整个流程串起来了。从准备棋盘格开始,到拍摄、检测、计算、验证,最后输出参数。如果中间出问题,就回到拍摄步骤重新来。

标定这件事,说白了就是「慢工出细活」。我见过有人图省事,只拍5张照片就标定,结果后面做融合时发现误差大得离谱。嗯,标定这一步省下来的时间,后面要花十倍的时间去填坑。

总结一下:

  • 张正友标定法用棋盘格做标定,简单可靠
  • 角点检测要亚像素精化,精度才能上去
  • 内参矩阵包含焦距和主点,畸变系数分径向和切向
  • 标定完一定要验证,重投影误差小于0.5像素才算合格

好了,内参标定这块就讲到这里。下一节我们会聊深度图和彩色图的外参标定,也就是怎么把两台相机的位置关系算出来。到时候见。


专注资料整理