DenseFusion实战:RGB-D位姿估计的逐点融合与ADD评估指标全解析
2026/9/15 21:48:55 网站建设 项目流程

在3D视觉领域,6D位姿估计一直是个绕不开的话题。抓取、AR叠加、机器人操作,凡是需要“知道物体在空间里具体什么位置、什么朝向”的场景,都得靠它。DenseFusion是CVPR 2019年提出来的方法,用RGB-D做单物体位姿估计,在LineMOD和YCB-Video两个主流基准上都排在最前面。最近不少朋友在后台问我这个模型的源码怎么读、推理流程怎么走、以及论文里的ADD和ADD-S指标到底怎么实现。我打算用这个系列完整拆一遍:代码怎么跑通、网络每个模块在做什么、中间特征怎么可视化、最后怎么算评估指标。这篇是第一篇,先把整体路径和核心原理讲清楚,你听完之后哪怕没看过源码,也能在心里搭出整条推理链路。

适合读这篇的人我分三类:一是刚入3D视觉、想找一个经典模型作为入门项目来刷代码的;二是已经在跑DenseFusion但被老版本PyTorch、数据集和评估代码折腾得想放弃的;三是只想弄懂ADD、ADD-S这些指标,回去好写论文对比实验的。无论你属于哪一类,这篇的内容都够你消化一阵子。

1. 先弄清DenseFusion在解决什么问题:RGB-D位姿估计的痛点

1.1 为什么单独用RGB或者单独用点云都不够

6D位姿估计要输出的是物体相对于相机坐标系的3个旋转自由度和3个平移自由度,也就是通常说的R和t。很多刚接触的人会想:这不就是目标检测加一个方向预测吗?实际完全不是一回事。物体旋转一个微小角度,投影到图像上可能只差几个像素,但对抓取来说,末端执行器过去就是几厘米甚至几毫米的偏差,直接就抓空了。

单张RGB图像的难点在于没有尺度信息。同一把钥匙,近大远小,你只知道它在图像里的边框,不知道它在空间里离相机多远,朝向到底是怎么偏的。就算估计出了位姿,遇到弱纹理物体、反光表面,特征提取很容易崩。单独用点云也不是不行,但深度相机获取的点云通常比较稀疏,尤其对细小物体表面、透明物体和深色物体,点数少得可怜,几何特征不够稳定。再加上点云没有颜色信息,你很难把两片形状相似但颜色不同的物体区分开。

所以学术界很早就意识到,把RGB和深度结合起来是更自然的路。问题是怎么结合。早期方法比如PointFusion,是把整张图的全局特征和整个点云的全局特征拼在一起,再丢给全连接层去回归位姿。这条路的问题在于,全局特征把不同空间位置的细节都混在一起了,模型很难从“平均信息”里学出精确的几何对应关系。打个比方:一群人在讨论一个问题,每个人都有自己的观察角度,但最后只把所有人的结论汇总成一句话,细节全丢了。

1.2 DenseFusion的核心思路:像素级和点级的密集融合

DenseFusion的关键改变,是把“所有人讨论出统一结论”改成了“每个人单独做判断,最终投票”。实际操作上,它对RGB图像提取到像素级颜色特征,对点云提取到点级几何特征,然后按照像素与3D点的一一对应关系,把每一对特征单独融合,再让每个融合后的点独立预测位姿,最后用预测置信度加权投票。

这种逐点融合的好处很明显。物体表面某个位置颜色特征明显,另一个位置几何边缘突出,每个点都能给位姿估计贡献自己最擅长的线索。比如一个纯色圆柱体,颜色信息几乎没用,但几何边缘能告诉模型对称轴在哪里;而一个花纹丰富的平面物体,颜色纹理又能提供很强的角度约束。两种线索在逐点尺度上互补,而不是在全局尺度上互相稀释。

从工程实现角度看,这个设计也特别好落地。点云天然是集合结构,逐点操作符合PointNet那一套;颜色特征又是像素对齐的,只要把RGB特征和点云特征在channel维度上concat,后端接MLP就行,不需要复杂的跨模态注意力机制。等到后面的代码拆解你会看到,核心融合层的代码量很少,但效果就是比全局融合好一大截,原因就在于保留了几何细节的独立性。

2. 跑通代码之前:环境配置、数据集准备和目录结构

2.1 环境版本踩坑记录

