RangeDet复现:range view激光雷达3D检测选型与落地
激光雷达3D目标检测这条线上,视角的选择几乎决定了一个方案的骨架。体素化走BEV,投影走range view,两条路各有拥趸,也各有各的坑。RangeDet这篇工作做的事很有意思——它站出来给range view说话,试图证明这个被不少人提前判了出局的表示方式,其实远没到退场的时候。我把这篇东西反复读了几遍,又照着自己的理解搭了一版复现流程,踩了一些坑,也有一些想明白了的地方。这篇文章就围绕RangeDet的核心思路展开:它到底在辩护什么、辩护得有没有道理、如果自己要落地一套类似的range view检测器该怎么做、哪些地方最容易翻车。适合已经做过一两个3D检测项目、想搞清楚视角选型底层逻辑的人看,也适合刚入门、被各种BEV方案刷屏之后想补一补另一条技术路线的同学。
1. 路线之争:为什么还要给range view翻案
1.1 三种点云表示法的成本账
聊range view之前,得先把三条主流路线的账算清楚,不然很难理解RangeDet的动机。点云本身是一堆无序的(x, y, z, intensity),网络吃不下,必须重排成某种规则结构。目前工程上真正跑起来的主要是三种:体素化到BEV、球面投影到range view、以及直接在原始点上做操作。
体素路线的优势太明显了:BEV是俯视图,物体的物理尺寸在图上不随距离变化,一个车长4.5米就是4.5米,卷积核的感受野对应固定的物理范围,检测头设计起来非常直觉。但代价也摆在那——体素化本身就是一次量化,把连续坐标塞进离散格子,边界上的点会被合并或丢弃;空体素占比极高,尤其在大范围场景里,稀疏卷积虽然能绕开无效计算,但索引构建和显存管理并不便宜。
原始点路线保留了全部信息,可邻居查询这一步在几万个点上做起来很费劲,工业部署时对延迟敏感的场景往往不太敢用。
range view走的是另一条路:把点云按球面角度展开成一张二维距离图。每个像素记录的是这个方向上最近的那个点的距离,顺带存x、y、z和反射强度。这张图可以直接喂给标准的2D卷积网络,不需要任何3D算子,不需要稀疏索引,卷积核扫过去就是规整的张量运算。这就是RangeDet敢于为它辩护的底气——它的计算结构最简单,硬件最友好。
| 表示方式 | 量化损失 | 计算结构 | 尺度一致性 | 主要痛点 |
|---|---|---|---|---|
| BEV体素 | 有,格子边界合并 | 稀疏3D卷积/柱卷积 | 好,物理尺度固定 | 显存与索引开销 |
| range view | 无,保留原始测量 | 标准2D卷积 | 差,随距离变化 | 尺度变化与形状变形 |
| 原始点 | 无 | 点算子/邻域查询 | 好 | 邻居查询慢,难部署 |
这张表里最关键的一列是"量化损失"。range view在这一项上是干净的:每个像素对应一条射线上的最近点,距离值就是传感器测出来的原始值,没有任何插值或合并。对于测距精度本身就是核心指标的检测任务,这一点在某些场景下是有实际价值的。
1.2 被诟病的两个"原罪"到底有多严重
range view被批评得最多的两点,一是尺度变化,二是形状变形。RangeDet的辩护逻辑,本质上是把这两条拆开来看,指出它们并不是表示方式本身的死穴,而是过去的方法没有针对性地处理。
先说尺度变化。这一点经常被误读。在range view里,一个物理尺寸固定的物体,距离越远,它在图像上占的像素越少,这是几何投影的必然结果,不是bug。有人会说BEV没有这个问题,但BEV的"没有"是靠把点云摊平到地面换来的,代价是高度信息被压缩、远处点云稀疏导致特征空洞。range view的尺度变化是真实几何的忠实反映,问题在于卷积网络的多尺度机制原本是为语义多尺度设计的,它并不知道"这个像素的尺度小是因为它远",缺乏距离这个先验,融合时自然会失衡。
再说形状变形。一个长方体的车,投影到球面上会变成什么形状?取决于它相对传感器的位置和朝向。正对着传感器的车,投影后大致还是个矩形;斜着的车,投影后会被拉成梯形甚至更奇怪的四边形;贴近地面的部分还会被扫描线的角度间隔拉扯。这个变形是真实存在的,任何基于range view的方法都躲不掉。但RangeDet的观察是:变形影响的是"框怎么定义"和"框怎么合并",而不是"特征能不能提取"。只要把回归目标和后处理按照变形的规律重新设计,这个问题是可以被大幅缓解的。
这里有个容易被忽略的点:很多人在range view上直接套用BEV那一套IoU定义和NMS逻辑,跑出来效果差,就归咎于表示方式。实际上问题出在后处理上,换什么视角都救不了。
1.3 RangeDet的整体骨架长什么样
把整条流水线摊开看,结构其实很朴素:原始点云先做球面投影,得到一张H×W的多通道range image;然后过一个标准的2D backbone提特征,接一个FPN做多尺度融合;融合之后插入一个专门处理距离条件的模块,也就是RangeDet的核心创新点RCPM;最后接一个anchor-free的检测头,输出类别、中心偏移、三维尺寸和朝向;推理阶段用带距离加权的NMS做后处理。
这个骨架的说服力在于它的每一块都是成熟组件,没有引入任何需要特殊算子支持的模块。backbone可以是任何2D网络,FPN是现成的,检测头是FCOS那一套改的,NMS是自己写的几十行代码。整套东西可以在一张普通显卡上跑起来,不依赖稀疏卷积库,不依赖自定义CUDA算子。对于一个想快速搭原型或者想在边缘设备上部署的团队,这个特性比多一个点的mAP更有吸引力。
我自己的判断是,RangeDet真正的贡献不在于刷了多高的分数,而在于它把"range view为什么不行"这个模糊的论断,拆解成了几个具体可解的子问题,并且给出了其中两个的解法。这种把定性批评转化为定量工程问题的思路,比结论本身更值得学。
2. 核心机制拆解:距离条件化与框的重新定义
2.1 球面投影:从点云到range image的每一步
这一步是所有range view方法的地基,做错一个细节后面全崩。给定一个点(x, y, z),先算它的斜距r,再算俯仰角和方位角。写成代码是这样:
import numpy as np def points_to_range_image(points, H=64, W=2048, fov_up=3.0, fov_down=-25.0): # points: (N, 4) -> x, y, z, intensity x, y, z, intensity = points.T r = np.sqrt(x**2 + y**2 + z**2) # 俯仰角,单位度 pitch = np.degrees(np.arcsin(z / np.maximum(r, 1e-6))) # 方位角,映射到 [0, 360) yaw = np.degrees(np.arctan2(y, x)) yaw = np.mod(yaw, 360.0) # 行:俯仰角从上往下 row = (fov_up - pitch) / (fov_up - fov_down) * H # 列:方位角从左往右 col = yaw / 360.0 * W row = np.floor(row).astype(np.int32) col = np.floor(col).astype(np.int32) valid = (row >= 0) & (row < H) & (col >= 0) & (col < W) row, col = row[valid], col[valid] # 每个像素只保留最近的点 order = np.argsort(-r[valid]) # 从远到近,近的覆盖远的 row, col = row[order], col[order] range_img = np.zeros((H, W, 5), dtype=np.float32) range_img[row, col, 0] = r[valid][order] range_img[row, col, 1] = x[valid][order] range_img[row, col, 2] = y[valid][order] range_img[row, col, 3] = z[valid][order] range_img[row, col, 4] = intensity[valid][order] return range_img几个参数的选择是有讲究的。行数H一般取传感器线数或者线数的两倍,64线和128线是常见值。列数W决定了方位角分辨率,W取2048时,每列大约0.176度;取2650时约0.136度。列数越大,远处物体保留的像素越多,但计算量线性增长,显存也跟着涨。
俯仰角的范围必须覆盖传感器的实际视场,写小了上方或下方的点会被裁掉,写大了会多出一堆空行浪费算力。这一点在换数据集时特别容易出错——不同数据集的传感器安装角度和线束分布差别很大,直接套用别人的fov_up/fov_down,结果就是一半的行是空的。
order = np.argsort(-r)这行是关键:同一像素可能落入多个点(比如远处相邻两条扫描线在某列上重叠),必须只保留最近的那个,因为激光雷达测的是这条射线上的首个回波。如果反过来保留最远的,会把背景当成前景,训练直接崩掉。
空的像素用0填充,意味着range=0。网络必须能区分"这里是0米"和"这里没有点",所以在损失计算和特征归一化时都要用掩码把无效像素排除掉。RangeDet的处理方式是把range作为特征通道输入,同时在计算损失时只对有效像素负责。
2.2 尺度变化到底有多剧烈:算一笔具体的账
光说"尺度变化"太抽象,我们拿具体数字感受一下。假设一辆车宽度1.8米,方位角分辨率是0.176度(W=2048)。
当车在10米处时,它的方位角跨度约为 1.8/10 = 0.18弧度,换算成度是10.3度,在图上占 10.3/0.176 ≈ 58 列。
当车在50米处时,方位角跨度约为 1.8/50 = 0.036弧度,约2.06度,占 2.06/0.176 ≈ 12 列。
线性尺寸差了将近5倍,面积差了25倍。这个差距比常规图像检测里的多尺度问题还要夸张。更麻烦的是俯仰方向:一辆高1.5米的车在50米处的俯仰跨度只有约1.7度,如果线束角度间隔是0.4度,那么它在垂直方向只占4行左右,稍微一池化就没了。
这就是为什么range view检测器在远距离小目标上总是吃亏。常规FPN的处理方式是把深层低分辨率特征和浅层高分辨率特征相加,让大感受野负责大物体、小感受野负责小物体。但在range view里,"大物体"和"小物体"的划分不是由语义决定的,而是由距离决定的。一辆远处的卡车可能在图上比一辆近处的自行车还小。FPN那套按层级分配尺度的假设,在这里失效了。
2.3 RCPM:把距离当作条件信号喂进去
RangeDet对这个问题的解法很直接:既然网络不知道尺度小的原因是距离远,那就把距离明确告诉它。这就是Range Conditioned Pyramid Module的核心思想。
具体做法是,在FPN的每一层融合之后,额外引入一份对应分辨率的range图,经过几层轻量卷积和一个sigmoid,生成一张0到1之间的权重图,然后和该层的特征做逐元素相乘。用伪代码表示:
class RangeConditionedBlock(nn.Module): def __init__(self, channels): super().__init__() self.gate = nn.Sequential( nn.Conv2d(1, channels // 4, 1), nn.ReLU(inplace=True), nn.Conv2d(channels // 4, channels, 1), nn.Sigmoid() ) def forward(self, feat, range_map): # range_map: (B, 1, H, W),已归一化到大致 0~1 w = self.gate(range_map) return feat * w这段结构看起来像SE注意力,但控制信号完全不同。SE是用全局池化后的特征自己生成通道权重,RCPM是用空间上每个像素的距离值生成逐像素的权重。远处像素的range值大,gate学到的响应就弱或者强,具体是哪种由训练决定,网络自己会找到合适的调制方式。
为什么用range图而不是直接把range拼接到特征上?我理解有两个考虑。拼接会让网络自己去学怎么用这个信息,但卷积核是空间共享的,在range view里同一个卷积核要同时处理近处和远处的像素,学起来很吃力。用gate的方式相当于给了一个乘性的调制,让同一套卷积核在不同距离上表现出不同的响应强度,表达能力更直接。
range图下采样到各层分辨率时,用平均池化比最大池化合理。因为range是连续物理量,平均池化保留了这一块区域的平均距离,最大池化会偏向最远的那个点,在物体边缘处容易产生跳变。这个细节看起来小,但在远距离密集目标场景里能感觉到差别。
还有一个容易做错的地方:range值要不要归一化。原始range可能从0到80米甚至更远,直接喂给卷积,数值范围太大,sigmoid容易饱和。我试过两种方式,一种是除以最大量程,一种是用对数变换。对数变换在近距离区分度更好,但在远距离会压缩得太厉害。实际用的时候,按数据集的量程做线性归一化,再把大于某个阈值的截断,效果比较稳。
复现时的经验:RCPM的gate分支不要做太深,两层1x1卷积就够了。做深了反而会过拟合,而且这部分参数在小数据集上很难训起来。
2.4 检测头:框在range view里该怎么定义
这是整个方法里最容易被低估的部分。range view里的"框"和图像检测里的框不是一回事。
一个三维框有中心(x, y, z)、尺寸(w, l, h)和朝向θ。把它投影到range image上,八个角点会落在八个位置,连起来是一个不规则的四边形。RangeDet的做法是把这八个角点都投影到图上,取它们的最小外接矩形作为二维框,用来做正样本分配。
为什么要取外接矩形而不是直接用中心点投影?因为在range view里,物体的投影可能被拉长,中心点的投影不一定在二维框的几何中心。如果按中心点附近的像素分配正样本,在物体斜置的时候会偏出真实区域。用外接矩形能保证覆盖到物体在图上占据的全部像素。
检测头是anchor-free的,每个正样本像素输出:类别分数、中心点相对该像素的偏移量、三维尺寸、朝向。朝向用sin和cos两个值编码,避免角度在±π处的不连续。尺寸回归这里有个细节,range view下的尺寸回归难度比BEV高,因为同样的物理尺寸在图上对应的像素数随距离变化,网络需要同时学到"这个物体多大"和"它离多远"。
我看过一些复现实现,在这里加了一个辅助分支,让网络额外预测每个正样本的range值,作为一种自监督信号。这个做法不是原文的必需项,但对收敛速度有帮助,因为range是已知的,可以直接作为监督目标。
正样本分配用中心度加权,离物体中心投影越近的像素权重越高,边缘像素权重衰减。这个设计在range view下比在普通图像检测里更有必要,因为物体边缘的投影变形最严重,那里的特征可靠性低。
2.5 Weighted NMS:为什么标准NMS在这里会失灵
后处理这一块是RangeDet另一个值得说的点。
标准NMS的逻辑是按分类分数排序,然后依次抑制与已保留框IoU超过阈值的框。这套逻辑成立的前提是"分数高的框质量更好"。在range view里,这个前提经常不成立。
原因在于遮挡和投影截断。一个物体如果被前面的物体挡住一部分,它在range image上会呈现为两块不连续的区域,检测器很可能对每一块都输出一个框。被挡住的那块面积小、特征弱,分类分数低;露出来的那块分数高但框不完整。标准NMS会按分数保留那个不完整的框,把另一个压掉,最终结果就是一个偏小的框。
RangeDet的思路是引入一个额外的预测分支,让网络在输出框的同时预测这个框的质量(可以理解为预测IoU),然后在NMS阶段用分类分数乘以质量分数作为排序依据。这样那些分类分数不高但框得很准的检测结果有机会被保留下来。
具体实现上,加权的方式有两种常见选择:一种是把质量分乘到分数上再排序,另一种是在框合并时按质量分做加权平均。前者简单,后者在密集场景下更稳。我两种都试过,在小目标密集的场景里,加权平均能减少框的抖动。
阈值的选择也和BEV不一样。BEV里常用的NMS阈值是0.5到0.7,range view里因为投影变形,同一个物体的两个框在二维图上的IoU可能天然就偏低,阈值设太高会保留一堆重复框。实际调的时候,我一般从0.4开始试,配合质量加权。
3. 复现路径:从数据到推理的完整链路
3.1 数据预处理流水线
复现的第一步是把数据集转成range image格式。这里以常见的激光雷达检测数据集为例,流程大致是:读取点云、过滤掉超出量程的点、做球面投影、保存为numpy或者直接在线生成。
几个工程上的选择值得说。第一是离线生成还是在线生成。离线生成把每帧的range image存成文件,训练时直接读,IO压力小但占磁盘;在线生成省空间,但每轮都要重算投影,CPU容易成为瓶颈。我倾向于离线生成一次,用多进程并行,几千帧的数据几分钟就能跑完。
第二是坐标系。有些数据集的点云已经转到车体坐标系,有些是传感器坐标系。球面投影必须在传感器坐标系下做,因为投影的原点是传感器位置。如果拿车体坐标系的点直接投,整个图会歪掉。这一步错了,后面所有指标都是废的,而且很难从loss曲线上看出来,只会表现为mAP莫名其妙地低。
第三是反射强度的归一化。不同传感器的intensity范围差别很大,有的0到255,有的0到1。统一归一化到0到1,避免网络对这个通道的敏感度过高。
| 预处理项 | 常见取值 | 踩坑提示 |
|---|---|---|
| 俯仰视场 | 需匹配传感器实际安装 | 直接套用别人参数会导致大量空行 |
| 方位分辨率 | 每列0.14~0.18度 | 列数过小则远处目标像素不足 |
| 最大量程 | 按数据集标注范围裁 | 裁太狠会丢远处标注框 |
| 无效像素 | 置0并记录掩码 | 不mask会在归一化时引入偏置 |
| 强度归一化 | 线性缩放到0~1 | 忘记做会让训练不稳定 |
3.2 网络结构与训练配置
backbone我用了两种做对比。一种是ResNet的变体,把第一层的步长改小,避免一开始就把方位方向的分辨率砍掉太多。这一点很关键:range image在垂直方向只有64行或者128行,经不起几次下采样;水平方向虽然有2000多列,但远处目标本来就只占十几列,下采样两次就剩几个像素了。常见的做法是垂直和水平方向用不同的下采样策略,或者干脆在浅层保持较高分辨率。
FPN部分输出三个或者四个尺度的特征,每一层接一个RCPM。检测头共享权重还是各尺度独立,我倾向于共享,参数量小,小数据集上不容易过拟合。
训练配置上,优化器用AdamW,初始学习率在千分之一量级,配合余弦退火和几百步的warmup。batch size受显存限制,单卡能放4到8帧,用梯度累积凑到等效16或者32。损失函数是分类用focal loss,回归用smooth L1,质量预测分支用BCE。权重上,分类损失占1,回归损失占2左右,质量损失占0.5到1之间,具体要看收敛情况调。
有个细节值得单独提:range image里无效像素占比可能很高,尤其是有遮挡的场景。如果focal loss把无效像素也当负样本算,正负样本比例会极端失衡,训练会偏向于全部预测背景。解决办法是在loss里把无效像素的贡献置零,只对有效像素计算。
3.3 数据增强的几何一致性
range view的数据增强比BEV更容易出错,因为任何变换都必须保持几何一致性。
水平翻转是常用增强,但在range view里它等价于绕垂直轴做镜像,对应的变换是x变成-x,y不变,方位角θ变成π减θ。如果只翻转图像而不改标注框的朝向,训练出来的朝向预测会系统性地偏180度。这个错误很隐蔽,因为框的位置看起来是对的,只有朝向指标掉。
全局旋转也是同理,旋转角度α之后,方位角要整体平移,物体的朝向也要加α。旋转的填充区域会变成无效像素,要用掩码标记出来。
缩放要慎用。range view里的缩放不等价于物理世界的缩放,你把图放大两倍,并不对应物体变大两倍,只对应方位分辨率变了。真要做尺度增强,应该是改变传感器的模拟参数重投影,而不是直接resize图像。我见过有人直接对range image做随机缩放,结果模型在验证集上完全失效,原因就在这里。
3.4 推理与后处理
推理阶段,前向得到每个像素的预测,先按类别分数做阈值过滤,然后做带质量的NMS,再把保留下来的框从range view反投影回三维坐标。
反投影这一步要注意:二维框的中心点对应的是球面上的一个方向,加上预测的range值才能确定三维位置。如果检测头没有单独预测range,就用框中心像素位置的原始range值。两种方式差别不大,但在物体表面不连续的时候,用预测值更稳。
耗时方面,在单张主流显卡上,64×2048的输入,一次前向大概在几十毫秒量级,加上后处理,整体能到实时或者接近实时。这比同精度的体素方案要快,主要省在不需要构建稀疏索引和做3D卷积。
显存占用主要来自高分辨率特征图。64×2048在浅层展开后通道数是64的话,一张特征图就是八百多万个元素,乘以batch size很容易吃满。控制显存的手段包括:浅层通道数压低、用checkpoint换显存、把batch size降到2配合梯度累积。
4. 常见问题与排查实录
4.1 训练不收敛或者loss突然爆掉
这是复现range view检测器最常见的第一个坎。表现是前几百步loss正常下降,然后突然变成nan,或者loss一直卡在一个较高的值不动。
按发生概率排,原因大概是这几个。range归一化没做,原始距离值是几十米量级,进了网络之后激活值爆炸,反向传播时梯度溢出。解决很简单,在输入端除以最大量程。无效像素没有mask,归一化时这些0值被当成真实距离参与统计,产生的均值方差完全错误。这个问题的表现是loss能降但降不下去,验证指标一直很低。学习率太大,range view的特征分布和普通图像差别大,用图像分类那套学习率容易崩,先降到千分之一以下试。
还有一个比较隐蔽的原因:朝向回归用的角度编码如果没做归一化,sin和cos的预测值可能超出-1到1的范围,反解角度时会出现异常值,间接导致框的尺寸计算溢出。解决方式是在这个分支上加tanh,或者在loss里对预测值做裁剪。
4.2 远距离目标大面积漏检
如果训练正常但远距离的召回明显偏低,先别急着改网络,先确认标注有没有被裁掉。有些预处理流程按距离过滤点云,把远处点删了,但标注框还留着,这些框在range image上根本找不到对应的像素,自然检不出来。
确认标注没问题之后,看range image上远处目标还剩几个像素。如果只剩两三列,那网络确实无能为力,要么提高输入分辨率,要么接受这个精度上限。这两条路的取舍很现实:提高分辨率,显存和耗时会成倍增加。
网络层面可以做的,一个是把RCPM的gate分支在远距离的表现单独看一下,可视化权重图,看看是不是远处像素的权重被压得太低。如果确实如此,可以在loss里给远距离正样本更高的权重。另一个是检查FPN的最深层是不是下采样过头了,远处小目标在最深层已经消失,融合时等于没贡献。
4.3 重复框和朝向翻转
重复框的典型表现是同一个物体输出了两三个位置接近但尺寸不同的框。前面说过,这多半是投影截断导致的。先看NMS阈值,从0.4往下调试试。如果调到0.3还是重复,说明质量分支没学好,检查质量标签的构造是否正确——质量标签应该是预测框和真实框的IoU,如果在range view下算二维IoU而不是三维IoU,标签本身就是错的。
朝向翻转的表现是预测的框位置对、尺寸对,但车头方向反了180度。这个在对称物体上尤其难分辨。排查方法是把预测框和真实框都画出来看,如果大量呈现系统性反向,多半是角度编码的问题;如果只是零星几个,可能是训练不充分。角度用sin/cos编码基本能避免这个问题,如果还在用直接回归角度值的方式,趁早换掉。
4.4 问题速查表
| 现象 | 最可能原因 | 排查动作 |
|---|---|---|
| loss变nan | range未归一化或学习率过大 | 检查输入数值范围,降学习率 |
| loss能降但指标不涨 | 无效像素未mask | 打印有效像素占比,加掩码 |
| 远距离召回低 | 输入分辨率不足或标注被裁 | 统计远处目标像素数 |
| 重复框多 | NMS阈值高或质量分支失效 | 降阈值,检查质量标签 |
| 朝向系统性反向 | 增强时未同步更新角度 | 检查翻转和旋转的标注变换 |
| 验证指标远低于训练 | 反投影坐标系错误 | 核对传感器与车体坐标系 |
| 显存溢出 | 浅层通道数过大 | 压通道,降batch,用checkpoint |
除了表里这些,再补一条我自己的经验:复现这类方法时,先在一个小规模子集上把过拟合跑通。取几十帧数据,关掉所有增强,让模型在这几十帧上把loss降到接近零,指标刷到很高。这一步能验证整条链路的正确性,包括投影、标注对齐、损失计算、反投影。如果连过拟合都做不到,说明链路上有bug,这时候去调超参是浪费时间。等过拟合跑通了,再放开全量数据和增强,问题会少很多。
5. 适用边界:什么时候该选range view
5.1 选型的判断依据
RangeDet的结论不是"range view全面优于BEV",这一点要读清楚。它证明的是range view在经过针对性设计后,能达到和主流方法相当的精度,同时在计算效率上有优势。
那么工程上该怎么选?我的判断依据主要看三条。第一是延迟预算。如果部署平台算力紧张,需要极低的推理延迟,range view的2D卷积结构有明显优势,不需要稀疏算子,对硬件友好。第二是测距精度的敏感度。如果应用场景对距离误差很敏感,range view保留原始测量的特性有价值,避免了体素化的量化误差。第三是目标尺寸分布。如果场景里远距离小目标占比很高,range view的分辨率瓶颈会更早暴露,这时候BEV或者多视角融合可能更合适。
反过来说,如果场景里主要是中近距离目标、对精度要求极高、算力充足,那BEV仍然是更稳妥的选择,生态成熟、调优经验多、出问题的概率低。
5.2 在多传感器融合里的位置
range view还有一个我觉得被低估的价值:它和图像的对齐关系天然更好。相机的成像模型也是基于射线的,把激光点投影到图像上做像素级对齐时,range view的像素和图像的像素在角度上是一一对应的,融合时的几何变换简单。BEV是对地面的俯视展开,和相机视角差得远,融合时需要更复杂的变换。
如果要做激光雷达和相机的特征级融合,range view提供了一个很自然的中间表示。这也是RangeDet这类方法在融合方案里经常被当作激光分支的原因。
5.3 落地时的几个现实约束
最后说几个实际问题。range image的列数在不同数据集之间不统一,跨数据集迁移时预处理要重写,这一点比BEV麻烦。BEV的网格尺寸虽然也要调,但调整的粒度更粗,影响没那么大。
另一个是标注格式的转换。大多数数据集的标注是三维框,要转到range view下做训练,需要一套投影和反投影的工具链,这套工具链的正确性直接决定训练效果。建议在正式训练前,写一个可视化脚本,把投影后的框画在range image上,人工看几十帧,确认框和点云对得上。这半小时的检查能省掉后面几天的debug。
我在实际搭这套流程的时候,最大的体会是:range view方法的难点不在网络结构,而在数据链路的几何一致性。投影公式、坐标系、增强变换、反投影,这四个环节里任何一个出了偏差,网络都不会给你明确的报错,只会表现为指标上不去。所以复现这类工作时,把验证几何正确性的工具先写好,比急着调模型参数重要得多。模型结构可以参考论文,但这条数据链路的坑,只能自己一个个踩过去。