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:

  1. Sample surface points and find antipodal contact pairs on the full mesh or an annotated region.

  2. Construct 6-DoF grasp frames that align the gripper opening axis with each contact pair and respect the requested approach direction.

  3. 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
 22import argparse
 23import numpy as np
 24import time
 25import torch
 26
 27from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
 28from embodichain.lab.sim.objects import Robot, RigidObject
 29from embodichain.lab.sim.utility.action_utils import interpolate_with_distance
 30from embodichain.lab.sim.shapes import MeshCfg
 31from embodichain.lab.sim.solvers import URSolverCfg
 32from embodichain.data import get_data_path
 33from embodichain.lab.gym.utils.gym_utils import add_env_launcher_args_to_parser
 34from dexsim.utility.path import get_resources_data_path
 35from embodichain.utils import logger
 36from embodichain.lab.sim.cfg import (
 37    RenderCfg,
 38    JointDrivePropertiesCfg,
 39    RobotCfg,
 40    LightCfg,
 41    RigidBodyAttributesCfg,
 42    RigidObjectCfg,
 43    URDFCfg,
 44)
 45from embodichain.toolkits.graspkit.pg_grasp.antipodal_generator import (
 46    GraspGenerator,
 47    GraspGeneratorCfg,
 48    AntipodalSamplerCfg,
 49)
 50from embodichain.toolkits.graspkit.pg_grasp.gripper_collision_checker import (
 51    GripperCollisionCfg,
 52)
 53
 54
 55def parse_arguments():
 56    """
 57    Parse command-line arguments to configure the simulation.
 58
 59    Returns:
 60        argparse.Namespace: Parsed arguments including number of environments and rendering options.
 61    """
 62    parser = argparse.ArgumentParser(
 63        description="Create and simulate a robot in SimulationManager"
 64    )
 65    add_env_launcher_args_to_parser(parser)
 66    return parser.parse_args()
 67
 68
 69def initialize_simulation(args) -> SimulationManager:
 70    """
 71    Initialize the simulation environment based on the provided arguments.
 72
 73    Args:
 74        args (argparse.Namespace): Parsed command-line arguments.
 75
 76    Returns:
 77        SimulationManager: Configured simulation manager instance.
 78    """
 79    config = SimulationManagerCfg(
 80        headless=True,
 81        sim_device=args.device,
 82        render_cfg=RenderCfg(renderer=args.renderer),
 83        physics_dt=1.0 / 100.0,
 84        arena_space=2.5,
 85    )
 86    sim = SimulationManager(config)
 87
 88    light = sim.add_light(
 89        cfg=LightCfg(
 90            uid="main_light",
 91            color=(0.6, 0.6, 0.6),
 92            intensity=30.0,
 93            init_pos=(1.0, 0, 3.0),
 94        )
 95    )
 96
 97    return sim
 98
 99
