Spaces:
Sleeping
Sleeping
# -*- coding: utf-8 -*- | |
# Helpful functions Pipeline | |
# | |
# Adapted from HoverNet | |
# HoverNet Network (https://doi.org/10.1016/j.media.2019.101563) | |
# Code Snippet adapted from HoverNet implementation (https://github.com/vqdang/hover_net) | |
# | |
# @ Fabian Hörst, fabian.hoerst@uk-essen.de | |
# Institute for Artifical Intelligence in Medicine, | |
# University Medicine Essen | |
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]] | |
# due to python indexing, need to add 1 to max | |
# else accessing will be 1px in the box, not out | |
rmax += 1 | |
cmax += 1 | |
return [rmin, rmax, cmin, cmax] | |
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: # shortcut for efficiency | |
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 | |
""" | |
# * Euclidean distance as the cost matrix | |
pair_distance = scipy.spatial.distance.cdist(setA, setB, metric="euclidean") | |
# * Munkres pairing with scipy library | |
# the algorithm return (row indices, matched column indices) | |
# if there is multiple same cost in a row, index of first occurence | |
# is return, thus the unique pairing is ensured | |
indicesA, paired_indicesB = linear_sum_assignment(pair_distance) | |
# extract the paired cost and remove instances | |
# outside of designated radius | |
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) | |
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 | |
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 | |