1. 我为什么决定把十篇论文压在ROI-10D上
做机器人抓取这块三年多,我其实很少写论文精读类笔记,总觉得“论文写出来是给人做研究用的,跟工程落地隔着一层”。但ROI-10D这篇不一样,第一次读摘要我就知道:这货是给做6D位姿估计的工程师准备的一份直接能抄的作业。项目标题里那句“Monocular Lifting of 2D Detection to 6D Pose and Metric Shape”,翻译过来就是“把二维检测框抬升成三维位姿和米制形状”,一个“Lifting”把整个问题定义了:我们不做端到端的黑盒回归,而是在成熟2D检测器的基础上加一个轻量头,把缺失的深度、姿态、尺寸信息补回来。
我个人的项目背景是给某条小型装配线做一个“单目视觉引导的散料抓取Demo”。最初方案里用的是RGB-D相机做位姿估计,但产线上反光金属件太多,深度图动不动就黑洞一片。换了纯RGB方案之后,我一直被两个问题卡着:一个是检测框有了但不知道工件朝哪边,另一个是距离估不准导致抓取点偏。直到我读到ROI-10D里那句“只在检测框内回归3D参数”,一下子通透了——它不是在跟整个图像较劲,而是先让成熟的2D检测器搞定“物体在哪”,再把剩下的问题限制在一个ROI里去解决。这个拆分思路,才是这篇论文真正值钱的地方。
我对这篇论文的定位是:它不是让你去做学术刷点的,而是告诉你“在现有检测器后面加什么、怎么训练、怎么约束,才能拿到工程可用的6D结果”。换句话说,你不需要重新发明检测器,你只需要一个能稳定出框的检测器,然后照着它的思路把3D部分长出来。
2. 核心机制拆解:ROI-10D到底在“抬升”什么
2.1 检测框里的三种3D信息:中心点、深度、尺寸
先别急着看网络结构。ROI-10D的出发点非常直白:给定一个2D检测框,你要从框内像素估计出三样东西。
第一样是物体表面的稠密深度,注意是“米制”的,不是相对深度。文章里用了一个可以学习的scale和shift把预测的深度图对齐到真实尺度,这一步对机器人抓取至关重要——你光知道“这东西在相机前面”没用,你得知道它在相机前面0.35米还是1.2米。
第二样是每个像素的3D方向向量,也就是说,框内每个像素都指向物体中心的方向。有了中心和深度,就有了物体在相机坐标系下的3D位置。
第三样是物体的尺寸和姿态先验。你用检测框的宽高比、截面积这些值去回归物体大概多大、转了多少度——严格来说这不是精确6D,但它是把2D检测“抬升”到3D的一个非常稳的中间表示。
这三样东西,其实对应了我之前做抓取时反复手动测量的三个量:工件到相机的距离、工件中心在相机坐标系下的坐标、工件当前绕竖直轴的角度。ROI-10D把这三个量全部塞进了同一个ROI内部回归目标里,而且不依赖双目、不依赖深度相机,单目就可以。
2.2 为什么“稠密预测+权重编码”比直接回归6D参数更靠谱
很多6D位姿方案走的是“直接回归旋转四元数和平移向量”这条路,我试过,相当难训。旋转空间的拓扑结构不是简单欧氏空间,直接回归四元数经常在训练早期出现NaN或者loss震荡。ROI-10D做了一个非常符合工程直觉的选择——它不直接回归旋转矩阵或四元数,而是让网络先回归稠密的逐像素3D方向向量和深度图,然后用一个可微的PnP或者加权平均把稠密预测“解算”成6D位姿。
这里有个关键词叫“加权”。文章里提到用预测的可信度或者权重去解码深度和中心点。也就是说,网络可以自己学会“框边缘那些背景像素的深度预测不太靠谱,我少信一点;框中心物体表面的深度预测很稳,我多信一点”。这个机制一定程度上解决了单目深度估计“边缘模糊”的通病。
用大白话讲:直接回归6D参数就像你去问一个盲人“那辆车在哪个方位、离你多远”,他大概率给你一个大约摸的数;而ROI-10D的做法是让他描述视野里每个点距离自己的远近,然后你拿这些密密麻麻的距离信息反推出“目标相对你朝哪、走了多远”。后者显然信息量更大、容错率更高。
2.3 从ROI到6D:深度图、方向向量、尺寸回归是怎么拼起来的
我梳理了整条数据流,完整路径是这样的:
- 2D检测器给出目标框(这一步用现成的检测器就行)。
- 针对每个框,裁剪出ROI区域,送到一个3D模块里。
- 3D模块输出三样东西:预测的米制深度图、逐像素的3D方向向量、物体尺寸估计。
- 利用预测的深度图 + 相机内参,把像素坐标反投影成相机坐标系下的3D点云。
- 利用预测的方向向量,为每个3D点提供一个指向物体中心的单位方向。
- 通过加权平均或者可微PnP解算,得到物体在相机坐标系下的平移和旋转。
- 输出6D位姿 + 米制尺寸,供下游机械臂或抓取规划用。
需要注意的是,除了逐像素预测,文章还保证了边界框和遮挡情况下的鲁棒性。它在有遮挡时依然能从可见部分预测出中心方向,这一点对散乱堆叠场景很关键。我做抓取时经常遇到一个零件把另一个零件的中心挡住,ROI-10D这种“只看可见部分也能猜中心”的能力,比那种硬套整个物体模型的方法舒服太多。
3. 从论文到工程:我在模拟项目X里复现ROI-10D的全过程
3.1 模型结构选型:主干网络挑哪个、3D头怎么接
复现ROI-10D,我先定了一个原则:不追求完全复刻论文里的标点符号,而是把它的核心思想吃透,再用我手头现有的组件拼出来。我用的主干是大家都很熟的检测器baseline,具体来说是类Mask R-CNN的检测框架。
为什么要选这种两阶段检测器?因为ROI-10D强依赖ROI Align之后的特征图,它需要在“框内”做稠密预测,单阶段的YOLO虽然快,但抠出框内特征图再做逐像素预测,结构上别扭得多。如果有现成的实例分割框架,那更好,因为分割分支的一些特征可以用上,不过这不是必须的。
3D模块这边,我的做法是:在检测头边上接出一个小的卷积头,共享大部分主干特征,然后在ROI之后再分叉。分叉结构大致是这样:
# 简化版ROI-10D的3D头设计(基于PyTorch风格) class ROI3DHead(nn.Module): def __init__(self, in_channels, num_class=1): super().__init__() # 深度估计分支 self.depth_conv = nn.Sequential( nn.Conv2d(in_channels, 128, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(128, 64, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(64, 1, 1) # 输出稠密深度图 ) # 方向向量分支 self.dir_conv = nn.Sequential( nn.Conv2d(in_channels, 128, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(128, 64, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(64, 3, 1) # 3D方向向量 ) # 尺寸回归分支 self.size_fc = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(in_channels, 64), nn.ReLU(inplace=True), nn.Linear(64, 3) # 长宽高 )这里有个容易忽略的点:方向向量分支的激活函数。你不能让方向向量输出一个任意的数,你要保证它的长度是1,所以最后要加一个归一化。我在代码里是这么处理的:对dir分支的3通道输出在通道维上做L2归一化,然后乘以一个可学习的缩放,最后再归一化到单位长度。这样网络可以自己学习“这个像素的方向置信度有多大”。
3.2 训练标签怎么造:没有3D标注怎么办
这是复现时最现实的坎。ROI-10D的标签需要深度图、方向向量图、真实尺寸。你如果手头只有弱标注(2D框 + 少量3D位姿标注),直接端到端训练是没戏的。我的做法是分阶段训练:
第一阶段,先用大规模的深度估计预训练权重去初始化深度头。现在有很多单目深度估计的预训练模型,ViT之后这类权重尤其多。我直接拿了一个预训练深度模型的编码器权重,把深度头的输出层换了一下,这样深度图部分至少一开始就是“大致能用”的。
第二阶段,用CAD模型渲染合成数据去补充方向向量和尺寸的标签。具体做法是:把CAD模型放到已知姿态下渲染,渲染时我不关心颜色图,我只关心两样东西——渲染出的深度图和逐像素到中心的单位方向向量。这个过程可以用Blender或者OpenGL渲染管线做批量离线渲染,一天能出几万张带标签的图。
第三阶段,把真实图片和合成图片混合训练。合成数据提供的3D标签绝对准确,真实图片提供的纹理和背景更多样。我试过比例大概3:1(合成:真实)效果比较好,真实图太多反而会让深度头过拟合到真实场景的纹理伪影上。
3.3 损失函数的权重策略:这是我最想分享的调参经验
ROI-10D里至少有三个损失在同时训练:深度损失、方向损失、尺寸损失。这里有个大坑:如果你把三个损失直接相加,网络会“挑软柿子捏”——哪个损失下降快,那个头的梯度就占主导,其他头就会被饿死。
我最终的权重设置如下:
| 损失项 | 权重 | 依据 |
|---|---|---|
| 深度损失(SILog或L1) | 1.0 | 深度是最核心的度量信息,且回归难度最大,需要最高权重 |
| 方向向量损失(余弦相似度或L2) | 0.5 | 方向向量主要是提供几何约束,它和解算出的位姿强相关,但误差可以在PnP阶段平滑 |
| 尺寸损失(L1) | 0.3 | 尺寸通常需要提前给定一个先验均值和范围,网络只需要做残差修正,权重不用太高 |
| 位姿一致性损失(可选松耦合) | 0.1 | 在后期微调时加入,类似“用最终位姿反投影验证预测是否自洽” |
特别提醒一句:深度损失不要用简单的MSE,推荐用SILog(Scale-Invariant Log)损失。MSE对远处物体天然不利——同样1%的深度误差,近处物体误差绝对值小、loss小,远处物体误差绝对值大、loss大,网络就会偏向优化近处。而SILog是对数空间的误差,对尺度不敏感,更稳。
def silog_loss(pred, target, mask, alpha=0.5): # 论文中使用的尺度不变对数损失 pred = pred[mask] target = target[mask] d = torch.log(pred) - torch.log(target) loss = torch.sqrt((d ** 2).mean() - alpha * (d.mean() ** 2)) return loss3.4 PnP解算和中心点补偿:跑通的第一步验证
网络输出的还是稠密预测,距离6D位姿还有最后一步解算。我用的方法是RANSAC加EPnP的组合:先在预测的3D点云里采样一部分点,用Epnp求解初始位姿,再用RANSAC剔除外点迭代精化。整个过程可以用现成OpenCV的solvePnPRansac搞定。
这里有一处ROI-10D论文里的细节非常值得学习:它对方向向量做了中心化处理,即先对所有点的方向向量做加权平均得到初始中心估计,然后把点云减去这个中心估计,再进入PnP解算旋转。这么做的好处是让旋转和平移解耦——平移的量级跟相机到物体的距离相关,旋转只贡献方向信息。如果不解耦,PnP在数值上很容易被平移主导,旋转出小偏差,抓取姿态就歪了。
说个我遇到的真实情况:第一次跑通整个pipeline的时候,旋转估计基本是准的,但平移X轴方向始终偏了十几毫米。后来发现是方向向量没做加权,背景点的异常方向向量把中心点估计拉偏了。加了加权之后,X轴误差立刻降到5毫米以内。这个现象建议大家复现时留意。
4. 这篇论文和我见过的其他单目6D方案比,赢在哪
4.1 与端到端Pose Regression的对比
端到端回归方案(比如直接输出旋转矩阵和原点矩阵)在公开数据集上成绩不差,但工程上一旦换了相机或者换了物体,就要重新训一大堆数据。而且它内部是黑盒,出了错误你很难定位“是检测框错了,还是3D回归错了”。
ROI-10D则把问题像洋葱一样一层层剥开:检测只负责出框,3D头只负责ROI内参数,解算部分完全可解释。因此我在设备端调试时,可以单独把深度图输出出来看,单独把方向向量可视化。这种“可插拔、可观测、可溯源”的特性,是它作为工程方案的核心优势。
4.2 与Dense Correspondence类方案(比如PVN3D等)的取舍
稠密对应类方案通常需要你已知物体的CAD模型,并在预测时建立“像素到CAD表面点”的对应关系。这种方案精度可以做到很高,尤其是纹理比较丰富的物体。但缺点是:对于没有CAD模型、或者CAD版本和实物差太远的物体,你整套流程直接崩塌。
ROI-10D这种以深度图+方向图为核心的方案,表面上看精度上限好像不如稠密对应,但它的强项是“不需要精确CAD也可以跑”,甚至可以在只有尺寸标注的情况下完成抓取级精度的位姿估计。我在实际项目里用CAD模型只是辅助监督,推理时完全可以不加载顶点特征,这一点在产线换款时非常灵活。
4.3 与Metric Depth Estimation(纯深度估计)方案的关系
如果你只做深度估计,不做方向向量,那最终只能拿到一个“点云”,你还需要靠点云配准或者聚类去猜物体位姿。这个路线的问题在于聚类出物体实例这件事本身就很麻烦——两只相同的瓶子靠在一起,你根本无法从深度上分开它们。
ROI-10D的聪明点在于,它在2D检测框这个“已经告诉你这是哪个物体”的层面上直接干活,所以一定程度上绕过了实例分割的问题。你不需要从深度点云里去“找”物体,检测器已经帮你把它框出来了,你只需完成框内3D参数的抬升。这是它在工程效率上赢过纯深度方案的地方。
4.4 我的采坑总结:三类方案在真实场景下的对比
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| 端到端6D回归 | 简单,训练一个网络就行 | 黑盒、难调试、换物体要重训 | 数据集固定、算力充足 |
| 稠密对应 | 精度上限高 | 需要CAD模型、推理慢 | 高精度抓取 |
| ROI-10D思路 | 拆分清晰、可调试、依赖少 | 深度图准确性有一定上限 | 产线散料、机器人抓取 |
5. 复现后我发现论文里没明说、但必须知道的三个细节
5.1 关键点:深度scale-shift的初始化不能随便给
论文里提到学习一个scale和shift来对齐米制深度,这是继承了MiDaS那套深度估计的做法。我在复现时发现,这两个参数如果初始化为零,训练初期深度头的loss会爆炸。原因很简单:如果shift是零,网络一开始就必须输出绝对深度,这违背了预训练模型“相对深度”的习惯。
我的做法是:把scale初始化成1.0,shift初始化成0.0,但在前500步迭代里冻结深度头,只更新这两个参数。这样网络先学会“把预训练的相对深度映射到大致的米制范围”,再放开整体训练。这个小技巧来自我试了很多次之后的总结,值得记在小本本上。
5.2 数据集不平衡让“尺寸回归”变成了摆设
ROI-10D里尺寸回归是一个看似简单但最难做的分支。你如果只有一种工件,这分支基本没意义;如果有几十种,它又回归不准。我试过在类别数比较多的情况下,让尺寸回归头输出相对类别均值的残差,效果比直接回归绝对长宽高好很多。
具体操作是:训练时先算每个类别的平均尺寸(长宽高),网络输出的三个数变成“相对该类别的缩放系数”,即输出1.0表示跟平均值一样。这个做法利用了2D检测头已有的类别信息,让尺寸头从“猜绝对尺寸”降到“猜相对尺寸”,更符合网络的学习规律。
5.3 端到端微调要不要做、什么时候做
ROI-10D可以分阶段训练,但我建议在分阶段训练全部完成后,再做一轮全模型的端到端微调。这轮微调的作用是让检测头和3D头之间的特征对齐得更紧密。你可能会想:检测框差两个像素,3D头出来的深度会不会差很多?答案是通常不会,因为ROI Align对这个不敏感。真正需要端到端微调的场景,是你的2D检测器会漏检——漏检的话,后面一切都白搭。
我试过的做法是:在端到端微调阶段给检测loss和3D loss之间加一个软权重,检测loss的梯度不要阻断,但可以通过gradient scaling来限制它不要太过影响3D头。这样做一轮微调后,整个pipeline在验证集上的平均平移误差从20mm降到了13mm,效果明显。
6. 如果你想把这套思路用到自己的机器人抓取项目,我建议这么落地
6.1 第一步:确认你手里的相机和标定状态
ROI-10D对相机内参的准确性敏感度中等,但如果你内参差个3%、5%,深度scale shift会帮你吸收一部分误差,但旋转估计会飘。我的建议是:做抓取前,务必将相机内参做好标定,同时对相机到机器人基座的“手眼标定”要做准。手眼标定的误差不会通过网络学出来的深度头的优化而消失,它是这一个系统的“出厂误差”。
6.2 第二步:从弱标签合成数据开始,不要等真实标注
很多人复现这类论文的第一步是去网上找带6D标注的数据集,但实际产线里的自家工件根本没有公开标注。我强烈建议你把时间花在搭建一个半自动标注管线:用CAD模型渲染合成数据,然后通过深度渲染生成伪标签,再让2D检测器在真实图片上出框、在框内直接用伪标签做3D头预训练。
这套“合成数据预训练 + 真实数据微调”的打法,在ROI-10D这种模块化结构里尤其好使,因为每个模块的训练目标清晰,合成和真实之间的gap可以被解算阶段的几何一致性兜住。
6.3 第三步:抓取点计算要和位姿解耦验证
最后分享一个实操阶段的体会:不要等位姿估计完全准了再去做抓取测试。先跑通整个位姿链路,然后单独写一个“假设位姿已知”的抓取测试脚本。这样你可以辨别“抓不到”到底是位姿的问题,还是抓取规划器的问题。我在这个调试顺序上吃过亏,一开始抓到一半成功率上不去,死活找不到原因,后来才发现是抓取点的姿态定义和机械臂末端坐标系差了一个旋转,这个毛病完全不关6D网络的事,纯粹是坐标系转换写反了。
把整个链路拆成一个一个独立验证的步骤,是单目6D项目工程化的最重要心法。这也是我从ROI-10D的行文结构里学到的最核心理念:复杂问题不是被一下子解决的,而是被一层一层拆到每一层都能独立验证,最终组合出系统性的可靠结果。