2. 相机标定基础:针孔模型、内参矩阵、畸变模型

做3D视觉抓取,第一步就是把相机“伺候”好。说白了,相机标定就是搞清楚两件事:光是怎么进来的,以及进来的光歪了多少。

我刚开始接触这个领域时,总觉得标定就是跑个脚本、打印个棋盘格就完事了。直到有一次在产线上,抓取精度死活差2毫米,查了三天才发现是畸变参数没给对。嗯,从那以后我再也不敢小看这一步了。

2.1 针孔模型——相机在“看”什么

先聊最基础的成像模型。你想想看,相机本质上就是一个黑盒子,前面开个小孔,光线穿过小孔在后面的感光元件上成像。这就是针孔模型。

数学上怎么描述这个过程?其实就三步:

  1. 世界坐标系里有个点 P(X, Y, Z)
  2. 光线穿过光心(小孔),投影到成像平面
  3. 成像平面上的物理坐标,再映射到像素坐标

公式长这样:

λ * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T

别被符号吓到。λ是个缩放因子,K是内参矩阵,[R|t]是外参。后面我们会一个个拆开讲。

核心理解:针孔模型假设光线是直线传播的,没有畸变。但现实中的镜头都有畸变,所以我们需要在标定中补偿它。

2.2 内参矩阵——相机的“身份证”

内参矩阵K,我习惯叫它相机的“身份证”。它记录了相机内部固定的几个参数:

参数 符号 含义 单位
焦距 fx, fy 光心到成像平面的距离,x和y方向可能不同 像素
主点偏移 cx, cy 光轴与成像平面交点的像素坐标 像素
倾斜因子 s 像素坐标系x轴和y轴不垂直时的修正 无量纲

内参矩阵的完整形式:

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

我在项目中遇到过一个问题:用同一个相机,换了不同分辨率模式,内参居然变了。为什么?因为fx = f / dx,其中dx是像素物理尺寸。分辨率变了,dx就变了,内参自然要重新标定。这个坑我踩过,提醒你注意。

个人经验:每次更换相机分辨率、裁剪ROI、或者换镜头后,必须重新标定内参。别偷懒,否则后面所有3D计算都是错的。

2.3 畸变模型——镜头不是完美的

现实中的镜头,尤其是广角镜头,成像会有畸变。主要分两类:

  • 径向畸变:光线通过镜头边缘时弯曲得更厉害,导致直线变弯。分为桶形畸变和枕形畸变。
  • 切向畸变:镜头和成像平面不平行,导致图像倾斜。

畸变模型用多项式来描述:

径向畸变:
x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)

切向畸变:
x_distorted = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_distorted = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]

其中r是点到光心的距离,k1、k2、k3是径向畸变系数,p1、p2是切向畸变系数。

避坑指南:我曾经在标定一个鱼眼镜头时,只用了k1和k2两个参数,结果边缘畸变校正后还是歪的。后来加上k3才搞定。对于大畸变镜头,至少用3个径向畸变参数。

2.4 标定流程——从棋盘格到参数

标定的核心思路:拍一张已知图案(棋盘格),检测角点,然后解方程求内参和畸变。

标准流程:

  1. 打印棋盘格,贴在平整的板上
  2. 从不同角度拍15-20张照片
  3. 检测每张图的角点坐标
  4. 用张正友标定法求解内参和畸变
  5. 验证重投影误差

我用OpenCV实现过很多次,核心代码就这几行:

import cv2
import numpy as np

# 准备棋盘格角点坐标
pattern_size = (9, 6)  # 内角点数量
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 = []  # 世界坐标
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)

我建议:标定完成后,一定要算一下重投影误差。如果误差大于0.5像素,说明标定质量不够好,需要重新拍图。误差来源通常是棋盘格不平整、照片数量不够、或者角度变化不够大。

2.5 知识结构总览

下面这张图把本章的核心逻辑串起来了。你可以把它当作一个快速索引:

相机标定知识结构图 相机标定 针孔模型 内参矩阵 K 畸变模型 光线直线传播假设 世界→相机→像素 fx, fy, cx, cy, s 分辨率变化需重标 径向畸变 k1,k2,k3 切向畸变 p1,p2 标定流程:拍图→角点检测→求解→验证 核心原则 重投影误差 < 0.5 像素 | 15-20 张不同角度照片 | 每次硬件变更后重新标定

2.6 标定验证——别信一次结果

标定完不是就完事了。我每次都会做两件事:

  • 重投影误差检查:把标定板角点投影回图像,算平均误差。小于0.3像素算优秀,0.5以内可接受。
  • 实际测量验证:放一个已知尺寸的物体,用标定结果去测,看偏差是否在容忍范围内。

有一次标定结果重投影误差只有0.2像素,但实际测量差了1毫米。后来发现是标定板没贴平,有微小翘曲。所以实际验证比理论误差更重要。

总结一下:针孔模型是基础,内参矩阵是核心,畸变模型是补偿。三者合在一起,才能把相机从“看”变成“量”。标定不是一次性的,每次硬件变动都要重新做。记住这个原则,后面3D抓取点的计算才不会跑偏。