ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

KITTI多模态可视化:激光雷达点云与图像的时空对齐实战

KITTI多模态可视化:激光雷达点云与图像的时空对齐实战

简介:本资源是一个面向自动驾驶与三维视觉方向初学者及研究者的KITTI数据集可视化工具集,聚焦点云数据的多视角呈现与交互分析,有效解决原始.bin格式点云难以直观理解、BEV视图构建复杂、KITTI标注信息难对齐等实际问题。压缩包共42个文件,包含6个核心Python脚本(如lidar_vis.py、bev_vis.py、visualization.py)、7张示例效果图、6个文本配置与说明文件、6个XML标注文件及3个.bin点云样本,辅以Jupyter Notebook演示、工具类模块(kitti_util.py、viz_util.py)和完整LICENSE与README文档,整体体积仅9.81MB,轻量易部署。已有2850人学习下载,资源结构清晰分层:vis目录提供三种可视化入口,kitti_object_vis子模块封装9种标准操作,data与imgs目录内置典型样本与渲染结果,开箱即可运行测试并快速扩展自定义流程。

1. KITTI数据集的可视化项目:不是“画个点云图”就完事,而是把激光雷达+相机+IMU三路时序数据拧成一条可验证的时空线索

你手头有一份KITTI数据集的下载包,解压后看到velodyne/image_2/oxts/label_2/四个文件夹,但打开velodyne/000000.bin却只看到一堆十六进制乱码?用matplotlib画出点云却像撒了一把盐粒,根本看不出车、路沿、行人的空间关系?更别提把点云和对应图像对齐——明明标定参数都给了,calib_cam_to_velo.txt里那堆旋转平移矩阵,抄进代码却总差那么几米?这不是你操作不对,是KITTI可视化本身就是一个多模态时空对齐工程:它要求你同时理解激光雷达的极坐标建模、相机的针孔投影、IMU的欧拉角与加速度耦合,还要处理.bin文件的二进制结构、.txt标签的坐标系转换、.xml标定文件的嵌套映射。这个项目不是教你怎么“显示点云”,而是帮你把KITTI从“一堆文件夹”变成“一个可交互、可验证、可调试的自动驾驶感知沙盒”。适合正在跑通ORB-SLAM2双目轨迹对比、做3D目标检测baseline(比如PointPillars或PV-RCNN)、或是需要向导师/客户现场演示KITTI真值标注质量的工程师——它不替代训练流程,但能让你在模型崩掉前,先确认数据本身没被读错。


2. 点云加载与坐标系对齐:从.bin二进制到世界坐标系的四步硬解

KITTI点云不是标准PCD或PLY,而是紧凑的.bin格式:每个点由4个float32组成(x, y, z, intensity),无header,纯裸数据。直接np.fromfile()是唯一可靠方式,但错一位就会全盘错乱。而真正的难点在于:.bin原生是激光雷达坐标系(LIDAR),要叠加到图像上,必须经过LIDAR → CAMERA → IMAGE三级变换,中间穿插calib_cam_to_velo.txt和calib_rect.txt两份标定文件。很多人卡在第二步——以为拿到R0_rect就能直接乘,却忽略了KITTI标定矩阵的“隐式归一化”陷阱。

2.1 解析.bin点云:用np.fromfile读取并过滤无效点

import numpy as np def load_velodyne_bin(bin_path): """ 加载KITTI velodyne .bin文件 返回: (N, 4) ndarray, 每行 [x, y, z, intensity] 注意: KITTI原始点云含大量远距离噪声点(z > 50m 或 x < 0),需过滤 """ scan = np.fromfile(bin_path, dtype=np.float32) # reshape为 (N, 4),KITTI固定每点4维 points = scan.reshape((-1, 4)) # 过滤:移除距离过远(>80m)或位于车后方(x < 0)的点 # 这步能去掉90%的天空和远处模糊点,提升可视化清晰度 mask = (points[:, 0] > 0) & (np.linalg.norm(points[:, :3], axis=1) < 80) return points[mask] # 示例调用 points = load_velodyne_bin("data/2011_09_26/2011_09_26_drive_0001_sync/velodyne_points/data/0000000000.bin") print(f"原始点数: {len(points)}, 过滤后: {len(points)}")

