2. ToF传感器标定流程搭建

任务定义

本案例目标是为ToF传感器搭建一套可复现的标定流程,覆盖从原始深度数据采集到标定参数生成与精度验证的完整闭环。需要解决的核心问题是:ToF传感器因镜头畸变、多路径干扰和温度漂移导致的深度测量偏差,通过标定获得内参、畸变系数和深度校正参数,使校正后的深度误差满足应用要求。

标定流程架构

标定流程分为四个阶段:数据采集、参数求解、参数应用、精度验证。数据采集阶段使用棋盘格靶标在不同距离和角度下拍摄多帧图像;参数求解阶段分别计算相机内参与畸变系数、深度偏移量与温度补偿系数;参数应用阶段将标定结果写入传感器驱动;精度验证阶段通过独立测试集评估校正效果。

阶段1: 数据采集 棋盘格多距离/角度拍摄 阶段2: 参数求解 内参/畸变/深度偏移/温补 阶段3: 参数应用 写入驱动/固件配置 阶段4: 精度验证 独立测试集评估 迭代优化: 若验证误差超阈值, 返回阶段1补充数据 判定标准: 深度误差 < 0.5% @ 1m, 平面拟合残差 < 3mm

核心代码实现

以下代码实现标定流程的核心算法,包括棋盘格角点提取、内参求解、深度偏移计算和温度补偿拟合。代码使用OpenCV和NumPy,输入为多帧深度图与对应灰度图,输出为标定参数JSON文件。

import cv2
import numpy as np
import json
import glob
from scipy.optimize import curve_fit

class ToFCalibrator:
    def __init__(self, board_size=(9, 6), square_size_mm=30.0):
        self.board_size = board_size  # 内角点数
        self.square_size = square_size_mm / 1000.0  # 转为米
        self.criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6)
        self.objp = np.zeros((board_size[0]*board_size[1], 3), np.float32)
        self.objp[:, :2] = np.mgrid[0:board_size[0], 0:board_size[1]].T.reshape(-1, 2)
        self.objp *= self.square_size

    def collect_corners(self, gray_images):
        """提取所有图像中的棋盘格角点"""
        obj_points, img_points = [], []
        valid_count = 0
        for img in gray_images:
            ret, corners = cv2.findChessboardCorners(img, self.board_size, None)
            if ret:
                corners_refined = cv2.cornerSubPix(
                    img, corners, (11, 11), (-1, -1), self.criteria)
                obj_points.append(self.objp)
                img_points.append(corners_refined)
                valid_count += 1
        if valid_count < 5:
            raise RuntimeError(f"有效标定图像不足: {valid_count}/5")
        return obj_points, img_points

    def calibrate_intrinsics(self, gray_images, depth_images):
        """标定相机内参、畸变系数和深度偏移"""
        obj_points, img_points = self.collect_corners(gray_images)
        h, w = gray_images[0].shape[:2]
        ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
            obj_points, img_points, (w, h), None, None)

        # 计算深度偏移: 对每帧棋盘格平面拟合深度值
        depth_offsets = []
        for i, (rvec, tvec) in enumerate(zip(rvecs, tvecs)):
            # 将棋盘格角点投影到图像坐标
            proj_points, _ = cv2.projectPoints(
                self.objp, rvec, tvec, mtx, dist)
            proj_points = proj_points.reshape(-1, 2).astype(int)

            # 提取对应位置的深度值并拟合平面
            valid_depths = []
            for (x, y) in proj_points:
                if 0 <= y < depth_images[i].shape[0] and 0 <= x < depth_images[i].shape[1]:
                    d = depth_images[i][y, x]
                    if d > 0:  # 有效深度
                        valid_depths.append(d)
            if len(valid_depths) > 10:
                # 真实距离 = 棋盘格到相机的Z距离
                real_dist = np.linalg.norm(tvec)
                measured_mean = np.mean(valid_depths) / 1000.0  # mm转m
                depth_offsets.append(measured_mean - real_dist)

        depth_offset = np.mean(depth_offsets) if depth_offsets else 0.0
        return mtx, dist, depth_offset

    def fit_temperature_coeff(self, temps, depth_errors):
        """拟合温度补偿系数: error = a * (T - T_ref)"""
        def linear_model(T, a):
            return a * (T - 25.0)  # 25°C为参考温度

        popt, _ = curve_fit(linear_model, temps, depth_errors)
        return popt[0]  # 温度系数 a (m/°C)

    def save_params(self, mtx, dist, depth_offset, temp_coeff, output_path):
        """保存标定参数为JSON"""
        params = {
            "camera_matrix": mtx.tolist(),
            "dist_coeffs": dist.tolist(),
            "depth_offset_m": float(depth_offset),
            "temp_coeff_m_per_c": float(temp_coeff),
            "ref_temp_c": 25.0,
            "calibration_date": "2024-01-15"
        }
        with open(output_path, 'w') as f:
            json.dump(params, f, indent=2)
        return params

    def apply_correction(self, raw_depth, temp, params):
        """应用标定参数校正深度"""
        # 深度偏移校正
        corrected = raw_depth / 1000.0 - params["depth_offset_m"]
        # 温度补偿
        corrected -= params["temp_coeff_m_per_c"] * (temp - params["ref_temp_c"])
        return corrected * 1000.0  # 转回mm

