From 82015ab178cb1171a5c44d655ddc2cfee64d2e57 Mon Sep 17 00:00:00 2001 From: Filippo Luca Ferretti <102977828+flferretti@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:28:05 +0200 Subject: [PATCH 1/2] Support user-defined cameras in `mujoco.loaders` --- src/jaxsim/mujoco/loaders.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/jaxsim/mujoco/loaders.py b/src/jaxsim/mujoco/loaders.py index 4a3012272..bb85eb4f3 100644 --- a/src/jaxsim/mujoco/loaders.py +++ b/src/jaxsim/mujoco/loaders.py @@ -159,6 +159,9 @@ def convert( considered_joints: list[str] | None = None, plane_normal: tuple[float, float, float] = (0, 0, 1), heightmap: bool | None = None, + cameras: ( + list[dict[str, str, str, str, str]] | dict[str, str, str, str, str] + ) = None, ) -> tuple[str, dict[str, Any]]: """ Converts a ROD model to a Mujoco MJCF string. @@ -166,6 +169,9 @@ def convert( Args: rod_model: The ROD model to convert. considered_joints: The list of joint names to consider in the conversion. + plane_normal: The normal vector of the plane. + heightmap: Whether to generate a heightmap. + cameras: The list of cameras to add to the scene. Returns: tuple: A tuple containing the MJCF string and the assets dictionary. @@ -470,6 +476,11 @@ def convert( fovy="60", ) + # Add user-defined camera + cameras = cameras if cameras is not None else {} + for camera in cameras if isinstance(cameras, list) else [cameras]: + _ = ET.SubElement(worldbody_element, "camera", **camera) + # ------------------------------------------------ # Add a light following the CoM of the first link # ------------------------------------------------ @@ -504,6 +515,9 @@ def convert( model_name: str | None = None, plane_normal: tuple[float, float, float] = (0, 0, 1), heightmap: bool | None = None, + cameras: ( + list[dict[str, str, str, str, str]] | dict[str, str, str, str, str] + ) = None, ) -> tuple[str, dict[str, Any]]: """ Converts a URDF file to a Mujoco MJCF string. @@ -512,6 +526,9 @@ def convert( urdf: The URDF file to convert. considered_joints: The list of joint names to consider in the conversion. model_name: The name of the model to convert. + plane_normal: The normal vector of the plane. + heightmap: Whether to generate a heightmap. + cameras: The list of cameras to add to the scene. Returns: tuple: A tuple containing the MJCF string and the assets dictionary. @@ -530,6 +547,7 @@ def convert( considered_joints=considered_joints, plane_normal=plane_normal, heightmap=heightmap, + cameras=cameras, ) @@ -541,6 +559,9 @@ def convert( model_name: str | None = None, plane_normal: tuple[float, float, float] = (0, 0, 1), heightmap: bool | None = None, + cameras: ( + list[dict[str, str, str, str, str]] | dict[str, str, str, str, str] + ) = None, ) -> tuple[str, dict[str, Any]]: """ Converts a SDF file to a Mujoco MJCF string. @@ -549,6 +570,9 @@ def convert( sdf: The SDF file to convert. considered_joints: The list of joint names to consider in the conversion. model_name: The name of the model to convert. + plane_normal: The normal vector of the plane. + heightmap: Whether to generate a heightmap. + cameras: The list of cameras to add to the scene. Returns: tuple: A tuple containing the MJCF string and the assets dictionary. @@ -567,4 +591,5 @@ def convert( considered_joints=considered_joints, plane_normal=plane_normal, heightmap=heightmap, + cameras=cameras, ) From 702af250ca674950bb0c54df888cfa1531a461e1 Mon Sep 17 00:00:00 2001 From: Filippo Luca Ferretti Date: Wed, 3 Apr 2024 16:11:40 +0200 Subject: [PATCH 2/2] Make camera setting more robust Co-authored-by: Diego Ferigo --- src/jaxsim/mujoco/loaders.py | 40 +++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/jaxsim/mujoco/loaders.py b/src/jaxsim/mujoco/loaders.py index bb85eb4f3..684322a40 100644 --- a/src/jaxsim/mujoco/loaders.py +++ b/src/jaxsim/mujoco/loaders.py @@ -1,3 +1,4 @@ +import dataclasses import pathlib import tempfile import warnings @@ -159,9 +160,7 @@ def convert( considered_joints: list[str] | None = None, plane_normal: tuple[float, float, float] = (0, 0, 1), heightmap: bool | None = None, - cameras: ( - list[dict[str, str, str, str, str]] | dict[str, str, str, str, str] - ) = None, + cameras: list[dict[str, str]] | dict[str, str] = None, ) -> tuple[str, dict[str, Any]]: """ Converts a ROD model to a Mujoco MJCF string. @@ -479,7 +478,10 @@ def convert( # Add user-defined camera cameras = cameras if cameras is not None else {} for camera in cameras if isinstance(cameras, list) else [cameras]: - _ = ET.SubElement(worldbody_element, "camera", **camera) + mj_camera = MujocoCamera.build(**camera) + _ = ET.SubElement( + worldbody_element, "camera", dataclasses.asdict(mj_camera) + ) # ------------------------------------------------ # Add a light following the CoM of the first link @@ -515,9 +517,7 @@ def convert( model_name: str | None = None, plane_normal: tuple[float, float, float] = (0, 0, 1), heightmap: bool | None = None, - cameras: ( - list[dict[str, str, str, str, str]] | dict[str, str, str, str, str] - ) = None, + cameras: list[dict[str, str]] | dict[str, str] = None, ) -> tuple[str, dict[str, Any]]: """ Converts a URDF file to a Mujoco MJCF string. @@ -559,9 +559,7 @@ def convert( model_name: str | None = None, plane_normal: tuple[float, float, float] = (0, 0, 1), heightmap: bool | None = None, - cameras: ( - list[dict[str, str, str, str, str]] | dict[str, str, str, str, str] - ) = None, + cameras: list[dict[str, str]] | dict[str, str] = None, ) -> tuple[str, dict[str, Any]]: """ Converts a SDF file to a Mujoco MJCF string. @@ -593,3 +591,25 @@ def convert( heightmap=heightmap, cameras=cameras, ) + + +@dataclasses.dataclass +class MujocoCamera: + name: str + mode: str + pos: str + xyaxes: str + fovy: str + + @classmethod + def build(cls, **kwargs): + if not all(isinstance(value, str) for value in kwargs.values()): + raise ValueError("Values must be strings") + + if len(kwargs["pos"].split()) != 3: + raise ValueError("pos must have three values separated by space") + + if len(kwargs["xyaxes"].split()) != 6: + raise ValueError("xyaxes must have six values separated by space") + + return cls(**kwargs)