2. 相机标定基础:针孔模型、内参矩阵、畸变模型
做3D视觉抓取,第一步就是把相机“伺候”好。说白了,相机标定就是搞清楚两件事:光是怎么进来的,以及进来的光歪了多少。
我刚开始接触这个领域时,总觉得标定就是跑个脚本、打印个棋盘格就完事了。直到有一次在产线上,抓取精度死活差2毫米,查了三天才发现是畸变参数没给对。嗯,从那以后我再也不敢小看这一步了。
2.1 针孔模型——相机在“看”什么
先聊最基础的成像模型。你想想看,相机本质上就是一个黑盒子,前面开个小孔,光线穿过小孔在后面的感光元件上成像。这就是针孔模型。
数学上怎么描述这个过程?其实就三步:
- 世界坐标系里有个点 P(X, Y, Z)
- 光线穿过光心(小孔),投影到成像平面
- 成像平面上的物理坐标,再映射到像素坐标
公式长这样:
λ * [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 标定流程——从棋盘格到参数
标定的核心思路:拍一张已知图案(棋盘格),检测角点,然后解方程求内参和畸变。
标准流程:
- 打印棋盘格,贴在平整的板上
- 从不同角度拍15-20张照片
- 检测每张图的角点坐标
- 用张正友标定法求解内参和畸变
- 验证重投影误差
我用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 知识结构总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作一个快速索引:
2.6 标定验证——别信一次结果
标定完不是就完事了。我每次都会做两件事:
- 重投影误差检查:把标定板角点投影回图像,算平均误差。小于0.3像素算优秀,0.5以内可接受。
- 实际测量验证:放一个已知尺寸的物体,用标定结果去测,看偏差是否在容忍范围内。
有一次标定结果重投影误差只有0.2像素,但实际测量差了1毫米。后来发现是标定板没贴平,有微小翘曲。所以实际验证比理论误差更重要。
总结一下:针孔模型是基础,内参矩阵是核心,畸变模型是补偿。三者合在一起,才能把相机从“看”变成“量”。标定不是一次性的,每次硬件变动都要重新做。记住这个原则,后面3D抓取点的计算才不会跑偏。