3. 相机成像模型与标定:针孔模型、畸变校正、张正友标定法、相机内参矩阵

相机标定,说白了就是搞清楚相机到底是怎么「看」世界的。你想想看,一个三维空间里的点,怎么就能映射到二维图像上?中间经历了哪些变换?又引入了哪些误差?

我刚开始做视觉抓取时,总觉得标定是件麻烦事,随便用个默认参数就开干。结果呢?抓取精度惨不忍睹,机械臂总是差那么几毫米。后来我才明白——相机标定是整个视觉系统的地基,地基不稳,上层建筑全是空中楼阁。

3.1 针孔模型:最朴素的成像原理

针孔模型是最经典的相机成像模型。它假设光线沿直线传播,穿过一个小孔后在成像平面上形成倒立的像。

核心思想:三维世界中的点 P(X, Y, Z) 通过光心 O 投影到成像平面上的点 p(x, y)。

数学上,这个投影关系可以写成:

x = f * (X / Z)
y = f * (Y / Z)

其中 f 是焦距,也就是光心到成像平面的距离。

但实际相机不是针孔,而是用透镜来聚光。这就引出了一个问题——透镜会引入畸变。

3.2 畸变校正:让直线变回直线

实际相机拍出来的图像,边缘的直线往往是弯的。这就是畸变。畸变主要分两种:

畸变类型 表现 产生原因
径向畸变 直线向外或向内弯曲 透镜形状不完美
切向畸变 图像倾斜变形 透镜与成像平面不平行

我记得有一次在工厂调试,发现抓取精度总是偏,排查了半天才发现是相机畸变没校正。边缘的工件位置偏差达到了 5 个像素,换算成物理距离就是好几毫米。

畸变校正的数学模型是这样的:

// 径向畸变
x_corrected = x * (1 + k1*r^2 + k2*r^4 + k3*r^6)
y_corrected = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)

// 切向畸变
x_corrected = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_corrected = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]

其中 k1、k2、k3 是径向畸变系数,p1、p2 是切向畸变系数。这些系数就是我们要标定出来的。

我曾经踩过的坑:畸变系数不是越多越好。对于普通工业相机,用 k1、k2 两个径向系数就够了。加上 k3 反而容易过拟合,尤其是标定板图片不够多的时候。

3.3 相机内参矩阵:从世界到像素的「翻译官」

内参矩阵 K 是相机标定的核心输出。它描述了从相机坐标系到像素坐标系的映射关系。

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

其中:

  • fx, fy:焦距在 x 和 y 方向上的像素尺度
  • cx, cy:光心在像素坐标系中的位置

为什么 fx 和 fy 可能不一样?因为像素不一定是正方形的。我见过一些工业相机,fx 和 fy 能差 5% 以上。

完整的投影过程是这样的:

1. 世界坐标系 → 相机坐标系(外参变换)
2. 相机坐标系 → 归一化平面(针孔投影)
3. 归一化平面 → 像素坐标系(内参矩阵)

我的习惯:拿到一个新相机,第一件事就是标定内参。把 fx、fy、cx、cy 记下来,贴在相机上。这样换相机时能快速定位问题。

3.4 张正友标定法:最实用的标定方案

张正友标定法是目前最主流的相机标定方法。它的核心思路是:用一张棋盘格标定板,从不同角度拍 10-20 张图片,然后通过数学优化求解内参和畸变系数。

为什么是棋盘格?因为棋盘格的角点容易检测,而且角点之间的物理距离是已知的。

标定流程如下:

  1. 准备标定板:打印一张棋盘格,贴在平整的硬板上
  2. 采集图片:从不同角度拍 15-20 张,覆盖视野的各个区域
  3. 角点检测:用 OpenCV 的 findChessboardCorners 检测角点
  4. 求解内参:用张正友算法求解内参矩阵和畸变系数
  5. 优化:用非线性最小二乘法优化所有参数

关键点:标定板要占图像面积的 1/4 到 1/3,太小了角点检测不准,太大了容易超出视野。

OpenCV 实现代码:

import cv2
import numpy as np

# 准备标定板的角点坐标
pattern_size = (9, 6)  # 内角点数量
square_size = 0.025    # 方格边长,单位:米

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 = []  # 世界坐标系中的点
imgpoints = []  # 像素坐标系中的点

# 遍历图片
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)
        imgpoints.append(corners)

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

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

我曾经踩过的坑:标定板一定要平整!有一次我用了一张有点皱的打印纸,标定出来的内参偏差很大。后来换了亚克力板贴标定纸,效果立竿见影。

3.5 标定质量评估

标定完了怎么知道好不好?我一般看三个指标:

指标 好 一般 差
重投影误差 < 0.3 像素 0.3 - 0.8 像素 > 0.8 像素
图片覆盖 视野四角 + 中心 覆盖大部分区域 集中在中心
图片数量 15-20 张 10-15 张 < 10 张

重投影误差是最直观的指标。它表示:我们把检测到的角点通过标定结果反投影回三维空间,再投影回图像,看和原始角点差了多少像素。

我的习惯:标定完成后,我会用 cv2.projectPoints 把标定板的角点投影到图像上,画出来看看。如果投影点和检测点重合得很好,说明标定没问题。

3.6 本章知识体系

下面这张图总结了相机成像模型与标定的核心逻辑:

相机成像模型与标定知识体系 相机成像模型与标定知识体系 针孔成像模型 畸变模型 内参矩阵 投影关系 x = f*X/Z y = f*Y/Z 径向畸变 k1,k2,k3 切向畸变 p1,p2 校正公式 fx, fy, cx, cy 像素坐标系 归一化平面 张正友标定法 棋盘格角点检测 → 求解内参 → 非线性优化 标定输出:内参矩阵 + 畸变系数 标定质量评估:重投影误差 < 0.3 像素为优秀,0.3-0.8 为可用,> 0.8 需重新标定

嗯,到这里相机成像模型和标定的核心内容就讲完了。记住一句话:标定不是一次性工作,换镜头、调焦距、甚至温度变化都可能影响标定结果。我建议每次项目开始前都重新标定一次,花 10 分钟换来整个系统的精度保障,这笔账怎么算都划算。