简介:面向机器人与计算机视觉从业者,6dof-graspnet-master 提供了一整套基于深度学习的六自由度抓取姿态预测实现,覆盖数据集读取、模型搭建、训练调参、评估可视化全流程,可用于研究 GraspNet 在机械臂抓取中的实际效果。压缩包共 54 个文件,大小约 9.83MB,包含 16 个 Python 脚本、12 个 npy 点云与抓取数据、STL/OBJ 三维模型文件,以及 YAML、JSON、Markdown 等配置和说明文档;脚本承担训练、预测、可视化与工具函数等角色,模型文件便于直接复用,配置文件可快速调整实验参数。项目在技术上融合了卷积神经网络特征提取、生成对抗网络生成抓取候选、强化学习优化策略等思路,适合希望深入理解 6D 抓取估计或做二次开发的读者。已有 629 人学习,资源紧凑、目录结构清晰,从数据加载到模型评估均有对应代码,可按需修改并复现实验。
1. 6dof-graspnet 到底是什么:让机械臂学会“斜着抓”的深度学习网络
抓取检测这件事,很多初学者以为就是把图片里的物体框出来、找两个平行夹爪的位置,但真去调过机械臂的人都知道,一个杯子立在桌上、旁边还有一堆杂物时,夹爪必须侧着伸进去、绕过遮挡,在三维空间里选一个带朝向的抓取点。6dof-graspnet 做的就是这个:输入一帧 RGB-D 点云,输出完整的 6 自由度抓取姿态——三维位置加三维旋转。它和常见的 3 自由度抓取方案最大的差异在于,不再假设夹爪只能从上往下竖着夹,而是真把每个抓取当成空间里的 6D 位姿来回归。这个 master 包一般指的是基于 GraspNet-6DoF 的复现工程,适合正在做机械臂抓取、机器人操作或者把视觉检测往真机部署的工程师。想直接靠它省掉从零写网络的时间,这篇文章会告诉你它值不值得投入。
2. 6 自由度抓取检测的网络结构与数据表达:从点云到 6D 姿态
2.1 抓取表达:为什么不用平面矩形,而用位置加旋转
多数人第一次接触抓取检测是在康奈尔 Grasp Dataset 上,输出的是图像里的一个矩形框,中心、角度、开度,这类方案叫 3 自由度抓取或平面抓取。它有一个隐含前提:相机光轴和桌面垂直,夹爪在二维投影里选择抓取角度就够了。但当机械臂对着一堆乱放的物体侧着伸手时,平面矩形表达会漏掉关键的俯仰角和偏航角信息,夹爪伸过去就撞到旁边的东西。6 自由度抓取用三维点云作为输入,每个抓取定义为:
- 夹爪两个指尖的接触点中心坐标 (x, y, z);
- 夹爪接近方向的单位向量,决定夹爪从哪个方向伸向物体;
- 绕接近方向的旋转角,决定夹爪开度在哪个方向;
- 夹爪张开宽度 w。
这个表示的好处是贴近真实机械臂的控制接口:拿到平移向量和旋转矩阵,机械臂控制器直接做逆运动学就能规划路径。GraspNet-6DoF 论文里用了球坐标系把这些参数参数化,把 6D 姿态预测拆成两个部分——先预测接近方向和抓取置信度,再预测旋转角,这样模型输出的中间变量有明确的物理含义。
我在实际部署时发现一个容易被忽略的点:旋转角度在很多实现里是离散化分类,而不是连续回归。因为连续角度回归在 SO(3) 流形上面临周期性边界问题,角度 -180 和 180 在数值上差 360,loss 却很小,网络早期训不动。所以复现项目里通常会看到角度输出头接个 softmax,分成比如 12 或 24 类,推理时再插值成连续角度。你拿到 6dof-graspnet-master 后,第一件事就是看清楚这个角度头是分类还是回归,因为后处理代码完全不一样。
2.2 GraspNet-6DoF 数据集:场景点云与正负标签怎么组织
这个 master 包默认训练依赖 GraspNet-6DoF 数据集,官方数据包含 100 多个真实场景的连续 RGB-D 扫描,每个场景有几十次不同视角的观测。数据组织一般是 scenes 目录下按 scene_0000、scene_0001 编号,每个场景里有 align 后的相机位姿文件和深度图、彩色图,还有一个 label 目录存放抓取标签。
标签文件以 npy 格式保存,里面并不是一幅图对应一个抓取框,而是每个场景对应一个三维张量。拿到的常见格式是 (num_points, 3) 的接触点列表,配套旋转矩阵、夹爪宽度和抓取质量分数。数据集的加载逻辑不简单,因为不是每个点都适合抓取,标签会分为正样本(可执行抓取)、负样本(会碰撞或易掉)和中性样本。这直接影响后面的 loss 设计。
我一般会先跑通一个数据可视化脚本,随机挑一个场景,把抓取姿态投影到点云上渲染,看看夹爪是不是真的贴合物体表面。这一步能发现很多数据对齐问题,比如点云坐标系和标签坐标系不一致、深度图单位不一致、相机内参对不上。如果跳过可视化直接进训练,模型训完你都不知道它学的是抓取还是学的是噪声。
2.3 网络主干:PointNet++ 编码加逐点抓取预测
6dof-graspnet 的模型结构并不复杂,核心是编码器加多任务头。编码器常见选择是 PointNet++,它把原始点云做多级 downsampling,提取局部几何特征。与分类网络不同是,抓取检测需要逐点输出,所以 PointNet++ 后面还要接 feature propagation,把下采样后的特征上采样回每个输入点,保证每个点都能预测自己的抓取置信度。
多任务头有三个:
- 置信度头:每个点是否适合作为接触点;
- 接近方向头:从该点出发的接近向量,预测值常是球面单位向量;
- 旋转角头:绕接近方向旋转的角度,通常分类输出。
如果你的 master 包里有 graspproposal 这类模块命名的文件,基本就是这套结构。训练时 PointNet++ 部分可以直接用官方预训练权重,也可以用简单的分类任务在自采数据上微调。我的建议是不到万不得已别从零训整个网络,纯随机初始化下损失会在很长一段时间内不下降,你很难判断是代码问题还是模型没收敛。
3. 跑通最小推理:加载预训练权重并输出抓取姿态
3.1 环境依赖与权重文件组织
这套项目依赖的深度学习环境不算复杂,常见组合是 Python 3.8、PyTorch 1.10 左右、CUDA 11.3,外加 open3d、numpy、scipy。先确认显卡驱动能跑 GPU 版 PyTorch,别在 CPU 上跑推理,点云前向虽然比图像算得快,但一个场景几百个候选抓取要排序和 NMS,CPU 还是明显卡。
解压 6dof-graspnet-master 后,目录里一般有几类关键文件:模型定义文件、推理脚本、数据集加载器、配置文件。权重文件不会跟着源码一起放在 master 包里,通常需要单独从官方发布或训练产出下载到 checkpoints 目录。没有权重文件的话,整体就是个空壳。
我习惯先看 config 或 yaml 文件里的参数:point_num(输入点数)、batch_size、是否用 pretrained PointNet++。这里最值得改的是 point_num,它只控制采样点数,不影响网络结构,但直接决定显存占用和推理速度。常见默认值是 1024 或 4096,跑不起来就降到 1024,效果一般不至于断崖式下跌。
3.2 最小推理脚本:从深度图到抓取姿态
我贴一段最简推理流程,先把点云生成和网络前向打通,不做可视化、不做碰撞检测。代码按常见写法整理:
import numpy as np import torch import open3d as o3d from graspnetAPI import GraspNet # 1. 读取相机内参和深度图,生成点云 def depth_to_pointcloud(depth, fx, fy, cx, cy, depth_scale=1000.0): h, w = depth.shape u, v = np.meshgrid(np.arange(w), np.arange(h)) z = depth.astype(np.float32) / depth_scale # 深度单位转成米 x = (u - cx) * z / fx y = (v - cy) * z / fy pts = np.stack([x, y, z], axis=-1).reshape(-1, 3) # 过滤掉深度为0的无效点 mask = z.reshape(-1) > 0 return pts[mask]这段代码的关键点是 depth_scale。不同深度相机的深度单位不一样,RealSense 输出毫米,Kinect 部分模式输出米,如果单位弄错,点云会被拉伸成一张“薄片”或者缩成一坨。模型输入直接用这张点云会完全失去几何意义。
接着是模型加载和前向,模型结构定义要和你下载的权重严格一致,层名对不上会直接报 missing key:
from model import graspnet_6dof # 权重路径按你自己环境改 ckpt_path = "checkpoints/model_epoch_200.tar" model = graspnet_6dof() state_dict = torch.load(ckpt_path, map_location="cpu")["model"] model.load_state_dict(state_dict) model.eval().cuda() # 输入点云,网络输出抓取候选 points = torch.from_numpy(pointcloud).float().unsqueeze(0).cuda() with torch.no_grad(): # 常见输出是 dict,包含置信度、旋转矩阵或四元数、宽度 output = model(points) scores = output["scores"] # (1, N) rotations = output["rotations"] # (1, N, 3, 3) translations = output["translations"] # (1, N, 3) widths = output["widths"] # (1, N, 1) # 按置信度排序,取前 k 个抓取 order = scores[0].argsort(descending=True) top_k = order[:50] top_rotations = rotations[0, top_k] top_translations = translations[0, top_k]这里有个细节常被忽略:网络输出的平移向量是抓取中心还是夹爪指尖中心如果是前者,机械臂手爪的中心要去对齐它就有偏差。GraspNet 标签定义的接触点通常指指尖夹持点,但某些复现版本会改成两指尖中心点。推理输出后我用 open3d 画一下夹爪模型和点云,直接肉眼检查偏移量。
3.3 抓取姿态可视化与置信度过滤
拿到 top_k 抓取候选后,别急着发给机械臂。先把置信度阈值设起来,常见做法是只保留 score 前 10% 或大于 0.5 的候选,然后做非极大值抑制——相同位置、朝向接近的抓取只留置信度最高的。不 NMS 的话,几十个候选里可能有大量重叠抓取,真机执行时机械臂会反复规划几乎一样的路径。
可视化脚本我用 open3d 画点云,在每个抓取位置放一个简化的夹爪模型,夹爪的朝向由旋转矩阵决定,宽度用开度画成两根小柱子:
for i in range(10): rot = top_rotations[i].cpu().numpy() trans = top_translations[i].cpu().numpy() width = float(widths[0, order[i]].cpu().numpy()) # 在 trans 处生成夹爪模型并旋转 jaw = o3d.geometry.TriangleMesh.create_box(width / 2, 0.02, 0.02) jaw.translate(trans) jaw.rotate(rot, center=trans) vis.add_geometry(jaw)这部分跑通后,你就能在屏幕上看到点云上散布的夹爪,这就是最小闭环。剩下的是工程化:把旋转矩阵转成机械臂能用的欧拉角或四元数,注意不同厂家机械臂坐标系惯用手差异,这部分放到后面真机章节。
4. 自己训练:数据准备、损失函数与关键参数
4.1 训练数据准备:从原始 npy 到网络输入
训练第一步不是写模型,而是把数据集加载器调通。GraspNet-6DoF 官方提供了 GraspNetAPI,用起来相对省事,但装完还要注意版本匹配。数据加载常见的坑是训练集和测试集的场景划分,每类物体被严格拆开,测试集里出现的物体形状是训练时没见过的,这才是衡量泛化能力的正确方式。
点云进入网络之前要做采样。原始场景点云动辄几十万点,显存放不下,所以加载器会对每个场景做视点相关的裁剪和降采样。具体的做法是:根据当前观测相机位姿,把点云变换到相机坐标系,然后保留视野范围内的点,再随机采样到固定点数。这个「视野裁剪」非常重要,不做的话,网络会看到相机背后的点陪你一起训练。
加载流程大致如下:
# 以官方 API 示例为准,这里只做逻辑演示 from graspnetAPI import GraspNet graspnet = GraspNet(root='data', camera='realsense', split='train') scene = graspnet.loadScene(scene_id=0, camera='realsense') # 加载深度图和标签 points, colors, depth, seg = scene.loadPointCloud() grasps = scene.loadGraspLabels() # 把标签转成抓取姿态字典,过滤掉质量低的负样本 grasp_dict = grasps.getGraspDict()这段代码跑通了,你的环境才算真正齐备。整个准备过程里,最容易出问题的是路径和相机型号标识不一致。官方数据集提供 realsense 和 kinect 两套传感器数据,模型对这两套数据的噪声特征要区别对待。千万不要用 realsense 的深度图配 kinect 的相机内参去生成点云。
4.2 训练脚本与超参数设置
训练的超参数我不建议直接照搬论文里的数值,先看自己显存。常见配置里 batch_size 是 4 到 8,点云点数 1024,初始学习率 0.001。如果只有一块 12G 显存的显卡,batch_size 开 2 也可能挤爆,因为每个抓取候选都要算 loss。
以下面这段简化训练循环为基础:
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=200) for epoch in range(200): for batch in train_loader: points = batch["points"].cuda() # (B, N, 3) gt_rot = batch["rotations"].cuda() # (B, N, 3, 3) gt_width = batch["widths"].cuda() # (B, N, 1) gt_score = batch["scores"].cuda() # (B, N) # 网络前向 output = model(points) # loss 由三部分组成 score_loss = F.binary_cross_entropy(output["scores"], gt_score) rot_loss = rotation_loss(output["rotations"], gt_rot) width_loss = F.smooth_l1_loss(output["widths"], gt_width) loss = score_loss + 0.1 * rot_loss + 0.5 * width_loss optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()损失函数权重是血泪经验重灾区。置信度是分类任务,训练初期如果旋转 loss 权重太大,网络会优先去拟合旋转,置信度输出直接废掉;反过来置信度 loss 权重大,网络只学“这个点能不能抓”,对角度细节不上心。我常用配比是分类 loss 权重 1.0、旋转 loss 权重 0.1、宽度 0.5。这组数字不神圣,但你从这组起步会顺很多。
4.3 评估指标:AP 与抓取成功率的关系
训练过程中每隔几个 epoch 要保存 checkpoints 并用测试集评估一次。官方评估指标是抓取 AP,它计算的是不同置信度阈值下的准确率-召回率曲线面积。AP 高说明这个模型对训练集分布内的物体有较好的判别能力,但 AP 并不直接等价于真机成功率。
原因在于仿真评估里,抓取成功与否用的是物理引擎中的力分析,而 AP 只看姿态和标准标签的夹角、距离误差。一个姿态偏移了 5 毫米、3 度的抓取,AP 可能只扣一点点分,但真机上可能夹到边缘掉下去。所以汇报结果时我会把 AP 和实际抓取成功率分开列:AP 代表模型感知能力,抓取成功率代表整体系统的执行能力。后期调优的重点应该放在后者,而 AP 更适合作为训练是否收敛的监测指标。
5. 避坑与常见问题排查:我踩过的 6 个坑
5.1 现象:训练时 loss 不下降,点云全是黑的
训练第一天按理说 loss 会往下降,但跑了一百个 iteration 还在震荡,打开可视化一看,输入点云坐标全是 nan 或零。
原因:生成点云时深度图的深度值为 0 的位置没有过滤,或者深度单位换算错误导致 z 方向被压缩成一条线。还有一种情况是相机内参是图像分辨率裁剪后的,点云生成时没把内参的 cx、cy 同步缩放。
解决:把深度图转点云的代码单独拎出来做单元测试,随机挑三张深度图生成点云,用 open3d 可视化确认点云形状合理。nan 直接过滤,深度小于 0.1 米的点也过滤掉,这些点通常是传感器噪点。
5.2 现象:推理结果全是竖直抓取,姿态没有多样性
模型对每个物体预测的抓取姿态几乎都是垂直向下,没有任何侧向抓取角度。
原因:训练数据里负样本太多,正样本里垂直向下的抓取又占据了绝对多数。因为物体放在桌面上,从上往下抓成功率天然高,数据集中接近垂直的标注就多。网络学到的只是数据分布的均值,而不是泛化后的抓取策略。
解决:训练时对标签做类别均衡采样,或者对场景中的物体做随机旋转数据增强。更直接的办法是减少垂直接近方向的训练样本比例,但这个操作要谨慎,真实场景里垂直抓取确实是最稳的,强行拉平分布可能损害整体抓取成功率。
5.3 现象:评估 AP 高但真机抓取失败
仿真评估时 AP 能到 0.7 以上,一上真机就抓一个掉一个。
原因:最常见的黑匣子问题是手眼标定误差。相机坐标转到机械臂基座时旋转矩阵偏了哪怕 2 度,在 60 厘米远处就会产生十几毫米的平移误差,对抓取来说已经致命。其次,仿真评估用的是完美点云,真实深度图有噪声和遮挡,模型没见过这种退化输入。
解决:先在仿真里加高斯噪声和随机丢点,再试试模型鲁棒性,判断模型还是标定问题。如果加了噪声 AP 掉得不多但真机还是失败,重点查标定板重投影误差和深度图的尺度。
5.4 现象:迁移到自己的相机视角,姿态跟着全歪
换了相机型号或者改了安装角度,输出抓取姿态整体偏差很大,像是点云被旋转了某个固定角度。
原因:模型训练时使用的深度相机型号固定,且方向是水平朝前或者 45 度朝下。你的相机安装视角和训练数据差异过大时,点云的坐标系相对观察方向不同,模型学到的抓取先验不匹配。更隐蔽的错误是相机内参里畸变系数没参与校正,画面边缘的点云有弯曲。
解决:保留相机安装角度不变,如果必须改,就用采集一批新场景微调模型。不要指望零样本跨相机泛化,6dof 姿态估计不像 2D 分类那样对传感器变化那么鲁棒。
5.5 现象:训练时显存 OOM
输入点云 4096 点加 batch_size 4 也会显存爆掉,报 CUDA out of memory。
原因:网络输出逐点抓取候选,每个点的特征维度很大,加上旋转矩阵输出是 9 维,候选数量多,forward 过程中的中间变量非常占显存。PyTorch 的 autograd 会把中间张量全部保留用于反向传播。
解决:把点云点数降到 1024,batch 降到 2,损失函数计算时先做下采样再算,把耗内存的张量 detach 掉。也可以启用 torch.cuda.amp 混合精度训练,显存占用能省接近一半,速度还快。如果你只有一块 8G 卡,AMP 是必须开的。
5.6 现象:权重文件加载报 missing key
加载网上下载的预训练权重时,报 missing keys 或 unexpected keys,模型结构定义和权重来源不一致。
原因:同一套网络有多个复现版本,模块命名不同,例如有的把旋转矩阵输出叫 rot,有的叫 rotation_head。即使层结构完全相同,dict 大小写不一致也会报 missing key。
解决:不要裸用 load_state_dict,加上 strict=False 加载可匹配部分,然后逐层打印 key 对比,把模型的命名映射到权重命名。这个映射写好后一次性解决问题,下次不再踩。
6. 在真实机械臂上验证:抓取姿态过滤与手眼标定配合
这章说说模型输出到机械臂执行的最后一公里。很多人跑到可视化这步觉得大功告成,这是最可惜的翻车点。模型输出 N 个抓取候选,直接按置信度排序丢给机械臂执行,大概率会撞。原因是模型只学习了抓取姿态本身,并不知道机械臂本体的碰撞体积和关节限位。我习惯在模型输出后加一步通用的后处理:把抓取姿态投影回原始点云,检查夹爪的每个手指体积与点云是否存在重叠。如果夹爪模型与物体点云在空间上有交叉,说明这个抓取实际上会撞进去,即使旋转矩阵完美也要丢弃。
另一个必须做的是把欧拉角转成机械臂的语言。不同机械臂对姿态角的定义顺序不同,ZYX 和 XYZ 不是差一个名字那么简单,同一个四元数转成不同顺序的欧拉角之后,关节规划的解完全不一样。我遇到过一次抓取姿态在 RViz 里看着完全正确,但机械臂执行路径疯狂绕大圈的情况,最后发现是目标姿态的旋转矩阵按列优先传给了控制器,差一个转置。这种问题只能靠保存日志对比期望姿态和实际 TCP 姿态,肉眼盯多就熟练了。
手眼标定是决定真机成功率的底层因素。眼在手上还是眼在手外,标定策略完全不同。眼在手上要标定相机到法兰盘的变换,眼在手外要标定相机到机器人基座的变换。我吃过一个教训:眼在手上时换了夹爪,忘了重新标定,因为夹爪长度变了,相机相对法兰盘的位姿也跟着变。此后我把标定写进换硬件的流程里,换了就标,不带侥幸心理。
模型置信度阈值和夹爪开度配合也很关键。程序里最后一步我会把宽度乘以 0.9 再发出,略小于模型预测的开度,给物体表面误差留一点余量。这些细节叠加起来,配合上前面几个章节讲的参数调整步骤,才是我真正愿意拿到产线上跑的版本。
如果你项目节奏紧张,我的建议是先用预训练权重加自己的点云可视化把链路跑通,再决定要不要自己重训。数据标签质量决定了这个方向的天花板,真机反馈才是模型迭代的真正驱动力。希望帮到你。
本文还有配套的精品资源,点击获取