拓冰建站拓冰建站
首页 / 资讯中心 / 正文

KITTI三维目标检测可视化工具:从数据解析到点云投影的完整实战

1. 为什么我要把KITTI可视化单独做成一个工具做目标检测做到一定阶段都会撞上一堵墙指标很好看但你看不到模型到底在干什么。尤其是三维目标检测光看AP、mAP、ATE、AOS这些数字你根本不知道某个框是歪了、短了还是整个预测目标完全错位。我以前调YOLO系列的2D检测时直接把框画到图上就能发现问题可一到自动驾驶场景的3D检测数据和结果的表达方式完全不同。点云是一堆散点3D框是一个带朝向的长方体没有可视化工具就像蒙着眼睛看报告越看越慌。后来我开始用KITTI数据集做实验这是自动驾驶感知领域最常用的公开数据集之一几乎所有做三维目标检测的人都要和它打交道。KITTI提供了激光雷达点云、双目图像、GPS数据和人工标注的3D目标框结构完整但问题也来了你拿模型的输出和真值对比的时候怎么快速确认“这个框是不是真的骑在车身上”你总不能每次都把点云数据导出成文件再找个专门查看器打开数一遍。一旦要对比多个epoch的预测结果、检查数据标注质量、或者排查某帧点云里远处小目标漏检手动处理会把人逼疯。所以我自己写了一个轻量级可视化工具叫kitti_vis。它做的事情说起来很简单把一个KITTI序列里的图片、激光雷达点云、2D框、3D框投影结果全部叠在同一套逻辑下按下空格切下一帧实时看到模型预测和真值之间的差别。运行起来只有一个脚本加一个配置文件不依赖重型框架也不需要额外部署服务。写完之后我自己的调试效率明显提高后来好几个做3D检测的同事也直接拿去用说明这类工具的需求不是个例。这篇内容我把kitti_vis的完整设计和实现思路拆开讲。从KITTI数据格式、相机和雷达坐标系的关系到3D框角点怎么算、投影矩阵怎么用再到实际踩过的坑全部摊开说。不管你是刚接触三维目标检测的初学者还是已经跑过几个模型、正在为“结果可视化”头疼的工程同学照着这套思路都可以自己搭一个趁手的调试工具。2. 动手之前先把KITTI的数据结构和坐标系吃透很多人在可视化上翻车不是因为绘图代码写错而是没搞明白KITTI的文件格式和坐标系。这部分是整个工具的基石必须放在第一步讲。2.1 一个序列目录里到底放了什么KITTI检测任务的最基本数据单元是“帧”。一个典型的序列目录下image_2放的是左相机彩色图velodyne放的是激光雷达点云bin文件label_2放的是这一帧里所有目标物体的标注信息calib放的是相机和雷达之间的标定矩阵。文件名一一对应比如000000.png、000000.bin、000000.txt这样组成同一帧的完整数据。想可视化一个结果最少需要同时读这三类文件。label_2里的每个txt文件一行代表一个目标物体字段顺序是固定的类型 截断程度 遮挡程度 观察角度 alpha 2D框左 2D框顶 2D框右 2D框底 高度h 宽度w 长度l 位置x 位置y 位置z 绕Y轴旋转角rotation_y score(可选)我第一次解析的时候只注意了前面几列结果3D框画出来满屏乱飘。这里有两个非常关键的字段容易被忽略。alpha是观测角指物体在相机坐标系下的朝向与相机光心连线之间的夹角和rotation_y不同而rotation_y才是物体自身绕Y轴旋转的角度画3D框的时候我们真正需要的是这个角度。score通常在真值文件里没有只有模型预测结果里才会出现解析的时候要用长度判断兼容两种格式。还有个更容易坑的地方位置x y z在KITTI官方约定里通常表示目标3D框底部中心在相机坐标系下的坐标y轴朝下、z轴朝前单位是米。有些开源代码或者模型输出会把它当作框的几何中心来用这两种解释画出来的框在垂直方向会差出半个车高。所以kitti_vis里我默认按“底部中心”处理一旦发现框悬空或者陷进地面优先怀疑这个语义问题。2.2 calib.txt里四个矩阵的关系标定文件里最常见的是P0、P1、P2、P3四组3x4投影矩阵分别对应左侧灰度相机、右侧灰度相机、左侧彩色相机、右侧彩色相机。我们可视化左彩色图核心就是P2矩阵。此外还有两个必须一起用的矩阵R0_rect是3x3的旋转矩阵用于把相机0坐标系下的点矫正到无畸变的相机坐标Tr_velo_to_cam是3x4的变换矩阵作用是把激光雷达坐标系下的点转换到相机坐标系。点云要投到左图上的完整公式是这样的Y P2 * R0_rect * Tr_velo_to_cam * X_velo其中X_velo是雷达点的齐次坐标(x, y, z, 1)R0_rect需要扩展成4x4再乘进去Tr_velo_to_cam本身是3x4补一行[0,0,0,1]后和前面的R0_rect组成4x4变换。最终得到的是一个3x1的齐次坐标除以第三个分量后前两个分量就是图像上的像素坐标。这个公式我建议直接抄进工具里因为它兼容了KITTI几乎所有版本的数据。有个细节很少有人提KITTI不同数据子集的标定文件略有区别有的只给了P0到P3和Tr_velo_to_cam没有单独给R0_rect。遇到这种情况不要慌把R0_rect直接设为单位矩阵多数情况下误差可接受但如果做严格评测还是从标定文件里找对应字段。kitti_vis在读取calib的时候我做了一个兼容分支缺哪个矩阵就自动补单位阵这样老版本数据也能打开。3. kitti_vis的整体设计思路和可视化规则工具的核心设计目标是“一屏看懂一帧”。我见过不少可视化工具把点云放一个窗口、图片放另一个窗口来回切窗口头都快扭断。kitti_vis的做法是把三层信息统一投影到图像坐标上做成合成视图。3.1 一帧画面里叠加三层信息第一层是原始左相机图片作为背景。第二层是点云投影点把雷达点按照2.2节里的公式投到图上每个点根据深度距离映射成不同颜色的像素点本质就是深度图。第三层是目标框包括2D矩形框和3D立体框。三层叠在一起你能直接看到3D框的四个底角是不是压在地面或者车体上、点云是否正好包裹住目标、预测框相对真值有没有前后偏。点云投影后直接在图上画像素点OpenCV就能做速度很快。颜色映射我用的是cv2.applyColorMap把点深度归一化后映射成JET色带近处红色远处蓝色。这样即便目标在远处很小你也能通过颜色判断它离自车多远。实测下来这样一个合成视图比分开看三个窗口高效得多约等于把“对帧”的时间缩短到原来的三分之一。3.2 类别颜色和框线绘制规则做目标检测可视化颜色策略看着简单实际影响体验很大。同一帧里可能有Car、Pedestrian、Cyclist、Van等多类目标如果颜色全靠随机看一会儿眼睛就花了。kitti_vis里我固定了一套类别颜色表比如Car用绿色Pedestrian用红色Cyclist用黄色Truck用蓝色。这个规则写在一个配置字典里模型输出对应KITTI类名时直接映射不认识的类别统一归成灰色。2D框用4条线段画3D框要画12条线段也就是一个立方体的12条棱。如果逐条用cv2.line去画代码写起来很啰嗦我把角点的连接关系预先定义成一张边表后端可以循环遍历。这里有个提升可读性的小技巧3D框垂直方向的那4条竖边用实线顶部和底部的轮廓线用稍细的线避免12条线混在一起分不清正反面。真值框画成实线预测框画成虚线一眼能区分。3.3 为什么3D框要单独做深度排序如果多个目标在图像上重叠后画的框会把先画的框盖住这在视觉上会造成误解。kitti_vis做的比较简单的处理在画图之前对每个目标的3D框中心点按深度从远到近排序先画远处的框再画近处的框。近处目标最后绘制叠在图上层符合日常观察习惯。对多数调试场景这种“按中心距离粗排序”已经够用。如果追求更精确的遮挡关系得用Z-Buffer思想去判断哪些线段被遮挡复杂度会上升一个级别。kitti_vis作为调试工具不追求可视化仿真级别的保真度排序逻辑已经能处理95%的场景。4. 核心实现三步画出一个可用的3D目标框我挑选三个最关键的实现环节把代码思路和参数计算过程完整过一遍。这部分代码按顺序组装起来就能跑通kitti_vis的核心链路。4.1 解析标定参数和标签文件第一步是从calib.txt里取出P2、R0_rect、Tr_velo_to_cam三个矩阵再从label文件里解析每个目标。解析P2的时候有个容易出错的地方P2在文本里是按行展开的12个数字需要reshape(3, 4)而不是reshape(4, 3)顺序一旦搞反投影结果会乱得完全没法看。R0_rect和Tr_velo_to_cam要转成4x4齐次矩阵。R0_rect是一个3x3旋转矩阵扩展方式是在右下角补一个1其余补0。Tr_velo_to_cam是3x4矩阵它本身就是旋转加平移扩展时在下面补一行[0,0,0,1]。为了后面矩阵乘法的效率可以在初始化阶段就把P2 * R0_rect * Tr_velo_to_cam整个组合矩阵算好存成一个4x4或3x4的组合投影矩阵这样每一帧所有点云点都只做一次矩阵乘法省掉大量重复计算。标签解析我之前在2.1节列过字段转换数值类型时记得把字符串转成floatoccluded这类字段转成int但可视化过程里主要用到的还是类型、尺寸、位置和rotation_y。4.2 计算3D边界框的8个角点这是整个工具最容易出错的环节。KITTI的3D框由高度h、宽度w、长度l、三维位置以及rotation_y共同确定。我这里实现的逻辑是先在以目标底部中心为原点的局部坐标系里定义8个角点再绕Y轴旋转rotation_y角度最后平移到真实位置。局部坐标系下的角点坐标可以这样定义def compute_3d_box_corners(h, w, l, loc_x, loc_y, loc_z, rotation_y): # 局部坐标系x向前y向下z向右8个点依次为底面4角、顶面4角 x_corners [l / 2, l / 2, -l / 2, -l / 2, l / 2, l / 2, -l / 2, -l / 2] y_corners [0, 0, 0, 0, -h, -h, -h, -h] z_corners [w / 2, -w / 2, -w / 2, w / 2, w / 2, -w / 2, -w / 2, w / 2] R np.array([ [np.cos(rotation_y), 0, np.sin(rotation_y)], [0, 1, 0], [-np.sin(rotation_y), 0, np.cos(rotation_y)] ]) corners np.vstack([x_corners, y_corners, z_corners]) corners R corners corners[0, :] loc_x corners[1, :] loc_y corners[2, :] loc_z return corners.T这段代码里y_corners的底面为0、顶面为-h是因为相机坐标系的y轴朝下车辆在地面上方所以顶部坐标是负值。rotation_y的旋转矩阵绕Y轴且遵循KITTI标签里的旋转定义角度单位为弧度正值表示物体朝向逆时针偏转还是顺时针偏转取决于坐标系方向。我建议开发的时候不要凭感觉猜直接画一帧出来对照原图里车辆的朝向验证符号是否正确。4.3 把点云和角点投影到图像平面3D角点从相机坐标转到图像坐标方法很直接把每个角点扩展成齐次坐标乘以P2然后除以第三个分量。def project_points_to_image(points, P2): pts np.hstack([points, np.ones((points.shape[0], 1))]) img_pts (P2 pts.T).T img_pts[:, 0] / img_pts[:, 2] img_pts[:, 1] / img_pts[:, 2] return img_pts[:, :2]注意投影之后可能会出现坐标为负或者超出图像边界的点这是正常现象目标在画面边缘之外时就会发生。画线之前做好裁剪即可不要盲目删除这些目标否则帧边缘的检测结果会被误杀。点云投影要比目标框投影多一步雷达坐标系到相机坐标系的转换也就是2.2节那个组合矩阵。拿到投影结果后把点的深度当作颜色映射的输入然后遍历所有点、用cv2.circle或直接给像素赋值的方式渲染到画布上。一帧大概有十万个点左右逐点调用cv2的画圆接口会很慢我实测直接对图像数组按索引赋值比逐点画圆快两到三倍批量处理时差距更明显。5. 实测里踩过的坑和排查方法工具写出来之后用了大半年中间遇到过不少奇怪现象。我把最典型的几个问题整理成一张排查表这些都是单看坐标计算公式很难发现的经验。5.1 框画对了但投影到图像上整体偏移现象3D框在点云视图里和车辆严丝合缝但投影到图片上整体向右或向左偏了几十像素。排查之后发现责任通常不在投影代码而在label文件里的x y z坐标语义。前面我说过KITTI的location可以是框底部中心也有的工具按几何中心理解。如果代码按底部中心计算角点实际数据给的是几何中心那画出来的框在垂直方向上会偏半个车高如果水平方向偏优先看Tr_velo_to_cam或者P2是否选错相机。工具里我加了一个one_click_shift调试模式可以直接在UI里输入一个三维偏移量手动把所有框平移歪到什么程度基本能一眼定位。5.2 点云投影到图像的深度层次异常另一个常见问题是点云投影后的颜色分布很奇怪近处目标的点反而显示成深蓝色远处目标却显示成红色。这通常不是颜色映射写错而是投影时忘记把组合矩阵里的坐标除以w分量。齐次坐标做透视除法是必须步骤漏掉之后深度值直接变成了带比例关系的投影值颜色自然乱掉。排查方法也简单取一个已知坐标的雷达点手工算一遍完整公式再把中间矩阵打印出来对比。我一般在工具启动时跑一个自检函数把某个固定雷达点(10, 0, 0, 1)投到图上输出像素坐标如果和标定手册给的结果一致说明矩阵链路没问题。5.3 截断程度和遮挡程度怎么用来过滤目标KITTI标签里的truncated和occluded字段在官方评测中用于划分难度级别但可视化时很多人会忽略。我的经验是调试模型输出质量时把truncated 0.2的目标用半透明框显示把occluded 2的目标弱化为细线框这样能更快聚焦到完整可见的目标上。注意预测结果的标签文件通常没有这两个字段解析时要统一补0。否则一旦模型输出和真值放在一起比较一个解析走了带截断判断的逻辑另一个不走显示结果就会出现“公平性”问题。kitti_vis的做法是统一在配置里指定是否启用截断过滤对所有目标一视同仁。6. 从kitti_vis到更通用的调试框架工具做到能我用的程度后我没有停下扩展。因为目标检测的可视化需求实在太相似了今天调KITTI明天可能就要调nuScenes后天可能跑一个自己采集的数据集。与其每次从零写不如把kitti_vis的架构抽象成几个可复用模块。6.1 投影函数独立成公共模块我把4.2和4.3节的角点计算、点云投影函数单独拆成kitti_geometry.py无论上层是绘制图像还是绘制点云都复用同一份几何逻辑。模型输出只要转成KITTI的标签格式就能直接喂给投影管线。我身边有人用这套逻辑去可视化OpenPCDet的验证结果只改了标签读取部分的字段映射就能正常使用。新数据集往往只是坐标系定义不同核心算法是一样的。只要把组合投影矩阵改一下或者传入一个不同的外参乘积结果整个管线就能复用。这也是我建议所有做3D检测相关开发的人养成的习惯把“几何变换”和“渲染展示”分开不要揉在一个函数里。6.2 批量处理和一键导出视频单帧调试够用之后批量处理的需求马上出现。kitti_vis增加了一个batch_mode输入一个序列目录自动遍历所有帧把合成视图逐帧保存为PNG再用OpenCV的VideoWriter拼成视频。这样训练跑完一整个epoch就能自动生成一段带预测框的序列视频不用守在电脑前一张一张看。批量处理时有一个性能优化点不要每帧都重新读calib文件初始化时读一次缓存到内存。点云bin文件比较大读取时用np.fromfile一次性加载不要用Python逐点读文件速度差出几十倍。序列视频建议先用临时目录存PNG再合成直接一帧帧写MP4一旦中间某帧异常退出前面处理的全部白费。我个人在实际使用中最深的体会是可视化工具不是写完就完了它最大的价值是给你提供“复现问题、验证假设、快速定位”的能力。很多时候你以为模型某个类别的检测精度不行把预测框可视化出来一看发现是标签本身坐标就错了模型反倒学对了。kitti_vis这类工具真正解决的不是“画框”这个问题而是帮你建立了从数据到结果之间的快速反馈回路。调试速度一快实验迭代节奏就跟着上去这才是它带来的最实在的收益。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门