diff --git a/README.md b/README.md index 44c5efb..a29651c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ A toolkit to load and operate T4 dataset. ### Installation -#### Install via GitHub +#### [For Users] Install via GitHub Note that the following command installs the latest `main` branch: @@ -31,7 +31,7 @@ By specifying `@`, you can install the particular version of `t4- pip install git+https://github.com/tier4/t4-devkit.git@main ``` -#### Install from source +#### [For Developers] Install from source You need to install `uv`. For details, please refer to [OFFICIAL DOCUMENT](https://docs.astral.sh/uv/). diff --git a/docs/assets/render_box2ds.png b/docs/assets/render_box2ds.png new file mode 100644 index 0000000..f1c18c0 Binary files /dev/null and b/docs/assets/render_box2ds.png differ diff --git a/docs/assets/render_box3ds.png b/docs/assets/render_box3ds.png new file mode 100644 index 0000000..4a3bc11 Binary files /dev/null and b/docs/assets/render_box3ds.png differ diff --git a/docs/assets/render_pointcloud.png b/docs/assets/render_pointcloud.png new file mode 100644 index 0000000..e8ea718 Binary files /dev/null and b/docs/assets/render_pointcloud.png differ diff --git a/docs/install.md b/docs/install.md index 8586456..8474b0d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,4 +1,4 @@ -## Install via GitHub +## [For Users] Install via GitHub Note that the following command installs the latest `main` branch: @@ -14,7 +14,7 @@ By specifying `@`, you can install the particular version of `t4- pip install git+https://github.com/tier4/t4-devkit.git@main ``` -## Install from source +## [For Developers] Install from source You need to install `uv`. For details, please refer to [OFFICIAL DOCUMENT](https://docs.astral.sh/uv/). diff --git a/docs/tutorials/render.md b/docs/tutorials/render.md index 98599a1..e598347 100644 --- a/docs/tutorials/render.md +++ b/docs/tutorials/render.md @@ -60,37 +60,88 @@ When you specify `save_dir`, viewer will not be spawned on your screen. ## Rendering with `RerunViewer` -### Rendering boxes - 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. ```python >>> from t4_devkit.viewer import RerunViewer # You need to specify `cameras` if you want to 2D spaces ->>> viewer = RerunViewer(app_id, cameras=) +>>> viewer = RerunViewer("foo", cameras=) + +# Timestamp in seconds +>>> seconds: int | float = ... +``` + +### Rendering 3D boxes + +```python # Rendering 3D boxes +>>> from t4_devkit.dataclass import Box3D +>>> box3ds = [Box3D(...)...] >>> viewer.render_box3ds(seconds, box3ds) -# Rendering 2D boxes ->>> viewer.render_box2ds(seconds, box2ds) ``` 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)] +>>> 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) +``` + +![Render Box3Ds](../assets/render_box3ds.png) + +### Rendering 2D boxes + +For 2D spaces, you need to specify camera names in the viewer constructor, and render images by specifying camera names: + +```python +# RerunViewer(, cameras=) +>>> viewer = RerunViewer("foo", cameras=["camera1"]) + +>>> import numpy as np +>>> image = np.zeros((100, 100, 3), dtype=np.uint8) +>>> viewer.render_image(seconds, "camera1", image) +``` + +```python # Rendering 2D boxes ->>> viewer.render_box2ds(seconds, rois, class_ids) +>>> from t4_devkit.dataclass import Box2D +>>> box2ds = [Box2D(...)...] +>>> viewer.render_box2ds(seconds, "camera1", box2ds) ``` -### Rendering lanelet map +It allows you to render boxes by specifying elements of boxes directly: -![Render Lanelet Map](../assets/render_map.png) +```python +# Rendering 2D boxes +>>> rois = [[0, 0, 10 * i, 10 * i] for i in range(10)] +>>> viewer.render_box2ds(seconds, "camera1", rois, class_ids) +``` -You can also render lanelet map by specifying `lanelet_path`: +![Render Box2Ds](../assets/render_box2ds.png) + +### Rendering point cloud + +```python +from t4_devkit.dataclass import LidarPointCloud +# Point cloud channel name +>>> lidar_channel = "LIDAR_TOP" +# Load point cloud from file +>>> pointcloud = LidarPointCloud.from_file() +>>> viewer.render_pointcloud(seconds, lidar_channel, pointcloud) +``` + +![Render Point Cloud](../assets/render_pointcloud.png) + +### Rendering lanelet map ```python # Rendering lanelet map ->>> viewer.render_map(lanelet_path) +>>> viewer.render_map() ``` + +![Render Lanelet Map](../assets/render_map.png) diff --git a/t4_devkit/viewer/record/box.py b/t4_devkit/viewer/record/box.py index d302d3a..0e1dae9 100644 --- a/t4_devkit/viewer/record/box.py +++ b/t4_devkit/viewer/record/box.py @@ -5,14 +5,15 @@ import numpy as np import rerun as rr import rerun.components as rrc -from attrs import converters, define, field +from attrs import converters, define, field, validators +from t4_devkit.common.converter import to_quaternion +from t4_devkit.dataclass import Future from t4_devkit.typing import Roi, Vector3 -from t4_devkit.typing.aliases import RoiLike if TYPE_CHECKING: - from t4_devkit.dataclass import Box2D, Box3D, Future - from t4_devkit.typing import RotationLike, Vector3Like + from t4_devkit.dataclass import Box2D, Box3D + from t4_devkit.typing import RoiLike, RotationLike, Vector3Like __all__ = ["BatchBox3D", "BatchBox2D"] @@ -35,12 +36,16 @@ class Record: """Inner class to represent a record of 3D box instance for rendering.""" center: Vector3 = field(converter=Vector3) - rotation: rr.Quaternion + rotation: rr.Quaternion = field(validator=validators.instance_of(rr.Quaternion)) size: Vector3 = field(converter=Vector3) - class_id: int - uuid: int | None = field(default=None) + class_id: int = field(validator=validators.instance_of(int)) + uuid: str | None = field( + default=None, validator=validators.optional(validators.instance_of(str)) + ) velocity: Vector3 | None = field(default=None, converter=converters.optional(Vector3)) - future: Future | None = field(default=None) + future: Future | None = field( + default=None, validator=validators.optional(validators.instance_of(Future)) + ) @overload def append(self, box: Box3D) -> None: @@ -105,6 +110,7 @@ def _append_with_elements( uuid: str | None = None, future: Future | None = None, ) -> None: + rotation = to_quaternion(rotation) rotation_xyzw = np.roll(rotation.q, shift=-1) width, length, height = size @@ -203,8 +209,10 @@ class Record: """Inner class to represent a record of 2D box instance for rendering.""" roi: Roi = field(converter=Roi) - class_id: int - uuid: str | None = field(default=None) + class_id: int = field(validator=validators.instance_of(int)) + uuid: str | None = field( + default=None, validator=validators.optional(validators.instance_of(str)) + ) @overload def append(self, box: Box2D) -> None: diff --git a/t4_devkit/viewer/viewer.py b/t4_devkit/viewer/viewer.py index ec0e6ae..5af872a 100644 --- a/t4_devkit/viewer/viewer.py +++ b/t4_devkit/viewer/viewer.py @@ -9,10 +9,10 @@ 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 from t4_devkit.lanelet import LaneletParser from t4_devkit.schema import SensorModality -from t4_devkit.typing import Quaternion, Roi, Vector3 from .color import distance_color from .geography import calculate_geodetic_point @@ -27,7 +27,7 @@ if TYPE_CHECKING: from t4_devkit.dataclass import Box2D, Box3D, Future, PointCloudLike from t4_devkit.schema import CalibratedSensor, EgoPose, Sensor - from t4_devkit.typing import CamIntrinsicLike, NDArrayU8 + from t4_devkit.typing import CamIntrinsicLike, NDArrayU8, RoiLike, RotationLike, Vector3Like __all__ = ["RerunViewer", "format_entity"] @@ -214,11 +214,11 @@ def render_box3ds(self, seconds: float, boxes: Sequence[Box3D]) -> None: def render_box3ds( self, seconds: float, - centers: Sequence[Vector3], - rotations: Sequence[Quaternion], - sizes: Sequence[Vector3], + centers: Sequence[Vector3Like], + rotations: Sequence[RotationLike], + sizes: Sequence[Vector3Like], class_ids: Sequence[int], - velocities: Sequence[Vector3] | None = None, + velocities: Sequence[Vector3Like] | None = None, uuids: Sequence[str] | None = None, futures: Sequence[Future] | None = None, ) -> None: @@ -226,11 +226,11 @@ def render_box3ds( Args: seconds (float): Timestamp in [sec]. - centers (Sequence[Vector3]): Sequence of 3D positions in the order of (x, y, z). - rotations (Sequence[Quaternion]): Sequence of quaternions. - sizes (Sequence[Vector3]): Sequence of box sizes in the order of (width, length, height). + 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). class_ids (Sequence[int]): Sequence of class IDs. - velocities (Sequence[Vector3] | None, optional): Sequence of velocities. + velocities (Sequence[Vector3Like] | None, optional): Sequence of velocities. uuids (Sequence[str] | None, optional): Sequence of unique identifiers. futures (Sequence[Future] | None, optional): Sequence future trajectories. """ @@ -276,11 +276,11 @@ def _render_box3ds_with_boxes(self, seconds: float, boxes: Sequence[Box3D]) -> N def _render_box3ds_with_elements( self, seconds: float, - centers: Sequence[Vector3], - rotations: Sequence[Quaternion], - sizes: Sequence[Vector3], + centers: Sequence[Vector3Like], + rotations: Sequence[RotationLike], + sizes: Sequence[Vector3Like], class_ids: Sequence[int], - velocities: Sequence[Vector3] | None = None, + velocities: Sequence[Vector3Like] | None = None, uuids: Sequence[str] | None | None = None, futures: Sequence[Future] | None = None, ) -> None: @@ -346,7 +346,7 @@ def render_box2ds( self, seconds: float, camera: str, - rois: Sequence[Roi], + rois: Sequence[RoiLike], class_ids: Sequence[int], uuids: Sequence[str] | None = None, ) -> None: @@ -355,7 +355,7 @@ def render_box2ds( Args: seconds (float): Timestamp in [sec]. camera (str): Camera name. - rois (Sequence[Roi]): Sequence of ROIs in the order of (xmin, ymin, xmax, ymax). + rois (Sequence[RoiLike]): Sequence of ROIs in the order of (xmin, ymin, xmax, ymax). class_ids (Sequence[int]): Sequence of class IDs. uuids (Sequence[str] | None, optional): Sequence of unique identifiers. """ @@ -391,7 +391,7 @@ def _render_box2ds_with_elements( self, seconds: float, camera: str, - rois: Sequence[Roi], + rois: Sequence[RoiLike], class_ids: Sequence[int], uuids: Sequence[str] | None = None, ) -> None: @@ -496,18 +496,18 @@ def render_ego(self, ego_pose: EgoPose) -> None: def render_ego( self, seconds: float, - translation: Vector3, - rotation: Quaternion, - geocoordinate: Vector3 | None = None, + translation: Vector3Like, + rotation: RotationLike, + geocoordinate: Vector3Like | None = None, ) -> None: """Render an ego pose. Args: seconds (float): Timestamp in [sec]. - translation (Vector3): 3D position in the map coordinate system + translation (Vector3Like): 3D position in the map coordinate system , in the order of (x, y, z) in [m]. - rotation (Quaternion): Rotation in the map coordinate system. - geocoordinate (Vector3 | None, optional): Coordinates in the WGS 84 + rotation (RotationLike): Rotation in the map coordinate system. + geocoordinate (Vector3Like | None, optional): Coordinates in the WGS 84 reference ellipsoid (latitude, longitude, altitude) in degrees and meters. """ pass @@ -530,18 +530,17 @@ def _render_ego_with_schema(self, ego_pose: EgoPose) -> None: def _render_ego_without_schema( self, seconds: float, - translation: Vector3, - rotation: Quaternion, - geocoordinate: Vector3 | None = None, + translation: Vector3Like, + rotation: RotationLike, + geocoordinate: Vector3Like | None = None, ) -> None: rr.set_time_seconds(self.timeline, seconds) - rotation_xyzw = np.roll(rotation.q, shift=-1) rr.log( self.ego_entity, rr.Transform3D( translation=translation, - rotation=rr.Quaternion(xyzw=rotation_xyzw), + rotation=_to_rerun_quaternion(rotation), relation=rr.TransformRelation.ParentFromChild, ), ) @@ -578,8 +577,8 @@ def render_calibration( self, channel: str, modality: str | SensorModality, - translation: Vector3, - rotation: Quaternion, + translation: Vector3Like, + rotation: RotationLike, camera_intrinsic: CamIntrinsicLike | None = None, ) -> None: """Render a sensor calibration. @@ -587,8 +586,8 @@ def render_calibration( Args: channel (str): Name of the sensor channel. modality (str | SensorModality): Sensor modality. - translation (Vector3): Sensor translation in ego centric coords. - rotation (Quaternion): Sensor rotation in ego centric coords. + translation (Vector3Like): Sensor translation in ego centric coords. + rotation (RotationLike): Sensor rotation in ego centric coords. camera_intrinsic (CamIntrinsicLike | None, optional): Camera intrinsic matrix. Defaults to None. """ @@ -618,8 +617,8 @@ def _render_calibration_without_schema( self, channel: str, modality: str | SensorModality, - translation: Vector3, - rotation: Quaternion, + translation: Vector3Like, + rotation: RotationLike, camera_intrinsic: CamIntrinsicLike | None = None, ) -> None: """Render a sensor calibration. @@ -627,16 +626,14 @@ def _render_calibration_without_schema( Args: channel (str): Name of the sensor channel. modality (str | SensorModality): Sensor modality. - translation (Vector3): Sensor translation in ego centric coords. - rotation (Quaternion): Sensor rotation in ego centric coords. + translation (Vector3Like): Sensor translation in ego centric coords. + rotation (RotationLike): Sensor rotation in ego centric coords. camera_intrinsic (CamIntrinsicLike | None, optional): Camera intrinsic matrix. Defaults to None. """ - rotation_xyzw = np.roll(rotation.q, shift=-1) - rr.log( format_entity(self.ego_entity, channel), - rr.Transform3D(translation=translation, rotation=rr.Quaternion(xyzw=rotation_xyzw)), + rr.Transform3D(translation=translation, rotation=_to_rerun_quaternion(rotation)), static=True, ) @@ -661,3 +658,8 @@ def render_map(self, filepath: str) -> None: render_ways(parser, root_entity) render_geographic_borders(parser, f"{self.geocoordinate_entity}/vector_map") + + +def _to_rerun_quaternion(rotation: RotationLike) -> rr.Quaternion: + rotation_xyzw = np.roll(to_quaternion(rotation).q, shift=-1) + return rr.Quaternion(xyzw=rotation_xyzw)