3D点云目标检测——KITTI数据集读取与处理

一、 数据基本情况

KITTI数据集是由德国卡尔斯鲁厄理工学院和丰田美国技术研究院联合创建的一个大规模自动驾驶场景下的计算机视觉算法评测数据集。以下是关于它的详细介绍:

  1. 数据集背景:为评估自动驾驶中计算机视觉算法的性能而设计。自动驾驶汽车需在复杂道路环境中准确识别、跟踪和预测其他车辆、行人等交通参与者的行为,KITTI数据集就是为提供这样一个基准测试平台而产生的。
  2. 数据采集:使用安装在改装丰田普锐斯车上的两台彩色相机、两台灰度相机、一个3D激光扫描仪和一个高精度GPS/IMU系统,在德国卡尔斯鲁厄的市区、乡村和高速公路等不同环境下行驶6小时采集数据。
  3. 数据内容:包含大量图像序列及对应的3D物体标注信息,标注信息有物体的位置、大小、方向及类别等,还提供了激光雷达扫描得到的点云数据。
  4. 数据标注:每个物体都被仔细标注,标注信息包括边界框、3D尺寸、方向角、截断程度以及遮挡程度等。
  5. 应用场景:主要用于评估自动驾驶中的目标检测、目标跟踪、3D重建、场景理解等算法的性能,也可用于研究计算机视觉、机器学习、模式识别等领域的其他问题。
  6. 评估指标:提供了准确率、召回率、F1分数等一套评估指标,帮助研究人员了解算法表现并进行比较。
  7. 数据集影响:自发布以来,已成为自动驾驶和计算机视觉领域的重要基准测试平台,推动了相关算法发展,促进了学术界和工业界的合作与交流,为自动驾驶技术的实际应用奠定了基础。

KITTI 数据集采集自德国卡尔斯鲁厄市,涵盖了市区、郊区、高速公路等多种交通场景。数据采集时间为 2011 年 09 月 26 日、28 日、29 日、30 日及 10 月 03 日的白天。

KITTI 数据采集平台如下图所示:

该平台包含以下设备:

  • 2 个 140 万像素的黑白相机
  • 2 个 140 万像素的彩色相机
  • 4 个爱特蒙特光学镜头
  • 1 个 64 线 Velodyne 3D 激光扫描仪
  • 1 个 OXTS RT3003 惯导系统

从上图可以看出:

  • 相机的坐标系中,Z 轴朝前,Y 轴朝下,整个坐标系为右手坐标系。
  • 激光雷达的 X 轴朝向正前方,Z 轴竖直向上,Y 轴根据右手定则确定。
  • IMU/GPS 系统的坐标系朝向与激光雷达一致。

总结来说,KITTI 数据集由 4 个相机、1 个激光雷达、1 个 IMU/GPS 惯导系统共同组成。我们需要厘清这 6 个传感器之间的坐标系关系和时间同步信息。

关于传感器的尺寸参数可以参考下图:

[date]-drive-sync-[sequence] 目录下存放了 6 个传感器对应的采集数据文件夹,分别为:

复制代码
- image_00
- image_01
- image_02
- image_03
- oxts
- velodyne-points
  • 时间戳 :在 velodyne-points 文件夹下有三个时间戳文件:
    • timestamps_start.txt:激光扫描仪一周开始扫描的时间。
    • timestamps_end.txt:激光扫描仪一周扫描结束的时间。
    • timestamps.txt:激光扫描到正前方触发相机拍照的时间。
  • 图像数据
    • 图像是裁剪掉了引擎盖和天空之后的图像。
    • 图像是去畸变之后的数据。
  • OXTS 数据:每一帧存储了包括经纬度、速度、加速度等 30 个不同的字段值。
  • 雷达数据
    • 浮点数的二进制文件,每个点由 4 个浮点数组成,分别为雷达坐标系下的 x,y,z 坐标和激光的反射强度 r
    • 每扫描一次,大约得到 120000 个 3D 点。
    • 激光雷达绕垂直轴逆时针转动。

传感器标定及时间同步

整个系统以激光雷达旋转一周为 1 帧。激光雷达旋转到某个特定位置时,通过一个弹簧片的物理接触触发相机拍照。IMU 无法通过这种触发方式采集数据,但 IMU 数据采集的频率达到了 100Hz,通过对 IMU 采集的数据记录时间戳,选择与相机采集时间最接近的作为当前帧的数据,最大时间误差为 5ms。

