资讯动态

DAIR-V2X数据集实战指南:车路协同3D目标检测与点云融合

发布时间:2026/10/14 3:43:01 来源:尧图企业网站定制
简介面向自动驾驶与车路协同研究者的一份数据集详解资料围绕车路协同常用数据集展开。DAIR-V2X是首个用于车路协同研究的大规模多模态多视角数据集分为协同、路端、车端三个子集可满足不同场景下的感知研究需求。资料重点拆解其中协同子集的下载、解压与目录组织方式逐层说明原始数据、标注文件与元数据各自的作用并详细解释二维与三维标注的内容结构以及坐标系变换原理帮助读者快速建立从图像到三维空间的感知数据认知框架。资源体积非常轻量压缩包内仅有两个文件以网页说明文档为主附加一段代码片段总大小约为四KB无需安装额外依赖打开即可浏览说明与代码。目前已有三百五十三人学习。除目录与标注解析外资料还给出了常用的网盘获取渠道并客观总结了该数据集在协同感知应用中的优势与局限性对刚接触该领域的研究者是很好的选型参考和入门指引也能辅助后续在标注格式和坐标体系上的深入理解。1. 先讲清楚 DAIR-V2X 数据集在解决什么问题从单车感知盲区到车路协同DAIR-V2X 数据集不是一份简单的图片集合它是一套把车端感知、路端感知和协同感知统一起来的真实道路场景数据集核心目标是为 3D 目标检测和车路协同融合提供可复现的数据基准。它要解决的核心问题很具体单车智能在遮挡、远距离小目标和路口盲区场景下感知能力会明显退化而路侧设备视角高、盲区少恰好能补位。这套数据集把车端数据、路端数据以及两端对齐后的协同数据分别整理成子集并统一了标注、标定和时间同步格式适合做感知算法、多模态融合或 V2X 落地的工程师和研究者在同一套数据上做对照实验。下面我会按数据整体架构、标注体系、代码实操、避坑排查、协同对齐这条链完整展开新手可以照步骤跑通熟手可以直接跳到踩坑和进阶部分。2. 数据集整体架构车端、路端、协同三个子集的划分与文件组织2.1 为什么把数据拆成车端、路端、协同三套DAIR-V2X 在发布时把数据分成三条主线车端、路端和协同这三个子集的命名逻辑不是随意的而是对应车路协同研究中的三个基本问题单靠车能感知到什么程度、单靠路端能感知到什么程度、两端融合之后能不能超越任何一端。有了这三套数据你所有实验都可以在同一份真实道路采集上做而不是像以前那样自己去拼接不同来源的数据集做对照。车端数据模拟的是一台搭载激光雷达和多目摄像头的自动驾驶车在真实道路上的感知视角。它的优势是覆盖范围随车辆移动能跟着车的行驶轨迹实时更新但弱点同样明显视角低、容易被前车或路侧建筑遮挡这是单车感知在市区道路上的天然短板。很多做感知的团队都会先把车端数据单独训练一个检测器把它当作“单车感知基线”存下来后面所有协同模型都要拿这个基线来比增益。路端数据则完全不同。它来自固定在路杆、龙门架或路侧机柜上的感知设备视角高、视野广能覆盖车端看不到的盲区。路端设备的观测位置是固定的扫描范围不会跟着车跑车辆一旦驶出覆盖区域路端就失去了作用。正是这种车端灵活、路端视野互补的特性构成了协同感知的基本出发点车端解决“跟车覆盖”的问题路端解决“盲区补偿”的问题。协同子集是这个数据集里最有价值的部分。它不是把车端和路端文件简单丢进一个目录而是按时间戳和空间位姿做了严格对齐保证车端和路端标注的是同一时刻、同一路段里的同一批目标。拿到协同子集后你才能正面回答“什么时候融合、在哪里融合、融合后能提升多少”这类问题。如果在这一步没有对齐后面所有协同算法实验都不具备可行性结果也经不起推敲。所以我强烈建议先花时间把三套数据的划分逻辑理解透再动手写代码这一步省时间也能避免后面反复返工。2.2 目录结构与文件命名拿到数据后先做完整性检查按常见的发布形式DAIR-V2X 解压后的目录结构大致长这样DAIR-V2X/ ├── vehicle/ │ ├── image/ # 车端摄像头图像 │ ├── lidar/ # 车端激光雷达点云 │ ├── label/ # 车端标注 JSON │ ├── calib/ # 车端标定参数 │ └── data_info/ # 采集时间、数据元信息 ├── infrastructure/ │ ├── image/ │ ├── lidar/ │ ├── label/ │ ├── calib/ │ └── data_info/ └── cooperative/ ├── image/ ├── lidar/ ├── label/ ├── calib/ └── data_info/这是 V2X 类数据集最通用的组织方式每个子集内部都有 image、lidar、label、calib 四个核心目录另外配一个 data_info 记录采集时间等元信息。image 里是 JPG 格式的彩色图像lidar 里是激光雷达点云常见的是 PCD 格式label 和 calib 则都是 JSON分别存放目标的 3D 标注框和传感器内外参。拿到压缩包后我一般会先做三件事来确认数据完整。第一统计每个子集下 image 和 lidar 的文件数量数量不一致说明中间有缺帧后面做时间匹配时会直接翻车第二检查每个文件的体积分卷传输或断点续传容易出现尾部损坏尤其是 PCD 这种二进制文件损坏后 open3d 读出来要么报错要么点云数量为零第三随机打开一个 calib JSON确认里面标定矩阵是否齐全不要等做到坐标转换时才发现缺了关键矩阵。提示如果数据集带 train/val 划分不要自己重新随机切分。协同子集的车辆端和路端是严格配对的乱打乱顺序会直接破坏时间对齐关系后续实验结果无法复现。2.3 数据分布统计不要急着看单帧先跑一遍全局统计真实道路数据的场景分布决定模型效果上限。很多人在 DAIR-V2X 上训练效果不佳不是因为模型结构有问题而是开始前根本不了解数据分布。我拿到数据后会第一时间写一个统计脚本遍历 label 目录统计每个类别的目标数量、每帧目标数以及遮挡目标的占比这个结果直接决定训练采样策略和数据增强的方向。import json from pathlib import Path label_dir Path(cooperative/label) class_counter {} obj_per_frame [] for label_file in label_dir.glob(*.json): with open(label_file, r, encodingutf-8) as f: data json.load(f) obj_per_frame.append(len(data)) for obj in data: t obj.get(type, unknown) class_counter[t] class_counter.get(t, 0) 1 print(类别统计:, class_counter) print(平均每帧目标数:, sum(obj_per_frame) / len(obj_per_frame))逻辑说明这段脚本依赖 json 和 pathlib 两个标准库遍历所有标注文件统计类别频次和帧密度。你还可以在此基础上扩展统计occluded字段不为 0 的目标数用来评估数据集中遮挡场景的比例。参数说明遍历路径要写成对应子集的 label 目录如果数据量有几万帧建议套一层tqdm显示进度否则扫描时长会让你以为是程序卡住了纯属玄学误判。分布统计做过之后你对这个数据集的认识会远超读十篇文章。你会直观地知道车辆类占多少、行人骑行者占多少、哪些帧目标特别密集这些数字直接影响你后续做 loss 加权还是做类别重采样值得花一晚上认真做。3. 数据格式与标注体系点云、图像、3D 框与标定参数3.1 点云与图像的存储格式及读取方式DAIR-V2X 的图像文件是标准 JPG直接用cv2.imread就能读进来没有特殊编码。点云则是 PCD 格式这是点云存储最通用的格式之一文件头部写明点数、字段和数据类型比如FIELDS x y z intensity表示每个点除了坐标还有反射强度字段。PCD 自描述性强一个文件就能独立完成读取不需要额外配套参数但不同来源的 PCD 可能分别使用 ASCII 和二进制两种编码读取时最好用成熟库而不是自己写解析器。import open3d as o3d import numpy as np pcd o3d.io.read_point_cloud(vehicle/lidar/000001.pcd) points np.asarray(pcd.points) print(f点云点数: {points.shape[0]}) print(fx 范围: [{points[:, 0].min():.2f}, {points[:, 0].max():.2f}])逻辑说明open3d 的read_point_cloud按扩展名自动识别格式读出来之后用points属性拿到 N×3 的坐标数组。最后打印 x 范围是为了确认读取结果是正常道路场景的数据范围如果你发现坐标范围出现了上千的数值先别纠结代码大概率是文件本身或坐标单位出了问题。参数说明open3d 对带 intensity 的 PCD 处理在不同版本上有差异某些版本会把反射强度解析到colors字段但points始终是三维坐标不影响本步骤。如果你更愿意用 pypcd它对 PCD 头信息解析更灵活代价是要多学一套接口。3.2 标签 JSON 的字段结构与 3D 框定义label 文件是一个 JSON 数组数组里每个元素描述一个 3D 目标字段结构和 KITTI 风格接近。像下面这样的结构是常见标注方式拿到手之后先打印一帧完整的 JSON确认你这份数据里字段名是否一致再写解析代码。不同批次的标注可能在字段命名上有细微差异直接照抄网上代码很常见地会踩坑。[ { type: Car, track_id: 10023, 3d_dimensions: { length: 4.5, width: 1.9, height: 1.6 }, 3d_location: { x: 12.34, y: 3.21, z: 0.0 }, rotation: 0.12, occluded: 0, truncated: 0 } ]各个字段的含义拆开来看字段含义单位/取值type目标类别Car、Truck、Bus、Pedestrian、Cyclist 等track_id跨帧追踪编号同一目标在不同帧中保持一致3d_dimensions目标三维尺寸length 为车头方向长度width 为侧面宽度height 为高度单位米3d_location目标中心坐标在传感器局部坐标系下x 向前、y 向左、z 向上单位米rotation绕 z 轴偏航角单位弧度occluded遮挡状态0 未遮挡1 部分遮挡2 大部分遮挡truncated截断状态目标超出图像边缘的比例逻辑说明这套标注格式和自动驾驶数据集的标准做法基本一致3d_location给出的是目标中心点在传感器坐标系下的位置rotation是车辆朝向与 x 轴的夹角。做 3D 检测时最常用的是 type、3d_location、rotation 和 3d_dimensions 四个字段。参数说明rotation 是弧度不是角度遇到数值在 0 到 6.28 之间可以直接用如果接近 0 到 360 就要先乘np.pi / 180转换再参与计算。3d_dimensions里的顺序也和部分框架不一致有些框架用 [height, width, length]这里用 [length, width, height]写转换代码前一定确认好。3.3 标定参数坐标转换链路中的三个关键矩阵标定文件解决的是不同传感器坐标系之间的换算问题。一套完整标定通常包含三类信息相机内参矩阵、激光雷达到相机的外参矩阵、以及车端和路端之间的世界坐标变换关系。相机内参是 3×3 矩阵把相机坐标系下的三维点投影到像素平面里面包含焦距 fx、fy 和主点坐标 cx、cy。做点云投影到图像时点要先转到相机坐标系再乘内参矩阵最后除以 z 分量才能得到像素坐标。外参矩阵是 4×4 齐次矩阵由旋转和平移拼成把激光雷达坐标系下的点转换到相机坐标系它来自传感器安装时的标定结果。车端和路端之间的变换则把路端感知结果转换到车端坐标系这块是协同数据特殊的地方做融合实验时一定会用到。import numpy as np import json def load_calib(path): with open(path, r) as f: calib json.load(f) lidar_to_camera np.array(calib[lidar_to_camera], dtypenp.float64) camera_intrinsic np.array(calib[camera_intrinsic], dtypenp.float64) return lidar_to_camera, camera_intrinsic逻辑说明代码从 JSON 里读出两个关键矩阵lidar_to_camera是 4×4 外参camera_intrinsic是 3×3 内参。读完后记得检查shape如果读出来是一维展开形式需要用reshape还原这是最常见的数据解析问题。参数说明JSON 里矩阵可能存成嵌套列表也可能是一维数组用np.array()之后马上打印 shape 验证。标定文件里如果还有车辆到路端的变换矩阵命名一般会带有 world 或 global 字样转换链路不要配错顺序。4. 用 Python 跑通一帧 DAIR-V2X 数据读取、投影、BEV 可视化4.1 读取图像与点云的最小代码跑通一帧数据是开始所有实验的前提完整读取只需要十几行。图像用 OpenCV点云用 Open3D这两个是目前工程上最主流的工具没必要自己实现解析器除非你要处理的是不公开的专有格式。import cv2 import open3d as o3d import numpy as np img cv2.imread(vehicle/image/000001.jpg) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) pcd o3d.io.read_point_cloud(vehicle/lidar/000001.pcd) points np.asarray(pcd.points) print(f图像分辨率: {img.shape}) print(f点云数量: {points.shape[0]})逻辑说明图像从 BGR 转成 RGB 是为了后续和 matplotlib 的颜色习惯一致如果你只是在调试面板里查看这行可以省掉。点云转成 numpy 数组后才可以和后面标注坐标做统一的矩阵运算。参数说明如果 open3d 读取 PCD 时报错先看文件头部描述的是 ASCII 还是二进制。用文本编辑器打开 PCD 文件前几行会有DATA ascii或DATA binary的标记如果标记和数据实际编码不一致open3d 就会解析失败这时候换 pypcd 往往能解决。4.2 把 3D 标注投影到图像上的完整流程图像、点云和标定参数都准备好之后把标注框投影到图像上是检查标定和标注是否一致的最直接手段。投影的数学链路是把激光雷达坐标系下的 3D 点转成齐次坐标乘以外参矩阵得到相机坐标系下的点再乘以内参矩阵得到像素坐标。import json import numpy as np def project_to_image(points_3d, lidar_to_camera, camera_intrinsic): points_h np.hstack([points_3d, np.ones((points_3d.shape[0], 1))]) cam_points points_h lidar_to_camera.T cam_points cam_points[:, :3] pixels cam_points camera_intrinsic.T pixels pixels[:, :2] / pixels[:, 2:3] return pixels with open(vehicle/label/000001.json) as f: label json.load(f) lidar_to_camera, camera_intrinsic load_calib(vehicle/calib/000001.json) for obj in label: loc obj[3d_location] pt np.array([[loc[x], loc[y], loc[z]]]) px project_to_image(pt, lidar_to_camera, camera_intrinsic) u, v int(px[0, 0]), int(px[0, 1]) cv2.circle(img, (u, v), 5, (0, 255, 0), -1)逻辑说明这段代码把 3D 点按行存储用points_h lidar_to_camera.T完成旋转和平移再乘内参矩阵最后用 z 分量归一化坐标。每标注个目标的中心点会画一个绿色圆点如果圆点和实际物体在图像上重叠说明标定和标注基本没问题。参数说明lidar_to_camera.T是转置操作因为这里的点按行排布而变换矩阵按常规列向量习惯计算转置后才能正确执行矩阵乘法。如果你习惯用列向量可以写成lidar_to_camera pt_h结果相同。投影中心点只能粗略验证对齐更严格的做法是把 3D 框的八个角点都投影到图像上再画线框。八个角点由中心点、长宽高和偏航角计算出来逐个执行同样的投影变换然后用线段连接。这个方法代码量会增加但能一次性暴露标定错误和标注错误值得在数据预处理阶段做一次。4.3 BEV 俯视图可视化检查标注框和点云的吻合程度图像投影只能验证 2D 对齐BEV 俯视图则能检查 3D 框的空间位置和方向是否真正吻合。这一步在协同数据上尤其重要因为 BEV 图能直接看出框是否转错了方向、尺寸是否对得上点云轮廓。import matplotlib.pyplot as plt def corners_from_box(cx, cy, length, width, yaw): c, s np.cos(yaw), np.sin(yaw) dxl, dyl 0.5 * length * c, 0.5 * length * s dxw, dyw 0.5 * width * -s, 0.5 * width * c pts np.array([ [cx dxl dxw, cy dyl dyw], [cx dxl - dxw, cy dyl - dyw], [cx - dxl - dxw, cy - dyl - dyw], [cx - dxl dxw, cy - dyl dyw], [cx dxl dxw, cy dyl dyw], ]) return pts plt.figure(figsize(8, 8)) plt.scatter(points[:, 0], points[:, 1], s0.05, cgray, alpha0.5) for obj in label: dim obj[3d_dimensions] loc obj[3d_location] corners corners_from_box(loc[x], loc[y], dim[length], dim[width], obj[rotation]) plt.plot(corners[:, 0], corners[:, 1], r-, linewidth2) plt.axis(equal) plt.xlabel(x (m)) plt.ylabel(y (m)) plt.show()逻辑说明corners_from_box根据中心点、长宽和偏航角计算四个角点并闭合回到第一个点形成矩形。x 方向是车辆前方y 方向是左侧所以 BEV 图看起来应该是一条道路的俯视场景。红色框应该刚好套住灰色点云轮廓。参数说明s0.05控制散点大小如果点云太密全变成灰色块就把 s 调小如果点太稀疏就调大。plt.axis(equal)必须保留否则横纵轴比例不一致时矩形会显示为拉长的平行四边形朝向看起来不正确。BEV 图里如果框和点云有偏移先确认你用的是车端标注还是路端标注。车端标注在车端激光雷达坐标系路端标注在路端激光雷达坐标系两个坐标系不能直接混用必须先把路端目标转到同一世界坐标系下再做可视化。这是协同数据最容易出错的地方之一把它理解透了后面做融合实验会顺利很多。5. 避坑排查DAIR-V2X 使用中常见的五个坑与解决方式5.1 现象按文件名直接匹配车端和路端帧对不上现象描述加载协同子集时通过文件名的相似编号直接匹配车端和路端帧结果可视化出来发现车端画面里是一辆正在左转的车路端画面里却是右转的路口两个视角完全对不上。原因分析虽然车端和路端的数据都按时间戳命名但两套设备的采集时钟可能不同步采集频率也不完全相等文件名的索引编号只是采集顺序并不能直接代表精确的同一时刻。直接拿文件名做 join等于假设两套设备从开机起就完全同步这在实际路采场景中几乎不成立。解决方式先从 data_info 目录读取每帧的精确时间戳然后做最近邻匹配。车端和路端各自的时间戳数组取差找到绝对值最小的配对同时设置一个最大时间差阈值超过阈值就丢弃这一对帧。匹配完成后再用可视化做交叉验证把同帧的两张图像并排放确认场景一致。5.2 现象点云投影到图像上偏移明显现象描述把激光雷达点云通过外参和内参投影到图像上结果点云没有出现在物体轮廓上而是整体平移了几米甚至几十米或者根本投到了画面之外。原因分析最常见的原因是标定矩阵的拼法错了。如果标定文件里给的是 3×3 旋转矩阵和 3×1 平移向量你需要把它们拼成 4×4 齐次矩阵顺序是前三行放旋转加平移最后一行补[0, 0, 0, 1]。不少人直接把旋转矩阵当外参用少了平移部分投影自然全偏。另外平移向量的单位也可能不统一有的标定用米有的用毫米相差 1000 倍。解决方式先打印标定矩阵的 shape检查是 4×4 还是由旋转和平移两块组成。如果是后者用np.hstack和np.vstack正确拼成齐次矩阵。然后取一帧点云和一帧图像投影一个远处的点和一个近处的点看偏差方向如果偏移量是固定的平移量基本可以判定是平移向量单位或拼接顺序问题。这一步是纯数值排查不依赖任何可视化工具。5.3 现象BEV 图上 3D 框朝向完全不对现象描述BEV 可视化时点云中的车辆轮廓朝北而 3D 框的长边却朝东每个框看起来像是整体旋转了 90 度有些框甚至反过来。原因分析rotation 也就是 yaw 角的方向定义在数据集中可能存在两种约定一种规定为从 x 轴正向逆时针旋转为正另一种规定为从 y 轴正向顺时针旋转为正。如果你的可视化代码用了全局固定公式而数据的旋转定义恰好相反就会出现统一偏移 90 度或 180 度的现象。这在多源数据集混用时特别常见因为每套数据集都有自己的方向约定。解决方式取一帧标注在 BEV 图上人工确认点云中车辆的朝向然后分别测试yaw、yaw π/2、yaw - π/2和-yaw四种情况看哪一种能把框套在点云轮廓上。找到正确公式后写成一个全局配置变量比如YAW_CONVENTION counterclockwise后续所有代码从这个配置读取换数据集时只需修改这一处不用到处改公式。5.4 现象PCD 点云读出来为空或点数极少现象描述open3d 读取 PCD 成功但np.asarray(pcd.points)得到的是空数组或者只有几百个点。打印 x 坐标范围时出现大量 NaN数据看起来完全不可用。原因分析一种情况是 PCD 文件本身是 ASCII 格式但文件尾部在传输或解压过程中丢失导致数据不完整另一种情况是文件头声明了FIELDS x y z intensity但实际数据体只有 x y z 三个维度解析器读到后半段时字段数量对不上于是把数据截断或标记为无效。两种情况的本质都是文件内容与头部声明不一致。解决方式先用文本编辑器打开 PCD 文件头部确认FIELDS和DATA两行。如果声明的是三通道但读取结果异常直接用 pypcd 再读一次或者用十六进制查看器检查文件尾部是否有截断标记。如果是下载导致的文件损坏重新从源位置下载对应文件即可这类问题通常不是代码的锅而文件校验才是根治办法。建议拿到数据后写一个简单的轮询脚本扫描所有 PCD 文件并统计点数一次性找出损坏文件而不是用到的时候才发愁。5.5 现象行人和骑行者类别训练效果差车辆类指标虚高现象描述训练完一个 3D 检测器在验证集上整体 mAP 看着不错但分拆到每个类别时Car 的 AP 明显高于其他类别Pedestrian 和 Cyclist 的 AP 特别低甚至不到 Car 的一半。原因分析这大概率不是模型结构的锅而是数据分布问题。真实道路数据集里车辆数量远多于行人和骑行者类别样本量相差一个数量级是很正常的。如果没有做类别平衡处理模型会被多数类主导把更多注意力放在车辆上少数类学不到有效特征。另一个原因是行人和骑行者在点云中的点数本身就少小目标在激光雷达数据里的特征较弱天然更难检测。解决方式先跑一遍第 2.3 节的统计脚本确定各类别的真实占比然后做分层采样让每个 epoch 里各类别的样本量按比例保持均衡。对小类别数据做数据增强比如水平翻转、随机平移、增加高斯噪声可以扩展一部分有效样本。另外也可以在 loss 里对不同类别加权重因为权重应该在训练初始阶段就设置纯靠训练后再调阈值来弥补少数类性能始终是事后补救效果有限。6. 进阶技巧把路端 3D 框转换到车端坐标系的完整流程协同感知里最核心的预处理就是把路端检测结果转换到车端坐标系这一步不做对后面所有融合策略都建立在错误的空间关系上。转换链路分三段路端激光雷达坐标系 → 世界坐标系 → 车端激光雷达坐标系需要数据集提供的两段变换矩阵。def convert_to_vehicle(box_rsu, transform_rsu_to_world, transform_world_to_vehicle): loc box_rsu[3d_location] pt_rsu np.array([loc[x], loc[y], loc[z], 1.0]) pt_world transform_rsu_to_world pt_rsu pt_vehicle transform_world_to_vehicle pt_world return pt_vehicle[:3]逻辑说明transform_rsu_to_world把路端局部坐标转到世界坐标transform_world_to_vehicle把世界坐标转到车端激光雷达坐标两个矩阵都是 4×4用齐次坐标连乘就能一次性完成。这样转换之后路端检测到的目标位置就能和你车端自己的感知结果放在同一个坐标系里做融合。参数说明注意两个矩阵的先后顺序一定是先路端到世界、再世界到车端。如果你发现转换后的坐标点全世界漂移先检查两个矩阵的乘序是否反了这是协同数据处理里最坑的一步。我自己的习惯是每次转换完都做一次离线验证把转换后的路端 3D 框画到车端点云的 BEV 图里如果框和车端点云里的同一辆车重合说明整个链路标定没问题。这个验证习惯帮我避开过很多次标定文件错用的问题。做协同融合先把空间对齐做扎实后面的模型实验才有意义希望这个思路能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