據(jù)集代碼拆解:從數(shù)據(jù)表關聯(lián)到3D框投影與點云變換)
簡介面向自動駕駛算法研究者的nuScenes數(shù)據(jù)集配套代碼包聚焦復雜駕駛場景下多傳感器數(shù)據(jù)的獲取、解析與可視化。資源圍繞nuScenes數(shù)據(jù)集的1000個20秒場景、40萬關鍵幀及1400萬3D標注框等核心內容給出基于nuscenes-devkit庫的操作示例幫助開發(fā)者快速上手傳感器數(shù)據(jù)、樣本注釋和地圖等模塊的讀取與分析。壓縮包共5個文件包含代碼入口、依賴清單、環(huán)境配置說明與使用文檔整體僅8KB輕量易用。已有118人學習下載適合正在學習自動駕駛數(shù)據(jù)集處理、需要快速理解nuScenes結構與devkit用法的初中級開發(fā)者。通過源碼可直觀掌握場景樣本、校準傳感器、車輛姿態(tài)等對象的調用方式并以此為起點擴展自己的數(shù)據(jù)挖掘與可視化腳本降低入門成本。 做自動駕駛感知的同學對nuScenes數(shù)據(jù)集應該不陌生但很多人卡在“數(shù)據(jù)下載好了卻不知道代碼怎么讀”這一步。這個數(shù)據(jù)集包含了6個相機、5個毫米波雷達和1個激光雷達的同步數(shù)據(jù)加上地圖信息和23類3D標注框是當前多模態(tài)3D檢測和BEV感知繞不開的benchmark。這篇文章我拿實際跑過的代碼來拆一遍從數(shù)據(jù)目錄結構、標注表關聯(lián)、坐標系變換到3D框投影到圖像、雷達點云讀取全程帶可運行示例適合剛開始接觸nuScenes、準備復現(xiàn)模型或者想把數(shù)據(jù)鏈路搞明白的工程師參考。我最早也被一堆JSON文件名勸退過什么sample、sample_data、sample_annotation、calibrated_sensor、ego_pose光看文檔完全理不清。后來耐著性子把數(shù)據(jù)表的關聯(lián)一個個打出來才意識到這套設計的邏輯其實很清晰。下面我按自己項目里的推進順序來寫盡量把踩過的坑和驗證方法都帶出來。1. 項目定位與代碼拆解思路1.1 為什么選擇nuScenes做多模態(tài)感知做自動駕駛感知候選數(shù)據(jù)集其實不少KITTI是單目64線激光雷達數(shù)據(jù)量小適合快速驗證Waymo Open Dataset規(guī)模大但傳感器配置較重數(shù)據(jù)格式和工具鏈上手成本高。nuScenes的定位比較居中傳感器配置完整關鍵幀是2Hz每個20秒的scene有40個關鍵幀加上中間的sweeps覆蓋了城市、郊區(qū)、夜晚、雨天多種場景。它的另一個優(yōu)勢是官方提供了nuScenes-devkit這個Python包雖然不是十全十美但至少把數(shù)據(jù)讀取和坐標系變換的臟活做了大半。訓練一個3D檢測模型之前首先要確保訓練樣本能正確對齊。nuScenes的相機、激光雷達、毫米波雷達都做了時間同步和標定每一條sample_data都有對應的calibrated_sensor和ego_pose這意味著代碼層面可以比較輕松地把不同模態(tài)的數(shù)據(jù)變換到同一坐標系下。這也是我選擇它來做多模態(tài)感知項目的原因數(shù)據(jù)底子干凈代碼側能少操很多心。1.2 代碼層面的三個推進階段我在項目里把nuScenes的代碼工作分成三個階段。第一階段是“能讀”把數(shù)據(jù)目錄和JSON標注表的結構摸清楚知道哪張表記錄什么信息第二階段是“能算”把坐標系變換鏈弄明白不管是把3D框投影到圖像還是把點云從傳感器坐標轉到自車坐標都要手寫出來并驗證第三階段是“能用”把這些讀取邏輯封裝成Dataset類供PyTorch訓練循環(huán)調用。這篇文章會覆蓋前兩個階段第三個階段涉及具體模型訓練不同模型差異比較大不太適合一概而論。但前兩步做扎實了第三步基本就是套模板的事。2. 數(shù)據(jù)底層結構目錄、標注表與坐標系2.1 samples和sweeps的區(qū)別nuScenes的數(shù)據(jù)目錄里有兩個容易混淆的文件夾samples和sweeps。簡單說samples是每個關鍵幀時刻的所有傳感器數(shù)據(jù)一個sample對應一次完整的“同時刻采集”頻率是2Hzsweeps則是兩個關鍵幀之間其他時刻的數(shù)據(jù)主要用于tracking或者利用歷史信息增強當前幀。理解這個區(qū)別對代碼調試很重要。我在早期寫代碼時直接用sweeps里的點云去和samples里的標注框對齊結果發(fā)現(xiàn)時間戳對不上框和點云總是差一段距離。后來才意識到標注框只標在關鍵幀時刻的傳感器數(shù)據(jù)上sweeps里的數(shù)據(jù)只是輔助信息不能直接與samples里的標注一一對應。2.2 核心標注表的關聯(lián)關系nuScenes的標注數(shù)據(jù)分布在多張JSON表里最核心的幾張是scene、sample、sample_data、sample_annotation、instance、category。它們之間的關系可以這樣理解一個scene是整個視頻片段包含多個sample每個sample是一個關鍵幀時刻通過sample[data]字段可以拿到該時刻所有傳感器的sample_data token每個sample_data對應一個具體的傳感器文件比如相機圖片或雷達點云。sample_annotation記錄的是某個sample時刻某個目標實例的3D標注框它通過instance關聯(lián)到類別通過sample關聯(lián)到具體時刻。用代碼查這張關聯(lián)鏈最直觀。比如要找到某個scene第一個sample的相機前視圖標注框代碼路徑是scene → first_sample_token → sample → data.CAM_FRONT → sample_data → sample_annotation。這種鏈式查詢一開始不太習慣但好處是結構清晰任何一環(huán)都可以用token精確定位不會出現(xiàn)跨表字段冗余的問題。2.3 坐標系與位姿變換鏈坐標系是nuScenes代碼里最容易出錯的地方。整個數(shù)據(jù)集涉及三套坐標系global坐標系是全局地圖坐標ego坐標系以自車為中心x向前、y向左、z向上sensor坐標系以傳感器自身為原點。每一幀sample_data都記錄了calibrated_sensor_token和ego_pose_token。calibrated_sensor存的是傳感器相對于ego的平移和旋轉ego_pose存的是ego相對于global的平移和旋轉。所以要把傳感器坐標系下的點變換到global坐標系變換鏈是sensor → ego → global反變換就是global → ego → sensor。很多教程直接調用nusc.get_sample_data接口拿投影結果這當然快但如果不理解背后的變換鏈一旦結果不對就不知道怎么排查。我在實際項目中是先把這條鏈路手寫出來跑通了再換官方接口這樣出了問題能一眼定位到是哪一步變換寫反了。3. 核心代碼實操加載、投影、點云變換3.1 環(huán)境配置與數(shù)據(jù)準備環(huán)境方面的坑我先說結論實測最穩(wěn)的組合是Python 3.8到3.10加上nuscenes-devkit 1.1.10。官方包依賴pyquaternion、numpy等庫如果你環(huán)境里已經裝了新版numpy直接pip安裝nuscenes-devkit可能會因為numpy API變動報一些奇怪的錯誤建議用虛擬環(huán)境單獨建一個不要放在全局環(huán)境里折騰。python -m venv nuscenes_env source nuscenes_env/bin/activate pip install nuscenes-devkit數(shù)據(jù)準備方面如果只是學習和調試代碼不需要把整個trainval下載下來。官方提供了v1.0-mini版本大約4GB包含了10個scene足夠把數(shù)據(jù)讀取流程跑通。下載好解壓后目錄結構要保持與dataroot設置一致。比如你解壓到./data/nuscenes里面有samples、sweeps、maps和v1.0-mini這幾個子目錄初始化時dataroot填./data/nuscenes就行。3.2 初始化NuScenes并遍歷場景初始化NuScenes對象時它會一次性把所有JSON標注表加載進內存并建立token索引。這個過程需要一點時間第一次跑看到卡頓不要慌不是死鎖是在建表。from nuscenes.nuscenes import NuScenes nusc NuScenes( versionv1.0-mini, dataroot./data/nuscenes, verboseTrue ) # 查看第一個場景 my_scene nusc.scene[0] print(my_scene[token], my_scene[nbr_samples]) print(my_scene[first_sample_token]) # 拿第一個關鍵幀 sample nusc.get(sample, my_scene[first_sample_token]) print(sample[timestamp]) print(list(sample[data].keys()))sample[data]是一個字典key是傳感器名稱包括CAM_FRONT、CAM_FRONT_LEFT、CAM_BACK_LEFT、LIDAR_TOP等value是該傳感器在sample_data表中的token。拿到token后再通過nusc.get(sample_data, token)就能拿到文件路徑、時間戳、位姿token等完整信息。這段代碼雖然簡單但我建議認真看一遍print輸出。很多初學者直接跳到模型復現(xiàn)連sample和sample_data都分不清后面寫數(shù)據(jù)加載器時就會到處碰壁。3.3 3D框投影到相機圖像的手寫實現(xiàn)這一節(jié)是重點。3D框投影到相機圖像官方提供了get_sample_data接口但我先給出手寫實現(xiàn)因為這里面每個矩陣的來龍去脈才是真正值錢的知識。投影鏈路分三步先把global坐標的3D框中心點變到ego坐標再由ego變到相機sensor坐標最后用相機內參投影到像素平面。import numpy as np from pyquaternion import Quaternion # 找到前視相機的sample_data cam_data nusc.get(sample_data, sample[data][CAM_FRONT]) # 獲取標定參數(shù) calib nusc.get(calibrated_sensor, cam_data[calibrated_sensor_token]) ego nusc.get(ego_pose, cam_data[ego_pose_token]) # sensor - ego sensor_to_ego np.eye(4) sensor_to_ego[:3, :3] Quaternion(calib[rotation]).rotation_matrix sensor_to_ego[:3, 3] np.array(calib[translation]) # ego - global ego_to_global np.eye(4) ego_to_global[:3, :3] Quaternion(ego[rotation]).rotation_matrix ego_to_global[:3, 3] np.array(ego[translation]) # 反變換global - ego - sensor global_to_ego np.linalg.inv(ego_to_global) ego_to_sensor np.linalg.inv(sensor_to_ego) global_to_sensor ego_to_sensor global_to_ego # 取一個3D標注框box.center是global坐標 box nusc.get(sample_annotation, sample[data][CAM_FRONT])等等這里有個細節(jié)要說明一下sample_annotation的token不是從sample[data]直接拿的正確做法是獲取該sample下的所有標注框再取第一個。修正代碼如下ann_tokens sample[anns] box nusc.get(sample_annotation, ann_tokens[0]) # box.center 是 global 坐標系的中心點 center_global np.array(box[translation]) center_sensor global_to_sensor[:3, :3] center_global global_to_sensor[:3, 3] # 相機內參 intrinsic np.array(calib[camera_intrinsic]) p intrinsic center_sensor p p / p[2] print(像素坐標:, p[:2])代碼跑通后可以把所有標注框的中心點投影到圖上用matplotlib畫出來驗證。如果所有點都落在相機視野內且位置大致合理說明變換鏈正確如果發(fā)現(xiàn)點嚴重偏移甚至跑到圖像外面九成是旋轉矩陣方向寫反或者平移向量沒加。3.4 激光雷達點云讀取與全局坐標變換點云文件的讀取用官方提供的LidarPointCloud類最省事。激光雷達點云文件里存的是sensor坐標系下的坐標要變到global坐標系需要經過sensor → ego → global這樣一條正變換鏈路。from nuscenes.utils.data_classes import LidarPointCloud import os lidar_data nusc.get(sample_data, sample[data][LIDAR_TOP]) pc LidarPointCloud.from_file(os.path.join(nusc.dataroot, lidar_data[filename])) # 點云形狀為 (4, N)前三行是xyz第四行是強度 print(pc.points.shape) # 激光雷達的標定參數(shù) calib_lidar nusc.get(calibrated_sensor, lidar_data[calibrated_sensor_token]) ego_lidar nusc.get(ego_pose, lidar_data[ego_pose_token]) lidar_to_ego np.eye(4) lidar_to_ego[:3, :3] Quaternion(calib_lidar[rotation]).rotation_matrix lidar_to_ego[:3, 3] np.array(calib_lidar[translation]) ego_to_global np.eye(4) ego_to_global[:3, :3] Quaternion(ego_lidar[rotation]).rotation_matrix ego_to_global[:3, 3] np.array(ego_lidar[translation]) # sensor - ego - global lidar_to_global ego_to_global lidar_to_ego points_global lidar_to_global[:3, :3] pc.points[:3, :] lidar_to_global[:3, 3:4]把點云變換到global坐標系后再配合全局地圖信息可以驗證點云是否與道路結構對齊。這個驗證我在項目里做過很多次結果基本一致說明這套變換邏輯是可靠的。如果你只是想快速把點云畫出來看個大概也可以直接用pc.points的前三行畫3D散點圖不需要任何變換。但如果你想把雷達點云和相機圖像做融合那就必須做坐標系變換不能偷懶。4. 常見問題與排查技巧實錄4.1 安裝和版本兼容問題nuscenes-devkit的版本兼容性是個實實在在的坑。我用Python 3.11裝最新版時遇到過numpy.ndarray沒有float屬性這類報錯原因是numpy把np.float、np.int這類別名刪掉了而舊版devkit代碼里還在用。后來我固定用Python 3.9加nuscenes-devkit 1.1.10問題就消失了。如果你在import階段遇到pyquaternion報錯直接重裝pyquaternion。這個包本身比較輕量一般重裝后能解決。另外不要在Windows系統(tǒng)上直接解壓數(shù)據(jù)包到含中文的路徑nuscenes的代碼內部對路徑做了字符串拼接中文路徑偶爾會出現(xiàn)編碼報錯把數(shù)據(jù)放在純英文路徑下是最省心的做法。4.2 坐標變換輸出異常怎么查投影結果不對時我建議按這個順序排查。第一步檢查ego_pose和calibrated_sensor是否取錯比如拿激光雷達的標定去投影相機數(shù)據(jù)第二步檢查旋轉矩陣是否轉置Quaternion的rotation_matrix得到的矩陣是從sensor系到ego系的旋轉如果你把矩陣用反了結果自然不對第三步檢查平移向量要不要加很多人在做反變換時只做了旋轉逆變換忘了平移也需要變換這個細節(jié)最容易漏。另外一個有效的驗證方式是拿官方get_sample_data的結果和手寫結果對比。如果官方接口輸出正確而手寫代碼輸出不對就把中間每一步的矩陣打印出來逐個對比。我遇到過好幾次手滑把矩陣乘法的順序寫反最后都是靠這種對比定位出來的。4.3 數(shù)據(jù)加載慢與內存占用nuScenes數(shù)據(jù)集如果不加節(jié)制地讀取內存會漲得很快。一個關鍵幀的點云文件大小還好但如果你把所有sweeps的點云都加載進內存幾個scene下來就是幾十GB很容易把電腦拖垮。我的經驗是兩個原則按需加載用完釋放。在訓練循環(huán)里每次只讀取當前sample需要的傳感器數(shù)據(jù)處理完就扔掉不要用list把所有場景的數(shù)據(jù)先預加載一遍。另外get_sample_data接口有一個參數(shù)use_flat_vehicle_coordinates默認是True意思是返回的box坐標已經投影到以自車為原點的平面坐標系。如果你要的是global坐標記得把這個參數(shù)設為False否則后續(xù)做全局變換會對不上。4.4 標注文件JSON的實操經驗最后說一個不太起眼但很實用的經驗nuScenes的標注JSON文件在運行時是可以被修改的。如果你只想做數(shù)據(jù)子集采樣比如只保留車輛和行人兩類不需要重新造數(shù)據(jù)集直接在dataroot下新建一個version文件夾復制一份并縮小category表的字段就行。我在項目里還遇到過一個和JSON解析有關的問題標注文件里的category_name是帶層級結構的比如vehicle.car、vehicle.truck在寫分類映射時不能直接比對字符串是否相等要用startswith或者split(.)[-1]來取葉節(jié)點名稱否則新版本數(shù)據(jù)集的類別層級一變你的代碼就默默失效了。nuScenes這套數(shù)據(jù)鏈路里最值錢的不是那幾個調用接口而是理解每一張表、每一個坐標系之間的映射關系。我個人的體會是花一個下午把3D框投影到圖像這個流程手寫一遍比看十遍文檔都有用。你可以在跑通之后試著做一個可視化腳本把相機圖像、投影框、點云投影結果疊在一張圖上看到框穩(wěn)穩(wěn)貼在車身上、點云輪廓和圖像邊緣對齊的那一刻整個數(shù)據(jù)集的邏輯基本就通透了大半。后續(xù)做BEV感知、多模態(tài)融合都是在吃透這一層之后水到渠成的事。本文還有配套的精品資源點擊獲取