Skip to content

Post-processing

All 6 methods--set post=15 options

A 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.

Option What it is Runs
approach-cone Keep grasps that approach within 60 degrees of the table normal: from above. 6/6
center-gripper Shift each grasp along its closing axis to the middle of what is between the fingers. 6/6
collision Model-free collision filter on the full cloud: drop a grasp whose gripper would hit observed points. 6/6
collision-strict The strict collision filter of HGGD: one point in a gripper box rejects a grasp. 6/6
furthest-sampling Pick 50 grasps spread over the scene by furthest point sampling of grasp points. 6/6
gripper-limits Remove grasps wider than 0.08 m, the opening of a Franka Emika hand. 6/6
mesh-collision Remove grasps at which the open gripper, as a mesh, would contain scene points. 6/6
nms Grasp non-maximum suppression: one grasp per 3 cm and 30 degrees, best first. 6/6
score-threshold Keep the better half of the grasps of a frame by score. 6/6
table-plane Remove grasps whose gripper box would reach below the table plane. 6/6
target-mask Keep the grasps on a target object: within 1 cm of a point of the mask target. no record
reads observation.masks, which no shipped dataset fills
to-base-frame Express the grasps in the robot base frame, with the grasp point at the fingertips. 6/6
top-k Keep the 50 best-scoring grasps of a frame, best first. 6/6
width-from-cloud Narrow each opening to the extent of the scene between the fingers plus 1 cm a side. 6/6
workspace Keep grasps whose grasp point is between 0.2 m and 1.0 m in front of the camera. 6/6
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

Compare under every method

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-frame last.
  • Three collision tests: collision on the full cloud, collision-strict on the cloud the network saw, mesh-collision with the mesh of your own gripper.
  • A robot wants a few good grasps: top-k=20 or furthest-sampling=20 after the filters. The benchmark evaluators apply their own suppression and top 50.
  • Scores differ between methods. Prefer score-threshold with mode: quantile or relative to 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-frame must be last; a step after it that reads the cloud stops with a ContractError.
  • 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
  • collision needs the cloud before sub-sampling, which some dataset sections drop. compose writes keep_full_points: true into the evaluation splits and says so; a config written by hand without it is refused, naming the section. collision:cloud=points tests the cloud the network saw instead.
  • A full cloud is half a million points. The refiners down-sample to 5 mm voxels first; WidthFromCloud adds half a voxel per side, so it errs wide.
  • table-plane depends 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 of grasppanda demo before trusting a clearance.
  • A fitted table is the largest plane. Below min_inliers (10 % of the cloud) the step refuses and asks for plane; the camera must be above the table.
  • trimesh's own containment test needs rtree or embreex, 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-limits can remove many grasps; width-from-cloud before it narrows the ones the cloud shows to be narrower.
  • The demo's picture is in the camera frame; after to-base-frame the printed table and grasps.npy are 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.