|
20 | 20 | from isaaclab.sensors import RayCasterCfg, patterns
|
21 | 21 | from isaaclab.sim import build_simulation_context
|
22 | 22 | from isaaclab.utils import configclass
|
| 23 | +from isaaclab.utils.warp import raycast_mesh |
23 | 24 | from isaaclab_assets.robots.anymal import ANYMAL_C_CFG
|
24 | 25 |
|
25 |
| -from nav_suite import NAVSUITE_TEST_ASSETS_DIR |
| 26 | +from nav_suite import NAVSUITE_DATA_DIR, NAVSUITE_TEST_ASSETS_DIR |
| 27 | +from nav_suite.sensors import MatterportRayCaster, MatterportRayCasterCamera, MatterportRayCasterCfg |
26 | 28 | from nav_suite.terrain_analysis import TerrainAnalysis, TerrainAnalysisCfg
|
27 | 29 | from nav_suite.terrains import NavTerrainImporterCfg
|
28 | 30 |
|
29 | 31 |
|
| 32 | +def _get_semantic_costs_for_points(terrain_analysis): |
| 33 | + """Helper method to get semantic costs for points in the terrain analysis. |
| 34 | +
|
| 35 | + Args: |
| 36 | + terrain_analysis: TerrainAnalysis instance with analyzed points |
| 37 | +
|
| 38 | + Returns: |
| 39 | + Tensor of semantic costs for each point, shape (N,) |
| 40 | + """ |
| 41 | + |
| 42 | + points = terrain_analysis.points |
| 43 | + if points is None or points.shape[0] == 0: |
| 44 | + return torch.tensor([], device=terrain_analysis.scene.device) |
| 45 | + |
| 46 | + # raycast vertically down from each point |
| 47 | + ray_directions = torch.zeros((points.shape[0], 3), dtype=torch.float32, device=points.device) |
| 48 | + ray_directions[:, 2] = -1.0 |
| 49 | + |
| 50 | + if isinstance(terrain_analysis._raycaster, MatterportRayCaster | MatterportRayCasterCamera): |
| 51 | + ray_face_ids = raycast_mesh( |
| 52 | + ray_starts=points.unsqueeze(0), |
| 53 | + ray_directions=ray_directions.unsqueeze(0), |
| 54 | + max_dist=terrain_analysis.cfg.wall_height * 2 - terrain_analysis._mesh_height_dimensions[0] + 1e2, |
| 55 | + return_face_id=True, |
| 56 | + **terrain_analysis._raycaster_mesh_param, |
| 57 | + )[3] |
| 58 | + |
| 59 | + # assign each hit the semantic class |
| 60 | + class_id = terrain_analysis._raycaster.face_id_category_mapping[ |
| 61 | + terrain_analysis._raycaster.cfg.mesh_prim_paths[0] |
| 62 | + ][ray_face_ids.flatten().type(torch.long)] |
| 63 | + # map category index to reduced set |
| 64 | + class_id = terrain_analysis._raycaster.mapping_mpcat40[class_id.type(torch.long) - 1] |
| 65 | + |
| 66 | + # get class_id to cost mapping |
| 67 | + class_id_to_cost = torch.ones(len(terrain_analysis._raycaster.classes_mpcat40), device=points.device) * max( |
| 68 | + list(terrain_analysis.semantic_costs.values()) |
| 69 | + ) |
| 70 | + for class_name, class_cost in terrain_analysis.semantic_costs.items(): |
| 71 | + class_id_to_cost[terrain_analysis._raycaster.classes_mpcat40 == class_name] = class_cost |
| 72 | + |
| 73 | + # get cost |
| 74 | + cost = class_id_to_cost[class_id] |
| 75 | + else: |
| 76 | + # Handle non-Matterport raycasters if needed |
| 77 | + raise NotImplementedError("Only Matterport raycasters supported in this helper") |
| 78 | + |
| 79 | + return cost |
| 80 | + |
| 81 | + |
30 | 82 | @configclass
|
31 | 83 | class BasicSceneCfg(InteractiveSceneCfg):
|
32 | 84 | """Configuration for a basic test scene with terrain."""
|
@@ -56,6 +108,35 @@ class BasicSceneCfg(InteractiveSceneCfg):
|
56 | 108 | )
|
57 | 109 |
|
58 | 110 |
|
| 111 | +@configclass |
| 112 | +class MatterportSceneCfg(InteractiveSceneCfg): |
| 113 | + """Configuration for a Matterport test scene with semantic terrain analysis.""" |
| 114 | + |
| 115 | + terrain = NavTerrainImporterCfg( |
| 116 | + prim_path="/World/Ground", |
| 117 | + terrain_type="usd", |
| 118 | + usd_path=os.path.join(NAVSUITE_DATA_DIR, "matterport", "2n8kARJN3HM_asset", "2n8kARJN3HM.usd"), |
| 119 | + num_envs=1, |
| 120 | + env_spacing=2.0, |
| 121 | + add_colliders=True, |
| 122 | + ) |
| 123 | + |
| 124 | + robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") |
| 125 | + |
| 126 | + # Matterport raycaster for terrain analysis with semantic information |
| 127 | + matterport_raycaster = MatterportRayCasterCfg( |
| 128 | + prim_path="{ENV_REGEX_NS}/Robot/base/lidar_cage", |
| 129 | + update_period=0, |
| 130 | + debug_vis=False, |
| 131 | + pattern_cfg=patterns.GridPatternCfg( |
| 132 | + resolution=0.1, |
| 133 | + size=(1.0, 1.0), |
| 134 | + ), |
| 135 | + mesh_prim_paths=[os.path.join("matterport", "2n8kARJN3HM_asset", "2n8kARJN3HM.ply")], |
| 136 | + attach_yaw_only=True, |
| 137 | + ) |
| 138 | + |
| 139 | + |
59 | 140 | @pytest.fixture(params=["cuda", "cpu"])
|
60 | 141 | def device(request):
|
61 | 142 | """Fixture providing both cuda and cpu devices for testing."""
|
@@ -129,6 +210,39 @@ def terrain_analysis_real(scene):
|
129 | 210 | return terrain_analysis
|
130 | 211 |
|
131 | 212 |
|
| 213 | +@pytest.fixture |
| 214 | +def matterport_scene(simulation_context): |
| 215 | + """Fixture providing an InteractiveScene with Matterport environment.""" |
| 216 | + scene_cfg = MatterportSceneCfg(num_envs=1, env_spacing=2.0) |
| 217 | + scene = InteractiveScene(scene_cfg) |
| 218 | + simulation_context.reset() |
| 219 | + return scene |
| 220 | + |
| 221 | + |
| 222 | +@pytest.fixture |
| 223 | +def terrain_analysis_matterport(matterport_scene): |
| 224 | + """Fixture providing terrain analysis with Matterport environment for semantic filtering tests.""" |
| 225 | + |
| 226 | + # Path to the real semantic costs YAML file |
| 227 | + semantic_costs_path = os.path.join(NAVSUITE_DATA_DIR, "matterport", "semantic_costs.yaml") |
| 228 | + |
| 229 | + # Create terrain analysis configuration with semantic filtering enabled |
| 230 | + terrain_analysis_cfg = TerrainAnalysisCfg( |
| 231 | + grid_resolution=0.5, |
| 232 | + sample_points=50, |
| 233 | + viz_graph=False, # Disable visualization for tests |
| 234 | + viz_height_map=False, |
| 235 | + semantic_cost_mapping=semantic_costs_path, |
| 236 | + semantic_point_filter=True, # Enable semantic point filtering |
| 237 | + semantic_cost_threshold=0.5, # Filter points with cost > 0.5 (obstacles have cost 1.0) |
| 238 | + raycaster_sensor="matterport_raycaster", |
| 239 | + ) |
| 240 | + |
| 241 | + terrain_analysis = TerrainAnalysis(terrain_analysis_cfg, scene=matterport_scene) |
| 242 | + |
| 243 | + return terrain_analysis |
| 244 | + |
| 245 | + |
132 | 246 | def test_get_height_single_position(terrain_analysis_test):
|
133 | 247 | """Test get_height with a single position."""
|
134 | 248 | # Test a position that should map to grid index [0, 0]
|
@@ -194,14 +308,33 @@ def test_get_height_empty_input(terrain_analysis_test):
|
194 | 308 | def test_analyse_basic_functionality(terrain_analysis_real):
|
195 | 309 | """Test that analyse() completes without errors and sets expected attributes."""
|
196 | 310 |
|
197 |
| - # Run analysis - this will automatically setup the raycaster and construct height map |
| 311 | + # Run analysis |
198 | 312 | terrain_analysis_real.analyse()
|
199 | 313 |
|
200 |
| - # Verify analysis completed and required attributes are set |
201 |
| - assert terrain_analysis_real.complete, "TerrainAnalysis should be complete after analyse()" |
| 314 | + # Verify required attributes are set |
202 | 315 | assert hasattr(terrain_analysis_real, "graph"), "graph attribute should be set after analyse()"
|
203 | 316 | assert hasattr(terrain_analysis_real, "samples"), "samples attribute should be set after analyse()"
|
204 | 317 | assert hasattr(terrain_analysis_real, "points"), "points attribute should be set after analyse()"
|
205 | 318 | assert terrain_analysis_real.graph is not None, "graph should not be None after analyse()"
|
206 | 319 | assert terrain_analysis_real.samples is not None, "samples should not be None after analyse()"
|
207 | 320 | assert terrain_analysis_real.points is not None, "points should not be None after analyse()"
|
| 321 | + |
| 322 | + |
| 323 | + |
| 324 | +def test_analyse_semantic_point_filtering(terrain_analysis_matterport): |
| 325 | + """Test semantic point filtering using real Matterport environment and MatterportRayCaster.""" |
| 326 | + |
| 327 | + # Run analysis |
| 328 | + terrain_analysis_matterport.analyse() |
| 329 | + |
| 330 | + assert terrain_analysis_matterport.points is not None, "Terrain Analysis should have sampled points" |
| 331 | + |
| 332 | + # Get semantic costs for all points using helper method |
| 333 | + point_costs = _get_semantic_costs_for_points(terrain_analysis_matterport) |
| 334 | + |
| 335 | + # Assert that all points in filtered analysis have semantic cost <= threshold |
| 336 | + max_cost = point_costs.max().item() |
| 337 | + assert max_cost <= terrain_analysis_matterport.cfg.semantic_cost_threshold, ( |
| 338 | + f"Semantic filtering failed: found points with cost {max_cost} > threshold " |
| 339 | + f"{terrain_analysis_matterport.cfg.semantic_cost_threshold}" |
| 340 | + ) |
0 commit comments