|
| 1 | +from pathlib import Path |
| 2 | +from typing import Optional |
| 3 | + |
| 4 | +import numpy as np |
| 5 | +import open3d as o3d |
| 6 | + |
| 7 | +from silvr.utils.eval import compute_p2p_distance, get_recon_metrics, save_error_cloud |
| 8 | + |
| 9 | + |
| 10 | +class EvalCloudCropper: |
| 11 | + def __init__( |
| 12 | + self, |
| 13 | + input_cloud: np.ndarray, |
| 14 | + gt_cloud: np.ndarray, |
| 15 | + output_folder: str, |
| 16 | + input_crop_voxel_grid: Optional[o3d.geometry.VoxelGrid] = None, |
| 17 | + gt_crop_voxel_grid: Optional[o3d.geometry.VoxelGrid] = None, |
| 18 | + voxel_size: float = 0.5, |
| 19 | + ): |
| 20 | + assert input_cloud.shape[1] == 3 and gt_cloud.shape[1] == 3 # N x 3 |
| 21 | + self.input_cloud = input_cloud |
| 22 | + self.gt_cloud = gt_cloud |
| 23 | + self.output_folder = Path(output_folder) |
| 24 | + self.output_folder.mkdir(parents=True, exist_ok=True) |
| 25 | + self.voxel_size = voxel_size |
| 26 | + |
| 27 | + def compute_crop_cloud_proposal(self, max_error: float = 1.0): |
| 28 | + lidar_error_cloud = self.compute_high_error_cloud(self.input_cloud, self.gt_cloud, max_error=max_error) |
| 29 | + lidar_error_cloud.paint_uniform_color([1, 0, 0]) |
| 30 | + o3d.io.write_point_cloud(str(self.output_folder / "lidar_error_cloud.pcd"), lidar_error_cloud) |
| 31 | + gt_error_cloud = self.compute_high_error_cloud(self.gt_cloud, self.input_cloud, max_error=max_error) |
| 32 | + gt_error_cloud.paint_uniform_color([1, 0, 0]) |
| 33 | + o3d.io.write_point_cloud(str(self.output_folder / "gt_error_cloud.pcd"), gt_error_cloud) |
| 34 | + return lidar_error_cloud, gt_error_cloud |
| 35 | + |
| 36 | + def compute_high_error_cloud(self, input_cloud: np.ndarray, gt_cloud: np.ndarray, max_error: float = 1.0): |
| 37 | + input_error_map = compute_p2p_distance(input_cloud, gt_cloud) |
| 38 | + input_error_mask = input_error_map > max_error |
| 39 | + input_error_cloud = input_cloud[input_error_mask] |
| 40 | + input_error_cloud_o3d = o3d.geometry.PointCloud() |
| 41 | + input_error_cloud_o3d.points = o3d.utility.Vector3dVector(input_error_cloud) |
| 42 | + # lidar_error_1m_o3d.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0) |
| 43 | + return input_error_cloud_o3d |
| 44 | + |
| 45 | + def compute_crop_voxel_grid(self, cloud: np.ndarray, voxel_size: float = 0.5): |
| 46 | + if isinstance(cloud, np.ndarray): |
| 47 | + cloud = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(cloud)) |
| 48 | + assert isinstance(cloud, o3d.geometry.PointCloud) |
| 49 | + voxel_grid = o3d.geometry.VoxelGrid.create_from_point_cloud(cloud, voxel_size) |
| 50 | + return voxel_grid |
| 51 | + |
| 52 | + def crop_cloud_by_voxel_grid(self, cloud: np.ndarray, voxel_grid: o3d.geometry.VoxelGrid): |
| 53 | + def points_in_voxels(points, voxel_grid): |
| 54 | + queries = o3d.utility.Vector3dVector(points) |
| 55 | + return voxel_grid.check_if_included(queries) |
| 56 | + |
| 57 | + if isinstance(cloud, np.ndarray): |
| 58 | + cloud = o3d.geometry.PointCloud(o3d.utility.Vector3dVector(cloud)) |
| 59 | + assert isinstance(cloud, o3d.geometry.PointCloud) |
| 60 | + points_to_crop = points_in_voxels(cloud.points, voxel_grid) |
| 61 | + points_to_keep = [not x for x in points_to_crop] |
| 62 | + remaining_cloud = cloud.select_by_index(np.where(points_to_keep)[0]) |
| 63 | + return remaining_cloud |
| 64 | + |
| 65 | + |
| 66 | +def convert_voxel_grid_to_point_cloud(voxel_grid): |
| 67 | + points = [] |
| 68 | + for voxel in voxel_grid.get_voxels(): |
| 69 | + voxel_centre = voxel_grid.get_voxel_center_coordinate(voxel.grid_index) |
| 70 | + points.append(voxel_centre) |
| 71 | + point_cloud = o3d.geometry.PointCloud() |
| 72 | + point_cloud.points = o3d.utility.Vector3dVector(points) |
| 73 | + return point_cloud |
| 74 | + |
| 75 | + |
| 76 | +if __name__ == "__main__": |
| 77 | + folder_path = Path("/home/yifu/workspace/T-RO_2025/Observatory_quater") |
| 78 | + lidar_map_path = folder_path / "ROQ_lidar_map_cleaned.pcd" |
| 79 | + leica_map_path = folder_path / "roq_5cm.pcd" |
| 80 | + |
| 81 | + T_leica_lidar = np.loadtxt(str(folder_path / "T_RTC_vilens.txt")) |
| 82 | + |
| 83 | + lidar_map = o3d.io.read_point_cloud(str(lidar_map_path)) |
| 84 | + # lidar_map.remove_statistical_outlier(nb_neighbors=20, std_ratio=2.0) |
| 85 | + lidar_map.transform(T_leica_lidar) |
| 86 | + lidar_map = np.array(lidar_map.points) |
| 87 | + leica_map = o3d.io.read_point_cloud(str(leica_map_path)) |
| 88 | + leica_map = np.array(leica_map.points) |
| 89 | + |
| 90 | + output_folder_path = folder_path / "test" |
| 91 | + eval_cloud_cropper = EvalCloudCropper(lidar_map, leica_map, output_folder_path) |
| 92 | + create_proposed_error_cloud = True |
| 93 | + crop_cloud = True |
| 94 | + evaluation = True |
| 95 | + |
| 96 | + lidar_error_cloud_path = output_folder_path / "lidar_error_cloud.pcd" |
| 97 | + gt_error_cloud_path = output_folder_path / "gt_error_cloud.pcd" |
| 98 | + cropped_lidar_cloud_save_path = output_folder_path / "lidar_cropped.pcd" |
| 99 | + cropped_gt_cloud_save_path = output_folder_path / "gt_cropped.pcd" |
| 100 | + |
| 101 | + if create_proposed_error_cloud: |
| 102 | + lidar_error_cloud, gt_error_cloud = eval_cloud_cropper.compute_crop_cloud_proposal(max_error=0.2) |
| 103 | + o3d.io.write_point_cloud(str(lidar_error_cloud_path), lidar_error_cloud) |
| 104 | + o3d.io.write_point_cloud(str(gt_error_cloud_path), gt_error_cloud) |
| 105 | + |
| 106 | + # load error cloud and convert to voxel grid, then crop the map |
| 107 | + if crop_cloud: |
| 108 | + lidar_error_cloud = o3d.io.read_point_cloud(str(lidar_error_cloud_path)) |
| 109 | + lidar_crop_voxel_grid = eval_cloud_cropper.compute_crop_voxel_grid(lidar_error_cloud) |
| 110 | + lidar_cropped_cloud = eval_cloud_cropper.crop_cloud_by_voxel_grid(lidar_map, lidar_crop_voxel_grid) |
| 111 | + o3d.io.write_point_cloud(str(cropped_lidar_cloud_save_path), lidar_cropped_cloud) |
| 112 | + |
| 113 | + gt_error_cloud = o3d.io.read_point_cloud(str(gt_error_cloud_path)) |
| 114 | + gt_crop_voxel_grid = eval_cloud_cropper.compute_crop_voxel_grid(gt_error_cloud) |
| 115 | + gt_cropped_cloud = eval_cloud_cropper.crop_cloud_by_voxel_grid(leica_map, gt_crop_voxel_grid) |
| 116 | + o3d.io.write_point_cloud(str(cropped_gt_cloud_save_path), gt_cropped_cloud) |
| 117 | + |
| 118 | + # apply cropping |
| 119 | + if evaluation: |
| 120 | + lidar_cropped_cloud_np = np.array(o3d.io.read_point_cloud(str(cropped_lidar_cloud_save_path)).points) |
| 121 | + gt_cropped_cloud_np = np.array(o3d.io.read_point_cloud(str(cropped_gt_cloud_save_path)).points) |
| 122 | + print(get_recon_metrics(lidar_cropped_cloud_np, gt_cropped_cloud_np)) |
| 123 | + lidar_error_cmap_cloud_save_path = output_folder_path / "lidar_error_cmap_cloud.ply" |
| 124 | + save_error_cloud(lidar_cropped_cloud_np, gt_cropped_cloud_np, lidar_error_cmap_cloud_save_path) |
| 125 | + gt_error_cmap_cloud_save_path = output_folder_path / "gt_error_cmap_cloud.ply" |
| 126 | + save_error_cloud(gt_cropped_cloud_np, lidar_cropped_cloud_np, gt_error_cmap_cloud_save_path) |
0 commit comments