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()
运行与验证方法
- 运行标定采集:连接Azure Kinect后运行代码,缓慢移动棋盘格使其出现在红外图像不同位置与角度,按空格采集至少10~15张图像,按ESC结束。
- 观察标定输出:终端打印的内参矩阵应接近相机出厂标称值(Azure Kinect红外内参约为
fx≈610, fy≈610, cx≈512, cy≈512),重投影误差通常小于0.5像素。 - 验证深度图:程序自动保存
depth_raw.npy(原始深度值,单位mm)与depth_visualized.png。打开可视化图,近处物体应显示为亮色,远处为暗色;深度图与红外图视角一致,无错位。 - 判定通过条件:标定重投影误差<1.0像素;深度图中可清晰分辨不同距离物体的灰度层次,且保存的npy文件可用
np.load("depth_raw.npy")正常读取并显示为合理深度值(0.5m~5m范围内)。
常见问题
- 找不到棋盘格角点:确保棋盘格平整、光照均匀,避免红外反光过强;可适当调整
cv2.findChessboardCorners的flags参数。 - 深度图全黑或全白:检查相机是否处于近距模式(NFOV)或远距模式(WFOV),不同模式深度范围不同;确认物体在有效测距范围内。
- 标定误差过大:增加采集图像数量并覆盖更多角度,避免所有图像中棋盘格都位于画面中心。