4. 相机标定:内参标定原理、棋盘格标定流程、手眼标定

相机标定,说白了就是给机器人的眼睛配一副准确的眼镜。你想想看,机械臂要抓一个杯子,如果相机看到的坐标和实际物理坐标对不上,那抓取精度肯定一塌糊涂。我在项目里见过太多因为标定马虎导致抓取失败的案例了。

这一章我们重点解决两个问题:相机自己怎么看世界(内参标定),以及相机和机械臂怎么对齐坐标系(手眼标定)。

4.1 内参标定原理

相机内参,描述的是光线从三维世界投影到二维图像传感器的过程。核心参数就这几个:

参数 符号 含义
焦距 fx, fy 镜头到成像平面的距离,单位像素
主点 cx, cy 光轴与成像平面的交点,一般在图像中心附近
畸变系数 k1, k2, p1, p2, k3 径向畸变和切向畸变的矫正参数

畸变这东西,我刚开始做的时候没太在意。结果有一次在产线上,机械臂抓取边缘的工件总是偏几毫米。查了半天,发现是镜头边缘畸变没矫正。嗯,从那以后我再也不敢跳过畸变标定了。

内参标定的数学本质,就是求解一个3x3的矩阵加上畸变向量。常用的方法是张正友标定法,用棋盘格作为已知尺寸的平面靶标。

4.2 棋盘格标定流程

棋盘格标定,我个人习惯用OpenCV的cv2.calibrateCamera()。流程其实不复杂,但有几个坑要注意。

  1. 打印棋盘格:用A4纸打印,贴在硬纸板上。格子大小要量准,我一般用30mm或40mm的方格。
  2. 采集图像:从不同角度拍15-20张。记住,棋盘格要占画面1/3以上,角度要覆盖俯仰、偏航、旋转。
  3. 提取角点:用cv2.findChessboardCorners()。如果光线不好,可以调低阈值或者用亚像素细化。
  4. 标定计算:传入世界坐标和图像坐标,调用calibrateCamera。
  5. 评估结果:看重投影误差,一般小于0.5像素就算合格。
我的小技巧:采集图像时,让棋盘格在画面四个角和中心都出现几次。这样畸变参数估计更准。我曾经偷懒只拍了10张,结果标定出来的fx和fy差了2%,后来老老实实补到20张。

代码示例,我常用的标定片段:

import cv2
import numpy as np

# 棋盘格尺寸,内角点数量
pattern_size = (9, 6)  # 9x6个内角点
square_size = 30.0     # 方格边长30mm

# 准备世界坐标
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

obj_points = []  # 世界坐标
img_points = []  # 图像坐标

# 遍历图像
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:
        obj_points.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)
        img_points.append(corners2)

# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    obj_points, img_points, gray.shape[::-1], None, None)

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)
避坑指南:我曾经遇到过标定结果重投影误差0.1像素,但实际测量偏差很大。后来发现是棋盘格没贴平,有轻微弯曲。所以,靶标平面度比精度更重要。建议用玻璃板或亚克力板垫着。

4.3 手眼标定

内参标定解决的是相机自己的问题。手眼标定解决的是相机坐标系和机械臂基坐标系之间的变换关系。这里有两种主流配置:

配置 相机安装位置 求解方程 适用场景
Eye-to-Hand 固定在机械臂外部(如支架上) AX = XB 大范围、固定工位
Eye-in-Hand 固定在机械臂末端(随臂移动) AX = XB 近距离、高精度、灵活

有意思的是,两种配置的数学形式都是AX = XB。A是机械臂末端到基座的变换,B是相机到标定板的变换,X就是我们要找的手眼矩阵。

我个人更常用Eye-in-Hand。为什么?因为相机跟着机械臂走,可以凑近看工件细节。但要注意,标定板必须固定在机械臂工作空间内不动。

4.4 手眼标定流程(以Eye-in-Hand为例)

  1. 固定标定板:放在机械臂工作空间内,确保相机在各个角度都能看到。
  2. 采集数据对:移动机械臂到15-20个不同位姿,记录每个位姿下:
    • 机械臂末端位姿(从控制器读取)
    • 相机拍摄标定板的位姿(用solvePnP计算)
  3. 求解AX=XB:用Tsai法或Park法求解。
  4. 验证:用一组新数据验证,看重投影误差。

代码实现,我习惯用OpenCV的cv2.calibrateHandEye():

import cv2
import numpy as np

# 假设已经采集了N组数据
# R_gripper2base, t_gripper2base: 机械臂末端到基座的旋转和平移
# R_target2cam, t_target2cam: 标定板到相机的旋转和平移

R_gripper2base = np.array([...])  # Nx3x3
t_gripper2base = np.array([...])  # Nx3
R_target2cam = np.array([...])    # Nx3x3
t_target2cam = np.array([...])    # Nx3

# 手眼标定
R_cam2gripper, t_cam2gripper = cv2.calibrateHandEye(
    R_gripper2base, t_gripper2base,
    R_target2cam, t_target2cam,
    method=cv2.CALIB_HAND_EYE_TSAI
)

print("相机到机械臂末端的旋转矩阵:\n", R_cam2gripper)
print("相机到机械臂末端的平移向量:\n", t_cam2gripper)
关键点:手眼标定的精度,很大程度上取决于数据采集的质量。我建议:
  • 机械臂位姿要多样化,不要只在一个小范围内移动
  • 每个位姿下,标定板要清晰可见,角点提取准确
  • 至少采集15组数据,20组更稳妥

下面这张图展示了Eye-in-Hand和Eye-to-Hand两种配置的坐标系关系:

手眼标定两种配置对比 Eye-in-Hand 基座 末端 相机 标定板 相机固定在末端,标定板固定在工作空间 Eye-to-Hand 基座 末端 标定板 相机 相机固定在工作空间,标定板固定在末端
我的经验:如果你做的是高精度抓取(比如装配),我建议用Eye-in-Hand。因为相机离工件近,图像分辨率高,定位误差小。但如果你做的是大范围分拣(比如从传送带上抓取),Eye-to-Hand更合适,视野大,不用频繁移动机械臂。

4.5 标定结果验证

标定完不是就完事了。我每次都会做一步验证:

  • 内参验证:用标定好的相机拍一张已知尺寸的物体,测量像素距离换算成物理距离,看误差。
  • 手眼验证:让机械臂末端去碰一个固定点,用相机定位该点,看两者坐标是否一致。

验证通过后,把内参矩阵和手眼矩阵保存成YAML或JSON文件,后续部署时直接加载。嗯,这一步千万别省,我见过有人标定完直接上产线,结果抓偏了才发现标定数据没保存。

重要提醒:标定不是一劳永逸的。如果相机被碰过、镜头焦距调过、或者机械臂重新安装过,都需要重新标定。我建议在产线维护计划中,每季度做一次标定验证。