Skip to content

Commit a62b4b3

Browse files
ktro2828mojomexSamratThapa120Copilot
authored
feat(viewer): add support of rendering pointcloud segmentation (#261)
* feat: add support of rendering segmentation pointcloud Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * feat: separate lidarseg method Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * 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> * refactor: remove lidarseg command from t4viz Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * refactor: replace lidarseg existence checke Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * feat: add support of rendering segmentation pointcloud Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * fix: resolve invalid usage of future submitting and enable to raise exceptions Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * docs: add visualization sample Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * refactor: remove RerunViewer.render_pointcloud(...) Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> --------- Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> Signed-off-by: Max SCHMELLER <max.schmeller@tier4.jp> Co-authored-by: Max Schmeller <6088931+mojomex@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 c51d395 commit a62b4b3

7 files changed

Lines changed: 139 additions & 58 deletions

File tree

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/assets/render_lidarseg.png

2.33 MB
Loading

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 ||

docs/tutorials/render.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,32 @@ It allows you to render boxes by specifying elements of boxes directly:
134134

135135
```python
136136
from t4_devkit.dataclass import LidarPointCloud
137+
from t4_devkit.viewer import PointCloudColorMode
137138
# Point cloud channel name
138139
>>> lidar_channel = "LIDAR_TOP"
139140
# Load point cloud from file
140141
>>> pointcloud = LidarPointCloud.from_file(<PATH_TO_POINTCLOUD.pcd.bin>)
141-
>>> viewer.render_pointcloud(seconds, lidar_channel, pointcloud)
142+
>>> color_mode = PointCloudColorMode.DISTANCE
143+
>>> viewer.render_pointcloud(seconds, lidar_channel, pointcloud, color_mode)
142144
```
143145

144146
![Render Point Cloud](../assets/render_pointcloud.png)
145147

148+
### Rendering LiDAR segmentation
149+
150+
```python
151+
from t4_devkit.dataclass import SegmentationPointCloud
152+
from t4_devkit.viewer import PointCloudColorMode
153+
# Point cloud channel name
154+
>>> lidar_channel = "LIDAR_TOP"
155+
# Load point cloud and label from file
156+
>>> pointcloud = SegmentationPointCloud.from_file("<PATH_TO_POINTCLOUD.pcd.bin>", "<PATH_TO_LABEL.pcd.bin>")
157+
>>> color_mode = PointCloudColorMode.SEGMENTATION
158+
>>> viewer.render_pointcloud(seconds, lidar_channel, pointcloud, color_mode)
159+
```
160+
161+
![Render LiDAR Segmentation](../assets/render_lidarseg.png)
162+
146163
### Rendering lanelet map
147164

148165
```python

t4_devkit/helper/rendering.py

Lines changed: 104 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from t4_devkit.common.geometry import view_points
1515
from t4_devkit.common.timestamp import microseconds2seconds, seconds2microseconds
16-
from t4_devkit.dataclass import LidarPointCloud, RadarPointCloud
16+
from t4_devkit.dataclass import LidarPointCloud, RadarPointCloud, SegmentationPointCloud
1717
from t4_devkit.schema import SensorModality
1818
from t4_devkit.viewer import (
1919
PointCloudColorMode,
@@ -56,9 +56,17 @@ def __init__(self, t4: Tier4) -> None:
5656
self._label2id: dict[str, int] = {
5757
category.name: category.index for category in self._t4.category
5858
}
59+
self._sample_data_to_lidarseg_filename: dict[str, str] | None = (
60+
{lidarseg.sample_data_token: lidarseg.filename for lidarseg in self._t4.lidarseg}
61+
if self._t4.lidarseg
62+
else None
63+
)
5964

6065
self._executor = concurrent.futures.ThreadPoolExecutor()
6166

67+
def _has_lidarseg(self) -> bool:
68+
return self._sample_data_to_lidarseg_filename is not None
69+
6270
def _init_viewer(
6371
self,
6472
app_id: str,
@@ -122,17 +130,24 @@ def render_scene(
122130
app_id = f"scene@{self._t4.dataset_id}"
123131
viewer = self._init_viewer(app_id, render_ann=True, save_dir=save_dir)
124132

125-
self._render_map(viewer)
133+
self._try_render_map(viewer)
126134

127135
scene: Scene = self._t4.scene[0]
128136
first_sample: Sample = self._t4.get("sample", scene.first_sample_token)
129137
max_timestamp_us = first_sample.timestamp + seconds2microseconds(max_time_seconds)
130138

131-
concurrent.futures.wait(
139+
pointcloud_color_mode = (
140+
PointCloudColorMode.SEGMENTATION
141+
if self._has_lidarseg()
142+
else PointCloudColorMode.DISTANCE
143+
)
144+
145+
futures = (
132146
self._render_lidar_and_ego(
133147
viewer=viewer,
134148
first_lidar_tokens=first_lidar_tokens,
135149
max_timestamp_us=max_timestamp_us,
150+
color_mode=pointcloud_color_mode,
136151
)
137152
+ self._render_radars(
138153
viewer=viewer,
@@ -146,23 +161,23 @@ def render_scene(
146161
)
147162
+ [
148163
self._executor.submit(
149-
self._render_annotation3ds(
150-
viewer=viewer,
151-
first_sample_token=scene.first_sample_token,
152-
max_timestamp_us=max_timestamp_us,
153-
future_seconds=future_seconds,
154-
)
164+
self._render_annotation3ds,
165+
viewer=viewer,
166+
first_sample_token=scene.first_sample_token,
167+
max_timestamp_us=max_timestamp_us,
168+
future_seconds=future_seconds,
155169
),
156170
self._executor.submit(
157-
self._render_annotation2ds(
158-
viewer=viewer,
159-
first_sample_token=scene.first_sample_token,
160-
max_timestamp_us=max_timestamp_us,
161-
)
171+
self._render_annotation2ds,
172+
viewer=viewer,
173+
first_sample_token=scene.first_sample_token,
174+
max_timestamp_us=max_timestamp_us,
162175
),
163176
]
164177
)
165178

179+
_handle_futures(futures)
180+
166181
def render_instance(
167182
self,
168183
instance_token: str | Sequence[str],
@@ -221,13 +236,20 @@ def render_instance(
221236
app_id = f"instance@{self._t4.dataset_id}"
222237
viewer = self._init_viewer(app_id, render_ann=True, save_dir=save_dir)
223238

224-
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+
)
225246

226-
concurrent.futures.wait(
247+
futures = (
227248
self._render_lidar_and_ego(
228249
viewer=viewer,
229250
first_lidar_tokens=first_lidar_tokens,
230251
max_timestamp_us=max_timestamp_us,
252+
color_mode=pointcloud_color_mode,
231253
)
232254
+ self._render_radars(
233255
viewer=viewer,
@@ -241,25 +263,25 @@ def render_instance(
241263
)
242264
+ [
243265
self._executor.submit(
244-
self._render_annotation3ds(
245-
viewer=viewer,
246-
first_sample_token=first_sample.token,
247-
max_timestamp_us=max_timestamp_us,
248-
future_seconds=future_seconds,
249-
instance_tokens=instance_tokens,
250-
)
266+
self._render_annotation3ds,
267+
viewer=viewer,
268+
first_sample_token=first_sample.token,
269+
max_timestamp_us=max_timestamp_us,
270+
future_seconds=future_seconds,
271+
instance_tokens=instance_tokens,
251272
),
252273
self._executor.submit(
253-
self._render_annotation2ds(
254-
viewer=viewer,
255-
first_sample_token=first_sample.token,
256-
max_timestamp_us=max_timestamp_us,
257-
instance_tokens=instance_tokens,
258-
)
274+
self._render_annotation2ds,
275+
viewer=viewer,
276+
first_sample_token=first_sample.token,
277+
max_timestamp_us=max_timestamp_us,
278+
instance_tokens=instance_tokens,
259279
),
260-
],
280+
]
261281
)
262282

283+
_handle_futures(futures)
284+
263285
def render_pointcloud(
264286
self,
265287
*,
@@ -282,7 +304,7 @@ def render_pointcloud(
282304
app_id = f"pointcloud@{self._t4.dataset_id}"
283305
viewer = self._init_viewer(app_id, render_ann=False, save_dir=save_dir)
284306

285-
self._render_map(viewer)
307+
self._try_render_map(viewer)
286308

287309
# search first lidar sample data token
288310
first_lidar_token: str | None = None
@@ -299,22 +321,24 @@ def render_pointcloud(
299321
max_time_seconds
300322
)
301323

302-
concurrent.futures.wait(
303-
self._render_lidar_and_ego(
304-
viewer=viewer,
305-
first_lidar_tokens=[first_lidar_token],
306-
max_timestamp_us=max_timestamp_us,
307-
)
308-
+ self._render_points_on_cameras(
309-
first_point_sample_data_token=first_lidar_token,
310-
max_timestamp_us=max_timestamp_us,
311-
min_dist=1.0,
312-
ignore_distortion=ignore_distortion,
313-
),
324+
# TODO: support rendering segmentation pointcloud on camera
325+
futures = self._render_lidar_and_ego(
326+
viewer=viewer,
327+
first_lidar_tokens=[first_lidar_token],
328+
max_timestamp_us=max_timestamp_us,
329+
) + self._render_points_on_cameras(
330+
first_point_sample_data_token=first_lidar_token,
331+
max_timestamp_us=max_timestamp_us,
332+
min_dist=1.0,
333+
ignore_distortion=ignore_distortion,
314334
)
315335

316-
def _render_map(self, viewer: RerunViewer) -> None:
336+
_handle_futures(futures)
337+
338+
def _try_render_map(self, viewer: RerunViewer) -> None:
317339
lanelet_path = osp.join(self._t4.map_dir, "lanelet2_map.osm")
340+
if not osp.exists(lanelet_path):
341+
return
318342
viewer.render_map(lanelet_path)
319343

320344
def _render_sensor_calibration(self, viewer: RerunViewer, sample_data_token: str) -> None:
@@ -337,6 +361,7 @@ def _render_lidar_and_ego(
337361
viewer: RerunViewer,
338362
first_lidar_tokens: list[str],
339363
max_timestamp_us: float,
364+
*,
340365
color_mode: PointCloudColorMode = PointCloudColorMode.DISTANCE,
341366
) -> list[Future]:
342367
def _render_single_lidar(first_lidar_token: str) -> None:
@@ -345,25 +370,39 @@ def _render_single_lidar(first_lidar_token: str) -> None:
345370
current_lidar_token = first_lidar_token
346371
while current_lidar_token != "":
347372
sample_data: SampleData = self._t4.get("sample_data", current_lidar_token)
373+
current_lidar_token = sample_data.next
348374

349375
if max_timestamp_us < sample_data.timestamp:
350376
break
351377

352378
ego_pose: EgoPose = self._t4.get("ego_pose", sample_data.ego_pose_token)
353379
viewer.render_ego(ego_pose=ego_pose)
354380

355-
pointcloud = LidarPointCloud.from_file(
356-
osp.join(self._t4.data_root, sample_data.filename)
357-
)
381+
# render segmentation pointcloud if available, otherwise render raw pointcloud
382+
if color_mode == PointCloudColorMode.SEGMENTATION:
383+
if not (
384+
self._has_lidarseg()
385+
and sample_data.token in self._sample_data_to_lidarseg_filename
386+
):
387+
continue
388+
389+
label_filename = self._sample_data_to_lidarseg_filename[sample_data.token]
390+
pointcloud = SegmentationPointCloud.from_file(
391+
point_filepath=osp.join(self._t4.data_root, sample_data.filename),
392+
label_filepath=osp.join(self._t4.data_root, label_filename),
393+
)
394+
else:
395+
pointcloud = LidarPointCloud.from_file(
396+
osp.join(self._t4.data_root, sample_data.filename)
397+
)
398+
358399
viewer.render_pointcloud(
359400
seconds=microseconds2seconds(sample_data.timestamp),
360401
channel=sample_data.channel,
361402
pointcloud=pointcloud,
362403
color_mode=color_mode,
363404
)
364405

365-
current_lidar_token = sample_data.next
366-
367406
return [self._executor.submit(_render_single_lidar, token) for token in first_lidar_tokens]
368407

369408
def _render_radars(
@@ -699,3 +738,19 @@ def _append_mask(
699738
camera_masks[camera]["class_ids"] = [class_id]
700739
camera_masks[camera]["uuids"] = [uuid]
701740
return camera_masks
741+
742+
743+
def _handle_futures(futures: list[Future]) -> None:
744+
"""Wait for all futures and raise exception if any.
745+
746+
Args:
747+
futures (list[Future]): List of futures.
748+
"""
749+
if not futures:
750+
return
751+
752+
concurrent.futures.wait(futures)
753+
for future in futures:
754+
if not future.done():
755+
continue
756+
future.result()

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: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
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
1213
from t4_devkit.lanelet import LaneletParser
1314
from t4_devkit.schema import SensorModality
1415

@@ -24,7 +25,7 @@
2425
from .record import BatchBox2D, BatchBox3D, BatchSegmentation2D
2526

2627
if TYPE_CHECKING:
27-
from t4_devkit.dataclass import Box2D, Box3D, Future, PointCloudLike
28+
from t4_devkit.dataclass import Box2D, Box3D, Future, PointCloudLike, SegmentationPointCloud
2829
from t4_devkit.schema import CalibratedSensor, EgoPose, Sensor
2930
from t4_devkit.typing import (
3031
CameraIntrinsicLike,
@@ -426,11 +427,18 @@ def render_pointcloud(
426427
# TODO(ktro2828): add support of rendering pointcloud on images
427428
rr.set_time_seconds(self.config.timeline, seconds)
428429

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-
)
430+
if color_mode == PointCloudColorMode.SEGMENTATION:
431+
assert isinstance(pointcloud, SegmentationPointCloud)
432+
rr.log(
433+
format_entity(self.config.ego_entity, channel),
434+
rr.Points3D(pointcloud.points[:3].T, class_ids=pointcloud.labels),
435+
)
436+
else:
437+
colors = pointcloud_color(pointcloud, color_mode=color_mode)
438+
rr.log(
439+
format_entity(self.config.ego_entity, channel),
440+
rr.Points3D(pointcloud.points[:3].T, colors=colors),
441+
)
434442

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

0 commit comments

Comments
 (0)