Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions docs/tutorials/render.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,18 @@ When you specify `save_dir`, viewer will not be spawned on your screen.
If you want to visualize your components, such as boxes that your ML-model estimated, `RerunViewer` allows you to visualize these components.
For details, please refer to the API references.

To initialize `RerunViewer`, you can use the `ViewerBuilder` class:

```python
>>> from t4_devkit.viewer import RerunViewer
>>> from t4_devkit.viewer import ViewerBuilder
# You need to specify `cameras` if you want to 2D spaces
>>> viewer = RerunViewer("foo", cameras=<CAMERA_NAMES:[str;N]>)
>>> viewer = (
ViewerBuilder()
.with_spatial3d()
.with_spatial2d(cameras=["CAM_FRONT", "CAM_BACK"], projection=True)
.with_labels({"car": 1, "pedestrian": 2})
.build("foo")
)

# Timestamp in seconds
>>> seconds: int | float = ...
Expand All @@ -86,10 +94,11 @@ It allows you to render boxes by specifying elements of boxes directly.
```python
# Rendering 3D boxes
>>> centers = [[i, i, i] for i in range(10)]
>>> frame_id = "base_link"
>>> rotations = [[1, 0, 0, 0] for _ in range(10)]
>>> sizes = [[1, 1, 1] for _ in range(10)]
>>> class_ids = [0 for _ in range(10)]
>>> viewer.render_box3ds(seconds, centers, rotations, sizes, class_ids)
>>> viewer.render_box3ds(seconds, frame_id, centers, rotations, sizes, class_ids)
```

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

```python
# RerunViewer(<APP_ID:str>, cameras=<CAMERA_NAMES:[str;N]>)
>>> viewer = RerunViewer("foo", cameras=["camera1"])
>>> viewer = (
ViewerBuilder()
.with_spatial3d()
.with_spatial2d(cameras=["camera1"])
.with_labels({"car": 1, "pedestrian": 2})
.build("foo")
)

>>> import numpy as np
>>> image = np.zeros((100, 100, 3), dtype=np.uint8)
Expand Down
59 changes: 14 additions & 45 deletions t4_devkit/helper/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from t4_devkit.common.timestamp import sec2us, us2sec
from t4_devkit.dataclass import LidarPointCloud, RadarPointCloud
from t4_devkit.schema import SensorModality
from t4_devkit.viewer import RerunViewer, distance_color, format_entity
from t4_devkit.viewer import RerunViewer, ViewerBuilder, distance_color, format_entity

if TYPE_CHECKING:
from t4_devkit import Tier4
Expand Down Expand Up @@ -57,44 +57,31 @@ def _init_viewer(
self,
app_id: str,
*,
render3d: bool = True,
render2d: bool = True,
render_ann: bool = True,
save_dir: str | None = None,
) -> RerunViewer:
if not (render3d or render2d):
raise ValueError("At least one of `render3d` or `render2d` must be True.")

cameras = (
[
sensor.channel
for sensor in self._t4.sensor
if sensor.modality == SensorModality.CAMERA
]
if render2d
else None
)
cameras = [
sensor.channel for sensor in self._t4.sensor if sensor.modality == SensorModality.CAMERA
]

