| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import math |
| from typing import Tuple |
|
|
| import numpy as np |
| import scipy |
| from numba import njit, prange |
| from scipy import ndimage |
| from scipy.optimize import linear_sum_assignment |
| from skimage.draw import polygon |
|
|
|
|
| def get_bounding_box(img): |
| """Get bounding box coordinate information.""" |
| rows = np.any(img, axis=1) |
| cols = np.any(img, axis=0) |
| rmin, rmax = np.where(rows)[0][[0, -1]] |
| cmin, cmax = np.where(cols)[0][[0, -1]] |
| |
| |
| rmax += 1 |
| cmax += 1 |
| return [rmin, rmax, cmin, cmax] |
|
|
|
|
| @njit |
| def cropping_center(x, crop_shape, batch=False): |
| """Crop an input image at the centre. |
| |
| Args: |
| x: input array |
| crop_shape: dimensions of cropped array |
| |
| Returns: |
| x: cropped array |
| |
| """ |
| orig_shape = x.shape |
| if not batch: |
| h0 = int((orig_shape[0] - crop_shape[0]) * 0.5) |
| w0 = int((orig_shape[1] - crop_shape[1]) * 0.5) |
| x = x[h0 : h0 + crop_shape[0], w0 : w0 + crop_shape[1], ...] |
| else: |
| h0 = int((orig_shape[1] - crop_shape[0]) * 0.5) |
| w0 = int((orig_shape[2] - crop_shape[1]) * 0.5) |
| x = x[:, h0 : h0 + crop_shape[0], w0 : w0 + crop_shape[1], ...] |
| return x |
|
|
|
|
| def remove_small_objects(pred, min_size=64, connectivity=1): |
| """Remove connected components smaller than the specified size. |
| |
| This function is taken from skimage.morphology.remove_small_objects, but the warning |
| is removed when a single label is provided. |
| |
| Args: |
| pred: input labelled array |
| min_size: minimum size of instance in output array |
| connectivity: The connectivity defining the neighborhood of a pixel. |
| |
| Returns: |
| out: output array with instances removed under min_size |
| |
| """ |
| out = pred |
|
|
| if min_size == 0: |
| return out |
|
|
| if out.dtype == bool: |
| selem = ndimage.generate_binary_structure(pred.ndim, connectivity) |
| ccs = np.zeros_like(pred, dtype=np.int32) |
| ndimage.label(pred, selem, output=ccs) |
| else: |
| ccs = out |
|
|
| try: |
| component_sizes = np.bincount(ccs.ravel()) |
| except ValueError: |
| raise ValueError( |
| "Negative value labels are not supported. Try " |
| "relabeling the input with `scipy.ndimage.label` or " |
| "`skimage.morphology.label`." |
| ) |
|
|
| too_small = component_sizes < min_size |
| too_small_mask = too_small[ccs] |
| out[too_small_mask] = 0 |
|
|
| return out |
|
|
|
|
| def pair_coordinates( |
| setA: np.ndarray, setB: np.ndarray, radius: float |
| ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: |
| """Use the Munkres or Kuhn-Munkres algorithm to find the most optimal |
| unique pairing (largest possible match) when pairing points in set B |
| against points in set A, using distance as cost function. |
| |
| Args: |
| setA (np.ndarray): np.array (float32) of size Nx2 contains the of XY coordinate |
| of N different points |
| setB (np.ndarray): np.array (float32) of size Nx2 contains the of XY coordinate |
| of N different points |
| radius (float): valid area around a point in setA to consider |
| a given coordinate in setB a candidate for match |
| |
| Returns: |
| Tuple[np.ndarray, np.ndarray, np.ndarray]: |
| pairing: pairing is an array of indices |
| where point at index pairing[0] in set A paired with point |
| in set B at index pairing[1] |
| unparedA: remaining point in set A unpaired |
| unparedB: remaining point in set B unpaired |
| """ |
| |
| pair_distance = scipy.spatial.distance.cdist(setA, setB, metric="euclidean") |
|
|
| |
| |
| |
| |
| indicesA, paired_indicesB = linear_sum_assignment(pair_distance) |
|
|
| |
| |
| pair_cost = pair_distance[indicesA, paired_indicesB] |
|
|
| pairedA = indicesA[pair_cost <= radius] |
| pairedB = paired_indicesB[pair_cost <= radius] |
|
|
| pairing = np.concatenate([pairedA[:, None], pairedB[:, None]], axis=-1) |
| unpairedA = np.delete(np.arange(setA.shape[0]), pairedA) |
| unpairedB = np.delete(np.arange(setB.shape[0]), pairedB) |
|
|
| return pairing, unpairedA, unpairedB |
|
|
|
|
| def fix_duplicates(inst_map: np.ndarray) -> np.ndarray: |
| """Re-label duplicated instances in an instance labelled mask. |
| |
| Parameters |
| ---------- |
| inst_map : np.ndarray |
| Instance labelled mask. Shape (H, W). |
| |
| Returns |
| ------- |
| np.ndarray: |
| The instance labelled mask without duplicated indices. |
| Shape (H, W). |
| """ |
| current_max_id = np.amax(inst_map) |
| inst_list = list(np.unique(inst_map)) |
| if 0 in inst_list: |
| inst_list.remove(0) |
|
|
| for inst_id in inst_list: |
| inst = np.array(inst_map == inst_id, np.uint8) |
| remapped_ids = ndimage.label(inst)[0] |
| remapped_ids[remapped_ids > 1] += current_max_id |
| inst_map[remapped_ids > 1] = remapped_ids[remapped_ids > 1] |
| current_max_id = np.amax(inst_map) |
|
|
| return inst_map |
|
|
|
|
| def polygons_to_label_coord( |
| coord: np.ndarray, shape: Tuple[int, int], labels: np.ndarray = None |
| ) -> np.ndarray: |
| """Render polygons to image given a shape. |
| |
| Parameters |
| ---------- |
| coord.shape : np.ndarray |
| Shape: (n_polys, n_rays) |
| shape : Tuple[int, int] |
| Shape of the output mask. |
| labels : np.ndarray, optional |
| Sorted indices of the centroids. |
| |
| Returns |
| ------- |
| np.ndarray: |
| Instance labelled mask. Shape: (H, W). |
| """ |
| coord = np.asarray(coord) |
| if labels is None: |
| labels = np.arange(len(coord)) |
|
|
| assert coord.ndim == 3 and coord.shape[1] == 2 and len(coord) == len(labels) |
|
|
| lbl = np.zeros(shape, np.int32) |
|
|
| for i, c in zip(labels, coord): |
| rr, cc = polygon(*c, shape) |
| lbl[rr, cc] = i + 1 |
|
|
| return lbl |
|
|
|
|
| def ray_angles(n_rays: int = 32): |
| """Get linearly spaced angles for rays.""" |
| return np.linspace(0, 2 * np.pi, n_rays, endpoint=False) |
|
|
|
|
| def dist_to_coord( |
| dist: np.ndarray, points: np.ndarray, scale_dist: Tuple[int, int] = (1, 1) |
| ) -> np.ndarray: |
| """Convert list of distances and centroids from polar to cartesian coordinates. |
| |
| Parameters |
| ---------- |
| dist : np.ndarray |
| The centerpoint pixels of the radial distance map. Shape (n_polys, n_rays). |
| points : np.ndarray |
| The centroids of the instances. Shape: (n_polys, 2). |
| scale_dist : Tuple[int, int], default=(1, 1) |
| Scaling factor. |
| |
| Returns |
| ------- |
| np.ndarray: |
| Cartesian cooridnates of the polygons. Shape (n_polys, 2, n_rays). |
| """ |
| dist = np.asarray(dist) |
| points = np.asarray(points) |
| assert ( |
| dist.ndim == 2 |
| and points.ndim == 2 |
| and len(dist) == len(points) |
| and points.shape[1] == 2 |
| and len(scale_dist) == 2 |
| ) |
| n_rays = dist.shape[1] |
| phis = ray_angles(n_rays) |
| coord = (dist[:, np.newaxis] * np.array([np.sin(phis), np.cos(phis)])).astype( |
| np.float32 |
| ) |
| coord *= np.asarray(scale_dist).reshape(1, 2, 1) |
| coord += points[..., np.newaxis] |
| return coord |
|
|
|
|
| def polygons_to_label( |
| dist: np.ndarray, |
| points: np.ndarray, |
| shape: Tuple[int, int], |
| prob: np.ndarray = None, |
| thresh: float = -np.inf, |
| scale_dist: Tuple[int, int] = (1, 1), |
| ) -> np.ndarray: |
| """Convert distances and center points to instance labelled mask. |
| |
| Parameters |
| ---------- |
| dist : np.ndarray |
| The centerpoint pixels of the radial distance map. Shape (n_polys, n_rays). |
| points : np.ndarray |
| The centroids of the instances. Shape: (n_polys, 2). |
| shape : Tuple[int, int]: |
| Shape of the output mask. |
| prob : np.ndarray, optional |
| The centerpoint pixels of the regressed distance transform. |
| Shape: (n_polys, n_rays). |
| thresh : float, default=-np.inf |
| Threshold for the regressed distance transform. |
| scale_dist : Tuple[int, int], default=(1, 1) |
| Scaling factor. |
| |
| Returns |
| ------- |
| np.ndarray: |
| Instance labelled mask. Shape (H, W). |
| """ |
| dist = np.asarray(dist) |
| points = np.asarray(points) |
| prob = np.inf * np.ones(len(points)) if prob is None else np.asarray(prob) |
|
|
| assert dist.ndim == 2 and points.ndim == 2 and len(dist) == len(points) |
| assert len(points) == len(prob) and points.shape[1] == 2 and prob.ndim == 1 |
|
|
| ind = prob > thresh |
| points = points[ind] |
| dist = dist[ind] |
| prob = prob[ind] |
|
|
| ind = np.argsort(prob, kind="stable") |
| points = points[ind] |
| dist = dist[ind] |
|
|
| coord = dist_to_coord(dist, points, scale_dist=scale_dist) |
|
|
| return polygons_to_label_coord(coord, shape=shape, labels=ind) |
|
|
|
|
| @njit(cache=True, fastmath=True) |
| def intersection(boxA: np.ndarray, boxB: np.ndarray): |
| """Compute area of intersection of two boxes. |
| |
| Parameters |
| ---------- |
| boxA : np.ndarray |
| First boxes |
| boxB : np.ndarray |
| Second box |
| |
| Returns |
| ------- |
| float64: |
| Area of intersection |
| """ |
| xA = max(boxA[..., 0], boxB[..., 0]) |
| xB = min(boxA[..., 2], boxB[..., 2]) |
| dx = xB - xA |
| if dx <= 0: |
| return 0.0 |
|
|
| yA = max(boxA[..., 1], boxB[..., 1]) |
| yB = min(boxA[..., 3], boxB[..., 3]) |
| dy = yB - yA |
| if dy <= 0.0: |
| return 0.0 |
|
|
| return dx * dy |
|
|
|
|
| @njit(parallel=True) |
| def get_bboxes( |
| dist: np.ndarray, points: np.ndarray |
| ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, int]: |
| """Get bounding boxes from the non-zero pixels of the radial distance maps. |
| |
| This is basically a translation from the stardist repo cpp code to python |
| |
| NOTE: jit compiled and parallelized with numba. |
| |
| Parameters |
| ---------- |
| dist : np.ndarray |
| The non-zero values of the radial distance maps. Shape: (n_nonzero, n_rays). |
| points : np.ndarray |
| The yx-coordinates of the non-zero points. Shape (n_nonzero, 2). |
| |
| Returns |
| ------- |
| Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, int]: |
| Returns the x0, y0, x1, y1 bbox coordinates, bbox areas and the maximum |
| radial distance in the image. |
| """ |
| n_polys = dist.shape[0] |
| n_rays = dist.shape[1] |
|
|
| bbox_x1 = np.zeros(n_polys) |
| bbox_x2 = np.zeros(n_polys) |
| bbox_y1 = np.zeros(n_polys) |
| bbox_y2 = np.zeros(n_polys) |
|
|
| areas = np.zeros(n_polys) |
| angle_pi = 2 * math.pi / n_rays |
| max_dist = 0 |
|
|
| for i in prange(n_polys): |
| max_radius_outer = 0 |
| py = points[i, 0] |
| px = points[i, 1] |
|
|
| for k in range(n_rays): |
| d = dist[i, k] |
| y = py + d * np.sin(angle_pi * k) |
| x = px + d * np.cos(angle_pi * k) |
|
|
| if k == 0: |
| bbox_x1[i] = x |
| bbox_x2[i] = x |
| bbox_y1[i] = y |
| bbox_y2[i] = y |
| else: |
| bbox_x1[i] = min(x, bbox_x1[i]) |
| bbox_x2[i] = max(x, bbox_x2[i]) |
| bbox_y1[i] = min(y, bbox_y1[i]) |
| bbox_y2[i] = max(y, bbox_y2[i]) |
|
|
| max_radius_outer = max(d, max_radius_outer) |
|
|
| areas[i] = (bbox_x2[i] - bbox_x1[i]) * (bbox_y2[i] - bbox_y1[i]) |
| max_dist = max(max_dist, max_radius_outer) |
|
|
| return bbox_x1, bbox_y1, bbox_x2, bbox_y2, areas, max_dist |
|
|