官方仓库的代码是很早之前写的,默认环境是Python 3.6 + PyTorch 0.4.1 + CUDA 9.0。你现在直接用新环境跑,基本没戏,光是一个torchvision的接口变动就能让你怀疑人生。我实际跑下来的建议是:不要硬刚最新版,用Conda单独建一个Python 3.6的环境,PyTorch装1.7到1.10之间的版本,再手动改几处API差异。

第一处必改的是torch.Tensor相关调用。老代码里常见的x.data在新版本还能用但会有警告,问题不大;真正要小心的是torch.unsqueezetorch.cat这些函数的维度行为,PyTorch在0.4到1.0之间改过一次,很多地方不加dim参数就会报错。第二处是torch.utils.data.DataLoadernum_workers,在Windows上如果设成大于0,训练时经常卡死,建议Linux环境跑,或者设成0。

编译扩展模块是另一个容易卡住的点。官方代码需要一个自定义的C++/CUDA扩展,如果你不需要改底层算子,大部分情况可以直接用纯Python版本替代。如果非要用编译版,记得在仓库根目录执行:

python setup.py build_ext --inplace

这个过程对CUDA版本很敏感。我机器上CUDA 11.x可以编译通过,但CUDA 12.x在某些卡上会报gcc版本不兼容。建议用CUDA 11.3搭配gcc 9,成功率最高。

2.2 LineMOD与YCB-Video数据集的下载和组织

DenseFusion官方在LineMOD和YCB-Video两个数据集上做实验。LineMOD包含15个低纹理物体,每个物体大概有一千多张带标注的RGB-D图像,背景是高度结构化的桌面或杂乱场景。这套数据集的难点在物体本身,很多是圆柱、方块、饮料罐这类形状简单但对称性强的目标。

下载完官方预处理数据后,目录结构一般是这样的:

Linemod_preprocessed/ ├── ape/ │ ├── depth/ # 深度图 │ ├── mask/ # 实例掩码 │ ├── rgb/ # 彩色图 │ └── pose.txt # 位姿标注 ├── benchvise/ ├── cam/ └── ...

每个物体的pose.txt里存的是4x4齐次变换矩阵,旋转在前三行,平移在最后一行的前三列。读取的时候注意坐标系单位,LineMOD里平移向量单位是米,有的版本是毫米,不统一的后果就是位姿误差直接偏大几十倍。

YCB-Video数据集就大得多了,92个视频序列,13万多帧,还包含21个物体的模型文件。官方提供的评估脚本是用MATLAB写的,之前很多人在这上面栽跟头。好在现在项目的tools/evaluate.py里已经有了Python实现,但你需要先把数据集转成代码里能识别的格式,主要是把模型的points.xyzposes对齐到同一个物体索引。

2.3 代码目录逐个记

拿到仓库源码后,先别急着点开训练脚本就往上冲,我的习惯是先把目录过一遍,搞清楚每个文件是干嘛的,后面排查问题会快很多。DenseFusion的代码量不大,核心文件集中在libtools两个目录里。

DenseFusion/ ├── lib/ │ ├── datasets/ # 数据加载逻辑 │ │ ├── ycb_dataset.py │ │ └── linemod_dataset.py │ ├── loss.py # ADD/ADD-S损失函数 │ ├── network.py # 网络结构核心文件 │ ├── pointnet.py # PointNet特征提取封装 │ └── ... ├── tools/ │ ├── train.py # 训练入口 │ ├── evaluate.py # 评估入口 │ └── test.py # 测试入口 ├── experiments/ # 实验配置和日志 └── setup.py # 扩展模块编译

network.py是整个项目的中枢,后面第3节重点讲它。loss.py里实现了两种损失,区分对称物体和非对称物体。data相关脚本处理图片和点云的加载,有一个很容易踩的坑是它们默认读的是.npy格式而不是.jpg,你要是直接换数据集,不转换格式的话,会在np.load那里报错。

3. 逐模块读代码:双流特征提取、密集融合与位姿输出

3.1 颜色特征提取:PSPNet在DenseFusion里怎么用

官方对RGB图像用的特征提取器是PSPNet,这是一个从语义分割任务里拿过来用的网络。它能输出保持空间分辨率的像素级特征,正好符合DenseFusion逐点融合的需求。在network.py里,这一部分封装在FeatureExtraction类中,实际加载的是去掉分类头之后的encoder部分。

