Skip to content

Commit 1e04bf4

Browse files
committed
feat: separate lidarseg method
Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp>
1 parent 027be5b commit 1e04bf4

7 files changed

Lines changed: 193 additions & 24 deletions

File tree

docs/cli/t4viz.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ For options, run `t4viz pointcloud -h`.
6464
t4viz pointcloud <DATA_ROOT> [OPTIONS]
6565
```
6666

67+
### LiDAR Segmentation
68+
69+
This command performs the same behavior with [`Tier4.render_lidarseg(...)`](../tutorials/render.md#rendering-lidar-segmentation).
70+
71+
For options, run `t4viz lidarseg -h`.
72+
73+
```shell
74+
t4viz lidarseg <DATA_ROOT> [OPTIONS]
75+
```
76+
6777
### Future Trajectories
6878

6979
`scene` and `instance` commands support visualizing future trajectories for each object.

docs/tutorials/render.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,22 @@ If you want to visualize annotation results, `Tier4` supports some rendering met
4848
```
4949
<!-- prettier-ignore-end -->
5050

51+
### LiDAR Segmentation
52+
53+
```python
54+
>>> t4.render_lidarseg()
55+
```
56+
57+
<!--![Render LiDAR Segmentation GIF](../assets/render_lidarseg.gif)-->
58+
59+
<!-- prettier-ignore-start -->
60+
!!! NOTE
61+
`render_lidarseg()` method requires `lidarseg` data as follows:
62+
63+
```shell
64+
```
65+
<!-- prettier-ignore-end -->
66+
5167
### Save Recording
5268

5369
You can save the rendering result as follows:
@@ -134,15 +150,31 @@ It allows you to render boxes by specifying elements of boxes directly:
134150

135151
```python
136152
from t4_devkit.dataclass import LidarPointCloud
153+
from t4_devkit.viewer import PointCloudColorMode
137154
# Point cloud channel name
138155
>>> lidar_channel = "LIDAR_TOP"
139156
# Load point cloud from file
140157
>>> pointcloud = LidarPointCloud.from_file(<PATH_TO_POINTCLOUD.pcd.bin>)
141-
>>> viewer.render_pointcloud(seconds, lidar_channel, pointcloud)
158+
>>> color_mode = PointCloudColorMode.DISTANCE
159+
>>> viewer.render_pointcloud(seconds, lidar_channel, pointcloud, color_mode)
142160
```
143161

144162
![Render Point Cloud](../assets/render_pointcloud.png)
145163

164+
### Rendering LiDAR segmentation
165+
166+
```python
167+
from t4_devkit.dataclass import SegmentationPointCloud
168+
from t4_devkit.viewer import PointCloudColorMode
169+
# Point cloud channel name
170+
>>> lidar_channel = "LIDAR_TOP"
171+
# Load point cloud and label from file
172+
>>> pointcloud = SegmentationPointCloud.from_file("<PATH_TO_POINTCLOUD.pcd.bin>", "<PATH_TO_LABEL.pcd.bin>")
173+
>>> viewer.render_lidarseg(seconds, lidar_channel, pointcloud)
174+
```
175+
176+
<!--![Render LiDAR Segmentation](../assets/render_lidarseg.png)-->
177+
146178
### Rendering lanelet map
147179