100def create_robot(sim: SimulationManager, position=[0.0, 0.0, 0.0]) -> Robot:
101    """
102    Create and configure a robot with an arm and a dexterous hand in the simulation.
103
104    Args:
105        sim (SimulationManager): The simulation manager instance.
106
107    Returns:
108        Robot: The configured robot instance added to the simulation.
109    """
110    # Retrieve URDF paths for the robot arm and hand
111    ur10_urdf_path = get_data_path("UniversalRobots/UR10/UR10.urdf")
112    gripper_urdf_path = get_data_path("DH_PGC_140_50_M/DH_PGC_140_50_M.urdf")
113    # Configure the robot with its components and control properties
114    cfg = RobotCfg(
115        uid="UR10",
116        urdf_cfg=URDFCfg(
117            components=[
118                {"component_type": "arm", "urdf_path": ur10_urdf_path},
119                {"component_type": "hand", "urdf_path": gripper_urdf_path},
120            ]
121        ),
122        drive_pros=JointDrivePropertiesCfg(
123            stiffness={"Joint[0-9]": 1e4, "FINGER[1-2]": 1e3},
124            damping={"Joint[0-9]": 1e3, "FINGER[1-2]": 1e2},
125            max_effort={"Joint[0-9]": 1e5, "FINGER[1-2]": 1e4},
126            drive_type="force",
127        ),
128        control_parts={
129            "arm": ["Joint[0-9]"],
130            "hand": ["FINGER[1-2]"],
131        },
132        solver_cfg={
133            "arm": URSolverCfg(
134                ur_type="ur10",
135                tcp=[
136                    [0.0, 1.0, 0.0, 0.0],
137                    [-1.0, 0.0, 0.0, 0.0],
138                    [0.0, 0.0, 1.0, 0.12],
139                    [0.0, 0.0, 0.0, 1.0],
140                ],
141            )
142        },
143        init_qpos=[0.0, -np.pi / 2, -np.pi / 2, np.pi / 2, -np.pi / 2, 0.0, 0.0, 0.0],
144        init_pos=position,
145    )
146    return sim.add_robot(cfg=cfg)
147
148
149def create_obj(sim: SimulationManager):
150    mug_cfg = RigidObjectCfg(
151        uid="table",
152        shape=MeshCfg(
153            fpath=get_resources_data_path("Model", "BakeTexture", "hdr_color_mesh.ply"),
154        ),
155        attrs=RigidBodyAttributesCfg(
156            mass=0.01,
157            dynamic_friction=0.97,
158            static_friction=0.99,
159        ),
160        max_convex_hull_num=16,
161        acd_method="vhacd",
162        init_pos=[0.55, 0.0, 0.08],
163        init_rot=[0.0, 0.0, 0.0],
164    )
165    mug = sim.add_rigid_object(cfg=mug_cfg)
166    return mug
167
168
169def get_grasp_traj(sim: SimulationManager, robot: Robot, grasp_xpos: torch.Tensor):
170    n_envs = sim.num_envs
171    rest_arm_qpos = robot.get_qpos("arm")
172
173    approach_xpos = grasp_xpos.clone()
174    approach_xpos[:, 2, 3] += 0.1
175
176    _, qpos_approach = robot.compute_ik(
177        pose=approach_xpos, joint_seed=rest_arm_qpos, name="arm"
178    )
179    _, qpos_grasp = robot.compute_ik(
180        pose=grasp_xpos, joint_seed=qpos_approach, name="arm"
181    )
182    hand_open_qpos = torch.tensor([0.00, 0.00], dtype=torch.float32, device=sim.device)
183    hand_close_qpos = torch.tensor(
184        [0.025, 0.025], dtype=torch.float32, device=sim.device
185    )
186
187    arm_trajectory = torch.cat(
188        [
189            rest_arm_qpos[:, None, :],
190            qpos_approach[:, None, :],
191            qpos_grasp[:, None, :],
192            qpos_grasp[:, None, :],
193            qpos_approach[:, None, :],
194            rest_arm_qpos[:, None, :],
195        ],
196        dim=1,
197    )
198    hand_trajectory = torch.cat(
199        [
200            hand_open_qpos[None, None, :].repeat(n_envs, 1, 1),
201            hand_open_qpos[None, None, :].repeat(n_envs, 1, 1),
202            hand_open_qpos[None, None, :].repeat(n_envs, 1, 1),
203            hand_close_qpos[None, None, :].repeat(n_envs, 1, 1),
204            hand_close_qpos[None, None, :].repeat(n_envs, 1, 1),
205            hand_close_qpos[None, None, :].repeat(n_envs, 1, 1),
206        ],
207        dim=1,
208    )
209    all_trajectory = torch.cat([arm_trajectory, hand_trajectory], dim=-1)
210    interp_trajectory = interpolate_with_distance(
211        trajectory=all_trajectory, interp_num=200, device=sim.device
212    )
213    return interp_trajectory
214
215
216if __name__ == "__main__":
217    import time
218
219    args = parse_arguments()
220    sim = initialize_simulation(args)
221    robot = create_robot(sim, position=[0.0, 0.0, 0.0])
222    obj = create_obj(sim)
223
224    # get mug grasp pose
225    grasp_cfg = GraspGeneratorCfg(
226        viser_port=11801,
227        antipodal_sampler_cfg=AntipodalSamplerCfg(
228            n_sample=10000, max_length=0.088, min_length=0.003
229        ),
230        is_partial_annotate=False,
231        is_filter_ground_collision=True,
232        n_top_grasps=30,
233    )
234    sim.open_window()
235
236    # Annotate part of the mug to be grasped by following the instructions in the visualization window:
237    # 1. View grasp object in browser (e.g http://localhost:11801)
238    # 2. press 'Rect Select Region', select grasp region
239    # 3. press 'Confirm Selection' to finish grasp region selection.
240
241    start_time = time.time()
242
243    gripper_collision_cfg = GripperCollisionCfg(
244        max_open_length=0.088, finger_length=0.078, point_sample_dense=0.012
245    )
246
247    # Extract mesh data from the mug and create grasp generator
248    vertices = obj.get_vertices(env_ids=[0], scale=True)[0]
249    triangles = obj.get_triangles(env_ids=[0])[0]
250    grasp_generator = GraspGenerator(
251        vertices=vertices,
252        triangles=triangles,
253        cfg=grasp_cfg,
254        gripper_collision_cfg=gripper_collision_cfg,
255    )
256
257    # Annotate grasp region (populates internal antipodal point pairs)
258    grasp_generator.annotate()
259
260    # Compute grasp poses per environment
261    approach_direction = torch.tensor(
262        [0, 0, -1], dtype=torch.float32, device=sim.device
263    )
264    obj_poses = obj.get_local_pose(to_matrix=True)
265    grasp_xpos_list = []
266
267    rest_xpos = robot.compute_fk(
268        qpos=robot.get_qpos("arm"), name="arm", to_matrix=True
269    )[0]
270    for i, obj_pose in enumerate(obj_poses):
271        is_success, grasp_pose, open_length = grasp_generator.get_grasp_poses(
272            obj_pose,
273            approach_direction,
274            visualize_collision=False,
275            visualize_pose=True,
276        )
277        if is_success:
278            grasp_xpos_list.append(grasp_pose.unsqueeze(0))
279        else:
280            logger.log_warning(f"No valid grasp pose found for {i}-th object.")
281            grasp_xpos_list.append(rest_xpos.unsqueeze(0))
282
283    grasp_xpos = torch.cat(grasp_xpos_list, dim=0)
284    cost_time = time.time() - start_time
285    logger.log_info(f"Get grasp pose cost time: {cost_time:.2f} seconds")
286
287    grab_traj = get_grasp_traj(sim, robot, grasp_xpos)
288    input("Press Enter to start the grab mug demo...")
289    n_waypoint = grab_traj.shape[1]
290    for i in range(n_waypoint):
291        robot.set_qpos(grab_traj[:, i, :])
292        sim.update(step=4)
293        time.sleep(1e-2)
294    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.