相机标定 :相机内外参标定使用的方法是 A toolbox for automatic calibration of range and camera sensors using a single shot。所有相机的中心都已对齐,即它们都在相同的 XY 平面上,这便于图像的校正(去除天空和引擎盖)。

每天开始采集数据前,都会对整个数据采集系统进行标定,以避免传感器间位置的偏移。

在每天的 date_calib.zip 文件夹下,有 3 个文本文件,用于记录传感器之间的坐标转换关系等信息。

二、 用于 3D 目标检测

最初,KITTI 数据集支持的任务包括双目、光流和里程计。后来,陆续支持了深度估计、2D 目标检测、3D 目标检测、BEV 目标检测、语义分割、实例分割、多目标追踪等任务。

在目标检测中,定义的类别有 8 种:Car/Van/Truck/Pedestrian/Person_sitting/Cyclist/Tram/Misc(其他)

对 3D 对象的标注是在激光雷达坐标系下进行的。需要注意的是,目前 3D 目标检测只由检测框中心点坐标 xyz、检测框的长宽高 length/width/height 和检测框的偏航角 yaw 这 7 个自由度组成。

不过这里有一个容易引起歧义的问题:length/width/height 分别对应 xyz 的哪个轴呢?从下图可以看出,length 对应 dxwidth 对应 dyheight 对应 dz。整个 3D 框的标注在激光雷达坐标系下。

KITTI 3D 目标检测数据集包括 7481 张训练数据和 7518 张测试数据。尽管 KITTI 数据集中包含了标注了 8 种对象,但只有 Car/Pedestrian 标注得比较充分,KITTI 官方用来评估算法。在 KITTI BenchMark 中使用的 3D 检测框类别有 3 个,分别是 Car/Pedestrian/Cyclist

下载的 3D 目标检测数据集包含以下文件夹:

  • image_02:左目彩色 png 格式的图像。
  • label_02:左目彩色图像中标注的对象标签。
  • calib:传感器之间的坐标转换关系。
  • velodyne:激光点云数据。
  • plane:在激光坐标系下,路面的平面方程。

标签文件中每一行内容如下:

复制代码
Pedestrian 0.00 0 -0.20 712.40 143.00 810.73 307.92 1.89 0.48 1.20 1.84 1.47 8.41 0.01

包含的字段有:

  • type:目标的类型,如 Car/Van 等,1 个字段。
  • truncated:浮点数 0-1,目标对象离开相机视野的比例,1 个字段。
  • occluded:整数 0,1,2,30 表示全部可见,1 表示部分遮挡,2 表示大部分未遮挡,3 表示未知,1 个字段。
  • alpha:对象的观测角,1 个字段。
  • bbox:2D 检测框像素坐标,x1,y1,x2,y2,4 个字段。
  • dimensions:3D 对象的尺寸,height,width,length,单位是米,3 个字段。
  • location:3D 对象在相机坐标系下的中心坐标 xyz,单位是米。
  • rotation_yyaw 角,偏航角。
  • score:目标对象的评分,用来计算 ROC 曲线或 MAP。

相机坐标系中的点可以通过 calib 中的变换矩阵变换到图像像素坐标中。

rotation_yalpha 的区别在于:alpha 度量的是相机中心到对象中心的角度。rotation_y 度量的是对象绕相机坐标系 y 轴的转角 yaw。以汽车为例,当一辆车在相机坐标系下位于 x 轴方向时,其 rotation_y 角为零,无论这辆车在 x,z 平面的哪个位置。而对于 alpha 角来说,仅当汽车在相机坐标系下 z 轴上时,alpha 角为零,偏离 z 轴时,alpha 角都不为零。

将激光点云投影到左目彩色图像中可以使用的公式为:X = P2 * R0_rect * Tr_vel_to_cam * Y

其中,R0_rect 是 3x3 的校正矩阵,Tr_vel_to_cam 是 3x4 的雷达变换到相机坐标系下的变换矩阵。

三、代码实战

使用 open3d 读取点云

python 复制代码
import numpy as np
import struct
import open3d as o3d

