diff --git a/generate_gallery.py b/generate_gallery.py
new file mode 100644
index 00000000..3b829ca3
--- /dev/null
+++ b/generate_gallery.py
@@ -0,0 +1,446 @@
+"""Generate a markdown table with images of some of the models in Menagerie.
+
+Requirements:
+pip install dm_control pillow numpy tqdm mdutils opencv-python
+
+Instructions:
+`python generate_gallery.py` will create a markdown document called `gallery.md`
+with a table of images. Copy this table into README.md to display the images.
+"""
+
+import math
+from pathlib import Path
+from dm_control import mjcf
+from PIL import Image
+import cv2
+import enum
+import numpy as np
+from tqdm.auto import tqdm
+from mdutils.mdutils import MdUtils
+
+
+DEFAULT_FOV = 40
+
+
+class ModelType(int, enum.Enum):
+ ARM = 0
+ DUAL_ARM = 1
+ END_EFFECTOR = 2
+ MOBILE_MANIPULATOR = 3
+ QUADRUPED = 4
+ BIPED = 5
+ HUMANOID = 6
+ DRONE = 7
+ BIOMECHANICAL = 8
+ MISC = 9
+
+
+NAME_MAP = {
+ "franka_emika_panda/panda": "panda",
+ "franka_emika_panda/hand": "panda gripper",
+ "franka_fr3/fr3": "franka fr3",
+ "ufactory_lite6/lite6": "lite6",
+ "flybody/fruitfly": "fruitfly",
+ "skydio_x2/x2": "skydio x2",
+ "unitree_h1/h1": "h1",
+ "bitcraze_crazyflie_2/cf2": "crazyflie 2",
+ "google_robot/robot": "google robot",
+ "unitree_a1/a1": "a1",
+ "google_barkour_v0/barkour_v0": "barkour v0",
+ "anybotics_anymal_b/anymal_b": "anymal b",
+ "unitree_go1/go1": "go1",
+ "unitree_z1/z1": "z1",
+ "anybotics_anymal_c/anymal_c": "anymal c",
+ "agility_cassie/cassie": "cassie",
+ "realsense_d435i/d435i": "d435i",
+ "universal_robots_ur5e/ur5e": "ur5e",
+ "aloha/aloha": "aloha 2",
+ "rethink_robotics_sawyer/sawyer": "sawyer",
+ "robotis_op3/op3": "op3",
+ "universal_robots_ur10e/ur10e": "ur10e",
+ "kuka_iiwa_14/iiwa14": "iiwa 14",
+ "trossen_vx300s/vx300s": "vx300s",
+ "unitree_g1/g1": "g1",
+ "robotiq_2f85/2f85": "2f85",
+ "ufactory_xarm7/hand": "xarm7 gripper",
+ "ufactory_xarm7/xarm7": "xarm7",
+ "hello_robot_stretch/stretch": "stretch 2",
+ "google_barkour_vb/barkour_vb": "barkour vb",
+ "unitree_go2/go2": "go2",
+ "boston_dynamics_spot/spot_arm": "spot",
+ "shadow_dexee/shadow_dexee": "dex-ee",
+ "pal_talos/talos": "talos",
+ "leap_hand/left_hand": "left leap",
+ "wonik_allegro/left_hand": "left allegro",
+ "shadow_hand/left_hand": "left shadow",
+ "shadow_hand/left_hand": "left shadow",
+ "kinova_gen3/gen3": "gen3",
+}
+
+MODEL_MAP = {
+ "franka_emika_panda/panda": ModelType.ARM,
+ "franka_emika_panda/hand": ModelType.END_EFFECTOR,
+ "franka_fr3/fr3": ModelType.ARM,
+ "ufactory_lite6/lite6": ModelType.ARM,
+ "flybody/fruitfly": ModelType.BIOMECHANICAL,
+ "wonik_allegro/left_hand": ModelType.END_EFFECTOR,
+ "shadow_hand/left_hand": ModelType.END_EFFECTOR,
+ "skydio_x2/x2": ModelType.DRONE,
+ "unitree_h1/h1": ModelType.HUMANOID,
+ "bitcraze_crazyflie_2/cf2": ModelType.DRONE,
+ "google_robot/robot": ModelType.MOBILE_MANIPULATOR,
+ "unitree_a1/a1": ModelType.QUADRUPED,
+ "google_barkour_v0/barkour_v0": ModelType.QUADRUPED,
+ "anybotics_anymal_b/anymal_b": ModelType.QUADRUPED,
+ "unitree_go1/go1": ModelType.QUADRUPED,
+ "unitree_z1/z1": ModelType.ARM,
+ "anybotics_anymal_c/anymal_c": ModelType.QUADRUPED,
+ "agility_cassie/cassie": ModelType.BIPED,
+ "realsense_d435i/d435i": ModelType.MISC,
+ "universal_robots_ur5e/ur5e": ModelType.ARM,
+ "aloha/aloha": ModelType.DUAL_ARM,
+ "rethink_robotics_sawyer/sawyer": ModelType.ARM,
+ "robotis_op3/op3": ModelType.HUMANOID,
+ "universal_robots_ur10e/ur10e": ModelType.ARM,
+ "kuka_iiwa_14/iiwa14": ModelType.ARM,
+ "trossen_vx300s/vx300s": ModelType.ARM,
+ "unitree_g1/g1": ModelType.HUMANOID,
+ "robotiq_2f85/2f85": ModelType.END_EFFECTOR,
+ "ufactory_xarm7/hand": ModelType.END_EFFECTOR,
+ "ufactory_xarm7/xarm7": ModelType.ARM,
+ "hello_robot_stretch/stretch": ModelType.MOBILE_MANIPULATOR,
+ "google_barkour_vb/barkour_vb": ModelType.QUADRUPED,
+ "unitree_go2/go2": ModelType.QUADRUPED,
+ "boston_dynamics_spot/spot_arm": ModelType.QUADRUPED,
+ "shadow_dexee/shadow_dexee": ModelType.END_EFFECTOR,
+ "pal_talos/talos": ModelType.HUMANOID,
+ "leap_hand/left_hand": ModelType.END_EFFECTOR,
+ "kinova_gen3/gen3": ModelType.ARM,
+}
+
+DEFAULT_FOV = 40
+
+CAMERA_MAP = {
+ "pal_talos/talos": dict(
+ pos="2.312 0.005 1.144",
+ xyaxes="-0.002 1.000 -0.000 -0.107 -0.000 0.994",
+ ),
+ "skydio_x2/x2": dict(
+ pos="-0.580 -0.260 0.622",
+ xyaxes="0.442 -0.897 -0.000 0.428 0.211 0.879",
+ fovy=60,
+ ),
+ "flybody/fruitfly": dict(
+ pos="0.430 -0.361 0.326", xyaxes="0.589 0.808 0.000 -0.486 0.354 0.799", fovy=50
+ ),
+ "wonik_allegro/left_hand": dict(
+ pos="0.002 0.043 0.432", xyaxes="0.052 -0.999 0.000 0.998 0.052 0.017"
+ ),
+ "ufactory_xarm7/xarm7": dict(
+ pos="0.852 -0.383 0.860",
+ xyaxes="0.487 0.874 0.000 -0.354 0.197 0.914",
+ fovy=DEFAULT_FOV,
+ ),
+ "ufactory_xarm7/hand": dict(
+ pos="-0.282 0.013 0.118",
+ xyaxes="-0.047 -0.999 0.000 0.160 -0.007 0.987",
+ fovy=45,
+ ),
+ "shadow_hand/left_hand": dict(
+ pos="0.172 0.005 0.615",
+ xyaxes="-0.508 -0.861 -0.000 0.861 -0.508 0.017",
+ fovy=45,
+ ),
+ "franka_emika_panda/panda": dict(
+ pos="0.412 1.106 0.849",
+ xyaxes="-0.994 0.108 0.000 -0.040 -0.369 0.928",
+ fovy=DEFAULT_FOV,
+ ),
+ "franka_fr3/fr3": dict(
+ pos="0.412 1.106 0.849",
+ xyaxes="-0.994 0.108 0.000 -0.040 -0.369 0.928",
+ fovy=DEFAULT_FOV,
+ ),
+ "franka_emika_panda/hand": dict(
+ pos="0.340 0.008 0.059",
+ xyaxes="-0.023 1.000 0.000 -0.084 -0.002 0.996",
+ fovy=DEFAULT_FOV,
+ ),
+ "kuka_iiwa_14/iiwa14": dict(
+ pos="0.212 1.138 0.977",
+ xyaxes="-1.000 0.027 0.000 -0.012 -0.441 0.898",
+ fovy=45,
+ ),
+ "ufactory_lite6/lite6": dict(
+ pos="0.077 -0.778 0.528",
+ xyaxes="1.000 -0.004 -0.000 0.001 0.274 0.962",
+ fovy=45,
+ ),
+ "unitree_g1/g1": dict(
+ pos="1.466 -0.082 1.271",
+ xyaxes="0.072 0.997 -0.000 -0.377 0.027 0.926",
+ fovy=45,
+ ),
+ "unitree_h1/h1": dict(
+ pos="2.098 0.006 1.893",
+ xyaxes="0.007 1.000 -0.000 -0.394 0.003 0.919",
+ fovy=45,
+ ),
+ "robotis_op3/op3": dict(
+ pos="0.673 -0.024 0.447", xyaxes="0.035 0.999 0.000 -0.252 0.009 0.968", fovy=45
+ ),
+ "universal_robots_ur5e/ur5e": dict(
+ pos="0.603 1.012 0.595",
+ xyaxes="-0.932 0.363 -0.000 -0.080 -0.206 0.975",
+ fovy=DEFAULT_FOV,
+ ),
+ "universal_robots_ur10e/ur10e": dict(
+ pos="1.286 -0.798 0.889",
+ xyaxes="0.696 0.718 -0.000 -0.224 0.218 0.950",
+ fovy=DEFAULT_FOV,
+ ),
+ "unitree_z1/z1": dict(
+ pos="0.305 -0.400 0.552",
+ xyaxes="0.755 0.656 0.000 -0.359 0.413 0.837",
+ fovy=DEFAULT_FOV,
+ ),
+ "unitree_go2/go2": dict(
+ pos="0.753 -0.427 0.433",
+ xyaxes="0.518 0.856 0.000 -0.284 0.172 0.943",
+ fovy=DEFAULT_FOV,
+ ),
+ "unitree_go1/go1": dict(
+ pos="0.679 -0.553 0.530",
+ xyaxes="0.638 0.770 -0.000 -0.328 0.272 0.905",
+ fovy=DEFAULT_FOV,
+ ),
+ "unitree_a1/a1": dict(
+ pos="0.654 -0.564 0.536",
+ xyaxes="0.676 0.737 -0.000 -0.327 0.299 0.896",
+ fovy=DEFAULT_FOV,
+ ),
+ "trossen_vx300s/vx300s": dict(
+ pos="0.583 0.317 0.549",
+ xyaxes="-0.531 0.847 0.000 -0.434 -0.272 0.859",
+ fovy=DEFAULT_FOV,
+ ),
+ "robotiq_2f85/2f85": dict(
+ pos="-0.009 -0.251 0.107",
+ xyaxes="0.999 -0.033 -0.000 0.005 0.150 0.989",
+ fovy=45,
+ ),
+ "rethink_robotics_sawyer/sawyer": dict(
+ pos="1.014 -0.494 0.876",
+ xyaxes="0.555 0.832 -0.000 -0.372 0.248 0.895",
+ fovy=DEFAULT_FOV,
+ ),
+ "realsense_d435i/d435i": dict(
+ pos="-0.000 -0.002 0.128",
+ xyaxes="1.000 -0.000 0.000 0.000 1.000 0.017",
+ fovy=DEFAULT_FOV,
+ ),
+ "hello_robot_stretch/stretch": dict(
+ pos="1.464 -0.514 1.439",
+ xyaxes="0.271 0.963 -0.000 -0.362 0.102 0.927",
+ fovy=45,
+ ),
+ "google_robot/robot": dict(
+ pos="1.753 -0.231 1.305",
+ xyaxes="0.025 1.000 0.000 -0.306 0.008 0.952",
+ fovy=50,
+ ),
+ "google_barkour_vb/barkour_vb": dict(
+ pos="0.887 0.338 0.565",
+ xyaxes="-0.388 0.922 0.000 -0.460 -0.194 0.867",
+ fovy=DEFAULT_FOV,
+ ),
+ "google_barkour_v0/barkour_v0": dict(
+ pos="0.733 0.320 0.558",
+ xyaxes="-0.416 0.909 -0.000 -0.441 -0.202 0.875",
+ fovy=DEFAULT_FOV,
+ ),
+ "bitcraze_crazyflie_2/cf2": dict(
+ pos="0.037 -0.142 0.206",
+ xyaxes="0.963 0.268 0.000 -0.167 0.599 0.783",
+ fovy=DEFAULT_FOV,
+ ),
+ "anybotics_anymal_b/anymal_b": dict(
+ pos="0.930 -1.239 1.221",
+ xyaxes="0.809 0.587 0.000 -0.308 0.424 0.852",
+ fovy=DEFAULT_FOV,
+ ),
+ "anybotics_anymal_c/anymal_c": dict(
+ pos="1.547 -0.577 0.941",
+ xyaxes="0.378 0.926 -0.000 -0.358 0.146 0.922",
+ fovy=45,
+ ),
+ "aloha/aloha": dict(
+ pos="0.484 1.158 0.836",
+ xyaxes="-0.939 0.345 -0.000 -0.162 -0.441 0.883",
+ fovy=DEFAULT_FOV,
+ ),
+ "agility_cassie/cassie": dict(
+ pos="1.277 -1.122 1.053",
+ xyaxes="0.655 0.756 0.000 -0.196 0.170 0.966",
+ fovy=DEFAULT_FOV,
+ ),
+ "boston_dynamics_spot/spot_arm": dict(
+ pos="1.326 -0.829 0.651",
+ xyaxes="0.573 0.820 -0.000 -0.154 0.108 0.982",
+ fovy=DEFAULT_FOV,
+ ),
+ "shadow_dexee/shadow_dexee": dict(
+ pos="-0.003 -0.541 0.221",
+ xyaxes="1.000 0.000 -0.000 -0.000 0.131 0.991",
+ fovy=45,
+ ),
+ "leap_hand/left_hand": dict(
+ pos="-0.123 0.096 0.512",
+ xyaxes="0.004 -1.000 -0.000 0.995 0.004 0.101",
+ ),
+ "kinova_gen3/gen3": dict(
+ pos="0.252 -1.047 0.521",
+ xyaxes="0.988 0.156 -0.000 -0.024 0.153 0.988",
+ ),
+}
+
+KEYFRAME_MAP = {
+ "pal_talos": "0 0 1.025 0 0 0 0 0 0.15 0 0 0.3 0.4 -0.5 -1.5 0 0 0 0 -0.4 0 0 0 0 0 -0.3 -0.4 0.5 -1.5 0 0 0 0 -0.4 0 0 0 0 0 0 0 -0.4 0.8 -0.4 0 0 0 -0.4 0.8 -0.4 0",
+ "robotis_op3": "0 0 0.2789 1 0 0 0 0.0 0.0 -0.0890 0.7931 -0.79 0.0874 -0.7946 0.7855 -0.0015 -0.0460 -0.1626 0.2316 0.1565 -0.0230 0.0 0.0445 0.1611 -0.2332 -0.1580 0.0215",
+ "google_barkour_vb": "0 0 0.21 1 0 0 0 0 0.5 1.0 0 0.5 1.0 0 0.5 1.0 0 0.5 1.0",
+ "hello_robot_stretch": "0 0 0 1 0 0 0 0 0 0.1325 0.07995 0.07995 0.07605 0.0702 1.585 0 0.198 0 0 0.126 0 0 0 0",
+ "google_robot": "-1.51699e-13 -1.16232e-12 -0.1444 2.9724 -0.146 -0.3759 1.15806e-12 0.5518 0.62275",
+ "aloha": "0.43988 -0.206468 1.08253 -0.443382 -1.084 -0.00397598 0.0084 0.00846495 -1.28822 -0.360594 0.717978 -0.000325086 -0.273415 6.76003e-05 0.0084 0.00839987",
+ "kuka_iiwa_14": "0 0 0 -1.5708 0 1.5708 0",
+}
+
+KEEP_LIGHT = ["go1", "a1", "op3", "aloha", "left_hand", "stretch"]
+
+
+def create_arena():
+ arena = mjcf.RootElement()
+
+ arena.visual.quality.shadowsize = 8192
+ arena.visual.headlight.diffuse = (0.6,) * 3
+ arena.visual.headlight.ambient = (0.3,) * 3
+ arena.visual.headlight.specular = (0.2,) * 3
+
+ getattr(arena.visual, "global").offheight = 720
+ getattr(arena.visual, "global").offwidth = 1280
+
+ arena.asset.add(
+ "texture",
+ type="skybox",
+ builtin="gradient",
+ height=512,
+ width=512,
+ rgb1="1 1 1",
+ rgb2="1 1 1",
+ )
+
+ return arena
+
+
+MODEL_XMLS = [Path(f"{k}.xml") for k in MODEL_MAP.keys()]
+
+
+# Sort XML files.
+def sort_func(xml):
+ name = f"{xml.parent.stem}/{xml.stem}"
+ return (MODEL_MAP[name], xml.stem)
+
+
+MODEL_XMLS = sorted(MODEL_XMLS, key=sort_func)
+
+paths = []
+pngs = []
+for xml in tqdm(MODEL_XMLS):
+ try:
+ robot_maker = xml.parent.stem
+ robot_name = xml.stem
+ robot = f"{robot_maker}/{robot_name}"
+
+ if robot not in CAMERA_MAP:
+ continue
+
+ arena = create_arena()
+
+ if robot_maker in KEYFRAME_MAP:
+ arena.keyframe.add("key", qpos=KEYFRAME_MAP[robot_maker])
+
+ model_xml = mjcf.from_path(xml.as_posix(), escape_separators=True)
+ for light in model_xml.find_all("light"):
+ if robot_name not in KEEP_LIGHT:
+ light.remove()
+
+ if robot in CAMERA_MAP:
+ camera_kwargs = CAMERA_MAP[robot]
+ arena.worldbody.add("camera", name="thumbnail", **camera_kwargs)
+
+ if robot_maker == "aloha":
+ right_base = model_xml.find("body", "right\\base_link")
+ right_base.pos[0] = 0.3
+ left_base = model_xml.find("body", "left\\base_link")
+ left_base.pos[0] = -0.3
+
+ arena.include_copy(model_xml, override_attributes=True)
+
+ physics = mjcf.Physics.from_mjcf_model(arena)
+
+ try:
+ physics.reset(keyframe_id=0)
+ except:
+ physics.reset()
+
+ physics.forward()
+
+ if robot in CAMERA_MAP:
+ img = physics.render(height=500, width=500, camera_id="thumbnail")
+ else:
+ img = physics.render(height=500, width=500)
+
+ img = cv2.putText(
+ img.copy(),
+ NAME_MAP[robot],
+ (5, 480),
+ cv2.FONT_HERSHEY_SIMPLEX,
+ 1.3,
+ (0, 0, 0),
+ 1,
+ cv2.LINE_AA,
+ )
+
+ filename = f"assets/{robot_maker}-{robot_name}.png"
+ paths.append(filename)
+
+ png = np.zeros((500, 500, 4), dtype=np.uint8)
+ u, v = np.where(np.all(img == 255, axis=-1))
+ png[u, v, -1] = 0
+ png[u, v, :3] = 0
+ u, v = np.where(np.any(img != 255, axis=-1))
+ png[u, v, :3] = img[u, v]
+ png[u, v, -1] = 255
+ pngs.append(png.copy())
+ Image.fromarray(png).save(filename)
+ except Exception as e:
+ print(e)
+ print(f"failed to load {xml.as_posix()}")
+
+
+N_MODELS = len(paths)
+N_COLS = 5
+N_ROWS = int(math.ceil(N_MODELS / N_COLS))
+table = []
+for r in range(N_ROWS):
+ row = []
+ for c in range(N_COLS):
+ i = r * N_COLS + c
+ if i >= N_MODELS:
+ row.append("")
+ else:
+ row.append(f"
")
+ table.extend(row)
+
+mdFile = MdUtils(file_name="gallery")
+mdFile.new_table(columns=N_COLS, rows=N_ROWS, text=table, text_align="center")
+mdFile.create_md_file()
diff --git a/robotis_op3/op3.xml b/robotis_op3/op3.xml
index 9a866b0e..ff0ab5f3 100644
--- a/robotis_op3/op3.xml
+++ b/robotis_op3/op3.xml
@@ -72,7 +72,7 @@
-
+
diff --git a/unitree_go1/go1.xml b/unitree_go1/go1.xml
index 08b48c90..2d1d4ebc 100644
--- a/unitree_go1/go1.xml
+++ b/unitree_go1/go1.xml
@@ -67,7 +67,7 @@
-
+