diff --git a/docs/tutorials/render.md b/docs/tutorials/render.md index e598347..b4a9354 100644 --- a/docs/tutorials/render.md +++ b/docs/tutorials/render.md @@ -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=) +>>> 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 = ... @@ -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) @@ -100,7 +109,13 @@ For 2D spaces, you need to specify camera names in the viewer constructor, and r ```python # RerunViewer(, cameras=) ->>> 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) diff --git a/t4_devkit/helper/rendering.py b/t4_devkit/helper/rendering.py index 29aca75..8c1f7b3 100644 --- a/t4_devkit/helper/rendering.py +++ b/t4_devkit/helper/rendering.py @@ -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 @@ -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, @@ -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) @@ -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) diff --git a/t4_devkit/viewer/__init__.py b/t4_devkit/viewer/__init__.py index 3628284..0eb2b02 100644 --- a/t4_devkit/viewer/__init__.py +++ b/t4_devkit/viewer/__init__.py @@ -1,3 +1,5 @@ +from .builder import * # noqa from .color import * # noqa from .geography import * # noqa from .viewer import * # noqa +from .config import * # noqa diff --git a/t4_devkit/viewer/builder.py b/t4_devkit/viewer/builder.py new file mode 100644 index 0000000..c0ec98f --- /dev/null +++ b/t4_devkit/viewer/builder.py @@ -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) diff --git a/t4_devkit/viewer/config.py b/t4_devkit/viewer/config.py new file mode 100644 index 0000000..a9aae02 --- /dev/null +++ b/t4_devkit/viewer/config.py @@ -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) diff --git a/t4_devkit/viewer/viewer.py b/t4_devkit/viewer/viewer.py index f825875..7663336 100644 --- a/t4_devkit/viewer/viewer.py +++ b/t4_devkit/viewer/viewer.py @@ -2,12 +2,10 @@ import os.path as osp import warnings -from typing import TYPE_CHECKING, Sequence, overload +from typing import TYPE_CHECKING, Callable, Sequence, overload import numpy as np import rerun as rr -import rerun.blueprint as rrb -from typing_extensions import Self from t4_devkit.common.converter import to_quaternion from t4_devkit.common.timestamp import us2sec @@ -15,6 +13,7 @@ from t4_devkit.schema import SensorModality from .color import distance_color +from .config import ViewerConfig, format_entity from .geography import calculate_geodetic_point from .lanelet import ( render_geographic_borders, @@ -36,104 +35,74 @@ Vector3Like, ) -__all__ = ["RerunViewer", "format_entity"] +__all__ = ["RerunViewer"] -def format_entity(*entities: Sequence[str]) -> str: - """Format entity path. +def _check_spatial3d(function: Callable) -> Callable: + """Check if the viewer has the 3D view space. + + Note: + This function is supposed to be used as a decorator for methods of RerunViewer. + """ + + def checker(viewer: RerunViewer, *args, **kwargs): + if not viewer.config.has_spatial3d(): + warnings.warn("There is no 3D view space") + return + else: + return function(viewer, *args, **kwargs) + + return checker - Args: - *entities: Entity path(s). - Returns: - Formatted entity path. +def _check_spatial2d(function: Callable) -> Callable: + """Check if the viewer has the 2D view space. - 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" + Note: + This function is supposed to be used as a decorator for methods of RerunViewer. """ - 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) + def checker(viewer: RerunViewer, *args, **kwargs): + if not viewer.config.has_spatial2d(): + warnings.warn("There is no 2D view space") + return + else: + return function(viewer, *args, **kwargs) + + return checker class RerunViewer: """A viewer class that renders some components powered by rerun.""" - # entity paths - map_entity = "map" - ego_entity = "map/base_link" - geocoordinate_entity = "geocoordinate" - timeline = "timestamp" - def __init__( self, app_id: str, - *, - cameras: Sequence[str] | None = None, - with_3d: bool = True, + config: ViewerConfig = ViewerConfig(), save_dir: str | None = None, ) -> None: """Construct a new object. Args: app_id (str): Application ID. - cameras (Sequence[str] | None, optional): Sequence of camera names. - If `None`, any 2D spaces will not be visualized. - with_3d (bool, optional): Whether to render objects with the 3D space. + config (ViewerConfig): Configuration of the viewer. save_dir (str | None, optional): Directory path to save the recording. Viewer will be spawned if it is None, otherwise not. Examples: - >>> from t4_devkit.viewer import RerunViewer - # Rendering both 3D/2D spaces - >>> viewer = RerunViewer("myapp", cameras=["camera0", "camera1"]) - # Rendering 3D space only - >>> viewer = RerunViewer("myapp") - # Rendering 2D space only - >>> viewer = RerunViewer("myapp", cameras=["camera0", "camera1"], with_3d=False) + >>> 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") + ) """ self.app_id = app_id - self.cameras = cameras - self.with_3d = with_3d - self.with_2d = self.cameras is not None - self.label2id: dict[str, int] | None = None - self.global_origin: tuple[float, float] | None = None - - if not (self.with_3d or self.with_2d): - raise ValueError("At least one of 3D or 2D spaces must be rendered.") - - view_container: list[rrb.Container | rrb.SpaceView] = [] - if self.with_3d: - view_container.extend( - [ - rrb.Horizontal( - rrb.Spatial3DView(name="3D", origin=self.map_entity), - rrb.Horizontal(rrb.MapView(name="Map", origin=self.geocoordinate_entity)), - column_shares=[3, 1], - ), - ] - ) - - if self.with_2d: - camera_space_views = [ - rrb.Spatial2DView(name=name, origin=format_entity(self.ego_entity, name)) - for name in self.cameras - ] - view_container.append(rrb.Grid(*camera_space_views)) - - self.blueprint = rrb.Vertical(*view_container, row_shares=[4, 2]) + self.config = config + self.blueprint = self.config.to_blueprint() rr.init( application_id=self.app_id, @@ -148,25 +117,10 @@ def __init__( if save_dir is not None: self._start_saving(save_dir=save_dir) - rr.log(self.map_entity, rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True) - - def with_labels(self, label2id: dict[str, int]) -> Self: - """Return myself after creating `rr.AnnotationContext` on the recording. - - Args: - label2id (dict[str, int]): Key-value mapping which maps label name to its class ID. - - Returns: - Self instance. - - Examples: - >>> label2id = {"car": 0, "pedestrian": 1} - >>> viewer = RerunViewer("myapp").with_labels(label2id) - """ - self.label2id = label2id + rr.log(self.config.map_entity, rr.ViewCoordinates.RIGHT_HAND_Z_UP, static=True) rr.log( - self.map_entity, + self.config.map_entity, rr.AnnotationContext( [ rr.AnnotationInfo(id=label_id, label=label) @@ -176,23 +130,13 @@ def with_labels(self, label2id: dict[str, int]) -> Self: static=True, ) - return self + @property + def label2id(self) -> dict[str, int]: + return self.config.label2id - def with_global_origin(self, lat_lon: tuple[float, float]) -> Self: - """Return myself after setting global origin. - - Args: - lat_lon (tuple[float, float]): Global origin of map (latitude, longitude). - - Returns: - Self instance. - - Examples: - >>> lat_lon = (42.336849169438615, -71.05785369873047) - >>> viewer = RerunViewer("myapp").with_global_origin(lat_lon) - """ - self.global_origin = lat_lon - return self + @property + def latlon(self) -> Vector2Like | None: + return self.config.latlon def _start_saving(self, save_dir: str) -> None: """Save recording result as `save_dir/{app_id}.rrd`. @@ -208,8 +152,7 @@ def _start_saving(self, save_dir: str) -> None: @overload def render_box3ds(self, seconds: float, boxes: Sequence[Box3D]) -> None: - """Render 3D boxes. Note that if the viewer initialized with `with_3d=False`, - no 3D box will be rendered. + """Render 3D boxes. Args: seconds (float): Timestamp in [sec]. @@ -221,6 +164,7 @@ def render_box3ds(self, seconds: float, boxes: Sequence[Box3D]) -> None: def render_box3ds( self, seconds: float, + frame_id: str, centers: Sequence[Vector3Like], rotations: Sequence[RotationLike], sizes: Sequence[Vector3Like], @@ -233,6 +177,7 @@ def render_box3ds( Args: seconds (float): Timestamp in [sec]. + frame_id (str): Frame ID. centers (Sequence[Vector3Like]): Sequence of 3D positions in the order of (x, y, z). rotations (Sequence[RotationLike]): Sequence of rotations. sizes (Sequence[Vector3Like]): Sequence of box sizes in the order of (width, length, height). @@ -243,6 +188,7 @@ def render_box3ds( """ pass + @_check_spatial3d def render_box3ds(self, *args, **kwargs) -> None: """Render 3D boxes.""" if len(args) + len(kwargs) == 2: @@ -251,11 +197,7 @@ def render_box3ds(self, *args, **kwargs) -> None: self._render_box3ds_with_elements(*args, **kwargs) def _render_box3ds_with_boxes(self, seconds: float, boxes: Sequence[Box3D]) -> None: - if not self.with_3d: - warnings.warn("There is no camera space.") - return - - rr.set_time_seconds(self.timeline, seconds) + rr.set_time_seconds(self.config.timeline, seconds) batches: dict[str, BatchBox3D] = {} for box in boxes: @@ -266,23 +208,24 @@ def _render_box3ds_with_boxes(self, seconds: float, boxes: Sequence[Box3D]) -> N for frame_id, batch in batches.items(): # record boxes 3d rr.log( - format_entity(self.map_entity, frame_id, "box"), + format_entity(self.config.map_entity, frame_id, "box"), batch.as_boxes3d(), ) # record velocities rr.log( - format_entity(self.map_entity, frame_id, "velocity"), + format_entity(self.config.map_entity, frame_id, "velocity"), batch.as_arrows3d(), ) # record futures rr.log( - format_entity(self.map_entity, frame_id, "future"), + format_entity(self.config.map_entity, frame_id, "future"), batch.as_linestrips3d(), ) def _render_box3ds_with_elements( self, seconds: float, + frame_id: str, centers: Sequence[Vector3Like], rotations: Sequence[RotationLike], sizes: Sequence[Vector3Like], @@ -327,15 +270,21 @@ def _render_box3ds_with_elements( future=future, ) - rr.set_time_seconds(self.timeline, seconds) + rr.set_time_seconds(self.config.timeline, seconds) - rr.log(format_entity(self.ego_entity, "box"), batch.as_boxes3d()) + rr.log(format_entity(self.config.map_entity, frame_id, "box"), batch.as_boxes3d()) if show_arrows: - rr.log(format_entity(self.ego_entity, "velocity"), batch.as_arrows3d()) + rr.log( + format_entity(self.config.map_entity, frame_id, "velocity"), + batch.as_arrows3d(), + ) if show_futures: - rr.log(format_entity(self.ego_entity, "future"), batch.as_linestrips3d()) + rr.log( + format_entity(self.config.map_entity, frame_id, "future"), + batch.as_linestrips3d(), + ) @overload def render_box2ds(self, seconds: float, boxes: Sequence[Box2D]) -> None: @@ -368,6 +317,7 @@ def render_box2ds( """ pass + @_check_spatial2d def render_box2ds(self, *args, **kwargs) -> None: """Render 2D boxes.""" if len(args) + len(kwargs) == 2: @@ -376,11 +326,7 @@ def render_box2ds(self, *args, **kwargs) -> None: self._render_box2ds_with_elements(*args, **kwargs) def _render_box2ds_with_boxes(self, seconds: float, boxes: Sequence[Box2D]) -> None: - if not self.with_2d: - warnings.warn("There is no camera space.") - return - - rr.set_time_seconds(self.timeline, seconds) + rr.set_time_seconds(self.config.timeline, seconds) batches: dict[str, BatchBox2D] = {} for box in boxes: @@ -390,7 +336,7 @@ def _render_box2ds_with_boxes(self, seconds: float, boxes: Sequence[Box2D]) -> N for frame_id, batch in batches.items(): rr.log( - format_entity(self.ego_entity, frame_id, "box"), + format_entity(self.config.ego_entity, frame_id, "box"), batch.as_boxes2d(), ) @@ -402,10 +348,6 @@ def _render_box2ds_with_elements( class_ids: Sequence[int], uuids: Sequence[str] | None = None, ) -> None: - if not self.with_2d: - warnings.warn("There is no camera space.") - return - if uuids is None: uuids = [None] * len(rois) @@ -413,9 +355,10 @@ def _render_box2ds_with_elements( for roi, class_id, uuid in zip(rois, class_ids, uuids, strict=True): batch.append(roi=roi, class_id=class_id, uuid=uuid) - rr.set_time_seconds(self.timeline, seconds) - rr.log(format_entity(self.ego_entity, camera, "box"), batch.as_boxes2d()) + rr.set_time_seconds(self.config.timeline, seconds) + rr.log(format_entity(self.config.ego_entity, camera, "box"), batch.as_boxes2d()) + @_check_spatial2d def render_segmentation2d( self, seconds: float, @@ -434,11 +377,7 @@ def render_segmentation2d( class_ids (Sequence[int]): Sequence of label ids. uuids (Sequence[str | None] | None, optional): Sequence of each instance ID. """ - if not self.with_2d or camera not in self.cameras: - warnings.warn(f"There is no camera space: {camera}") - return - - rr.set_time_seconds(self.timeline, seconds) + rr.set_time_seconds(self.config.timeline, seconds) batch = BatchSegmentation2D() if uuids is None: @@ -447,10 +386,11 @@ def render_segmentation2d( batch.append(mask, class_id, uuid) rr.log( - format_entity(self.ego_entity, camera, "segmentation"), + format_entity(self.config.ego_entity, camera, "segmentation"), batch.as_segmentation_image(), ) + @_check_spatial3d def render_pointcloud(self, seconds: float, channel: str, pointcloud: PointCloudLike) -> None: """Render pointcloud. @@ -460,17 +400,15 @@ def render_pointcloud(self, seconds: float, channel: str, pointcloud: PointCloud pointcloud (PointCloudLike): Inherence object of `PointCloud`. """ # TODO(ktro2828): add support of rendering pointcloud on images - rr.set_time_seconds(self.timeline, seconds) + rr.set_time_seconds(self.config.timeline, seconds) colors = distance_color(np.linalg.norm(pointcloud.points[:3].T, axis=1)) rr.log( - format_entity(self.ego_entity, channel), - rr.Points3D( - pointcloud.points[:3].T, - colors=colors, - ), + format_entity(self.config.ego_entity, channel), + rr.Points3D(pointcloud.points[:3].T, colors=colors), ) + @_check_spatial2d def render_image(self, seconds: float, camera: str, image: str | NDArrayU8) -> None: """Render an image. @@ -479,16 +417,12 @@ def render_image(self, seconds: float, camera: str, image: str | NDArrayU8) -> N camera (str): Name of the camera channel. image (str | NDArrayU8): Image tensor or path of the image file. """ - if not self.with_2d or camera not in self.cameras: - warnings.warn(f"There is no camera space: {camera}") - return - - rr.set_time_seconds(self.timeline, seconds) + rr.set_time_seconds(self.config.timeline, seconds) if isinstance(image, str): - rr.log(format_entity(self.ego_entity, camera), rr.ImageEncoded(path=image)) + rr.log(format_entity(self.config.ego_entity, camera), rr.ImageEncoded(path=image)) else: - rr.log(format_entity(self.ego_entity, camera), rr.Image(image)) + rr.log(format_entity(self.config.ego_entity, camera), rr.Image(image)) @overload def render_ego(self, ego_pose: EgoPose) -> None: @@ -519,6 +453,7 @@ def render_ego( """ pass + @_check_spatial3d def render_ego(self, *args, **kwargs) -> None: """Render an ego pose.""" if len(args) + len(kwargs) == 1: @@ -541,10 +476,10 @@ def _render_ego_without_schema( rotation: RotationLike, geocoordinate: Vector3Like | None = None, ) -> None: - rr.set_time_seconds(self.timeline, seconds) + rr.set_time_seconds(self.config.timeline, seconds) rr.log( - self.ego_entity, + self.config.ego_entity, rr.Transform3D( translation=translation, rotation=_to_rerun_quaternion(rotation), @@ -555,13 +490,13 @@ def _render_ego_without_schema( if geocoordinate is not None: latitude, longitude, _ = geocoordinate rr.log( - self.geocoordinate_entity, + self.config.geocoordinate_entity, rr.GeoPoints(lat_lon=(latitude, longitude)), ) - elif self.global_origin is not None: - latitude, longitude = calculate_geodetic_point(translation, self.global_origin) + elif self.latlon is not None: + latitude, longitude = calculate_geodetic_point(translation, self.latlon) rr.log( - self.geocoordinate_entity, + self.config.geocoordinate_entity, rr.GeoPoints(lat_lon=(latitude, longitude)), ) @@ -603,6 +538,7 @@ def render_calibration( """ pass + @_check_spatial3d def render_calibration(self, *args, **kwargs) -> None: """Render a sensor calibration.""" if len(args) + len(kwargs) <= 3: @@ -645,18 +581,19 @@ def _render_calibration_without_schema( resolution (Vector2Like | None, optional): Camera resolution (width, height). """ rr.log( - format_entity(self.ego_entity, channel), + format_entity(self.config.ego_entity, channel), rr.Transform3D(translation=translation, rotation=_to_rerun_quaternion(rotation)), static=True, ) if modality == SensorModality.CAMERA: rr.log( - format_entity(self.ego_entity, channel), + format_entity(self.config.ego_entity, channel), rr.Pinhole(image_from_camera=camera_intrinsic, resolution=resolution), static=True, ) + @_check_spatial3d def render_map(self, filepath: str) -> None: """Render vector map. @@ -665,12 +602,12 @@ def render_map(self, filepath: str) -> None: """ parser = LaneletParser(filepath, verbose=False) - root_entity = format_entity(self.map_entity, "vector_map") + root_entity = format_entity(self.config.map_entity, "vector_map") render_lanelets(parser, root_entity) render_traffic_elements(parser, root_entity) render_ways(parser, root_entity) - render_geographic_borders(parser, f"{self.geocoordinate_entity}/vector_map") + render_geographic_borders(parser, f"{self.config.geocoordinate_entity}/vector_map") def _to_rerun_quaternion(rotation: RotationLike) -> rr.Quaternion: diff --git a/tests/viewer/conftest.py b/tests/viewer/conftest.py index 4b586d2..949ac51 100644 --- a/tests/viewer/conftest.py +++ b/tests/viewer/conftest.py @@ -2,7 +2,7 @@ import pytest -from t4_devkit.viewer import RerunViewer +from t4_devkit.viewer import RerunViewer, ViewerBuilder @pytest.fixture(scope="session") @@ -14,8 +14,10 @@ def dummy_viewer(tmpdir_factory, spawn_viewer, label2id) -> RerunViewer: """ save_dir = None if spawn_viewer else tmpdir_factory.mktemp("sample_recording") - return RerunViewer( - "test_viewer", - cameras=["camera"], - save_dir=save_dir, - ).with_labels(label2id=label2id) + return ( + ViewerBuilder() + .with_spatial3d() + .with_spatial2d(["camera"]) + .with_labels(label2id) + .build("test_viewer", save_dir=save_dir) + ) diff --git a/tests/viewer/test_viewer.py b/tests/viewer/test_viewer.py index 1ad8494..4c63e03 100644 --- a/tests/viewer/test_viewer.py +++ b/tests/viewer/test_viewer.py @@ -20,6 +20,7 @@ def test_format_entity() -> None: def test_render_box3ds(dummy_viewer, dummy_box3ds) -> None: """Test rendering 3D boxes with `RerunViewer`.""" seconds = 1.0 # [sec] + frame_id = "map" dummy_viewer.render_box3ds(seconds, dummy_box3ds) @@ -32,6 +33,7 @@ def test_render_box3ds(dummy_viewer, dummy_box3ds) -> None: dummy_viewer.render_box3ds( seconds, + frame_id=frame_id, centers=centers, rotations=rotations, sizes=sizes,