def _compute_background_amplitude(image_spot): """Compute amplitude of a spot and background minimum value. Parameters ---------- image_spot : np.ndarray, np.uint A 3-d image with detected spot and shape (z, y, x). Returns ------- psf_amplitude : float or int Amplitude of the spot. psf_background : float or int Background minimum value of the voxel. """ # check parameters stack.check_array(image_spot, ndim=[2, 3], dtype=[np.uint8, np.uint16, np.float32, np.float64], allow_nan=False) # compute values image_min, image_max = image_spot.min(), image_spot.max() psf_amplitude = image_max - image_min psf_background = image_min return psf_amplitude, psf_background
def from_surface_to_boundaries(binary_surface): """Convert the binary surface to binary boundaries. Parameters ---------- binary_surface : np.ndarray, np.uint or np.int or bool Binary image with shape (y, x). Returns ------- binary_boundaries : np.ndarray, np.uint or np.int or bool Binary image with shape (y, x). """ # check parameters stack.check_array(binary_surface, ndim=2, dtype=[np.uint8, np.uint16, np.int64, bool]) original_dtype = binary_surface.dtype # pad the binary surface in case object if on the edge binary_surface_ = np.pad(binary_surface, [(1, 1)], mode="constant") # compute distance map of the surface binary mask distance_map = ndi.distance_transform_edt(binary_surface_) # get binary boundaries binary_boundaries_ = (distance_map < 2) & (distance_map > 0) binary_boundaries_ = binary_boundaries_.astype(original_dtype) # remove pad binary_boundaries = binary_boundaries_[1:-1, 1:-1] return binary_boundaries
def build_cyt_binary_mask(image_projected, threshold=None): """Compute a binary mask of the cytoplasm. Parameters ---------- image_projected : np.ndarray, np.uint A 2-d projection of the cytoplasm with shape (y, x). threshold : int Intensity pixel threshold to compute the binary mask. If None, an Otsu threshold is computed. Returns ------- mask : np.ndarray, bool Binary mask of the cytoplasm with shape (y, x). """ # check parameters stack.check_array(image_projected, ndim=2, dtype=[np.uint8, np.uint16]) stack.check_parameter(threshold=(int, type(None))) # get a threshold if threshold is None: threshold = threshold_otsu(image_projected) # compute a binary mask mask = (image_projected > threshold) mask = remove_small_objects(mask, 3000) mask = remove_small_holes(mask, 2000) return mask
def log_cc(image, sigma, threshold): """Find connected regions above a fixed threshold on a LoG filtered image. Parameters ---------- image : np.ndarray Image with shape (z, y, x) or (y, x). sigma : float or Tuple(float) Sigma used for the gaussian filter (one for each dimension). If it's a float, the same sigma is applied to every dimensions. threshold : float or int A threshold to detect peaks. Considered as a relative threshold if float. Returns ------- cc : np.ndarray, np.int64 Image labelled with shape (z, y, x) or (y, x). """ # check parameters stack.check_array(image, ndim=[2, 3], dtype=[np.uint8, np.uint16, np.float32, np.float64]) stack.check_parameter(sigma=(float, int, tuple), threshold=(float, int)) # cast image in np.float and apply LoG filter image_filtered = stack.log_filter(image, sigma, keep_dtype=True) # find connected components cc = get_cc(image_filtered, threshold) # TODO return coordinate of the centroid return cc
def simulate_fitted_gaussian_3d(f, grid, popt, original_shape=None): """Use the optimized parameter to simulate a gaussian signal. Parameters ---------- f : func A 3-d gaussian function with some parameters fixed. grid : np.ndarray, np.float Grid data to compute the gaussian function for different voxel within a volume V. In nanometer, with shape (3, V_z * V_y * V_x). popt : np.ndarray Fitted parameters. original_shape : Tuple Shape of the spot image to reshape the simulation. Returns ------- values : np.ndarray, np.float Value of each voxel within the volume V according to the 3-d gaussian parameters. Shape (V_z, V_y, V_x,) or (V_z * V_y * V_x,). """ # check parameters stack.check_array(grid, ndim=2, dtype=np.float32, allow_nan=False) stack.check_parameter(popt=np.ndarray, original_shape=(tuple, type(None))) # compute gaussian values values = f(grid, *popt) # reshape values if necessary if original_shape is not None: values = np.reshape(values, original_shape).astype(np.float32) return values
def complete_coord_boundaries(coord): """Complete a 2-d coordinates array, by generating/interpolating missing points. Parameters ---------- coord : np.ndarray, np.int64 Array of coordinates to complete, with shape (nb_points, 2). Returns ------- coord_completed : np.ndarray, np.int64 Completed coordinates arrays, with shape (nb_points, 2). """ # check parameters stack.check_array(coord, ndim=2, dtype=[np.int64]) # for each array in the list, complete its coordinates using the scikit # image method 'polygon_perimeter' coord_y, coord_x = polygon_perimeter(coord[:, 0], coord[:, 1]) coord_y = coord_y[:, np.newaxis] coord_x = coord_x[:, np.newaxis] coord_completed = np.concatenate((coord_y, coord_x), axis=-1) return coord_completed
def from_binary_to_coord(binary): """Extract coordinates from a 2-d binary matrix. As the resulting coordinates represent the external boundaries of the object, the coordinates values can be negative. Parameters ---------- binary : np.ndarray, np.uint or np.int or bool Binary image with shape (y, x). Returns ------- coord : np.ndarray, np.int64 Array of boundaries coordinates with shape (nb_points, 2). """ # check parameters stack.check_array(binary, ndim=2, dtype=[np.uint8, np.uint16, np.int64, bool]) # we enlarge the binary mask with one pixel to be sure the external # boundaries of the object still fit within the frame binary_ = np.pad(binary, [(1, 1)], mode="constant") # get external boundaries coordinates coord = find_contours(binary_, level=0)[0].astype(np.int64) # remove the pad coord -= 1 return coord
def get_cc(image, threshold): """Find connected regions above a fixed threshold. Parameters ---------- image : np.ndarray Image with shape (z, y, x) or (y, x). threshold : float or int A threshold to detect peaks. Returns ------- cc : np.ndarray, np.int64 Image labelled with shape (z, y, x) or (y, x). """ # check parameters stack.check_array(image, ndim=[2, 3], dtype=[np.uint8, np.uint16, np.float32, np.float64]) stack.check_parameter(threshold=(float, int)) # Compute binary mask of the filtered image mask = image > threshold # find connected components cc = label(mask) return cc
def compute_mean_size_object(image_labelled): """Compute the averaged size of the segmented objects. For each object, we compute the diameter of an object with an equivalent surface. Then, we average the diameters. Parameters ---------- image_labelled : np.ndarray, np.uint Labelled image with shape (y, x). Returns ------- mean_diameter : float Averaged size of the segmented objects. """ # check parameters stack.check_array(image_labelled, ndim=2, dtype=[np.uint8, np.uint16, np.int64]) # compute properties of the segmented object props = regionprops(image_labelled) # get equivalent diameter and average it diameter = [] for prop in props: diameter.append(prop.equivalent_diameter) mean_diameter = np.mean(diameter) return mean_diameter
def from_3_classes_to_instances(label_3_classes): """Extract instance labels from 3-classes Unet output. Parameters ---------- label_3_classes : np.ndarray, np.float32 Model prediction about the nucleus surface and boundaries, with shape (y, x, 3). Returns ------- label : np.ndarray, np.int64 Labelled image. Each instance is characterized by the same pixel value. """ # check parameters stack.check_array(label_3_classes, ndim=3, dtype=[np.float32]) # get classes indices label_3_classes = np.argmax(label_3_classes, axis=-1) # keep foreground predictions mask = label_3_classes > 1 # instantiate each individual foreground surface predicted label = label_instances(mask) # dilate label label = label.astype(np.float64) label = stack.dilation_filter(label, kernel_shape="disk", kernel_size=1) label = label.astype(np.int64) return label
def _get_nuclear_edge_probability_map(cell_mask, nuc_map): """Compute a probability map to sample a nuclear edge pattern. Parameters ---------- cell_mask : np.ndarray, bool Binary mask of the cell surface with shape (z, y, x). nuc_map : np.ndarray, np.float32 Distance map from the nucleus edge with shape (z, y, x). Returns ------- probability_map : np.ndarray, np.float32 Probability map to sample spots coordinates with shape (z, y, x). """ # check parameters stack.check_array(cell_mask, ndim=3, dtype=bool) stack.check_array(nuc_map, ndim=3, dtype=[np.float32, np.float64]) # nuclear edge probability map probability_map = nuc_map.copy().astype(np.float32) probability_map[nuc_map > 2.] = 0. probability_map[nuc_map <= 2.] = 1. probability_map[~cell_mask] = 0. probability_map /= probability_map.sum() return probability_map
def dilate_erode_labels(label): """Substract an eroded label to a dilated one in order to prevent boundaries contact. Parameters ---------- label : np.ndarray, np.uint or np.int Labelled image with shape (y, x). Returns ------- label_final : np.ndarray, np.int64 Labelled image with shape (y, x). """ # check parameters stack.check_array(label, ndim=2, dtype=[np.uint8, np.uint16, np.int64]) # handle 64 bit integer if label.dtype == np.int64: label = label.astype(np.uint16) # erode-dilate mask label_dilated = stack.dilation_filter(label, "disk", 2) label_eroded = stack.erosion_filter(label, "disk", 2) borders = label_dilated - label_eroded label_final = label.copy() label_final[borders > 0] = 0 label_final = label_final.astype(np.int64) return label_final
def _get_random_in_probability_map(cell_mask, nuc_mask): """Compute a probability map to sample a random pattern inside nucleus. Parameters ---------- cell_mask : np.ndarray, bool Binary mask of the cell surface with shape (z, y, x). nuc_mask : np.ndarray, bool Binary mask of the nucleus surface with shape (z, y, x). Returns ------- probability_map : np.ndarray, np.float32 Probability map to sample spots coordinates with shape (z, y, x). """ # check parameters stack.check_array(cell_mask, ndim=3, dtype=bool) stack.check_array(nuc_mask, ndim=3, dtype=bool) # random in probability map probability_map = nuc_mask.copy().astype(np.float32) probability_map /= probability_map.sum() return probability_map
def count_instances(image_label): """Count the number of instances annotated in the image. Parameters ---------- image_label : np.ndarray, np.int or np.uint Labelled image with shape (y, x). Returns ------- nb_instances : int Number of instances in the image. """ # check parameters stack.check_array(image_label, ndim=2, dtype=[np.uint8, np.uint16, np.int64]) indices = set(image_label.ravel()) if 0 in indices: nb_instances = len(indices) - 1 else: nb_instances = len(indices) return nb_instances
def compute_surface_ratio(image_label): """Compute the averaged surface ratio of the segmented instances. We compute the proportion of surface occupied by instances. Parameters ---------- image_label : np.ndarray, np.int or np.uint Labelled image with shape (y, x). Returns ------- surface_ratio : float Surface ratio of the segmented instances. """ # check parameters stack.check_array(image_label, ndim=2, dtype=[np.uint8, np.uint16, np.int64]) # compute surface ratio surface_instances = image_label > 0 area_instances = surface_instances.sum() surface_ratio = area_instances / image_label.size return surface_ratio
def thresholding(image, threshold): """Segment a 2-d image to discriminate object from background applying a threshold. Parameters ---------- image : np.ndarray, np.uint A 2-d image to segment with shape (y, x). threshold : int or float Pixel intensity threshold used to discriminate foreground from background. Returns ------- image_segmented : np.ndarray, bool Binary 2-d image with shape (y, x). """ # check parameters stack.check_array(image, ndim=2, dtype=[np.uint8, np.uint16]) stack.check_parameter(threshold=(float, int)) # discriminate nuclei from background, applying a threshold. image_segmented = image >= threshold return image_segmented
def compute_instances_mean_diameter(image_label): """Compute the averaged size of the segmented instances. For each instance, we compute the diameter of an object with an equivalent surface. Then, we average the diameters. Parameters ---------- image_label : np.ndarray, np.int64 Labelled image with shape (y, x). Returns ------- mean_diameter : float Averaged size of the segmented instances. """ # check parameters stack.check_array(image_label, ndim=2, dtype=np.int64) # compute properties of the segmented instances props = regionprops(image_label) # get equivalent diameter and average it diameter = [] for prop in props: diameter.append(prop.equivalent_diameter) mean_diameter = np.mean(diameter) return mean_diameter
def fit_gaussian_3d(f, grid, image_spot, p0, lower_bound=None, upper_bound=None): """Fit a gaussian function to a 3-d image. # TODO add equations and algorithm Parameters ---------- f : func A 3-d gaussian function with some parameters fixed. grid : np.ndarray, np.float Grid data to compute the gaussian function for different voxel within a volume V. In nanometer, with shape (3, V_z * V_y * V_x). image_spot : np.ndarray, np.uint A 3-d image with detected spot and shape (z, y, x). p0 : List List of parameters to estimate. lower_bound : List List of lower bound values for the different parameters. upper_bound : List List of upper bound values for the different parameters. Returns ------- popt : np.ndarray Fitted parameters. pcov : np.ndarray Estimated covariance of 'popt'. """ # check parameters stack.check_array(grid, ndim=2, dtype=np.float32, allow_nan=False) stack.check_array(image_spot, ndim=3, dtype=[np.uint8, np.uint16, np.float32, np.float64], allow_nan=False) stack.check_parameter(p0=list, lower_bound=(list, type(None)), upper_bound=(list, type(None))) # compute lower bound and upper bound if lower_bound is None: lower_bound = [-np.inf for _ in p0] if upper_bound is None: upper_bound = [np.inf for _ in p0] bounds = (lower_bound, upper_bound) # Apply non-linear least squares to fit a gaussian function to a 3-d image y = np.reshape(image_spot, (image_spot.size, )).astype(np.float32) popt, pcov = curve_fit(f=f, xdata=grid, ydata=y, p0=p0, bounds=bounds) return popt, pcov
def local_maximum_detection(image, min_distance): """Compute a mask to keep only local maximum, in 2-d and 3-d. #. We apply a multidimensional maximum filter. #. A pixel which has the same value in the original and filtered images is a local maximum. Several connected pixels can have the same value. In such a case, the local maximum is not unique. In order to make the detection robust, it should be applied to a filtered image (using :func:`bigfish.stack.log_filter` for example). Parameters ---------- image : np.ndarray Image to process with shape (z, y, x) or (y, x). min_distance : int, float, Tuple(int, float), List(int, float) Minimum distance (in pixels) between two spots we want to be able to detect separately. One value per spatial dimension (zyx or yx dimensions). If it's a scalar, the same distance is applied to every dimensions. Returns ------- mask : np.ndarray, bool Mask with shape (z, y, x) or (y, x) indicating the local peaks. """ # check parameters stack.check_array(image, ndim=[2, 3], dtype=[np.uint8, np.uint16, np.float32, np.float64]) stack.check_parameter(min_distance=(int, float, tuple, list)) # compute the kernel size (centered around our pixel because it is uneven) if isinstance(min_distance, (tuple, list)): if len(min_distance) != image.ndim: raise ValueError( "'min_distance' should be a scalar or a sequence with one " "value per dimension. Here the image has {0} dimensions and " "'min_distance' {1} elements.".format(image.ndim, len(min_distance))) else: min_distance = (min_distance, ) * image.ndim min_distance = np.ceil(min_distance).astype(image.dtype) kernel_size = 2 * min_distance + 1 # apply maximum filter to the original image image_filtered = ndi.maximum_filter(image, size=kernel_size) # we keep the pixels with the same value before and after the filtering mask = image == image_filtered return mask
def identify_objects_in_region(mask, coord, ndim): """Identify cellular objects in specific region. Parameters ---------- mask : np.ndarray, bool Binary mask of the targeted region with shape (y, x). coord : np.ndarray Array with two dimensions. One object per row, zyx or yx coordinates in the first 3 or 2 columns. ndim : int Number of spatial dimensions to consider (2 or 3). Returns ------- coord_in : np.ndarray Coordinates of the objects detected inside the region. coord_out : np.ndarray Coordinates of the objects detected outside the region. """ # check parameters stack.check_parameter(ndim=int) stack.check_array(mask, ndim=2, dtype=[np.uint8, np.uint16, np.int64, bool]) stack.check_array(coord, ndim=2, dtype=[np.int64, np.float64]) # check number of dimensions if ndim not in [2, 3]: raise ValueError("The number of spatial dimension requested should be " "2 or 3, not {0}.".format(ndim)) if coord.shape[1] < ndim: raise ValueError("Coord array should have at least {0} features to " "match the number of spatial dimensions requested. " "Currently {1} is not enough.".format( ndim, coord.shape[1])) # binarize nuclei mask if needed if mask.dtype != bool: mask = mask.astype(bool) # cast coordinates dtype if necessary if coord.dtype == np.int64: coord_int = coord else: coord_int = np.round(coord).astype(np.int64) # remove objects inside the region mask_in = mask[coord_int[:, ndim - 2], coord_int[:, ndim - 1]] coord_in = coord[mask_in] coord_out = coord[~mask_in] return coord_in, coord_out
def clean_segmentation(image, small_object_size=None, fill_holes=False, smoothness=None, delimit_instance=False): """Clean segmentation results (binary masks or integer labels). Parameters ---------- image : np.ndarray, np.int64 or bool Labelled or masked image with shape (y, x). small_object_size : int or None Areas with a smaller surface (in pixels) are removed. fill_holes : bool Fill holes within a labelled or masked area. smoothness : int or None Radius of a median kernel filter. The higher the smoother instance boundaries are. delimit_instance : bool Delimit clearly instances boundaries by preventing contact between each others. Returns ------- image_cleaned : np.ndarray, np.int64 or bool Cleaned image with shape (y, x). """ # check parameters stack.check_array(image, ndim=2, dtype=[np.int64, bool]) stack.check_parameter(small_object_size=(int, type(None)), fill_holes=bool, smoothness=(int, type(None)), delimit_instance=bool) # initialize cleaned image image_cleaned = image.copy() # remove small object if small_object_size is not None: image_cleaned = _remove_small_area(image_cleaned, small_object_size) # fill holes if fill_holes: image_cleaned = _fill_hole(image_cleaned) if smoothness: image_cleaned = _smooth_instance(image_cleaned, smoothness) if delimit_instance: image_cleaned = _delimit_instance(image_cleaned) return image_cleaned
def plot_illumination_surface(illumination_surface, r=0, framesize=(15, 15), titles=None, path_output=None, ext="png"): """Subplot the yx plan of the dimensions of an illumination surface for all channels. Parameters ---------- illumination_surface : np.ndarray, np.float A 4-d tensor with shape (r, c, y, x) approximating the average differential of illumination in our stack of images, for each channel and each round. r : int Index of the round to keep. framesize : tuple Size of the frame used to plot with 'plt.figure(figsize=framesize)'. titles : List[str] Titles of the subplots (one per channel). path_output : str Path to save the image (without extension). ext : str or List[str] Extension used to save the plot. If it is a list of strings, the plot will be saved several times. Returns ------- """ # TODO add title in the plot and remove axes # TODO add parameter for vmin and vmax # check tensor stack.check_array(illumination_surface, ndim=4, dtype=[np.float32, np.float64]) # get the number of channels nb_channels = illumination_surface.shape[1] # plot fig, ax = plt.subplots(1, nb_channels, sharex='col', figsize=framesize) for i in range(nb_channels): ax[i].imshow(illumination_surface[r, i, :, :]) if titles is not None: ax[i].set_title(titles[i], fontweight="bold", fontsize=15) plt.tight_layout() save_plot(path_output, ext) plt.show() return
def remove_transcription_site(rna, clusters, nuc_mask, ndim): """Distinguish RNA molecules detected in a transcription site from the rest. A transcription site is defined as as a foci detected within the nucleus. Parameters ---------- rna : np.ndarray Coordinates of the detected RNAs with shape (nb_spots, 4) or (nb_spots, 3). One coordinate per dimension (zyx or yx coordinates) plus the index of the cluster assigned to the RNA. If no cluster was assigned, value is -1. clusters : np.ndarray Array with shape (nb_clusters, 5) or (nb_clusters, 4). One coordinate per dimension for the clusters centroid (zyx or yx coordinates), the number of RNAs detected in the clusters and their index. nuc_mask : np.ndarray, bool Binary mask of the nuclei region with shape (y, x). ndim : int Number of spatial dimensions to consider (2 or 3). Returns ------- rna_out_ts : np.ndarray Coordinates of the detected RNAs with shape (nb_spots, 4) or (nb_spots, 3). One coordinate per dimension (zyx or yx coordinates) plus the index of the foci assigned to the RNA. If no foci was assigned, value is -1. RNAs from transcription sites are removed. foci : np.ndarray Array with shape (nb_foci, 5) or (nb_foci, 4). One coordinate per dimension for the foci centroid (zyx or yx coordinates), the number of RNAs detected in the foci and its index. ts : np.ndarray Array with shape (nb_ts, 5) or (nb_ts, 4). One coordinate per dimension for the transcription site centroid (zyx or yx coordinates), the number of RNAs detected in the transcription site and its index. """ # check parameters stack.check_array(rna, ndim=2, dtype=[np.int64, np.float64]) # discriminate foci from transcription sites ts, foci = identify_objects_in_region(nuc_mask, clusters, ndim) # filter out rna from transcription sites rna_in_ts = ts[:, ndim + 1] mask_rna_in_ts = np.isin(rna[:, ndim], rna_in_ts) rna_out_ts = rna[~mask_rna_in_ts] return rna_out_ts, foci, ts
def add_white_noise(image, noise_level, random_noise=0.05): """Generate and add white noise to an image. Parameters ---------- image : np.ndarray Image with shape (z, y, x) or (y, x). noise_level : int or float Reference level of noise background to add in the image. random_noise : int or float, default=0.05 Background noise follows a normal distribution around the provided noise values. The scale used is: .. math:: \\mbox{scale} = \\mbox{noise_level} * \\mbox{random_noise} Returns ------- noised_image : np.ndarray Noised image with shape (z, y, x) or (y, x). """ # check parameters stack.check_array(image, ndim=[2, 3], dtype=[np.uint8, np.uint16, np.float32, np.float64]) stack.check_parameter(noise_level=(int, float), random_noise=(int, float)) # original dtype original_dtype = image.dtype if np.issubdtype(original_dtype, np.integer): original_max_bound = np.iinfo(original_dtype).max else: original_max_bound = np.finfo(original_dtype).max # compute scale scale = noise_level * random_noise # generate noise noise_raw = np.random.normal(loc=noise_level, scale=scale, size=image.size) noise_raw[noise_raw < 0] = 0. noise_raw = np.random.poisson(lam=noise_raw, size=noise_raw.size) noise = np.reshape(noise_raw, image.shape) # add noise noised_image = image.astype(np.float64) + noise noised_image = np.clip(noised_image, 0, original_max_bound) noised_image = noised_image.astype(original_dtype) return noised_image
def spots_thresholding(image, sigma, mask_lm, threshold): """Filter detected spots and get coordinates of the remaining spots. Parameters ---------- image : np.ndarray, np.uint Image with shape (z, y, x) or (y, x). sigma : float or Tuple(float) Sigma used for the gaussian filter (one for each dimension). If it's a float, the same sigma is applied to every dimensions. mask_lm : np.ndarray, bool Mask with shape (z, y, x) or (y, x) indicating the local peaks. threshold : float or int A threshold to detect peaks. Returns ------- spots : np.ndarray, np.int64 Coordinate of the local peaks with shape (nb_peaks, 3) or (nb_peaks, 2) for 3-d or 2-d images respectively. radius : float or Tuple(float) Radius of the detected peaks. mask : np.ndarray, bool Mask with shape (z, y, x) or (y, x) indicating the spots. """ # TODO make 'radius' output more consistent # check parameters stack.check_array(image, ndim=[2, 3], dtype=[np.uint8, np.uint16, np.float32, np.float64]) stack.check_array(mask_lm, ndim=[2, 3], dtype=[bool]) stack.check_parameter(sigma=(float, int, tuple), threshold=(float, int)) # remove peak with a low intensity mask = (mask_lm & (image > threshold)) # get peak coordinates spots = np.nonzero(mask) spots = np.column_stack(spots) # compute radius if isinstance(sigma, tuple): radius = [np.sqrt(image.ndim) * sigma_ for sigma_ in sigma] radius = tuple(radius) else: radius = np.sqrt(image.ndim) * sigma return spots, radius, mask
def automated_threshold_setting(image, mask_local_max): """Automatically set the optimal threshold to detect spots. In order to make the thresholding robust, it should be applied to a filtered image (using :func:`bigfish.stack.log_filter` for example). The optimal threshold is selected based on the spots distribution. The latter should have an elbow curve discriminating a fast decreasing stage from a more stable one (a plateau). Parameters ---------- image : np.ndarray Image with shape (z, y, x) or (y, x). mask_local_max : np.ndarray, bool Mask with shape (z, y, x) or (y, x) indicating the local peaks. Returns ------- optimal_threshold : int Optimal threshold to discriminate spots from noisy blobs. """ # check parameters stack.check_array(image, ndim=[2, 3], dtype=[np.uint8, np.uint16, np.float32, np.float64]) stack.check_array(mask_local_max, ndim=[2, 3], dtype=[bool]) # get threshold values we want to test thresholds = _get_candidate_thresholds(image.ravel()) # get spots count and its logarithm first_threshold = float(thresholds[0]) spots, mask_spots = spots_thresholding(image, mask_local_max, first_threshold, remove_duplicate=False) value_spots = image[mask_spots] thresholds, count_spots = _get_spot_counts(thresholds, value_spots) # select threshold where the break of the distribution is located if count_spots.size > 0: optimal_threshold, _, _ = get_breaking_point(thresholds, count_spots) # case where no spots were detected else: optimal_threshold = None return optimal_threshold
def log_lm(image, sigma, threshold, minimum_distance=1): """Apply LoG filter followed by a Local Maximum algorithm to detect spots in a 2-d or 3-d image. 1) We smooth the image with a LoG filter. 2) We apply a multidimensional maximum filter. 3) A pixel which has the same value in the original and filtered images is a local maximum. 4) We remove local peaks under a threshold. Parameters ---------- image : np.ndarray Image with shape (z, y, x) or (y, x). sigma : float or Tuple(float) Sigma used for the gaussian filter (one for each dimension). If it's a float, the same sigma is applied to every dimensions. threshold : float or int A threshold to detect peaks. minimum_distance : int Minimum distance (in number of pixels) between two local peaks. Returns ------- spots : np.ndarray, np.int64 Coordinate of the spots with shape (nb_spots, 3) or (nb_spots, 2) for 3-d or 2-d images respectively. radius : float, Tuple[float] Radius of the detected peaks. """ # check parameters stack.check_array(image, ndim=[2, 3], dtype=[np.uint8, np.uint16, np.float32, np.float64]) stack.check_parameter(sigma=(float, int, tuple), minimum_distance=(float, int), threshold=(float, int)) # cast image in np.float and apply LoG filter image_filtered = stack.log_filter(image, sigma, keep_dtype=True) # find local maximum mask = local_maximum_detection(image_filtered, minimum_distance) # remove spots with a low intensity and return coordinates and radius spots, radius, _ = spots_thresholding(image, sigma, mask, threshold) return spots, radius
def match_nuc_cell(nuc_label, cell_label): """Match each nucleus instance with the most overlapping cell instance. Parameters ---------- nuc_label : np.ndarray, np.int or np.uint Labelled image of nuclei with shape (y, x). cell_label : np.ndarray, np.int or np.uint Labelled image of cells with shape (y, x). Returns ------- new_nuc_label : np.ndarray, np.int or np.uint Labelled image of nuclei with shape (y, x). new_cell_label : np.ndarray, np.int or np.uint Labelled image of cells with shape (y, x). """ # check parameters stack.check_array(nuc_label, ndim=2, dtype=[np.uint8, np.uint16, np.int64]) stack.check_array(cell_label, ndim=2, dtype=[np.uint8, np.uint16, np.int64]) # initialize new labelled images new_nuc_label = np.zeros_like(nuc_label) new_cell_label = np.zeros_like(cell_label) # match nuclei and cells label_max = nuc_label.max() for i_nuc in range(1, label_max + 1): # check if a nucleus is labelled with this value nuc_mask = nuc_label == i_nuc if nuc_mask.sum() == 0: continue # check if a cell is labelled with this value i_cell = _get_most_frequent_value(cell_label[nuc_mask]) if i_cell == 0: continue # assign cell and nucleus cell_mask = cell_label == i_cell cell_mask |= nuc_mask new_nuc_label[nuc_mask] = i_nuc new_cell_label[cell_mask] = i_nuc # remove pixel already assigned nuc_label[nuc_mask] = 0 cell_label[cell_mask] = 0 return new_nuc_label, new_cell_label
def local_maximum_detection(image, min_distance): """Compute a mask to keep only local maximum, in 2-d and 3-d. 1) We apply a multidimensional maximum filter. 2) A pixel which has the same value in the original and filtered images is a local maximum. Parameters ---------- image : np.ndarray Image to process with shape (z, y, x) or (y, x). min_distance : int, float or Tuple(float) Minimum distance (in pixels) between two spots we want to be able to detect separately. One value per spatial dimension (zyx or yx dimensions). If it's a scalar, the same distance is applied to every dimensions. Returns ------- mask : np.ndarray, bool Mask with shape (z, y, x) or (y, x) indicating the local peaks. """ # check parameters stack.check_array(image, ndim=[2, 3], dtype=[np.uint8, np.uint16, np.float32, np.float64]) stack.check_parameter(min_distance=(float, int, tuple)) # compute the kernel size (centered around our pixel because it is uneven) if isinstance(min_distance, (int, float)): min_distance = (min_distance,) * image.ndim min_distance = np.ceil(min_distance).astype(image.dtype) elif image.ndim != len(min_distance): raise ValueError("'min_distance' should be a scalar or a tuple with " "one value per dimension. Here the image has {0} " "dimensions and 'min_distance' {1} elements." .format(image.ndim, len(min_distance))) else: min_distance = np.ceil(min_distance).astype(image.dtype) kernel_size = 2 * min_distance + 1 # apply maximum filter to the original image image_filtered = ndi.maximum_filter(image, size=kernel_size) # we keep the pixels with the same value before and after the filtering mask = image == image_filtered return mask
def features_in_out_nucleus(rna_coord, rna_coord_out_nuc, check_input=True): """Compute nucleus related features. Parameters ---------- rna_coord : np.ndarray, np.int64 Coordinates of the detected RNAs with zyx or yx coordinates in the first 3 or 2 columns. rna_coord_out_nuc : np.ndarray, np.int64 Coordinates of the detected RNAs with zyx or yx coordinates in the first 3 or 2 columns. Spots detected inside the nucleus are removed. check_input : bool Check input validity. Returns ------- proportion_rna_in_nuc : float Proportion of RNAs detected inside the nucleus. nb_rna_out_nuc : float Number of RNAs detected outside the nucleus. nb_rna_in_nuc : float Number of RNAs detected inside the nucleus. """ # check parameters stack.check_parameter(check_input=bool) if check_input: stack.check_array(rna_coord, ndim=2, dtype=np.int64) stack.check_array(rna_coord_out_nuc, ndim=2, dtype=np.int64) # count total number of rna nb_rna = float(len(rna_coord)) # case where no rna are detected if nb_rna == 0: features = (0., 0., 0.) return features # number of rna outside and inside nucleus nb_rna_out_nuc = float(len(rna_coord_out_nuc)) nb_rna_in_nuc = nb_rna - nb_rna_out_nuc # compute the proportion of rna in the nucleus proportion_rna_in_nuc = nb_rna_in_nuc / nb_rna features = (proportion_rna_in_nuc, nb_rna_out_nuc, nb_rna_in_nuc) return features