4、相机标定与畸变校正:相机内参、外参、畸变模型、张正友标定法、OpenCV标定实践
相机标定,说白了就是给相机做「体检」。
你想想看,我们拿相机去拍物体,目的是要算出它真实的尺寸。但相机镜头天生就有畸变,成像平面也不是完美的。如果不做标定,你测出来的尺寸就是「虚的」。
我个人习惯,拿到一个新相机,第一件事就是标定。哪怕厂家给了参数,我也要自己跑一遍。为什么?因为每台相机都有个体差异,流水线上出来的东西,不可能一模一样。
4.1 相机内参:相机自己的「基因」
内参描述的是相机内部的光学结构。它包含四个核心参数:
- fx, fy:焦距在像素坐标系下的尺度。说白了,就是镜头把真实世界「拉近」了多少倍。
- cx, cy:主点坐标。也就是光轴穿过成像平面的那个点,理论上应该在图像正中心,但实际总有偏移。
- 畸变系数:k1, k2, k3(径向畸变),p1, p2(切向畸变)。
内参矩阵通常写成这样:
K = [[fx, 0, cx],
[ 0, fy, cy],
[ 0, 0, 1]]
我在项目中遇到过一台工业相机,标定出来的cx偏了20多个像素。一开始以为是标定板没放好,反复标了三次,结果都一样。后来拆开镜头一看,原来是CMOS传感器装歪了。嗯,这就是内参的意义——它告诉你相机真实的「底子」是什么样的。
4.2 相机外参:相机在空间中的「位置和姿态」
外参描述的是相机坐标系相对于世界坐标系的旋转和平移。说白了,就是相机放在哪里、朝哪个方向看。
外参包含:
- 旋转矩阵 R:3x3 的正交矩阵,描述三个轴的旋转。
- 平移向量 t:3x1 的向量,描述相机在世界坐标系中的位置。
为什么外参重要?因为你要把世界坐标系中的点,投影到相机坐标系中。公式很简单:
P_cam = R * P_world + t
我曾经犯过一个低级错误:把旋转矩阵和平移向量的顺序搞反了。结果算出来的三维坐标全部是错的,排查了整整一个下午。后来我养成了一个习惯——每次标定完,先打印出外参看一眼,R矩阵的行列式是不是接近1,t向量的量级是不是合理。这能帮你快速发现标定是否失败。
4.3 畸变模型:镜头为什么「骗人」
镜头畸变主要分两种:
4.3.1 径向畸变
这是最常见的畸变。说白了,就是镜头边缘的放大率跟中心不一样。拍出来的直线会变成曲线。
- 桶形畸变:图像向外凸出,像水桶。常见于广角镜头。
- 枕形畸变:图像向内凹陷,像枕头。常见于长焦镜头。
径向畸变的数学模型:
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)
其中 r 是像素到主点的距离。k1, k2, k3 就是我们要标定的径向畸变系数。
4.3.2 切向畸变
这个是因为镜头和CMOS传感器没有完全平行安装造成的。说白了,就是镜头「歪了」。
切向畸变的数学模型:
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]
4.4 张正友标定法:最经典的「棋盘格标定」
张正友标定法,是1998年张正友教授提出的。它为什么经典?因为只需要拍几张棋盘格照片,就能标定出全部内参和外参。不需要昂贵的精密设备,一张打印的棋盘格就够了。
核心思路是这样的:
- 拍摄多张不同角度的棋盘格图像(一般10-20张)。
- 检测每张图像中的角点坐标。
- 利用棋盘格已知的物理尺寸(比如每个格子30mm),建立世界坐标系到像素坐标系的映射关系。
- 通过最小二乘法求解内参和外参的初始值。
- 用非线性优化(比如Levenberg-Marquardt算法)对所有参数进行精调。
为什么需要多张不同角度的照片?因为单张照片只能解出部分参数,多张不同角度的照片才能把内参和外参「解耦」开。我一般会拍15张左右,覆盖相机的四个角落和中心区域。
4.5 OpenCV标定实践:手把手教你跑通
下面是我常用的标定流程,用OpenCV实现。代码不长,但每一步都有讲究。
4.5.1 准备标定板
打印一张棋盘格,贴在硬纸板上。格子数量建议用 9x6 或 10x7(内角点数量)。每个格子的物理尺寸要精确测量,比如 30mm x 30mm。
4.5.2 拍摄标定图像
用相机从不同角度拍摄棋盘格。注意:
- 光照要均匀,不要有反光。
- 棋盘格要清晰,不能模糊。
- 角度变化要大:俯仰、偏航、旋转都要有。
- 距离也要变化:近景、中景、远景各拍几张。
4.5.3 核心代码
import cv2
import numpy as np
import glob
# 棋盘格内角点数量(列,行)
pattern_size = (9, 6)
# 每个格子的物理尺寸(单位:mm)
square_size = 30.0
# 准备世界坐标系中的点
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 = [] # 图像坐标系中的点
images = glob.glob('calib_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 查找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
objpoints.append(objp)
# 亚像素精度优化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
imgpoints.append(corners2)
# 绘制角点并显示
cv2.drawChessboardCorners(img, pattern_size, corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)
4.5.4 评估标定质量
标定完不能直接收工。我一般会做两件事:
- 看重投影误差:ret 值越小越好。一般小于0.5像素就算不错,小于0.3像素就是优秀。
- 做畸变校正测试:用标定结果对一张新图像做畸变校正,看看直线是不是真的变直了。
# 畸变校正
img = cv2.imread('test_image.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 裁剪图像(去掉校正后产生的黑边)
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv2.imshow('original', img)
cv2.imshow('undistorted', dst)
cv2.waitKey(0)
4.6 知识体系总览
下面这张图,是我自己总结的相机标定知识体系。你可以把它当作一个「导航图」,每次做标定前看一眼,不容易漏掉步骤。
这张图把标定的四个核心模块串起来了。内参和外参是「标定什么」,畸变模型是「为什么要校正」,张正友法是「怎么标定」,OpenCV实践是「怎么实现」。你每次做标定,都可以沿着这个流程走一遍。
公众号:蓝海资料掘金营,微信deep3321