Skip to content

Commit c968a0e

Browse files
committed
Add test for semantic point filtering
1 parent 0da2395 commit c968a0e

File tree

1 file changed

+137
-4
lines changed

1 file changed

+137
-4
lines changed

exts/nav_suite/tests/test_terrain_analysis.py

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,65 @@
2020
from isaaclab.sensors import RayCasterCfg, patterns
2121
from isaaclab.sim import build_simulation_context
2222
from isaaclab.utils import configclass
23+
from isaaclab.utils.warp import raycast_mesh
2324
from isaaclab_assets.robots.anymal import ANYMAL_C_CFG
2425

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
2628
from nav_suite.terrain_analysis import TerrainAnalysis, TerrainAnalysisCfg
2729
from nav_suite.terrains import NavTerrainImporterCfg
2830

2931

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+
3082
@configclass
3183
class BasicSceneCfg(InteractiveSceneCfg):
3284
"""Configuration for a basic test scene with terrain."""
@@ -56,6 +108,35 @@ class BasicSceneCfg(InteractiveSceneCfg):
56108
)
57109

58110

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+
59140
@pytest.fixture(params=["cuda", "cpu"])
60141
def device(request):
61142
"""Fixture providing both cuda and cpu devices for testing."""
@@ -129,6 +210,39 @@ def terrain_analysis_real(scene):
129210
return terrain_analysis
130211

131212

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+
132246
def test_get_height_single_position(terrain_analysis_test):
133247
"""Test get_height with a single position."""
134248
# Test a position that should map to grid index [0, 0]
@@ -194,14 +308,33 @@ def test_get_height_empty_input(terrain_analysis_test):
194308
def test_analyse_basic_functionality(terrain_analysis_real):
195309
"""Test that analyse() completes without errors and sets expected attributes."""
196310

197-
# Run analysis - this will automatically setup the raycaster and construct height map
311+
# Run analysis
198312
terrain_analysis_real.analyse()
199313

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
202315
assert hasattr(terrain_analysis_real, "graph"), "graph attribute should be set after analyse()"
203316
assert hasattr(terrain_analysis_real, "samples"), "samples attribute should be set after analyse()"
204317
assert hasattr(terrain_analysis_real, "points"), "points attribute should be set after analyse()"
205318
assert terrain_analysis_real.graph is not None, "graph should not be None after analyse()"
206319
assert terrain_analysis_real.samples is not None, "samples should not be None after analyse()"
207320
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

Comments
 (0)