original url:
现在支持对 3d 点云数据进行标记。关于其他功能的更多详细信息,请参阅。在本文中,我们将专门介绍如何对 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 使用。我们从 开始,这是现有流行的自动驾驶数据集。 除了视频数据外,该数据集还包含由 velodyne lidar 传感器生成的 3d 点云数据。具体操作步骤如下:
明确 sagemaker ground truth 3d 点云标记作业的输入数据格式与要求。
将 kitti 数据集从局部坐标系转换为世界坐标系。
将视频数据与同步 lidar 数据相关联,以进行传感器融合。
准备一个输入 sagemaker ground truth 的 manifest 文件。
为 3d 点云对象检测创建一个标记作业,并跨越一系列帧进行跟踪。
在工作人员标记 ui 界面当中使用辅助标记工具。
要完成本轮演练,请使用 ground truth labeling jobs 下 notebook 实例中选项卡下的 3d-point-cloud-input-data-processing.ipynb notebook。您也可以在 github 上获取此。
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 之间的转换将仅需要使用摄像机外部矩阵。
如果在世界坐标系中存在一个旋转矩阵(由轴旋转组成)与平移矢量(或原点),而非单一外部变换矩阵,则可以直接使用旋转与平移来计算车辆姿态。具体参见以下代码:
python
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 为原点平移矢量,则意味着您可以从矩阵中提取旋转矩阵与平移矢量,具体如下所示:
python
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 文件:
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)均以四元数在空间方向上的朝向为指标。对于每台摄像机,分别提供自摄像机外部提取到的姿态数据。
摄像机校准、内部与失真
几何摄像机校准(也称为摄像机切除)用来标定摄像机的镜头和图像传感器参数。您可以使用这些参数校正镜头失真、测量对象的真实大小或确定场景中摄像机的位置。此外,如果相机图像失真,您可以提供额外的校准参数(如内部和失真)来校正图像。
相机参数包括本征矩阵参数、外部矩阵参数与失真系数,了解更多。详见以下代码:
python
# 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.
复制代码
摄像机的内部变换通过以下公式进行定义,其中*代表矩阵乘法。
python
|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 原始数据集中包含各个帧的旋转矩阵与平移矢量,您可以使用此数据为各个帧计算外部矩阵。处理原始数据可能比较困难,因此建议大家使用 python 模块以降低 kitti 数据集的处理门槛。
在数据集中,dataset.oxts[i].t_w_imu
负责给出第 i 帧的 lidar 外部变换,您可以将其与该帧中的各点相乘,以使用 numpy 矩阵乘法函数 matmul(lidar_transform_matrix, points)
将其转换为世界坐标系下的帧。通常,将 lidar 帧中的各点与 lidar 外部矩阵相乘,即可实现世界坐标系转换。其中 t_w_imu 约定即代表从 imu 到世界坐标系的转移(因此标记为t_destinationframe_originframe
)。
以下示例代码,说明了如何将 kitti 点云帧转换为世界坐标系:
python
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 数据集,您可以使用 python 模块加载 kitti 数据。在数据集中, .oxts[i].t_w_imu
负责给出第 i 帧的 lidar 外部矩阵(lidar_extrinsic_transform
)。您可以将其转换为平移与四元数指向形式,分别代表 json 格式的 lidar 朝向与位置。具体请参见以下代码:
python
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
的朝向与位置。详见以下代码:
python
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
当中就包含以下相机内部矩阵:
python
创建输入 manifest 文件
将 kitti 数据集中的一系列帧转换为世界坐标系,并从 lidar 及相机外部矩阵提取姿态信息之后,大家就可以创建一个包含传感器融合数据的 3d 点云序列 manifest 文件了。您可以使用以下函数为序列输入 manifest 文件自动创建序列文件:
python
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 的更多详细信息,请参阅。以下为工作人员标记 ui 中提供的部分辅助标记工具:
标签自动填充——当工作人员向帧中添加框体时,此功能会自动将框体请回至序列中的所有帧。当工作人员手动调整其他帧中同一对象的注释时,自动填充标签也将进行自动调整。
标签插值——工作人员在两个帧中标记单一对象后,ground truth 会使用这些注释在两帧之间的移动中对该对象进行插值。
捕捉——工作人员可以在对象周边添加一个框体,并使用键盘快捷键或菜单选项让 ground truth 自动拟合工具紧紧贴合对象的边界。
适应地面——在工作人员向 3d 场景中添加框体后,工作人员可以自动使该框体与地面相适应。例如,工作人员可以使用此功能将框体贴合至场景中的道路或人行道。
多视图标记——工作人员将 3d 框体添加至 3d 场景之后,侧面板上会显示正面与侧面两个透视图,以帮助工作人员在对象周边紧密调整该框体。工作人员可以在 3d 点云或侧面板上调整标签,相应调整也会实时显示在其他视图当中。
传感器融合——如果您提供传感器融合数据,则工作人员可以在 3d 场景与 2d 图像当中调整注释,且该注释会被实时投射至其他视图当中。
以下视频演示了插值的过程。工作人员只需要将两个长方形框体添加至序列中的第一帧与最后一帧。这些手动添加的框体将在左下方的导航栏中以红色段表示,中间的所有标签(在导航栏中显示为蓝色)均由 ground truth 负责插入。
下图所示,为多视图标记与传感器融合。将长方形框体添加至 3d 点云可视化图后,我们可以在三个侧视图与摄像机图像中对其进行调整。
除了标签之外,大家还可以添加标签属性以收集各个对象上的其他元数据。在以下视频中,可以看到带有“car”标签的长方形框体。该标签还包含与之关联的两项标签属性:occlusion 与 color。工作人员可以为各个标签属性选择对应的值。
ground truth 每 15 分钟自动保存一次注释。在作业完成之后,选择 submit。当任务完成后,输出数据将被保存于您在humantaskconfig
中指定的 (amazon s3)存储桶当中。
要了解关于输出数据格式的更多详细信息,请参阅 ground truth 输出数据页面中的部分。
总结
在本次实验中,我们了解了 ground truth 3d 点云标记作业对于输入数据的要求与选项,同时尝试创建了对象跟踪标记作业。关于金马国际能够在 3d 点云标记作业中实现的其他任务类型,请参阅。另外,我们还要感谢 kitti 团队为我们提供这套宝贵的数据集,用于演示如何准备 3d 点云数据并将其引入 sagemaker ground truth。
kitti 数据集拥有自己的使用许可。请保证您对数据集的使用方式完全符合其中提出的条款与要求。
作者介绍:
vikram madan 是 amazon sagemaker ground truth 产品经理。他专注于交付可以更轻松构建机器学习金马国际的解决方案的产品。闲暇时间,他喜欢长跑和观看纪录片。
aws ai 高级软件开发工程师。他主要负责开发复杂的机器学习服务与大规模分布式系统。他对计算机视觉、人工智能以及大数据技术抱有浓厚兴趣。在业余时间,他喜欢阅读历史书籍。
是一位专攻机器学习和人工智能的 aws 技术撰稿人。她与 aws 的多个团队合作,为使用 amazon sagemaker、mxnet 和 autogluon 的客户创建技术文档和教程。闲暇时,她喜欢在大自然中冥想和散步。
本文转载自亚马逊 aws 官方博客。
原文链接:
评论