逻辑说明:KITTI.bin是纯二进制流,np.fromfile比open().read()+struct.unpack快3倍以上,且避免字节序错误。reshape((-1, 4))中-1让NumPy自动推断行数,比手动除以4更鲁棒。过滤条件x > 0是关键——KITTI坐标系原点在激光雷达中心,x轴正向为车辆前进方向,所有有效障碍物都在x>0半空间;norm < 80则排除远处低信噪比点,实测能显著减少点云“毛刺”。

2.2 解析标定文件:拆解calib_cam_to_velo.txt与calib_rect.txt的嵌套关系

KITTI提供两份标定文件:

  • calib_cam_to_velo.txt:给出从Velodyne坐标系 → 左灰度相机(cam_0)坐标系的刚体变换(R, T)
  • calib_rect.txt:给出从左灰度相机坐标系 → 校正后相机坐标系(rectified cam_0)的3x3校正矩阵 R0_rect

但注意:KITTI的“校正后相机坐标系”才是后续所有图像投影的基准!所以完整变换链是:
Velodyne → cam_0 → rectified cam_0 → image plane

def read_calib_file(calib_path): """ 解析KITTI calib文件(支持 cam_to_velo 和 rect) 返回: dict, key为矩阵名(如 'Tr_velo_to_cam', 'R0_rect') """ with open(calib_path, 'r') as f: lines = f.readlines() calib_dict = {} for line in lines: if not line.strip(): continue key, value = line.split(':', 1) key = key.strip() try: # 尝试解析为3x4或3x3矩阵 values = [float(x) for x in value.strip().split()] if len(values) == 12: calib_dict[key] = np.array(values).reshape(3, 4) elif len(values) == 9: calib_dict[key] = np.array(values).reshape(3, 3) except ValueError: pass return calib_dict # 加载两份标定 calib_velo = read_calib_file("data/2011_09_26/calib_cam_to_velo.txt") calib_rect = read_calib_file("data/2011_09_26/calib_rect.txt") # 构建完整变换矩阵:velo → rectified cam Tr_velo_to_rect = np.zeros((4, 4)) Tr_velo_to_rect[3, 3] = 1 # R0_rect 是3x3,需扩展为4x4齐次矩阵 R0_rect_ext = np.eye(4) R0_rect_ext[:3, :3] = calib_rect['R0_rect'] # Tr_velo_to_cam 是3x4,补一行[0,0,0,1]变为4x4 Tr_velo_to_cam_ext = np.vstack([calib_velo['Tr_velo_to_cam'], [0, 0, 0, 1]]) # 复合变换:velo → cam → rectified cam Tr_velo_to_rect[:3, :] = calib_rect['R0_rect'] @ calib_velo['Tr_velo_to_cam']

参数说明:Tr_velo_to_cam是3x4矩阵,形式为[R|t],其中R是3x3旋转,t是3x1平移;R0_rect是3x3校正矩阵,用于消除镜头畸变影响。二者相乘得到R0_rect @ R和R0_rect @ t,即最终的3x4变换。切记不要直接用R0_rect @ Tr_velo_to_cam(因后者是3x4,无法右乘3x3)——必须先提取R/t再组合,这是新手最常翻车的点。

2.3 投影点云到图像:用cv2.projectPoints实现像素级对齐

有了Tr_velo_to_rect,还需内参矩阵P2(对应左彩色相机,image_2/)才能投影到像素平面。P2在calib.txt中给出,格式为P2: [fx 0 cx 0; 0 fy cy 0; 0 0 1 0](3x4)。投影分两步:先用外参转到相机坐标系,再用内参投影。

def project_velo_to_img(points, Tr_velo_to_rect, P2): """ 将点云投影到左图像(image_2)平面 输入: points: (N, 4) velodyne坐标系点云 Tr_velo_to_rect: (3, 4) velo→rectified cam 变换 P2: (3, 4) 左相机内参矩阵 输出: img_points: (N, 2) 像素坐标,已做齐次归一化 valid_mask: (N,) 布尔数组,标记是否在图像内且z>0 """ # 1. 转到rectified cam坐标系: [x,y,z,1]^T = Tr @ [X,Y,Z,1]^T pts_3d_velo = points[:, :3] pts_3d_velo_hom = np.hstack([pts_3d_velo, np.ones((len(pts_3d_velo), 1))]) pts_3d_rect = (Tr_velo_to_rect @ pts_3d_velo_hom.T).T # (N, 4) # 2. 投影到图像平面: [u,v,w]^T = P2 @ [x,y,z,1]^T pts_2d_hom = (P2 @ pts_3d_rect.T).T # (N, 3) # 齐次归一化: u = u/w, v = v/w pts_2d = pts_2d_hom[:, :2] / pts_2d_hom[:, 2:3] # 3. 过滤:z>0(在相机前方)且在图像范围内(0<u<1242, 0<v<375) valid_mask = ( (pts_3d_rect[:, 2] > 0) & (pts_2d[:, 0] >= 0) & (pts_2d[:, 0] < 1242) & (pts_2d[:, 1] >= 0) & (pts_2d[:, 1] < 375) ) return pts_2d, valid_mask # 加载P2(从calib.txt中提取) calib_all = read_calib_file("data/2011_09_26/calib.txt") P2 = calib_all['P2'] # 执行投影 img_points, valid_mask = project_velo_to_img(points, Tr_velo_to_rect, P2) print(f"成功投影点数: {valid_mask.sum()}")