输入图像通常被resize到480x640,经过归一化后进网络,输出shape是B x C x H x W。这里的C在官方配置里通常是128,对应每个像素最终的颜色特征向量。很多第一次读代码的人会疑惑:为什么不用更轻量的ResNet?因为PSPNet的感受野更大,对不同尺度的物体特征更鲁棒,代价是计算量大一些。如果你的显存不够,可以换ResNet-34的encoder,精度会掉一些,但能跑起来还是继续用官方配置更稳。

3.2 几何特征提取:从深度图到逐点特征

几何特征走的是PointNet的路线。首先要从深度图生成点云,这一步用的是相机内参矩阵做反投影。假设你有一个像素坐标(u, v),深度值是d,那么对应的3D点坐标是:

x = (u - cx) * d / fx y = (v - cy) * d / fy z = d

其中fx, fy, cx, cy是相机内参,LineMOD和YCB-Video的内参在数据集标注里都给了。模型并不会对整张图的全部点做计算,而是先根据mask把前景点选出来,再随机采样1024个点送入PointNet。这里有个重要的细节:点云在进入网络之前要做中心化处理。官方代码里用的是一个get_pose函数把点云变换到以物体中心为原点的局部坐标系。这样网络预测的是相对于物体中心的旋转和平移,而不是直接预测相机坐标下的绝对位姿,有利于模型在不同位置上泛化。

PointNet输出的是逐点几何特征,shape和颜色特征对齐。它没有做全局池化,保留的是每个点在局部区域内的几何描述,这也是DenseFusion能和逐点融合配合好的前提。如果像原始PointNet分类网络那样直接全局池化,几何细节就没了。

3.3 密集融合层:concat只是开始,关键是加权投票

密集融合层是DenseFusion最核心的部分,但代码却短得让不少人吃惊。基本逻辑就是把颜色特征和几何特征在channel维度上拼接,然后过一个MLP做特征交互:

# 伪代码,实际的channel数和层数以官方实现为准 fused = torch.cat([color_feature, geometry_feature], dim=1) # B x (128 + 128) x N fused = mlp(fused) # 投影到更高维或固定维度的融合特征

concat之后得到的是每个点自己的融合特征,接下来代码会对每个点预测一个位姿和一个置信度。所谓位姿,就是旋转和平移;置信度则是后续投票用的权重。网络输出的旋转通常是四元数形式,4个分量,平移是3个分量。因为四元数必须归一化代表合法旋转,代码里会做一次normalize

最后一步,把所有点的预测按置信度加权求和,得到最终的位姿输出。这个加权求和可以用softmax温度参数控制平滑程度,训练初期温度高一些,让梯度容易传播,后期温度低一些,让最优点的权重更突出。实际实验下来,这个温度参数对精度影响不大,但会影响训练的稳定性,建议先用默认值跑通,再慢慢调。

3.4 从融合特征到位姿:MLP头的结构细节

每个点的位姿预测头通常包含两个分支:一个分支输出旋转四元数,另一个输出平移向量。两条分支共享融合特征,但在最后几层各自独立。这么做的好处是,旋转和平移的优化尺度不一样,独立分支可以让各自梯度的量级互不干扰。

训练代码里还有一个非常隐蔽但重要的操作:对平移向量做了均值归一化。因为点云是以物体中心做中心化的,平移的预测范围比较小,居中处理能提高数值稳定性。旋转则始终用四元数加归一化,没有用欧拉角或者旋转矩阵直接回归,前者避免了万向锁问题,后者则避免了对非连续空间的直接回归。

4. 训练目标与迭代精化网络:ADD/ADD-S损失和逐步精修

4.1 ADD和ADD-S损失为什么这样设计

定义模型上的M个3D点集合,记作model_points。对于非对称物体,ADD损失计算的是这些点经过真实位姿变换后的位置,与经过预测位姿变换后的位置之间的平均欧式距离:

ADD = (1 / M) * sum || R_gt * p_i + t_gt - (R_pred * p_i + t_pred) ||

这个指标的本质是“预测的点云和真实的点云在3D空间里有多接近”。如果位姿完全正确,这个距离就是0;有一点偏差,距离就会变大。直接用这个距离作为损失训练,模型会倾向于把所有点都对齐,整体精度高。

对于对称物体就不能这么算了。比如一个圆柱体,绕中心轴旋转180度,外观完全不变,但ADD计算会把这部分旋转惩罚成为误差。所以对称物体用ADD-S,也就是对每个模型点,在真实点云中找最近邻点来计算距离:

ADD-S = (1 / M) * sum min_j || R_gt * p_i + t_gt - (R_pred * p_j + t_pred) ||

