1. 相机标定与手眼标定基础
任务目标
本案例完成两项标定:相机内参标定(获取焦距、主点及畸变系数)与眼在手外(Eye-to-Hand)手眼标定(获取相机坐标系到机器人基坐标系的变换矩阵)。标定结果将用于后续视觉引导抓取中,将像素坐标转换为机器人基坐标系下的三维位置。
环境与依赖
- Python 3.8+,安装OpenCV(
pip install opencv-python opencv-contrib-python)与NumPy - 棋盘格标定板(如10×7内角点,方格边长30mm)
- 机器人需支持通过API读取当前末端位姿(本例以通用接口示意,实际使用时替换为对应机器人SDK调用)
实现方案
内参标定采用张正友法,采集15~20张不同位姿的棋盘格图像,提取角点后计算内参矩阵与畸变系数。手眼标定采用眼在手外构型,固定标定板于机器人末端,控制机器人移动多个位姿,在每个位姿下同时记录:①相机拍摄标定板得到的外参(标定板→相机);②机器人读取的末端→基座变换。利用AX=XB方程求解相机→基座的固定变换。
关键完整代码
import cv2
import numpy as np
import glob
# ========== 1. 相机内参标定 ==========
def calibrate_camera(img_paths, pattern_size=(7, 10), square_size=30.0):
"""img_paths: 棋盘格图像路径列表; pattern_size: 内角点数(宽,高)"""
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
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) * square_size
obj_points, img_points = [], []
for fname in img_paths:
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)
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)
return mtx, dist # 内参矩阵与畸变系数
# ========== 2. 眼在手外手眼标定 ==========
def hand_eye_calibration(robot_poses, cam_poses):
"""
robot_poses: 机器人末端→基座的变换列表, 每个为4x4矩阵 (从机器人API读取)
cam_poses: 标定板→相机的变换列表, 每个为4x4矩阵 (由cv2.solvePnP获得)
返回: 相机→基座的4x4变换矩阵
"""
R_gripper2base, t_gripper2base = [], []
R_target2cam, t_target2cam = [], []
for T_base_gripper, T_cam_target in zip(robot_poses, cam_poses):
# 机器人返回的是 末端→基座, 即 gripper→base
R_gripper2base.append(T_base_gripper[:3, :3])
t_gripper2base.append(T_base_gripper[:3, 3])
# 相机外参给出 标定板→相机, 即 target→cam
R_target2cam.append(T_cam_target[:3, :3])
t_target2cam.append(T_cam_target[:3, 3])
R_cam2base, t_cam2base = cv2.calibrateHandEye(
R_gripper2base, t_gripper2base, R_target2cam, t_target2cam, method=cv2.CALIB_HAND_EYE_TSAI)
T_cam2base = np.eye(4)
T_cam2base[:3, :3] = R_cam2base
T_cam2base[:3, 3] = t_cam2base.flatten()
return T_cam2base
# ========== 3. 采集标定板位姿(示例: 单帧处理) ==========
def get_cam_pose_from_image(img, mtx, dist, pattern_size=(7, 10), square_size=30.0):
"""从单张图像计算标定板→相机的变换矩阵"""
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if not ret:
return None
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) * square_size
ret, rvec, tvec = cv2.solvePnP(objp, corners, mtx, dist)
T_cam_target = np.eye(4)
R, _ = cv2.Rodrigues(rvec)
T_cam_target[:3, :3] = R
T_cam_target[:3, 3] = tvec.flatten()
return T_cam_target
# ========== 主流程 ==========
if __name__ == "__main__":
# --- 内参标定 ---
mtx, dist = calibrate_camera(glob.glob("calib_images/*.jpg"))
print("内参矩阵:\n", mtx)
print("畸变系数:", dist)
# --- 手眼标定数据采集(需在实际机器人上执行) ---
robot_poses = [] # 每个元素: 4x4矩阵, 末端→基座
cam_poses = [] # 每个元素: 4x4矩阵, 标定板→相机
# 示例: 假设已采集N个位姿(实际运行时循环采集)
# for i in range(15):
# img = cv2.imread(f"handeye_images/{i}.jpg")
# T_cam_target = get_cam_pose_from_image(img, mtx, dist)
# T_base_gripper = robot.get_pose() # 替换为实际机器人API
# if T_cam_target is not None:
# robot_poses.append(T_base_gripper)
# cam_poses.append(T_cam_target)
# --- 计算手眼变换 ---
# T_cam2base = hand_eye_calibration(robot_poses, cam_poses)
# print("相机→基座变换矩阵:\n", T_cam2base)
# np.save("cam2base.npy", T_cam2base) # 保存结果供后续使用
运行与验证
- 内参标定:准备15~20张不同角度拍摄的棋盘格图像,放入
calib_images/目录,运行代码后观察重投影误差(可在calibrateCamera返回值中获取),通常RMS应小于0.5像素。 - 手眼标定数据采集:将标定板固定在机器人末端,控制机器人运动15个以上不同位姿(保证姿态差异明显),每个位姿下同时拍摄图像并记录机器人末端位姿。将代码中注释部分取消注释并接入实际机器人SDK。
- 验证方法:计算得到
T_cam2base后,选取一个未参与标定的测试位姿,将标定板角点的像素坐标通过内参反投影到相机坐标系,再用T_cam2base变换到机器人基坐标系,与机器人实际读取的该角点基坐标位置比较,误差小于5mm即标定合格。
常见问题
- 内参标定图像数量不足或角度单一会导致畸变估计不准,确保覆盖画面边缘区域。
- 手眼标定中机器人位姿变化范围过小会降低求解精度,尽量让末端做多方向旋转运动。
- 若使用不同棋盘格尺寸,需同步修改
pattern_size与square_size参数。