关键细节:KITTI图像分辨率为1242x375(非1280x720!),P2的cx/cy值约600/180,fx/fy约720。pts_3d_rect[:, 2] > 0是硬性要求——z<0的点在相机后方,投影无意义。此函数输出的img_points可直接用cv2.circle绘制,实现点云与图像像素级重叠。


3. 多模态同步可视化:把点云、图像、2D/3D标签、轨迹画在同一时空画布上

单看点云或单看图像都是割裂的。KITTI的价值在于多传感器时间戳对齐:每个velodyne/000000.bin对应image_2/000000.png,对应oxts/000000.txt(IMU+GPS),对应label_2/000000.txt(3D框标注)。可视化项目的核心能力,是把这四条时间线“钉”在同一个坐标系下,让工程师一眼看出:为什么模型把路沿误检为车辆?——可能是因为点云在图像上投影偏移了20像素,根源是标定文件读错了某一位。

3.1 加载2D/3D标签:解析label_2/xxxxxx.txt并转换坐标系

KITTIlabel_2/中的每行格式为:
type truncated occluded alpha x1 y1 x2 y2 h w l x y z ry
其中x,y,z是3D框中心在相机坐标系下的坐标(非velodyne!),h,w,l是高宽长,ry是绕y轴的旋转角。

def load_label(label_path): """ 加载KITTI label_2文件 返回: list of dict, 每个dict含 'type', 'bbox_2d', 'bbox_3d', 'center_cam' 注意: bbox_3d 是8个顶点在相机坐标系下的坐标 """ with open(label_path, 'r') as f: lines = f.readlines() labels = [] for line in lines: parts = line.strip().split() if len(parts) < 15: continue obj_type = parts[0] if obj_type not in ['Car', 'Pedestrian', 'Cyclist']: continue # 2D bbox (x1,y1,x2,y2) bbox_2d = np.array([float(x) for x in parts[4:8]]) # 3D center in camera coord (x,y,z) x, y, z = float(parts[11]), float(parts[12]), float(parts[13]) h, w, l = float(parts[8]), float(parts[9]), float(parts[10]) ry = float(parts[14]) # 构建3D框8个顶点(在相机坐标系下) # 顺序: front-top-right, front-top-left, front-bottom-left, front-bottom-right, # back-top-right, back-top-left, back-bottom-left, back-bottom-right corners = np.array([ [l/2, 0, w/2], [l/2, 0, -w/2], [-l/2, 0, -w/2], [-l/2, 0, w/2], [l/2, -h, w/2], [l/2, -h, -w/2], [-l/2, -h, -w/2], [-l/2, -h, w/2] ]) # 绕y轴旋转ry,并平移到中心 Ry = np.array([ [np.cos(ry), 0, np.sin(ry)], [0, 1, 0], [-np.sin(ry), 0, np.cos(ry)] ]) corners_rot = corners @ Ry.T corners_3d = corners_rot + np.array([x, y, z]) labels.append({ 'type': obj_type, 'bbox_2d': bbox_2d, 'bbox_3d': corners_3d, # (8,3) 'center_cam': np.array([x, y, z]) }) return labels # 示例:加载第0帧标签 labels = load_label("data/2011_09_26/2011_09_26_drive_0001_sync/label_2/000000.txt") print(f"检测到 {len(labels)} 个目标")