这个min操作让模型不用去区分对称等价位姿,只优化那些真正影响外观的偏差。代码里的做法是给每个物体定义一个symmetry标志,在loss.py里根据标志选择不同的损失函数。LineMOD里像eggboxglue这些物体就属于对称类别。

4.2 置信度加权的实现细节

DenseFusion不是简单地把所有点的loss平均,而是按置信度做了加权。具体来说,每个点预测出一个置信度w_i,用softmax归一化后,最终的损失是:

L = sum w_i * L_i

其中L_i是该点自身的ADD或ADD-S距离。这里的思路有点像注意力机制:网络在训练初期不知道哪些点靠谱,所有点一视同仁;随着训练推进,那些特征清晰、几何约束强的点置信度会越来越高,特征模糊或者处在遮挡边界的点置信度会降低,相当于模型自动过滤掉了干扰点。

这里有一个容易忽略的坑:置信度不能和loss一起无限增长,否则模型会走捷径,把所有点的置信度都推高,然后加权loss退化成平均loss。官方代码在置信度分支的输出上做过约束,通常是加一个小的正则项或者对置信度做归一化。你在复现时如果发现训练loss降不下去,可以去检查一下置信度是否出现了极端分布。

4.3 迭代精化网络:预测残差而不是直接预测位姿

DenseFusion还有一个对精度提升非常关键的模块:迭代精化网络。设计思路很直接:第一次网络输出的位姿可能粗糙,把点云按照这个位姿变换回去,得到一个新的对齐后的点云,再和原RGB特征融合,重新预测一次“残差位姿”。这个残差不是绝对位姿,而是对上一次预测的修正量。

代码中这个迭代一般做2次。第一次输出的位姿记为R_1, t_1,用它变换点云,输入精化网络,得到delta_R, delta_t,更新位姿:

R_2 = delta_R * R_1 t_2 = delta_R * t_1 + delta_t

训练时每次迭代的loss都会被计算并回传,也就是说精化网络会学着逐步修正自己的预测。推理时同样走2次精化,最终输出的是第二次精化后的位姿。实测下来,精化模块能让ADD指标提升5到10个百分点,代价只是多跑两次前向,非常划算。

理解这个模块可以用一个类比:第一次预测就像是快速看一眼大概方位,第二次精修则是根据“当前对齐效果好不好”来微调。因为第二次的输入包含了第一次的预测结果,网络能感知到“错在哪儿”,从而做出有针对性的修正,而不是盲目重新预测。

5. 可视化实践:中间特征、训练曲线和预测结果怎么画

5.1 在图像上绘制预测的3D包围盒

可视化最直观的需求,就是把预测位姿对应的3D包围盒投影到2D图像上看效果。这里要用到相机内参矩阵做投影。思路是先定义物体模型的3D包围盒8个角点,然后用预测的R和t把角点变换到相机坐标系,最后通过内参投影到图像平面:

import cv2 import numpy as np # corners_3d: (8, 3) 模型坐标系下的包围盒角点 # rvec, tvec: 预测位姿(旋转向量和平移向量) # camera_matrix: 相机内参 projected, _ = cv2.projectPoints(corners_3d, rvec, tvec, camera_matrix, None) projected = projected.reshape(-1, 2).astype(int)

然后用cv2.line把8个角点按包围盒的12条边连起来,画到RGB图像上。不同物体可以用不同颜色,比如主对角线用红色,其他边用绿色,这样可以直观看出包围盒方向是否正确。这个可视化代码独立于训练流程,完全可以用在你自己测试任意一张图片时。

5.2 特征图和置信度可视化:看网络到底关注哪里

DenseFusion的逐点置信度是非常适合可视化的中间产物。把每个点的置信度映射成颜色,叠加到原始点云或者图像上,你能直接看到网络对物体表面哪些区域更信赖。通常边缘角点、纹理丰富的区域置信度很高,遮挡边界和弱纹理区域置信度低。

具体做法是把置信度归一化到0到1之间,然后映射到一个colormap上,比如matplotlibjet。再把每个点的颜色和它原始的颜色混合,透明度设为0.5左右,渲染出来就是一张“注意力图”。我之前调试时发现,如果某个物体整体置信度都不高,大概率是mask不准,或者点云采样时采到了大量背景点。这个可视化对排查数据问题很有帮助。

5.3 训练曲线:不要只盯着loss,还要看ADD趋势

