Parallel-Gripper Grasp Generation#
The GraspKit toolkit generates feasible grasp poses for parallel-jaw grippers from a target object’s triangle mesh. It supports programmatic antipodal sampling, browser-based grasp-region annotation, collision filtering, candidate ranking, and on-disk caching of sampled contact pairs.
This page also demonstrates how to execute a generated pose with a robot arm. It covers scene initialization, robot and object creation, grasp pose computation, and trajectory execution in the simulation loop.
Processing Pipeline#
Grasp generation has three stages:
Sample surface points and find antipodal contact pairs on the full mesh or an annotated region.
Construct 6-DoF grasp frames that align the gripper opening axis with each contact pair and respect the requested approach direction.
Remove candidates that collide with the object or ground, rank the remaining poses, and return the best candidates with their required opening lengths.
Tutorial Source#
The tutorial corresponds to the grasp_generator.py script in the scripts/tutorials/grasp directory.
Code for grasp_generator.py
1# ----------------------------------------------------------------------------
2# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15# ----------------------------------------------------------------------------
16
17"""
18This script demonstrates the creation and simulation of a robot that grasps a rigid mug
19in a simulated environment using the SimulationManager and grasp planning utilities.
20"""
21
22from __future__ import annotations
23
24import argparse
25import time
26from embodichain.cli.sim import add_sim_args_to_parser
27
28
29def build_parser() -> argparse.ArgumentParser:
30 """Build CLI options without initializing simulation resources."""
31 parser = argparse.ArgumentParser(
32 description="Create and simulate a robot in SimulationManager"
33 )
34 add_sim_args_to_parser(parser)
35 return parser
36
37
38def parse_arguments(argv: list[str] | None = None) -> argparse.Namespace:
39 """
40 Parse command-line arguments to configure the simulation.
41
42 Returns:
43 argparse.Namespace: Parsed arguments including number of environments and rendering options.
44 """
45 parser = build_parser()
46 return parser.parse_args() if argv is None else parser.parse_args(argv)
47
48
49if __name__ == "__main__":
50 # Parse before importing optional simulation/planning dependencies.
51 _cli_args = parse_arguments()
52
53
54import numpy as np
55import torch
56
57from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
58from embodichain.lab.visualization import visualization_cfg_from_args
59from embodichain.lab.sim.objects import Robot, RigidObject
60from embodichain.compute.trajectory import interpolate_with_distance
61from embodichain.lab.sim.shapes import MeshCfg, MeshCollisionCfg
62from embodichain.lab.sim.motion.solvers import URSolverCfg
63from embodichain.data import get_data_path
64from embodichain.toolkits.graspkit import ParallelJawGripperModelCfg
65from embodichain.utils import logger
66from embodichain.lab.sim.cfg import (
67 RenderCfg,
68 physics_cfg_for_backend,
69 JointDrivePropertiesCfg,
70 RobotCfg,
71 LightCfg,
72 RigidBodyPhysicsCfg,
73 RigidObjectCfg,
74 URDFCfg,
75)
76from embodichain.toolkits.graspkit.pg_grasp import (
77 AntipodalGraspPoseGenerator,
78 AntipodalGraspPoseGeneratorCfg,
79 GraspAnnotationCfg,
80 ParallelJawGraspCollisionCfg,
81)
82
83
84def initialize_simulation(args) -> SimulationManager:
85 """
86 Initialize the simulation environment based on the provided arguments.
87
88 Args:
89 args (argparse.Namespace): Parsed command-line arguments.
90
91 Returns:
92 SimulationManager: Configured simulation manager instance.
93 """
94 config = SimulationManagerCfg(
95 headless=True,
96 device=args.device,
97 render_cfg=RenderCfg(renderer=args.renderer),
98 physics_cfg=physics_cfg_for_backend(args.physics),
99 physics_dt=1.0 / 100.0,
100 arena_space=2.5,
101 visualization=visualization_cfg_from_args(args),
102 )
103 sim = SimulationManager(config)
104
105 light = sim.add_light(
106 cfg=LightCfg(
107 uid="main_light",
108 color=(0.6, 0.6, 0.6),
109 intensity=30.0,
110 init_pos=(1.0, 0, 3.0),
111 )
112 )
113
114 return sim
115
116
117def create_robot(sim: SimulationManager, position=[0.0, 0.0, 0.0]) -> Robot:
118 """
119 Create and configure a robot with an arm and a dexterous hand in the simulation.
120
121 Args:
122 sim (SimulationManager): The simulation manager instance.
123
124 Returns:
125 Robot: The configured robot instance added to the simulation.
126 """
127 # Retrieve URDF paths for the robot arm and hand
128 ur10_urdf_path = get_data_path("UniversalRobots/UR10/UR10.urdf")
129 gripper_urdf_path = get_data_path("DH_PGC_140_50_M/DH_PGC_140_50_M.urdf")
130 # Configure the robot with its components and control properties
131 cfg = RobotCfg(
132 uid="UR10",
133 urdf_cfg=URDFCfg(
134 components=[
135 {"component_type": "arm", "urdf_path": ur10_urdf_path},
136 {"component_type": "hand", "urdf_path": gripper_urdf_path},
137 ]
138 ),
139 joint_drive_props=JointDrivePropertiesCfg(
140 stiffness={"Joint[0-9]": 1e4, "FINGER[1-2]": 1e3},
141 damping={"Joint[0-9]": 1e3, "FINGER[1-2]": 1e2},
142 max_effort={"Joint[0-9]": 1e5, "FINGER[1-2]": 1e4},
143 drive_type="force",
144 ),
145 control_parts={
146 "arm": ["Joint[0-9]"],
147 "eef": ["FINGER[1-2]"],
148 },
149 solver_cfg={
150 "arm": URSolverCfg(
151 ur_type="ur10",
152 tcp=[
153 [0.0, 1.0, 0.0, 0.0],
154 [-1.0, 0.0, 0.0, 0.0],
155 [0.0, 0.0, 1.0, 0.14],
156 [0.0, 0.0, 0.0, 1.0],
157 ],
158 )
159 },
160 init_qpos=[0.0, -np.pi / 2, -np.pi / 2, np.pi / 2, -np.pi / 2, 0.0, 0.0, 0.0],
161 init_pos=position,
162 )
163 return sim.add_robot(cfg=cfg)
164
165
166def create_obj(sim: SimulationManager):
167 mug_cfg = RigidObjectCfg(
168 uid="table",
169 shape=MeshCfg(
170 fpath=get_data_path("BakeTextureObj/hdr_color_mesh.ply"),
171 collision=MeshCollisionCfg(
172 approximation="convex_decomposition",
173 max_hulls=16,
174 acd_method="coacd",
175 ),
176 ),
177 attrs=RigidBodyPhysicsCfg.from_dict(
178 {
179 "mass_props": {"mass": 0.01},
180 "material_props": {"dynamic_friction": 0.97, "static_friction": 0.99},
181 }
182 ),
183 init_pos=[0.55, 0.0, 0.08],
184 init_rot=[0.0, 0.0, 0.0],
185 )
186 mug = sim.add_rigid_object(cfg=mug_cfg)
187 return mug
188
189
190def get_grasp_traj(sim: SimulationManager, robot: Robot, grasp_xpos: torch.Tensor):
191 num_envs = sim.num_envs
192 rest_arm_qpos = robot.get_qpos("arm")
193
194 approach_xpos = grasp_xpos.clone()
195 approach_xpos[:, 2, 3] += 0.1
196
197 _, qpos_approach = robot.compute_ik(
198 pose=approach_xpos, joint_seed=rest_arm_qpos, name="arm"
199 )
200 _, qpos_grasp = robot.compute_ik(
201 pose=grasp_xpos, joint_seed=qpos_approach, name="arm"
202 )
203 hand_open_qpos = torch.tensor([0.00, 0.00], dtype=torch.float32, device=sim.device)
204 hand_close_qpos = torch.tensor(
205 [0.025, 0.025], dtype=torch.float32, device=sim.device
206 )
207
208 arm_trajectory = torch.cat(
209 [
210 rest_arm_qpos[:, None, :],
211 qpos_approach[:, None, :],
212 qpos_grasp[:, None, :],
213 qpos_grasp[:, None, :],
214 qpos_approach[:, None, :],
215 rest_arm_qpos[:, None, :],
216 ],
217 dim=1,
218 )
219 hand_trajectory = torch.cat(
220 [
221 hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
222 hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
223 hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
224 hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
225 hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
226 hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
227 ],
228 dim=1,
229 )
230 all_trajectory = torch.cat([arm_trajectory, hand_trajectory], dim=-1)
231 interp_trajectory = interpolate_with_distance(
232 trajectory=all_trajectory, interp_num=200, device=sim.device
233 )
234 return interp_trajectory
235
236
237if __name__ == "__main__":
238 import time
239
240 args = _cli_args
241 sim = initialize_simulation(args)
242 robot = create_robot(sim, position=[0.0, 0.0, 0.0])
243 obj = create_obj(sim)
244 sim.prepare()
245
246 # get mug grasp pose
247 if not args.headless:
248 sim.open_window()
249
250 # Annotate part of the mug to be grasped by following the instructions in the visualization window:
251 # 1. View grasp object in browser (e.g http://localhost:11801)
252 # 2. press 'Rect Select Region', select grasp region
253 # 3. press 'Confirm Selection' to finish grasp region selection.
254
255 start_time = time.time()
256
257 # Construct one standalone generator. The same instance can also be passed
258 # to AtomicActionEngine(grasp_pose_generators={"hand": grasp_generator}).
259 grasp_generator = AntipodalGraspPoseGenerator(
260 ParallelJawGripperModelCfg(
261 model_id="tutorial_parallel_jaw",
262 min_opening_width=0.003,
263 max_opening_width=0.088,
264 finger_length=0.078,
265 ),
266 algorithm_cfg=AntipodalGraspPoseGeneratorCfg(
267 sample_count=10_000,
268 max_candidates=30,
269 ),
270 collision_cfg=ParallelJawGraspCollisionCfg(
271 point_sample_density=0.012,
272 filter_ground_collision=True,
273 ),
274 annotation_cfg=GraspAnnotationCfg(
275 selection_mode="whole_mesh",
276 viser_port=11801,
277 ),
278 )
279
280 # Extract target-local geometry. The generator, rather than the target,
281 # owns all algorithm and end-effector parameters.
282 vertices = obj.get_vertices(env_ids=[0], scale=True)[0]
283 triangles = obj.get_triangles(env_ids=[0])[0]
284
285 # Compute grasp poses per environment
286 approach_direction = torch.tensor(
287 [0, 0, -1], dtype=torch.float32, device=sim.device
288 )
289 obj_poses = obj.get_local_pose(to_matrix=True)
290 rest_xpos = robot.compute_fk(
291 qpos=robot.get_qpos("arm"), name="arm", to_matrix=True
292 )[0]
293 grasp_success, grasp_xpos, _ = grasp_generator.get_best_grasp_poses(
294 mesh_vertices=vertices,
295 mesh_triangles=triangles,
296 obj_poses=obj_poses,
297 approach_direction=approach_direction,
298 )
299 if not bool(grasp_success.all().item()):
300 logger.log_warning("At least one environment has no valid grasp pose.")
301 grasp_xpos = torch.where(
302 grasp_success[:, None, None],
303 grasp_xpos,
304 rest_xpos.expand_as(grasp_xpos),
305 )
306 cost_time = time.time() - start_time
307 logger.log_info(f"Get grasp pose cost time: {cost_time:.2f} seconds")
308
309 grab_traj = get_grasp_traj(sim, robot, grasp_xpos)
310 input("Press Enter to start the grab mug demo...")
311 n_waypoint = grab_traj.shape[1]
312 for i in range(n_waypoint):
313 robot.set_qpos(grab_traj[:, i, :])
314 sim.update(step=4)
315 time.sleep(1e-2)
316 input("Press Enter to exit the simulation...")
Tutorial Walkthrough#
Configuring the simulation#
Command-line arguments are parsed with argparse to select the number of parallel environments, the compute device, and optional rendering features such as renderer backend and headless mode.
The parsed arguments are passed to initialize_simulation, which builds a SimulationManagerCfg and creates the SimulationManager instance. When ray tracing is enabled a directional cfg.LightCfg is also added to the scene.
def initialize_simulation(args) -> SimulationManager:
"""
Initialize the simulation environment based on the provided arguments.
Args:
args (argparse.Namespace): Parsed command-line arguments.
Returns:
SimulationManager: Configured simulation manager instance.
"""
config = SimulationManagerCfg(
headless=True,
device=args.device,
render_cfg=RenderCfg(renderer=args.renderer),
physics_cfg=physics_cfg_for_backend(args.physics),
physics_dt=1.0 / 100.0,
arena_space=2.5,
visualization=visualization_cfg_from_args(args),
)
sim = SimulationManager(config)
light = sim.add_light(
cfg=LightCfg(
uid="main_light",
color=(0.6, 0.6, 0.6),
intensity=30.0,
init_pos=(1.0, 0, 3.0),
)
)
return sim
Creating a robot and a target object#
A UR10 arm with a parallel-jaw gripper is created via SimulationManager.add_robot(). The gripper URDF and drive properties are configured so that the arm joints and finger joints can be controlled independently.
def create_robot(sim: SimulationManager, position=[0.0, 0.0, 0.0]) -> Robot:
"""
Create and configure a robot with an arm and a dexterous hand in the simulation.
Args:
sim (SimulationManager): The simulation manager instance.
Returns:
Robot: The configured robot instance added to the simulation.
"""
# Retrieve URDF paths for the robot arm and hand
ur10_urdf_path = get_data_path("UniversalRobots/UR10/UR10.urdf")
gripper_urdf_path = get_data_path("DH_PGC_140_50_M/DH_PGC_140_50_M.urdf")
# Configure the robot with its components and control properties
cfg = RobotCfg(
uid="UR10",
urdf_cfg=URDFCfg(
components=[
{"component_type": "arm", "urdf_path": ur10_urdf_path},
{"component_type": "hand", "urdf_path": gripper_urdf_path},
]
),
joint_drive_props=JointDrivePropertiesCfg(
stiffness={"Joint[0-9]": 1e4, "FINGER[1-2]": 1e3},
damping={"Joint[0-9]": 1e3, "FINGER[1-2]": 1e2},
max_effort={"Joint[0-9]": 1e5, "FINGER[1-2]": 1e4},
drive_type="force",
),
control_parts={
"arm": ["Joint[0-9]"],
"eef": ["FINGER[1-2]"],
},
solver_cfg={
"arm": URSolverCfg(
ur_type="ur10",
tcp=[
[0.0, 1.0, 0.0, 0.0],
[-1.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.14],
[0.0, 0.0, 0.0, 1.0],
],
)
},
init_qpos=[0.0, -np.pi / 2, -np.pi / 2, np.pi / 2, -np.pi / 2, 0.0, 0.0, 0.0],
init_pos=position,
)
return sim.add_robot(cfg=cfg)
The target object (a mug) is loaded as a objects.RigidObject from a PLY mesh file:
def create_obj(sim: SimulationManager):
mug_cfg = RigidObjectCfg(
uid="table",
shape=MeshCfg(
fpath=get_data_path("BakeTextureObj/hdr_color_mesh.ply"),
collision=MeshCollisionCfg(
approximation="convex_decomposition",
max_hulls=16,
acd_method="coacd",
),
),
attrs=RigidBodyPhysicsCfg.from_dict(
{
"mass_props": {"mass": 0.01},
"material_props": {"dynamic_friction": 0.97, "static_friction": 0.99},
}
),
init_pos=[0.55, 0.0, 0.08],
init_rot=[0.0, 0.0, 0.0],
)
mug = sim.add_rigid_object(cfg=mug_cfg)
return mug
Annotating and computing grasp poses#
Grasp generation is performed by
AntipodalGraspPoseGenerator.
It implements the robot-independent
GraspPoseGenerator contract and the
shared
ParallelJawGraspPoseGenerator base for
two-finger parallel-jaw grippers.
The generator owns the gripper model, algorithm, collision, and annotation
configuration. Target-local mesh vertices and triangles are supplied to each
call. This separation lets a handwritten environment call the service directly
and lets an AtomicActionEngine install the same instance under
grasp_pose_generators={"hand": generator}. Scene affordances do not own a
live generator or robot-specific parameters.
For each environment,
get_best_grasp_poses()
returns a success flag, a (4, 4) world-frame grasp pose, and the required
opening width. Antipodal contact pairs are cached and reused automatically.
Set GraspAnnotationCfg.selection_mode="interactive" to select a partial
region through Viser, or use "whole_mesh" for unattended generation.
The approach direction is the unit vector along which the gripper approaches the object. In this tutorial, we use a fixed approach direction (straight down in world frame) for simplicity, but it can be customized based on the task or object geometry.
# Construct one standalone generator. The same instance can also be passed
# to AtomicActionEngine(grasp_pose_generators={"hand": grasp_generator}).
grasp_generator = AntipodalGraspPoseGenerator(
ParallelJawGripperModelCfg(
model_id="tutorial_parallel_jaw",
min_opening_width=0.003,
max_opening_width=0.088,
finger_length=0.078,
),
algorithm_cfg=AntipodalGraspPoseGeneratorCfg(
sample_count=10_000,
max_candidates=30,
),
collision_cfg=ParallelJawGraspCollisionCfg(
point_sample_density=0.012,
filter_ground_collision=True,
),
annotation_cfg=GraspAnnotationCfg(
selection_mode="whole_mesh",
viser_port=11801,
),
)
# Extract target-local geometry. The generator, rather than the target,
# owns all algorithm and end-effector parameters.
vertices = obj.get_vertices(env_ids=[0], scale=True)[0]
triangles = obj.get_triangles(env_ids=[0])[0]
# Compute grasp poses per environment
approach_direction = torch.tensor(
[0, 0, -1], dtype=torch.float32, device=sim.device
)
obj_poses = obj.get_local_pose(to_matrix=True)
rest_xpos = robot.compute_fk(
qpos=robot.get_qpos("arm"), name="arm", to_matrix=True
)[0]
grasp_success, grasp_xpos, _ = grasp_generator.get_best_grasp_poses(
mesh_vertices=vertices,
mesh_triangles=triangles,
obj_poses=obj_poses,
approach_direction=approach_direction,
)
if not bool(grasp_success.all().item()):
logger.log_warning("At least one environment has no valid grasp pose.")
grasp_xpos = torch.where(
grasp_success[:, None, None],
grasp_xpos,
rest_xpos.expand_as(grasp_xpos),
)
cost_time = time.time() - start_time
logger.log_info(f"Get grasp pose cost time: {cost_time:.2f} seconds")
Building and executing the grasp trajectory#
Once a grasp pose is obtained, a waypoint trajectory is built that moves the arm from its rest configuration to an approach pose (offset above the grasp), down to the grasp pose, closes the fingers, lifts, and returns. The trajectory is interpolated for smooth motion and executed step-by-step in the simulation loop.
def get_grasp_traj(sim: SimulationManager, robot: Robot, grasp_xpos: torch.Tensor):
num_envs = sim.num_envs
rest_arm_qpos = robot.get_qpos("arm")
approach_xpos = grasp_xpos.clone()
approach_xpos[:, 2, 3] += 0.1
_, qpos_approach = robot.compute_ik(
pose=approach_xpos, joint_seed=rest_arm_qpos, name="arm"
)
_, qpos_grasp = robot.compute_ik(
pose=grasp_xpos, joint_seed=qpos_approach, name="arm"
)
hand_open_qpos = torch.tensor([0.00, 0.00], dtype=torch.float32, device=sim.device)
hand_close_qpos = torch.tensor(
[0.025, 0.025], dtype=torch.float32, device=sim.device
)
arm_trajectory = torch.cat(
[
rest_arm_qpos[:, None, :],
qpos_approach[:, None, :],
qpos_grasp[:, None, :],
qpos_grasp[:, None, :],
qpos_approach[:, None, :],
rest_arm_qpos[:, None, :],
],
dim=1,
)
hand_trajectory = torch.cat(
[
hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
hand_open_qpos[None, None, :].repeat(num_envs, 1, 1),
hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
hand_close_qpos[None, None, :].repeat(num_envs, 1, 1),
],
dim=1,
)
all_trajectory = torch.cat([arm_trajectory, hand_trajectory], dim=-1)
interp_trajectory = interpolate_with_distance(
trajectory=all_trajectory, interp_num=200, device=sim.device
)
return interp_trajectory
Configuration#
Configuration ownership is split by meaning:
ParallelJawGripperModelCfgdescribes physical opening limits, finger dimensions, and palm depth. A concrete EEF name belongs only in itsmodel_id.AntipodalGraspPoseGeneratorCfgcontrols sample count, angular deviations, approach variants, and result count.ParallelJawGraspCollisionCfgcontrols collision margin, point density, decomposition cost, and ground filtering.GraspAnnotationCfgcontrols whole-mesh versus interactive selection, the Viser port, and explicit cache refresh.
Running the Tutorial#
To run the script, execute the following command from the project root:
python scripts/tutorials/grasp/grasp_generator.py
A simulation window will open showing the robot and the mug. The tutorial uses whole-mesh annotation by default and therefore does not require a browser.
You can customize the run with additional arguments:
python scripts/tutorials/grasp/grasp_generator.py --num_envs <n> --device <cuda/cpu> --renderer <auto|hybrid|fast-rt|rt> --headless
The script computes a grasp pose, prints the elapsed time, and then waits for you to press Enter before executing the full grasp trajectory. Press Enter again to exit once the motion is complete.
Grasp Annotation CLI#
EmbodiChain provides a dedicated CLI for interactively annotating grasp regions on a mesh and caching the resulting antipodal point pairs, without requiring a full simulation environment.
The CLI constructs the same AntipodalGraspPoseGenerator
used by tasks and calls its prepare_mesh()
method; it does not use a separate annotation-time generator.
Basic usage:
embodichain annotate-grasp --mesh_path /path/to/object.ply
This will:
Load the mesh file via
trimesh.Launch a browser-based annotator (default port
15531).Open
http://localhost:15531in your browser, use Rect Select Region to highlight the graspable area, then click Confirm Selection.Compute antipodal point pairs on the selected region and cache them to disk.
Common options:
embodichain annotate-grasp \
--mesh_path /path/to/object.ply \
--viser_port 15531 \
--n_sample 20000 \
--max_length 0.1 \
--min_length 0.001
Option |
Default |
Description |
|---|---|---|
|
(required) |
Path to the mesh file ( |
|
|
Port for the browser-based annotation UI. |
|
|
Number of surface points to sample for antipodal pair detection. |
|
|
Maximum distance (metres) between antipodal pairs; should match the gripper’s maximum opening width. |
|
|
Minimum distance (metres) between antipodal pairs; filters out degenerate pairs. |
|
|
Compute device ( |