|
8 | 8 | import numpy as np |
9 | 9 | import rerun as rr |
10 | 10 | import yaml |
| 11 | +from PIL import Image |
11 | 12 |
|
| 13 | +from t4_devkit.common.geometry import view_points |
12 | 14 | from t4_devkit.common.timestamp import sec2us, us2sec |
13 | 15 | from t4_devkit.dataclass import LidarPointCloud, RadarPointCloud |
14 | 16 | from t4_devkit.schema import SensorModality |
|
28 | 30 | Sensor, |
29 | 31 | SurfaceAnn, |
30 | 32 | ) |
| 33 | + from t4_devkit.typing import NDArrayF64, NDArrayU8 |
31 | 34 |
|
32 | 35 | __all__ = ["RenderingHelper"] |
33 | 36 |
|
@@ -489,7 +492,7 @@ async def render_points_on_camera( |
489 | 492 | if max_timestamp_us < sample.timestamp: |
490 | 493 | break |
491 | 494 |
|
492 | | - points_on_image, depths, image = self._t4.project_pointcloud( |
| 495 | + points_on_image, depths, image = self._project_pointcloud( |
493 | 496 | point_sample_data_token=current_point_sample_data_token, |
494 | 497 | camera_sample_data_token=camera_sample_data_token, |
495 | 498 | min_dist=min_dist, |
@@ -519,6 +522,86 @@ async def render_points_on_camera( |
519 | 522 | ] |
520 | 523 | ) |
521 | 524 |
|
| 525 | + def _project_pointcloud( |
| 526 | + self, |
| 527 | + point_sample_data_token: str, |
| 528 | + camera_sample_data_token: str, |
| 529 | + min_dist: float = 1.0, |
| 530 | + *, |
| 531 | + ignore_distortion: bool = True, |
| 532 | + ) -> tuple[NDArrayF64, NDArrayF64, NDArrayU8]: |
| 533 | + """Project pointcloud on image plane. |
| 534 | +
|
| 535 | + Args: |
| 536 | + point_sample_data_token (str): Sample data token of lidar or radar sensor. |
| 537 | + camera_sample_data_token (str): Sample data token of camera. |
| 538 | + min_dist (float, optional): Distance from the camera below which points are discarded. |
| 539 | + ignore_distortion (bool, optional): Whether to ignore distortion parameters. |
| 540 | +
|
| 541 | + Returns: |
| 542 | + Projected points [2, n], their normalized depths [n] and an image. |
| 543 | + """ |
| 544 | + point_sample_data: SampleData = self._t4.get("sample_data", point_sample_data_token) |
| 545 | + pc_filepath = osp.join(self._t4.data_root, point_sample_data.filename) |
| 546 | + if point_sample_data.modality == SensorModality.LIDAR: |
| 547 | + pointcloud = LidarPointCloud.from_file(pc_filepath) |
| 548 | + elif point_sample_data.modality == SensorModality.RADAR: |
| 549 | + pointcloud = RadarPointCloud.from_file(pc_filepath) |
| 550 | + else: |
| 551 | + raise ValueError(f"Expected sensor lidar/radar, but got {point_sample_data.modality}") |
| 552 | + |
| 553 | + camera_sample_data: SampleData = self._t4.get("sample_data", camera_sample_data_token) |
| 554 | + if camera_sample_data.modality != SensorModality.CAMERA: |
| 555 | + f"Expected camera, but got {camera_sample_data.modality}" |
| 556 | + |
| 557 | + img = Image.open(osp.join(self._t4.data_root, camera_sample_data.filename)) |
| 558 | + |
| 559 | + # 1. transform the pointcloud to the ego vehicle frame for the timestamp to the sweep. |
| 560 | + point_cs_record: CalibratedSensor = self._t4.get( |
| 561 | + "calibrated_sensor", point_sample_data.calibrated_sensor_token |
| 562 | + ) |
| 563 | + pointcloud.rotate(point_cs_record.rotation.rotation_matrix) |
| 564 | + pointcloud.translate(point_cs_record.translation) |
| 565 | + |
| 566 | + # 2. transform from ego to the global frame. |
| 567 | + point_ego_pose: EgoPose = self._t4.get("ego_pose", point_sample_data.ego_pose_token) |
| 568 | + pointcloud.rotate(point_ego_pose.rotation.rotation_matrix) |
| 569 | + pointcloud.translate(point_ego_pose.translation) |
| 570 | + |
| 571 | + # 3. transform from global into the ego vehicle frame for the timestamp of the image |
| 572 | + camera_ego_pose: EgoPose = self._t4.get("ego_pose", camera_sample_data.ego_pose_token) |
| 573 | + pointcloud.translate(-camera_ego_pose.translation) |
| 574 | + pointcloud.rotate(camera_ego_pose.rotation.rotation_matrix.T) |
| 575 | + |
| 576 | + # 4. transform from ego into the camera |
| 577 | + camera_cs_record: CalibratedSensor = self._t4.get( |
| 578 | + "calibrated_sensor", camera_sample_data.calibrated_sensor_token |
| 579 | + ) |
| 580 | + pointcloud.translate(-camera_cs_record.translation) |
| 581 | + pointcloud.rotate(camera_cs_record.rotation.rotation_matrix.T) |
| 582 | + |
| 583 | + depths = pointcloud.points[2, :] |
| 584 | + |
| 585 | + distortion = None if ignore_distortion else camera_cs_record.camera_distortion |
| 586 | + |
| 587 | + points_on_img = view_points( |
| 588 | + points=pointcloud.points[:3, :], |
| 589 | + intrinsic=camera_cs_record.camera_intrinsic, |
| 590 | + distortion=distortion, |
| 591 | + normalize=True, |
| 592 | + )[:2] |
| 593 | + |
| 594 | + mask = np.ones(depths.shape[0], dtype=bool) |
| 595 | + mask = np.logical_and(mask, depths > min_dist) |
| 596 | + mask = np.logical_and(mask, 1 < points_on_img[0]) |
| 597 | + mask = np.logical_and(mask, points_on_img[0] < img.size[0] - 1) |
| 598 | + mask = np.logical_and(mask, 1 < points_on_img[1]) |
| 599 | + mask = np.logical_and(mask, points_on_img[1] < img.size[1] - 1) |
| 600 | + points_on_img = points_on_img[:, mask] |
| 601 | + depths = depths[mask] |
| 602 | + |
| 603 | + return points_on_img, depths, np.array(img, dtype=np.uint8) |
| 604 | + |
522 | 605 | async def _render_annotation3ds( |
523 | 606 | self, |
524 | 607 | viewer: RerunViewer, |
|
0 commit comments