十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

KITTI预处理全流程指南:从下载到BEV编码,打通Complex-YOLO数据链路

KITTI预处理全流程指南:从下载到BEV编码,打通Complex-YOLO数据链路 我最早复现Complex-YOLO的时候就在KITTI数据上栽了个大跟头。网上下了一套KITTI解压完才发现里面只有image和velodyne_points翻了半天没找到label_2和calib目录。后来才搞清楚那是SLAM教程里最常用的odometry版本拿来做视觉里程计没问题但放到3D目标检测训练里就是缺胳膊少腿。更折磨人的是就算换到object detection数据包点云用的是雷达坐标系标签却写在相机坐标系里不把这两个坐标系对齐画出来的3D框全飘在空中。这篇博文就是整理我做完KITTI预处理全流程之后的东西覆盖下载哪些压缩包、目录怎么组织、怎么读bin点云、怎么做三通道BEV编码、怎么把3D框从相机坐标变换到雷达坐标以及最后怎么验证数据没做错。如果你正在复现Complex-YOLO或者想把3D点云目标检测的数据链路彻底搞清楚这篇能帮你少踩不少坑。1. 训练Complex-YOLO前先分清KITTI里那几套“长得像”的数据1.1 为什么我拿SLAM那套KITTI数据来训练直接失败很多人是从SLAM相关的书和视频里第一次听说KITTI数据集的比如SLAM十四讲里提到KITTI下载跟着教程拿到的是odometry里程计数据集。这个系列目录下确实有图像、有velodyne点云、有位姿真值看起来“什么都有”但3D目标检测训练需要的东西恰恰不在里面。目标检测数据集需要的是每帧点云对应的3D边界框标签也就是每个物体类别、截断程度、遮挡程度、2D框、3D尺寸、3D中心位置、绕y轴旋转角。odometry数据集提供的是相机位姿不是物体级别的标注。你拿odometry数据去跑Complex-YOLO训练代码解析label文件时会直接报错或者更糟糕的是你不会立刻发现因为你可能会自己去写一套“无监督”逻辑但那已经偏离原论文的复现路线了。所以下单之前先看清楚文件名object detection相关的压缩包统一带data_object_前缀odometry相关的则带data_odometry_前缀。这个区别在KITTI官网首页一眼就能看到千万别顺手点错。1.2 雷达坐标系、相机坐标系与BEV视图的关系KITTI里涉及三套坐标很多人栽在这里。Velodyne激光雷达坐标系定义是x轴指向车头正前方y轴指向车身左侧z轴垂直向上。相机坐标系则完全不同x轴指向右侧y轴指向下方z轴指向相机前方。也就是说在雷达坐标里“前方”是x轴方向在相机坐标里“前方”是z轴方向两个坐标系之间隔着一个旋转平移矩阵。BEVBirds Eye View就是把点云从空中往下看把z轴方向压平留下x-y平面。Complex-YOLO不走常规的3D卷积路线而是先把点云编码成一张BEV图像再用类似YOLO的2D检测思想去预测目标。由于雷达坐标系天然是x向前、y向左、z向上所以BEV图上通常让x方向作为图像的行方向y方向作为列方向这样车头方向在图像里是“向上”的。这个过程可以类比成两个人面对面坐着看同一张地图一个习惯于“前方是门”另一个习惯于“右侧是窗”中间必须有一套统一的坐标换算规则。KITTI提供的calib标定文件就是这套换算规则的“官方字典”。1.3 Complex-YOLO对数据格式的隐性要求如果你去翻Complex-YOLO论文会发现它对输入有一个明确的预处理约定点云限制在x方向0到70.4米、y方向-40米到40米、z方向-3米到1米然后以0.1米分辨率投影成一张704乘800的BEV三通道图。这三个范围不是随便拍脑袋定的。前方70.4米覆盖了KITTI里绝大多数有效目标左右各40米能包住多车道高速和城区道路场景z方向上的-3到1米则把地面以下噪点和头顶树枝、立交桥等无关反射剔除掉。分辨率0.1米意味着每10个像素对应1米这个粒度在远处目标上还能保留足够的点云轮廓再粗就分不清行人和骑行者了。这几个参数在预处理脚本里是全局常量后面很多代码都依赖它们。后面你看到的BEV尺寸、栅格索引、标签编码全都围绕这个范围展开。我的建议是先把这三个取值范围和0.1米分辨率写死等整个流程跑通之后再考虑要不要调。2. 下载解压与目录整理从压缩包到标准数据集的完整动作2.1 该下哪几个压缩包一份能直接照着做的清单Complex-YOLO训练只需要KITTI Object Detection数据集里的三样东西点云、3D标签、校准文件。如果你要做可视化检查建议再把左相机图像也下了。下面是明确清单。压缩包用途训练是否必须说明data_object_velodyne.zip激光雷达点云必须约29GBtraining和testing都包含在其中data_object_label_2.zip3D目标检测标签必须很小只有几MBdata_object_calib.zip相机/雷达标定文件必须很小data_object_image_2.zip左相机图像强烈建议约12GB用于投影验证GT框data_object_planes.zip地面平面参数可选某些方法需要Complex-YOLO基本用不上有人会问testing部分要不要保留。训练时只用training目录testing主要是官方评测时用的你本地复现可以完全不管它。但下载还是整包下载解压时如果嫌占空间可以只解压training子目录。2.2 解压后的目录结构长什么样三个压缩包分别解压后最终应该整理成这样的结构KITTI/ ├── training/ │ ├── calib/ │ │ ├── 000000.txt │ │ ├── 000001.txt │ │ └── ... │ ├── image_2/ │ │ ├── 000000.png │ │ ├── 000001.png │ │ └── ... │ ├── label_2/ │ │ ├── 000000.txt │ │ ├── 000001.txt │ │ └── ... │ └── velodyne/ │ ├── 000000.bin │ ├── 000001.bin │ └── ... └── testing/ ├── calib/ ├── image_2/ └── velodyne/这里有个坑三个压缩包解压后各自会带一层目录比如data_object_velodyne/training/velodyne、data_object_label_2/training/label_2它们不是天然的合并结构。训练代码通常按KITTI/training/{velodyne,label_2,calib,image_2}这种目录去找文件所以解压完最好手动把子目录移动到同一个根目录下。可以先把压缩包放在不同临时目录里解压然后用cp -r或rsync合并。目录名必须完全对齐velodyne、label_2、calib、image_2大小写和单复数都不能错否则后续代码路径会拼接失败。2.3 下载中断、文件损坏与一致性校验KITTI压缩包体积不小尤其是点云包下载过程中断是常态。我不推荐用浏览器默认下载因为断点续传能力差。用命令行工具加-c参数可以在中断后继续下载wget -c https://s3.eu-central-1.amazonaws.com/avg-kitti/data_object_velodyne.zip下完后不要急着解压。先做两件事第一比较压缩包大小是不是和官网标注一致第二解压时检查有没有CRC错误。如果解压报错有效做法是删掉重下而不是强行解压出一部分文件来用。数据是否完整还有更细的检查方式。每一帧velodyne点云文件都是float32数组每个点由x、y、z、reflectance四个float组成因此文件字节数必须能被16整除。写一个脚本扫一遍所有bin文件顺手统计点数范围能帮你发现下载损坏或截断的文件。import os import numpy as np def check_velodyne_dir(velodyne_dir): for name in sorted(os.listdir(velodyne_dir)): path os.path.join(velodyne_dir, name) size os.path.getsize(path) if size % 16 ! 0: print(f{name}: size {size} not divisible by 16) continue points np.fromfile(path, dtypenp.float32).reshape(-1, 4) if len(points) 1000: print(f{name}: only {len(points)} points, suspicious) print(done)3. 点云读取与BEV编码把Velodyne bin变成网络能看的图3.1 一个bin点云文件的内部结构KITTI的velodyne点云不是常见的有头PCD或者LAS格式而是最简单纯粹的二进制裸数组。每一帧000000.bin里连续存放若干个float32每4个float组成一行依次是x、y、z、反射强度。读取代码非常短import numpy as np def load_kitti_bin(bin_path): data np.fromfile(bin_path, dtypenp.float32).reshape(-1, 4) points data[:, :3] # x, y, z reflectance data[:, 3] # intensity return points, reflectance一帧64线Velodyne点云大概有10万到12万个点。读文件时不要一次性np.load之类直接用np.fromfile就好速度已经很快。3.2 裁剪范围和分辨率怎么定原始点云覆盖整个360度范围但训练时模型只需要车辆前方的点。Complex-YOLO按论文约定裁剪到这样一块区域x方向0到70.4米y方向-40米到40米z方向-3米到1米。为什么x方向从0开始因为雷达坐标里x轴指向车头前方x小于0的点在车身后方而KITTI目标检测的主要标注对象都在前方车身后方的点对训练目标帮助有限。y方向左右对称是因为道路场景中目标会出现在左右两侧。z方向控制在-3到1米既能保留地面附近的有效反射又能去掉大量的高空噪点。分辨率0.1米是论文作者选的。我们把这个值换成代码DISCRETIZATION 0.1 X_MIN, X_MAX 0.0, 70.4 Y_MIN, Y_MAX -40.0, 40.0 Z_MIN, Z_MAX -3.0, 1.0 BEV_WIDTH int((X_MAX - X_MIN) / DISCRETIZATION) # 704 BEV_HEIGHT int((Y_MAX - Y_MIN) / DISCRETIZATION) # 8003.3 三通道BEV编码的具体实现与归一化点云投影到BEV之后不能只保留一个二值占用图那样点云高度、反射强度、点密度这些信息全丢了。Complex-YOLO使用三通道编码每个通道对应一种点云统计特征。height通道每个栅格内点的最大z值再除以z范围做归一化。它保留了物体的立体轮廓。intensity通道每个栅格内点的最大反射强度。路面车道线、汽车金属表面的反射率和行人差异明显对分类很有用。density通道每个栅格内点的数量做对数压缩避免近处点云数量淹没远处目标。先给出最直观的循环版本帮助理解但实际训练时速度太慢只建议用于复现逻辑def create_bev_naive(points, reflectance): bev np.zeros((BEV_HEIGHT, BEV_WIDTH, 3), dtypenp.float32) for i in range(len(points)): x, y, z points[i] if not (X_MIN x X_MAX and Y_MIN y Y_MAX and Z_MIN z Z_MAX): continue col int((x - X_MIN) / DISCRETIZATION) row int((y - Y_MIN) / DISCRETIZATION) if 0 row BEV_HEIGHT and 0 col BEV_WIDTH: if z bev[row, col, 0]: bev[row, col, 0] z if reflectance[i] bev[row, col, 1]: bev[row, col, 1] reflectance[i] bev[row, col, 2] 1.0 bev[:, :, 0] (bev[:, :, 0] - Z_MIN) / (Z_MAX - Z_MIN) bev[:, :, 2] np.minimum(1.0, np.log(bev[:, :, 2] 1.0) / np.log(64.0)) return bev真正用于生成训练集时用向量化方法替代逐点循环def create_bev_vectorized(points, reflectance): mask ( (points[:, 0] X_MIN) (points[:, 0] X_MAX) (points[:, 1] Y_MIN) (points[:, 1] Y_MAX) (points[:, 2] Z_MIN) (points[:, 2] Z_MAX) ) points points[mask] reflectance reflectance[mask] col ((points[:, 0] - X_MIN) / DISCRETIZATION).astype(np.int32) row ((points[:, 1] - Y_MIN) / DISCRETIZATION).astype(np.int32) height_map np.zeros((BEV_HEIGHT, BEV_WIDTH), dtypenp.float32) intensity_map np.zeros((BEV_HEIGHT, BEV_WIDTH), dtypenp.float32) density_map np.zeros((BEV_HEIGHT, BEV_WIDTH), dtypenp.float32) np.maximum.at(height_map, (row, col), points[:, 2]) np.maximum.at(intensity_map, (row, col), reflectance) np.add.at(density_map, (row, col), 1.0) bev np.stack([height_map, intensity_map, density_map], axis-1) bev[:, :, 0] (bev[:, :, 0] - Z_MIN) / (Z_MAX - Z_MIN) bev[:, :, 2] np.minimum(1.0, np.log(bev[:, :, 2] 1.0) / np.log(64.0)) return bev注意行和列的对应关系row对应y方向col对应x方向最终BEV形状是(800, 704, 3)其中800是y方向网格数704是x方向网格数。如果你把这两者反过来后面标签的中心点映射也会跟着全错。4. 标签转换从KITTI标定文件到Complex-YOLO训练目标4.1 label_2文件每一列的含义KITTI的3D标签文件是文本格式每行一个目标。打开一个典型文件你会看到类似这样的内容Car 0.00 0 1.58 599.41 156.40 629.75 189.25 1.67 1.87 3.69 -7.06 1.60 24.56 -1.58每一列依次是列位字段含义1type目标类别如Car、Pedestrian、Cyclist、Van、Truck等2truncated截断程度0到1目标在图像边缘被截断的比例3occluded遮挡级别0到34alpha观察角弧度表示物体中心相对相机光轴的方向5-8bbox2D框x1, y1, x2, y2在左相机图像上9-11dimensions3D尺寸高、宽、长单位米定义在相机坐标系12-14location3D中心坐标tx, ty, tz单位米定义在相机坐标系15rotation_y绕相机y轴的旋转角弧度注意dimensions顺序是height、width、length不是我们习惯的长宽高。height对应相机坐标系的y轴方向width对应x轴方向length对应z轴方向。这与后面计算3D框角点时矩阵的行顺序直接相关。4.2 calib标定文件的读取与矩阵组合calib目录下每个txt文件对应一帧的标定参数。内容大致如下P0: 7.070912e02 ... P1: 7.070912e02 ... P2: 7.070912e02 ... P3: 7.070912e02 ... R0_rect: 9.999239e-01 ... Tr_velo_to_cam: 6.927964e-03 ... Tr_imu_to_velo: 9.999976e-01 ...其中P2是左相机的3x4投影矩阵把相机坐标投影到图像像素坐标。R0_rect是3x3旋转矩阵用来校正在相机坐标系。Tr_velo_to_cam是3x4变换矩阵把Velodyne雷达坐标变换到相机坐标。对于Complex-YOLO训练来说标签中心在相机坐标点云在雷达坐标我们需要的是从相机坐标变换到雷达坐标的矩阵。也就是说先组合一个“雷达到相机”的4x4矩阵再求逆。import numpy as np def read_calib(calib_path): calib {} with open(calib_path, r) as f: for line in f.readlines(): key, val line.split(:) values [float(v) for v in val.split()] if key in [P0, P1, P2, P3]: calib[key] np.array(values).reshape(3, 4) elif key R0_rect: calib[key] np.array(values).reshape(3, 3) elif key in [Tr_velo_to_cam, Tr_imu_to_velo]: calib[key] np.array(values).reshape(3, 4) return calib def build_cam_to_velo(calib): R_rect np.eye(4) R_rect[:3, :3] calib[R0_rect] Tr_velo_cam np.eye(4) Tr_velo_cam[:3, :] calib[Tr_velo_to_cam] T_velo_to_cam R_rect Tr_velo_cam T_cam_to_velo np.linalg.inv(T_velo_to_cam) return T_cam_to_velo一定要记得把R0_rect扩展成4x4单位矩阵形式再把Tr_velo_to_cam也扩展成4x4否则矩阵乘法的维数对不上。这里也是一个经典报错点。4.3 从相机坐标到雷达坐标的3D框角点变换KITTI标注里的3D框由中心位置、尺寸和rotation_y定义。rotation_y是绕相机y轴的旋转。我们需要先在这个“相机坐标系语义”下把8个角点算出来再整体变换到雷达坐标。def compute_corners3d_cam(center, dims, rotation_y): h, w, l dims x, y, z center 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.array([ [l / 2, l / 2, -l / 2, -l / 2, l / 2, l / 2, -l / 2, -l / 2], [0, 0, 0, 0, -h, -h, -h, -h], [w / 2, -w / 2, -w / 2, w / 2, w / 2, -w / 2, -w / 2, w / 2] ]) corners R corners corners[0, :] x corners[1, :] y corners[2, :] z return corners.T # 8x3相机坐标然后变换到雷达坐标系def corners_cam_to_velo(corners_cam, T_cam_to_velo): pts np.hstack([corners_cam, np.ones((len(corners_cam), 1))]) pts_velo pts T_cam_to_velo.T return pts_velo[:, :3]为什么不用“只把中心点变换过去再手动转换yaw角”的简化方案因为相机y轴方向朝下雷达z轴方向朝上两者的旋转轴并不一致。中心坐标用刚体变换很简单但角度变换很容易在弧度转换、正负号、以及“哪个轴对齐哪个轴”上出错。角点法把整个框的几何结构都变换过去再从角点里反推朝向即使你中间的符号写错了可视化阶段也会很快暴露问题。4.4 输出目标编码中心、尺寸与复数角度对每个目标我们需要从变换后的雷达坐标系框里提取出BEV视角下的关键信息中心的x和y、框的宽度和长度、绕垂直轴的偏航角。在BEV图上目标是一个类矩形区域。中心坐标可以直接用雷达坐标变换后的中心点然后投影到栅格。宽度w在雷达坐标系下对应y方向跨度长度l对应x方向跨度。这一点和相机坐标系里的“width、length”语义不同必须重新换算。计算雷达BEV朝向角的方法可以通过框的左右前角点位置来求def compute_yaw_from_corners(corners_velo): # corners_velo是8x3 center corners_velo.mean(axis0) front_x center[0] corners_velo[:, 0].max() - center[0] # 用朝向的中点来算更稳的是取前侧两个角点的连线的中点 front_center (corners_velo[0] corners_velo[1]) / 2.0 if corners_velo.shape[0] 8 else None yaw np.arctan2(front_center[1] - center[1], front_center[0] - center[0]) return yaw但这个函数容易混淆角点顺序。更稳妥的通用做法是在雷达坐标下生成一个朝向向量目标朝向就是长轴方向。把这样一个向量定义为从中心指向某个角点的方向然后取arctan2。Complex-YOLO最特别的设计是角度不用单一弧度值而是用复数的实部和虚部来编码也就是cos(θ)和sin(θ)。原因是角度回归如果用弧度值在-π和π的边界会出现很大的数值跳变明明两个朝向几乎一样loss却突然翻倍。用复数编码后角度被映射到单位圆上的两个连续值回归起来平滑得多。所以最终落到训练target里的每个目标大致长这样def build_target_entry(center_velo, dims_bev, yaw_velo, class_id): x_cell (center_velo[0] - X_MIN) / DISCRETIZATION y_cell (center_velo[1] - Y_MIN) / DISCRETIZATION w_cell dims_bev[0] / DISCRETIZATION l_cell dims_bev[1] / DISCRETIZATION return { x: x_cell, y: y_cell, w: w_cell, l: l_cell, sin: np.sin(yaw_velo), cos: np.cos(yaw_velo), class_id: class_id }真正训练时这些值还要进一步归一化到输出特征图的网格上比如把整张BEV图下采样到特征图尺寸再让网络预测相对当前grid cell的偏移。这部分在训练篇二展开但预处理这一步已经把所有关键信息都提取好了。5. 不急着开训可视化验证与训练集/验证集划分5.1 验证集划分不是随机了一下就算完整个object detection training数据集一共7481帧不能全拿去训练。你需要留出一部分做验证否则训练loss下降曲线没有任何可信度。KITTI官方提供了多种划分方式。最省事的做法是直接用官方devkit里的train.txt和val.txt。如果你没有下载devkit也可以用一个被很多复现项目采用的划分前3712帧做训练后3769帧做验证。注意这两个数字加起来正好是7481。更严谨一点由于KITTI里Car类样本远多于Pedestrian和Cyclist如果按顺序硬切可能某些小类只在一边出现。建议统计每一帧里存在的类别集合做带类别约束的随机划分保证Car、Pedestrian、Cyclist三类在训练集和验证集里都有足够数量。5.2 用BEV视图叠加GT框做第一道检查预处理脚本写完后最该做的第一件事不是立刻训练而是可视化。打开一张BEV图把转换后的GT框画上去人眼一秒钟就能看出坐标有没有错位。from PIL import Image, ImageDraw import numpy as np def visualize_bev_with_boxes(bev, boxes): # bev: (800, 704, 3)值在0~1 img (bev * 255).astype(np.uint8) img Image.fromarray(img) draw ImageDraw.Draw(img) for box in boxes: x_cell, y_cell, w_cell, l_cell, yaw, cls box # 简化绘制先画一个框中心点再画一个粗略矩形 cx x_cell cy y_cell draw.ellipse([cx - 2, cy - 2, cx 2, cy 2], fill(255, 0, 0)) # 实际建议画旋转矩形 return img如果你的GT框中心在BEV图上落在点云轮廓中间方向也和车头一致那很大概率是正确的。如果框整体偏移、旋转方向反了、或者中心落在完全没有点云的空白区域马上回去查矩阵组合和角点计算。5.3 三个容易静默出错的点第一栅格方向反了。BEV图里row对应y方向、col对应x方向如果标签中心映射时也写成row对应x那么框会绕着原点转90度。这种错误从数字上很难发现但可视化时一眼就能看到所有GT框和点云轮廓成垂直关系。第二yaw符号错了。在相机坐标里rotation_y是绕朝下的y轴旋转转换到雷达坐标后正负号可能被颠倒了。检查方法把3D框的8个角点从雷达坐标再投回图像像素坐标和label_2里给的2D bbox对比。如果3D框投影出的轮廓和2D bbox贴合说明旋转矩阵方向没问题。def project_3d_box_to_image(corners_velo, calib): R_rect np.eye(4) R_rect[:3, :3] calib[R0_rect] Tr_velo_cam np.eye(4) Tr_velo_cam[:3, :] calib[Tr_velo_to_cam] pts np.hstack([corners_velo, np.ones((len(corners_velo), 1))]) pts_cam Tr_velo_cam pts.T pts_img calib[P2] R_rect pts_cam pts_img pts_img[:2] / pts_img[2] return pts_img.T第三GT中心落在裁剪范围外被静默丢弃。某些目标虽然中心在x 0到70.4米范围内但它的局部点云可能超出范围或者某些目标中心因为遮挡等原因在裁剪边界附近被过滤掉了。写一个统计函数输出“每帧有多少个GT被丢弃”如果丢弃率超过几个百分点要回到范围定义去检查而不是直接忽略。6. 预处理代码提速与复现性不只是能跑还要能快跑6.1 用np.maximum.at和np.add.at代替逐点循环第一版做BEV编码时如果直接用纯Python循环遍历10万个点再写进800乘704的矩阵一帧大概要3到5秒。7481帧全量预处理就是好几个小时的无意义等待。向量化版本用np.maximum.at和np.add.at一帧可以压到几十毫秒。虽然np.maximum.at在部分numpy版本里不算特别快但已经比纯循环快一个量级。如果还想再快可以用np.histogram2d算densityheight和intensity用np.maximum.at整体瓶颈已经不在CPU上。6.2 多进程并行处理7481帧数据预处理天然适合并行。每一帧的bin、label、calib相互独立不存在顺序依赖。直接用multiprocessing.Pool就能把整个数据集压到几分钟内处理完。from multiprocessing import Pool def process_frame(frame_id): bin_path fKITTI/training/velodyne/{frame_id}.bin calib_path fKITTI/training/calib/{frame_id}.txt label_path fKITTI/training/label_2/{frame_id}.txt # 返回bev图或目标列表 return bev, targets if __name__ __main__: frame_ids [f{i:06d} for i in range(7481)] with Pool(8) as p: results p.map(process_frame, frame_ids)注意一点如果每个子进程都要读取calib文件或构建同一个矩阵建议把它们写成一个全局只读对象或者直接在子进程内读取避免用大对象跨进程传递否则序列化开销会抵消并行收益。6.3 把中间产物保存为npy还是逐帧缓存我建议把每一帧预处理后的BEV图保存为npy文件同时把标签打包成单独的pkl或npy。这样训练脚本读取时不需要重新处理原始bin能显著减少IO压力也方便在训练时做DataLoader打乱。磁盘占用可以粗略估算一张BEV图是800乘704乘3每个float32占4字节大约6.7MB。整套7481帧下来差不多50GB。如果你觉得占用太大可以把三通道转成float16或者只在内存里做缓存、不落盘。真正训练时我一般保留原始bin文件和预处理后的npy两种形式。npy负责快速读取和验证预处理管线原始bin文件留着以防后续需要调整预处理参数重新生成。最后再分享一个我每次做完预处理都会执行的检查随机抽50帧把所有标注的3D框投影到左相机图像上再与label_2里的2D bbox对比。如果投影结果和2D框重叠率低于80%几乎可以断定是calib矩阵组合顺序错了或者维数不对。如果图像上没问题但BEV上框和点云轮廓对不上那就要回到x/y方向的栅格映射去排查。这个小检查只花几分钟但能挡下后面可能白跑几天的训练。数据预处理这一步慢就是快。
返回列表