def convert_kitti_bin_to_pcd(binFilePath):
    size_float = 4
    list_pcd = []
    with open(binFilePath, "rb") as f:
        byte = f.read(size_float * 4)
        print(byte)
        while byte:
            x, y, z, intensity = struct.unpack("ffff", byte)
            list_pcd.append([x, y, z])
            byte = f.read(size_float * 4)
    np_pcd = np.asarray(list_pcd)
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(np_pcd)
    return pcd

bs = "/xx/xx/data/code/mmdetection3d/demo/data/kitti/000008.bin"
pcds = convert_kitti_bin_to_pcd(bs)
o3d.visualization.draw_geometries([pcds])

# save
o3d.io.write_point_cloud('000008.pcd', pcds, write_ascii=False, compressed=False, print_progress=False)

通过 numpy 读取:

python 复制代码
def load_bin(bin_file):
    data = np.fromfile(bin_file, np.float32).reshape((-1, 4))
    data = data[:, :-1]
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(data)
    return pcd

上述代码可以读取并可视化点云数据,并将其保存为 pcd 格式的点云。

选取的 3D 目标检测任务数据集中的 training/image_02/000008.png,对应的标签文件为 000008.txt,内容如下:

复制代码
Car 0.88 3 -0.69 0.00 192.37 402.31 374.00 1.60 1.57 3.23 -2.70 1.74 3.68 -1.29
Car 0.00 1 2.04 334.85 178.94 624.50 372.04 1.57 1.50 3.68 -1.17 1.65 7.86 1.90
Car 0.34 3 -1.84 937.29 197.39 1241.00 374.00 1.39 1.44 3.08 3.81 1.64 6.15 -1.31
Car 0.00 1 -1.33 597.59 176.18 720.90 261.14 1.47 1.60 3.66 1.07 1.55 14.44 -1.25
Car 0.00 0 1.74 741.18 168.83 792.25 208.43 1.70 1.63 4.08 7.24 1.55 33.20 1.95
Car 0.00 0 -1.65 884.52 178.31 956.41 240.18 1.59 1.59 2.47 8.48 1.75 19.96 -1.25

将其画在 000008.png 上的代码如下:

python 复制代码
import cv2
img = cv2.imread(img_s)
with open(label_s) as f:
    lines = f.read().split("n")[:-1]
for item in lines:
    boxes = item.split()[4:8]
    boxes = [float(x) for x in boxes]
    bb = np.array(boxes, dtype=np.int32)
    cv2.rectangle(img, bb[:2], bb[-2:], (0,0,255), 1)
cv2.imwrite("/xx/xx/data/code/mmdetection3d/demo/data/kitti/08_res.png", img)

从图中可以看出,左下角两个检测框的边缘部分标注得并不理想。

对应的点云数据如下:

KITTI 3D 目标检测的数据标签给出的 3D 中心点的坐标是在左目彩色相机坐标系中。

使用 Open3D 可视化检测框的代码可以参考以下内容:

python 复制代码
"""
from https://github.com/dtczhl/dtc-KITTI-For-
Beginners.git
"""
import matplotlib.pyplot as plt
import matplotlib.image as mping
import numpy as np
import os
import open3d as o3d
import open3d.visualization as o3d_vis
from shapely.geometry import Point
from shapely.geometry.polygon import Polygon


MARKER_COLOR = {
    'Car': [1, 0, 0],               # red
    'DontCare': [0, 0, 0],          # black
    'Pedestrian': [0, 0, 1],        # blue
    'Van': [1, 1, 0],               # yellow
    'Cyclist': [1, 0, 1],           # magenta
    'Truck': [0, 1, 1],             # cyan
    'Misc': [0.5, 0, 0],            # maroon
    'Tram': [0, 0.5, 0],            # green
    'Person_sitting': [0, 0, 0.5]}  # navy

# image border width
BOX_BORDER_WIDTH = 5

# point size
POINT_SIZE = 0.005

