简介这份资源面向机器人视觉抓取方向的学习者与研究者提供一套基于Python实现的GRCNN机械臂平面抓取完整代码方案适合具备一定深度学习与计算机视觉基础、希望复现或二次开发抓取检测算法的中高级开发者。包内共89个文件以34个Python脚本为核心涵盖模型定义、训练与推理流程、相机标定及抓取生成等模块辅以PNG、JPG图像样本、TXT说明与配置文件、Shell脚本、XML工程配置及PDF论文资料压缩包整体约72.55MB目录按models、hardware、utils、data、trained-models等分层组织结构清晰便于按模块查阅。资源同时附带Cornell与Jacquard数据集的获取脚本及多组预训练权重可帮助读者快速跑通离线与实时抓取流程理解GRCNN网络在平面抓取任务中的训练与部署思路。目前已有828人学习下载适合作为机械臂视觉抓取项目的实践参考。1. 从一张桌面照片到抓取位姿GRCNN 在机械臂视觉平面抓取里到底扮演什么角色桌面散落着几个零件相机固定在机械臂基座旁你希望机械臂自己判断“抓哪里、从哪个角度下手”。这件事在工业分拣、3D 打印件下料、教学实验台上反复出现核心难点不是机械臂能不能动而是平面抓取检测能不能稳定给出一个可执行的抓取矩形。基于 Python 实现的 GRCNNGrasp Rectangle Convolutional Neural Network抓取矩形卷积网络就是干这个的输入一张 RGB 或 RGB-D 图像输出一组抓取矩形参数中心点、宽高、角度、置信度再换算成机械臂末端位姿去执行。它适合谁适合已经有一台能通过 ROS 或串口控制的机械臂六轴、AR3、总线舵机臂都行、手上有 RealSense 或普通 USB 相机、想用 Python 把“视觉抓取”这条链路跑通的工程师和学生。GRCNN 不是端到端大模型它是一个结构清晰、参数量可控的抓取检测网络配合 Python 生态里的 PyTorch、OpenCV、NumPy 就能落地。这一章先把概念立住后面几章讲怎么搭、怎么调、坑在哪。2. GRCNN 的网络结构与抓取表示为什么用残差级联而不是直接回归2.1 抓取矩形的五参数表示与角度周期性问题平面抓取最常用的表示是一个旋转矩形记作 $g (x, y, w, h, \theta)$。$x, y$ 是矩形中心在图像像素坐标下的位置$w, h$ 是夹爪张开宽度和矩形高度$\theta$ 是夹爪接近方向与图像水平轴的夹角。机械臂执行时把像素坐标通过相机内参和手眼标定矩阵反投影到机械臂基座坐标系$\theta$ 再叠加相机安装角度得到末端绕 Z 轴的旋转。这里第一个反直觉的点是角度的周期性。$\theta$ 和 $\theta \pi$ 在物理上是同一个抓取方向夹爪对称但直接回归角度会让网络在 $\pi$ 附近产生跳变损失函数震荡。常见做法有两种一是把角度离散成 N 个 bin 做分类GRCNN 类方法常用 6 或 12 个 bin二是回归 $(\cos 2\theta, \sin 2\theta)$ 两个分量天然消除周期歧义。我一般用第二种因为输出连续、后处理简单。import torch import torch.nn as nn import math class GraspHead(nn.Module): 抓取检测头输出中心偏移、宽高、角度双分量、置信度 def __init__(self, in_channels512, num_anchors1): super().__init__() self.num_anchors num_anchors # 中心点偏移 (dx, dy) self.reg_xy nn.Conv2d(in_channels, 2 * num_anchors, 1) # 宽高 (w, h)用 exp 保证为正 self.reg_wh nn.Conv2d(in_channels, 2 * num_anchors, 1) # 角度双分量 (cos2θ, sin2θ) self.reg_angle nn.Conv2d(in_channels, 2 * num_anchors, 1) # 置信度 self.cls nn.Conv2d(in_channels, 1 * num_anchors, 1) def forward(self, x): xy self.reg_xy(x) wh self.reg_wh(x) ang self.reg_angle(x) cls self.cls(x) # 角度归一化到单位圆避免模长漂移 ang ang / (ang.norm(dim1, keepdimTrue) 1e-6) return xy, wh, ang, cls这段代码的关键在reg_angle输出后做了一次 L2 归一化。如果不归一化训练中 $(\cos 2\theta, \sin 2\theta)$ 的模长会偏离 1反解角度时atan2结果虽然不受模长影响但损失函数里的角度项会失真。参数上in_channels512对应骨干网络最后一层通道数实际用 ResNet-18 时是 512用轻量骨干要相应改。num_anchors在单尺度特征图上设 1 就够多尺度才需要加。2.2 残差连接与级联细化GRCNN 名字里的“GR”从哪来GRCNN 的“GR”通常指 Grasp Rectangle但结构上它借鉴了级联检测的思路先粗定位再逐步细化。骨干网络用带残差连接的卷积堆叠解决深层网络梯度消失检测头不是一次输出最终矩形而是分阶段回归残差。第一级输出粗略中心第二级在粗略中心附近采样特征再回归偏移量。为什么这么做平面抓取里中心点定位误差 5 个像素夹爪就可能夹空或撞到物体边缘。单级回归在物体密集、遮挡场景下容易把相邻物体的特征混在一起。级联细化相当于给网络一个“后悔药”第一级错了第二级还有机会在局部窗口里修正。class CascadeRefine(nn.Module): 两级级联细化粗回归 残差修正 def __init__(self, backbone, head): super().__init__() self.backbone backbone self.head head self.refine_conv nn.Sequential( nn.Conv2d(512, 256, 3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(256, 4, 1) # 输出 dx, dy, dw, dh 残差 ) def forward(self, x, roi_boxesNone): feat self.backbone(x) xy, wh, ang, cls self.head(feat) # 第一级结果作为初始 proposal # 第二级在 proposal 对应特征区域上做残差回归 # 实际实现用 ROIAlign 采样这里简化为全局残差 residual self.refine_conv(feat) return xy, wh, ang, cls, residualrefine_conv输出 4 个通道对应中心和高宽残差。实际工程里第二级要用torchvision.ops.roi_align把第一级 proposal 对应的特征块抠出来再送进一个小卷积网络而不是像上面这样全局回归。这里简化是为了说明残差修正的接口形态。参数上ROIAlign 的output_size设 7×7 或 14×14spatial_scale要和特征图下采样倍率一致否则采样位置整体偏移这是新手最容易翻车的地方。2.3 损失函数为什么角度项要用 smooth L1 而不是 MSE抓取检测的损失一般由四部分组成中心点损失、宽高损失、角度损失、置信度损失。中心点和宽高用 smooth L1置信度用二分类交叉熵角度项我建议也用 smooth L1 作用在 $(\cos 2\theta, \sin 2\theta)$ 上而不是直接对 $\theta$ 用 MSE。原因很直接$\theta$ 在 $-\pi/2$ 和 $\pi/2$ 附近数值差异大但物理方向接近MSE 会给出一个巨大的惩罚把网络带偏。作用在双分量上两个物理接近的角度在向量空间里也接近损失平滑。权重上中心点损失权重给 1.0宽高给 1.0角度给 0.5 到 1.0置信度给 1.0。角度权重不宜过大否则网络为了拟合角度牺牲中心定位精度抓取时角度再准中心偏了也白搭。def grasp_loss(pred_xy, pred_wh, pred_ang, pred_cls, gt_xy, gt_wh, gt_ang, gt_cls, w_xy1.0, w_wh1.0, w_ang0.5, w_cls1.0): 抓取检测多任务损失 loss_xy nn.functional.smooth_l1_loss(pred_xy, gt_xy) loss_wh nn.functional.smooth_l1_loss(pred_wh, gt_wh) # 角度转双分量后计算 gt_ang_vec torch.stack([torch.cos(2 * gt_ang), torch.sin(2 * gt_ang)], dim1) loss_ang nn.functional.smooth_l1_loss(pred_ang, gt_ang_vec) loss_cls nn.functional.binary_cross_entropy_with_logits(pred_cls, gt_cls) return w_xy * loss_xy w_wh * loss_wh w_ang * loss_ang w_cls * loss_clsgt_ang是标注角度单位弧度。binary_cross_entropy_with_logits要求gt_cls是 0/1 浮点张量形状要和pred_cls对齐。如果正负样本极度不平衡一张图里可能只有一个可抓取矩形要在pos_weight里给正样本加权常见设 5 到 10。3. 用 Python 把 GRCNN 跑起来数据、训练、推理的最小闭环3.1 数据集准备与标注格式转换GRCNN 类方法常用的公开数据集是 Cornell Grasping Dataset包含 885 张 RGB-D 图像和对应的抓取矩形标注。标注文件是每行一个矩形x y w h thetatheta 是弧度。如果你用自己的相机采数据标注工具可以用 labelme 画旋转矩形再写脚本转成同样格式。import numpy as np import os def load_cornell_annotations(ann_path): 读取 Cornell 格式标注返回 (N,5) 数组 grasps [] with open(ann_path, r) as f: for line in f: parts line.strip().split() if len(parts) 5: continue x, y, w, h, theta map(float, parts[:5]) grasps.append([x, y, w, h, theta]) return np.array(grasps, dtypenp.float32) def normalize_grasp(grasp, img_w, img_h): 把像素坐标归一化到 [0,1]角度保持弧度 x, y, w, h, theta grasp return np.array([x / img_w, y / img_h, w / img_w, h / img_h, theta], dtypenp.float32)load_cornell_annotations里做了长度判断因为有些标注文件末尾有空行或注释行。normalize_grasp把中心点和宽高归一化训练时网络输出也在归一化空间推理后再乘回图像尺寸。注意角度不归一化因为弧度本身有物理意义归一化反而引入额外缩放。数据增强对抓取检测很关键。常用的是随机旋转、随机裁剪、颜色抖动。旋转增强时抓取矩形的角度要同步加上旋转角否则图像转了、标注没转网络学到的就是错的。这是血泪经验我见过有人只转图像不转标注训练 loss 降不下去排查半天才发现是增强脚本的锅。3.2 训练脚本的关键参数与显存控制训练 GRCNN 用 PyTorch骨干网络 ResNet-18 或 ResNet-50。输入尺寸常见 224×224 或 320×320。batch size 在 8GB 显存上224 输入用 ResNet-18 可以跑到 16320 输入只能跑到 8。学习率初始 1e-3用 Adam 或 SGDSGD 要配 momentum 0.9 和 weight decay 1e-4。import torch from torch.utils.data import DataLoader from torch.optim import Adam from torch.optim.lr_scheduler import StepLR def train_one_epoch(model, loader, optimizer, device): model.train() total_loss 0.0 for imgs, grasps in loader: imgs imgs.to(device) grasps grasps.to(device) gt_xy grasps[:, :2] gt_wh grasps[:, 2:4] gt_ang grasps[:, 4] gt_cls torch.ones(imgs.size(0), 1, devicedevice) pred_xy, pred_wh, pred_ang, pred_cls model(imgs) loss grasp_loss(pred_xy, pred_wh, pred_ang, pred_cls, gt_xy, gt_wh, gt_ang, gt_cls) optimizer.zero_grad() loss.backward() # 梯度裁剪防止级联结构梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() return total_loss / len(loader) # 训练循环骨架 device torch.device(cuda if torch.cuda.is_available() else cpu) model CascadeRefine(backbone, head).to(device) optimizer Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler StepLR(optimizer, step_size30, gamma0.1)clip_grad_norm_的max_norm5.0是经验值。级联结构里第二级残差分支如果初始化不好梯度容易放大裁剪能稳住训练。StepLR每 30 个 epoch 降一次学习率总 epoch 一般 100 到 200。如果 loss 在前 20 个 epoch 就平了检查标注角度是否和图像方向一致以及数据增强有没有同步旋转标注。显存不够时优先降输入尺寸而不是降 batch size。抓取检测对分辨率敏感224 输入下小物体抓取矩形可能只有十几个像素网络看不清。320 输入显存翻倍但精度提升明显。折中方案是用 256 或 288。3.3 推理与抓取位姿解算从网络输出到机械臂坐标推理阶段网络输出归一化的(x, y, w, h, theta)先乘回图像尺寸得到像素坐标再通过相机内参反投影。假设相机已经做过手眼标定得到相机到机械臂基座的变换矩阵 $T_{cam}^{base}$像素点 $(u, v)$ 对应的深度 $z$ 从深度图读取反投影公式$$ \begin{bmatrix} X_c \ Y_c \ Z_c \end{bmatrix} z \cdot K^{-1} \begin{bmatrix} u \ v \ 1 \end{bmatrix} $$再左乘 $T_{cam}^{base}$ 得到基座坐标。抓取角度 $\theta$ 是图像平面内的末端绕 Z 轴旋转要加上相机光轴与基座 Z 轴的夹角。import cv2 import numpy as np def pixel_to_base(u, v, depth, K, T_cam_base): 像素坐标 深度 - 机械臂基座坐标 fx, fy K[0, 0], K[1, 1] cx, cy K[0, 2], K[1, 2] Xc (u - cx) * depth / fx Yc (v - cy) * depth / fy Zc depth pt_cam np.array([Xc, Yc, Zc, 1.0]) pt_base T_cam_base pt_cam return pt_base[:3] def grasp_to_pose(grasp_pixel, depth_img, K, T_cam_base, cam_z_angle0.0): 抓取矩形 - 末端位姿 (x,y,z,rz) u, v, w, h, theta grasp_pixel depth depth_img[int(v), int(u)] / 1000.0 # 假设深度单位 mm if depth 0: return None pos pixel_to_base(u, v, depth, K, T_cam_base) rz theta cam_z_angle return np.array([pos[0], pos[1], pos[2], rz])depth_img单位如果是毫米要除以 1000 转米。cam_z_angle是相机安装时绕光轴的偏角手眼标定后从旋转矩阵里提取。如果深度图在抓取中心处有空洞反光、透明物体常见要往周围邻域找有效深度取中值而不是最近值否则 Z 坐标跳变机械臂会突然下扎。4. 避坑与排查GRCNN 抓取落地时最容易翻车的 5 个地方4.1 抓取中心总是偏到物体边缘现象推理可视化时抓取矩形中心不在物体几何中心偏向一侧夹爪执行时夹空或只夹到一角。原因训练集里正样本标注本身有偏或者损失函数里中心点权重太低网络优先拟合了宽高和角度。另一个常见原因是特征图下采样倍率大中心点回归的量化误差被放大。解决检查标注是否一致把中心点损失权重从 1.0 提到 2.0如果骨干下采样 32 倍改用下采样 16 倍的特征图接检测头或者加一个上采样层。我一般会在检测头前加一层nn.Upsample(scale_factor2)中心精度能提升 3 到 5 个像素。4.2 角度预测在 0 和 π 之间反复跳现象同一物体连续两帧推理角度一个接近 0一个接近 π机械臂末端来回转。原因角度用单值回归且损失函数没有处理周期性或者双分量没有归一化模长漂移导致atan2解出错误象限。解决确认角度输出是 $(\cos 2\theta, \sin 2\theta)$ 且做了 L2 归一化后处理时用0.5 * atan2(sin2θ, cos2θ)反解得到 $[-\pi/2, \pi/2]$ 范围内的角度再根据夹爪对称性决定是否需要加 π。如果还是跳在时序上做滑动平均连续 3 帧角度差超过阈值就丢弃当前帧。4.3 深度图空洞导致 Z 坐标异常现象机械臂在某个物体上方突然下扎或抬得过高日志里 Z 坐标和周围点差异巨大。原因物体表面反光、透明或黑色吸光深度相机在该像素返回 0 或极大值。解决在grasp_to_pose里加深度有效性检查depth 0或depth 2000毫米时在 5×5 邻域内找有效深度取中值。如果邻域也没有放弃该抓取让网络输出次优候选。别用均值均值会被空洞边缘的异常值拉偏。4.4 手眼标定矩阵方向搞反现象抓取矩形在图像里位置正确但机械臂移动到完全错误的位置甚至往反方向走。原因$T_{cam}^{base}$ 和 $T_{base}^{cam}$ 用混了或者标定时的旋转矩阵没有正交化。解决验证方法很简单把标定板放在相机视野中心用标定矩阵算出的基座坐标应该和机械臂实际移动到该点的坐标一致。旋转矩阵用 SVD 正交化U, _, Vt np.linalg.svd(R); R U Vt。平移向量单位确认是米还是毫米和深度单位统一。4.5 训练 loss 下降但实际抓取成功率不涨现象验证集 loss 很低但真机抓取十次成功两三次。原因训练数据分布和实际场景差异大光照、背景、物体材质或者 loss 低是因为网络学会了预测数据集中出现频率最高的抓取模式而不是真正理解物体。解决在真实场景下采 50 到 100 张图人工标注后做微调学习率降到 1e-4。同时检查数据增强是否过强颜色抖动幅度太大会让网络忽略颜色线索。另外抓取成功率和夹爪控制也有关接近速度、夹取力度、抬升轨迹都要调别把锅全甩给视觉。5. 进阶技巧用抓取质量排序和时序滤波把成功率再提一档网络输出的是单个抓取矩形但实际场景里一个物体可能有多个可行抓取。GRCNN 类方法可以在检测头上输出多个候选多 anchor 或多 bin推理时按置信度排序取前 K 个依次尝试。这比只取 Top-1 稳得多尤其当 Top-1 因为深度空洞或碰撞风险不可执行时Top-2 能顶上。def rank_grasps(grasps, scores, depth_img, K, T_cam_base, topk5): 按置信度排序并过滤不可执行抓取 order np.argsort(-scores) candidates [] for idx in order[:topk]: g grasps[idx] pose grasp_to_pose(g, depth_img, K, T_cam_base) if pose is None: continue # 过滤工作空间外的抓取 if not (0.1 pose[0] 0.8 and -0.4 pose[1] 0.4): continue candidates.append((pose, scores[idx])) return candidatestopk5是经验值太多会增加决策时间。工作空间过滤的阈值根据你的机械臂臂展设别照抄。scores是置信度经过 sigmoid 后的值排序前确认没有做 softmax二分类用 sigmoid 就够。时序滤波是另一个提成功率的技巧。机械臂在移动过程中相机连续采图对连续多帧的抓取位姿做卡尔曼滤波或简单滑动平均能压掉单帧噪声。我一般用 5 帧窗口位置和角度分别平均角度平均前先解到 $[-\pi/2, \pi/2]$ 再算避免跨 π 跳变。验证方法上别只看离线指标。搭一个简单的评估台固定相机和机械臂放 10 个不同形状的物体每个物体跑 20 次抓取记录成功次数。成功率低于 70% 就回去查数据分布和标定高于 90% 再考虑加物体种类和遮挡场景。这个评估台花半天搭比调一周参数有用。我自己踩过最深的坑是忽略夹爪宽度和网络输出的 $w$ 的对应关系。网络输出的 $w$ 是归一化宽度乘回图像尺寸后是像素宽度要通过深度和相机内参换算成物理宽度再和夹爪最大张开度比较。如果物理宽度超过夹爪行程这个抓取直接不可执行但网络不知道你的夹爪型号它只学数据集的标注。所以后处理里必须加一道物理可行性过滤这一步省不得。希望帮到你。本文还有配套的精品资源点击获取