Visualizing Worlds

Visualizing Worlds#

This tutorial explains you how to visualize a world. There are two recommended ways of doing it. One light weight way through RVIZ2 and a more heavy weight way through simulation with multiverse. Let’s load a world first to get started.

import logging
import os

from importlib.resources import files
from pathlib import Path

from semantic_digital_twin.adapters.urdf import URDFParser 

logging.disable(logging.CRITICAL)
apartment = os.path.join(Path(files("semantic_digital_twin")).parent.parent, "resources", "urdf", "apartment.urdf")
world = URDFParser.from_file(apartment).parse()
Unknown tag "material" in /robot[@name='apartment']/link[@name='coffe_machine']/collision[1]
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[1], line 11
      7 from semantic_digital_twin.adapters.urdf import URDFParser
      8 
      9 logging.disable(logging.CRITICAL)
     10 apartment = os.path.join(Path(files("semantic_digital_twin")).parent.parent, "resources", "urdf", "apartment.urdf")
---> 11 world = URDFParser.from_file(apartment).parse()

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/adapters/urdf.py:194, in URDFParser.parse(self)
    192 world = World()
    193 world.name = self.prefix
--> 194 with world.modify_world():
    195     world.add_kinematic_structure_entity(root)
    196     main_joints = []

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/world.py:314, in WorldModelUpdateContextManager.__exit__(self, exc_type, exc_val, exc_tb)
    312 try:
    313     if exc_type is None:
--> 314         self.world._notify_model_change(
    315             publish_changes=self.publish_changes
    316         )
    317         run_pending_publications = True
    318 finally:

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/world.py:2129, in World._notify_model_change(self, publish_changes, **kwargs)
   2123 def _notify_model_change(self, publish_changes: bool = True, **kwargs) -> None:
   2124     """
   2125     Notifies the system of a model change and updates the necessary states, caches,
   2126     and forward kinematics expressions while also triggering registered callbacks
   2127     for model changes.
   2128     """
-> 2129     self._model_manager.update_model_version_and_notify_callbacks(
   2130         publish_changes=publish_changes, **kwargs
   2131     )
   2132     self.notify_state_change(
   2133         publish_changes=publish_changes, force_republish=True, **kwargs
   2134     )
   2136     for callback in list(self.state.state_change_callbacks):

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/world.py:523, in WorldModelManager.update_model_version_and_notify_callbacks(self, **kwargs)
    521 self.version += 1
    522 for callback in list(self.model_change_callbacks):
--> 523     callback.notify_model_change(**kwargs)

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/callbacks/callback.py:171, in ModelChangeCallback.notify_model_change(self, **kwargs)
    169 def notify_model_change(self, **kwargs):
    170     if not self._is_paused:
--> 171         self.on_model_change(**kwargs)

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/collision_checking/collision_detector.py:142, in CollisionDetectorModelUpdater.on_model_change(self, **kwargs)
    140 if self._world.is_empty():
    141     return
--> 142 self.collision_detector.sync_world_model()
    143 self.compile_collision_fks()

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/collision_checking/pybullet_collision_detector.py:390, in BulletCollisionDetector.sync_world_model(self)
    388     return
    389 for body in self._world.bodies_with_collision:
--> 390     self.add_body(body)
    391 self._ordered_bullet_objects = list(self.body_to_bullet_object.values())

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/collision_checking/pybullet_collision_detector.py:405, in BulletCollisionDetector.add_body(self, body)
    404 def add_body(self, body: Body):
--> 405     o = create_shape_from_body(body=body, mesh_decomposer=self.mesh_decomposer)
    406     self.kineverse_world.add_collision_object(o)
    407     self.body_to_bullet_object[body] = o

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/collision_checking/pybullet_collision_detector.py:226, in create_shape_from_body(body, mesh_decomposer)
    224 shapes = []
    225 for collision_id, geometry in enumerate(body.collision):
--> 226     shape = create_shape_from_geometry(
    227         geometry=geometry, mesh_decomposer=mesh_decomposer
    228     )
    229     link_T_geometry = bullet.Transform.from_np(geometry.origin.to_np())
    230     shapes.append((link_T_geometry, shape))

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/collision_checking/pybullet_collision_detector.py:199, in create_shape_from_geometry(geometry, mesh_decomposer)
    194     shape = create_cylinder_shape(
    195         diameter=geometry.width, height=geometry.height
    196     )
    198 case Mesh():
--> 199     shape = load_convex_mesh_shape(
    200         mesh=geometry,
    201         single_shape=False,
    202         scale=geometry.scale,
    203         mesh_decomposer=mesh_decomposer,
    204     )
    206 case _:
    207     raise NotImplementedError()

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/collision_checking/pybullet_collision_detector.py:266, in load_convex_mesh_shape(mesh, single_shape, scale, mesh_decomposer)
    256 """
    257 Loads a convex mesh shape from a mesh.
    258 
   (...)    263 :return: the bullet convex shape.
    264 """
    265 if not mesh.mesh.is_convex and mesh_decomposer is not None:
