[software] 添加16DOF早期训练仿真与Sim2Real闭环
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
"""Render images of each sub-terrain type for documentation.
|
||||
|
||||
Generates one PNG per terrain type at a fixed difficulty level.
|
||||
Images are saved to docs/source/_static/terrains/.
|
||||
|
||||
Run with:
|
||||
uv run python scripts/tools/render_terrain_gallery.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import mujoco
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
import mjlab.terrains as terrain_gen
|
||||
from mjlab.terrains import TerrainEntity, TerrainEntityCfg
|
||||
from mjlab.terrains.terrain_generator import TerrainGeneratorCfg
|
||||
from mjlab.utils import spec_config as spec_cfg
|
||||
|
||||
OUTPUT_DIR = Path("docs/source/_static/terrains")
|
||||
WIDTH = 1080
|
||||
HEIGHT = 1080
|
||||
DIFFICULTY = 0.85
|
||||
PATCH_SIZE = (8.0, 8.0)
|
||||
|
||||
|
||||
# Each entry: (filename, SubTerrainCfg instance).
|
||||
TERRAIN_TYPES: list[tuple[str, terrain_gen.SubTerrainCfg]] = [
|
||||
# Primitive terrains.
|
||||
(
|
||||
"box_flat",
|
||||
terrain_gen.BoxFlatTerrainCfg(proportion=1.0),
|
||||
),
|
||||
(
|
||||
"box_pyramid_stairs",
|
||||
terrain_gen.BoxPyramidStairsTerrainCfg(
|
||||
proportion=1.0,
|
||||
step_height_range=(0.0, 0.2),
|
||||
step_width=0.3,
|
||||
platform_width=3.0,
|
||||
border_width=1.0,
|
||||
),
|
||||
),
|
||||
(
|
||||
"box_inverted_pyramid_stairs",
|
||||
terrain_gen.BoxInvertedPyramidStairsTerrainCfg(
|
||||
proportion=1.0,
|
||||
step_height_range=(0.0, 0.2),
|
||||
step_width=0.3,
|
||||
platform_width=3.0,
|
||||
border_width=1.0,
|
||||
),
|
||||
),
|
||||
(
|
||||
"box_random_stairs",
|
||||
terrain_gen.BoxRandomStairsTerrainCfg(
|
||||
proportion=1.0,
|
||||
step_width=0.8,
|
||||
step_height_range=(0.1, 0.3),
|
||||
platform_width=1.0,
|
||||
border_width=0.25,
|
||||
),
|
||||
),
|
||||
(
|
||||
"box_open_stairs",
|
||||
terrain_gen.BoxOpenStairsTerrainCfg(
|
||||
proportion=1.0,
|
||||
step_height_range=(0.1, 0.2),
|
||||
step_width_range=(0.4, 0.8),
|
||||
platform_width=1.0,
|
||||
border_width=0.25,
|
||||
),
|
||||
),
|
||||
(
|
||||
"box_random_grid",
|
||||
terrain_gen.BoxRandomGridTerrainCfg(
|
||||
proportion=1.0,
|
||||
grid_width=0.4,
|
||||
grid_height_range=(0.0, 0.3),
|
||||
platform_width=1.0,
|
||||
),
|
||||
),
|
||||
(
|
||||
"box_random_spread",
|
||||
terrain_gen.BoxRandomSpreadTerrainCfg(
|
||||
proportion=1.0,
|
||||
num_boxes=80,
|
||||
box_width_range=(0.1, 1.0),
|
||||
box_length_range=(0.1, 2.0),
|
||||
box_height_range=(0.05, 0.3),
|
||||
platform_width=1.0,
|
||||
border_width=0.25,
|
||||
),
|
||||
),
|
||||
(
|
||||
"box_stepping_stones",
|
||||
terrain_gen.BoxSteppingStonesTerrainCfg(
|
||||
proportion=1.0,
|
||||
stone_size_range=(0.4, 0.8),
|
||||
stone_distance_range=(0.2, 0.5),
|
||||
stone_height=0.2,
|
||||
stone_height_variation=0.1,
|
||||
stone_size_variation=0.2,
|
||||
displacement_range=0.1,
|
||||
floor_depth=2.0,
|
||||
platform_width=1.0,
|
||||
border_width=0.25,
|
||||
),
|
||||
),
|
||||
(
|
||||
"box_narrow_beams",
|
||||
terrain_gen.BoxNarrowBeamsTerrainCfg(
|
||||
proportion=1.0,
|
||||
num_beams=12,
|
||||
beam_width_range=(0.2, 0.8),
|
||||
beam_height=0.2,
|
||||
spacing=0.8,
|
||||
platform_width=1.0,
|
||||
border_width=0.25,
|
||||
floor_depth=2.0,
|
||||
),
|
||||
),
|
||||
(
|
||||
"box_tilted_grid",
|
||||
terrain_gen.BoxTiltedGridTerrainCfg(
|
||||
proportion=1.0,
|
||||
grid_width=1.0,
|
||||
tilt_range_deg=20.0,
|
||||
height_range=0.3,
|
||||
platform_width=1.0,
|
||||
border_width=0.25,
|
||||
floor_depth=2.0,
|
||||
),
|
||||
),
|
||||
(
|
||||
"box_nested_rings",
|
||||
terrain_gen.BoxNestedRingsTerrainCfg(
|
||||
proportion=1.0,
|
||||
num_rings=8,
|
||||
ring_width_range=(0.3, 0.6),
|
||||
gap_range=(0.1, 0.4),
|
||||
height_range=(0.1, 0.4),
|
||||
platform_width=1.0,
|
||||
border_width=0.25,
|
||||
floor_depth=2.0,
|
||||
),
|
||||
),
|
||||
# Heightfield terrains.
|
||||
(
|
||||
"hf_pyramid_slope",
|
||||
terrain_gen.HfPyramidSlopedTerrainCfg(
|
||||
proportion=1.0,
|
||||
slope_range=(0.0, 0.7),
|
||||
platform_width=2.0,
|
||||
border_width=0.25,
|
||||
),
|
||||
),
|
||||
(
|
||||
"hf_random_uniform",
|
||||
terrain_gen.HfRandomUniformTerrainCfg(
|
||||
proportion=1.0,
|
||||
noise_range=(0.02, 0.10),
|
||||
noise_step=0.02,
|
||||
border_width=0.25,
|
||||
),
|
||||
),
|
||||
(
|
||||
"hf_wave",
|
||||
terrain_gen.HfWaveTerrainCfg(
|
||||
proportion=1.0,
|
||||
amplitude_range=(0.1, 0.5),
|
||||
num_waves=6,
|
||||
border_width=0.25,
|
||||
),
|
||||
),
|
||||
(
|
||||
"hf_discrete_obstacles",
|
||||
terrain_gen.HfDiscreteObstaclesTerrainCfg(
|
||||
proportion=1.0,
|
||||
obstacle_width_range=(0.3, 1.0),
|
||||
obstacle_height_range=(0.05, 0.3),
|
||||
num_obstacles=40,
|
||||
border_width=0.25,
|
||||
),
|
||||
),
|
||||
(
|
||||
"hf_perlin_noise",
|
||||
terrain_gen.HfPerlinNoiseTerrainCfg(
|
||||
proportion=1.0,
|
||||
height_range=(0.0, 1.0),
|
||||
octaves=4,
|
||||
persistence=0.3,
|
||||
lacunarity=2.0,
|
||||
scale=10.0,
|
||||
horizontal_scale=0.1,
|
||||
border_width=0.50,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
CAMERA_DISTANCE = 8.0
|
||||
CAMERA_ELEVATION_DEG = 50.0 # Degrees from horizontal (90 = top-down).
|
||||
CAMERA_AZIMUTH_DEG = 135.0 # Rotation around the vertical axis.
|
||||
FOV_PADDING = 1.1
|
||||
|
||||
|
||||
def render_terrain(
|
||||
name: str,
|
||||
sub_terrain_cfg: terrain_gen.SubTerrainCfg,
|
||||
) -> np.ndarray:
|
||||
"""Generate and render a single terrain type, return RGB array."""
|
||||
terrain_cfg = TerrainEntityCfg(
|
||||
terrain_type="generator",
|
||||
terrain_generator=TerrainGeneratorCfg(
|
||||
seed=42,
|
||||
size=PATCH_SIZE,
|
||||
num_rows=1,
|
||||
num_cols=1,
|
||||
border_width=0.0,
|
||||
curriculum=False,
|
||||
difficulty_range=(DIFFICULTY, DIFFICULTY),
|
||||
color_scheme="height",
|
||||
sub_terrains={name: sub_terrain_cfg},
|
||||
),
|
||||
lights=(
|
||||
spec_cfg.LightCfg(
|
||||
name="sun",
|
||||
type="directional",
|
||||
dir=(-0.15, -0.15, -0.97),
|
||||
castshadow=True,
|
||||
),
|
||||
),
|
||||
)
|
||||
terrain = TerrainEntity(terrain_cfg, device="cpu")
|
||||
|
||||
# Place camera at an isometric-ish angle to reveal 3D structure.
|
||||
elev = np.deg2rad(CAMERA_ELEVATION_DEG)
|
||||
azim = np.deg2rad(CAMERA_AZIMUTH_DEG)
|
||||
cam_pos = CAMERA_DISTANCE * np.array(
|
||||
[
|
||||
np.cos(elev) * np.cos(azim),
|
||||
np.cos(elev) * np.sin(azim),
|
||||
np.sin(elev),
|
||||
]
|
||||
)
|
||||
|
||||
# Look at a point slightly below the origin so the terrain centers
|
||||
# in the frame instead of sitting in the bottom half.
|
||||
lookat = np.array([0.0, 0.0, -2.0])
|
||||
forward = lookat - cam_pos
|
||||
forward /= np.linalg.norm(forward)
|
||||
# MuJoCo camera convention: -z is forward, y is up.
|
||||
up = np.array([0.0, 0.0, 1.0])
|
||||
right = np.cross(forward, up)
|
||||
right /= np.linalg.norm(right)
|
||||
cam_up = np.cross(right, forward)
|
||||
|
||||
# Build rotation matrix (columns: right, cam_up, -forward).
|
||||
rot = np.column_stack([right, cam_up, -forward])
|
||||
# Convert to quaternion (wxyz).
|
||||
from scipy.spatial.transform import Rotation
|
||||
|
||||
quat_xyzw = Rotation.from_matrix(rot).as_quat()
|
||||
quat_wxyz = [quat_xyzw[3], quat_xyzw[0], quat_xyzw[1], quat_xyzw[2]]
|
||||
|
||||
# FOV sized to fit the patch diagonal from this viewing angle.
|
||||
apparent_size = max(PATCH_SIZE) * FOV_PADDING
|
||||
fovy_rad = 2 * np.arctan2(apparent_size / 2, CAMERA_DISTANCE)
|
||||
|
||||
terrain.spec.worldbody.add_camera(
|
||||
name="gallery",
|
||||
pos=cam_pos.tolist(),
|
||||
quat=quat_wxyz,
|
||||
fovy=np.rad2deg(fovy_rad),
|
||||
)
|
||||
|
||||
model = terrain.spec.compile()
|
||||
model.vis.global_.offheight = HEIGHT
|
||||
model.vis.global_.offwidth = WIDTH
|
||||
data = mujoco.MjData(model)
|
||||
mujoco.mj_forward(model, data)
|
||||
|
||||
with mujoco.Renderer(model, height=HEIGHT, width=WIDTH) as renderer:
|
||||
renderer.update_scene(data, camera="gallery")
|
||||
return renderer.render()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for filename, sub_cfg in TERRAIN_TYPES:
|
||||
print(f"Rendering {filename}...")
|
||||
img = render_terrain(filename, sub_cfg)
|
||||
Image.fromarray(img).save(OUTPUT_DIR / f"{filename}.png")
|
||||
print(f" Saved {OUTPUT_DIR / filename}.png")
|
||||
|
||||
print(f"\nDone. {len(TERRAIN_TYPES)} images saved to {OUTPUT_DIR}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user