148180
```python

t4_devkit/cli/visualize.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,43 @@ def pointcloud(
120120
t4.render_pointcloud(ignore_distortion=ignore_distortion, save_dir=output)
121121

122122

123+
@cli.command("lidarseg", help="Visualize LiDAR segmentation pointcloud.")
124+
def lidarseg(
125+
data_root: Annotated[str, typer.Argument(help="Root directory path to the dataset.")],
126+
revision: Annotated[
127+
str | None,
128+
typer.Option(
129+
...,
130+
"-rv",
131+
"--revision",
132+
help="Specify if you want to load the specific version.",
133+
),
134+
] = None,
135+
ignore_distortion: Annotated[
136+
bool,
137+
typer.Option(
138+
...,
139+
"-ig",
140+
"--ignore-distortion",
141+
help="Indicates whether to ignore camera distortion",
142+
),
143+
] = True,
144+
output: Annotated[
145+
str | None,
146+
typer.Option(
147+
...,
148+
"-o",
149+
"--output",
150+
help="Output directory to save recorded .rrd file.",
151+
),
152+
] = None,
153+
) -> None:
154+
_create_dir(output)
155+
156+
t4 = Tier4(data_root, revision=revision, verbose=False)
157+
t4.render_lidarseg(ignore_distortion=ignore_distortion, save_dir=output)
158+
159+
123160
def _create_dir(dir_path: str | None) -> None:
124161
"""Create a directory with the specified path.
125162

t4_devkit/helper/rendering.py

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,53 @@ def render_pointcloud(
318318
),
319319
)
320320

321+
def render_lidarseg(
322+
self,
323+
*,
324+
max_time_seconds: float = np.inf,
325+
ignore_distortion: bool = True,
326+
save_dir: str | None = None,
327+
) -> None:
328+
"""Render lidar segmentation.
329+
330+
Args:
331+
max_time_seconds (float, optional): Max time length to be rendered [s].
332+
ignore_distortion (bool, optional): Whether to ignore distortion parameters.
333+
save_dir (str | None, optional): Directory path to save the recording.
334+
Viewer will be spawned if it is None, otherwise not.
335+
"""
336+
if self._sample_data_to_lidarseg_filename is None:
337+
return
338+
339+
app_id = f"lidarseg@{self._t4.dataset_id}"
340+
viewer = self._init_viewer(app_id, render_ann=True, save_dir=save_dir)
341+
342+
self._render_map(viewer)
343+
344+
# search first lidar sample data token
345+
first_lidar_token: str | None = None
346+
for sensor in self._t4.sensor:
347+
if sensor.modality != SensorModality.LIDAR:
348+
continue
349+
first_lidar_token = sensor.first_sd_token
350+
351+
if first_lidar_token is None:
352+
raise ValueError("There is no 3D pointcloud data.")
353+
354+
first_lidar_sample_data: Sample = self._t4.get("sample_data", first_lidar_token)
355+
max_timestamp_us = first_lidar_sample_data.timestamp + seconds2microseconds(
356+
max_time_seconds
357+
)
358+
359+
concurrent.futures.wait(
360+
self._render_lidar_and_ego(
361+
viewer=viewer,
362+
first_lidar_tokens=[first_lidar_token],
363+
max_timestamp_us=max_timestamp_us,
364+
color_mode=PointCloudColorMode.SEGMENTATION,
365+
)
366+
)
367+
321368
def _render_map(self, viewer: RerunViewer) -> None:
322369
lanelet_path = osp.join(self._t4.map_dir, "lanelet2_map.osm")
323370
viewer.render_map(lanelet_path)
@@ -342,6 +389,7 @@ def _render_lidar_and_ego(
342389
viewer: RerunViewer,
343390
first_lidar_tokens: list[str],
344391
max_timestamp_us: float,
392+
*,
345393
color_mode: PointCloudColorMode = PointCloudColorMode.DISTANCE,
346394
) -> list[Future]:
347395
def _render_single_lidar(first_lidar_token: str) -> None:
@@ -358,26 +406,34 @@ def _render_single_lidar(first_lidar_token: str) -> None:
358406
viewer.render_ego(ego_pose=ego_pose)
359407

360408
# render segmentation pointcloud if available, otherwise render raw pointcloud
361-
if (
362-
self._sample_data_to_lidarseg_filename
363-
and sample_data.token in self._sample_data_to_lidarseg_filename
364-
):
409+
if color_mode == PointCloudColorMode.SEGMENTATION:
410+
if not (
411+
self._sample_data_to_lidarseg_filename
412+
and sample_data.token in self._sample_data_to_lidarseg_filename
413+
):
414+
continue
415+
365416
label_filename = self._sample_data_to_lidarseg_filename[sample_data.token]
366417
pointcloud = SegmentationPointCloud.from_file(
367418
point_filepath=osp.join(self._t4.data_root, sample_data.filename),
368419
label_filepath=osp.join(self._t4.data_root, label_filename),
369420
)
421+
viewer.render_lidarseg(
422+
seconds=microseconds2seconds(sample_data.timestamp),
423+
channel=sample_data.channel,
424+
pointcloud=pointcloud,
425+
)
370426
else:
371427
pointcloud = LidarPointCloud.from_file(
372428
osp.join(self._t4.data_root, sample_data.filename)
373429
)
374430

375-
viewer.render_pointcloud(
376-
seconds=microseconds2seconds(sample_data.timestamp),
377-
channel=sample_data.channel,
378-
pointcloud=pointcloud,
379-
color_mode=color_mode,
380-
)
431+
viewer.render_pointcloud(
432+
seconds=microseconds2seconds(sample_data.timestamp),
433+
channel=sample_data.channel,
434+
pointcloud=pointcloud,
435+
color_mode=color_mode,
436+
)
381437

382438
current_lidar_token = sample_data.next
383439

t4_devkit/tier4.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,3 +799,23 @@ def render_pointcloud(
799799
ignore_distortion=ignore_distortion,
800800
save_dir=save_dir,
801801
)
802+
803+
def render_lidarseg(
804+
self,
805+
*,
806+
max_time_seconds: float = np.inf,
807+
ignore_distortion: bool = True,
808+
save_dir: str | None = None,
809+
) -> None:
810+
"""Render lidarseg on 3D view.
811+
812+
Args:
813+
max_time_seconds (float, optional): Max time length to be rendered [s].
814+
ignore_distortion (bool, optional): Whether to ignore distortion parameters.
815+
save_dir (str | None, optional): Directory path to save the recording.
816+
"""
817+
self._rendering_helper.render_lidarseg(
818+
max_time_seconds=max_time_seconds,
819+
ignore_distortion=ignore_distortion,
820+
save_dir=save_dir,
821+
)

t4_devkit/viewer/color.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class PointCloudColorMode(str, Enum):
2020

2121
DISTANCE = "distance"
2222
INTENSITY = "intensity"
23+
SEGMENTATION = "segmentation"
2324

2425

2526
def pointcloud_color(

t4_devkit/viewer/viewer.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
from t4_devkit.common.converter import to_quaternion
1111
from t4_devkit.common.timestamp import microseconds2seconds
12-
from t4_devkit.dataclass import SegmentationPointCloud
1312
from t4_devkit.lanelet import LaneletParser
1413
from t4_devkit.schema import SensorModality
1514

@@ -25,7 +24,7 @@
2524
from .record import BatchBox2D, BatchBox3D, BatchSegmentation2D
2625

2726
if TYPE_CHECKING:
28-
from t4_devkit.dataclass import Box2D, Box3D, Future, PointCloudLike
27+
from t4_devkit.dataclass import Box2D, Box3D, Future, PointCloudLike, SegmentationPointCloud
2928
from t4_devkit.schema import CalibratedSensor, EgoPose, Sensor
3029
from t4_devkit.typing import (
3130
CameraIntrinsicLike,
@@ -427,17 +426,31 @@ def render_pointcloud(
427426
# TODO(ktro2828): add support of rendering pointcloud on images
428427
rr.set_time_seconds(self.config.timeline, seconds)
429428

430-
if isinstance(pointcloud, SegmentationPointCloud):
431-
rr.log(
432-
format_entity(self.config.ego_entity, channel),
433-
rr.Points3D(pointcloud.points[:3].T, class_ids=pointcloud.labels),
434-
)
435-
else:
436-
colors = pointcloud_color(pointcloud, color_mode=color_mode)
437-
rr.log(
438-
format_entity(self.config.ego_entity, channel),
439-
rr.Points3D(pointcloud.points[:3].T, colors=colors),
440-
)
429+
colors = pointcloud_color(pointcloud, color_mode=color_mode)
430+
rr.log(
431+
format_entity(self.config.ego_entity, channel),
432+
rr.Points3D(pointcloud.points[:3].T, colors=colors),
433+
)
434+
435+
@_check_spatial3d
436+
def render_lidarseg(
437+
self,
438+
seconds: float,
439+
channel: str,
440+
pointcloud: SegmentationPointCloud,
441+
) -> None:
442+
"""Render a LiDAR segmentation point cloud.
443+
444+
Args:
445+
seconds (float): Timestamp in [sec].
446+
channel (str): Name of the pointcloud sensor channel.
447+
pointcloud (SegmentationPointCloud): Segmentation pointcloud.
448+
"""
449+
rr.set_time_seconds(self.config.timeline, seconds)
450+
rr.log(
451+
format_entity(self.config.ego_entity, channel),
452+
rr.Points3D(pointcloud.points[:3].T, class_ids=pointcloud.labels),
453+
)
441454

442455
@_check_spatial2d
443456
def render_image(self, seconds: float, camera: str, image: str | NDArrayU8) -> None:

0 commit comments

Comments
 (0)