Post-processing¶
--set post=15 optionsA post-processing step removes, re-orders or corrects predicted grasps before an evaluator or a
robot gets them. Steps are chained, and every method returns the same GraspGroup, so every
step applies to every method.
Use it¶
grasppanda compose graspnet_baseline/graspnet-baseline_pointnet2_g1b-realsense_18e \
--set post=nms -o graspnet-baseline_nms.yaml
grasppanda compose graspnet_baseline/graspnet-baseline_pointnet2_g1b-realsense_18e --list post
grasppanda compose gsnet/gsnet_sunet14_g1b-realsense_10e \
--set post=nms -o gsnet_nms.yaml
grasppanda compose gsnet/gsnet_sunet14_g1b-realsense_10e --list post
grasppanda compose scale_balanced_grasp/sbg_pointnet2-transformer_g1b-realsense_18e \
--set post=nms -o scale-balanced-grasp_nms.yaml
grasppanda compose scale_balanced_grasp/sbg_pointnet2-transformer_g1b-realsense_18e --list post
grasppanda compose economicgrasp/economicgrasp_sunet14_g1b-realsense_10e \
--set post=nms -o economicgrasp_nms.yaml
grasppanda compose economicgrasp/economicgrasp_sunet14_g1b-realsense_10e --list post
grasppanda compose finegrasp/finegrasp_sunet14_g1b-realsense_10e \
--set post=nms -o finegrasp_nms.yaml
grasppanda compose finegrasp/finegrasp_sunet14_g1b-realsense_10e --list post
grasppanda compose hggd/hggd_r34w8_g1b-realsense_15e \
--set post=nms -o hggd_nms.yaml
grasppanda compose hggd/hggd_r34w8_g1b-realsense_15e --list post
The first command writes a checked config; the second lists every option of the slot under that method, with what was measured. Then grasppanda probe <file> tries the config on real frames and grasppanda train <file> trains it (Swap a component). Each method page lists the same: GraspNet-baseline · GSNet · Scale-Balanced-Grasp · EconomicGrasp · FineGrasp · HGGD.
A chain joins steps with + and sets a main option with =: --set post=collision+nms+top-k=20.
grasppanda test, infer and demo take the same steps as --post collision nms top-k=20, on
weights you already have.
Options¶
A probe shows that a combination trains on real frames of GraspNet-1Billion, not how good it is. Accuracy is claimed only for a method trained and evaluated here.
What the columns say, and who is left out
- Runs: out of the methods the option is a swap under, those under which its probe trained on real frames without error. A method that ships the option is named own and not counted; a method still undecided is counted and named. Measured on 20 training iterations on 8 RealSense frames of GraspNet-1Billion (one scene), seed 0, one run per cell; nothing was measured on GraspClutter6D or on Kinect frames.
- Options have no level 2: post-processing trains nothing: it changes predictions, not the training.
- How we measure defines every word of these columns.
Sources and extras
The work each option follows; extra: the optional install it needs (every extra).
| Source | Options |
|---|---|
Generic; filter_approach_direction of GPD (ten Pas et al., arXiv 1706.09911), approach_steering of the AnyGrasp SDK, the gravity cone of ThinkGrasp (github.com/H-Freax/ThinkGrasp) |
approach-cone |
| Generic, written here from the description in AnyGrasp (Fang et al., arXiv 2212.08333, section IV-D) | center-gripper |
| Collision filter of the GraspNet-baseline and HGGD test scripts | collision, collision-strict |
| Generic, on grasppanda.ops.furthest_point_sample; the idea is the grasp selection of Contact-GraspNet (Sundermeyer et al., ICRA 2021) | furthest-sampling |
Generic; max_gripper_width of the AnyGrasp SDK, min_aperture/max_aperture of GPD (arXiv 1706.09911) |
gripper-limits |
Bridge to trimesh (Dawson-Haggerty et al., github.com/mikedh/trimesh, MIT): mesh loading, body split, watertightness, convex hull and box primitives; the point-cloud-against-gripper-mesh check of GraspGen (Murali et al., arXiv 2507.13097) as a containment test · extra trimesh |
mesh-collision |
| Grasp NMS rule of graspnetAPI | nms |
Generic; choose_fn: better_than_threshold of 6-DOF GraspNet (ICCV 2019) |
score-threshold |
| Generic; the table test of the ZeroGrasp collision detector (Iwase et al., CVPR 2025) with a plane fitted to the cloud instead of a calibrated table frame | table-plane |
Generic; filter_grasps of Contact-GraspNet (Sundermeyer et al., ICRA 2021) and apply_object_mask of the AnyGrasp SDK |
target-mask |
| Generic; GraspGroup.transform with a calibration from the config | to-base-frame |
| Generic; the top-k step of AnyGrasp (Fang et al., T-RO, arXiv 2212.08333, section IV-D) and of GPD | top-k |
| Generic; the idea of the contact-based width refinement of ZeroGrasp (Iwase et al., arXiv 2504.10857), on the sensor cloud alone | width-from-cloud |
| Generic; the workspace limits of GPD (ten Pas et al., arXiv 1706.09911) and AnyGrasp SDK | workspace |
Choosing¶
- Order matters. Steps that change a pose or a width (
center-gripper,width-from-cloud) go before a collision filter; selection (top-k,furthest-sampling) after the filters;to-base-framelast. - Three collision tests:
collisionon the full cloud,collision-stricton the cloud the network saw,mesh-collisionwith the mesh of your own gripper. - A robot wants a few good grasps:
top-k=20orfurthest-sampling=20after the filters. The benchmark evaluators apply their own suppression and top 50. - Scores differ between methods. Prefer
score-thresholdwithmode: quantileorrelativeto an absolute limit. - A target object needs your segmentation on the cloud:
target-mask, from Python (below).
Details¶
Input and output
class PostProcessor:
def __call__(self, grasps: GraspGroup, observation: Observation) -> GraspGroup: ...
grasps |
the grasps of one frame: translation (G, 3) in metres in grasps.frame (camera: x right, y down, z forward); rotation (G, 3, 3) with column 0 the approach axis and column 1 the closing axis; width, depth, score (its scale depends on the method); G may be 0. |
observation |
the batch the grasps came from: points, the cloud the network saw; full_points, the cloud before sub-sampling, when the dataset keeps it; masks, named masks over points (no shipped dataset fills them); images when the method reads them. Never ground truth. |
| returns | a GraspGroup of the same frame: fewer grasps, another order, or a changed pose, width or frame. |
grasppanda test scores every variant of evaluation.variants separately (a variant that
removes grasps is reported beside raw, never instead of it); grasppanda infer,
grasppanda demo and GraspInferencer apply a chain too.
- A step that reads a cloud needs the grasps in the cloud's frame, so
to-base-framemust be last; a step after it that reads the cloud stops with aContractError. - What a step needs from the observation is checked when it runs, with a message that names the missing input and how to supply it.
Three more ways to add steps
On a command line, with weights you have; steps run left to right,
name:option=value,option=value sets any option:
grasppanda demo <method config> --weights <file> --depth depth.png \
--intrinsics 927.17,927.37,651.32,349.62 --depth-scale 1000 \
--post center-gripper width-from-cloud collision table-plane approach-cone=45 \
gripper-limits=0.08 nms furthest-sampling=20
In a config, as a variant of its own (raw stays empty):
evaluation:
variants:
robot:
- {type: GripperCentering, max_shift: 0.02}
- {type: WidthFromCloud, clearance: 0.01, mode: shrink}
- {type: ModelFreeCollisionFilter, voxel_size: 0.01, collision_threshold: 0.01}
- {type: TablePlaneFilter, clearance: 0.005}
- {type: ApproachAngleFilter, direction: table_normal, max_angle: 60}
- {type: GripperLimits, max_width: 0.08}
- {type: GraspNMS}
- {type: TopK, k: 20}
- {type: ToFrame, transform: calibration/T_base_camera.txt, frame: base, origin: fingertips}
In Python, with a target object:
from grasppanda.apis import GraspInferencer
from grasppanda.structures import Mask, Provenance
inferencer = GraspInferencer("<method config>", weights="<file>")
grasps = inferencer(depth=depth, intrinsics=K, depth_scale=1000, post="collision+nms+top-k=20")
observation = inferencer.observe(depth=depth, intrinsics=K, depth_scale=1000)
observation.masks["target"] = Mask(on_target, Provenance.PREDICTED) # (N,) bool over observation.points
grasps = inferencer.predict(observation, post="target-mask+collision+top-k=10")
The settings that matter
| Option | Main setting (name=value) |
How to choose |
|---|---|---|
top-k |
k (50) |
50 is what the GraspNet-1Billion evaluator scores; a robot that tries candidates in order needs 10 to 20. |
score-threshold |
threshold (0.5, mode: quantile) |
quantile and relative need no knowledge of the score scale; absolute is tuned per method. min_keep=1 keeps the best grasp whatever its score. |
furthest-sampling |
num (50) |
candidates (300) is how far down the ranking the spread may reach. |
nms |
none: nms:translation_threshold=0.05 |
0.03 m and 30° are the benchmark rule. |
workspace |
z ([0.2, 1.0]) |
workspace:x=[-0.3,0.3],y=[-0.2,0.2],z=[0.3,0.8]; with transform the box is in the robot base frame. |
approach-cone |
max_angle (60°) |
top-down picking: 30 to 45; direction=camera for a camera looking straight down; direction=[x,y,z] for a shelf. |
table-plane |
clearance (0.005 m) |
give plane=[a,b,c,d] when the table is not the largest plane in view. |
target-mask |
mask (target) |
max_distance: 0.005 m when grasp points are cloud points (seed-based methods); HGGD computes centres and needs 0.01 to 0.02. invert=true avoids an object. |
gripper-limits |
max_width (0.08 m) |
Franka hand 0.08, Robotiq 2F-85 0.085, 2F-140 0.14. |
center-gripper |
max_shift (0.02 m) |
before a collision filter. |
width-from-cloud |
clearance (0.01 m) |
mode=shrink never widens; fit also widens. Before a collision filter. |
collision |
none | needs the full cloud; compose switches keep_full_points on for the evaluation splits where a dataset drops it, and says so. |
collision-strict |
none | one point rejects; reads the network's cloud, so it runs everywhere. |
mesh-collision |
mesh (none: built-in boxes) |
see below. max_points 2 or 3 tolerates depth noise. |
to-base-frame |
transform |
replace the placeholder with your calibration; origin=fingertips gives a tool centre point. Last step. |
A mesh of your own gripper
- type: GripperMeshCollisionFilter
mesh: meshes/my_gripper_collision.stl # the open gripper, any format trimesh reads
scale: 0.001 # the file is in millimetres
mesh_transform: # file coordinates -> tool frame
[[0, 0, 1, 0], [0, 1, 0, 0], [-1, 0, 0, 0], [0, 0, 0, 1]]
bodies: exact # or convex: hull of every body, faster
max_points: 2
The tool frame has its origin between the fingertips, x along the approach, y along the
closing direction, z along the finger height, in metres. Points between the fingers do not
count; a neighbour an open finger would hit does. The mesh is tested as it is, with no
approach sweep: put it after collision, not in its place. Install the extra trimesh.
Adding your own step
A class in grasppanda/models/postprocessors/, registered with
@POSTPROCESSORS.register(), with __call__(grasps, observation) -> GraspGroup, explicit
constructor options, status, source and, for the name=value shorthand, primary.
Helpers for the cloud of a frame, points in gripper frames, a rigid transform and a plane fit
are in grasppanda/models/postprocessors/scene.py. Add a preset under
grasppanda/presets/post/ and it appears in --list post and in the probed grid.
Pitfalls met while building this
collisionneeds the cloud before sub-sampling, which some dataset sections drop.composewriteskeep_full_points: trueinto the evaluation splits and says so; a config written by hand without it is refused, naming the section.collision:cloud=pointstests the cloud the network saw instead.- A full cloud is half a million points. The refiners down-sample to 5 mm voxels first;
WidthFromCloudadds half a voxel per side, so it errs wide. table-planedepends on what the cloud holds. A whole view with the table and a cloud cropped around the objects give different planes; look at the picture ofgrasppanda demobefore trusting a clearance.- A fitted table is the largest plane. Below
min_inliers(10 % of the cloud) the step refuses and asks forplane; the camera must be above the table. - trimesh's own containment test needs
rtreeorembreex, which its wheel does not bring; the filter tests containment itself. - A probe trains for a few iterations, so its post-processing record shows that a step runs on real predictions under a method, not what it does to a trained one.
- Heads predict wide openings.
gripper-limitscan remove many grasps;width-from-cloudbefore it narrows the ones the cloud shows to be narrower. - The demo's picture is in the camera frame; after
to-base-framethe printed table andgrasps.npyare in the new frame.
Not wrapped
| Candidate | Why not |
|---|---|
| GraRe re-ranker (grare, MIT) | a learned re-ranker per detector and camera with three more models; it needs a second score per grasp on GraspGroup first. |
| GraspGen-X collision filter and discriminator | needs an instance mask per grasp, or scores single objects in another gripper convention; mesh-collision covers the use without one. |
| ZeroGrasp contact refinement | CC BY-NC 4.0; its two ideas that work on a sensor cloud are width-from-cloud and table-plane, written independently. |
| AnyGrasp SDK filters | a licensed binary; its documented steps are here as generic filters. |
Contact-GraspNet select_grasps, SceneCollisionNet, CabiNet |
NVIDIA non-commercial licences; the selection idea is furthest-sampling. |
| Soft-NMS, score fusion, score calibration | they change scores, which are tied to a method's scale. |
| cuRobo or RM4D reachability | needs a robot description; workspace with transform is the cheap stand-in. |