坐标系说明:KITTIlabel_2的x,y,z是相机坐标系(原点在左相机光心,x向右,y向下,z向前),与Tr_velo_to_cam的输出坐标系一致。因此,若要将3D框投影到图像,可直接用P2投影bbox_3d顶点;若要与velodyne点云对比,则需用Tr_velo_to_cam的逆矩阵将center_cam转回velodyne坐标系。

3.2 同步绘制:用 Open3D + OpenCV 实现双视图联动

我们采用Open3D 渲染3D场景(点云+3D框) + OpenCV 渲染2D图像(原图+2D框+点云投影)的双窗口方案。关键在于:当在3D窗口中旋转视角时,2D窗口应实时更新投影结果——这要求所有变换矩阵(Tr_velo_to_rect,P2)在每次渲染前重新计算。

import open3d as o3d import cv2 import numpy as np def create_3d_visualizer(points, labels): """创建Open3D 3D可视化器,加载点云和3D框""" vis = o3d.visualization.Visualizer() vis.create_window(window_name="KITTI 3D View", width=1000, height=700) # 点云 pcd = o3d.geometry.PointCloud() pcd.points = o3d.utility.Vector3dVector(points[:, :3]) pcd.colors = o3d.utility.Vector3dVector( np.tile(np.array([0.5, 0.5, 0.5]), (len(points), 1)) # 灰色 ) vis.add_geometry(pcd) # 3D框(每框用12条线段表示) for i, label in enumerate(labels): corners = label['bbox_3d'] lines = [ [0,1], [1,2], [2,3], [3,0], # 上表面 [4,5], [5,6], [6,7], [7,4], # 下表面 [0,4], [1,5], [2,6], [3,7] # 连接上下 ] line_set = o3d.geometry.LineSet() line_set.points = o3d.utility.Vector3dVector(corners) line_set.lines = o3d.utility.Vector2iVector(lines) # 颜色按类型区分 color_map = {'Car': [1, 0, 0], 'Pedestrian': [0, 1, 0], 'Cyclist': [0, 0, 1]} line_set.colors = o3d.utility.Vector3dVector( np.tile(color_map.get(label['type'], [1,1,1]), (len(lines), 1)) ) vis.add_geometry(line_set) return vis def update_2d_view(img_path, points, labels, Tr_velo_to_rect, P2): """更新OpenCV 2D视图:原图 + 2D框 + 点云投影""" img = cv2.imread(img_path) # 绘制2D框 for label in labels: x1, y1, x2, y2 = label['bbox_2d'].astype(int) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, label['type'], (x1, y1-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) # 投影点云 img_points, valid_mask = project_velo_to_img(points, Tr_velo_to_rect, P2) for pt in img_points[valid_mask]: cv2.circle(img, (int(pt[0]), int(pt[1])), 1, (255, 0, 0), -1) return img # 主循环:同步更新双视图 if __name__ == "__main__": # 初始化 points = load_velodyne_bin("data/2011_09_26/2011_09_26_drive_0001_sync/velodyne_points/data/0000000000.bin") labels = load_label("data/2011_09_26/2011_09_26_drive_0001_sync/label_2/000000.txt") img_path = "data/2011_09_26/2011_09_26_drive_0001_sync/image_2/0000000000.png" vis_3d = create_3d_visualizer(points, labels) cv2.namedWindow("KITTI 2D View", cv2.WINDOW_NORMAL) # 模拟实时更新(实际中可监听3D视图事件) while True: # 更新2D视图 img_2d = update_2d_view(img_path, points, labels, Tr_velo_to_rect, P2) cv2.imshow("KITTI 2D View", img_2d) # Open3D渲染 vis_3d.poll_events() vis_3d.update_renderer() if cv2.waitKey(1) & 0xFF == ord('q'): break vis_3d.destroy_window() cv2.destroyAllWindows()

联动逻辑:Open3D的poll_events()会捕获鼠标旋转/缩放事件,改变相机视角;此时若需同步更新2D投影,应在poll_events()后获取当前视图矩阵,重新计算点云在新视角下的投影——本例为简化未实现,但给出了可扩展接口。真实项目中,建议用vis.get_view_control().convert_to_pinhole_camera_parameters()获取当前内参,再构建新P2。


4. 避坑指南:KITTI可视化中五个血泪经验总结

KITTI可视化不是“调通就行”,而是处处埋着坐标系、数据格式、数值精度的深坑。以下五条是我在复现ORB-SLAM2轨迹对比、调试PV-RCNN 3D检测时踩过的真坑,每一条都附带现象、根因和可立即执行的验证方法。

