3、相机成像原理:小孔成像模型、透镜畸变、相机内参矩阵、OpenCV相机标定实战
大家好,我是你们的老朋友。
这一章,我们来聊聊相机。说白了,相机就是自动驾驶的「眼睛」。你想想看,如果眼睛看不清东西,那后面的感知算法再牛也没用。所以,搞懂相机成像原理,是咱们做感知的第一步。
3.1 小孔成像模型:最朴素的相机
先别急着想那些复杂的镜头。最原始的相机,其实就是一个黑盒子,前面戳个小孔。
光线从物体上反射过来,穿过这个小孔,在后面的感光平面上形成一个倒立的像。这就是小孔成像。
这个模型虽然简单,但它奠定了所有相机模型的基础。我们常说的「针孔相机模型」,就是用它来抽象真实相机的。
核心公式:
小孔成像的几何关系,说白了就是相似三角形。
假设物体高度为 H,距离相机为 Z,成像高度为 h,焦距为 f,那么:
h / f = H / Z
这个公式,你会在后面无数次的代码里看到它的影子。
我个人习惯,在项目初期做方案评估时,会先用小孔模型快速估算一下:这个相机能不能看到远处的行人?视野够不够宽?嗯,这一步能帮你省掉很多后期才发现「选错相机」的麻烦。
3.2 透镜畸变:理想很丰满,现实很骨感
小孔模型很完美,但现实中的相机用的是透镜。透镜嘛,为了多进点光,就得把孔做大,但一做大,光线就「不听话」了。
这就引出了畸变问题。畸变主要分两种:径向畸变和切向畸变。
3.2.1 径向畸变
径向畸变,就是图像边缘的直线变弯了。你拍一张棋盘格,如果边缘的格子不是正方形,而是像鱼眼一样鼓出来,那就是径向畸变。
为什么会这样?因为光线穿过透镜边缘时,折射角度和中心不一样。离中心越远,变形越严重。
数学上,我们用多项式来修正它:
// 径向畸变修正公式
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)
其中,r 是像素点到图像中心的距离,k1、k2、k3 是畸变系数。
我的经验:
我在项目中遇到过,有些廉价摄像头,k1 和 k2 的值特别大。标定完一看,边缘的像素都快被「拉」到画面外面去了。这时候,我建议你直接换镜头,别想着靠算法硬扛。
3.2.2 切向畸变
切向畸变,说白了就是透镜和成像平面没装平行。镜头歪了,导致图像看起来像被「拧」了一下。
修正公式也不复杂:
// 切向畸变修正公式
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]
p1、p2 就是切向畸变系数。
避坑指南:
我曾经在调试一个车载摄像头时,发现标定出来的切向畸变系数特别大。排查了半天,结果是镜头支架的螺丝松了。嗯,硬件问题别总想着用软件修。
3.3 相机内参矩阵:从3D世界到2D像素的「翻译官」
好了,现在我们把小孔模型和畸变模型合在一起,就得到了相机内参矩阵。
内参矩阵 K 长这样:
K = [fx, 0, cx]
[ 0, fy, cy]
[ 0, 0, 1]
其中:
- fx, fy:焦距在 x 和 y 方向上的像素长度。说白了,就是镜头有多「放大」。
- cx, cy:光心在图像上的像素坐标。一般就是图像的中心点。
这个矩阵的作用,就是把相机坐标系下的一个 3D 点 (X, Y, Z),映射到图像上的像素坐标 (u, v)。
一句话总结:
内参矩阵,就是告诉你「现实世界中的一个点,到底落在图像传感器的哪个像素上」。
你想想看,如果没有这个矩阵,你连一个物体在图像里有多大都不知道,更别提测距了。
3.4 OpenCV相机标定实战:动手干吧
理论说完了,咱们来点实际的。用 OpenCV 做相机标定,其实就几步。
3.4.1 准备标定板
打印一张棋盘格,贴在硬纸板上。格子数量建议 9x6 或 8x6,格子大小要量准,比如 30mm x 30mm。
我的习惯:
我一般会打印两张不同大小的棋盘格,一张近距用,一张远距用。这样标定出来的内参,在不同距离下都准。
3.4.2 采集图像
拿着标定板,在相机前摆各种姿势:上下左右、倾斜、旋转。拍个 20-30 张就够了。
注意:标定板要占画面 1/4 以上,别拍得太小。
3.4.3 运行标定代码
OpenCV 提供了现成的函数,咱们直接调用就行。
import cv2
import numpy as np
# 准备棋盘格角点的世界坐标
objp = np.zeros((6*9, 3), np.float32)
objp[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2) * 30 # 30mm 格子大小
# 存储所有图像的世界坐标和像素坐标
objpoints = [] # 3D 点
imgpoints = [] # 2D 点
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 查找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, (9, 6), None)
if ret:
objpoints.append(objp)
imgpoints.append(corners)
# 可视化角点
cv2.drawChessboardCorners(img, (9, 6), corners, ret)
# 标定相机
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
3.4.4 评估标定结果
标定完,一定要看看重投影误差。OpenCV 会返回一个 ret 值,一般小于 0.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))
避坑指南:
我曾经标定完,发现重投影误差高达 2 个像素。排查了半天,原来是标定板没贴平,有褶皱。嗯,硬件细节决定成败。
3.5 本章小结
这一章,咱们从最朴素的小孔模型讲起,聊了透镜畸变,推导了内参矩阵,最后用 OpenCV 跑了一遍标定流程。
说白了,相机标定就是给相机「配眼镜」。配好了,后面的感知算法才能看得清、看得准。
下一章,咱们会聊聊「图像预处理」,也就是怎么把相机拍到的原始数据,变成算法能吃的「干净数据」。到时候见。
公众号:蓝海资料掘金营,微信deep3321