4. 相机标定原理与实战:相机成像模型、内参外参、张正友标定法、使用OpenCV进行标定
各位同学,欢迎来到第四章。这一章我们聊相机标定。
说实话,相机标定是视觉抓取里最基础、也最容易被忽视的一环。我见过不少项目,算法调得再好,标定没做好,抓取精度就是上不去。说白了,相机标定就是给相机“配眼镜”,让它看清真实世界的尺寸和位置。
4.1 相机成像模型:小孔成像的数学表达
先说说相机是怎么看到东西的。本质上,它就是个高级版的小孔成像。
光线从物体反射,穿过镜头中心(光心),投射到感光芯片上。这个过程中,三维世界里的一个点,被映射到了二维图像上。
数学上,我们用四个坐标系来描述这个过程:
- 世界坐标系:你放工件的地方,单位是毫米。
- 相机坐标系:以相机光心为原点,Z轴朝前。
- 图像坐标系:在成像平面上,单位还是毫米。
- 像素坐标系:最终你看到的图像,单位是像素。
从世界坐标到像素坐标,经历了两步变换:
- 刚体变换(外参):世界坐标 → 相机坐标。旋转+平移。
- 透视投影 + 像素缩放(内参):相机坐标 → 像素坐标。
公式长这样(别怕,OpenCV帮你算):
s * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T
其中K就是内参矩阵,[R|t]是外参矩阵。s是个缩放因子。
核心理解:内参是相机的“出厂设置”,外参是相机相对于世界的“摆放姿势”。标定的目的,就是求出这两个东西。
4.2 内参与外参:相机自己的“身份证”
内参矩阵K包含:
- fx, fy:焦距在x、y方向的像素当量
- cx, cy:主点坐标(光心在像素坐标系的位置)
- 畸变系数:k1, k2, p1, p2, k3(径向畸变和切向畸变)
外参矩阵[R|t]包含:
- R:旋转矩阵(3x3),描述相机朝向
- t:平移向量(3x1),描述相机位置
我在项目中遇到过一个问题:换了一个镜头,没重新标定,结果抓取偏差了2毫米。嗯,这就是内参变了。镜头拧松一点,内参都会变。所以每次拆装相机,我建议重新标定一次。
4.3 张正友标定法:用棋盘格搞定一切
张正友标定法是目前最主流的标定方法。为什么?因为它只需要你打印一张棋盘格,拍几张照片就行。
核心思路是这样的:
- 棋盘格上的角点,世界坐标是已知的(比如格子边长30mm)。
- 从不同角度拍棋盘格,提取图像中的角点像素坐标。
- 利用多张照片的对应关系,解算出内参和外参。
你想想看,这其实是个优化问题。我们让重投影误差最小——就是把算出来的3D点投影回图像,看和实际提取的像素点差多少。
个人经验:拍10-15张就够了。角度要覆盖:正对、左倾、右倾、上仰、下俯。千万别只拍正面的,那样解算不稳定。
4.4 使用OpenCV进行标定:手把手实战
好了,理论说完了,咱们直接上代码。我用的是Python + OpenCV。
第一步:准备棋盘格。打印一张8x6的棋盘格,每个格子30mm。注意,OpenCV的角点检测需要内角点数量,8x6的棋盘格内角点是7x5。
第二步:拍照。从不同角度拍15张左右。
第三步:写代码标定。
import cv2
import numpy as np
import glob
# 棋盘格参数
CHECKERBOARD = (7, 5) # 内角点数量
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 世界坐标系中的点
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
objp = objp * 30 # 格子边长30mm
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, CHECKERBOARD, None)
if ret:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
cv2.drawChessboardCorners(img, CHECKERBOARD, 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)
跑完这段代码,你就得到了内参矩阵和畸变系数。我建议把结果保存下来,下次直接用。
避坑指南:我曾经因为棋盘格没贴平,导致标定结果偏差很大。一定要把棋盘格贴在平整的硬板上,不要有褶皱。
4.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))
误差小于0.5像素,说明标定质量不错。如果大于1个像素,建议重新拍一组照片。
另外,我习惯用标定后的参数去矫正一张图,看看直线是不是变直了:
img = cv2.imread('test.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)
cv2.imwrite('calibrated.jpg', dst)
肉眼看一下,边缘的畸变是不是消失了。嗯,这一步很直观。
4.6 知识体系总览
下面这张图,帮你理清本章的核心逻辑:
从这张图可以看得很清楚:成像模型是理论基础,内参外参是我们要解算的目标,张正友标定法就是求解的工具。三者缺一不可。
4.7 标定后的注意事项
标定完,别急着部署到产线。我一般会做两件事:
- 保存标定文件:用np.savez保存mtx和dist,下次直接加载。
- 现场验证:放一个已知尺寸的工件,用标定后的参数测量一下,看误差是否在容忍范围内。
另外,如果环境温度变化大(比如冬天和夏天),镜头热胀冷缩,内参可能会漂移。我建议每季度重新标定一次。
好了,这一章的内容就到这里。标定是视觉抓取的基石,基础打牢了,后面的手眼标定、抓取位姿计算才能准确。动手试试吧,拍一组棋盘格照片,跑一遍代码,看看你的相机内参是多少。
核心总结:相机标定 = 求解内参 + 外参 + 畸变系数。张正友标定法用棋盘格搞定这一切。OpenCV的calibrateCamera函数是核心工具。重投影误差小于0.5像素才算合格。