def show_object_in_image(img_filename, label_filename):
    img = mping.imread(img_filename)
    with open(label_filename) as f_label:
        lines = f_label.readlines()
        for line in lines:
            line = line.strip('n').split()
            left_pixel, top_pixel, right_pixel, bottom_pixel = [int(float(line[i])) for i in range(4, 8)]
            box_border_color = MARKER_COLOR[line[0]]
            for i in range(BOX_BORDER_WIDTH):
                img[top_pixel+i, left_pixel:right_pixel, :] = box_border_color
                img[bottom_pixel-i, left_pixel:right_pixel, :] = box_border_color
                img[top_pixel:bottom_pixel, left_pixel+i, :] = box_border_color
                img[top_pixel:bottom_pixel, right_pixel-i, :] = box_border_color
    plt.imshow(img)
    plt.show()

def show_object_in_point_cloud(point_cloud_filename, label_filename, calib_filename):
    pc_data = np.fromfile(point_cloud_filename, '<f4')  # little-endian float32
    pc_data = np.reshape(pc_data, (-1, 4))
    cloud = o3d.geometry.PointCloud()
    cloud.points = o3d.utility.Vector3dVector(pc_data[:,:-1])
    pc_color = np.ones((len(pc_data), 3))
    calib = load_kitti_calib(calib_filename)
    rot_axis = 2
    with open(label_filename) as f_label:
        lines = f_label.readlines()
        bboxes_3d = []
        for line in lines:
            line = line.strip('n').split()
            point_color = MARKER_COLOR[line[0]]
            veloc, dims, rz, box3d_corner = camera_coordinate_to_point_cloud(line[8:15], calib['Tr_velo_to_cam'])
            bboxes_3d.append(np.concatenate((veloc, dims, np.array([rz]))))
        bboxes_3d = np.array(bboxes_3d)
        print(bboxes_3d.shape)
        lines = []
        for i in range(len(bboxes_3d)):
            center = bboxes_3d[i, 0:3]
            dim = bboxes_3d[i, 3:6]
            yaw = np.zeros(3)
            yaw[rot_axis] = bboxes_3d[i, 6]
            rot_mat = o3d.geometry.get_rotation_matrix_from_xyz(yaw)
            # bottom center to gravity center
            center[rot_axis] += dim[rot_axis] / 2

            box3d = o3d.geometry.OrientedBoundingBox(center, rot_mat, dim)
            
            line_set = o3d.geometry.LineSet.create_from_oriented_bounding_box(
                box3d)
            line_set.paint_uniform_color(np.array(point_color) / 255.)
            lines.append(line_set)
        
        for i, v in enumerate(pc_data):
            if point_in_cube(v[:3], box3d_corner) is True:
                pc_color[i, :] = point_color
                
        cloud.colors = o3d.utility.Vector3dVector(pc_color)
        o3d_vis.draw([*lines, cloud])

def point_in_cube(point, cube):
    z_min = np.amin(cube[:, 2], 0)
    z_max = np.amax(cube[:, 2], 0)

    if point[2] > z_max or point[2] < z_min:
        return False

    point = Point(point[:2])
    polygon = Polygon(cube[:4, :2])

    return polygon.contains(point)

def load_kitti_calib(calib_file):
    """
    This script is copied from https://github.com/AI-liu/Complex-YOLO
    """
    with open(calib_file) as f_calib:
        lines = f_calib.readlines()

    P0 = np.array(lines[0].strip('n').split()[1:], dtype=np.float32)
    P1 = np.array(lines[1].strip('n').split()[1:], dtype=np.float32)
    P2 = np.array(lines[2].strip('n').split()[1:], dtype=np.float32)
    P3 = np.array(lines[3].strip('n').split()[1:], dtype=np.float32)
    R0_rect = np.array(lines[4].strip('n').split()[1:], dtype=np.float32)
    Tr_velo_to_cam = np.array(lines[5].strip('n').split()[1:], dtype=np.float32)
    Tr_imu_to_velo = np.array(lines[6].strip('n').split()[1:], dtype=np.float32)

    return {'P0': P0, 'P1': P1, 'P2': P2, 'P3': P3, 'R0_rect': R0_rect,
            'Tr_velo_to_cam': Tr_velo_to_cam.reshape(3, 4),
            'Tr_imu_to_velo': Tr_imu_to_velo}

