-
Notifications
You must be signed in to change notification settings - Fork 9
Add grasp annotator #196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add grasp annotator #196
Changes from all commits
5fd8dda
1e15c77
baf731a
f1f043b
1887304
63ec5e6
73781d8
e0d129d
7c55249
35dcb44
508a712
2adfb8e
bc1b03c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| Generating and Executing Robot Grasps | ||
| ====================================== | ||
|
|
||
| .. currentmodule:: embodichain.lab.sim | ||
|
|
||
| This tutorial demonstrates how to generate antipodal grasp poses for a target object and execute a full grasp trajectory with a robot arm. It covers scene initialization, robot and object creation, interactive grasp region annotation, grasp pose computation, and trajectory execution in the simulation loop. | ||
|
|
||
| The Code | ||
| ~~~~~~~~ | ||
|
|
||
| The tutorial corresponds to the ``grasp_generator.py`` script in the ``scripts/tutorials/grasp`` directory. | ||
|
|
||
| .. dropdown:: Code for grasp_generator.py | ||
| :icon: code | ||
|
|
||
| .. literalinclude:: ../../../scripts/tutorials/grasp/grasp_generator.py | ||
| :language: python | ||
| :linenos: | ||
|
|
||
|
|
||
| The Code Explained | ||
| ~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| 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 ray tracing and headless mode. | ||
|
|
||
| .. literalinclude:: ../../../scripts/tutorials/grasp/grasp_generator.py | ||
| :language: python | ||
| :start-at: def parse_arguments(): | ||
| :end-at: return parser.parse_args() | ||
|
|
||
| The parsed arguments are passed to ``initialize_simulation``, which builds a :class:`SimulationManagerCfg` and creates the :class:`SimulationManager` instance. When ray tracing is enabled a directional :class:`cfg.LightCfg` is also added to the scene. | ||
|
|
||
| .. literalinclude:: ../../../scripts/tutorials/grasp/grasp_generator.py | ||
| :language: python | ||
| :start-at: def initialize_simulation(args) -> SimulationManager: | ||
| :end-at: return sim | ||
|
|
||
| Annotating and computing grasp poses | ||
| ------------------------------------- | ||
|
|
||
| Grasp generation is performed by :meth:`objects.RigidObject.get_grasp_pose`, which internally runs an antipodal sampler on the object mesh. A :class:`toolkits.graspkit.pg_grasp.GraspAnnotatorCfg` 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. | ||
|
|
||
| The function returns a batch of ``(N_envs, 4, 4)`` homogeneous transformation matrices representing candidate grasp frames in the world coordinate system. | ||
|
|
||
| For each grasp pose, gripper approach direction in world coordinate is required to compute the antipodal grasp. 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. | ||
|
|
||
| .. literalinclude:: ../../../scripts/tutorials/grasp/grasp_generator.py | ||
| :language: python | ||
| :start-at: # get mug grasp pose | ||
| :end-at: logger.log_info(f"Get grasp pose cost time: {cost_time:.2f} seconds") | ||
|
|
||
|
|
||
| The Code Execution | ||
| ~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| To run the script, execute the following command from the project root: | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| 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: | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| python scripts/tutorials/grasp/grasp_generator.py --num_envs <n> --device <cuda/cpu> --enable_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. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ Tutorials | |
| sensor | ||
| motion_gen | ||
| gizmo | ||
| grasp_generator | ||
| basic_env | ||
| modular_env | ||
| rl | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -35,6 +35,11 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from embodichain.utils.math import convert_quat | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from embodichain.utils.math import matrix_from_quat, quat_from_matrix, matrix_from_euler | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from embodichain.utils import logger | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from embodichain.toolkits.graspkit.pg_grasp.antipodal_annotator import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GraspAnnotator, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| GraspAnnotatorCfg, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+38
to
+41
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from embodichain.toolkits.graspkit.pg_grasp.antipodal_annotator import ( | |
| GraspAnnotator, | |
| GraspAnnotatorCfg, | |
| ) | |
| def _load_grasp_annotator_module(): | |
| """Lazily import the grasp annotator module. | |
| This avoids importing optional heavy UI/geometry dependencies (e.g., viser, | |
| trimesh, open3d) at module import time. The import is performed only when | |
| grasp annotation functionality is actually used. | |
| """ | |
| try: | |
| from embodichain.toolkits.graspkit.pg_grasp import antipodal_annotator | |
| except ImportError as exc: | |
| raise ImportError( | |
| "Grasp annotator dependencies are not installed. " | |
| "To use grasp annotation (RigidObject.get_grasp_pose and related " | |
| "functionality), install the optional grasp/geometry extras, e.g.:\n\n" | |
| " pip install 'embodichain[grasp]'\n\n" | |
| "or ensure that packages like 'viser', 'trimesh', and 'open3d' are " | |
| "available in your environment." | |
| ) from exc | |
| return antipodal_annotator | |
| class GraspAnnotator: # type: ignore[misc] | |
| """Lazy proxy for the real GraspAnnotator class. | |
| The actual class is imported from `embodichain.toolkits.graspkit.pg_grasp` | |
| only when this proxy is instantiated. | |
| """ | |
| def __new__(cls, *args, **kwargs): | |
| module = _load_grasp_annotator_module() | |
| real_cls = module.GraspAnnotator | |
| return real_cls(*args, **kwargs) | |
| class GraspAnnotatorCfg: # type: ignore[misc] | |
| """Lazy proxy for the real GraspAnnotatorCfg class. | |
| The actual class is imported only when this proxy is instantiated. | |
| """ | |
| def __new__(cls, *args, **kwargs): | |
| module = _load_grasp_annotator_module() | |
| real_cls = module.GraspAnnotatorCfg | |
| return real_cls(*args, **kwargs) |
Copilot
AI
Mar 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Importing GraspAnnotator (and its dependencies like viser) at module import time makes embodichain.lab.sim.objects.rigid_object depend on the annotator stack even when users never call get_grasp_pose(). Consider moving these imports inside get_grasp_pose() (or guarding with a lazy/optional import) to reduce import-time overhead and optional-dep failures.
| from embodichain.toolkits.graspkit.pg_grasp.antipodal_annotator import ( | |
| GraspAnnotator, | |
| GraspAnnotatorCfg, | |
| ) | |
| try: | |
| from embodichain.toolkits.graspkit.pg_grasp.antipodal_annotator import ( | |
| GraspAnnotator, | |
| GraspAnnotatorCfg, | |
| ) | |
| except ImportError: | |
| logger.warning( | |
| "Optional dependency 'embodichain.toolkits.graspkit.pg_grasp.antipodal_annotator' " | |
| "could not be imported. Grasp-related functionality may be unavailable." | |
| ) | |
| GraspAnnotator = None # type: ignore[assignment] | |
| GraspAnnotatorCfg = None # type: ignore[assignment] |
Copilot
AI
Mar 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Importing GraspAnnotator at module import time pulls in heavy optional UI dependencies (e.g., viser, trimesh, open3d) whenever RigidObject is imported, even if grasp annotation is never used. To reduce baseline dependencies and import overhead (and avoid failures in minimal/headless installs), consider moving these imports inside get_grasp_pose() and raising a clear error if optional deps are missing.
Copilot
AI
Mar 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New public method RigidObject.get_grasp_pose() adds user-facing behavior (caching + pose computation) but there is no automated test coverage for it. Since tests/sim/objects/test_rigid_object.py exists, please add at least a unit test for the deterministic math path and a smoke test for the [num_envs, 4, 4] output shape.
Copilot
AI
Mar 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type annotations are misleading here: grasp_poses: tuple[torch.Tensor] = [] and open_lengths: tuple[torch.Tensor] = [] are initialized as lists and later appended to. Prefer list[torch.Tensor] (or build tensors directly) to avoid type confusion for readers and static checkers.
| grasp_poses: tuple[torch.Tensor] = [] | |
| open_lengths: tuple[torch.Tensor] = [] | |
| grasp_poses: list[torch.Tensor] = [] | |
| open_lengths: list[torch.Tensor] = [] |
Copilot
AI
Mar 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_grasp_pose() assumes self._grasp_annotator.annotate() always returns a valid tensor and that get_approach_grasp_poses() always finds a grasp. However annotate() can return None and grasp generation can fail (empty candidates), which will currently crash inside the loop. Please add explicit validation/error handling here (e.g., raise a clear exception when annotation/grasp search fails) so callers get a deterministic failure mode.
| poses = self.get_local_pose(to_matrix=True) | |
| poses = torch.as_tensor(poses, dtype=torch.float32, device=self.device) | |
| grasp_poses: tuple[torch.Tensor] = [] | |
| open_lengths: tuple[torch.Tensor] = [] | |
| for pose in poses: | |
| grasp_pose, open_length = self._grasp_annotator.get_approach_grasp_poses( | |
| self._hit_point_pairs, pose, approach_direction, is_visual=False | |
| ) | |
| grasp_poses.append(grasp_pose) | |
| open_lengths.append(open_length) | |
| if self._hit_point_pairs is None or ( | |
| hasattr(self._hit_point_pairs, "__len__") | |
| and len(self._hit_point_pairs) == 0 | |
| ): | |
| raise RuntimeError( | |
| "RigidObject.get_grasp_pose(): grasp annotation failed; " | |
| "no antipodal point pairs were generated for the object." | |
| ) | |
| poses = self.get_local_pose(to_matrix=True) | |
| poses = torch.as_tensor(poses, dtype=torch.float32, device=self.device) | |
| grasp_poses: List[torch.Tensor] = [] | |
| open_lengths: List[torch.Tensor] = [] | |
| for idx, pose in enumerate(poses): | |
| grasp_pose, open_length = self._grasp_annotator.get_approach_grasp_poses( | |
| self._hit_point_pairs, pose, approach_direction, is_visual=False | |
| ) | |
| if grasp_pose is None or open_length is None: | |
| raise RuntimeError( | |
| f"RigidObject.get_grasp_pose(): failed to compute grasp pose " | |
| f"for pose index {idx}; no valid grasp candidates found." | |
| ) | |
| grasp_poses.append(grasp_pose) | |
| open_lengths.append(open_length) | |
| if len(grasp_poses) == 0: | |
| raise RuntimeError( | |
| "RigidObject.get_grasp_pose(): no grasp poses were generated." | |
| ) |
Copilot
AI
Mar 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the is_visual block, vertices, triangles, and scale are recomputed but never used (the visualizer uses self._grasp_annotator’s stored mesh). This is dead code and can be removed to avoid confusion and extra work on large meshes.
| vertices = self._entities[0].get_vertices() | |
| triangles = self._entities[0].get_triangles() | |
| scale = self._entities[0].get_body_scale() | |
| vertices = vertices * scale |
Copilot
AI
Mar 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
open_lengths contains tensors from get_approach_grasp_poses(), but visualize_grasp_pose() expects open_length: float and uses it in Open3D geometry transforms. Convert to a Python float (e.g., open_length.item()) before passing it to avoid runtime type errors in visualization.
Copilot
AI
Mar 30, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New public method RigidObject.get_grasp_pose() introduces non-trivial behavior (caching, mesh extraction/scaling, grasp selection, collision filtering). There are existing tests for RigidObject under tests/sim/objects/; please add focused tests that cover at least the non-interactive paths (e.g., handling of missing/empty grasp candidates and correct output shape/dtype).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tutorial references
scripts/tutorials/grasp/grasp_generator.pyvia multipleliteralincludedirectives, but there is no such script in the repo (onlygrasp_mug.pyexists underscripts/tutorials/grasp). This will break the Sphinx build; either add the referenced script or update the doc to include the correct filename/path.