尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

grasp_nms安装与并行NMS原理:6DoF抓取候选高效过滤实战

发布时间:2026/9/16 2:43:17

资讯中心
01
ARTICLE

grasp_nms安装与并行NMS原理:6DoF抓取候选高效过滤实战

grasp_nms安装与并行NMS原理:6DoF抓取候选高效过滤实战
简介grasp_nms 1.0.2是一个面向机器人抓取与目标检测场景的非极大值抑制加速库基于Cython和C编写专注于解决抓取候选框数量大、重叠度高时的后处理效率问题适合有一定Python和C基础的视觉算法工程师使用。整个发布包共19个文件压缩后仅47KB主要包含C核心实现.cpp/.h、Cython类型声明与绑定.pyx/.pxd、构建配置setup.py/setup.cfg/pyproject.toml及软件包元数据与说明文档安装时可通过pip直接调用setup.py完成编译体积小巧却涵盖了源码级定制的全部要素。该资源已有295人学习当前在抓取检测方案中需要替代通用NMS或提升推理吞吐的开发者可以直接参考。包内graspnms.cpp与grasp_nms.pyx展示了算法从声明到实现在Cython层的完整封装流程处理函数入口清晰配合README中记录的接口调用说明用户可以快速迁移到自己的项目也可基于C源码替换IoU计算逻辑或加入自定义阈值策略进行深度优化。1. 抓取候选太多先把 grasp_nms 跑起来在 6DoF 抓取检测里模型输出往往一次给出一两万个候选抓取其中大量抓取在三维空间里高度重叠同一个物体表面可能有几十个角度接近、开合宽度几乎一样的候选。如果直接把这些候选交给运动规划器光是碰撞检测和逆解就要多出几个数量级的计算量。grasp_nms 这个库就是干这个的它专门为抓取候选设计了一套非极大值抑制NMS实现用并行计算把互相重叠的候选压缩到一个稀疏集合。这个库以 tar.gz 源码包发布需要本地编译安装所以很多人第一次接触它不是在写抓取算法而是在折腾 Python 环境和 CUDA 工具链。下面从安装开始把每一条参数和报错都讲清楚。2. 从 grasp_nms-1.0.2.tar.gz 开始源码包安装与构建2.1 tar.gz 到底是什么为什么不是 wheelsgrasp_nms-1.0.2.tar.gz 是一个打包好的源码发布格式。很多时候你是在 vscode 里打开了某个项目的终端准备开始装依赖结果第一步解压就碰壁。与纯 Python 库的 wheel 不同tar.gz 通常意味着库里包含需要本地编译的部分——这里就是 CUDA 扩展。我用tar -tzf看一眼包结构tar -tzf grasp_nms-1.0.2.tar.gz典型输出会列出grasp_nms-1.0.2/ grasp_nms-1.0.2/setup.py grasp_nms-1.0.2/grasp_nms/ grasp_nms-1.0.2/grasp_nms/nms.py grasp_nms-1.0.2/src/ grasp_nms-1.0.2/src/grasp_nms_kernel.cu grasp_nms-1.0.2/README.md看到.cu文件基本可以确认这是一个 CUDA 扩展。setup.py会负责调用nvcc把 CUDA 源码编译成 Python 能直接调用的.so。所以安装前必须确认三件事Python 版本常见要求 3.63.10、CUDA 工具链nvcc 版本需要和运行时的 CUDA 匹配、PyTorch 版本如果扩展依赖 PyTorch 的 tensor 接口。这三者任何一个不一致最后都会以编译或导入失败收场。2.2 使用 pip 直接安装 tar.gz 包最简单的安装方式是把文件路径传给 pippip install ./grasp_nms-1.0.2.tar.gzpip会先解压到临时目录读取setup.py如果有扩展模块就调用构建流程。这个过程跟我下面要说的手动解压安装最终产物是一样的。区别在于pip install会管理依赖元数据卸载时也干净。如果setup.py里声明了torch作为依赖pip 会尝试去 PyPI 拉取匹配的 torch 版本——这经常出问题因为本地已经是某种版本的 torchpip 可能想把它升级导致你原本的模型代码突然跑不了。所以我更推荐的常见做法是先手动解压然后用setup.py构建但不安装只生成.so文件内容放到当前目录里这样不会污染 site-packages。特别是做机器人抓取研究的时候经常要来回切分支这种方式最省心。2.3 手动解压 setup.py 构建的具体步骤tar -xzf grasp_nms-1.0.2.tar.gz cd grasp_nms-1.0.2 python setup.py build_ext --inplace这里--inplace是关键参数它会在当前源码目录内生成编译好的扩展文件而不是复制到 site-packages。生成后直接在grasp_nms-1.0.2/目录下就能import grasp_nms适合调试。构建过程中的日志要认真看重点是有没有调用到 nvcc以及最后有没有生成.sols -la grasp_nms/*.so如果看到类似grasp_nms/nms.cpython-38-x86_64-linux-gnu.so的文件说明编译成功。如果没有最常见的原因是setup.py里用的CUDA_HOME环境变量指向了不存在的目录。如果 tar 解压时提示“没有那个文件或目录”先确认你当前路径是否真的存在这个 tar.gz 文件ls -l看一下有时只是下载不完整或者名字带了个.1后缀。接下来检查编译环境echo $CUDA_HOME nvcc --version我遇到过多起nvcc --version显示 CUDA 11.7但CUDA_HOME是空的情况setup.py 就找不到头文件。解决办法是在构建前显式声明export CUDA_HOME/usr/local/cuda-11.72.4 安装时的常见报错与排查报错特征可能原因处理方式Error: nvcc not foundPATH 里没有 nvcc检查/usr/local/cuda/bin是否在 PATHexport PATH/usr/local/cuda/bin:$PATHfatal error: torch/extension.h: No such file or directory没有安装 PyTorch 或版本不匹配pip install torch1.13.1cu117重新安装匹配版本gcc: error: unrecognized command line option -stdc14gcc 版本太旧升级到 GCC 5.4 以上undefined symbol: _ZN2at....so 编译时的 PyTorch 版本和运行时不匹配在同一虚拟环境下重新编译不要混用不同环境的.so这些坑几乎每个接触这个库的人都会遇到一轮。本质上都是环境一致性问题编译时用到的库和运行时加载的库必须来自同一套 Python 环境和 CUDA 运行时。虚拟环境是必须的我一般用 conda 创建conda create -n grasp python3.8 conda activate grasp conda install pytorch1.13.1 torchvision cudatoolkit11.7 -c pytorch pip install ./grasp_nms-1.0.2.tar.gz这套组合在 nvcc 11.7 gcc 9 的环境下基本一次通过。如果你用的是更新的 CUDA 12.x记得把 PyTorch 也换成对应的 cu118 或 cu121 轮子否则编译出来的符号会对不上。3. grasp_nms 的并行 NMS 原理从抓取表示到 IoU 计算3.1 抓取候选的数学表示要理解 grasp_nms 为什么需要特殊的 NMS先要看抓取候选是怎么表示的。常见的抓取表示是一组参数中心点(x, y, z)、抓取旋转角θ、张开宽度w、接近方向v以及夹爪指长等。在 grasp_nms 里典型的一条抓取记录是一个高维向量其中前几位是平移和旋转的四元数或角度。模型输出的这些参数直接描述了夹爪在三维空间中的位置和姿态。NMS 要做的是在这一堆候选里找到局部最“好”的那些。评价抓取好坏一般有 confidence score就是模型输出的抓取成功率。NMS 的过程可以简化为按 score 从高到低排序取最高的一个然后去掉所有和它重叠度超过阈值的候选再取剩下的里面 score 最高的重复直到集合为空。这是标准 NMS 的思路。3.2 传统 NMS 为什么慢如果直接对每个抓取计算与其他所有抓取的重叠度复杂度是 O(n²)。候选数经常是两万那就要比较四亿对还要对每个对做三维几何计算。在 Python 里用 for 循环写一轮要几十秒。即使转成 numpy 批处理在 CPU 上也要一秒以上。而且这里的重叠度不是简单的 3D IoU。抓取候选有朝向两个中心点很接近但朝向差 90° 的抓取在实际执行时可能完全不冲突反过来中心点隔着一段距离但如果夹爪完全张开也可能互相干涉。更合理的度量是抓取角度差 中心点距离 宽度差异的组合。grasp_nms 的实现里一般将两个抓取之间的相似度定义为def compute_grasp_similarity(g_i, g_j): center_dist torch.norm(g_i[:3] - g_j[:3]) angle_diff torch.abs(g_i[3] - g_j[3]) width_diff torch.abs(g_i[4] - g_j[4]) return center_dist angle_diff * alpha width_diff * beta其中alpha和beta是两个权重参数把角度和宽度统一到距离量纲。实际实现还会考虑旋转矩阵上的差异比如用旋转向量范数。这部分计算很重所以抓取 NMS 通常都在 GPU 上用多线程并行做。3.3 grasp_nms 的并行化策略grasp_nms 的核心是一个 CUDA kernel它会在 GPU 上一次性载入所有抓取候选然后为每个抓取启动一个线程块。每个线程块里的线程分别计算该抓取与其他抓取的距离通过原子操作标记需要抑制的项。这里给出一个逻辑等价的伪代码// 伪代码展示并行NMS标记逻辑 __global__ void nms_kernel(const float* scores, const float* rois, float threshold, int* keep, int num) { int idx blockIdx.x * blockDim.x threadIdx.x; if (idx num) return; // 线程idx负责候选idx for (int j 0; j num; j) { if (scores[idx] scores[j]) { // 如果另一个候选得分更高且重叠度大当前候选被抑制 if (compute_iou(rois[idx], rois[j]) threshold) { keep[idx] 0; break; } } } }这种写法的关键点有两个依赖得分排序。如果候选索引不是按得分降序排列就会出现两个高分候选互相抑制的问题。所以调用 nms 前必须先对候选按 confidence 排序且把排序后的索引也传进去。线程只写自己的keep[idx]没有写冲突所以不需要原子操作。但实际实现里抑制结果可能还要记录“被哪个候选抑制”这时需要原子操作维护一个全局最大值表。3.4 调用 grasp_nms 的 Python 接口安装成功后导入并调用通常是这样import torch import grasp_nms scores torch.rand(20000, devicecuda, dtypetorch.float32) grasps torch.rand(20000, 6, devicecuda, dtypetorch.float32) # x,y,z,theta,w,v threshold 0.5 scores_sorted, indices torch.sort(scores, descendingTrue) grasps_sorted grasps[indices] keep grasp_nms.nms(grasps_sorted, scores_sorted, threshold)这里的nms函数一般接收三个参数候选抓取张量、置信度张量、重叠阈值。返回的是一个布尔掩码或索引列表。注意传入前一定要自己做好排序因为 CUDA kernel 内部可能默认输入已经按得分降序排列。如果不排序结果会非常奇怪而且很难排查。参数threshold的语义也很容易踩坑它不是 IoU 阈值而是“重合度”阈值。有的实现里距离小于该值的两个抓取视为重叠。这个值通常取 0.10.3 之间具体看抓取坐标系的量纲。如果坐标单位是毫米那么阈值 0.5 表示中心距离小于 0.5 毫米且角度差不大的抓取会被抑制。我建议把这个参数和抓取中心坐标的标准差绑定来调。3.5 与其他 NMS 变体的区别常见的 torchvision 的nms是 2D 边界框 NMS输入是(N,4)的框和得分计算 IoU 之后抑制。grasp_nms 的输入维度更高且相似度指标不是 IoU。另一个容易混淆的是torch.ops.torchvision.nms只支持 CPU/GPU 的矩形框。如果把抓取中心点和抓取角度直接塞进 2D NMS会导致同一位置不同角度的抓取被错误地抑制掉。这一点在理解 grasp_nms 的价值时很关键它不是把 3D 问题降维而是在真正的六维流形上做邻域搜索。4. 实战把 grasp_nms 集成到抓取检测管线4.1 数据流与调用时机在实际的抓取检测管线里模型输出的是原始候选接下来通常会做两件事先根据置信度过滤一部分再用 NMS 压缩。grasp_nms 的调用时机在两端如果候选太多比如大于 10000先做一个粗糙阈值过滤把 score 低于 0.3 的丢掉然后对剩下的跑 NMS。如果候选本来就少比如几百个直接跑 NMS 也能很快。下面是一段完整的推理片段模拟从模型输出到最终抓取列表import torch import grasp_nms def select_grasps(model_output, threshold0.2, score_thresh0.3): # 假设 model_output 是 (N, 7) 的 tensor # 前6维是抓取参数最后一维是置信度 scores model_output[:, -1] grasps model_output[:, :6].contiguous() # 1. 低分过滤 mask scores score_thresh grasps grasps[mask] scores scores[mask] # 2. 按分数降序排序 scores_sorted, order torch.sort(scores, descendingTrue) grasps_sorted grasps[order] # 3. 调用 grasp_nms keep grasp_nms.nms(grasps_sorted, scores_sorted, threshold) return grasps_sorted[keep], scores_sorted[keep]这段代码的一个细节grasps.contiguous()很重要。因为经过 mask 和 index 操作之后tensor 在内存中可能不连续而 CUDA kernel 往往要求输入是 contiguous 的。如果忘了你会收到类似RuntimeError: CUDA error: an illegal memory access was encountered的报错这种错误很难定位。4.2 参数调优threshold 和 score_thresh 的配合threshold直接决定输出抓取数量与多样性之间的平衡。下表给出了一个典型调参参考假设抓取中心坐标单位为毫米threshold保留数量/10000候选效果0.1约 1500几乎不过滤仅去除完全重合的抓取0.3约 400保留主要姿态适合后续精评估0.5约 120只剩非常稀疏的候选可能漏掉好的抓握点0.7约 30只保留全局最突出的几个适合快速执行我的经验是如果下游还有抓取质量评估模块threshold取 0.20.3 比较合适如果直接交给机械臂执行可以取 0.5减少姿态切换次数。另一个容易被忽视的是score_thresh对 NMS 结果影响很大。如果低分先被过滤那么高分候选之间可能没有重叠NMS 几乎不工作。所以保留较多候选再 NMS 往往比先过滤更合理。但候选太多会让 NMS 变慢因为并行块数量受限。grasp_nms 如果用固定线程块处理最大候选数可能限制在 8192 或 16384。在调用前可以打印一下print(candidate num:, grasps.shape[0]) assert grasps.shape[0] 20000如果超过上限需要分批 NMS或者先用均匀采样降低候选数。4.3 排错输出 keep 全为 True 或全为 False遇到 keep 全 True多半是 threshold 设得过小或者阈值语义理解反了。有些目光实现里 threshold 是“最小距离”分数最高的候选会抑制所有距离小于 threshold 的候选如果 threshold 远小于数据尺度自然几乎不抑制。验证手段是把候选之间的最小距离打印出来# 采样前500个计算距离矩阵 sub grasps_sorted[:500, :3] dist torch.cdist(sub, sub) print(min dist:, dist[dist 0].min())然后看 threshold 是不是比这个最小值还小很多。遇到全 False则要检查是否传入了未排序的数据。如果 indices 排序后没有把 grasps 重排列那么第一个“最高分”候选实际不是最高分它会把一个较低分的候选当作高价导致误抑制。这也是最常见的使用错误。我建议在调用前写个断言assert (scores_sorted scores_sorted.sort(descendingTrue).values).all()或者直接检查前 10 个分数是否递减。如果发现没有递减就说明排序逻辑没接好。5. 进阶NMS 效果验证与批量场景封装5.1 用重复生成法验证 grasp_nms 接口正确性NMS 没有 ground truth但可以用“重复叠加”验证实现是否符合预期。构造一组完全相同的抓取加上少量不同抓取跑 NMS 后应该每组重复抓取只保留一个。用 Python 写这个校验base torch.rand(10, 6, devicecuda) dup torch.cat([base, base.clone()], dim0) # 每个抓取重复两次 scores torch.rand(20, devicecuda) scores_sorted, _ torch.sort(scores, descendingTrue) keep grasp_nms.nms(dup, scores_sorted, threshold0.01) # 期望 keep 数量约为10 print(kept:, keep.sum().item())这里 threshold 设置得很小0.01因为 base 里复制出来的抓取完全一样距离为 0任何正阈值都应该抑制其中一个。跑通这个测试基本可以确认库的安装和基本接口没有问题。我每次换了新环境都会先跑一次这个省得后面把时间浪费在定位环境问题上。5.2 性能基准候选数量与耗时为了知道 grasp_nms 到底多快可以在固定环境里统计不同候选数的耗时。在 10000 候选下纯 Python 的 for 循环 NMS 可能需要十几秒grasp_nms 通常在毫秒级。实测 1000、5000、10000 候选的耗时大致如下表候选数grasp_nms 耗时ms10000.450001.3100003.1200008.7这个增长趋势主要来自线程块之间对全局标记数组的读写竞争。如果应用场景要求每秒处理多帧点云这个时间完全可接受。需要注意的是这个表只是同环境下的参考不同 GPU 和 CUDA 版本会差出两倍以上。5.3 批量场景下的调用封装如果你同时处理一个 batch 里多张点云不同场景之间的抓取候选不应该互相抑制。grasp_nms 如果只接收一个二维张量无法区分 batch。常见做法是循环每个 sample 单独调用batch 尺寸小比如 4时不影响性能。我一般写一个简单的 wrapperdef batched_nms(batched_grasps, batched_scores, threshold): keeps [] for g, s in zip(batched_grasps, batched_scores): s_sorted, order torch.sort(s, descendingTrue) g_sorted g[order] keep grasp_nms.nms(g_sorted, s_sorted, threshold) keeps.append(keep) return keeps这个 wrapper 避免了候选中混入不同场景导致互相抑制。另外要记住阈值必须和坐标量纲匹配。如果你的抓取中心是相对于相机坐标系坐标范围从 -1 到 1threshold 的取值就得按 0.010.1 去调。如果抓取中心是相对于物体坐标系可能取值范围更大。每个项目都要根据实际数据分布重新验证不能直接把别的项目的 threshold 拿过来用。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。