简介:本资源是一份面向自动驾驶算法工程师与计算机视觉研究者的nuScenes数据集入门实践指南,聚焦数据集结构解析、devkit环境搭建及核心API调用方法,解决初学者在真实场景3D感知任务中难以快速上手高质量开源数据的痛点。压缩包共5个文件(8KB),含README类说明文档(.md)、依赖配置(.txt)、网页版可视化示例(.html)、开发规范(.gitignore)及平台适配配置(.inscode),轻量简洁,便于快速部署与代码复用。已有119人学习下载,适合中初级开发者结合官方devkit开展场景探索、样本检索、传感器标定、3D标注框可视化及多模态数据联动分析。资源覆盖从数据加载、关键帧提取、相机-激光雷达跨模态对齐到类别统计与实例追踪的完整操作链,附带可直接运行的代码片段与结构化注释,显著降低nuScenes数据理解与工程接入门槛。
1. nuScenes 不是“另一个自动驾驶数据集”:它是多传感器时序闭环的硬核标尺,专治模型在真实城市场景中“认不清、跟不上、判不准”的顽疾
nuScenes 数据集远不止是一堆带标注的图像和点云——它是一套严格同步、跨模态对齐、带完整车辆运动学与交通语义的5秒闭环片段(scene)集合。每个 scene 包含 32 帧激光雷达扫描(32-line Velodyne)、6 个环视相机(前/后/左/右/左前/右前)、IMU+GPS 轨迹、20Hz 的车辆控制信号(油门/刹车/转向角),以及人工精标(≥1.5h/scene)的 3D bounding box(含速度、加速度、属性、可行驶区域、地图拓扑)。这意味着:你训练的检测模型若只在 KITTI 上跑通,放到 nuScenes 上大概率会在雨雾天漏检静止卡车、在路口误判被遮挡的骑行者、或把广告牌当成障碍物——因为 nuScenes 强制你直面传感器时间偏移、跨相机视差、动态物体轨迹连续性、地图先验融合这四大真实瓶颈。它适合正在落地 L2+/L3 自动驾驶感知模块的算法工程师、想验证多模态融合架构鲁棒性的研究员,以及需要构建可复现 baseline 的高校团队。如果你还在用单帧图像+离散框做“伪3D”,nuScenes 就是那剂必须打的清醒针。
2. 从零解压到加载第一帧:用官方 SDK 在本地跑通 nuScenes 最小闭环流程
nuScenes 官方 SDK 是唯一被全量验证过的接口层,它不提供“一键训练”,但把所有数据组织逻辑、坐标系转换、标注结构封装成可调用的 Python 对象。跳过 SDK 直接读原始 JSON/PCD 文件,90% 的人会在ego_pose和calibrated_sensor的嵌套坐标系里迷失方向。以下步骤基于nuScenes v1.0 full dataset(约32GB)和SDK v1.1.10(2023年稳定版),全程离线可执行。
2.1 下载与目录结构校验:别让压缩包毁掉前三小时
nuScenes 官网分三部分下载:v1.0-mini(约1.8GB,仅10个scene,用于快速验证)、v1.0-trainval(约27GB)、v1.0-test(约3.5GB,无标注)。强烈建议先下 mini 版——它包含完整数据结构,且nuscenes-devkit可直接加载,避免因网络中断或校验失败导致反复重试。
# 创建标准目录结构(必须严格匹配,SDK 依赖此路径) mkdir -p /data/nuscenes/{maps,images,sweeps,samples,annotations} # 将下载的 mini 包解压到对应子目录(注意:不是解压到 /data/nuscenes 根目录!) # 例如:mini-samples.tgz → /data/nuscenes/samples/ # mini-sweeps.tgz → /data/nuscenes/sweeps/ # mini-annotations.tgz → /data/nuscenes/annotations/ # mini-maps.tgz → /data/nuscenes/maps/ # mini-images.tgz → /data/nuscenes/images/提示:解压后检查
samples/CAM_FRONT/n008-2018-08-01-14-50-39-0400__CAM_FRONT__1533151603512404.jpg是否存在,这是 SDK 示例脚本默认加载的首帧图像。若缺失,SDK 会报KeyError: 'CAM_FRONT'而非文件路径错误——这是新手最常卡住的第一步。
2.2 安装 SDK 并初始化 Nuscenes 对象:绕开 pip install 的隐式依赖陷阱
官方文档推荐pip install nuscenes-devkit,但实际部署中常因pyquaternion版本冲突(v4.9.0+ 与旧版 numpy 不兼容)或numba编译失败导致 import 失败。生产环境应强制指定版本并禁用二进制加速:
pip install nuscenes-devkit==1.1.10 pyquaternion==4.8.0 numba==0.56.4 --no-binary numba初始化时必须传入dataroot和version,且version必须与下载包一致(mini 版为v1.0-mini,trainval 版为v1.0-trainval):
from nuscenes.nuscenes import NuScenes # 关键:dataroot 必须指向 /data/nuscenes(即 maps/images/sweeps 等子目录的父目录) nusc = NuScenes(version='v1.0-mini', dataroot='/data/nuscenes', verbose=True) print(f"Loaded {len(nusc.scene)} scenes, first scene token: {nusc.scene[0]['token']}") # 输出应为:Loaded 10 scenes, first scene token: ca9a282c9e77460f8360f564d31a8c32逻辑说明:NuScenes类在初始化时会扫描dataroot下所有 JSON 文件(scene.json,sample.json,sample_data.json等),构建内存索引表。verbose=True会打印加载进度,若卡在Loading scenes...超过30秒,大概率是dataroot路径错误或 JSON 文件损坏。
2.3 加载首帧并可视化:验证传感器同步与坐标系转换是否生效
nuScenes 的核心价值在于多传感器时间对齐。以下代码加载 scene-0 的第一帧(sample_token),并绘制 CAM_FRONT 图像 + 对应的 LiDAR 点云投影:
from nuscenes.utils.data_classes import LidarPointCloud from nuscenes.utils.geometry_utils import view_points, box_in_image import numpy as np import cv2 # 获取 scene-0 的第一个 sample my_scene = nusc.scene[0] first_sample_token = my_scene['first_sample_token'] sample = nusc.get('sample', first_sample_token) # 加载 CAM_FRONT 图像 cam_front_token = sample['data']['CAM_FRONT'] cam_front_data = nusc.get('sample_data', cam_front_token) im = cv2.imread(nusc.get_sample_data_path(cam_front_token)) # 加载对应 LiDAR sweep(注意:不是 sample_data 中的 lidar_token,而是该 sample 关联的 lidar 数据) lidar_token = sample['data']['LIDAR_TOP'] lidar_data = nusc.get('sample_data', lidar_token) lidar_pointcloud = LidarPointCloud.from_file(nusc.get_sample_data_path(lidar_token)) # 关键:将点云从 lidar 坐标系 → ego vehicle → camera 坐标系 → 图像平面 cs_record = nusc.get('calibrated_sensor', lidar_data['calibrated_sensor_token']) ego_record = nusc.get('ego_pose', lidar_data['ego_pose_token']) # lidar → ego lidar_pointcloud.rotate(Quaternion(cs_record['rotation']).rotation_matrix) lidar_pointcloud.translate(np.array(cs_record['translation'])) # ego → global → ego of camera → camera cam_cs_record = nusc.get('calibrated_sensor', cam_front_data['calibrated_sensor_token']) cam_ego_record = nusc.get('ego_pose', cam_front_data['ego_pose_token']) # 先 ego → global lidar_pointcloud.translate(-np.array(ego_record['translation'])) lidar_pointcloud.rotate(Quaternion(ego_record['rotation']).rotation_matrix.T) # 再 global → ego of camera lidar_pointcloud.translate(np.array(cam_ego_record['translation'])) lidar_pointcloud.rotate(Quaternion(cam_ego_record['rotation']).rotation_matrix) # 最后 ego of camera → camera lidar_pointcloud.rotate(Quaternion(cam_cs_record['rotation']).rotation_matrix.T) lidar_pointcloud.translate(-np.array(cam_cs_record['translation'])) # 投影到图像 points = view_points(lidar_pointcloud.points[:3, :], np.array(cam_cs_record['camera_intrinsic']), normalize=True) # 过滤超出图像边界的点 mask = np.logical_and(points[0, :] > 0, points[0, :] < im.shape[1]) mask = np.logical_and(mask, points[1, :] > 0) mask = np.logical_and(mask, points[1, :] < im.shape[0]) points = points[:, mask] # 绘制点云投影(绿色点) for i in range(points.shape[1]): cv2.circle(im, (int(points[0, i]), int(points[1, i])), 1, (0, 255, 0), -1) cv2.imshow('CAM_FRONT with LiDAR projection', im) cv2.waitKey(0)参数说明:
view_points()是 SDK 提供的投影函数,第三个参数normalize=True表示归一化到 [0,1],需手动乘以图像宽高;cs_record['camera_intrinsic']是 3×3 内参矩阵,包含焦距和主点偏移;- 所有旋转使用
Quaternion类而非 raw matrix,避免欧拉角万向节死锁; - 点云平移/旋转顺序必须严格按
lidar → ego → global → ego_cam → camera执行,颠倒顺序会导致投影偏移超100像素。
3. 解析 nuScenes 标注结构:从 JSON 到可训练 label 的三步映射
nuScenes 的标注不是扁平的 bbox 列表,而是由sample_annotation.json、instance.json、category.json三层嵌套构成的图结构。直接读取sample_annotation会得到 3D 框中心在全局坐标系下的(x,y,z),但训练模型需要的是相对于 ego vehicle 的局部坐标系下的(x,y,z,l,w,h,yaw),且需过滤掉被遮挡、截断、尺寸异常的实例。以下是工业级清洗 pipeline。
3.1 从 sample_annotation 到 ego-centric 3D box:坐标系转换的不可省略环节
sample_annotation中的translation字段是 box 中心在global coordinate system下的位置,而模型输入必须是相对于当前 ego vehicle 的位置。转换需两步:
- 获取该 annotation 关联的
sample_data(即 lidar sweep)的ego_pose; - 用
ego_pose的逆变换将 global 坐标转为 ego 坐标。
def get_ego_centric_box(nusc, ann_token): """返回 ego-centric 坐标系下的 3D box 参数 [x,y,z,l,w,h,yaw]""" ann_record = nusc.get('sample_annotation', ann_token) # 获取该 annotation 所属 sample 的 lidar 数据 sample = nusc.get('sample', ann_record['sample_token']) lidar_token = sample['data']['LIDAR_TOP'] lidar_sd = nusc.get('sample_data', lidar_token) ego_pose = nusc.get('ego_pose', lidar_sd['ego_pose_token']) # global → ego 变换:先平移再旋转(注意逆序) global_xyz = np.array(ann_record['translation']) # 平移:减去 ego 在 global 下的位置 ego_xyz = global_xyz - np.array(ego_pose['translation']) # 旋转:用 ego_pose 的旋转矩阵的转置(即逆旋转) R = Quaternion(ego_pose['rotation']).rotation_matrix.T ego_xyz = R @ ego_xyz # 获取尺寸和 yaw(yaw 在 global 坐标系下,需转换到 ego 坐标系) size = np.array(ann_record['size']) # [l, w, h] # yaw 是 global 坐标系下 box 的朝向角(绕 z 轴),转换到 ego 坐标系需减去 ego 的 yaw global_yaw = Quaternion(ann_record['rotation']).yaw_pitch_roll[0] ego_yaw = Quaternion(ego_pose['rotation']).yaw_pitch_roll[0] yaw_ego = global_yaw - ego_yaw return np.concatenate([ego_xyz, size, [yaw_ego]]) # 示例:获取 scene-0 第一个 sample 的第一个 annotation sample = nusc.get('sample', nusc.scene[0]['first_sample_token']) ann_token = sample['anns'][0] # 第一个标注 token box_ego = get_ego_centric_box(nusc, ann_token) print(f"Ego-centric box: {box_ego}") # [x,y,z,l,w,h,yaw] 单位:米/弧度逻辑说明:yaw的转换是易错点。nuScenes 的rotation字段是四元数,yaw_pitch_roll[0]提取的是绕 z 轴的偏航角(范围 [-π, π])。直接相减可能产生 [-2π, 2π] 范围,需用np.arctan2(np.sin(diff), np.cos(diff))归一化,但实际训练中多数框架(如 mmdetection3d)会自动处理,此处暂省略。
3.2 过滤低质量标注:用 visibility_token 和 num_lidar_pts 做双保险
nuScenes 为每个 annotation 提供visibility_token(值为 "0-4",对应 0%-100% 可见度)和num_lidar_pts(该物体在 lidar sweep 中的点数)。必须过滤 visibility < 2(即可见度 < 40%)且 num_lidar_pts < 5 的实例,否则模型会学习到大量噪声标签:
def filter_annotations(nusc, sample_token): """返回高质量 annotation tokens 列表""" sample = nusc.get('sample', sample_token) ann_tokens = sample['anns'] valid_anns = [] for ann_token in ann_tokens: ann = nusc.get('sample_annotation', ann_token) # 过滤 visibility < 2 的实例 if ann['visibility_token'] in ['1', '0']: # 1=10-40%, 0=0-10% continue # 过滤 lidar 点数过少的实例(常见于远处小物体或遮挡严重) if ann['num_lidar_pts'] < 5: continue # 过滤类别为 barrier、trafficcone 等非关键障碍物(按需) category = nusc.get('category', ann['category_token'])['name'] if category in ['movable_object.barrier', 'movable_object.trafficcone']: continue valid_anns.append(ann_token) return valid_anns # 应用过滤 valid_anns = filter_annotations(nusc, sample['token']) print(f"Original {len(sample['anns'])} annotations → filtered {len(valid_anns)}")注意:
num_lidar_pts是该物体在当前 lidar sweep中的点数,不是整个 scene 的累计点数。nuScenes 的标注是 per-sweep 的,因此同一辆车在不同帧可能有不同的num_lidar_pts。
3.3 构建 YOLO3D 兼容 label:生成 .txt 文件的标准格式
主流 3D 检测框架(如 OpenPCDet、mmdet3d)要求 label 文件为每行一个 object,格式为class_id x y z l w h yaw。nuScenes 的category名称需映射为整数 ID:
| Category Name | ID | Description |
|---|---|---|
| car | 0 | 乘用车 |
| truck | 1 | 卡车/货车 |
| bus | 2 | 公交车 |
| trailer | 3 | 拖车 |
| construction_vehicle | 4 | 工程车 |
| pedestrian | 5 | 行人 |
| motorcycle | 6 | 摩托车 |
| bicycle | 7 | 自行车 |
CATEGORY_MAP = { 'vehicle.car': 0, 'vehicle.truck': 1, 'vehicle.bus.rigid': 2, 'vehicle.trailer': 3, 'vehicle.construction': 4, 'human.pedestrian': 5, 'vehicle.motorcycle': 6, 'vehicle.bicycle': 7 } def save_yolo3d_label(nusc, sample_token, label_dir): """生成 YOLO3D 格式 label 文件""" sample = nusc.get('sample', sample_token) valid_anns = filter_annotations(nusc, sample_token) label_path = os.path.join(label_dir, f"{sample_token}.txt") with open(label_path, 'w') as f: for ann_token in valid_anns: ann = nusc.get('sample_annotation', ann_token) cat_token = ann['category_token'] cat_name = nusc.get('category', cat_token)['name'] # 映射类别 ID if cat_name not in CATEGORY_MAP: continue class_id = CATEGORY_MAP[cat_name] # 获取 ego-centric box box_ego = get_ego_centric_box(nusc, ann_token) # 写入:class_id x y z l w h yaw(单位:米/弧度) line = f"{class_id} {box_ego[0]:.4f} {box_ego[1]:.4f} {box_ego[2]:.4f} " line += f"{box_ego[3]:.4f} {box_ego[4]:.4f} {box_ego[5]:.4f} {box_ego[6]:.4f}\n" f.write(line) # 为 scene-0 的所有 sample 生成 label scene = nusc.scene[0] sample_token = scene['first_sample_token'] while sample_token != '': save_yolo3d_label(nusc, sample_token, '/data/nuscenes/yolo3d_labels') sample = nusc.get('sample', sample_token) sample_token = sample['next'] # 遍历 scene 内所有 sample4. 多模态数据加载的避坑指南:那些让 DataLoader 卡死、显存爆满、坐标错乱的血泪经验
nuScenes 的多传感器异构数据(图像、点云、雷达回波、IMU)在 PyTorch DataLoader 中极易触发隐式 bug。以下 4 条是我在 3 个项目中踩出的硬核坑,每条都附带现象、根因和可复制的修复代码。
4.1 现象:DataLoader worker 进程卡死在cv2.imread(),GPU 显存占用为 0
原因:OpenCV 的cv2.imread()在多进程环境下(num_workers > 0)会与某些 GPU 驱动冲突,尤其在 Ubuntu 20.04 + CUDA 11.3 组合中。根本原因是 OpenCV 默认启用 IPP(Intel Performance Primitives)加速,而 IPP 的线程池与 PyTorch DataLoader 的 fork 机制不兼容。
解决:强制禁用 IPP,并改用 PIL 读图(PIL 在多进程下更稳定):
# 替换 cv2.imread() from PIL import Image import numpy as np def pil_load_img(path): """用 PIL 安全读图,避免 cv2 多进程卡死""" with Image.open(path) as img: return np.array(img) # RGB uint8 array # 在 Dataset.__getitem__() 中调用 img_path = nusc.get_sample_data_path(cam_token) image = pil_load_img(img_path) # 而非 cv2.imread(img_path)提示:PIL 读图默认为 RGB,与 OpenCV 的 BGR 一致,无需
cv2.cvtColor()转换。
4.2 现象:点云投影到图像后整体偏移 50 像素,且随帧数增加偏移量变大
原因:sample_data的timestamp与ego_pose的timestamp存在微秒级偏差(nuScenes 文档明确说明:lidar timestamp 与 ego_pose timestamp 最大偏差为 50ms)。若直接用sample_data['ego_pose_token']查找 pose,会选错最近的 ego_pose,导致坐标系转换累积误差。
解决:对每个sample_data,搜索时间戳最接近的ego_pose(而非直接查 token):
def get_closest_ego_pose(nusc, timestamp, sensor_token): """根据 timestamp 查找最近的 ego_pose,解决 timestamp 偏差""" # 获取该 sensor 的所有 ego_pose 记录(按时间排序) pose_records = nusc.ego_pose # 找到时间戳最接近的记录 diffs = [abs(p['timestamp'] - timestamp) for p in pose_records] closest_idx = np.argmin(diffs) return pose_records[closest_idx] # 在点云加载时调用 lidar_sd = nusc.get('sample_data', lidar_token) closest_ego = get_closest_ego_pose(nusc, lidar_sd['timestamp'], lidar_token) # 后续用 closest_ego['token'] 替代 lidar_sd['ego_pose_token']4.3 现象:训练时 loss 突然 nan,debug 发现box_ego[2](z 坐标)为极大负数(如 -1e8)
原因:sample_annotation['translation']中的 z 值是全局坐标系下的绝对高度,但 nuScenes 的地图高程基准面(map origin)与 ego vehicle 的 z=0 平面不重合。当车辆驶入坡道或桥下时,global_z - ego_z可能超出 float32 表示范围,导致数值溢出。
解决:对 z 坐标做截断,并添加 sanity check:
def safe_get_ego_centric_box(nusc, ann_token): box_ego = get_ego_centric_box(nusc, ann_token) # 截断 z 坐标到 [-5, 5] 米(nuScenes 中所有物体 z 范围在此内) box_ego[2] = np.clip(box_ego[2], -5.0, 5.0) # 检查尺寸合理性 if not (0.1 < box_ego[3] < 20 and 0.1 < box_ego[4] < 5 and 0.1 < box_ego[5] < 5): return None # 过滤异常尺寸 return box_ego4.4 现象:multi-GPU 训练时,不同 GPU 加载的图像分辨率不一致(如 1600x900 vs 1280x720)
原因:nuScenes 的CAM_FRONT图像原始分辨率为 1600×900,但部分sample_data的is_key_frame为 False,其图像可能被压缩或降采样(官方未明说,但实测存在)。DataLoader 若未统一 resize,会导致 batch 内图像 shape 不一致。
解决:在 Dataset 中强制 resize 到固定尺寸,并记录原始尺寸用于后续后处理:
from torchvision.transforms import functional as F def resize_image(image, target_size=(900, 1600)): """保持宽高比 resize,并 pad 到 target_size""" h, w = image.shape[:2] scale = min(target_size[0]/h, target_size[1]/w) new_h, new_w = int(h*scale), int(w*scale) resized = cv2.resize(image, (new_w, new_h)) # pad to target_size pad_h = target_size[0] - new_h pad_w = target_size[1] - new_w padded = cv2.copyMakeBorder(resized, 0, pad_h, 0, pad_w, cv2.BORDER_CONSTANT) return padded, (h, w), (new_h, new_w), (pad_h, pad_w) # 返回值用于后续 bbox 坐标映射 image, orig_shape, resized_shape, pad_shape = resize_image(image)5. 用 nuScenes 评估你的模型:不只是 mAP,还有 nuScenes Detection Score(NDS)的 5 个维度拆解
nuScenes 的官方评估指标 NDS(nuScenes Detection Score)不是单一数值,而是mAP(mean Average Precision)与 4 个 error term 的加权和:NDS = 0.55 * mAP + 0.15 * mATE + 0.15 * mASE + 0.15 * mAOE。其中mATE(mean Translation Error)、mASE(mean Scale Error)、mAOE(mean Orientation Error)直接反映模型对物理世界的建模能力。忽略它们,等于放弃 nuScenes 的核心价值。
5.1 mAP 计算的陷阱:IoU 阈值不是 0.5,而是 0.5(3D)+ 0.5(BEV)+ 0.5(2D)三重判定
nuScenes 的 mAP 不是传统 2D IoU > 0.5,而是要求3D IoU > 0.5 AND BEV IoU > 0.5 AND 2D IoU > 0.5同时满足才计为 TP。这意味着:你的模型若在 3D 空间定位准但投影到图像有偏移,或 BEV 准但高度估计差,都会被判定为 FP。SDK 提供的DetectionBox类已封装此逻辑:
from nuscenes.eval.detection.evaluate import DetectionEval from nuscenes.eval.common.loaders import load_prediction, load_gt # 你的预测结果需按 nuScenes 格式组织(JSON) pred_boxes, _ = load_prediction('/path/to/prediction.json', max_boxes_per_sample=500) gt_boxes, _ = load_gt(nusc, 'val', detection_classes=['car', 'pedestrian']) # 初始化 evaluator evaluator = DetectionEval( nusc, config=NUCLEUS_CONFIG, # 使用官方 config(含 multi-threshold) result_path='/path/to/prediction.json', eval_set='val', output_dir='/path/to/eval_output', verbose=True ) evaluator.main() # 自动生成 mAP 和 NDS注意:
NUCLEUS_CONFIG是 nuScenes 官方定义的评估配置,包含 10 个 IoU 阈值(0.1~0.9),最终 mAP 是这 10 个阈值下的 AP 平均值。不要自行修改阈值。
5.2 mATE/mASE/mAOE 的物理意义:为什么它们比 mAP 更值得优化
| Metric | 公式 | 物理意义 | 优化方向 |
|---|---|---|---|
| mATE | mean(√[(x_pred-x_gt)²+(y_pred-y_gt)²+(z_pred-z_gt)²]) | 3D 中心位置误差(米) | 改进 backbone 的空间建模能力(如用 BEVFormer) |
| mASE | mean( | l_pred/l_gt -1 | + |
| mAOE | mean(min( | yaw_pred-yaw_gt | , 2π- |
关键洞察:mAP 高但 mATE > 1.0m 的模型,在真实自动驾驶中可能无法安全变道;mASE > 0.3 的模型,对卡车和轿车的尺寸区分能力弱,易导致跟车距离误判。因此,训练时应监控这三项指标,而非只盯 mAP。
5.3 用 nuScenes SDK 可视化失败案例:定位 mATE 高的根源帧
SDK 提供render_sample()和render_sample_data()函数,但默认渲染不显示误差。以下代码生成带误差标注的可视化图:
def render_error_analysis(nusc, sample_token, pred_boxes, gt_boxes, save_dir): """渲染预测与真值的差异,标注 mATE""" sample = nusc.get('sample', sample_token) # 渲染 CAM_FRONT 图像 cam_token = sample['data']['CAM_FRONT'] nusc.render_sample_data(cam_token, axes_limit=40, out_path=f'{save_dir}/front.png') # 计算并标注每个 box 的 mATE for i, pred in enumerate(pred_boxes): if i >= len(gt_boxes): continue gt = gt_boxes[i] # 计算 3D 中心距离 dist = np.linalg.norm(np.array(pred['translation']) - np.array(gt['translation'])) # 在图像上标注距离(需投影到图像平面) # ...(投影逻辑同 2.3 节) # cv2.putText(im, f'ATE: {dist:.2f}m', (x, y), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255), 1) # 保存带误差标注的图 cv2.imwrite(f'{save_dir}/error_{sample_token[:8]}.png', im) # 调用 render_error_analysis(nusc, 'ca9a282c9e77460f8360f564d31a8c32', pred_list, gt_list, '/vis/error')我习惯在训练第 50 个 epoch 后,抽 100 个 mATE > 0.8m 的样本,用此脚本批量生成误差图。90% 的 case 都集中在密集路口的遮挡车辆和雨天反光路面的低矮障碍物——这直接指导我增加对应场景的数据增强(如随机遮挡、雨滴 overlay)。
希望帮到你。
本文还有配套的精品资源,点击获取