viewer = RerunViewer(
app_id=app_id,
cameras=cameras,
with_3d=render3d,
save_dir=save_dir,
# project 3D boxes if there is no 2D annotation
projection = len(self._t4.object_ann) == 0 and len(self._t4.surface_ann) == 0
builder = (
ViewerBuilder().with_spatial3d().with_spatial2d(cameras=cameras, projection=projection)
)

if render_ann:
viewer = viewer.with_labels(self._label2id)
builder = builder.with_labels(self._label2id)

global_map_filepath = osp.join(self._t4.data_root, "map/global_map_center.pcd.yaml")
if osp.exists(global_map_filepath):
with open(global_map_filepath) as f:
map_metadata: dict = yaml.safe_load(f)
map_origin: dict = map_metadata["/**"]["ros__parameters"]["map_origin"]
latitude = map_origin["latitude"]
longitude = map_origin["longitude"]
viewer = viewer.with_global_origin((latitude, longitude))
latitude, longitude = map_origin["latitude"], map_origin["longitude"]
builder = builder.with_streetmap((latitude, longitude))

return viewer
return builder.build(app_id, save_dir=save_dir)

def render_scene(
self,
Expand Down Expand Up @@ -128,17 +115,8 @@ def render_scene(
if sensor.modality == SensorModality.CAMERA
]

render3d = len(first_lidar_tokens) > 0 or len(first_radar_tokens) > 0
render2d = len(first_camera_tokens) > 0

app_id = f"scene@{self._t4.dataset_id}"
viewer = self._init_viewer(
app_id,
render3d=render3d,
render2d=render2d,
render_ann=True,
save_dir=save_dir,
)
viewer = self._init_viewer(app_id, render_ann=True, save_dir=save_dir)

self._render_map(viewer)

Expand Down Expand Up @@ -236,17 +214,8 @@ def render_instance(
if sensor.modality == SensorModality.CAMERA
]

render3d = len(first_lidar_tokens) > 0 or len(first_radar_tokens) > 0
render2d = len(first_camera_tokens) > 0

app_id = f"instance@{self._t4.dataset_id}"
viewer = self._init_viewer(
app_id,
render3d=render3d,
render2d=render2d,
render_ann=True,
save_dir=save_dir,
)
viewer = self._init_viewer(app_id, render_ann=True, save_dir=save_dir)

self._render_map(viewer)

Expand Down
2 changes: 2 additions & 0 deletions t4_devkit/viewer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from .builder import * # noqa
from .color import * # noqa
from .geography import * # noqa
from .viewer import * # noqa
from .config import * # noqa
65 changes: 65 additions & 0 deletions t4_devkit/viewer/builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Sequence

import rerun.blueprint as rrb
from typing_extensions import Self

from .config import ViewerConfig, format_entity
from .viewer import RerunViewer

if TYPE_CHECKING:
from t4_devkit.typing import Vector2Like

__all__ = ["ViewerBuilder"]


class ViewerBuilder:
"""Builder for creating a RerunViewer instance.

Examples:
>>> from t4_devkit.viewer import ViewerBuilder
>>> viewer = (
ViewerBuilder()
.with_spatial3d()
.with_spatial2d(cameras=["CAM_FRONT", "CAM_BACK"])
.with_labels(label2id={"car": 1, "pedestrian": 2})
.with_streetmap(latlon=[48.8566, 2.3522])
.build(app_id="my_viewer")
)
"""

def __init__(self) -> None:
self._config = ViewerConfig()

def with_spatial3d(self) -> Self:
self._config.spatial3ds.append(rrb.Spatial3DView(name="3D", origin=ViewerConfig.map_entity))
return self

def with_spatial2d(self, cameras: Sequence[str], *, projection: bool = False) -> Self:
overrides = {} # TODO(ktro2828): add support of projecting 3D elements on image
self._config.spatial2ds.extend(
[
rrb.Spatial2DView(
name=name,
origin=format_entity(ViewerConfig.ego_entity, name),
overrides=overrides,
)
for name in cameras
]
)
return self

def with_labels(self, label2id: dict[str, int]) -> Self:
self._config.label2id = label2id
return self

def with_streetmap(self, latlon: Vector2Like) -> Self:
self._config.spatial3ds.append(
rrb.MapView(name="Map", origin=self._config.geocoordinate_entity)
)
self._config.latlon = latlon
return self

def build(self, app_id: str, save_dir: str | None = None) -> RerunViewer:
return RerunViewer(app_id=app_id, config=self._config, save_dir=save_dir)
72 changes: 72 additions & 0 deletions t4_devkit/viewer/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from __future__ import annotations

from typing import TYPE_CHECKING, ClassVar, Sequence

import rerun.blueprint as rrb
from attrs import define, field

if TYPE_CHECKING:
from t4_devkit.typing import Vector2Like


__all__ = ["ViewerConfig", "format_entity"]


@define
class ViewerConfig:
map_entity: ClassVar[str] = "map"
ego_entity: ClassVar[str] = "map/base_link"
geocoordinate_entity: ClassVar[str] = "geocoordinate"
timeline: ClassVar[str] = "timeline"

spatial3ds: list[rrb.SpaceView] = field(factory=list)
spatial2ds: list[rrb.SpaceView] = field(factory=list)
label2id: dict[str, int] = field(factory=dict)
latlon: Vector2Like | None = field(default=None)

def to_blueprint(self) -> rrb.BlueprintLike:
"""Return the recording blueprint."""
views = []
if self.spatial3ds:
views.append(rrb.Horizontal(*self.spatial3ds, column_shares=[3, 1]))
if self.spatial2ds:
views.append(rrb.Grid(*self.spatial2ds))

return rrb.Vertical(*views, row_shares=[4, 2])

def has_spatial3d(self) -> bool:
"""Return `True` if the configuration contains 3D view space."""
return len(self.spatial3ds) > 0

def has_spatial2d(self) -> bool:
"""Return `True` if the configuration contains 2D view space."""
return len(self.spatial2ds) > 0


def format_entity(*entities: Sequence[str]) -> str:
"""Format entity path.

Args:
*entities: Entity path(s).

Returns:
Formatted entity path.

Examples:
>>> format_entity("map")
"map"
>>> format_entity("map", "map/base_link")
"map/base_link"
>>> format_entity("map", "map/base_link", "camera")
"map/base_link/camera"
"""
if not entities:
return ""

flattened = []
for entity in entities:
for part in entity.split("/"):
if part and flattened and flattened[-1] == part:
continue
flattened.append(part)
return "/".join(flattened)
Loading
Loading