nuScenes 3D 框转 2D 投影:坐标系对齐与 Python 实现
简介这份Python源码面向计算机视觉与自动驾驶方向的开发者聚焦nuScenes数据集中3D标注到2D图像的转换问题。资源围绕坐标系变换、相机投影、畸变校正以及3D边界框顶点映射等关键环节展开帮助读者理解如何将激光雷达等传感器中的三维检测结果准确落到摄像头图像平面上适用于算法验证、数据可视化与感知结果分析等场景。压缩包共2个文件均为py脚本整体约4KB分别承担2D边界框绘制与整体流程串联的职责结构轻量、便于直接阅读和二次修改。目前已有2646人学习下载说明该实现具备一定的参考价值。通过运行与调试这些代码读者可以掌握从世界坐标系到相机坐标系、再到像素坐标的完整转换链路并借助OpenCV完成边界框绘制为自动驾驶感知算法的开发与调试提供可复用的实践基础。1. 从 nuScenes 的 3D 框到 2D 框为什么你的标注投影总对不上做过自动驾驶感知的人多半碰过这个场景手头有一份 nuScenes 数据集想拿它训练 2D 检测器或者只想把 3D 标注框可视化出来核对一下结果把 3D box 往图像上一投框要么偏出目标几十个像素要么整个跑到画面外。这不是玄学是坐标系没对齐。nuScenes 的 3D 标注活在激光雷达和全局坐标系里而图像是相机坐标系下的针孔投影结果中间隔着外参、内参、畸变、时间戳同步四道关。所谓「nuScenes 数据集 3D 转 2D python 源码」本质就是写一套把 3D 框的八个角点经过传感器标定链路投影到每张相机图上并取外接矩形的代码。它解决的是「3D 真值复用」问题适合想用 nuScenes 做 2D 检测、做投影验证、做多模态对齐的从业者。下面我按自己踩过的顺序把这条链路拆开讲透。2. 先搞懂 nuScenes 的坐标系链路再动手写投影投影对不上九成是坐标系没理清。nuScenes 里一个 3D 框的坐标不是直接给相机的它要经过「全局 → 自车 → 传感器 → 相机 → 像素」这条链。很多人一上来就套K [R|t]结果忽略了自车坐标系和全局坐标系之间的位姿框自然飘。2.1 四个坐标系和它们之间的变换关系nuScenes 的标注框sample_annotation给的是全局坐标系下的translation、size、rotation。全局坐标系是那个大范围的地图坐标系自车坐标系ego pose是车辆自身位置传感器坐标系是每个相机、激光雷达自己的原点。相机坐标系是传感器坐标系的一种但方向定义不同相机看向 Z而激光雷达常见看向 X。变换关系是全局到自车用ego_pose自车到传感器用calibrated_sensor传感器到相机再套相机自身的旋转。这条链少一环投影就错。我一般会先把每个 sample 的ego_pose和calibrated_sensor都取出来确认时间戳一致再往下算。2.2 3D 框的八个角点怎么算出来nuScenes 的框是中心点加尺寸加四元数旋转。要投影得先把它展开成八个角点。尺寸是[width, length, height]注意顺序nuScenes 里 width 对应 xlength 对应 yheight 对应 z。旋转用四元数[w, x, y, z]转成旋转矩阵后作用到角点上。import numpy as np from pyquaternion import Quaternion def box_to_corners(center, size, rotation): # center: [x, y, z], size: [w, l, h], rotation: 四元数 [w, x, y, z] w, l, h size # 八个角点在框本地坐标系下的偏移 x_corners [w/2, w/2, -w/2, -w/2, w/2, w/2, -w/2, -w/2] y_corners [l/2, -l/2, -l/2, l/2, l/2, -l/2, -l/2, l/2] z_corners [h/2, h/2, h/2, h/2, -h/2, -h/2, -h/2, -h/2] corners np.array([x_corners, y_corners, z_corners]) # 3x8 # 旋转矩阵 rot Quaternion(rotation).rotation_matrix corners rot corners corners np.array(center).reshape(3, 1) return corners # 3x8全局坐标系这段代码的关键在角点顺序和旋转矩阵的作用方式。Quaternion的rotation_matrix返回 3x3直接左乘本地角点再加中心点得到全局坐标。参数上size的顺序千万别搞反nuScenes 文档里写的是 width、length、height但很多人按 length、width、height 写框就会横竖颠倒。2.3 从全局到相机的完整变换矩阵拿到全局角点后要把它转到相机坐标系。步骤是全局 → 自车 → 传感器 → 相机。每一步都是一个 4x4 的变换矩阵。自车到全局的逆是ego_pose的逆传感器到自车的逆是calibrated_sensor的逆相机到传感器的逆是相机标定里的旋转平移。def global_to_camera(corners, ego_pose, calib_sensor, cam_intrinsic): # corners: 3x8 全局坐标 # ego_pose: dict含 translation 和 rotation # calib_sensor: dict含 translation 和 rotation # 全局 - 自车 ego_t np.array(ego_pose[translation]) ego_r Quaternion(ego_pose[rotation]).rotation_matrix # 自车 - 传感器 cs_t np.array(calib_sensor[translation]) cs_r Quaternion(calib_sensor[rotation]).rotation_matrix # 组合先减自车平移再转再减传感器平移再转 corners_ego ego_r.T (corners - ego_t.reshape(3, 1)) corners_sensor cs_r.T (corners_ego - cs_t.reshape(3, 1)) # 传感器 - 相机nuScenes 相机坐标系需要额外旋转 # 常见做法是套一个从传感器到相机的固定旋转 cam_rot np.array([[0, -1, 0], [0, 0, -1], [1, 0, 0]]) # 示例按实际标定调整 corners_cam cam_rot corners_sensor return corners_cam这里cam_rot是个容易翻车的地方。nuScenes 的相机传感器坐标系和标准相机坐标系之间有一个固定旋转不同版本、不同相机可能不一样。我一般会先用一个已知的框去验证如果投影出来上下颠倒就是旋转矩阵的符号问题。参数上ego_pose和calib_sensor的rotation都是四元数顺序是[w, x, y, z]别当成[x, y, z, w]。2.4 内参和畸变最后一步别漏了相机内参cam_intrinsic是 3x3 矩阵把相机坐标系下的点投影到像素。公式是u fx * X/Z cxv fy * Y/Z cy。但 nuScenes 的图像有畸变尤其是鱼眼相机直接投会偏。常见做法是用cv2.projectPoints带上畸变系数或者先做去畸变再投。import cv2 def project_to_image(corners_cam, cam_intrinsic, dist_coeffsNone): # corners_cam: 3x8 # 只保留 Z 0 的点否则在相机后面 valid corners_cam[2, :] 0 if not np.any(valid): return None pts corners_cam[:, valid].T.reshape(-1, 1, 3) if dist_coeffs is not None: img_pts, _ cv2.projectPoints(pts, np.zeros(3), np.zeros(3), cam_intrinsic, dist_coeffs) else: img_pts cv2.projectPoints(pts, np.zeros(3), np.zeros(3), cam_intrinsic, None)[0] img_pts img_pts.reshape(-1, 2) # 取外接矩形 x_min, y_min img_pts.min(axis0) x_max, y_max img_pts.max(axis0) return [x_min, y_min, x_max, y_max]cv2.projectPoints的dist_coeffs传 None 就是无畸变模型。nuScenes 的相机畸变系数在calibrated_sensor里通常是 5 个参数。注意projectPoints的输入是 Nx1x3输出是 Nx1x2reshape 别搞错。取外接矩形时如果框有一部分在相机后面那些点要丢掉否则会得到负的 Z 导致投影爆炸。3. 用 nuScenes devkit 跑通最小投影脚本理论理清了落到代码。nuScenes 官方有 devkit但很多人不想装全套我一般会直接读 JSON 或者用 devkit 的nusc对象。下面是一个最小可跑脚本从加载数据到输出 2D 框。3.1 加载 sample 和对应的标注先初始化 nuScenes 对象拿到一个 sample再取它的所有 annotation 和对应的相机数据。from nuscenes.nuscenes import NuScenes nusc NuScenes(versionv1.0-mini, dataroot/path/to/nuscenes, verboseTrue) sample nusc.sample[0] # 取第一个 sample ann_tokens sample[anns] cam_channels [CAM_FRONT, CAM_FRONT_LEFT, CAM_FRONT_RIGHT, CAM_BACK, CAM_BACK_LEFT, CAM_BACK_RIGHT]version和dataroot按实际填。v1.0-mini是迷你版适合调试。sample[anns]是标注 token 列表每个 token 对应一个 3D 框。相机通道名是固定的别写错。3.2 遍历相机并做投影对每个相机取它的calibrated_sensor和ego_pose然后对每个标注做投影。for cam in cam_channels: cam_data nusc.get(sample_data, sample[data][cam]) calib nusc.get(calibrated_sensor, cam_data[calibrated_sensor_token]) ego nusc.get(ego_pose, cam_data[ego_pose_token]) intrinsic np.array(calib[camera_intrinsic]) dist np.array(calib.get(camera_distortion, [])) or None for ann_token in ann_tokens: ann nusc.get(sample_annotation, ann_token) corners box_to_corners(ann[translation], ann[size], ann[rotation]) corners_cam global_to_camera(corners, ego, calib, intrinsic) bbox project_to_image(corners_cam, intrinsic, dist) if bbox: print(f{cam}: {bbox})这段代码把前面几个函数串起来。camera_intrinsic是 3x3 列表转 numpy。camera_distortion可能不存在用get加默认值。投影出来的bbox是[x_min, y_min, x_max, y_max]可以拿去画框。3.3 把结果画到图像上验证光打印不够得画出来看。用 OpenCV 读图画矩形保存。import os img_path os.path.join(nusc.dataroot, cam_data[filename]) img cv2.imread(img_path) if bbox: x1, y1, x2, y2 map(int, bbox) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(fout_{cam}.jpg, img)filename是相对路径拼上dataroot。画框时坐标要转 int。如果框画出来偏先检查global_to_camera里的旋转矩阵再检查内参。我一般会先用一个正前方的框去对正前方对上了其他方向基本没问题。4. 投影链路里最容易翻车的五个坑这条链路我调过不下十次每次翻车都差不多是那几个地方。下面按「现象 → 原因 → 解决」列出来你对着排查能省不少时间。4.1 框整体偏移几十像素现象投影出来的框和目标差一个固定偏移所有框都偏同一个方向。原因内参的cx、cy用错了或者图像做了裁剪但内参没更新。解决核对camera_intrinsic里的cx、cy是否等于图像宽高的一半如果不是说明有裁剪或缩放内参要同步调整。4.2 框上下颠倒或左右镜像现象框的位置对但方向反了。原因传感器到相机的旋转矩阵符号错了。nuScenes 的相机坐标系定义和常见 OpenCV 坐标系有差异。解决用一个已知框验证如果上下颠倒把旋转矩阵的第二行取反如果左右镜像第一行取反。别硬套网上的矩阵以实际数据为准。4.3 框跑到画面外或 Z 为负现象投影出来坐标是负数或者极大值。原因角点里有 Z 0 的点即框有一部分在相机后面。解决投影前过滤掉 Z 0 的点只对可见部分取外接矩形。如果整个框都在后面直接跳过。4.4 鱼眼相机投影严重变形现象前视相机还行鱼眼相机框歪得离谱。原因没用畸变系数或者畸变模型不对。解决nuScenes 的畸变系数在calibrated_sensor里用cv2.projectPoints时传进去。如果还不对试试先cv2.undistort再去投影。4.5 时间戳不同步导致框漂移现象静态场景还行动态场景框跟不上目标。原因sample_data的时间戳和sample_annotation的时间戳不一致自车在动。解决用sample_data的ego_pose而不是sample的确保每个相机图对应它自己时刻的位姿。nuScenes 里每个sample_data都有自己的ego_pose_token别偷懒用 sample 级别的。5. 进阶批量导出 2D 标注和几个提效技巧单张跑通后下一步是批量导出成训练格式。我一般会导成 COCO 或 YOLO 格式方便直接喂给 2D 检测器。这里说几个提效的点。5.1 批量导出 COCO 格式的 2D 框COCO 格式需要images、annotations、categories三个字段。遍历所有 sample对每个相机图生成一条 image 记录对每个可见框生成一条 annotation。import json coco {images: [], annotations: [], categories: [{id: 1, name: vehicle}]} ann_id 1 for sample in nusc.sample: for cam in cam_channels: cam_data nusc.get(sample_data, sample[data][cam]) img_info {id: len(coco[images]) 1, file_name: cam_data[filename], width: 1600, height: 900} coco[images].append(img_info) # 投影逻辑同上略 for bbox in bboxes: coco[annotations].append({ id: ann_id, image_id: img_info[id], category_id: 1, bbox: bbox, area: (bbox[2]-bbox[0])*(bbox[3]-bbox[1]), iscrowd: 0 }) ann_id 1 json.dump(coco, open(nuscenes_2d.json, w))width和height按实际图像填nuScenes 是 1600x900。bbox是[x, y, w, h]注意 COCO 的格式和前面的[x1, y1, x2, y2]差一个转换。类别按你的需求映射nuScenes 有 23 类可以合并。5.2 用向量化加速批量投影逐框循环在 mini 版还行全量数据集会慢。我一般会把角点计算和投影都向量化一次处理一个 sample 的所有框。def batch_project(anns, ego, calib, intrinsic): all_corners [] for ann in anns: all_corners.append(box_to_corners(ann[translation], ann[size], ann[rotation])) corners np.concatenate(all_corners, axis1) # 3x(8N) corners_cam global_to_camera(corners, ego, calib, intrinsic) # 后续投影按列分组处理 return corners_camnp.concatenate把多个框的角点拼在一起一次做矩阵变换。投影时再按 8 个一组切分。这样比逐框循环快好几倍全量数据集上差别明显。5.3 验证投影正确性的两个土办法没有绝对可靠的自动验证我一般用两个土办法。一是选一个正前方、距离已知的框手动算一下像素位置和代码输出对。二是把投影框画到图上肉眼看是否贴合目标。如果框和目标差得离谱先查坐标系链路再查内参。别一上来就怀疑数据有问题九成是代码里的旋转或平移符号错了。最后说个习惯每次改完投影代码我都会先用一个 sample 跑一遍把六个相机的图都画出来看一遍确认没问题再批量跑。这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