训练可视化最常见的做法就是画loss曲线,但我建议你同时记录验证集上的ADD/ADD-S指标变化。因为DenseFusion的训练loss和最终精度不是完全线性的关系,早期loss降得很快,后期曲线变平,但ADD还在继续下降,尤其迭代精化网络带来的提升往往发生在loss变化很小的时候。

matplotlib画图是最轻量的方案。在train.py里每个epoch记录一次训练loss和验证ADD,存成history.json,训练结束后加载绘制。如果你习惯用TensorBoard,也可以在代码里插入SummaryWriter,每个epoch写入scalar。两种方式我都试过,小规模实验用matplotlib足够,长时间训练用TensorBoard更方便。

6. 评估指标到底怎么算:ADD、ADD-S、5cm5°的实现细节

6.1 三个指标的定义和适用场景

论文和竞赛里最常看到的评估指标有三个:ADD、ADD-S和5cm5°。前两个在第4节已经介绍过,既是损失函数也是评估指标,只不过评估时用的是模型点集的平均误差。5cm5°则是另一个角度的评估,它分别计算旋转误差和平移误差,当旋转误差小于5度且平移误差小于5cm时,认为这次预测是正确的。

用哪个指标取决于任务。机器人抓取更关注5cm5°,因为末端执行器的容错范围是确定的;AR应用更关注ADD,因为它反映的是3D模型和实际物体表面的一致程度。写论文时一般三个指标都会给出,LineMOD上ADD和ADD-S用得最多,YCB-Video上还要额外计算AUC,即在不同阈值下的平均准确率。

6.2 评估代码的核心逻辑

评估流程一般分这几步:

  1. 加载训练好的模型,对测试集的每张图像预测位姿
  2. 加载该图像对应的模型点云、真实位姿
  3. 计算ADD或ADD-S距离
  4. 根据阈值判断是否预测正确,统计准确率

ADD的计算代码非常直接:

def compute_add(gt_pose, pred_pose, model_points): gt_transformed = model_points @ gt_pose[:3, :3].T + gt_pose[:3, 3] pred_transformed = model_points @ pred_pose[:3, :3].T + pred_pose[:3, 3] distances = np.linalg.norm(gt_transformed - pred_transformed, axis=1) return np.mean(distances)

ADD-S的代码需要计算点和点之间的最近邻距离,用scipy.spatial.distance.cdist可以高效完成。注意这里不能用暴力两层循环,模型点通常有好几千个,两层循环在评估集上会慢到无法接受。用cdist一次性算出距离矩阵,再取每行的最小值即可。

6.3 跑评估时最容易被忽视的坑

我自己动手写评估脚本时踩过不少坑,这里挑几个最典型的说一下。

第一个是坐标系单位问题。LineMOD某些版本的标注单位是厘米,预测网络输出的是米,如果你不统一单位,ADD距离会整体大100倍,导致所有结果都是0分。这个问题排查起来很隐蔽,因为loss曲线看起来很正常,只有评估时才发现完全不对。

第二个是模型点云的采样密度。ADD对模型点采样很敏感,同样一个物体,用1000个点算出的距离和用10000个点算出的距离是有差异的。官方评估一般是从物体模型表面均匀采样,建议你在评估时保持与训练时一致的点数,否则对比别人的报告时会发现数值对不上。

第三个是对称物体的mask覆盖不完整。YCB-Video里很多物体是被遮挡的,mask有可能只覆盖了物体的一部分。这时候如果直接用mask内的点云做评估,会发现ADD-S的值被高估,因为可见部分的对齐效果好,遮挡部分根本没有参与计算。稳妥做法是评估时也用完整模型点云,而不是mask中的可见点云。

第四个坑是随机采样导致的评估不稳定。YCB-Video测试时可能每帧采样不同数量的点,如果你的评估代码里用了固定随机种子,结果可能刚好偏好在某个子集上;换一台机器跑,因为numpy随机数状态不同,结果可能差0.5个百分点。要复现稳定结果,最好固定随机种子,并在多次采样后取平均值。

整个流程跑下来你会发现,DenseFusion的核心优势不在于某个模块特别花哨,而是“逐点融合加置信度投票加迭代精化”这一套组合拳非常实用。代码量不大,每块逻辑都清楚,非常适合作为3D视觉入门和复现学习的范本。我在实际使用中还有一个体会是,当你把可视化工具链搭好之后,排查问题会轻松非常多,建议在一开始就把结果绘制、特征图导出这些基础设施准备好,而不是等模型训出了问题再回头补。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询