4. 激光器与相机同轴标定

任务说明

本案例完成激光焊接视觉定位系统中激光器与工业相机之间的同轴标定。目标是通过拍摄激光光斑在已知间距网格上的图像,计算相机像素坐标系与激光器扫描坐标系之间的映射关系,使后续焊接路径能够依据视觉定位结果准确映射到激光扫描位置。

标定原理与数据布局

同轴标定的核心是建立两个坐标系间的变换:相机拍摄到的光斑像素坐标 (u, v) 与激光器控制指令坐标 (X, Y) 之间的对应关系。本案例采用简化线性模型,假设激光扫描区域近似平面且相机光轴与激光扫描面近似垂直,使用仿射变换描述映射关系:

[ X ]   [ a  b  c ] [ u ]
[ Y ] = [ d  e  f ] [ v ]
                   [ 1 ]

标定时,激光器在已知物理间距的网格点上依次出光,相机同步采集图像并提取光斑中心像素坐标。通过最小二乘法求解6个仿射参数。标定完成后,任意视觉检测到的特征点像素坐标均可通过该矩阵转换为激光扫描坐标。

工业相机 激光扫描头 同轴光路 网格间距 5mm 光斑中心提取 像素坐标 (u,v) 扫描坐标 (X,Y) 仿射矩阵 H (6参数)

代码实现

以下Python代码实现完整的同轴标定流程:生成标定控制点、模拟光斑图像坐标采集(含噪声)、求解仿射变换矩阵、计算重投影误差并输出标定结果。


import numpy as np
import cv2

# ========== 1. 标定参数配置 ==========
GRID_SIZE = 5          # 网格点数量 5x5
GRID_SPACING = 5.0     # 网格间距 5mm
NOISE_LEVEL = 0.8      # 模拟像素噪声标准差

# ========== 2. 生成激光扫描坐标(物理坐标) ==========
scan_pts = []
for i in range(GRID_SIZE):
    for j in range(GRID_SIZE):
        x = i * GRID_SPACING
        y = j * GRID_SPACING
        scan_pts.append([x, y])
scan_pts = np.array(scan_pts, dtype=np.float32)

# ========== 3. 模拟相机采集:真实仿射矩阵 + 噪声 ==========
# 真实变换参数(模拟实际光学系统)
true_H = np.array([[2.5, 0.3, 120.0],
                   [0.2, 2.8, 80.0]], dtype=np.float32)

def apply_affine(H, pts):
    """应用仿射变换: pts为Nx2, H为2x3"""
    ones = np.ones((pts.shape[0], 1), dtype=np.float32)
    pts_h = np.hstack([pts, ones])
    return pts_h @ H.T

# 生成理想像素坐标并添加噪声模拟检测误差
ideal_pix = apply_affine(true_H, scan_pts)
rng = np.random.default_rng(42)
noise = rng.normal(0, NOISE_LEVEL, ideal_pix.shape).astype(np.float32)
pixel_pts = ideal_pix + noise

# ========== 4. 最小二乘求解仿射矩阵 ==========
def estimate_affine(src_pts, dst_pts):
    """
    求解仿射变换: dst = H * [src; 1]
    src_pts: Nx2 源点(像素坐标)
    dst_pts: Nx2 目标点(扫描坐标)
    返回: 2x3 仿射矩阵
    """
    N = src_pts.shape[0]
    A = np.zeros((2*N, 6), dtype=np.float32)
    b = np.zeros((2*N, 1), dtype=np.float32)
    for i in range(N):
        u, v = src_pts[i]
        X, Y = dst_pts[i]
        A[2*i]   = [u, v, 1, 0, 0, 0]
        A[2*i+1] = [0, 0, 0, u, v, 1]
        b[2*i]   = X
        b[2*i+1] = Y
    # 最小二乘解
    params, _, _, _ = np.linalg.lstsq(A, b, rcond=None)
    H = params.reshape(2, 3)
    return H.astype(np.float32)

# 使用像素坐标作为源,扫描坐标作为目标
H_est = estimate_affine(pixel_pts, scan_pts)

# ========== 5. 验证与误差评估 ==========
# 用估计矩阵将像素坐标映射回扫描坐标
scan_reprojected = apply_affine(H_est, pixel_pts)
# 计算重投影误差(欧氏距离)
errors = np.linalg.norm(scan_reprojected - scan_pts, axis=1)
rmse = np.sqrt(np.mean(errors**2))
max_err = np.max(errors)

# ========== 6. 输出标定结果 ==========
print("=== 同轴标定结果 ===")
print("估计仿射矩阵 H:")
print(H_est)
print(f"\n重投影误差 RMSE: {rmse:.4f} mm")
print(f"最大误差: {max_err:.4f} mm")
print(f"\n标定完成: 像素坐标 -> 扫描坐标 映射已建立")

# ========== 7. 保存标定参数(供后续焊接路径使用) ==========
np.savez("calibration_result.npz",
         H=H_est,
         rmse=rmse,
         grid_spacing=GRID_SPACING)
print("\n标定参数已保存至 calibration_result.npz")

运行与验证

运行方式:确保已安装NumPy和OpenCV(pip install numpy opencv-python),直接运行上述脚本。

验证方法:观察输出的RMSE值。由于模拟噪声标准差为0.8像素,且真实仿射矩阵数值在2.5~2.8量级,预期RMSE应在0.3~0.6mm范围内。若RMSE显著大于1mm,说明求解过程存在错误。同时检查输出的仿射矩阵与真实矩阵 true_H 的数值差异,估计值应接近真实值。

判定标准:脚本正常输出仿射矩阵和误差指标,且RMSE小于0.6mm即判定标定流程通过。保存的 calibration_result.npz 文件可直接用于后续焊接路径的坐标转换。

关键说明

本案例使用模拟数据验证标定算法流程的正确性。在实际设备中,将第3步替换为真实相机采集:激光器按 scan_pts 顺序出光,相机同步拍照,通过灰度质心法或椭圆拟合提取光斑中心像素坐标,即可得到真实的 pixel_pts。其余求解与验证步骤完全一致。标定完成后,视觉系统检测到焊缝特征点的像素坐标,即可通过矩阵 H_est 转换为激光扫描坐标,实现焊接路径的视觉引导。