def parse_arguments():
    """
    Parse command-line arguments to configure the simulation.

    Returns:
        argparse.Namespace: Parsed arguments including number of environments and rendering options.
    """
    parser = argparse.ArgumentParser(
        description="Create and simulate a robot in SimulationManager"
    )
    add_env_launcher_args_to_parser(parser)
    return parser.parse_args()

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,
        sim_device=args.device,
        render_cfg=RenderCfg(renderer=args.renderer),
        physics_dt=1.0 / 100.0,
        arena_space=2.5,
    )
    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},
            ]
        ),
        drive_pros=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]"],
            "hand": ["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.12],
                    [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_resources_data_path("Model", "BakeTexture", "hdr_color_mesh.ply"),
        ),
        attrs=RigidBodyAttributesCfg(
            mass=0.01,
            dynamic_friction=0.97,
            static_friction=0.99,
        ),
        max_convex_hull_num=16,
        acd_method="vhacd",
        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 GraspGenerator, which runs an antipodal sampler on the object mesh. The mesh data (vertices and triangles) is extracted from the objects.RigidObject via its accessor methods. A GraspGeneratorCfg controls sampler parameters (sample count, gripper jaw limits) and the interactive annotation workflow:

  1. Open the visualization in a browser at the reported port (e.g. http://localhost:11801).

  2. Use Rect Select Region to highlight the area of the object that should be grasped.

  3. Click Confirm Selection to finalize the region.

After annotation, antipodal point pairs are cached to disk and automatically reused. Call annotate() again to select a new region.

For each environment, a grasp pose is computed by calling get_grasp_poses() with the object pose and desired approach direction. The result is a (4, 4) homogeneous transformation matrix representing the grasp frame in world coordinates. Set visualize=True to open an Open3D window showing the selected grasp on the object.

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.

    gripper_collision_cfg = GripperCollisionCfg(
        max_open_length=0.088, finger_length=0.078, point_sample_dense=0.012
    )

    # Extract mesh data from the mug and create grasp generator
    vertices = obj.get_vertices(env_ids=[0], scale=True)[0]
    triangles = obj.get_triangles(env_ids=[0])[0]
    grasp_generator = GraspGenerator(
        vertices=vertices,
        triangles=triangles,
        cfg=grasp_cfg,
        gripper_collision_cfg=gripper_collision_cfg,
    )

    # Annotate grasp region (populates internal antipodal point pairs)
    grasp_generator.annotate()

    # 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)
    grasp_xpos_list = []

    rest_xpos = robot.compute_fk(
        qpos=robot.get_qpos("arm"), name="arm", to_matrix=True
    )[0]
    for i, obj_pose in enumerate(obj_poses):
        is_success, grasp_pose, open_length = grasp_generator.get_grasp_poses(
            obj_pose,
            approach_direction,
            visualize_collision=False,
            visualize_pose=True,
        )
        if is_success:
            grasp_xpos_list.append(grasp_pose.unsqueeze(0))
        else:
            logger.log_warning(f"No valid grasp pose found for {i}-th object.")
            grasp_xpos_list.append(rest_xpos.unsqueeze(0))

    grasp_xpos = torch.cat(grasp_xpos_list, dim=0)
    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):
    n_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(n_envs, 1, 1),
            hand_open_qpos[None, None, :].repeat(n_envs, 1, 1),
            hand_open_qpos[None, None, :].repeat(n_envs, 1, 1),
            hand_close_qpos[None, None, :].repeat(n_envs, 1, 1),
            hand_close_qpos[None, None, :].repeat(n_envs, 1, 1),
            hand_close_qpos[None, None, :].repeat(n_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#

GraspGeneratorCfg#

GraspGeneratorCfg controls the overall grasp annotation workflow. The key parameters are listed below.

GraspGeneratorCfg parameters#

Parameter

Default

Description

viser_port

15531

Port used by the Viser browser-based visualizer for interactive grasp region annotation.

use_largest_connected_component

False

When True, only the largest connected component of the object mesh is used for sampling. Useful for meshes that contain disconnected fragments.

antipodal_sampler_cfg

AntipodalSamplerCfg()

Nested configuration for the antipodal point sampler. See the table below for its parameters.

max_deviation_angle

π / 6

Maximum allowed angle (in radians) between the specified approach direction and the axis connecting an antipodal point pair. Pairs that deviate more than this threshold are discarded.

n_deviated_approach_directions

4

Number of evenly deviated approach directions considered while sampling grasp poses.

n_top_grasps

50

Maximum number of top-ranked grasp poses returned after filtering and scoring.

is_partial_annotate

False

When True, the annotator allows selecting a partial region of the mesh for grasp sampling. If False, the entire mesh is used.

is_filter_ground_collision

True

Whether to filter out grasp poses that would cause the gripper to collide.

AntipodalSamplerCfg#

The antipodal_sampler_cfg field accepts an AntipodalSamplerCfg instance, which controls how antipodal point pairs are sampled on the mesh surface.

AntipodalSamplerCfg parameters#

Parameter

Default

Description

n_sample

20000

Number of surface points uniformly sampled from the mesh before ray casting. Higher values yield denser coverage but increase computation time.

max_angle

π / 12

Maximum angle (in radians) used to randomly perturb the ray direction away from the inward normal. Larger values increase diversity of sampled antipodal pairs. Setting this to 0 disables perturbation and samples strictly along surface normals.

max_length

0.1

Maximum allowed distance (in metres) between an antipodal pair. Pairs farther apart than this value are discarded; set this to match the maximum gripper jaw opening width.

min_length

0.001

Minimum allowed distance (in metres) between an antipodal pair. Pairs closer together than this value are discarded to avoid degenerate or self-intersecting grasps.

GripperCollisionCfg#

GripperCollisionCfg models the geometry of a parallel-jaw gripper as a point cloud and is used to filter out grasp candidates that would collide with the object. All length parameters are in metres.

GripperCollisionCfg parameters#

Parameter

Default

Description

max_open_length

0.1

Maximum finger separation of the gripper when fully open. Should match the physical gripper specification.

finger_length

0.08

Length of each finger along the Z-axis (depth direction from the root). Should match the physical gripper specification.

y_thickness

0.03

Thickness of the gripper body and fingers along the Y-axis (perpendicular to the opening direction).

x_thickness

0.01

Thickness of each finger along the X-axis (parallel to the opening direction).

root_z_width

0.08

Extent of the gripper root block along the Z-axis.

point_sample_dense

0.01

Approximate number of sample points per unit length along each edge of the gripper point cloud. Higher values produce denser point clouds and improve collision-check accuracy at the cost of additional computation.

max_decomposition_hulls

16

Maximum number of convex hulls used when decomposing the object mesh for collision checking. More hulls give a tighter shape approximation but increase cost.

open_check_margin

0.01

Extra clearance added to the gripper open length during collision checking to account for pose uncertainty or mesh inaccuracies.

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. A browser-based visualizer will also launch (default port 11801) for interactive grasp region annotation.

You can customize the run with additional arguments:

python scripts/tutorials/grasp/grasp_generator.py --num_envs <n> --device <cuda/cpu> --renderer <legacy|hybrid|fast-rt|rt> --headless

After confirming the grasp region in the browser, the script will compute a grasp pose, print the elapsed time, and then wait for you to press Enter before executing the full grasp trajectory in the simulation. 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.

Basic usage:

python -m embodichain annotate-grasp --mesh_path /path/to/object.ply

This will:

  1. Load the mesh file via trimesh.

  2. Launch a browser-based annotator (default port 15531).

  3. Open http://localhost:15531 in your browser, use Rect Select Region to highlight the graspable area, then click Confirm Selection.

  4. Compute antipodal point pairs on the selected region and cache them to disk.

Common options:

python -m embodichain annotate-grasp \
    --mesh_path /path/to/object.ply \
    --viser_port 15531 \
    --n_sample 20000 \
    --max_length 0.1 \
    --min_length 0.001
CLI options#

Option

Default

Description

--mesh_path

(required)

Path to the mesh file (.ply, .obj, .stl, etc.).

--viser_port

15531

Port for the browser-based annotation UI.

--n_sample

20000

Number of surface points to sample for antipodal pair detection.

--max_length

0.1

Maximum distance (metres) between antipodal pairs; should match the gripper’s maximum opening width.

--min_length

0.001

Minimum distance (metres) between antipodal pairs; filters out degenerate pairs.

--device

cpu

Compute device (cpu or cuda).