4. 相机标定原理与实战:相机成像模型、内参外参、张正友标定法、使用OpenCV进行标定

各位同学,欢迎来到第四章。这一章我们聊相机标定。

说实话,相机标定是视觉抓取里最基础、也最容易被忽视的一环。我见过不少项目,算法调得再好,标定没做好,抓取精度就是上不去。说白了,相机标定就是给相机“配眼镜”,让它看清真实世界的尺寸和位置。

4.1 相机成像模型:小孔成像的数学表达

先说说相机是怎么看到东西的。本质上,它就是个高级版的小孔成像。

光线从物体反射,穿过镜头中心(光心),投射到感光芯片上。这个过程中,三维世界里的一个点,被映射到了二维图像上。

数学上,我们用四个坐标系来描述这个过程:

  • 世界坐标系:你放工件的地方,单位是毫米。
  • 相机坐标系:以相机光心为原点,Z轴朝前。
  • 图像坐标系:在成像平面上,单位还是毫米。
  • 像素坐标系:最终你看到的图像,单位是像素。

从世界坐标到像素坐标,经历了两步变换:

  1. 刚体变换(外参):世界坐标 → 相机坐标。旋转+平移。
  2. 透视投影 + 像素缩放(内参):相机坐标 → 像素坐标。

公式长这样(别怕,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 张正友标定法:用棋盘格搞定一切

张正友标定法是目前最主流的标定方法。为什么?因为它只需要你打印一张棋盘格,拍几张照片就行。

核心思路是这样的:

  1. 棋盘格上的角点,世界坐标是已知的(比如格子边长30mm)。
  2. 从不同角度拍棋盘格,提取图像中的角点像素坐标。
  3. 利用多张照片的对应关系,解算出内参和外参。

你想想看,这其实是个优化问题。我们让重投影误差最小——就是把算出来的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 知识体系总览

下面这张图,帮你理清本章的核心逻辑:

相机标定知识体系 相机标定 成像模型 小孔成像原理 四个坐标系变换 世界→相机→图像→像素 透视投影公式 内参 & 外参 内参:fx, fy, cx, cy 畸变系数:k1, k2, p1, p2 外参:旋转R + 平移t 描述相机位姿 张正友标定法 棋盘格角点检测 多角度拍摄 最小化重投影误差 OpenCV实现 输出:内参矩阵 + 畸变系数 + 外参

从这张图可以看得很清楚:成像模型是理论基础,内参外参是我们要解算的目标,张正友标定法就是求解的工具。三者缺一不可。

4.7 标定后的注意事项

标定完,别急着部署到产线。我一般会做两件事:

  • 保存标定文件:用np.savez保存mtx和dist,下次直接加载。
  • 现场验证:放一个已知尺寸的工件,用标定后的参数测量一下,看误差是否在容忍范围内。

另外,如果环境温度变化大(比如冬天和夏天),镜头热胀冷缩,内参可能会漂移。我建议每季度重新标定一次。

好了,这一章的内容就到这里。标定是视觉抓取的基石,基础打牢了,后面的手眼标定、抓取位姿计算才能准确。动手试试吧,拍一组棋盘格照片,跑一遍代码,看看你的相机内参是多少。

核心总结:相机标定 = 求解内参 + 外参 + 畸变系数。张正友标定法用棋盘格搞定这一切。OpenCV的calibrateCamera函数是核心工具。重投影误差小于0.5像素才算合格。


专注资料整理