Skip to content

Commit f621ba1

Browse files
Gamenotsaulfield
andauthored
Extract panda3d renderer (#1921)
* Add interface to start extraction * Move renderer out * Update smarts/core/renderer_base.py Co-authored-by: Saul Field <[email protected]> * Fix renderer inheritance. * Finish extraction * Update tests. * Update changelog. * Fix typing issue. --------- Co-authored-by: Saul Field <[email protected]>
1 parent 6d35ed9 commit f621ba1

16 files changed

+463
-176
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ Copy and pasting the git commit messages is __NOT__ enough.
5151
- Benchmark listing may specify specialised metric formula for each benchmark.
5252
- Changed `benchmark_runner_v0.py` to only average records across scenarios that share the same environment. Records are not averaged across different environments, because the scoring formula may differ in different environments.
5353
- Renamed GapBetweenVehicles cost to VehicleGap cost in metric module.
54+
- Camera metadata now uses radians instead of degrees.
55+
- The `Panda3d` implementation of `Renderer` has been extracted from the interface and moved to `smarts.p3d`.
5456
### Deprecated
5557
### Fixed
5658
- Fixed issues related to waypoints in junctions on Argoverse maps. Waypoints will now be generated for all paths leading through the lane(s) the vehicle is on.

smarts/core/observations.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class GridMapMetadata(NamedTuple):
115115
"""Map height in # of cells."""
116116
camera_position: Tuple[float, float, float]
117117
"""Camera position when projected onto the map."""
118-
camera_heading_in_degrees: float
118+
camera_heading: float
119119
"""Camera rotation angle along z-axis when projected onto the map."""
120120

121121

smarts/core/renderer_base.py

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Copyright (C) 2020. Huawei Technologies Co., Ltd. All rights reserved.
2+
#
3+
# Permission is hereby granted, free of charge, to any person obtaining a copy
4+
# of this software and associated documentation files (the "Software"), to deal
5+
# in the Software without restriction, including without limitation the rights
6+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
# copies of the Software, and to permit persons to whom the Software is
8+
# furnished to do so, subject to the following conditions:
9+
#
10+
# The above copyright notice and this permission notice shall be included in
11+
# all copies or substantial portions of the Software.
12+
#
13+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
16+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
# THE SOFTWARE.
20+
21+
22+
# to allow for typing to refer to class being defined (Renderer)
23+
from __future__ import annotations
24+
25+
import logging
26+
from dataclasses import dataclass
27+
from enum import IntEnum
28+
from typing import Tuple
29+
30+
import numpy as np
31+
32+
from .coordinates import Pose
33+
34+
35+
class DEBUG_MODE(IntEnum):
36+
"""The rendering debug information level."""
37+
38+
SPAM = 1
39+
DEBUG = 2
40+
INFO = 3
41+
WARNING = 4
42+
ERROR = 5
43+
44+
45+
@dataclass
46+
class OffscreenCamera:
47+
"""A camera used for rendering images to a graphics buffer."""
48+
49+
renderer: RendererBase
50+
51+
def wait_for_ram_image(self, img_format: str, retries=100):
52+
"""Attempt to acquire a graphics buffer."""
53+
# Rarely, we see dropped frames where an image is not available
54+
# for our observation calculations.
55+
#
56+
# We've seen this happen fairly reliable when we are initializing
57+
# a multi-agent + multi-instance simulation.
58+
#
59+
# To deal with this, we can try to force a render and block until
60+
# we are fairly certain we have an image in ram to return to the user
61+
raise NotImplementedError
62+
63+
def update(self, pose: Pose, height: float):
64+
"""Update the location of the camera.
65+
Args:
66+
pose:
67+
The pose of the camera target.
68+
height:
69+
The height of the camera above the camera target.
70+
"""
71+
raise NotImplementedError
72+
73+
@property
74+
def image_dimensions(self) -> Tuple[int, int]:
75+
"""The dimensions of the output camera image."""
76+
raise NotImplementedError
77+
78+
@property
79+
def position(self) -> Tuple[float, float, float]:
80+
"""The position of the camera."""
81+
raise NotImplementedError
82+
83+
@property
84+
def heading(self) -> float:
85+
"""The heading of this camera."""
86+
raise NotImplementedError
87+
88+
def teardown(self):
89+
"""Clean up internal resources."""
90+
raise NotImplementedError
91+
92+
93+
class RendererBase:
94+
"""The base class for rendering
95+
96+
Returns:
97+
RendererBase:
98+
"""
99+
100+
@property
101+
def id(self):
102+
"""The id of the simulation rendered."""
103+
raise NotImplementedError
104+
105+
@property
106+
def is_setup(self) -> bool:
107+
"""If the renderer has been fully initialized."""
108+
raise NotImplementedError
109+
110+
@property
111+
def log(self) -> logging.Logger:
112+
"""The rendering logger."""
113+
raise NotImplementedError
114+
115+
def remove_buffer(self, buffer):
116+
"""Remove the rendering buffer."""
117+
raise NotImplementedError
118+
119+
def setup(self, scenario):
120+
"""Initialize this renderer."""
121+
raise NotImplementedError
122+
123+
def render(self):
124+
"""Render the scene graph of the simulation."""
125+
raise NotImplementedError
126+
127+
def reset(self):
128+
"""Reset the render back to initialized state."""
129+
raise NotImplementedError
130+
131+
def step(self):
132+
"""provided for non-SMARTS uses; normally not used by SMARTS."""
133+
raise NotImplementedError
134+
135+
def sync(self, sim_frame):
136+
"""Update the current state of the vehicles within the renderer."""
137+
raise NotImplementedError
138+
139+
def teardown(self):
140+
"""Clean up internal resources."""
141+
raise NotImplementedError
142+
143+
def destroy(self):
144+
"""Destroy the renderer. Cleans up all remaining renderer resources."""
145+
raise NotImplementedError
146+
147+
def create_vehicle_node(self, glb_model: str, vid: str, color, pose: Pose):
148+
"""Create a vehicle node."""
149+
raise NotImplementedError
150+
151+
def begin_rendering_vehicle(self, vid: str, is_agent: bool):
152+
"""Add the vehicle node to the scene graph"""
153+
raise NotImplementedError
154+
155+
def update_vehicle_node(self, vid: str, pose: Pose):
156+
"""Move the specified vehicle node."""
157+
raise NotImplementedError
158+
159+
def remove_vehicle_node(self, vid: str):
160+
"""Remove a vehicle node"""
161+
raise NotImplementedError
162+
163+
def camera_for_id(self, camera_id) -> OffscreenCamera:
164+
"""Get a camera by its id."""
165+
raise NotImplementedError
166+
167+
def build_offscreen_camera(
168+
self,
169+
name: str,
170+
mask: int,
171+
width: int,
172+
height: int,
173+
resolution: float,
174+
) -> OffscreenCamera:
175+
"""Generates a new offscreen camera."""
176+
raise NotImplementedError

smarts/core/sensor.py

+29-25
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
Vias,
4747
)
4848
from smarts.core.plan import Plan
49+
from smarts.core.renderer_base import RendererBase
4950
from smarts.core.road_map import RoadMap, Waypoint
5051
from smarts.core.signals import SignalState
5152
from smarts.core.utils.math import squared_dist
@@ -79,8 +80,8 @@ class CameraSensor(Sensor):
7980