def camera_coordinate_to_point_cloud(box3d, Tr):
    """
    This script is copied from https://github.com/AI-liu/Complex-YOLO
    """
    def project_cam2velo(cam, Tr):
        T = np.zeros([4, 4], dtype=np.float32)
        T[:3, :] = Tr
        T[3, 3] = 1
        T_inv = np.linalg.inv(T)
        lidar_loc_ = np.dot(T_inv, cam)
        lidar_loc = lidar_loc_[:3]
        return lidar_loc.reshape(1, 3)

    def ry_to_rz(ry):
        angle = -ry - np.pi / 2

        if angle >= np.pi:
            angle -= np.pi
        if angle < -np.pi:
            angle = 2 * np.pi + angle
        return angle

    h, w, l, tx, ty, tz, ry = [float(i) for i in box3d]
    cam = np.ones([4, 1])
    cam[0] = tx
    cam[1] = ty
    cam[2] = tz
    t_lidar = project_cam2velo(cam, Tr)

    Box = np.array([[-l / 2, -l / 2, l / 2, l / 2, -l / 2, -l / 2, l / 2, l / 2],
                    [w / 2, -w / 2, -w / 2, w / 2, w / 2, -w / 2, -w / 2, w / 2],
                    [0, 0, 0, 0, h, h, h, h]])

    rz = ry_to_rz(ry)

    rotMat = np.array([
        [np.cos(rz), -np.sin(rz), 0.0],
        [np.sin(rz), np.cos(rz), 0.0],
        [0.0, 0.0, 1.0]])

    velo_box = np.dot(rotMat, Box)

    cornerPosInVelo = velo_box + np.tile(t_lidar, (8, 1)).T

    box3d_corner = cornerPosInVelo.transpose()
    dims = np.array([l, w, h])
    # t_lidar: the x, y coordinator of the center of the object
    # box3d_corner: the 8 corners
    print(t_lidar.shape)
    return t_lidar.reshape(-1), dims, rz, box3d_corner.astype(np.float32)

if __name__ == '__main__':

    # updates
    ROOT = "/media/lx/data/code/mmdetection3d/demo/data/kitti"
    IMG_DIR = f'{ROOT}/image_2'
    LABEL_DIR = f'{ROOT}/label_2'
    POINT_CLOUD_DIR = f'{ROOT}/velo'
    CALIB_DIR = f'{ROOT}/calib'

    # id for viewing
    file_id = 8

    img_filename = os.path.join(IMG_DIR, '{0:06d}.png'.format(file_id))
    label_filename = os.path.join(LABEL_DIR, '{0:06d}.txt'.format(file_id))
    pc_filename = os.path.join(POINT_CLOUD_DIR, '{0:06d}.bin'.format(file_id))
    calib_filename = os.path.join(CALIB_DIR, '{0:06d}.txt'.format(file_id))

    # show object in image
    show_object_in_image(img_filename, label_filename)

    # show object in point cloud
    show_object_in_point_cloud(pc_filename, label_filename, calib_filename)

可视化的结果如下:

以上内容对 KITTI 数据集用于 3D 目标检测任务的情况进行了基本介绍。后续在涉及多模态时,将补充如何结合 2D 检测框来完成目标的识别和定位。

相关推荐
byxdaz10 分钟前
OpenCV实现图像分割与无缝合并
人工智能·opencv·计算机视觉
CASAIM16 分钟前
3D打印VS传统注塑:小批量生产如何选择?
3d·自动化
时光旅人01号40 分钟前
神经网络微调技术解析
人工智能·深度学习·神经网络
是十一月末1 小时前
Opencv之掩码实现图片抠图
人工智能·python·opencv·计算机视觉·图片识别
AI技术控1 小时前
计算机视觉算法实战——遗留物检测检测(主页有源码)
人工智能·算法·计算机视觉
AI技术控1 小时前
计算机视觉算法实战——花卉识别(主页有源码)
人工智能·算法·计算机视觉
壹十壹1 小时前
CV:图像的直方图均衡化
人工智能·opencv·计算机视觉
大势智慧1 小时前
用DasViewer的时候3Dtiles 转osgb 可以直接指定目标坐标系吗?
3d·实景三维·三维模型
今天炼丹了吗2 小时前
YOLOv11融合[CVPR2025]ARConv中的自适应矩阵卷积
人工智能·深度学习
时光旅人01号2 小时前
主流开源大模型能力对比矩阵
人工智能·pytorch·python·深度学习·开源