全国水系矢量数据:可计算的地理底图骨架与空间分析实战指南

全国水系矢量数据:可计算的地理底图骨架与空间分析实战指南

简介:本资源为全国水系矢量数据集,面向GIS初学者、地理信息专业学生、城乡规划与水利相关从业者,解决基础空间分析中缺乏权威、分级清晰的中国水系底图问题。压缩包共27个文件,含6组shp(几何)、dbf&#xf…

2026/10/12 19:55:31 阅读更多 →
SAM+UNet息肉分割实战:边界先验注入与调参避坑指南

SAM+UNet息肉分割实战:边界先验注入与调参避坑指南

简介:本资源面向医学图像分割方向的研究者与开发者,提供一套在Unet基础上引入SAM提示框机制的息肉肿瘤语义分割完整方案,适合具备一定深度学习基础、希望复现或改进医学分割模型的中高级学习者。压缩包共2000个文件,约263.61MB&am…

2026/10/11 18:18:47 阅读更多 →
YOLOv8+PyQt5课堂行为检测系统:从数据集训练到界面部署全攻略

YOLOv8+PyQt5课堂行为检测系统:从数据集训练到界面部署全攻略

简介:基于YOLOv8与PyQt5的课堂检测系统完整实现,面向教育技术研究者、计算机视觉初学者及有课堂管理需求的学校/机构,解决传统人工课堂监控效率低、无法实时量化学生行为的问题。系统利用YOLOv8目标检测算法进行高精度实时推理,通…

