| import torch |
| import src |
| from src.dependencies.FRNN import frnn |
| from torch_scatter import scatter |
| from torch_geometric.utils import coalesce |
| from src.utils.scatter import scatter_nearest_neighbor |
|
|
|
|
| __all__ = [ |
| 'knn_1', 'knn_1_graph', 'knn_2', 'inliers_split', 'outliers_split', |
| 'inliers_outliers_splits', 'cluster_radius_nn_graph'] |
|
|
|
|
| def knn_1( |
| xyz, |
| k, |
| r_max=1, |
| batch=None, |
| oversample=False, |
| self_is_neighbor=False, |
| verbose=False): |
| """Search k-NN for a 3D point cloud xyz. This search differs |
| from `knn_2` in that it operates on a single cloud input (search and |
| query are the same) and it allows oversampling the neighbors when |
| less than `k` neighbors are found within `r_max`. Optionally, |
| passing `batch` will ensure the neighbor search does not mix up |
| batch items. |
| """ |
| assert isinstance(xyz, torch.Tensor) |
| assert k >= 1 |
| assert xyz.dim() == 2 |
| assert batch is None or batch.shape[0] == xyz.shape[0] |
|
|
| |
| |
| |
| batch_offset = 0 |
| if batch is not None: |
| z_offset = xyz[:, 2].max() - xyz[:, 2].min() + r_max + 1 |
| batch_offset = torch.zeros_like(xyz) |
| batch_offset[:, 2] = batch * z_offset |
|
|
| |
| device = xyz.device |
| xyz_query = (xyz + batch_offset).view(1, -1, 3) |
| xyz_search = (xyz + batch_offset).view(1, -1, 3) |
| if not xyz.is_cuda: |
| xyz_query = xyz_query.cuda() |
| xyz_search = xyz_search.cuda() |
|
|
| |
| k_search = k if self_is_neighbor else k + 1 |
| distances, neighbors, _, _ = frnn.frnn_grid_points( |
| xyz_query, xyz_search, K=k_search, r=r_max) |
|
|
| |
| neighbors = neighbors[0] if self_is_neighbor else neighbors[0][:, 1:] |
| distances = distances[0] if self_is_neighbor else distances[0][:, 1:] |
|
|
| |
| if oversample: |
| neighbors, distances = oversample_partial_neighborhoods( |
| neighbors, distances, k) |
|
|
| |
| if neighbors.device != device: |
| neighbors = neighbors.to(device) |
| distances = distances.to(device) |
|
|
| if not verbose and not src.is_debug_enabled(): |
| return neighbors, distances |
|
|
| |
| num_nodes = neighbors.shape[0] |
| n_missing = (neighbors < 0).sum(dim=1) |
| n_partial = (n_missing > 0).sum() |
| n_empty = (n_missing == k).sum() |
| if n_partial == 0: |
| return neighbors, distances |
|
|
| print( |
| f"\nWarning: {n_partial}/{num_nodes} points have partial " |
| f"neighborhoods and {n_empty}/{num_nodes} have empty " |
| f"neighborhoods (missing neighbors are indicated by -1 indices).") |
|
|
| return neighbors, distances |
|
|
|
|
| def knn_1_graph( |
| xyz, |
| k, |
| r_max=1, |
| batch=None, |
| oversample=False, |
| self_is_neighbor=False, |
| verbose=False, |
| trim=True): |
| """Search k-NN for a 3D point cloud xyz and convert the output into |
| torch_geometric's `edge_index`, `edge_attr` format. This search |
| differs from `knn_2` in that it operates on a single cloud input |
| (search and query are the same) and it allows oversampling the |
| neighbors when less than `k` neighbors are found within `r_max`. |
| Optionally, passing `batch` will ensure the neighbor search does not |
| mix up batch items. |
| |
| Importantly, the output graph will be coalesced: duplicate edges |
| will be removed. Besides, if `trim=True`, the graph will be further |
| reduced using `to_trimmed()` (see function documentation for more |
| information). |
| """ |
| |
| neighbors, distances = knn_1( |
| xyz, |
| k, |
| r_max=r_max, |
| batch=batch, |
| oversample=oversample, |
| self_is_neighbor=self_is_neighbor, |
| verbose=verbose) |
|
|
| |
| num_points = xyz.shape[0] |
| source = torch.arange(num_points, device=xyz.device).repeat_interleave(k) |
| target = neighbors.flatten() |
| edge_index = torch.vstack((source, target)) |
| distances = distances.flatten() |
|
|
| |
| missing_point_edge = edge_index[1] == -1 |
| edge_index = edge_index[:, ~missing_point_edge] |
| distances = distances[~missing_point_edge] |
|
|
| |
| |
| |
| |
| if trim: |
| from src.utils import to_trimmed |
| edge_index, distances = to_trimmed( |
| edge_index, edge_attr=distances, reduce='min') |
| |
| else: |
| edge_index, distances = coalesce( |
| edge_index, edge_attr=distances, reduce='min') |
|
|
| return edge_index, distances |
|
|
|
|
| def knn_2( |
| x_search, |
| x_query, |
| k, |
| r_max=1, |
| batch_search=None, |
| batch_query=None): |
| """Search k-NN of x_query inside x_search, within radius `r_max`. |
| Optionally, passing `batch_search` and `batch_query` will ensure the |
| neighbor search does not mix up batch items. |
| """ |
| assert isinstance(x_search, torch.Tensor) |
| assert isinstance(x_query, torch.Tensor) |
| assert k >= 1 |
| assert x_search.dim() == 2 |
| assert x_query.dim() == 2 |
| assert x_query.shape[1] == x_search.shape[1] |
| assert bool(batch_search) == bool(batch_query) |
| assert batch_search is None or batch_search.shape[0] == x_search.shape[0] |
| assert batch_query is None or batch_query.shape[0] == x_query.shape[0] |
|
|
| k = torch.tensor([k]) |
| r_max = torch.tensor([r_max]) |
|
|
| |
| |
| |
| batch_search_offset = 0 |
| batch_query_offset = 0 |
| if batch_search is not None: |
| hi = max(x_search[:, 2].max(), x_query[:, 2].max()) |
| lo = min(x_search[:, 2].min(), x_query[:, 2].min()) |
| z_offset = hi - lo + r_max + 1 |
| batch_search_offset = torch.zeros_like(x_search) |
| batch_search_offset[:, 2] = batch_search * z_offset |
| batch_query_offset = torch.zeros_like(x_query) |
| batch_query_offset[:, 2] = batch_query * z_offset |
|
|
| |
| device = x_search.device |
| xyz_query = (x_query + batch_query_offset).view(1, -1, 3).cuda() |
| xyz_search = (x_search + batch_search_offset).view(1, -1, 3).cuda() |
|
|
| |
| distances, neighbors, _, _ = frnn.frnn_grid_points( |
| xyz_query, xyz_search, K=k, r=r_max) |
|
|
| |
| neighbors = neighbors[0].to(device) |
| distances = distances[0].to(device) |
| if k == 1: |
| neighbors = neighbors[:, 0] |
| distances = distances[:, 0] |
|
|
| return neighbors, distances |
|
|
|
|
| def inliers_split( |
| xyz_query, xyz_search, k_min, r_max=1, recursive=False, q_in_s=False): |
| """Optionally recursive inlier search. The `xyz_query` and |
| `xyz_search`. Search for points with less than `k_min` neighbors |
| within a radius of `r_max`. |
| |
| Since removing outliers may cause some points to become outliers |
| themselves, this problem can be tackled with the `recursive` option. |
| Note that this recursive search holds no guarantee of reasonable |
| convergence as one could design a point cloud for given `k_min` and |
| `r_max` whose points would all recursively end up as outliers. |
| """ |
| return inliers_outliers_splits( |
| xyz_query, xyz_search, k_min, r_max=r_max, recursive=recursive, |
| q_in_s=q_in_s)[0] |
|
|
|
|
| def outliers_split( |
| xyz_query, xyz_search, k_min, r_max=1, recursive=False, q_in_s=False): |
| """Optionally recursive outlier search. The `xyz_query` and |
| `xyz_search`. Search for points with less than `k_min` neighbors |
| within a radius of `r_max`. |
| |
| Since removing outliers may cause some points to become outliers |
| themselves, this problem can be tackled with the `recursive` option. |
| Note that this recursive search holds no guarantee of reasonable |
| convergence as one could design a point cloud for given `k_min` and |
| `r_max` whose points would all recursively end up as outliers. |
| """ |
| return inliers_outliers_splits( |
| xyz_query, xyz_search, k_min, r_max=r_max, recursive=recursive, |
| q_in_s=q_in_s)[1] |
|
|
|
|
| def inliers_outliers_splits( |
| xyz_query, xyz_search, k_min, r_max=1, recursive=False, q_in_s=False): |
| """Optionally recursive outlier search. The `xyz_query` and |
| `xyz_search`. Search for points with less than `k_min` neighbors |
| within a radius of `r_max`. |
| |
| Since removing outliers may cause some points to become outliers |
| themselves, this problem can be tackled with the `recursive` option. |
| Note that this recursive search holds no guarantee of reasonable |
| convergence as one could design a point cloud for given `k_min` and |
| `r_max` whose points would all recursively end up as outliers. |
| """ |
| |
| device = xyz_query.device |
| xyz_query = xyz_query.view(1, -1, 3).cuda() |
| xyz_search = xyz_search.view(1, -1, 3).cuda() |
|
|
| |
| neighbors = frnn.frnn_grid_points( |
| xyz_query, xyz_search, K=k_min + q_in_s, r=r_max)[1] |
|
|
| |
| |
| if q_in_s: |
| neighbors = neighbors[0][:, 1:] |
|
|
| |
| |
| |
| n_found_nn = (neighbors != -1).sum(dim=1) |
|
|
| |
| |
| mask_outliers = n_found_nn < k_min |
| idx_outliers = torch.where(mask_outliers)[0] |
| idx_inliers = torch.where(~mask_outliers)[0] |
|
|
| |
| if not recursive: |
| return idx_outliers.to(device), idx_inliers.to(device) |
|
|
| |
| |
| idx_potential = torch.where( |
| torch.isin(neighbors[idx_inliers], idx_outliers).any(dim=1))[0] |
|
|
| |
| if idx_potential.shape[0] == 0: |
| return idx_outliers.to(device), idx_inliers.to(device) |
|
|
| |
| xyz_query_sub = xyz_query[0, idx_inliers[idx_potential]] |
| xyz_search_sub = xyz_search[0, idx_inliers] |
| idx_outliers_sub, idx_inliers_sub = inliers_outliers_splits( |
| xyz_query_sub, xyz_search_sub, k_min, r_max=r_max, recursive=True, |
| q_in_s=True) |
|
|
| |
| mask_outliers[idx_inliers[idx_potential][idx_outliers_sub]] = True |
| idx_outliers = torch.where(mask_outliers)[0] |
| idx_inliers = torch.where(~mask_outliers)[0] |
|
|
| return idx_outliers.to(device), idx_inliers.to(device) |
|
|
|
|
| def oversample_partial_neighborhoods(neighbors, distances, k): |
| """Oversample partial neighborhoods with less than k points. Missing |
| neighbors are indicated by the "-1" index. |
| |
| Remarks |
| - Neighbors and distances are assumed to be sorted in order of |
| increasing distance |
| - All neighbors are assumed to have at least one valid neighbor. |
| See `search_outliers` to remove points with not enough neighbors |
| """ |
| |
| assert neighbors.dim() == distances.dim() == 2 |
| device = neighbors.device |
|
|
| |
| |
| |
| n_found_nn = (neighbors != -1).sum(dim=1) |
|
|
| |
| |
| |
| idx_partial = torch.where(n_found_nn < k)[0] |
| neighbors_partial = neighbors[idx_partial] |
| distances_partial = distances[idx_partial] |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| n_valid = n_found_nn[idx_partial].repeat_interleave( |
| k - n_found_nn[idx_partial]) |
|
|
| |
| idx_x_sampling = torch.arange( |
| neighbors_partial.shape[0], device=device).repeat_interleave( |
| k - n_found_nn[idx_partial]) |
|
|
| |
| |
| |
| |
| idx_y_sampling = (n_valid * torch.rand( |
| n_valid.shape[0], device=device) * 0.9999).floor().long() |
|
|
| |
| idx_missing = torch.where(neighbors_partial == -1) |
| neighbors_partial[idx_missing] = neighbors_partial[ |
| idx_x_sampling, idx_y_sampling] |
| distances_partial[idx_missing] = distances_partial[ |
| idx_x_sampling, idx_y_sampling] |
|
|
| |
| neighbors[idx_partial] = neighbors_partial |
| distances[idx_partial] = distances_partial |
|
|
| return neighbors, distances |
|
|
|
|
| def cluster_radius_nn_graph( |
| x_points, |
| idx, |
| k_max=100, |
| gap=0, |
| batch=None, |
| trim=True, |
| cycles=3, |
| chunk_size=100000): |
| """Compute the radius neighbors of clusters. Two clusters are |
| considered neighbors if 2 of their points are distant of `gap` of |
| less. |
| |
| The underlying strategy searches the cluster centroids within a |
| certain radius, based each cluster's radius and the chosen `gap`. |
| This approach is a proxy to avoid the actual computation of all |
| pointwise distances. |
| |
| :param x_points: |
| :param idx: |
| :param k_max: |
| :param gap: |
| :param batch: |
| Passing `batch` will ensure the neighbor search does |
| not mix up batch items. This batch tensor is a tensor of size |
| `num_clusters=idx.max() + 1` indicating which batch item each |
| cluster belongs to |
| :param trim bool |
| If True, the output `edge_index` will be trimmed using |
| `to_trimmed`, to save compute and memory |
| :param cycles int |
| Number of iterations. Starting from a point X in set A, one |
| cycle accounts for searching the nearest neighbor, in A, of the |
| nearest neighbor of X in set B |
| :param chunk_size: int, float |
| Allows mitigating memory use when computing the neighbors. If |
| `chunk_size > 1`, `edge_index` will be processed into chunks of |
| `chunk_size`. If `0 < chunk_size < 1`, then `edge_index` will be |
| divided into parts of `edge_index.shape[1] * chunk_size` or less |
| :return: |
| """ |
| assert batch is None or batch.shape[0] == idx.max() + 1 |
|
|
| device = x_points.device |
|
|
| |
| |
| |
| bbox_low = scatter(x_points, idx, dim=0, reduce='min') |
| bbox_high = scatter(x_points, idx, dim=0, reduce='max') |
| diam = (bbox_high - bbox_low).max(dim=1).values |
| center = (bbox_high + bbox_low) / 2 |
|
|
| |
| |
| |
| |
| |
| |
| |
| r_search = float(diam.max() + gap) |
| neighbors, distances = knn_1(center, k_max, r_max=r_search, batch=batch) |
|
|
| |
| num_clusters = idx.max() + 1 |
| source = torch.arange(num_clusters, device=device).repeat_interleave(k_max) |
| target = neighbors.flatten() |
| edge_index = torch.vstack((source, target)) |
| distances = distances.flatten() |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| r_segment = diam / 2 |
| r_max_edge = r_segment[edge_index].sum(dim=0) + 1.732 * gap |
| in_gap_range = distances <= r_max_edge |
| edge_index = edge_index[:, in_gap_range] |
| distances = distances[in_gap_range] |
|
|
| |
| missing_point_edge = edge_index[1] == -1 |
| edge_index = edge_index[:, ~missing_point_edge] |
| distances = distances[~missing_point_edge] |
|
|
| |
| |
| |
| |
| if trim: |
| from src.utils import to_trimmed |
| edge_index, distances = to_trimmed( |
| edge_index, edge_attr=distances, reduce='min') |
| |
| else: |
| edge_index, distances = coalesce( |
| edge_index, edge_attr=distances, reduce='min') |
|
|
| |
| |
| |
| |
| |
| |
| |
| anchors = scatter_nearest_neighbor( |
| x_points, idx, edge_index, cycles=cycles, chunk_size=chunk_size)[1] |
| d_nn = (x_points[anchors[0]] - x_points[anchors[1]]).norm(dim=1) |
|
|
| |
| in_gap_range = d_nn <= gap |
| edge_index = edge_index[:, in_gap_range] |
| distances = d_nn[in_gap_range] |
|
|
| return edge_index, distances |
|
|