Skip to content

Commit b0077d3

Browse files
authored
refactor: add ViewerBuilder (#193)
* feat: adopt builder pattern using ViewerBuilder Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> docs: update tutorial document Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> feat: adopt builder pattern using ViewerBuilder Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * feat: add decorators to check if the corresponding view space exists Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * docs: update document Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> --------- Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp>
1 parent 47eb584 commit b0077d3

8 files changed

Lines changed: 281 additions & 217 deletions

File tree

docs/tutorials/render.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,18 @@ When you specify `save_dir`, viewer will not be spawned on your screen.
6363
If you want to visualize your components, such as boxes that your ML-model estimated, `RerunViewer` allows you to visualize these components.
6464
For details, please refer to the API references.
6565

66+
To initialize `RerunViewer`, you can use the `ViewerBuilder` class:
67+
6668
```python
67-
>>> from t4_devkit.viewer import RerunViewer
69+
>>> from t4_devkit.viewer import ViewerBuilder
6870
# You need to specify `cameras` if you want to 2D spaces
69-
>>> viewer = RerunViewer("foo", cameras=<CAMERA_NAMES:[str;N]>)
71+
>>> viewer = (
72+
ViewerBuilder()
73+
.with_spatial3d()
74+
.with_spatial2d(cameras=["CAM_FRONT", "CAM_BACK"], projection=True)
75+
.with_labels({"car": 1, "pedestrian": 2})
76+
.build("foo")
77+
)
7078

7179
# Timestamp in seconds
7280
>>> seconds: int | float = ...
@@ -86,10 +94,11 @@ It allows you to render boxes by specifying elements of boxes directly.
8694
```python
8795
# Rendering 3D boxes
8896
>>> centers = [[i, i, i] for i in range(10)]
97+
>>> frame_id = "base_link"
8998
>>> rotations = [[1, 0, 0, 0] for _ in range(10)]
9099
>>> sizes = [[1, 1, 1] for _ in range(10)]
91100
>>> class_ids = [0 for _ in range(10)]
92-
>>> viewer.render_box3ds(seconds, centers, rotations, sizes, class_ids)
101+
>>> viewer.render_box3ds(seconds, frame_id, centers, rotations, sizes, class_ids)
93102
```
94103

95104
![Render Box3Ds](../assets/render_box3ds.png)
@@ -100,7 +109,13 @@ For 2D spaces, you need to specify camera names in the viewer constructor, and r
100109

101110
```python
102111
# RerunViewer(<APP_ID:str>, cameras=<CAMERA_NAMES:[str;N]>)
103-
>>> viewer = RerunViewer("foo", cameras=["camera1"])
112+
>>> viewer = (
113+
ViewerBuilder()
114+
.with_spatial3d()
115+
.with_spatial2d(cameras=["camera1"])
116+
.with_labels({"car": 1, "pedestrian": 2})
117+
.build("foo")
118+
)
104119

105120
>>> import numpy as np
106121
>>> image = np.zeros((100, 100, 3), dtype=np.uint8)

t4_devkit/helper/rendering.py

Lines changed: 14 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from t4_devkit.common.timestamp import sec2us, us2sec
1717
from t4_devkit.dataclass import LidarPointCloud, RadarPointCloud
1818
from t4_devkit.schema import SensorModality
19-
from t4_devkit.viewer import RerunViewer, distance_color, format_entity
19+
from t4_devkit.viewer import RerunViewer, ViewerBuilder, distance_color, format_entity
2020

2121
if TYPE_CHECKING:
2222
from t4_devkit import Tier4
@@ -57,44 +57,31 @@ def _init_viewer(
5757
self,
5858
app_id: str,
5959
*,
60-
render3d: bool = True,
61-
render2d: bool = True,
6260
render_ann: bool = True,
6361
save_dir: str | None = None,
6462
) -> RerunViewer:
65-
if not (render3d or render2d):
66-
raise ValueError("At least one of `render3d` or `render2d` must be True.")
67-
68-
cameras = (
69-
[
70-
sensor.channel
71-
for sensor in self._t4.sensor
72-
if sensor.modality == SensorModality.CAMERA
73-
]
74-
if render2d
75-
else None
76-
)
63+
cameras = [
64+
sensor.channel for sensor in self._t4.sensor if sensor.modality == SensorModality.CAMERA
65+
]
7766

78-
viewer = RerunViewer(
79-
app_id=app_id,
80-
cameras=cameras,
81-
with_3d=render3d,
82-
save_dir=save_dir,
67+
# project 3D boxes if there is no 2D annotation
68+
projection = len(self._t4.object_ann) == 0 and len(self._t4.surface_ann) == 0
69+
builder = (
70+
ViewerBuilder().with_spatial3d().with_spatial2d(cameras=cameras, projection=projection)
8371
)
8472

8573
if render_ann:
86-
viewer = viewer.with_labels(self._label2id)
74+
builder = builder.with_labels(self._label2id)
8775

8876
global_map_filepath = osp.join(self._t4.data_root, "map/global_map_center.pcd.yaml")
8977
if osp.exists(global_map_filepath):
9078
with open(global_map_filepath) as f:
9179
map_metadata: dict = yaml.safe_load(f)
9280
map_origin: dict = map_metadata["/**"]["ros__parameters"]["map_origin"]
93-
latitude = map_origin["latitude"]
94-
longitude = map_origin["longitude"]
95-
viewer = viewer.with_global_origin((latitude, longitude))
81+
latitude, longitude = map_origin["latitude"], map_origin["longitude"]
82+
builder = builder.with_streetmap((latitude, longitude))
9683

97-
return viewer
84+
return builder.build(app_id, save_dir=save_dir)
9885

9986
def render_scene(
10087
self,
@@ -128,17 +115,8 @@ def render_scene(
128115
if sensor.modality == SensorModality.CAMERA
129116
]
130117

131-
render3d = len(first_lidar_tokens) > 0 or len(first_radar_tokens) > 0
132-
render2d = len(first_camera_tokens) > 0
133-
134118
app_id = f"scene@{self._t4.dataset_id}"
135-
viewer = self._init_viewer(
136-
app_id,
137-
render3d=render3d,
138-
render2d=render2d,
139-
render_ann=True,
140-
save_dir=save_dir,
141-
)
119+
viewer = self._init_viewer(app_id, render_ann=True, save_dir=save_dir)
142120

143121
self._render_map(viewer)
144122

@@ -236,17 +214,8 @@ def render_instance(
236214
if sensor.modality == SensorModality.CAMERA
237215
]
238216

239-
render3d = len(first_lidar_tokens) > 0 or len(first_radar_tokens) > 0
240-
render2d = len(first_camera_tokens) > 0
241-
242217
app_id = f"instance@{self._t4.dataset_id}"
243-
viewer = self._init_viewer(
244-
app_id,
245-
render3d=render3d,
246-
render2d=render2d,
247-
render_ann=True,
248-
save_dir=save_dir,
249-
)
218+
viewer = self._init_viewer(app_id, render_ann=True, save_dir=save_dir)
250219

251220
self._render_map(viewer)
252221

t4_devkit/viewer/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from .builder import * # noqa
12
from .color import * # noqa
23
from .geography import * # noqa
34
from .viewer import * # noqa
5+
from .config import * # noqa

t4_devkit/viewer/builder.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Sequence
4+
5+
import rerun.blueprint as rrb
6+
from typing_extensions import Self
7+
8+
from .config import ViewerConfig, format_entity
9+
from .viewer import RerunViewer
10+
11+
if TYPE_CHECKING:
12+
from t4_devkit.typing import Vector2Like
13+
14+
__all__ = ["ViewerBuilder"]
15+
16+
17+
class ViewerBuilder:
18+
"""Builder for creating a RerunViewer instance.
19+
20+
Examples:
21+
>>> from t4_devkit.viewer import ViewerBuilder
22+
>>> viewer = (
23+
ViewerBuilder()
24+
.with_spatial3d()
25+
.with_spatial2d(cameras=["CAM_FRONT", "CAM_BACK"])
26+
.with_labels(label2id={"car": 1, "pedestrian": 2})
27+
.with_streetmap(latlon=[48.8566, 2.3522])
28+
.build(app_id="my_viewer")
29+
)
30+
"""
31+
32+
def __init__(self) -> None:
33+
self._config = ViewerConfig()
34+
35+
def with_spatial3d(self) -> Self:
36+
self._config.spatial3ds.append(rrb.Spatial3DView(name="3D", origin=ViewerConfig.map_entity))
37+
return self
38+
39+
def with_spatial2d(self, cameras: Sequence[str], *, projection: bool = False) -> Self:
40+
overrides = {} # TODO(ktro2828): add support of projecting 3D elements on image
41+
self._config.spatial2ds.extend(
42+
[
43+
rrb.Spatial2DView(
44+
name=name,
45+
origin=format_entity(ViewerConfig.ego_entity, name),
46+
overrides=overrides,
47+
)
48+
for name in cameras
49+
]
50+
)
51+
return self
52+
53+
def with_labels(self, label2id: dict[str, int]) -> Self:
54+
self._config.label2id = label2id
55+
return self
56+
57+
def with_streetmap(self, latlon: Vector2Like) -> Self:
58+
self._config.spatial3ds.append(
59+
rrb.MapView(name="Map", origin=self._config.geocoordinate_entity)
60+
)
61+
self._config.latlon = latlon
62+
return self
63+
64+
def build(self, app_id: str, save_dir: str | None = None) -> RerunViewer:
65+
return RerunViewer(app_id=app_id, config=self._config, save_dir=save_dir)

t4_devkit/viewer/config.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, ClassVar, Sequence
4+
5+
import rerun.blueprint as rrb
6+
from attrs import define, field
7+
8+
if TYPE_CHECKING:
9+
from t4_devkit.typing import Vector2Like
10+
11+
12+
__all__ = ["ViewerConfig", "format_entity"]
13+
14+
15+
@define
16+
class ViewerConfig:
17+
map_entity: ClassVar[str] = "map"
18+
ego_entity: ClassVar[str] = "map/base_link"
19+
geocoordinate_entity: ClassVar[str] = "geocoordinate"
20+
timeline: ClassVar[str] = "timeline"
21+
22+
spatial3ds: list[rrb.SpaceView] = field(factory=list)
23+
spatial2ds: list[rrb.SpaceView] = field(factory=list)
24+
label2id: dict[str, int] = field(factory=dict)
25+
latlon: Vector2Like | None = field(default=None)
26+
27+
def to_blueprint(self) -> rrb.BlueprintLike:
28+
"""Return the recording blueprint."""
29+
views = []
30+
if self.spatial3ds:
31+
views.append(rrb.Horizontal(*self.spatial3ds, column_shares=[3, 1]))
32+
if self.spatial2ds:
33+
views.append(rrb.Grid(*self.spatial2ds))
34+
35+
return rrb.Vertical(*views, row_shares=[4, 2])
36+
37+
def has_spatial3d(self) -> bool:
38+
"""Return `True` if the configuration contains 3D view space."""
39+
return len(self.spatial3ds) > 0
40+
41+
def has_spatial2d(self) -> bool:
42+
"""Return `True` if the configuration contains 2D view space."""
43+
return len(self.spatial2ds) > 0
44+
45+
46+
def format_entity(*entities: Sequence[str]) -> str:
47+
"""Format entity path.
48+
49+
Args:
50+
*entities: Entity path(s).
51+
52+
Returns:
53+
Formatted entity path.
54+
55+
Examples:
56+
>>> format_entity("map")
57+
"map"
58+
>>> format_entity("map", "map/base_link")
59+
"map/base_link"
60+
>>> format_entity("map", "map/base_link", "camera")
61+
"map/base_link/camera"
62+
"""
63+
if not entities:
64+
return ""
65+
66+
flattened = []
67+
for entity in entities:
68+
for part in entity.split("/"):
69+
if part and flattened and flattened[-1] == part:
70+
continue
71+
flattened.append(part)
72+
return "/".join(flattened)

0 commit comments

Comments
 (0)