--> 266     obj_pkg_filename = convert_to_decomposed_obj_and_save_in_tmp(
    267         mesh=mesh, mesh_decomposer=mesh_decomposer
    268     )
    269 else:
    270     obj_pkg_filename = str(mesh.local_file)

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/collision_checking/pybullet_collision_detector.py:313, in convert_to_decomposed_obj_and_save_in_tmp(mesh, mesh_decomposer, cache_dir, log_path)
    311 if not trimesh_obj.is_convex and mesh_decomposer is not None:
    312     with suppress_stdout_stderr():
--> 313         mesh_decomposer.apply_to_mesh_and_save(mesh, obj_file_name)
    314     logging.info(f'Saved convex decomposition to "{obj_file_name}".')
    315 else:

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/pipeline/mesh_decomposition/vhacd.py:132, in VHACDMeshDecomposer.apply_to_mesh_and_save(self, mesh, output_path)
    131 def apply_to_mesh_and_save(self, mesh: Mesh, output_path: str) -> str:
--> 132     parts = self.apply_to_mesh(mesh)
    133     trimesh.Scene([p.mesh for p in parts]).export(output_path, file_type="obj")
    134     return output_path

File /__w/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/semantic_digital_twin/src/semantic_digital_twin/pipeline/mesh_decomposition/vhacd.py:113, in VHACDMeshDecomposer.apply_to_mesh(self, mesh)
    112 def apply_to_mesh(self, mesh: Mesh) -> List[Mesh]:
--> 113     decomposed = mesh.mesh.convex_decomposition(
    114         maxConvexHulls=self.max_convex_hulls,
    115         resolution=self.resolution,
    116         minimumVolumePercentErrorAllowed=self.minimum_volume_percent_error_allowed,
    117         maxRecursionDepth=self.max_recursion_depth,
    118         shrinkWrap=self.shrink_wrap,
    119         fillMode=self.fill_mode.value,
    120         maxNumVerticesPerCH=self.max_vertices_per_convex_hull,
    121         asyncACD=self.asynchronous,
    122         minEdgeLength=self.min_edge_length,
    123         findBestPlane=self.find_best_plane,
    124     )
    125     new_geometry = [
    126         Mesh.from_trimesh(mesh=decomposed_part, origin=mesh.origin)
    127         for decomposed_part in decomposed
    128     ]
    129     return new_geometry

File /opt/ros/cram-env/lib/python3.12/site-packages/trimesh/base.py:3048, in Trimesh.convex_decomposition(self, **kwargs)
   3035 def convex_decomposition(self, **kwargs) -> list["Trimesh"]:
   3036     """
   3037     Compute an approximate convex decomposition of a mesh
   3038     using `pip install pyVHACD`.
   (...)   3044     **kwargs : VHACD keyword arguments
   3045     """
   3046     return [
   3047         Trimesh(**kwargs)
-> 3048         for kwargs in decomposition.convex_decomposition(self, **kwargs)
   3049     ]

File /opt/ros/cram-env/lib/python3.12/site-packages/trimesh/decomposition.py:48, in convex_decomposition(mesh, **kwargs)
     37 # the faces are triangulated in a (len(face), ...vertex-index)
     38 # for vtkPolyData
     39 # i.e. so if shaped to four columns the first column is all 3
     40 faces = (
     41     np.column_stack((np.ones(len(mesh.faces), dtype=np.int64) * 3, mesh.faces))
     42     .ravel()
     43     .astype(np.uint32)
     44 )
     46 return [
     47     {"vertices": v, "faces": f}
---> 48     for v, f in compute_vhacd(mesh.vertices, faces, **kwargs)
     49 ]

KeyboardInterrupt: 

For the RVIZ2 way, ROS2 and the TFPublisher is needed. A caveat of this approach is that you have to manage the lifecycle of a ROS2 node yourself. We recommend to put the spinning into sperate threads and just shutdown the thread when exiting the system.

from semantic_digital_twin.adapters.ros.tf_publisher import TFPublisher
from semantic_digital_twin.adapters.ros.visualization.viz_marker import VizMarkerPublisher
import threading
import rclpy
rclpy.init()

node = rclpy.create_node("semantic_digital_twin")
thread = threading.Thread(target=rclpy.spin, args=(node,), daemon=True)
thread.start()

tf_publisher = TFPublisher(_world=world, node=node)
viz = VizMarkerPublisher(_world=world, node=node)

When you want to stop visualizing, you have to stop the visualizer and afterwards clean up ROS2.

node.destroy_node()
rclpy.shutdown()

The world can also be visualized directly through a running simulation. Although this approach is computationally heavier, it provides the important advantage of enabling interaction with the environment. In addition to visualization, the physics engine can be used to simulate dynamics, contacts, and collisions. Further details are provided in the physics simulators section.

If you have followed the guide until here, you have probably noticed that we have used the RayTracer to visualize the world a few times. This is a convenient way of visualizing a world inside a notebook, like in these guides, but it is not recommended for normal usage.