def main():
    """标定主流程"""
    # 1. 加载采集数据(示例路径, 实际使用时替换为真实数据)
    gray_files = sorted(glob.glob("calib_data/gray_*.png"))
    depth_files = sorted(glob.glob("calib_data/depth_*.npy"))
    temp_log = np.loadtxt("calib_data/temp_log.csv", delimiter=",")

    if len(gray_files) != len(depth_files):
        raise ValueError("灰度图与深度图数量不匹配")

    gray_images = [cv2.imread(f, cv2.IMREAD_GRAYSCALE) for f in gray_files]
    depth_images = [np.load(f) for f in depth_files]

    # 2. 初始化标定器
    calib = ToFCalibrator(board_size=(9, 6), square_size_mm=30.0)

    # 3. 标定内参与深度偏移
    mtx, dist, depth_offset = calib.calibrate_intrinsics(gray_images, depth_images)

    # 4. 温度补偿拟合(使用采集时记录的温度与对应深度误差)
    temps = temp_log[:, 0]
    depth_errors = temp_log[:, 1]
    temp_coeff = calib.fit_temperature_coeff(temps, depth_errors)

    # 5. 保存参数
    params = calib.save_params(mtx, dist, depth_offset, temp_coeff,
                               "tof_calib_params.json")
    print(f"标定完成: 深度偏移={depth_offset*1000:.2f}mm, "
          f"温度系数={temp_coeff*1000:.3f}mm/°C")

    # 6. 验证示例(使用第一帧数据)
    test_depth = depth_images[0]
    corrected = calib.apply_correction(test_depth, 30.0, params)
    print(f"校正前中心深度: {test_depth[240, 320]}mm, "
          f"校正后: {corrected[240, 320]:.1f}mm")

if __name__ == "__main__":
    main()

数据采集规范

标定数据采集需满足以下条件:棋盘格靶标尺寸为30mm方格,9×6内角点;拍摄距离覆盖0.5m至3m范围,至少5个距离档位;每个距离下旋转棋盘格至不同姿态(俯仰±30°、偏航±30°);同时记录传感器温度与对应深度误差样本。采集时保持环境光照稳定,避免强反射物体进入视场。

验证方法与判定标准

完成标定后,使用独立测试集(不参与标定的数据)进行验证:

  1. 深度精度测试:将靶标置于已知距离(如1.0m、2.0m),采集100帧深度数据,计算校正后深度均值与真实距离的误差,判定标准为误差小于真实距离的0.5%;
  2. 平面拟合残差:对校正后的深度图拟合平面,计算点到平面的均方根残差,判定标准为小于3mm;
  3. 温度稳定性:在15°C至45°C温度范围内测试,校正后深度漂移不超过±2mm。

运行python tof_calibrator.py执行标定,程序输出标定参数文件tof_calib_params.json。验证时调用apply_correction()函数处理测试数据,对比校正前后的深度误差变化。若验证不通过,需补充更多姿态和距离的标定数据后重新标定。