From 1d7ee2a35087a36b8f8835f8ec5962c29379a662 Mon Sep 17 00:00:00 2001 From: ktro2828 Date: Mon, 26 May 2025 21:16:52 +0900 Subject: [PATCH 1/3] feat: add support of loading dataset metadata Signed-off-by: ktro2828 --- docs/tutorials/initialize.md | 2 +- t4_devkit/cli/visualize.py | 6 ++-- t4_devkit/common/sanity.py | 22 ++++-------- t4_devkit/tier4.py | 65 +++++++++++++++++++++++++++++++----- 4 files changed, 67 insertions(+), 28 deletions(-) diff --git a/docs/tutorials/initialize.md b/docs/tutorials/initialize.md index 4c447c8..bec61cc 100644 --- a/docs/tutorials/initialize.md +++ b/docs/tutorials/initialize.md @@ -25,7 +25,7 @@ You can initialize a `Tier4` instance as follows: >>> from t4_devkit import Tier4 ->>> t4 = Tier4("annotation", "data/tier4/", verbose=True) +>>> t4 = Tier4("data/tier4/", verbose=True) ====== Loading T4 tables in `annotation`... Reverse indexing... diff --git a/t4_devkit/cli/visualize.py b/t4_devkit/cli/visualize.py index 82107ff..6e1d929 100644 --- a/t4_devkit/cli/visualize.py +++ b/t4_devkit/cli/visualize.py @@ -41,7 +41,7 @@ def scene( ) -> None: _create_dir(output) - t4 = Tier4("annotation", data_root, verbose=False) + t4 = Tier4(data_root, verbose=False) scene_token = t4.scene[0].token t4.render_scene(scene_token, future_seconds=future, save_dir=output) @@ -71,7 +71,7 @@ def instance( ) -> None: _create_dir(output) - t4 = Tier4("annotation", data_root, verbose=False) + t4 = Tier4(data_root, verbose=False) t4.render_instance(instance_token=instance, future_seconds=future, save_dir=output) @@ -99,7 +99,7 @@ def pointcloud( ) -> None: _create_dir(output) - t4 = Tier4("annotation", data_root, verbose=False) + t4 = Tier4(data_root, verbose=False) scene_token = t4.scene[0].token t4.render_pointcloud( scene_token, diff --git a/t4_devkit/common/sanity.py b/t4_devkit/common/sanity.py index 10a6374..248681a 100644 --- a/t4_devkit/common/sanity.py +++ b/t4_devkit/common/sanity.py @@ -1,12 +1,11 @@ from __future__ import annotations -import re import warnings from pathlib import Path from attrs import define -from t4_devkit import Tier4 +from t4_devkit import Tier4, load_metadata __all__ = ["DBException", "sanity_check"] @@ -30,17 +29,6 @@ def sanity_check(db_root: str | Path, *, include_warning: bool = False) -> DBExc Returns: Exception or warning if exits, otherwise returns None. """ - db_root_path = Path(db_root) - - version_pattern = re.compile(r".*/\d+$") - versions = [d.name for d in db_root_path.iterdir() if version_pattern.match(str(d))] - - if versions: - version = sorted(versions)[-1] - data_root = db_root_path.joinpath(version).as_posix() - else: - version = None - data_root = db_root_path.as_posix() with warnings.catch_warnings(): if include_warning: @@ -49,12 +37,14 @@ def sanity_check(db_root: str | Path, *, include_warning: bool = False) -> DBExc warnings.filterwarnings("ignore") try: - _ = Tier4("annotation", data_root=data_root, verbose=False) + _ = Tier4(data_root=db_root, verbose=False) exception = None except Exception as e: + metadata = load_metadata(db_root) + exception = DBException( - dataset_id=db_root_path.name, - version=version, + dataset_id=metadata.dataset_id, + version=metadata.version, message=str(e), ) return exception diff --git a/t4_devkit/tier4.py b/t4_devkit/tier4.py index 971ac4a..d6dff23 100644 --- a/t4_devkit/tier4.py +++ b/t4_devkit/tier4.py @@ -1,11 +1,14 @@ from __future__ import annotations import os.path as osp +import re import time import warnings +from pathlib import Path from typing import TYPE_CHECKING, Sequence import numpy as np +from attrs import define from pyquaternion import Quaternion from t4_devkit.common.geometry import is_box_in_image @@ -38,13 +41,46 @@ Visibility, ) -__all__ = ("Tier4",) +__all__ = ["DBMetadata", "load_metadata", "Tier4"] + + +@define +class DBMetadata: + data_root: str + dataset_id: str + version: str | None + + +def load_metadata(db_root: str) -> DBMetadata: + """Load metadata of T4 dataset including root directory path, dataset ID, and version. + + Args: + db_root (str): Path to root directory of database. + + Returns: + Metadata of T4 dataset. + """ + db_root_path = Path(db_root) + + version_pattern = re.compile(r".*/\d+$") + versions = [d.name for d in db_root_path.iterdir() if version_pattern.match(d.as_posix())] + + if versions: + version = sorted(versions)[-1] + data_root = db_root_path.joinpath(version).as_posix() + else: + version = None + data_root = db_root_path.as_posix() + + return DBMetadata(data_root=data_root, dataset_id=db_root_path.name, version=version) class Tier4: """Database class for T4 dataset to help query and retrieve information from the database.""" - def __init__(self, version: str, data_root: str, verbose: bool = True) -> None: + schema_dir: str = "annotation" + + def __init__(self, data_root: str, verbose: bool = True) -> None: """Load database and creates reverse indexes and shortcuts. Args: @@ -54,7 +90,7 @@ def __init__(self, version: str, data_root: str, verbose: bool = True) -> None: Examples: >>> from t4_devkit import Tier4 - >>> t4 = Tier4("annotation", "data/tier4") + >>> t4 = Tier4("data/tier4") ====== Loading T4 tables in `annotation`... Reverse indexing... @@ -80,16 +116,14 @@ def __init__(self, version: str, data_root: str, verbose: bool = True) -> None: ====== """ - self.version = version - self.data_root = data_root - self.verbose = verbose + self._metadata = load_metadata(data_root) if not osp.exists(self.data_root): raise FileNotFoundError(f"Database directory is not found: {self.data_root}") start_time = time.time() if verbose: - print(f"======\nLoading T4 tables in `{self.version}`...") + print(f"======\nLoading T4 tables in `{self.schema_dir}`...") # assign tables explicitly self.attribute: list[Attribute] = self.__load_table__(SchemaName.ATTRIBUTE) @@ -127,6 +161,21 @@ def __init__(self, version: str, data_root: str, verbose: bool = True) -> None: self._timeseries_helper = TimeseriesHelper(self) self._rendering_helper = RenderingHelper(self) + @property + def data_root(self) -> str: + """Return the path to dataset root directory.""" + return self._metadata.data_root + + @property + def dataset_id(self) -> str: + """Return the dataset ID.""" + return self._metadata.dataset_id + + @property + def version(self) -> str | None: + """Return the dataset version, or None if it is failed to lookup.""" + return self._metadata.version + def __load_table__(self, schema: SchemaName) -> list[SchemaTable]: """Load schema table from a json file. @@ -139,7 +188,7 @@ def __load_table__(self, schema: SchemaName) -> list[SchemaTable]: Returns: Loaded table data saved in `.json`. """ - filepath = osp.join(self.data_root, self.version, schema.filename) + filepath = osp.join(self.data_root, self.schema_dir, schema.filename) if not osp.exists(filepath) and schema.is_optional(): return [] From 4acb90716bc6f3ebae06b288cbc7a1c53699b920 Mon Sep 17 00:00:00 2001 From: ktro2828 Date: Tue, 27 May 2025 08:39:34 +0900 Subject: [PATCH 2/3] docs: update document Signed-off-by: ktro2828 --- docs/tutorials/initialize.md | 52 +++++++++++++++++++++++++----------- t4_devkit/tier4.py | 1 - 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/docs/tutorials/initialize.md b/docs/tutorials/initialize.md index bec61cc..2f71869 100644 --- a/docs/tutorials/initialize.md +++ b/docs/tutorials/initialize.md @@ -2,22 +2,42 @@ --- -`Tier4` class expects the following dataset directly structure: - -```shell -data/tier4/ -├── annotation ...contains `*.json` files. -├── data -│   ├── CAM_BACK -│   ├── CAM_BACK_LEFT -│   ├── CAM_BACK_RIGHT -│   ├── CAM_FRONT -│   ├── CAM_FRONT_LEFT -│   ├── CAM_FRONT_RIGHT -│   ├── LIDAR_CONCAT -│   └── ...Other sensor channels -... -``` +`Tier4` class expects both following dataset directly structure with or without `` directory: + +- With `` directory: + + ```shell + data/tier4/ + └── ...version number + ├── annotation ...contains `*.json` files + ├── data + │   ├── CAM_BACK + │   ├── CAM_BACK_LEFT + │   ├── CAM_BACK_RIGHT + │   ├── CAM_FRONT + │   ├── CAM_FRONT_LEFT + │   ├── CAM_FRONT_RIGHT + │   ├── LIDAR_CONCAT + │   └── ...Other sensor channels + ... + ``` + +- Without `` directory: + + ```shell + data/tier4/ + ├── annotation ...contains `*.json` files + ├── data + │   ├── CAM_BACK + │   ├── CAM_BACK_LEFT + │   ├── CAM_BACK_RIGHT + │   ├── CAM_FRONT + │   ├── CAM_FRONT_LEFT + │   ├── CAM_FRONT_RIGHT + │   ├── LIDAR_CONCAT + │   └── ...Other sensor channels + ... + ``` You can initialize a `Tier4` instance as follows: diff --git a/t4_devkit/tier4.py b/t4_devkit/tier4.py index d6dff23..8f0c102 100644 --- a/t4_devkit/tier4.py +++ b/t4_devkit/tier4.py @@ -84,7 +84,6 @@ def __init__(self, data_root: str, verbose: bool = True) -> None: """Load database and creates reverse indexes and shortcuts. Args: - version (str): Directory name of database json files. data_root (str): Path to the root directory of dataset. verbose (bool, optional): Whether to display status during load. From 270421c1919c087e2dc3b91bb92c7a314515cb8a Mon Sep 17 00:00:00 2001 From: ktro2828 Date: Tue, 27 May 2025 17:25:10 +0900 Subject: [PATCH 3/3] feat: use dataset ID as viewer application ID Signed-off-by: ktro2828 --- docs/tutorials/render.md | 22 ++++++++++++---------- t4_devkit/cli/visualize.py | 10 ++-------- t4_devkit/helper/rendering.py | 13 ++++--------- t4_devkit/tier4.py | 6 ------ 4 files changed, 18 insertions(+), 33 deletions(-) diff --git a/docs/tutorials/render.md b/docs/tutorials/render.md index 5be3c79..5f935b6 100644 --- a/docs/tutorials/render.md +++ b/docs/tutorials/render.md @@ -5,8 +5,7 @@ If you want to visualize annotation results, `Tier4` supports some rendering met ### Scene ```python ->>> scene_token = t4.scene[0].token ->>> t4.render_scene(scene_token) +>>> t4.render_scene() ``` ![Render Scene GIF](../assets/render_scene.gif) @@ -20,18 +19,21 @@ If you want to visualize annotation results, `Tier4` supports some rendering met ![Render Instance GIF](../assets/render_instance.gif) -You can also render multiple instances at once: + +!!! NOTE + You can also render multiple instances at once: -```python ->>> instance_tokens = [inst.token for inst in t4.instance[:3]] ->>> t4.render_instance(instance_tokens) -``` + + ```python + >>> instance_tokens = [inst.token for inst in t4.instance[:3]] + >>> t4.render_instance(instance_tokens) + ``` + ### PointCloud ```python ->>> scene_token = t4.scene[0].token ->>> t4.render_pointcloud(scene_token) +>>> t4.render_pointcloud() ``` ![Render PointCloud GIF](../assets/render_pointcloud.gif) @@ -42,7 +44,7 @@ You can also render multiple instances at once: ```python - >>> t4.render_pointcloud(scene_token, ignore_distortion=True) + >>> t4.render_pointcloud(ignore_distortion=True) ``` diff --git a/t4_devkit/cli/visualize.py b/t4_devkit/cli/visualize.py index 6e1d929..92ac1b7 100644 --- a/t4_devkit/cli/visualize.py +++ b/t4_devkit/cli/visualize.py @@ -42,8 +42,7 @@ def scene( _create_dir(output) t4 = Tier4(data_root, verbose=False) - scene_token = t4.scene[0].token - t4.render_scene(scene_token, future_seconds=future, save_dir=output) + t4.render_scene(future_seconds=future, save_dir=output) @cli.command("instance", help="Visualize a particular instance in the corresponding scene.") @@ -100,12 +99,7 @@ def pointcloud( _create_dir(output) t4 = Tier4(data_root, verbose=False) - scene_token = t4.scene[0].token - t4.render_pointcloud( - scene_token, - ignore_distortion=ignore_distortion, - save_dir=output, - ) + t4.render_pointcloud(ignore_distortion=ignore_distortion, save_dir=output) def _create_dir(dir_path: str | None) -> None: diff --git a/t4_devkit/helper/rendering.py b/t4_devkit/helper/rendering.py index 81ff5d2..e9b3bda 100644 --- a/t4_devkit/helper/rendering.py +++ b/t4_devkit/helper/rendering.py @@ -97,7 +97,6 @@ def _init_viewer( def render_scene( self, - scene_token: str, *, max_time_seconds: float = np.inf, future_seconds: float = 0.0, @@ -106,7 +105,6 @@ def render_scene( """Render specified scene. Args: - scene_token (str): Unique identifier of scene. max_time_seconds (float, optional): Max time length to be rendered [s]. future_seconds (float, optional): Future time in [s]. save_dir (str | None, optional): Directory path to save the recording. @@ -132,7 +130,7 @@ def render_scene( render3d = len(first_lidar_tokens) > 0 or len(first_radar_tokens) > 0 render2d = len(first_camera_tokens) > 0 - app_id = f"scene@{scene_token}" + app_id = f"scene@{self._t4.dataset_id}" viewer = self._init_viewer( app_id, render3d=render3d, @@ -141,7 +139,7 @@ def render_scene( save_dir=save_dir, ) - scene: Scene = self._t4.get("scene", scene_token) + scene: Scene = self._t4.scene[0] first_sample: Sample = self._t4.get("sample", scene.first_sample_token) max_timestamp_us = first_sample.timestamp + sec2us(max_time_seconds) @@ -212,7 +210,6 @@ def render_instance( if last_sample is None or current_last_sample.timestamp > last_sample.timestamp: last_sample = current_last_sample - scene_token = first_sample.scene_token max_timestamp_us = last_sample.timestamp # search first sample data tokens @@ -235,7 +232,7 @@ def render_instance( render3d = len(first_lidar_tokens) > 0 or len(first_radar_tokens) > 0 render2d = len(first_camera_tokens) > 0 - app_id = f"instance@{scene_token}" + app_id = f"instance@{self._t4.dataset_id}" viewer = self._init_viewer( app_id, render3d=render3d, @@ -279,7 +276,6 @@ def render_instance( def render_pointcloud( self, - scene_token: str, *, max_time_seconds: float = np.inf, ignore_distortion: bool = True, @@ -288,7 +284,6 @@ def render_pointcloud( """Render pointcloud on 3D and 2D view. Args: - scene_token (str): Scene token. max_time_seconds (float, optional): Max time length to be rendered [s]. ignore_distortion (bool, optional): Whether to ignore distortion parameters. save_dir (str | None, optional): Directory path to save the recording. @@ -298,7 +293,7 @@ def render_pointcloud( Add an option of rendering radar channels. """ # initialize viewer - app_id = f"pointcloud@{scene_token}" + app_id = f"pointcloud@{self._t4.dataset_id}" viewer = self._init_viewer(app_id, render_ann=False, save_dir=save_dir) # search first lidar sample data token diff --git a/t4_devkit/tier4.py b/t4_devkit/tier4.py index 8f0c102..d5583ee 100644 --- a/t4_devkit/tier4.py +++ b/t4_devkit/tier4.py @@ -692,7 +692,6 @@ def box_velocity(self, sample_annotation_token: str, max_time_diff: float = 1.5) def render_scene( self, - scene_token: str, *, max_time_seconds: float = np.inf, future_seconds: float = 0.0, @@ -701,13 +700,11 @@ def render_scene( """Render specified scene. Args: - scene_token (str): Unique identifier of scene. max_time_seconds (float, optional): Max time length to be rendered [s]. future_seconds (float, optional): Future time in [s]. save_dir (str | None, optional): Directory path to save the recording. """ self._rendering_helper.render_scene( - scene_token=scene_token, max_time_seconds=max_time_seconds, future_seconds=future_seconds, save_dir=save_dir, @@ -735,7 +732,6 @@ def render_instance( def render_pointcloud( self, - scene_token: str, *, max_time_seconds: float = np.inf, ignore_distortion: bool = True, @@ -744,7 +740,6 @@ def render_pointcloud( """Render pointcloud on 3D and 2D view. Args: - scene_token (str): Scene token. max_time_seconds (float, optional): Max time length to be rendered [s]. save_dir (str | None, optional): Directory path to save the recording. ignore_distortion (bool, optional): Whether to ignore distortion parameters. @@ -753,7 +748,6 @@ def render_pointcloud( Add an option of rendering radar channels. """ self._rendering_helper.render_pointcloud( - scene_token=scene_token, max_time_seconds=max_time_seconds, ignore_distortion=ignore_distortion, save_dir=save_dir,