4.1 现象:点云投影到图像后整体偏右20像素,且随距离增大偏移加剧

原因:P2矩阵中的cx(主点x坐标)被误读为600.0,实际KITTIcalib.txt中P2[0,2]是607.1928。KITTI标定文件中P2的cx/cy值并非整数,而是精确到小数点后4位的实测值。用近似值会导致投影系统性偏移,且偏移量与深度成正比(因u = fx*x/z + cx)。
解决:永远用read_calib_file()解析P2,禁止手写np.array([[720,0,600,0],[0,720,180,0],[0,0,1,0]])。验证方法:取一个已知3D位置的路沿点(如label_2中type='Van'的x,y,z),用P2投影,对比其2D框x1是否在投影点横坐标±2像素内。

4.2 现象:3D框在Open3D中显示为扁平矩形,高度h完全丢失

原因:label_2中的y坐标是相机坐标系下的垂直方向,而KITTI坐标系y轴向下(与图像y轴同向),但Open3D默认y轴向上。直接将bbox_3d传入Open3D,会导致所有点y坐标被镜像翻转,h方向变成负值,框体塌陷。
解决:在构建bbox_3d后,对y坐标取反:corners_3d[:, 1] *= -1。验证方法:打印corners_3d[:, 1].min()和max(),正常应为负值(如-1.5到0.0),若为正值则说明未翻转。

4.3 现象:oxts/000000.txt读出的经纬度与velodyne/000000.bin时间戳无法对齐

原因:KITTIoxts数据是毫秒级时间戳(如1234567890123),而velodyne和image_2的文件名000000对应的是同步后的索引号,非原始时间戳。二者通过oxts/timestamps.txt和velodyne_points/timestamps.txt中的纳秒级时间戳对齐,而非文件名顺序。
解决:不要用文件名序号匹配,而要用timestamps.txt中的时间戳插值。例如:velodyne_points/timestamps.txt第0行是1234567890123456789,oxts/timestamps.txt第5行是1234567890123456800,则认为二者同步。验证方法:加载同一帧的oxts和velodyne,计算oxts中lat, lon经WGS84转ECEF坐标,再用Tr_velo_to_cam逆变换,看是否与velodyne点云中心位置一致(误差应<1m)。

4.4 现象:用cv2.projectPoints投影3D框顶点,部分顶点投影到图像外,但valid_mask显示z>0

原因:cv2.projectPoints默认使用针孔模型,但KITTIP2已包含校正后的内参(即R0_rect已应用),而projectPoints若传入原始P2会重复校正。正确做法是:先用Tr_velo_to_rect将点转到rectified cam坐标系,再用P2的前3列(即K = P2[:, :3])进行投影,而非直接用projectPoints。
解决:弃用cv2.projectPoints,改用手动矩阵乘法(见2.3节代码)。验证方法:对一个已知在图像中央的3D点(如x=10,y=-1,z=30),手动计算u = P2[0,:] @ [x,y,z,1]^T / P2[2,:] @ [x,y,z,1]^T,与projectPoints结果对比,若差>1像素则说明projectPoints用法错误。

4.5 现象:label_2中truncated=0.99的车辆,其2D框x1却大于0,未被截断

原因:truncated字段是人工标注时估计的截断比例(0=未截断,1=完全截断),非算法计算值。KITTI官方说明明确:“truncatedis the ratio of the object that is outside the image along the x-direction”。但它不参与任何坐标计算,仅作标注参考。很多教程误将其当作过滤条件,导致漏标。
解决:truncated仅用于统计分析(如“评估模型在截断目标上的性能”),不可用于过滤点云或框。验证方法:查看KITTI官网文档devkit_object/readme.txt,搜索truncated,原文为 “truncated: Float from 0 (non-truncated) to 1 (truncated)”.


5. 轨迹对比实战:用KITTI可视化验证ORB-SLAM2双目轨迹精度

当你跑完ORB-SLAM2在KITTI双目序列上的SLAM,得到CameraTrajectory.txt,下一步不是直接算ATE(绝对轨迹误差),而是用可视化定位问题根源。ATE只是一个标量(如0.85m),但可视化能告诉你:误差是均匀漂移?还是在某个路口突变?抑或与IMU数据冲突?本章教你用本项目搭建的可视化框架,把SLAM轨迹、真值轨迹(oxts)、点云、图像四者叠在一起,做毫米级精度排查。

