1. ToF相机标定与深度图获取

任务目标

本案例使用棋盘格标定板对ToF相机进行内参标定,获取相机内参矩阵与畸变系数,并采集一帧原始深度图进行保存与可视化验证。通过该流程理解ToF相机基于飞行时间测距的成像原理,以及标定参数对后续点云生成的基础作用。

环境与依赖

  • Python 3.8+,安装OpenCV(opencv-python)、NumPy、Matplotlib
  • ToF相机(以Azure Kinect DK为例,需安装pykinect_azure),或使用任意可输出深度图的相机/数据集
  • 棋盘格标定板(本例使用7×9内角点,方格边长30mm)

实现原理简述

ToF相机通过测量红外光发射与接收的相位差计算深度。标定仅针对红外相机内参(与深度图对齐),采用标准张正友标定法:采集多角度棋盘格图像,提取角点后求解内参矩阵K与畸变系数D。深度图获取则直接调用相机SDK的深度通道。

关键完整代码

import cv2
import numpy as np
import pykinect_azure as pykinect

# ========== 1. ToF相机初始化 ==========
pykinect.initialize_libraries()
device = pykinect.start_device()
device.set_color_format(pykinect.K4A_IMAGE_FORMAT_COLOR_BGRA32)

# ========== 2. 采集标定图像 ==========
# 棋盘格内角点数(列, 行)
pattern_size = (7, 9)
square_size = 0.030  # 方格边长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) * square_size

obj_points = []  # 世界坐标系中的3D点
img_points = []  # 图像中的2D点

print("请将棋盘格置于相机视野内,按空格键采集,按ESC结束...")
capture_count = 0

while True:
    ret, frame = device.update()
    if not ret:
        continue

    # 获取红外图(与深度图对齐的灰度图)
    ir_image = frame.get_ir_image()
    if ir_image is None:
        continue

    ir_img = ir_image.to_numpy()
    ir_img_8u = cv2.normalize(ir_img, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
    ir_img_color = cv2.cvtColor(ir_img_8u, cv2.COLOR_GRAY2BGR)

    # 查找棋盘格角点
    found, corners = cv2.findChessboardCorners(ir_img_8u, pattern_size)
    if found:
        cv2.drawChessboardCorners(ir_img_color, pattern_size, corners, found)

    cv2.imshow("IR Image - Press SPACE to capture", ir_img_color)
    key = cv2.waitKey(1) & 0xFF

    if key == ord(' ') and found:
        obj_points.append(objp)
        corners_refined = cv2.cornerSubPix(
            ir_img_8u, corners, (5, 5), (-1, -1),
            (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
        img_points.append(corners_refined)
        capture_count += 1
        print(f"已采集 {capture_count} 张有效标定图像")
    elif key == 27:  # ESC
        break

cv2.destroyAllWindows()

# ========== 3. 执行相机标定 ==========
if len(obj_points) < 5:
    raise RuntimeError("有效标定图像不足5张,无法进行标定")

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

print("\n=== 标定结果 ===")
print("内参矩阵 K:")
print(mtx)
print("\n畸变系数 (k1, k2, p1, p2, k3):")
print(dist.ravel())

# ========== 4. 获取并保存原始深度图 ==========
ret, frame = device.update()
if ret:
    depth_image = frame.get_depth_image()
    if depth_image is not None:
        depth_img = depth_image.to_numpy()  # 原始深度值,单位mm
        np.save("depth_raw.npy", depth_img)
        print(f"\n深度图已保存: depth_raw.npy, 尺寸 {depth_img.shape}, 数据类型 {depth_img.dtype}")

        # 可视化(深度值范围截断到0.2m~3m便于观察)
        depth_vis = np.clip(depth_img, 200, 3000)
        depth_vis = cv2.normalize(depth_vis, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
        cv2.imwrite("depth_visualized.png", depth_vis)
        print("深度可视化图已保存: depth_visualized.png")

# 保存标定结果
np.savez("tof_calibration.npz", mtx=mtx, dist=dist)
print("标定结果已保存: tof_calibration.npz")

device.close()

运行与验证方法

  1. 运行标定采集:连接Azure Kinect后运行代码,缓慢移动棋盘格使其出现在红外图像不同位置与角度,按空格采集至少10~15张图像,按ESC结束。
  2. 观察标定输出:终端打印的内参矩阵应接近相机出厂标称值(Azure Kinect红外内参约为 fx≈610, fy≈610, cx≈512, cy≈512),重投影误差通常小于0.5像素。
  3. 验证深度图:程序自动保存depth_raw.npy(原始深度值,单位mm)与depth_visualized.png。打开可视化图,近处物体应显示为亮色,远处为暗色;深度图与红外图视角一致,无错位。
  4. 判定通过条件:标定重投影误差<1.0像素;深度图中可清晰分辨不同距离物体的灰度层次,且保存的npy文件可用np.load("depth_raw.npy")正常读取并显示为合理深度值(0.5m~5m范围内)。

常见问题

  • 找不到棋盘格角点:确保棋盘格平整、光照均匀,避免红外反光过强;可适当调整cv2.findChessboardCorners的flags参数。
  • 深度图全黑或全白:检查相机是否处于近距模式(NFOV)或远距模式(WFOV),不同模式深度范围不同;确认物体在有效测距范围内。
  • 标定误差过大:增加采集图像数量并覆盖更多角度,避免所有图像中棋盘格都位于画面中心。