Skip to content

Commit 3f5a0d2

Browse files
mojomexktro2828SamratThapa120Copilot
authored
chore(viewer): lidarseg tweaks (#256)
* chore(deps): restrict rerun-sdk to versions >=0.20.0,<0.28.0 (#248) * Bump version from 0.5.2 to 0.5.3 (#247) * Bump version from 0.5.2 to 0.5.3 * Pin rerun-sdk dependency to version 0.20.0 * refactor: update custom validator definition (#250) * refactor: update custom validator definition Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * Update t4_devkit/schema/tables/base.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: modity type of size to tuple[int, int] (#251) Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * fix: abstract away category indexing differences Semseg and non-semseg datasets behave differently (position-based vs. explicit indexing). Now computing index field in case position-based indexing is used. Signed-off-by: Max SCHMELLER <max.schmeller@tier4.jp> * fix(rendering): don't fail when map is not present Signed-off-by: Max SCHMELLER <max.schmeller@tier4.jp> * fix(rendering): fix iteration freezing when skipping a pointcloud without lidarseg file Signed-off-by: Max SCHMELLER <max.schmeller@tier4.jp> * feat(rendering): auto-enable SEGMENTATION coloring when lidarseg is available Signed-off-by: Max SCHMELLER <max.schmeller@tier4.jp> * docs: checkmark for semseg viz Signed-off-by: Max SCHMELLER <max.schmeller@tier4.jp> * fix(rendering): fix wrongly named `color_mode` args Signed-off-by: Max SCHMELLER <max.schmeller@tier4.jp> * refactor(compatibility): move to schema package Signed-off-by: Max SCHMELLER <max.schmeller@tier4.jp> --------- Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> Signed-off-by: Max SCHMELLER <max.schmeller@tier4.jp> Co-authored-by: Kotaro Uetake <60615504+ktro2828@users.noreply.github.com> Co-authored-by: Samrat Thapa <38401989+SamratThapa120@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 1e04bf4 commit 3f5a0d2

19 files changed

Lines changed: 117 additions & 48 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ More details, please refer to [`t4viz` CLI](./docs/cli/t4viz.md) or [API usage](
5959
| Feature | Task | Support |
6060
| :------ | :-------------------------- | :-----: |
6161
| 3D | 3D Boxes ||
62-
| | PointCloud Segmentation | |
62+
| | PointCloud Segmentation | |
6363
| | Raw PointCloud ||
6464
| | 3D Trajectories ||
6565
| | TF Links ||

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ More details, please refer to [`t4viz` CLI](./cli/t4viz.md) or [API usage](./tut
1717
| Feature | Task | Support |
1818
| :------ | :-------------------------- | :-----: |
1919
| 3D | 3D Boxes ||
20-
| | PointCloud Segmentation | |
20+
| | PointCloud Segmentation | |
2121
| | Raw PointCloud ||
2222
| | 3D Trajectories ||
2323
| | TF Links ||

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "t4-devkit"
7-
version = "0.5.2"
7+
version = "0.5.3"
88
description = "A toolkit to load and operate T4 dataset."
99
authors = [{ name = "Kotaro Uetake", email = "kotaro.uetake@tier4.jp" }]
1010
readme = "README.md"
1111
requires-python = ">=3.10,<3.13"
1212
dependencies = [
13-
"rerun-sdk>=0.20.0",
13+
"rerun-sdk>=0.20.0,<0.28.0",
1414
"pyquaternion>=0.9.9",
1515
"matplotlib>=3.9.2",
1616
"shapely<2.0.0; python_version=='3.10'",

t4_devkit/helper/rendering.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __init__(self, t4: Tier4) -> None:
5454
"""
5555
self._t4 = t4
5656
self._label2id: dict[str, int] = {
57-
category.name: idx for idx, category in enumerate(self._t4.category)
57+
category.name: category.index for category in self._t4.category
5858
}
5959
self._sample_data_to_lidarseg_filename: dict[str, str] | None = (
6060
{lidarseg.sample_data_token: lidarseg.filename for lidarseg in self._t4.lidarseg}
@@ -64,6 +64,9 @@ def __init__(self, t4: Tier4) -> None:
6464

6565
self._executor = concurrent.futures.ThreadPoolExecutor()
6666

67+
def _has_lidarseg(self) -> bool:
68+
return self._sample_data_to_lidarseg_filename is not None
69+
6770
def _init_viewer(
6871
self,
6972
app_id: str,
@@ -127,17 +130,24 @@ def render_scene(
127130
app_id = f"scene@{self._t4.dataset_id}"
128131
viewer = self._init_viewer(app_id, render_ann=True, save_dir=save_dir)
129132

130-
# self._render_map(viewer)
133+
self._try_render_map(viewer)
131134

132135
scene: Scene = self._t4.scene[0]
133136
first_sample: Sample = self._t4.get("sample", scene.first_sample_token)
134137
max_timestamp_us = first_sample.timestamp + seconds2microseconds(max_time_seconds)
135138

139+
pointcloud_color_mode = (
140+
PointCloudColorMode.SEGMENTATION
141+
if self._has_lidarseg()
142+
else PointCloudColorMode.DISTANCE
143+
)
144+
136145
concurrent.futures.wait(
137146
self._render_lidar_and_ego(
138147
viewer=viewer,
139148
first_lidar_tokens=first_lidar_tokens,
140149
max_timestamp_us=max_timestamp_us,
150+
color_mode=pointcloud_color_mode,
141151
)
142152
+ self._render_radars(
143153
viewer=viewer,
@@ -226,13 +236,20 @@ def render_instance(
226236
app_id = f"instance@{self._t4.dataset_id}"
227237
viewer = self._init_viewer(app_id, render_ann=True, save_dir=save_dir)
228238

229-
self._render_map(viewer)
239+
self._try_render_map(viewer)
240+
241+
pointcloud_color_mode = (
242+
PointCloudColorMode.SEGMENTATION
243+
if self._has_lidarseg()
244+
else PointCloudColorMode.DISTANCE
245+
)
230246

231247
concurrent.futures.wait(
232248
self._render_lidar_and_ego(
233249
viewer=viewer,
234250
first_lidar_tokens=first_lidar_tokens,
235251
max_timestamp_us=max_timestamp_us,
252+
color_mode=pointcloud_color_mode,
236253
)
237254
+ self._render_radars(
238255
viewer=viewer,
@@ -287,7 +304,7 @@ def render_pointcloud(
287304
app_id = f"pointcloud@{self._t4.dataset_id}"
288305
viewer = self._init_viewer(app_id, render_ann=False, save_dir=save_dir)
289306

290-
self._render_map(viewer)
307+
self._try_render_map(viewer)
291308

292309
# search first lidar sample data token
293310
first_lidar_token: str | None = None
@@ -304,17 +321,25 @@ def render_pointcloud(
304321
max_time_seconds
305322
)
306323

324+
pointcloud_color_mode = (
325+
PointCloudColorMode.SEGMENTATION
326+
if self._has_lidarseg()
327+
else PointCloudColorMode.DISTANCE
328+
)
329+
307330
concurrent.futures.wait(
308331
self._render_lidar_and_ego(
309332
viewer=viewer,
310333
first_lidar_tokens=[first_lidar_token],
311334
max_timestamp_us=max_timestamp_us,
335+
color_mode=pointcloud_color_mode,
312336
)
313337
+ self._render_points_on_cameras(
314338
first_point_sample_data_token=first_lidar_token,
315339
max_timestamp_us=max_timestamp_us,
316340
min_dist=1.0,
317341
ignore_distortion=ignore_distortion,
342+
color_mode=pointcloud_color_mode,
318343
),
319344
)
320345

@@ -339,7 +364,7 @@ def render_lidarseg(
339364
app_id = f"lidarseg@{self._t4.dataset_id}"
340365
viewer = self._init_viewer(app_id, render_ann=True, save_dir=save_dir)
341366

342-
self._render_map(viewer)
367+
self._try_render_map(viewer)
343368

344369
# search first lidar sample data token
345370
first_lidar_token: str | None = None
@@ -365,8 +390,10 @@ def render_lidarseg(
365390
)
366391
)
367392

368-
def _render_map(self, viewer: RerunViewer) -> None:
393+
def _try_render_map(self, viewer: RerunViewer) -> None:
369394
lanelet_path = osp.join(self._t4.map_dir, "lanelet2_map.osm")
395+
if not osp.exists(lanelet_path):
396+
return
370397
viewer.render_map(lanelet_path)
371398

372399
def _render_sensor_calibration(self, viewer: RerunViewer, sample_data_token: str) -> None:
@@ -398,6 +425,7 @@ def _render_single_lidar(first_lidar_token: str) -> None:
398425
current_lidar_token = first_lidar_token
399426
while current_lidar_token != "":
400427
sample_data: SampleData = self._t4.get("sample_data", current_lidar_token)
428+
current_lidar_token = sample_data.next
401429

402430
if max_timestamp_us < sample_data.timestamp:
403431
break
@@ -435,8 +463,6 @@ def _render_single_lidar(first_lidar_token: str) -> None:
435463
color_mode=color_mode,
436464
)
437465

438-
current_lidar_token = sample_data.next
439-
440466
return [self._executor.submit(_render_single_lidar, token) for token in first_lidar_tokens]
441467

442468
def _render_radars(

t4_devkit/schema/compatibility.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Compatibility helpers that abstract away differences between T4Dataset revisions."""
2+
3+
from __future__ import annotations
4+
5+
from t4_devkit.schema import Category
6+
7+
8+
def fix_category_table(categories: list[Category]) -> list[Category]:
9+
"""Fix unpopulated index fields in some T4Dataset revisions.
10+
11+
This function behaves differently in the below three cases:
12+
13+
- All `index` fields are set: the list is returned unmodified.
14+
- All `index` fields are `None`: the position of each category in the list is used to compute
15+
`index`. The resulting indices start at `0`.
16+
- Some `index` fields are set and some are `None`: raise a `ValueError`.
17+
18+
Args:
19+
categories (list[Category]): List of categories to fix.
20+
21+
Returns:
22+
list[Category]: Fixed list of categories.
23+
24+
Raises:
25+
ValueError: If the `index` field is set for some categories and `None` for others.
26+
"""
27+
if any(category.index is None for category in categories):
28+
if not all(category.index is None for category in categories):
29+
raise ValueError("Category index is not set for some categories.")
30+
31+
for idx, category in enumerate(categories):
32+
category.index = idx
33+
return categories

t4_devkit/schema/tables/autolabel_metadata.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,12 @@ class AutolabelModel:
1818

1919
name: str = field(validator=validators.instance_of(str))
2020
score: float = field(
21-
validator=[
22-
validators.instance_of(float),
23-
validators.and_(validators.ge(0.0), validators.le(1.0)),
24-
]
21+
validator=(validators.instance_of(float), validators.ge(0.0), validators.le(1.0))
2522
)
2623
uncertainty: float | None = field(
2724
default=None,
2825
validator=validators.optional(
29-
(validators.instance_of(float), validators.and_(validators.ge(0.0), validators.le(1.0)))
26+
validators.and_(validators.instance_of(float), validators.ge(0.0), validators.le(1.0))
3027
),
3128
)
3229

t4_devkit/schema/tables/base.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,23 @@
1111
__all__ = ["SchemaBase", "SchemaTable"]
1212

1313

14-
def impossible_empty(instance, attribute, value: Sized) -> None:
15-
"""A validator that raises ValueError if value is empty."""
16-
if len(value) == 0:
17-
raise ValueError(f"{attribute.name} cannot be empty")
14+
def impossible_empty():
15+
"""Factory function that returns a validator instance which raises ValueError if value is empty."""
16+
return _ImpossibleEmptyValidator()
17+
18+
19+
@define(slots=True, unsafe_hash=True)
20+
class _ImpossibleEmptyValidator:
21+
def __call__(self, instance, attribute, value: Sized) -> None:
22+
if len(value) == 0:
23+
raise ValueError(f"{attribute.name} cannot be empty")
1824

1925

2026
@define
2127
class SchemaBase(ABC):
2228
"""Abstract base dataclass of schema tables."""
2329

24-
token: str = field(validator=(validators.instance_of(str), impossible_empty))
30+
token: str = field(validator=(validators.instance_of(str), impossible_empty()))
2531

2632
@classmethod
2733
def from_json(cls, filepath: str) -> list[SchemaTable]:

t4_devkit/schema/tables/calibrated_sensor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class CalibratedSensor(SchemaBase):
2626
camera_distortion (CameraDistortion): Camera distortion array. Empty for sensors that are not cameras.
2727
"""
2828

29-
sensor_token: str = field(validator=(validators.instance_of(str), impossible_empty))
29+
sensor_token: str = field(validator=(validators.instance_of(str), impossible_empty()))
3030
translation: Vector3 = field(converter=Vector3)
3131
rotation: Quaternion = field(converter=to_quaternion)
3232
camera_intrinsic: CameraIntrinsic = field(converter=CameraIntrinsic)

t4_devkit/schema/tables/instance.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class Instance(SchemaBase):
2525
Empty if the dataset only contains 2D annotations.
2626
"""
2727

28-
category_token: str = field(validator=(validators.instance_of(str), impossible_empty))
28+
category_token: str = field(validator=(validators.instance_of(str), impossible_empty()))
2929
instance_name: str = field(validator=validators.instance_of(str))
3030
nbr_annotations: int = field(validator=validators.instance_of(int))
3131
first_annotation_token: str = field(validator=validators.instance_of(str))

t4_devkit/schema/tables/keypoint.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ class Keypoint(SchemaBase):
2929
num_keypoints (int): The number of keypoints to be annotated.
3030
"""
3131

32-
sample_data_token: str = field(validator=(validators.instance_of(str), impossible_empty))
33-
instance_token: str = field(validator=(validators.instance_of(str), impossible_empty))
32+
sample_data_token: str = field(validator=(validators.instance_of(str), impossible_empty()))
33+
instance_token: str = field(validator=(validators.instance_of(str), impossible_empty()))
3434
category_tokens: list[str] = field(
35-
validator=validators.deep_iterable((validators.instance_of(str), impossible_empty))
35+
validator=validators.deep_iterable((validators.instance_of(str), impossible_empty()))
3636
)
3737
keypoints: KeypointLike = field(converter=np.array)
3838
num_keypoints: int = field(validator=validators.instance_of(int))

0 commit comments

Comments
 (0)