11import sys
22sys .path .append ('..' )
3- from visualization .trajectory_rendering import Visualization
3+ from visualization .trajectory_rendering import Visualization , _DT , _FUTURE_TIMESTEPS
44import torch
55import pytest
66from PIL import Image
@@ -44,4 +44,133 @@ def test_visualization_with_dummy_data(tmp_path: Path):
4444 assert result_img .mode == mock_map .mode , "Mode does not match"
4545 assert list (map_copy .getdata ()) == list (mock_map .getdata ()), "Original image mutated"
4646 assert list (result_img .getdata ()) != list (mock_map .getdata ()), "Image file was not created in the target directory"
47- assert os .path .isfile (output_path ), "Image file was not created in the target directory"
47+ assert os .path .isfile (output_path ), "Image file was not created in the target directory"
48+
49+ def test_accel_and_curv_to_meters_trajectory_straight_no_accel ():
50+ # 1. Create a dummy action sequence for going straight with no acceleration
51+ action_sequence = torch .zeros (_FUTURE_TIMESTEPS * 2 )
52+ current_speed = 10.0 # 10 m/s
53+
54+ # 2. Run the function
55+ trajectory_m = Visualization .accel_and_curv_to_meters_trajectory (action_sequence , current_speed , _FUTURE_TIMESTEPS )
56+
57+ # 3. Assertions
58+ assert trajectory_m .shape == (_FUTURE_TIMESTEPS + 1 , 2 ), "Shape of trajectory tensor is incorrect"
59+ # The car should move straight along the y-axis (forward)
60+ # X should be 0, Y should increase based on speed
61+ v = current_speed
62+ for i in range (1 , _FUTURE_TIMESTEPS + 1 ):
63+ # Note: In the function, positive Y is up, positive X is right.
64+ assert trajectory_m [i , 0 ].item () == pytest .approx (0.0 ), "X should be 0"
65+ assert trajectory_m [i , 1 ].item () > trajectory_m [i - 1 , 1 ].item (), "Y should be increasing"
66+ assert trajectory_m [i , 1 ].item () == pytest .approx (trajectory_m [i - 1 , 1 ].item () + v * _DT ), "Integration is incorrect"
67+
68+ def test_accel_and_curv_to_meters_trajectory_stationary ():
69+ # Edge case: 0 speed, 0 acceleration -> Car should remain at origin (0, 0)
70+ action_sequence = torch .zeros (_FUTURE_TIMESTEPS * 2 )
71+ current_speed = 0.0
72+
73+ trajectory_m = Visualization .accel_and_curv_to_meters_trajectory (action_sequence , current_speed , _FUTURE_TIMESTEPS )
74+
75+ for i in range (_FUTURE_TIMESTEPS + 1 ):
76+ assert trajectory_m [i , 0 ].item () == pytest .approx (0.0 )
77+ assert trajectory_m [i , 1 ].item () == pytest .approx (0.0 )
78+
79+ def test_accel_and_curv_to_meters_trajectory_constant_acceleration_from_standstill ():
80+ # Edge case: starting from 0 speed, but applying constant acceleration
81+ action_sequence = torch .zeros (_FUTURE_TIMESTEPS * 2 )
82+ action_sequence [0 ::2 ] = 2.0 # Constant 2.0 m/s^2 acceleration (every even index is accel)
83+ current_speed = 0.0
84+
85+ trajectory_m = Visualization .accel_and_curv_to_meters_trajectory (action_sequence , current_speed , _FUTURE_TIMESTEPS )
86+
87+ assert trajectory_m [0 , 0 ].item () == pytest .approx (0.0 )
88+ assert trajectory_m [0 , 1 ].item () == pytest .approx (0.0 )
89+
90+ # Check that distance covered in each timestep is strictly increasing
91+ for i in range (2 , _FUTURE_TIMESTEPS + 1 ):
92+ dist_prev = trajectory_m [i - 1 , 1 ].item () - trajectory_m [i - 2 , 1 ].item ()
93+ dist_curr = trajectory_m [i , 1 ].item () - trajectory_m [i - 1 , 1 ].item ()
94+
95+ assert trajectory_m [i , 0 ].item () == pytest .approx (0.0 ), "X should be 0, no curvature applied"
96+ assert dist_curr > dist_prev , "Distance per timestep should increase under constant acceleration"
97+
98+ def test_accel_and_curv_to_meters_trajectory_turning ():
99+ # Edge case: turning left with constant speed
100+ action_sequence = torch .zeros (_FUTURE_TIMESTEPS * 2 )
101+ action_sequence [1 ::2 ] = 0.1 # Constant positive curvature (left turn)
102+ current_speed = 10.0
103+
104+ trajectory_m = Visualization .accel_and_curv_to_meters_trajectory (action_sequence , current_speed , _FUTURE_TIMESTEPS )
105+
106+ # After 64 timesteps, X should be negative (left of the starting Y-axis) and Y should be positive
107+ assert trajectory_m [- 1 , 0 ].item () < - 0.1 , "Car should have turned left (negative X)"
108+ assert trajectory_m [- 1 , 1 ].item () > 0.1 , "Car should have moved forward (positive Y)"
109+
110+ def test_accel_and_curv_to_meters_trajectory_extreme_spiral ():
111+ # Edge case: extreme spiral
112+ # Constant acceleration and linearly increasing curvature.
113+ action_sequence = torch .zeros (_FUTURE_TIMESTEPS * 2 )
114+ action_sequence [0 ::2 ] = 0.5 # Constant acceleration
115+ action_sequence [1 ::2 ] = torch .linspace (0.5 , 1.0 , _FUTURE_TIMESTEPS ) # Increasing curvature
116+ current_speed = 5.0
117+
118+ trajectory_m = Visualization .accel_and_curv_to_meters_trajectory (action_sequence , current_speed , _FUTURE_TIMESTEPS )
119+
120+ assert not torch .isnan (trajectory_m ).any (), "Trajectory contains NaNs"
121+ assert not torch .isinf (trajectory_m ).any (), "Trajectory contains Infs"
122+
123+ # A tight spiral with these parameters will complete multiple full 360-degree rotations.
124+ # This means the vehicle must travel "backwards" relative to its start at some point.
125+ assert trajectory_m [:, 1 ].min ().item () < - 0.5 , "Car did not loop backwards significantly"
126+
127+ def test_meters_to_pixels_trajectory ():
128+ trajectory_m = torch .tensor ([
129+ [0.0 , 0.0 ],
130+ [10.0 , 0.0 ],
131+ [10.0 , 10.0 ],
132+ [0.0 , 10.0 ],
133+ ])
134+ radius_m = 20.0
135+ map_image = Image .new ("RGB" , (400 , 400 ))
136+
137+ trajectory_px = Visualization .meters_to_pixels_trajectory (trajectory_m , radius_m , map_image )
138+
139+ assert trajectory_px .shape == trajectory_m .shape
140+ # Check pixel coordinates
141+ # Origin (0,0) in meters is at the top-center of the image. Y is increasing down.
142+ # Image dimensions: 400x400. Center X is 200.
143+ # Meter to pixel scale: 400 pixels / (2 * 20m) = 10 pixels/meter
144+ assert trajectory_px [0 , 0 ] == 200 and trajectory_px [0 , 1 ] == 200 # Origin
145+ assert trajectory_px [1 , 0 ] == 300 and trajectory_px [1 , 1 ] == 200 # 10m right
146+ assert trajectory_px [2 , 0 ] == 300 and trajectory_px [2 , 1 ] == 100 # 10m right, 10m up
147+ assert trajectory_px [3 , 0 ] == 200 and trajectory_px [3 , 1 ] == 100 # 10m up
148+
149+ def test_overlay_the_trajectory_with_map ():
150+ map_image = Image .new ("RGB" , (400 , 400 ), color = "black" )
151+ trajectory_px = torch .tensor ([
152+ [200 , 399 ], # Start at bottom center, slightly off edge
153+ [300 , 399 ],
154+ [300 , 300 ],
155+ ])
156+
157+ overlaid_image = Visualization .overlay_the_trajectory_with_map (trajectory_px , map_image )
158+
159+ assert overlaid_image is not None
160+ assert isinstance (overlaid_image , Image .Image )
161+ assert overlaid_image .size == map_image .size
162+
163+ # Check if pixels are colored correctly
164+ # The trajectory should be a non-black color
165+ # We check points along the drawn line segments
166+ p1 = (trajectory_px [0 ,0 ].item (), trajectory_px [0 ,1 ].item ())
167+ p2 = (trajectory_px [1 ,0 ].item (), trajectory_px [1 ,1 ].item ())
168+ p3 = (trajectory_px [2 ,0 ].item (), trajectory_px [2 ,1 ].item ())
169+
170+ assert overlaid_image .getpixel (p1 ) != (0 , 0 , 0 )
171+ assert overlaid_image .getpixel (p2 ) != (0 , 0 , 0 )
172+ assert overlaid_image .getpixel (p3 ) != (0 , 0 , 0 )
173+
174+ # Check a point on the line between p1 and p2
175+ mid_p1_p2 = (int ((p1 [0 ]+ p2 [0 ])/ 2 ), int ((p1 [1 ]+ p2 [1 ])/ 2 ))
176+ assert overlaid_image .getpixel (mid_p1_p2 ) != (0 ,0 ,0 )
0 commit comments