Original URL: https://aws.amazon.com/cn/blogs/machine-learning/labeling-data-for-3d-object-tracking-and-sensor-fusion-in-amazon-sagemaker-ground-truth/
Amazon SageMaker Ground Truth 现在支持对3D点云数据进行标记。关于其他功能的更多详细信息,请参阅AWS News博客。在本文中,我们将专门介绍如何对3D点云数据执行数据转换,从而使用SageMaker Ground Truth来标记3D对象跟踪。
自动驾驶车辆(AV)厂商通常使用LiDAR传感器生成对车辆周边环境的3D成像结果。例如,他们会将LiDAR安装在车辆顶端,借此连续捕捉周边3D环境的时间点快照。LiDAR传感器输出的是一系列3D点云图像帧(常见的捕捉速率为每秒10帧)。为了建立起能够自动跟踪车辆周边重点对象(例如其他车辆及行人)的感知系统,自动驾驶厂商往往首先在3D点云图像帧中手动标记对象,而后使用标记后的3D图像帧训练机器学习(ML)模型。
在构建感知系统方面,目前最常见的做法是使用来自多个传感器的输入以弥补单一传感器的局限。例如,摄像机能够提供重要的上下文信息——例如当前交通信号灯显示红色、黄色还是绿色。但其在黑暗条件下的感知能力却非常有限。在另一方面,LiDAR传感器无法提供特定的上下文背景(例如交通信号灯的颜色),但却能在360度范围内实现纵深信息的收集,且无论外部光照条件如何。
SageMaker Ground Truth能够轻松在一系列3D点云帧上标记对象以构建机器学习训练数据集,且支持将多达8台摄像机输入的LiDAR传感数据加以融合。SageMaker Ground Truth做图像融合要求视频帧与3D点云帧进行预先同步。在启用传感器融合功能之后,标记人员可以配合同步视频帧查看3D点云帧。除了为标记工作提供更多视觉环境信息之外,传感器融合功能还会将3D点云中绘制的任何标签投射至视频帧,保证在某一帧内完成的标记调整将准确出现在另一帧中。
在本文中,我们将演示如何准备3D点云数据与同步视频数据,以供SageMaker Ground Truth使用。我们从 KITTI Vision Benchmark Suite开始,这是现有流行的自动驾驶数据集。[1] 除了视频数据外,该数据集还包含由Velodyne LiDAR传感器生成的3D点云数据。具体操作步骤如下:
- 明确SageMaker Ground Truth 3D点云标记作业的输入数据格式与要求。
- 将KITTI数据集从局部坐标系转换为世界坐标系。
- 将视频数据与同步LiDAR数据相关联,以进行传感器融合。
- 准备一个输入SageMaker Ground Truth 的 manifest文件。
- 为3D点云对象检测创建一个标记作业,并跨越一系列帧进行跟踪。
- 在工作人员标记UI界面当中使用辅助标记工具。
要完成本轮演练,请使用Ground Truth Labeling Jobs下notebook实例中Amazon SageMaker Examples选项卡下的3D-point-cloud-input-data-processing.ipynb notebook。您也可以在GitHub上获取此notebook。
3D点云标记作业输入数据
在本节中,我们将介绍Ground Truth输入数据的概念,以及对于3D点云标记作业的基本要求。
3D点云帧与3D点云序列
所谓“点云帧”,是指某一时刻3D场景下所有3D点的集合。每个点都使用三个坐标来描述,分别为x、y与z。向点云中添加颜色及点强度变化,即可使点具备其他属性,例如强度i或者红色(r)、绿色(g)及蓝色(b)色彩通道的值(8位)。所有位置坐标(x, y, z)皆以米为单位。在创建Ground Truth 3D点云标记作业时,大家可以使用ASCII或者二进制格式的点云数据。
而“序列”的定义,则是在LiDAR移动时(例如位于车辆顶端的LiDAR)所捕捉到的3D点云帧的时间序列集合。SageMaker Ground Truth将序列文件格式定义为各帧的有序排列结果,其中每个帧都与时间戳相关联。
在本文中,我们将演示如何根据KITTI数据集创建SageMaker Ground Truth序列文件,并以此为基础创建用于3D对象跟踪的标记作业。
世界坐标系
在对象跟踪中,大家可以在参考点(自动驾驶汽车)移动时跟踪对象(例如人行道上的行人)的运动轨迹。您的参考点上安装有传感器以感应周边的环境,这也是目前“自主车辆”的常见设计思路。
在执行对象跟踪时,我们需要使用世界坐标系(也称为全局参照系),这是因为自主车辆本身确实是在世界范围内移动。通常,SageMaker Ground Truth要求您将点云数据转换为您所选定的参考坐标系。大家一般可以通过将3D帧中的各个点与LiDAR传感器的外部矩阵相乘来完成此类转换。传感器的外部矩阵是一种同构转换矩阵,用于将数据的透视图从传感器自身的坐标系转换为世界坐标系。均匀变换是指对三个旋转轴(x轴、y轴与z轴)以及原点平移的序列转换,旋转矩阵则为定义三者旋转序列的3 x 3矩阵。
本文将向大家介绍如何使用外部矩阵将KITTI 3D帧从局部坐标系转换为世界坐标系。KITTI数据集为每个3D点云帧提供一对应的外部矩阵。您可以使用来自自主车辆的GPS数据得出外部矩阵,并使用NumPy矩阵乘法函数将此矩阵与帧中的各个点相乘,将其转换为KITTI数据集使用的世界坐标系。
通过自定义设置,大家还可以使用GPS/IMU与自主车辆上LiDAR传感器的相对位置与方向(纬度/经度/高度/侧倾角/俯角/仰角)计算外部变换矩阵。例如,您可以基于pose = convertOxtsToPose(oxts) 从KITTI原始数据计算车辆姿态,并将oxts数据转换为由4 x 4刚性变换矩阵指定的局部欧氏姿势。接下来,您可以使用世界坐标系中的参考帧转换矩阵将该姿态转换矩阵转换为全局参考帧。
传感器融合
LiDAR传感器与每台摄像机都拥有自己的外部矩阵,SageMaker Ground Truth利用它们来实现传感器融合功能。要将标签从3D点云投射至摄像机平面图像,SageMaker Ground Truth需要将3D点由LiDAR坐标系转换为摄像机坐标系。这一转换通常是使用LiDAR外部矩阵将3D点从LiDAR自有坐标转换为世界坐标系来完成的。接下来,我们使用摄像机外部矩阵的逆(从世界到摄像机)将上一步中获得的世界坐标系 3D点转换为摄像机平面图像。如果您的3D点云数据已经转换为世界坐标系形式,则无需进行第一次转换,而且3D与2D之间的转换将仅需要使用摄像机外部矩阵。
如果在世界坐标系中存在一个旋转矩阵(由轴旋转组成)与平移矢量(或原点),而非单一外部变换矩阵,则可以直接使用旋转与平移来计算车辆姿态。具体参见以下代码:
rotation = [[ 9.96714314e-01, -8.09890350e-02, 1.16333982e-03],
[ 8.09967396e-02, 9.96661051e-01, -1.03090934e-02],
[-3.24531964e-04, 1.03694477e-02, 9.99946183e-01]]
origin= [1.71104606e+00
5.80000039e-01
9.43144935e-01]
from scipy.spatial.transform import Rotation as R
# position is the origin
position = origin
r = R.from_matrix(np.asarray(rotation))
# heading in WCS using scipy
heading = r.as_quat()
如果您拥有一个4 x 4外部矩阵,且矩阵形式为[R T; 0 0 0 1],其中R为旋转矩阵,T为原点平移矢量,则意味着您可以从矩阵中提取旋转矩阵与平移矢量,具体如下所示:
import numpy as np
transformation
= [[ 9.96714314e-01, -8.09890350e-02, 1.16333982e-03, 1.71104606e+00],
[ 8.09967396e-02, 9.96661051e-01, -1.03090934e-02, 5.80000039e-01],
[-3.24531964e-04, 1.03694477e-02, 9.99946183e-01, 9.43144935e-01],
[ 0, 0, 0, 1]]
transformation = np.array(transformation )
rotation = transformation[0:3][0:3]
translation= transformation[0:3][3]
from scipy.spatial.transform import Rotation as R
# position is the origin translation
position = translation
r = R.from_matrix(np.asarray(rotation))
# heading in WCS using scipy
heading = r.as_quat()
print(f"position:{position}\nheading: {heading}")
在传感器融合当中,我们可以通过原点位置(用于平移)以及四元数指向(用于指示xyz轴的旋转)以传感器姿态的形式提供外部矩阵。以下示例代码是在输入manifest文件中使用的姿态JSON文件:
{
"position": {
"y": -152.77584902657554,
"x": 311.21505956090624,
"z": -10.854137529636024
},
"heading": {
"qy": -0.7046155108831117,
"qx": 0.034278837280808494,
"qz": 0.7070617895701465,
"qw": -0.04904659893885366
}
}
所有位置坐标(x, y, z)皆以米为单位,所有姿态朝向(qx, qy, qz, qw)均以四元数在空间方向上的朝向为指标。对于每台摄像机,分别提供自摄像机外部提取到的姿态数据。
摄像机校准、内部与失真
几何摄像机校准(也称为摄像机切除)用来标定摄像机的镜头和图像传感器参数。您可以使用这些参数校正镜头失真、测量对象的真实大小或确定场景中摄像机的位置。此外,如果相机图像失真,您可以提供额外的校准参数(如内部和失真)来校正图像。
相机参数包括本征矩阵参数、外部矩阵参数与失真系数,点击此处了解更多。详见以下代码:
# intrinsic paramaters
fx (float) - focal length in x direction.
fy (float) - focal length in y direction.
cx (float) - x coordinate of principal point.
cy (float) - y coordinate of principal point.
# Radial distortion parameters
k1 (float) - Radial distortion coefficient.
k2 (float) - Radial distortion coefficient.
k3 (float) - Radial distortion coefficient.
k4 (float) - Radial distortion coefficient.
# Tangential distortion parameters
p1 (float) - Tangential distortion coefficient.
p2 (float) - Tangential distortion coefficient.
摄像机的内部变换通过以下公式进行定义,其中*代表矩阵乘法。
|x| |f_x, 0, c_x| |X|
|y| = |0, f_y, c_y| * |Y|
|z| | 0, 0, 1 | |Z|
输入manifest文件
Ground Truth采用输入manifest文件,其中的每一行都描述了需要由注释人员(或者对某些内置任务类型进行自动标记)完成的任务单元。输入manifest文件的格式由您的实际任务类型决定:
- 3D点云对象检测或者语义分段标记作业——输入manifest文件中的每一行,都包含与单一3D点云帧及传感器融合数据相关的信息。此manifest文件被称为3D点云帧输入manifest。
- 3D点云对象检测与标记作业跟踪——输入manifest文件中的每一行都包含指向某一序列文件的链接,该文件负责定义一系列与3D点云帧及传感器融合瞻前顾后 数据。其中各序列被称为3D点云序列,而此manifest被称为点云序列输入manifest。
在本次实验中,我们将创建一个点云序列manifest文件。大家也可以修改解决方案以创建自己的点云帧输入manifest。示例notebook中提供更多详细信息。
将KITTI数据集转换为世界坐标系
您可以使用notebook运行本节中的各代码片段。
世界坐标系由具体数据集决定。某些数据集会使用第一帧中的LiDAR位置作为原点,数据集中所有其他3D帧皆以第一帧作为参考对象,包括车辆的行驶方向与位置也将以第一帧为原点。也有一部分数据集会选取不同于原点的设备位置作为起点。KITTI数据集数据集使用第一帧位置作为其世界坐标系的参考对象。
本文将展示如何相对于第一帧中的LiDAR传感器原点,将KITTI数据集转换为全局参考帧,以便SageMaker Ground Truth能够实际使用。KITTI原始数据集中包含各个帧的旋转矩阵与平移矢量,您可以使用此数据为各个帧计算外部矩阵。处理原始数据可能比较困难,因此建议大家使用pykitti Python模块以降低KITTI数据集的处理门槛。
在数据集中,dataset.oxts[i].T_w_imu
负责给出第i帧的LiDAR外部变换,您可以将其与该帧中的各点相乘,以使用NumPy矩阵乘法函数 matmul: matmul(lidar_transform_matrix, points)
将其转换为世界坐标系下的帧。通常,将LiDAR帧中的各点与LiDAR外部矩阵相乘,即可实现世界坐标系转换。其中T_w_imu约定即代表从IMU到世界坐标系的转移(因此标记为T_destinationFrame_originFrame
)。
以下示例代码,说明了如何将KITTI点云帧转换为世界坐标系:
import pykitti
import numpy as np
basedir = '/Users/nameofuser/kitti-data'
date = '2011_09_26'
drive = '0079'
# The 'frames' argument is optional - default: None, which loads the whole dataset.
# Calibration, timestamps, and IMU data are read automatically.
# Camera and velodyne data are available via properties that create generators
# when accessed, or through getter methods that provide random access.
data = pykitti.raw(basedir, date, drive, frames=range(0, 50, 5))
# i is frame number
i = 0
# customer computes lidar extrinsics
lidar_extrinsic_matrix = data.oxts[i].T_w_imu
# velodyne raw point cloud in lidar scanners own coordinate system
points = data.get_velo(i)
# transform points from lidar to global frame using lidar_extrinsic_matrix
def generate_transformed_pcd_from_point_cloud(points, lidar_extrinsic_matrix):
tps = []
for point in points:
transformed_points = np.matmul(lidar_extrinsic_matrix, np.array([point[0], point[1], point[2], 1], dtype=np.float32).reshape(4,1)).tolist()
if len(point) > 3 and point[3] is not None:
tps.append([transformed_points[0][0], transformed_points[1][0], transformed_points[2][0], point[3]])
return tps
# customer transforms points from lidar to global frame using lidar_extrinsic_matrix
transformed_pcl = generate_transformed_pcd_from_point_cloud(points, lidar_extrinsic_matrix)
将视频数据与LiDAR数据相关联,以实现传感器融合
KITTI提供LiDAR外部与相机外部矩阵。您可以使用这些矩阵提取姿态数据,而后根据3D点云序列输入manifest的需求将这部分数据转换为JSON格式。
对于KITTI数据集,您可以使用pykitti Python模块加载KITTI数据。在数据集中, .oxts[i].T_w_imu
负责给出第i帧的LiDAR外部矩阵(lidar_extrinsic_transform
)。您可以将其转换为平移与四元数指向形式,分别代表JSON格式的LiDAR朝向与位置。具体请参见以下代码:
from scipy.spatial.transform import Rotation as R
# utility to convert extrinsic matrix to pose heading quaternion and position
def convert_extrinsic_matrix_to_trans_quaternion_mat(lidar_extrinsic_transform):
position = lidar_extrinsic_transform[0:3, 3]
rot = np.linalg.inv(lidar_extrinsic_transform[0:3, 0:3])
quaternion= R.from_matrix(np.asarray(rot)).as_quat()
trans_quaternions = {
"translation": {
"x": position[0],
"y": position[1],
"z": position[2]
},
"rotation": {
"qx": quaternion[0],
"qy": quaternion[1],
"qz": quaternion[2],
"qw": quaternion[3]
}
}
return trans_quaternions
同样,您也可以使用相机外部参数提取相机姿态数据。您可以通过inv(matmul(dataset.calib.T_cam0_velo, inv(dataset.oxts[i].T_w_imu)))
在第i帧中计算cam0
的Camera_extrinsic_transform
,进而将其转换为cam0
的朝向与位置。详见以下代码:
def convert_camera_inv_extrinsic_matrix_to_trans_quaternion_mat(camera_extrinsic_transform):
position = camera_extrinsic_transform[0:3, 3]
rot = np.linalg.inv(camera_extrinsic_transform[0:3, 0:3])
quaternion= R.from_matrix(np.asarray(rot)).as_quat()
trans_quaternions = {
"translation": {
"x": position[0],
"y": position[1],
"z": position[2]
},
"rotation": {
"qx": quaternion[0],
"qy": quaternion[1],
"qz": quaternion[2],
"qw": -quaternion[3]
}
}
return trans_quaternions
相机校准:内部与失真
KITTI数据集为每台摄像机提供一项校准参数。例如,data.calib.K_cam0
当中就包含以下相机内部矩阵:
创建输入manifest文件
将KITTI数据集中的一系列帧转换为世界坐标系,并从LiDAR及相机外部矩阵提取姿态信息之后,大家就可以创建一个包含传感器融合数据的3D点云序列manifest文件了。您可以使用以下函数为序列输入manifest文件自动创建序列文件:
def convert_to_gt():
# The 'frames' argument is optional - default: None, which loads the whole dataset.
# Calibration, timestamps, and IMU data are read automatically.
# Camera and velodyne data are available via properties that create generators
# when accessed, or through getter methods that provide random access.
data = pykitti.raw(basedir, date, drive, frames=range(0, 50, 5))
image_paths = [data.cam0_files, data.cam1_files, data.cam2_files, data.cam3_files]
camera_extrinsic_calibrations = [data.calib.T_cam0_velo, data.calib.T_cam1_velo, data.calib.T_cam2_velo, data.calib.T_cam3_velo]
camera_intrinsics = [data.calib.K_cam0, data.calib.K_cam1, data.calib.K_cam2, data.calib.K_cam3]
seq_json = {}
seq_json["seq-no"] = 1
seq_json["prefix"] = 's3://pdx-groundtruth-lidar-test-bucket/pdx-groundtruth-sequences/kittiseq2/frames/'
seq_json["number-of-frames"] = len(data)
seq_json["frames"] = []
default_position = {"x": 0, "y": 0, "z": 0}
default_heading = {"qx": 0, "qy": 0, "qz": 0, "qw": 1}
for i in range(len(data)):
# customer computes lidar extrinsics
lidar_extrinsic_matrix = data.oxts[i].T_w_imu
# velodyne raw point cloud in lidar scanners own coordinate system
points = data.get_velo(i)
# customer transforms points from lidar to global frame using lidar_extrinsic_matrix
transformed_pcl = generate_transformed_pcd_from_point_cloud(points, lidar_extrinsic_matrix)
# Customer computes rotation quaternion and translation from LiDAR Extrinsic
trans_quaternions = convert_extrinsic_matrix_to_trans_quaternion_mat(lidar_extrinsic_matrix)
# Customer uses trans_quaternions to populates GT ego veicle pose
ego_vehicle_pose = {}
ego_vehicle_pose['heading'] = trans_quaternions['rotation']
ego_vehicle_pose['position'] = trans_quaternions['translation']
# open file to write the transformed pcl
with open(output_base+"/"+str(i)+'.txt', "w") as out:
writer = csv.writer(out, delimiter=' ', quotechar='"', quoting=csv.QUOTE_MINIMAL)
for point in transformed_pcl:
writer.writerow((point[0], point[1], point[2], point[3]))
frame = {}
frame["frame-no"] = i
frame["frame"] = str(i)+'.txt'
frame["unix-timestamp"] = data.timestamps[i].replace(tzinfo=timezone.utc).timestamp()
frame["ego-vehicle-pose"] = ego_vehicle_pose
images = []
image_dir_path = os.path.join(output_base, 'images')
if not os.path.exists(image_dir_path):
os.makedirs(image_dir_path)
for j in range(len(image_paths)):
# copy image
image_path = image_paths[j][i]
image_suffix_path = 'images/frame_'+str(i)+'_camera_'+str(j)+'.jpg'
copyfile(image_path, os.path.join(output_base, image_suffix_path))
# If customer has the camera extrinsics then they use them to compute the camera transform
camera_transform= np.linalg.inv(np.matmul(camera_extrinsic_calibrations[j], np.linalg.inv(data.oxts[i].T_w_imu)))
# Customer computes rotation quaternion and translation from camera inverse Extrinsic
cam_trans_quaternions = convert_camera_inv_extrinsic_matrix_to_trans_quaternion_mat(camera_transform)
image_json = {}
image_json["image-path"] = image_suffix_path
image_json["unix-timestamp"] = frame["unix-timestamp"]
image_json['heading'] = cam_trans_quaternions['rotation']
image_json['position'] = cam_trans_quaternions['translation']
image_json['fx'] = camera_intrinsics[j][0][0]
image_json['fy'] = camera_intrinsics[j][1][1]
image_json['cx'] = camera_intrinsics[j][0][2]
image_json['cy'] = camera_intrinsics[j][1][2]
image_json['k1'] = 0
image_json['k2'] = 0
image_json['k3'] = 0
image_json['k4'] = 0
image_json['p1'] = 0
image_json['p2'] = 0
image_json['skew'] = 0
images.append(image_json)
frame["images"]=images
seq_json["frames"].append(frame)
with open(output_base+'/mykitti-seq2.json', 'w') as outfile:
json.dump(seq_json, outfile)
创建标记作业
输入manifest文件创建完成之后,即可以用notebook创建一个标记作业。在创建标记作业时(参照notebook内的相关说明),请使用内部工作团队,以保证您能够在工作人员门户中随时查看各工作人员任务并与工作人员标记UI进行交互。
标记作业的预处理时间,是任务开始时显示在工作人员门户中的所需时间。具体时长取决于输入数据的大小、点云的分辨率以及用于传感器融合的数据(如果存在)。因此,在使用notebook时,您的标记作业可能需要几分钟时间才会显示在工作人员门户当中。在任务出现后,将其选定并选择Start Working。
关于工作人员UI的更多详细信息,请参阅3D点云对象跟踪。以下为工作人员标记UI中提供的部分辅助标记工具:
- 标签自动填充——当工作人员向帧中添加框体时,此功能会自动将框体请回至序列中的所有帧。当工作人员手动调整其他帧中同一对象的注释时,自动填充标签也将进行自动调整。
- 标签插值——工作人员在两个帧中标记单一对象后,Ground Truth会使用这些注释在两帧之间的移动中对该对象进行插值。
- 捕捉——工作人员可以在对象周边添加一个框体,并使用键盘快捷键或菜单选项让Ground Truth自动拟合工具紧紧贴合对象的边界。
- 适应地面——在工作人员向3D场景中添加框体后,工作人员可以自动使该框体与地面相适应。例如,工作人员可以使用此功能将框体贴合至场景中的道路或人行道。
- 多视图标记——工作人员将3D框体添加至3D场景之后,侧面板上会显示正面与侧面两个透视图,以帮助工作人员在对象周边紧密调整该框体。工作人员可以在3D点云或侧面板上调整标签,相应调整也会实时显示在其他视图当中。
- 传感器融合——如果您提供传感器融合数据,则工作人员可以在3D场景与2D图像当中调整注释,且该注释会被实时投射至其他视图当中。
以下视频演示了插值的过程。工作人员只需要将两个长方形框体添加至序列中的第一帧与最后一帧。这些手动添加的框体将在左下方的导航栏中以红色段表示,中间的所有标签(在导航栏中显示为蓝色)均由Ground Truth负责插入。
下图所示,为多视图标记与传感器融合。将长方形框体添加至3D点云可视化图后,我们可以在三个侧视图与摄像机图像中对其进行调整。
除了标签之外,大家还可以添加标签属性以收集各个对象上的其他元数据。在以下视频中,可以看到带有“Car”标签的长方形框体。该标签还包含与之关联的两项标签属性:Occlusion与Color。工作人员可以为各个标签属性选择对应的值。
Ground Truth每15分钟自动保存一次注释。在作业完成之后,选择Submit。当任务完成后,输出数据将被保存于您在HumanTaskConfig
中指定的Amazon Simple Storage Service (Amazon S3)存储桶当中。
要了解关于输出数据格式的更多详细信息,请参阅Ground Truth输出数据页面中的3D点云对象跟踪部分。
总结
在本次实验中,我们了解了Ground Truth 3D点云标记作业对于输入数据的要求与选项,同时尝试创建了对象跟踪标记作业。关于我们能够在3D点云标记作业中实现的其他任务类型,请参阅3D点云任务类型。另外,我们还要感谢KITTI团队为我们提供这套宝贵的数据集,用于演示如何准备3D点云数据并将其引入SageMaker Ground Truth。
[1] KITTI数据集拥有自己的使用许可。请保证您对数据集的使用方式完全符合其中提出的条款与要求。
本篇作者