55
66"""Sim-free tests for the Cartpole camera observation term.
77
8- The term must hand the policy a float32 tensor for every camera data type. Segmentation is the
9- interesting case: it is ``uint8`` RGBA when colorized and ``int32`` label ids when not, and the
10- feature extractor's first convolution rejects both integer dtypes .
8+ Segmentation output is ``uint8`` RGBA when colorized and ``int32`` label ids when not. Either
9+ integer dtype crashes the feature extractor's first convolution, so the term must return float32
10+ for both. ``frame_stack`` is parametrized because the stacking path defers normalization .
1111"""
1212
1313from __future__ import annotations
@@ -29,56 +29,44 @@ def device(request):
2929 return request .param
3030
3131
32- def _make_env ( camera_output : dict [ str , torch .Tensor ] , frame_stack : int , device : str ) -> SimpleNamespace :
33- """Build the minimal environment stub the observation term reads from ."""
34- camera = SimpleNamespace (data = SimpleNamespace (output = camera_output ))
35- return SimpleNamespace (
32+ def _observe ( images : torch .Tensor , frame_stack : int , device : str ) -> torch . Tensor :
33+ """Run the observation term over ``images`` using a minimal environment stub ."""
34+ camera = SimpleNamespace (data = SimpleNamespace (output = { "semantic_segmentation" : images } ))
35+ env = SimpleNamespace (
3636 cfg = SimpleNamespace (frame_stack = frame_stack ),
37- num_envs = next ( iter ( camera_output . values ())) .shape [0 ],
37+ num_envs = images .shape [0 ],
3838 device = device ,
3939 scene = SimpleNamespace (sensors = {"tiled_camera" : camera }),
4040 )
41-
42-
43- @pytest .mark .parametrize ("frame_stack" , [1 , 2 ])
44- @pytest .mark .parametrize (
45- "dtype,num_channels" ,
46- [(torch .uint8 , 4 ), (torch .int32 , 1 )],
47- ids = ["colorized_uint8_rgba" , "non_colorized_int32_labels" ],
48- )
49- def test_semantic_segmentation_observation_is_float32 (device , frame_stack , dtype , num_channels ):
50- """Segmentation observations are float32 regardless of the renderer's ``colorize`` setting."""
51- torch .manual_seed (0 )
52- images = torch .randint (0 , 5 , (2 , 8 , 8 , num_channels ), dtype = dtype , device = device )
53- env = _make_env ({"semantic_segmentation" : images }, frame_stack , device )
5441 term = CameraImageStack (ObservationTermCfg (func = CameraImageStack ), env )
42+ return term (env , SceneEntityCfg ("tiled_camera" ), "semantic_segmentation" )
5543
56- observation = term (env , SceneEntityCfg ("tiled_camera" ), "semantic_segmentation" )
5744
58- assert observation .dtype == torch .float32
59- assert observation .shape == (2 , frame_stack * num_channels , 8 , 8 )
45+ def _to_expected_layout (images : torch .Tensor , frame_stack : int ) -> torch .Tensor :
46+ """Convert BHWC to the channel-first layout, repeated as the ring buffer fills on first append."""
47+ return images .permute (0 , 3 , 1 , 2 ).repeat (1 , frame_stack , 1 , 1 )
6048
6149
62- def test_colorized_segmentation_matches_rgb_normalization (device ):
63- """Colorized segmentation gets the same ``(x / 255) - per-image mean`` treatment as RGB."""
50+ @pytest .mark .parametrize ("frame_stack" , [1 , 2 ])
51+ def test_colorized_segmentation_is_normalized_like_rgb (device , frame_stack ):
52+ """Colorized uint8 RGBA segmentation gets the same ``(x / 255) - per-image mean`` as RGB."""
6453 torch .manual_seed (0 )
6554 images = torch .randint (0 , 255 , (2 , 8 , 8 , 4 ), dtype = torch .uint8 , device = device )
66- env = _make_env ({"semantic_segmentation" : images }, frame_stack = 1 , device = device )
67- term = CameraImageStack (ObservationTermCfg (func = CameraImageStack ), env )
6855
69- observation = term ( env , SceneEntityCfg ( "tiled_camera" ), "semantic_segmentation" )
56+ observation = _observe ( images , frame_stack , device )
7057
7158 expected = images .float () / 255.0
7259 expected = expected - torch .mean (expected , dim = (1 , 2 ), keepdim = True )
73- torch .testing .assert_close (observation , expected .permute (0 , 3 , 1 , 2 ), atol = 1e-5 , rtol = 1e-5 )
60+ assert observation .dtype == torch .float32
61+ torch .testing .assert_close (observation , _to_expected_layout (expected , frame_stack ), atol = 1e-5 , rtol = 1e-5 )
7462
7563
76- def test_non_colorized_segmentation_preserves_label_ids (device ):
77- """Label ids carry no scale, so the int32 map is only cast, never rescaled."""
64+ @pytest .mark .parametrize ("frame_stack" , [1 , 2 ])
65+ def test_non_colorized_segmentation_is_cast_to_float (device , frame_stack ):
66+ """Non-colorized int32 label ids are cast to float32 and, carrying no scale, left unrescaled."""
7867 images = torch .arange (2 * 8 * 8 , dtype = torch .int32 , device = device ).reshape (2 , 8 , 8 , 1 ) % 5
79- env = _make_env ({"semantic_segmentation" : images }, frame_stack = 1 , device = device )
80- term = CameraImageStack (ObservationTermCfg (func = CameraImageStack ), env )
8168
82- observation = term ( env , SceneEntityCfg ( "tiled_camera" ), "semantic_segmentation" )
69+ observation = _observe ( images , frame_stack , device )
8370
84- torch .testing .assert_close (observation , images .to (torch .float32 ).permute (0 , 3 , 1 , 2 ))
71+ assert observation .dtype == torch .float32
72+ torch .testing .assert_close (observation , _to_expected_layout (images .float (), frame_stack ))
0 commit comments