2026/10/11 18:18:47 阅读更多 →

最新新闻

MinGW for Win64 编译与分发实战:从工具链选型到静态链接避坑

MinGW for Win64 编译与分发实战:从工具链选型到静态链接避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:24:05 阅读更多 →
Windows SSH 从入门到精通:密钥管理、配置优化与安全加固

Windows SSH 从入门到精通:密钥管理、配置优化与安全加固

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:24:05 阅读更多 →
STM32F407+ADS1256高精度体重秤设计与实现

STM32F407+ADS1256高精度体重秤设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:24:05 阅读更多 →
问卷设计不是凑问题,科迅捷AI帮你设计出能用的问卷

问卷设计不是凑问题,科迅捷AI帮你设计出能用的问卷

问卷设计这件事,看起来门槛最低:找几个问题拼一拼,不就是问卷吗?但真正发出去才发现,回收的数据根本没法用:问题有歧义、选项不互斥、维度覆盖不全,统计出来的结果也说明不了任何问题。问卷设计…

2026/10/13 1:24:05 阅读更多 →
开源本地智能体 OpenClaw 3.1.0,完整落地保姆级指南

开源本地智能体 OpenClaw 3.1.0,完整落地保姆级指南

📌 说明 本文基于 OpenClaw 版本进行讲解,全程采用图形化可视化交互模式。整合包已内置所有运行依赖,普通用户即可轻松复现完整的部署流程。 ✨核心亮点: 全程可视化图形交互界面,自动补齐全部运行依赖,数据…

2026/10/13 1:24:05 阅读更多 →
树莓派连接LM35温度传感器必备ADC0832模数转换方案

树莓派连接LM35温度传感器必备ADC0832模数转换方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 1:23:05 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →