Skip to content

Commit 14d3e5b

Browse files
committed
add heterogeneous object tests
1 parent 576e7fb commit 14d3e5b

5 files changed

Lines changed: 244 additions & 12 deletions

File tree

source/isaaclab/test/sensors/test_multi_mesh_ray_caster_camera.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"""Rest everything follows."""
1717

1818
import copy
19+
from collections.abc import Callable
1920

2021
import numpy as np
2122
import pytest
@@ -24,13 +25,18 @@
2425
import omni.replicator.core as rep
2526
from pxr import Gf
2627

28+
import isaaclab.cloner as lab_cloner
2729
import isaaclab.sim as sim_utils
30+
from isaaclab.cloner import ClonePlan
2831
from isaaclab.sensors.camera import Camera, CameraCfg
2932
from isaaclab.sensors.ray_caster import MultiMeshRayCasterCamera, MultiMeshRayCasterCameraCfg, patterns
3033
from isaaclab.sim import PinholeCameraCfg
3134
from isaaclab.terrains.trimesh.utils import make_plane
3235
from isaaclab.terrains.utils import create_prim_from_mesh
3336

37+
from isaaclab_assets.robots.anymal import ANYMAL_C_CFG
38+
from isaaclab_assets.robots.spot import SPOT_CFG
39+
3440
# sample camera poses (quaternions in xyzw format)
3541
POSITION = [2.5, 2.5, 2.5]
3642
QUAT_ROS = [0.33985114, 0.82047325, -0.42470819, -0.17591989]
@@ -433,6 +439,174 @@ def test_output_equal_to_usdcamera(setup_simulation, data_types):
433439
del camera_usd, camera_warp
434440

435441

442+
@pytest.mark.isaacsim_ci
443+
def test_depth_output_equal_to_usd_camera_heterogeneous_scene(setup_simulation):
444+
"""Compare ray-caster and USD depth cameras in a heterogeneous cloned scene.
445+
446+
The scene contains 16 environments with alternating Spot / ANYmal-C robot
447+
prototypes and alternating cube / sphere objects. The ray-caster consumes
448+
the same clone plan used to build the USD scene and should match the batched
449+
USD camera's stable ``distance_to_image_plane`` pixels for every environment.
450+
"""
451+
sim, dt, _ = setup_simulation
452+
num_envs = 16
453+
stage = sim_utils.get_current_stage()
454+
env_fmt = "/World/envs/env_{}"
455+
env_ids = torch.arange(num_envs, dtype=torch.long, device=sim.device)
456+
env_origins, _ = lab_cloner.grid_transforms(num_envs, spacing=4.0, device=sim.device)
457+
458+
sim_utils.create_prim("/World/envs", "Xform", stage=stage)
459+
for env_id, origin in enumerate(env_origins.cpu().tolist()):
460+
sim_utils.create_prim(env_fmt.format(env_id), "Xform", translation=tuple(origin), stage=stage)
461+
462+
# Prototype rows: even environments use Spot + cube, odd environments use ANYmal-C + sphere.
463+
robot_mask = torch.zeros((2, num_envs), dtype=torch.bool, device=sim.device)
464+
robot_mask[0, 0::2] = True
465+
robot_mask[1, 1::2] = True
466+
object_mask = robot_mask.clone()
467+
468+
spot_spawn = copy.deepcopy(SPOT_CFG.spawn)
469+
anymal_spawn = copy.deepcopy(ANYMAL_C_CFG.spawn)
470+
spot_spawn.func(env_fmt.format(0) + "/Robot", spot_spawn, translation=SPOT_CFG.init_state.pos)
471+
anymal_spawn.func(env_fmt.format(1) + "/Robot", anymal_spawn, translation=ANYMAL_C_CFG.init_state.pos)
472+
473+
cube_cfg = sim_utils.CuboidCfg(
474+
size=(0.35, 0.25, 0.25),
475+
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.7, 0.2, 0.2)),
476+
)
477+
sphere_cfg = sim_utils.SphereCfg(
478+
radius=0.18,
479+
visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.2, 0.2, 0.7)),
480+
)
481+
cube_spawn = cube_cfg.func
482+
sphere_spawn = sphere_cfg.func
483+
assert isinstance(cube_spawn, Callable)
484+
assert isinstance(sphere_spawn, Callable)
485+
cube_spawn(env_fmt.format(0) + "/Object", cube_cfg, translation=(0.45, 0.0, 0.25))
486+
sphere_spawn(env_fmt.format(1) + "/Object", sphere_cfg, translation=(0.45, 0.0, 0.25))
487+
488+
lab_cloner.usd_replicate(
489+
stage,
490+
[env_fmt.format(i) + f"/{asset_name}" for asset_name in ("Robot", "Object") for i in range(2)],
491+
[env_fmt + "/Robot", env_fmt + "/Robot", env_fmt + "/Object", env_fmt + "/Object"],
492+
env_ids,
493+
mask=torch.cat([robot_mask, object_mask], dim=0),
494+
)
495+
sim.set_clone_plan(
496+
ClonePlan(
497+
sources=[
498+
env_fmt.format(0) + "/Robot",
499+
env_fmt.format(1) + "/Robot",
500+
env_fmt.format(0) + "/Object",
501+
env_fmt.format(1) + "/Object",
502+
],
503+
destinations=[
504+
env_fmt + "/Robot",
505+
env_fmt + "/Robot",
506+
env_fmt + "/Object",
507+
env_fmt + "/Object",
508+
],
509+
clone_mask=torch.cat([robot_mask, object_mask], dim=0),
510+
)
511+
)
512+
sim_utils.update_stage()
513+
514+
height, width = 96, 128
515+
camera_pattern_cfg = patterns.PinholeCameraPatternCfg(
516+
focal_length=24.0,
517+
horizontal_aperture=20.955,
518+
height=height,
519+
width=width,
520+
)
521+
mesh_prim_paths = [
522+
"/World/defaultGroundPlane",
523+
MultiMeshRayCasterCameraCfg.RaycastTargetCfg(
524+
prim_expr="/World/envs/env_.*/Object",
525+
track_mesh_transforms=False,
526+
),
527+
MultiMeshRayCasterCameraCfg.RaycastTargetCfg(
528+
prim_expr="/World/envs/env_.*/Robot/.+",
529+
track_mesh_transforms=True,
530+
),
531+
]
532+
camera_cfg_warp = MultiMeshRayCasterCameraCfg(
533+
prim_path="/World/envs/env_.*/RayCasterCamera",
534+
mesh_prim_paths=mesh_prim_paths,
535+
update_period=0,
536+
debug_vis=False,
537+
pattern_cfg=camera_pattern_cfg,
538+
max_distance=25.0,
539+
data_types=["distance_to_image_plane"],
540+
depth_clipping_behavior="max",
541+
update_mesh_ids=True,
542+
)
543+
camera_warp = MultiMeshRayCasterCamera(camera_cfg_warp)
544+
545+
camera_cfg_usd = CameraCfg(
546+
height=height,
547+
width=width,
548+
prim_path="/World/envs/env_.*/UsdCamera",
549+
update_period=0,
550+
data_types=["distance_to_image_plane"],
551+
spawn=PinholeCameraCfg(
552+
focal_length=24.0,
553+
focus_distance=400.0,
554+
horizontal_aperture=20.955,
555+
clipping_range=(0.01, 25.0),
556+
),
557+
)
558+
camera_usd = Camera(camera_cfg_usd)
559+
560+
sim.reset()
561+
sim.play()
562+
563+
eyes = env_origins + torch.tensor((1.8, -2.5, 2.5), dtype=torch.float32, device=sim.device)
564+
targets = env_origins + torch.tensor((0.0, 0.0, 0.0), dtype=torch.float32, device=sim.device)
565+
camera_warp.set_world_poses_from_view(eyes=eyes, targets=targets)
566+
camera_usd.set_world_poses_from_view(eyes=eyes, targets=targets)
567+
568+
for _ in range(5):
569+
sim.render()
570+
571+
camera_usd.update(dt)
572+
camera_warp.update(dt)
573+
574+
ray_depth = camera_warp.data.output["distance_to_image_plane"]
575+
usd_depth = camera_usd.data.output["distance_to_image_plane"]
576+
assert ray_depth.shape == (num_envs, height, width, 1)
577+
assert usd_depth.shape == ray_depth.shape
578+
depth_diff = (ray_depth - usd_depth).abs()
579+
mesh_ids = getattr(camera_warp.data, "image_mesh_ids", None)
580+
assert mesh_ids is not None
581+
assert torch.any(mesh_ids == 1), "Expected object pixels in the heterogeneous scene"
582+
assert torch.any(mesh_ids > 1), "Expected robot pixels in the heterogeneous scene"
583+
584+
# The RTX and ray-cast backends can disagree by a pixel along complex robot
585+
# silhouettes. Compare the stable ground pixels after dilating object/robot
586+
# edges and depth discontinuities.
587+
target_mask = mesh_ids[..., 0] != 0
588+
discontinuity_mask = torch.zeros_like(target_mask)
589+
for depth in (ray_depth, usd_depth):
590+
depth_image = depth[..., 0]
591+
discontinuity_mask[:, 1:, :] |= (depth_image[:, 1:, :] - depth_image[:, :-1, :]).abs() > 0.3
592+
discontinuity_mask[:, :, 1:] |= (depth_image[:, :, 1:] - depth_image[:, :, :-1]).abs() > 0.3
593+
edge_mask = target_mask | discontinuity_mask
594+
silhouette_mask = torch.nn.functional.max_pool2d(
595+
edge_mask[:, None, :, :].float(), kernel_size=21, stride=1, padding=10
596+
).to(dtype=torch.bool)
597+
stable_mask = ~silhouette_mask[:, 0, :, :, None]
598+
assert stable_mask.float().mean() > 0.7
599+
stable_ray_depth = ray_depth[stable_mask]
600+
stable_usd_depth = usd_depth[stable_mask]
601+
stable_depth_diff = depth_diff[stable_mask]
602+
stable_close = torch.isclose(stable_ray_depth, stable_usd_depth, atol=5e-5, rtol=5e-6)
603+
assert stable_close.float().mean() > 0.999
604+
assert torch.quantile(stable_depth_diff, 0.999) < 5.0e-5
605+
assert torch.quantile(depth_diff, 0.99) < 5.0e-3
606+
607+
del camera_usd, camera_warp
608+
609+
436610
@pytest.mark.isaacsim_ci
437611
def test_output_equal_to_usdcamera_offset(setup_simulation):
438612
"""Test that ray caster camera output equals USD camera output with offset."""

source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/ray_caster.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ def _find_physics_ancestor(prim):
5555

5656

5757
def _newton_body_pattern(body_path: str) -> str:
58-
"""Strip a concrete env prefix so Newton can register a cloned body pattern."""
59-
return re.sub(r"^/World/envs/env_\d+/", "", body_path)
58+
"""Convert a concrete env index to a regex wildcard for prototype body matching."""
59+
return re.sub(r"^(/World/envs/)env_\d+/", r"\1env_.*/", body_path)
6060

6161

6262
def _xform_from_pose(pos, quat) -> wp.transform:

source/isaaclab_physx/isaaclab_physx/sensors/ray_caster/ray_caster.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,19 @@ def _create_tracked_target_view(self: Any, target_prim_path: str):
137137
prims = sim_utils.find_matching_prims(target_prim_path)
138138
if len(prims) == 0:
139139
raise RuntimeError(f"No tracked target prims matched: {target_prim_path}")
140-
body = _find_physics_ancestor(prims[0])
141-
if body is None:
142-
raise RuntimeError(
143-
f"Cannot track non-physics ray-cast target '{target_prim_path}' with PhysX. "
144-
"Set track_mesh_transforms=False for static targets, or apply RigidBodyAPI to dynamic targets."
145-
)
146-
body_expr = _body_expr_from_sensor_expr(target_prim_path, prims[0], body)
140+
body_paths = []
141+
for prim in prims:
142+
body = _find_physics_ancestor(prim)
143+
if body is None:
144+
raise RuntimeError(
145+
f"Cannot track non-physics ray-cast target '{target_prim_path}' with PhysX. "
146+
"Set track_mesh_transforms=False for static targets, or apply RigidBodyAPI to dynamic targets."
147+
)
148+
body_paths.append(body.GetPath().pathString)
147149
physics_sim_view = PhysxManager.get_physics_sim_view()
148150
if physics_sim_view is None:
149151
raise RuntimeError("PhysX simulation view is not initialized.")
150-
return physics_sim_view.create_rigid_body_view(body_expr.replace(".*", "*"))
152+
return physics_sim_view.create_rigid_body_view(body_paths)
151153

152154
def _update_mesh_transforms(self: Any) -> None:
153155
"""Refresh dynamic multi-mesh targets directly from PhysX views."""

source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/config/kuka_allegro/camera_cfg.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from isaaclab.managers import ObservationGroupCfg as ObsGroup
1010
from isaaclab.managers import ObservationTermCfg as ObsTerm
1111
from isaaclab.managers import SceneEntityCfg
12-
from isaaclab.sensors import CameraCfg
12+
from isaaclab.sensors import CameraCfg, MultiMeshRayCasterCameraCfg, patterns
1313
from isaaclab.utils import configclass
1414
from isaaclab.utils.noise import UniformNoiseCfg as Unoise
1515

@@ -50,6 +50,54 @@
5050
renderer_cfg=MultiBackendRendererCfg(),
5151
)
5252