8081
def __init__(
8182
self,
82-
vehicle_state,
83-
renderer, # type Renderer or None
83+
vehicle_state: VehicleState,
84+
renderer: RendererBase,
8485
name: str,
8586
mask: int,
8687
width: int,
@@ -108,7 +109,7 @@ def __eq__(self, __value: object) -> bool:
108109
)
109110

110111
def teardown(self, **kwargs):
111-
renderer = kwargs.get("renderer")
112+
renderer: Optional[RendererBase] = kwargs.get("renderer")
112113
if not renderer:
113114
return
114115
camera = renderer.camera_for_id(self._camera_name)
@@ -121,7 +122,7 @@ def step(self, sim_frame, **kwargs):
121122
sim_frame.actor_states_by_id[self._target_actor], kwargs.get("renderer")
122123
)
123124

124-
def _follow_actor(self, vehicle_state, renderer):
125+
def _follow_actor(self, vehicle_state: VehicleState, renderer: RendererBase):
125126
if not renderer:
126127
return
127128
camera = renderer.camera_for_id(self._camera_name)
@@ -137,11 +138,11 @@ class DrivableAreaGridMapSensor(CameraSensor):
137138

138139
def __init__(
139140
self,
140-
vehicle_state,
141+
vehicle_state: VehicleState,
141142
width: int,
142143
height: int,
143144
resolution: float,
144-
renderer, # type Renderer or None
145+
renderer: RendererBase,
145146
):
146147
super().__init__(
147148
vehicle_state,
@@ -154,22 +155,23 @@ def __init__(
154155
)
155156
self._resolution = resolution
156157

157-
def __call__(self, renderer) -> DrivableAreaGridMap:
158+
def __call__(self, renderer: RendererBase) -> DrivableAreaGridMap:
158159
camera = renderer.camera_for_id(self._camera_name)
159160
assert camera is not None, "Drivable area grid map has not been initialized"
160161

161162
ram_image = camera.wait_for_ram_image(img_format="A")
162163
mem_view = memoryview(ram_image)
163-
image = np.frombuffer(mem_view, np.uint8)
164-
image.shape = (camera.tex.getYSize(), camera.tex.getXSize(), 1)
164+
image: np.ndarray = np.frombuffer(mem_view, np.uint8)
165+
width, height = camera.image_dimensions
166+
image.shape = (height, width, 1)
165167
image = np.flipud(image)
166168

167169
metadata = GridMapMetadata(
168170
resolution=self._resolution,
169171
height=image.shape[0],
170172
width=image.shape[1],
171-
camera_position=camera.camera_np.getPos(),
172-
camera_heading_in_degrees=camera.camera_np.getH(),
173+
camera_position=camera.position,
174+
camera_heading=camera.heading,
173175
)
174176
return DrivableAreaGridMap(data=image, metadata=metadata)
175177

@@ -179,11 +181,11 @@ class OGMSensor(CameraSensor):
179181

180182
def __init__(
181183
self,
182-
vehicle_state,
184+
vehicle_state: VehicleState,
183185
width: int,
184186
height: int,
185187
resolution: float,
186-
renderer, # type Renderer or None
188+
renderer: RendererBase,
187189
):
188190
super().__init__(
189191
vehicle_state,
@@ -196,22 +198,23 @@ def __init__(
196198
)
197199
self._resolution = resolution
198200

199-
def __call__(self, renderer) -> OccupancyGridMap:
201+
def __call__(self, renderer: RendererBase) -> OccupancyGridMap:
200202
camera = renderer.camera_for_id(self._camera_name)
201203
assert camera is not None, "OGM has not been initialized"
202204

203205
ram_image = camera.wait_for_ram_image(img_format="A")
204206
mem_view = memoryview(ram_image)
205-
grid = np.frombuffer(mem_view, np.uint8)
206-
grid.shape = (camera.tex.getYSize(), camera.tex.getXSize(), 1)
207+
grid: np.ndarray = np.frombuffer(mem_view, np.uint8)
208+
width, height = camera.image_dimensions
209+
grid.shape = (height, width, 1)
207210
grid = np.flipud(grid)
208211

209212
metadata = GridMapMetadata(
210213
resolution=self._resolution,
211214
height=grid.shape[0],
212215
width=grid.shape[1],
213-
camera_position=camera.camera_np.getPos(),
214-
camera_heading_in_degrees=camera.camera_np.getH(),
216+
camera_position=camera.position,
217+
camera_heading=camera.heading,
215218
)
216219
return OccupancyGridMap(data=grid, metadata=metadata)
217220

@@ -221,11 +224,11 @@ class RGBSensor(CameraSensor):
221224

222225
def __init__(
223226
self,
224-
vehicle_state,
227+
vehicle_state: VehicleState,
225228
width: int,
226229
height: int,
227230
resolution: float,
228-
renderer, # type Renderer or None
231+
renderer: RendererBase,
229232
):
230233
super().__init__(
231234
vehicle_state,
@@ -238,22 +241,23 @@ def __init__(
238241
)
239242
self._resolution = resolution
240243

241-
def __call__(self, renderer) -> TopDownRGB:
244+
def __call__(self, renderer: RendererBase) -> TopDownRGB:
242245
camera = renderer.camera_for_id(self._camera_name)
243246
assert camera is not None, "RGB has not been initialized"
244247

245248
ram_image = camera.wait_for_ram_image(img_format="RGB")
246249
mem_view = memoryview(ram_image)
247-
image = np.frombuffer(mem_view, np.uint8)
248-
image.shape = (camera.tex.getYSize(), camera.tex.getXSize(), 3)
250+
image: np.ndarray = np.frombuffer(mem_view, np.uint8)
251+
width, height = camera.image_dimensions
252+
image.shape = (height, width, 3)
249253
image = np.flipud(image)
250254

251255
metadata = GridMapMetadata(
252256
resolution=self._resolution,
253257
height=image.shape[0],
254258
width=image.shape[1],
255-
camera_position=camera.camera_np.getPos(),
256-
camera_heading_in_degrees=camera.camera_np.getH(),
259+
camera_position=camera.position,
260+
camera_heading=camera.heading,
257261
)
258262
return TopDownRGB(data=image, metadata=metadata)
259263

smarts/core/sensor_manager.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from typing import Dict, FrozenSet, List, Optional, Set, Tuple
2525

2626
from smarts.core import config
27+
from smarts.core.renderer_base import RendererBase
2728
from smarts.core.sensors import Observation, Sensor, Sensors, SensorState
2829
from smarts.core.sensors.local_sensor_resolver import LocalSensorResolver
2930
from smarts.core.sensors.parallel_sensor_resolver import ParallelSensorResolver
@@ -85,7 +86,7 @@ def observe(
8586
sim_frame,
8687
sim_local_constants,
8788
agent_ids,
88-
renderer_ref,
89+
renderer_ref: RendererBase,
8990
physics_ref,
9091
):
9192
"""Runs observations and updates the sensor states.
@@ -96,7 +97,7 @@ def observe(
9697
The values that should stay the same for a simulation over a reset.
9798
agent_ids ({str, ...}):
9899
The agent ids to process.
99-
renderer_ref (Optional[Renderer]):
100+
renderer_ref (Optional[RendererBase]):
100101
The renderer (if any) that should be used.
101102
physics_ref (bc.BulletClient):
102103
The physics client.

0 commit comments

Comments
 (0)