5.1 解析ORB-SLAM2轨迹:从CameraTrajectory.txt提取SE3位姿

ORB-SLAM2输出的CameraTrajectory.txt每行格式为:
timestamp tx ty tz qx qy qz qw
其中tx,ty,tz是相机原点在世界坐标系下的位置,qx,qy,qz,qw是四元数表示的旋转(世界→相机)。但KITTI的oxts给出的是车辆坐标系下的IMU姿态(即世界→车辆),而ORB-SLAM2的世界坐标系原点在第一帧相机位置。二者需统一到同一参考系。

def load_orb_trajectory(traj_path): """ 加载ORB-SLAM2 CameraTrajectory.txt 返回: dict, key为timestamp(秒),value为4x4 SE3矩阵(世界→相机) """ traj = {} with open(traj_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) < 8: continue ts = float(parts[0]) tx, ty, tz = map(float, parts[1:4]) qx, qy, qz, qw = map(float, parts[4:8]) # 四元数转旋转矩阵(scipy可省,此处手写) R = np.array([ [1-2*qy**2-2*qz**2, 2*qx*qy-2*qz*qw, 2*qx*qz+2*qy*qw], [2*qx*qy+2*qz*qw, 1-2*qx**2-2*qz**2, 2*qy*qz-2*qx*qw], [2*qx*qz-2*qy*qw, 2*qy*qz+2*qx*qw, 1-2*qx**2-2*qy**2] ]) T = np.eye(4) T[:3, :3] = R T[:3, 3] = [tx, ty, tz] traj[ts] = T return traj # 加载SLAM轨迹 orb_traj = load_orb_trajectory("ORB_SLAM2/Examples/Monocular/KITTI/00/CameraTrajectory.txt")

坐标系对齐关键:ORB-SLAM2的“世界坐标系”是第一帧相机坐标系,而KITTIoxts的“世界坐标系”是WGS84地理坐标系。二者不能直接对比。正确做法是:取ORB-SLAM2第一帧的T_wc(世界→相机),其逆T_cw即为相机→世界的变换;再取oxts第一帧的T_vw(车辆→世界),则T_cv = T_cw @ T_vw即为相机→车辆的变换。后续所有ORB-SLAM2位姿,都左乘T_cv转到车辆坐标系,再与oxts对比。

5.2 可视化轨迹对比:在3D点云场景中绘制真值与SLAM轨迹线

我们将ORB-SLAM2轨迹(红色)和KITTI真值轨迹(蓝色)同时绘制在Open3D点云场景中,并用不同颜色区分路段(如直行/转弯),便于定位误差爆发点。

def plot_trajectories(vis, orb_traj, oxts_traj, color_orb=[1,0,0], color_oxts=[0,0,1]): """ 在Open3D中绘制两条轨迹线 orb_traj/oxts_traj: dict, key为timestamp, value为4x4 SE3矩阵(世界→相机/车辆) """ # 提取轨迹点(只取平移部分) orb_pts = np.array([T[:3, 3] for T in orb_traj.values()]) oxts_pts = np.array([T[:3, 3] for T in oxts_traj.values()]) # 创建线段(每两点连一线) def create_line_set(points, color): line_set = o3d.geometry.LineSet() line_set.points = o3d.utility.Vector3dVector(points) lines = [[i, i+1] for i in range(len(points)-1)] line_set.lines = o3d.utility.Vector2iVector(lines) line_set.colors = o3d.utility.Vector3dVector( np.tile(color, (len(lines), 1)) ) return line_set vis.add_geometry(create_line_set(orb_pts, color_orb)) vis.add_geometry(create_line_set(oxts_pts, color_oxts)) # 添加起点标记 vis.add_geometry(o3d.geometry.TriangleMesh.create_sphere(radius=0.5).translate(orb_pts[0]).paint_uniform_color([1,0,0])) vis.add_geometry(o3d.geometry.TriangleMesh.create_sphere(radius=0.5).translate(oxts_pts[0]).paint_uniform_color([0,0,1])) # 示例:加载oxts轨迹(需先用oxts_to_world.py转换) # oxts_traj = load_oxts_as_world("data/2011_09_26/2011_09_26_drive_0001_sync/oxts/data/") # plot_trajectories(vis_3d, orb_traj, oxts_traj)

实战技巧:不要一次性绘制全部10000帧轨迹——Open3D会卡死。应分段加载:先加载前100帧,确认对齐无误

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进