53+
RAY_PATTERN = patterns.PinholeCameraPatternCfg(focal_length=24.0, horizontal_aperture=20.955)
54+
55+
RAYCASTER_CAMERA_MESH_PRIM_PATHS = [
56+
MultiMeshRayCasterCameraCfg.RaycastTargetCfg(
57+
prim_expr="/World/envs/env_.*/table",
58+
track_mesh_transforms=False,
59+
),
60+
MultiMeshRayCasterCameraCfg.RaycastTargetCfg(
61+
prim_expr="/World/GroundPlane",
62+
track_mesh_transforms=False,
63+
),
64+
MultiMeshRayCasterCameraCfg.RaycastTargetCfg(
65+
prim_expr="/World/envs/env_.*/Object",
66+
track_mesh_transforms=True,
67+
),
68+
MultiMeshRayCasterCameraCfg.RaycastTargetCfg(
69+
prim_expr="/World/envs/env_.*/Robot/.*/visuals",
70+
track_mesh_transforms=True,
71+
),
72+
]
73+
74+
BASE_RAYCASTER_CAMERA_CFG = MultiMeshRayCasterCameraCfg(
75+
prim_path="/World/envs/env_.*/Camera",
76+
offset=MultiMeshRayCasterCameraCfg.OffsetCfg(
77+
pos=(0.57, -0.8, 0.5),
78+
rot=(0.6124, 0.3536, 0.3536, 0.6124),
79+
convention="opengl",
80+
),
81+
mesh_prim_paths=RAYCASTER_CAMERA_MESH_PRIM_PATHS,
82+
max_distance=2.5,
83+
data_types=["distance_to_image_plane"],
84+
pattern_cfg=MISSING,
85+
)
86+
87+
WRIST_RAYCASTER_CAMERA_CFG = MultiMeshRayCasterCameraCfg(
88+
prim_path="/World/envs/env_.*/Robot/ee_link/palm_link/Camera",
89+
offset=MultiMeshRayCasterCameraCfg.OffsetCfg(
90+
pos=(0.038, -0.38, -0.18),
91+
rot=(0.641, 0.641, -0.299, 0.299),
92+
convention="opengl",
93+
),
94+
mesh_prim_paths=RAYCASTER_CAMERA_MESH_PRIM_PATHS,
95+
max_distance=2.5,
96+
data_types=["distance_to_image_plane"],
97+
pattern_cfg=MISSING,
98+
debug_vis=True,
99+
)
100+
53101

54102
@configclass
55103
class BaseTiledCameraCfg(PresetCfg):
@@ -88,6 +136,10 @@ class BaseTiledCameraCfg(PresetCfg):
88136
semantic_segmentation64 = BASE_CAMERA_CFG.replace(data_types=["semantic_segmentation"], width=64, height=64)
89137
semantic_segmentation128 = BASE_CAMERA_CFG.replace(data_types=["semantic_segmentation"], width=128, height=128)
90138
semantic_segmentation256 = BASE_CAMERA_CFG.replace(data_types=["semantic_segmentation"], width=256, height=256)
139+
# raycaster camera presets
140+
raycaster_depth64 = BASE_RAYCASTER_CAMERA_CFG.replace(pattern_cfg=RAY_PATTERN.replace(width=64, height=64))
141+
raycaster_depth128 = BASE_RAYCASTER_CAMERA_CFG.replace(pattern_cfg=RAY_PATTERN.replace(width=128, height=128))
142+
raycaster_depth256 = BASE_RAYCASTER_CAMERA_CFG.replace(pattern_cfg=RAY_PATTERN.replace(width=256, height=256))
91143
default = rgb64
92144

93145

@@ -128,6 +180,10 @@ class WristTiledCameraCfg(PresetCfg):
128180
semantic_segmentation64 = WRIST_CAMERA_CFG.replace(data_types=["semantic_segmentation"], width=64, height=64)
129181
semantic_segmentation128 = WRIST_CAMERA_CFG.replace(data_types=["semantic_segmentation"], width=128, height=128)
130182
semantic_segmentation256 = WRIST_CAMERA_CFG.replace(data_types=["semantic_segmentation"], width=256, height=256)
183+
# raycaster camera presets
184+
raycaster_depth64 = WRIST_RAYCASTER_CAMERA_CFG.replace(pattern_cfg=RAY_PATTERN.replace(width=64, height=64))
185+
raycaster_depth128 = WRIST_RAYCASTER_CAMERA_CFG.replace(pattern_cfg=RAY_PATTERN.replace(width=128, height=128))
186+
raycaster_depth256 = WRIST_RAYCASTER_CAMERA_CFG.replace(pattern_cfg=RAY_PATTERN.replace(width=256, height=256))
131187
default = rgb64
132188

133189

source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/dexsuite/dexsuite_env_cfg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ def validate_config(self):
442442
cam = getattr(self.scene, cam_attr, None)
443443
if cam is None:
444444
continue
445-
renderer_type = getattr(cam.renderer_cfg, "renderer_type", None)
445+
renderer_type = getattr(getattr(cam, "renderer_cfg", None), "renderer_type", None)
446446
if renderer_type == "newton_warp":
447447
unsupported = set(cam.data_types) - warp_supported
448448
if unsupported:

0 commit comments

Comments
 (0)