Source code for timagetk.components.labelled_image

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#  Copyright (c) 2022 Univ. Lyon, ENS de Lyon, UCB Lyon 1, CNRS, INRAe, Inria
#  All rights reserved.
#  This file is part of the TimageTK library, and is released under the "GPLv3"
#  license. Please see the LICENSE.md file that should have been included as
#  part of this package.
# ------------------------------------------------------------------------------

"""LabelledImage class and associated functionalities.

Return rule according to object (label, surfel, linel, pointel) input type:
  * if a list of ids is given, a dictionary with the ids as keys & the features (bounding-box, neighbors,...) as values is returned
  * if a single id is given, the corresponding feature is return, type depending on feature type.

Id rules according to object type:
  * all 'label features' have a unique id represented by an integer;
  * all 'surfel features' have a unique id represented by a len-2 unordered tuple made of the two label ids defining it;
  * all 'linel features' have a unique id represented by a len-3 unordered tuple made of the three label ids defining it;
  * all 'pointel features' have a unique id represented by a len-4 unordered tuple made of the four label ids defining it;

The term 'unordered tuple' means that the order of ids defining the tuple does not matter: ``(i,j) == (j,i)``.
The ids are sorted by ascending order anyway.
"""

import time

import numpy as np
import scipy.ndimage as nd
from tqdm.autonotebook import tqdm

from timagetk.algorithms.slices import dilation_by
from timagetk.algorithms.slices import real_indices
from timagetk.algorithms.topological_elements import topological_elements_extraction2D
from timagetk.algorithms.topological_elements import topological_elements_extraction3D
from timagetk.bin.logger import get_logger
from timagetk.components.spatial_image import SpatialImage
from timagetk.util import clean_type
from timagetk.util import elapsed_time
from timagetk.util import get_attributes
from timagetk.util import get_class_name
from timagetk.util import stuple

log = get_logger(__name__)


[docs] def assert_labelled_image(obj, obj_name=None): """Tests whether given object is a `LabelledImage`. Parameters ---------- obj : instance Object to test. obj_name : str, optional If given used as object name for ``TypeError`` printing. Raises ------ TypeError If `obj` is not a ``LabelledImage`` instance. Examples -------- >>> from timagetk.components.labelled_image import assert_labelled_image >>> from timagetk import LabelledImage >>> from timagetk.array_util import DUMMY_SEG_2D >>> # Example 1 - NumPy array: >>> assert_labelled_image(DUMMY_SEG_2D, "dummy labelled array") TypeError: Input 'dummy labelled array' is not a `LabelledImage` instance. >>> # Example 2 - LabelledImage: >>> lab_image = LabelledImage(DUMMY_SEG_2D, voxelsize=[0.5,0.5], not_a_label=0) >>> assert_labelled_image(lab_image, "dummy labelled array") """ if obj_name is None: try: obj_name = obj.filename except AttributeError: obj_name = clean_type(obj) err = "Input '{}' is not a `LabelledImage` instance." try: assert isinstance(obj, LabelledImage) except AssertionError: raise TypeError(err.format(obj_name)) return
# ------------------------------------------------------------------------------ # # Morphology functions, array based (not to use with VT algorithms): # # ------------------------------------------------------------------------------
[docs] def connectivity_4(): """Create a 2D structuring element (array) of radius 1 with a 4-neighborhood. Returns ------- numpy.ndarray A boolean array defining the 2D structuring element. """ return nd.generate_binary_structure(2, 1)
[docs] def connectivity_6(): """Create a 3D structuring element (array) of radius 1 with a 6-neighborhood. Returns ------- numpy.ndarray A boolean array defining the 3D structuring element. """ return nd.generate_binary_structure(3, 1)
[docs] def connectivity_8(): """Create a 2D structuring element (array) of radius 1 with a 8-neighborhood. Returns ------- numpy.ndarray A boolean array defining the 2D structuring element. """ return nd.generate_binary_structure(2, 2)
[docs] def connectivity_18(): """Create a 3D structuring element (array) of radius 1 with a 18-neighborhood. Returns ------- numpy.ndarray A boolean array defining the 3D structuring element. """ return nd.generate_binary_structure(3, 2)
[docs] def connectivity_26(): """Create a 3D structuring element (array) of radius 1 with a 26-neighborhood. Returns ------- numpy.ndarray A boolean array defining the 3D structuring element. """ return nd.generate_binary_structure(3, 3)
[docs] def structuring_element(connectivity=26): """Create a structuring element. Connectivity is among the 4-, 6-, 8-, 18-, 26-neighborhoods. ``4`` and ``8`` are 2D elements, the others are 3D. Parameters ---------- connectivity : int, optional Connectivity or neighborhood of the structuring element, default is ``26``. Returns ------- numpy.ndarray A boolean array defining the required structuring element. """ assert connectivity in [4, 6, 8, 18, 26] if connectivity == 4: struct = connectivity_4() elif connectivity == 6: struct = connectivity_6() elif connectivity == 8: struct = connectivity_8() elif connectivity == 18: struct = connectivity_18() else: struct = connectivity_26() return struct
[docs] def default_structuring_element2d(): """Default 2D structuring element.""" return connectivity_6()
[docs] def default_structuring_element3d(): """Default 3D structuring element.""" return connectivity_26()
def _test_structuring_element(array, struct): """Test if the array and the structuring element are compatible, *i.e.* of same dimensionality. Parameters ---------- array : numpy.ndarray Array on which the structuring element should be applied. struct : numpy.ndarray Array defining the structuring element. Returns ------- bool ``True`` if compatible, ``False`` otherwise. """ return array.ndim == struct.ndim # ------------------------------------------------------------------------------ # # LABEL based functions: # # ------------------------------------------------------------------------------
[docs] def labels_at_stack_margins(labelled_img, voxel_distance_from_margin=1, labels_to_keep=None): """Return a list of labels in contact with the margins of the stack. All ids within a defined (1 by default) `voxel_distance_from_margin` will be considered. Parameters ---------- labelled_img : timagetk.LabelledImage The labelled image to use. voxel_distance_from_margin : int, optional The voxel distance from the stack margin to consider. labels_to_keep : None or list A list of label to keep even if detected at the stack margin. Returns ------- list The list of labels in contact with the margins of the stack. Examples -------- >>> from timagetk import LabelledImage >>> from timagetk.io import imread >>> from timagetk.io.util import shared_dataset >>> from timagetk.components.labelled_image import labels_at_stack_margins >>> seg_im = LabelledImage(imread(shared_dataset("p58", "segmented")[0])) >>> margin_labels = labels_at_stack_margins(seg_im, 5, labels_to_keep=[1]) >>> print(f"Found {len(margin_labels)} labels at the stack margins!") >>> print(margin_labels) """ vx_dist = voxel_distance_from_margin margins = [] margins.extend(np.unique(labelled_img[0:vx_dist, :])) margins.extend(np.unique(labelled_img[-vx_dist:, :])) margins.extend(np.unique(labelled_img[:, 0:vx_dist])) margins.extend(np.unique(labelled_img[:, -vx_dist:])) if labels_to_keep is not None: if isinstance(labels_to_keep, int): labels_to_keep = [labels_to_keep] return list(set(margins) - set(labels_to_keep)) else: return list(set(margins))
[docs] def label_inner_margin(labelled_img, label, struct=None, connectivity_order=1): """Return an array with only inner-margin values of a label. Parameters ---------- labelled_img : numpy.ndarray or timagetk.LabelledImage A labelled image containing the given label. label : int Label to use for inner-margin detection. struct : numpy.ndarray, optional A binary structure to use for erosion. connectivity_order : int, optional Connectivity order determines which elements of the output array belong to the structure, *i.e.* are considered as neighbors of the central element. Elements up to a squared distance of connectivity from the center are considered neighbors, thus it may range from 1 (no diagonal elements are neighbors) to rank (all elements are neighbors), with rank the number of dimensions of the image. Returns ------- numpy.ndarray or timagetk.LabelledImage A labelled array with only the inner-margin position as non-null value. Examples -------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> from timagetk.components.labelled_image import label_inner_margin >>> im = LabelledImage(a) >>> label_inner_margin(im, 7) LabelledImage([[0, 0, 7, 7, 0, 0], [0, 0, 0, 7, 0, 0], [0, 0, 0, 7, 0, 0], [0, 0, 0, 0, 0, 0]]) """ if struct is None: rank = labelled_img.ndim struct = nd.generate_binary_structure(rank, connectivity_order) # Create boolean mask of the label position in the image mask_img = labelled_img == label # Binary dilation of the mask er_mask_img = nd.binary_erosion(mask_img, structure=struct) # Define a mask giving outer-margin position for label inner_margin = mask_img ^ er_mask_img # return the labelled array with only the inner-margin position: return labelled_img * inner_margin
[docs] def label_outer_margin(labelled_img, label, struct=None, connectivity_order=1): """Return an array with only outer-margin values of a label. Parameters ---------- labelled_img : numpy.ndarray or timagetk.LabelledImage A labelled image containing the given label. label : int Label to use for its outer-margin detection. struct : numpy.ndarray, optional A binary structure to use for dilation. connectivity_order : int, optional Connectivity order determines which elements of the output array belong to the structure, *i.e.* are considered as neighbors of the central element. Elements up to a squared distance of connectivity from the center are considered neighbors, thus it may range from 1 (no diagonal elements are neighbors) to rank (all elements are neighbors), with rank the number of dimensions of the image. Returns ------- numpy.ndarray or timagetk.LabelledImage A labelled array with only the outer-margin position as non-null value. Examples -------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> from timagetk.components.labelled_image import label_outer_margin >>> im = LabelledImage(a) >>> label_outer_margin(im, 7) LabelledImage([[0, 2, 0, 0, 1, 0], [0, 0, 5, 0, 3, 0], [0, 0, 1, 0, 3, 0], [0, 0, 0, 4, 0, 0]]) """ if struct is None: rank = labelled_img.ndim struct = nd.generate_binary_structure(rank, connectivity_order) # Create boolean mask of 'label_id' position in the image mask_img = labelled_img == label # Binary dilation of the mask dil_mask_img = nd.binary_dilation(mask_img, structure=struct) # Define a mask giving outer-margin position for 'label_id' outer_margin = dil_mask_img ^ mask_img # return the labelled array with only the outer-margin position: return labelled_img * outer_margin
[docs] def label_neighbors(labelled_img, label, **kwargs): """List neighbors of `label` in labelled image. List of unique non-null labels as found in `label` outer-margin. Parameters ---------- labelled_img : numpy.ndarray or timagetk.LabelledImage A labelled image containing the given label. label : int or list of int Label to use for neighbors detection. Other Parameters ---------------- struct : numpy.ndarray, optional A binary structure to use for dilation connectivity_order : int, optional Connectivity order determines which elements of the output array belong to the structure, *i.e.* are considered as neighbors of the central element. Elements up to a squared distance of connectivity from the center are considered neighbors, thus it may range from 1 (no diagonal elements are neighbors) to `rank` (all elements are neighbors), with `rank` the number of dimensions of the image. Returns ------- list Neighbors of given label. See Also -------- timagetk.components.labelled_image.label_outer_margin Examples -------- >>> import numpy as np >>> from timagetk.components.labelled_image import label_neighbors >>> arr = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> label_neighbors(arr, 7) # works with a numpy array [1, 2, 3, 4, 5] >>> from timagetk import LabelledImage >>> from timagetk.components.labelled_image import connectivity_8 >>> im = LabelledImage(arr) >>> label_neighbors(im, 7) # works with a LabelledImage [1, 2, 3, 4, 5] >>> label_neighbors(im, 5) [1, 6, 7] >>> label_neighbors(im, 5, struct=connectivity_8()) # `struct=connectivity_4()` by default [1, 2, 6, 7] >>> label_neighbors(im, 5, connectivity_order=2) # `connectivity_order=1` by default [1, 2, 6, 7] >>> label_neighbors(arr, [5, 6]) # works with a list of labels {5: [1, 6, 7], 6: [1, 2, 5]} """ if isinstance(label, int): # Get outer-margin array & return unique list of labels: return list(set(np.unique(label_outer_margin(labelled_img, label, **kwargs))) - {0}) elif isinstance(label, list): return {l: list(set(np.unique(label_outer_margin(labelled_img, l, **kwargs))) - {0}) for l in label} else: raise TypeError(f"Parameter 'label' should be an integer or a list, got '{type(label)}'!")
# ------------------------------------------------------------------------------ # # WHOLE LABELLED IMAGE functions: # # ------------------------------------------------------------------------------
[docs] def image_with_labels(image, labels, erase_value=None): """Create a new image containing only the given labels. Use `image` as template to get shape, origin, voxel-size & metadata. Parameters ---------- image : timagetk.LabelledImage Labelled spatial image to use as template for labels extraction. labels : list The list of labels to keep in the image. erase_value : int, optional The value to use to erase given `labels`. Default to ``image.not_a_label``. Returns ------- timagetk.LabelledImage The image containing only the given `labels`. Examples -------- >>> from timagetk.components.labelled_image import image_with_labels >>> from timagetk.synthetic_data.labelled_image import example_layered_sphere_labelled_image >>> im = example_layered_sphere_labelled_image(n_points=10, n_layers=1, extent=50.) >>> print(im.labels()) # `1` is background and `12` is the central round 'core' label [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] >>> im2 = image_with_labels(im, [1, 2, 3, 4, 5]) >>> print(im2.labels()) [1, 2, 3, 4, 5] """ from timagetk.components.image import get_image_attributes attr = get_image_attributes(image) if erase_value is None: erase_value = image.not_a_label # - Initialising empty template image log.info("Initialising empty template image...") if erase_value == 0: template_im = np.zeros_like(image.get_array()) else: template_im = np.ones_like(image.get_array()) * erase_value nb_labels = len(labels) boundingbox = image.boundingbox(labels) # - Add selected 'labels' to the empty image: no_bbox = [] log.info(f"Adding {nb_labels} labels to the empty template image...") for n, label in tqdm(enumerate(labels), total=nb_labels, unit='label'): try: bbox = boundingbox[label] xyz = np.array(np.where((image[bbox]) == label)).T xyz = tuple([xyz[:, n] + bbox[n].start for n in range(image.ndim)]) template_im[xyz] = label except ValueError: no_bbox.append(label) template_im[image == label] = label # - If some bounding-boxes were missing, print about it: if no_bbox: n = len(no_bbox) log.warning(f"Could not find bounding-boxes for {n} labels: {no_bbox}") return LabelledImage(template_im, **attr)
[docs] def image_without_labels(image, labels, erase_value=None): """Create a new image without the given labels. Use `image` as template to get shape, origin, voxelsize & metadata. Parameters ---------- image : timagetk.LabelledImage Labelled spatial image to use as template for labels deletion. labels : list The list of labels to remove from the image. erase_value : int, optional The value to use to erase given `labels`. Default to ``image.not_a_label``. Returns ------- timagetk.LabelledImage An image without the given `labels`. Examples -------- >>> from timagetk.components.labelled_image import image_without_labels >>> from timagetk.synthetic_data.labelled_image import example_layered_sphere_labelled_image >>> from timagetk.io import imread >>> from timagetk import TissueImage3D >>> im = example_layered_sphere_labelled_image(n_points=10, n_layers=1, extent=50.) >>> print(im.labels()) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] >>> im2 = image_without_labels(im, [1, 2, 3, 4, 5]) >>> print(im2.labels()) [6, 7, 8, 9, 10, 11, 12] """ from timagetk.components.image import get_image_attributes attr = get_image_attributes(image) if erase_value is None: erase_value = image.not_a_label # - Initialising template image: log.info("Initialising a template image...") template_im = image.get_array() nb_labels = len(labels) boundingbox = image.boundingbox(labels) # - Remove selected 'labels' from the empty image: no_bbox = [] log.info(f"Removing {nb_labels} labels from the template image...") for _n, label in tqdm(enumerate(labels), total=nb_labels, unit='label'): # Try to get the label's bounding-box: try: bbox = boundingbox[label] except KeyError: no_bbox.append(label) bbox = None # Performs value replacement: template_im = array_replace_label(template_im, label, erase_value, bbox) # - If some bounding-boxes were missing, print about it: if no_bbox: n = len(no_bbox) log.warning(f"Could not find bounding-boxes for {n} labels: {no_bbox}") return LabelledImage(template_im, **attr)
[docs] def array_replace_label(array, label, new_label, bbox=None): """Replace a label by a new one in a numpy array. Providing a bounding-box of the `label` should speed up the process. Parameters ---------- array : numpy.ndarray Labelled array with integer values. label : int Label to replace. new_label : int New label to use as replacement. bbox : tuple of slice, optional Tuple of slices indicating the location of the `label` within the `image`. Returns ------- numpy.ndarray The modified array. Examples -------- >>> import numpy as np >>> a = np.array([[1, 2, 2, 2, 2, 3, 3, 3], [1, 2, 2, 2, 2, 3, 3, 3], [1, 2, 2, 2, 2, 3, 3, 3], [1, 2, 2, 2, 2, 3, 3, 3]]) >>> from timagetk.components.labelled_image import array_replace_label >>> array_replace_label(a, label=1, new_label=0) array([[0, 2, 2, 2, 2, 3, 3, 3], [0, 2, 2, 2, 2, 3, 3, 3], [0, 2, 2, 2, 2, 3, 3, 3], [0, 2, 2, 2, 2, 3, 3, 3]]) """ if bbox is not None: xyz = np.array(np.where((array[bbox]) == label)).T xyz = tuple([xyz[:, n] + bbox[n].start for n in range(array.ndim)]) array[xyz] = new_label else: array[array == label] = new_label return array
[docs] def hollow_out_labelled_image(image, **kwargs): """Return a labelled image containing only the label margins. Parameters ---------- image : timagetk.LabelledImage Labelled image to transform. Returns ------- timagetk.LabelledImage Labelled image containing hollowed out labels (only their margins). Notes ----- The 'non-margin voxels' are set to `image.not_a_label`. The Laplacian filter is used to detect label margins, as it highlights regions of rapid intensity change. Examples -------- >>> from timagetk.components.labelled_image import hollow_out_labelled_image >>> from timagetk import LabelledImage >>> from timagetk.io import imread >>> from timagetk.io.util import shared_dataset >>> from timagetk.visu.stack import orthogonal_view >>> seg_img_path = shared_dataset("sphere", "segmented")[0] >>> seg_img = imread(seg_img_path, rtype=LabelledImage, not_a_label=0) >>> hollow_seg = hollow_out_labelled_image(seg_img) >>> orthogonal_view(hollow_seg, cmap='glasbey', val_range='auto') >>> # Create a membrane image from the labelled image >>> hollow_seg = hollow_out_labelled_image(seg_img) >>> from skimage import img_as_ubyte >>> from timagetk import SpatialImage >>> membrane_img = SpatialImage(img_as_ubyte(hollow_seg), voxelsize=seg_img.get_voxelsize()) # convert image to unsigned 8bits >>> membrane_img[membrane_img != 0] = 255 >>> orthogonal_view(membrane_img,cmap='gray') >>> # Make it a bit more realistic: >>> from timagetk.algorithms.linearfilter import gaussian_filter >>> real_membrane_img = gaussian_filter(membrane_img, sigma=0.5, real=True) >>> orthogonal_view(real_membrane_img, cmap='gray') """ verbose = kwargs.get('verbose', True) log.info('Hollowing out labelled numpy array... ') t_start = time.time() # - The laplacian allows to quickly get a mask with all the label margins: laplacian_mask = np.array(nd.laplace(image)) != 0 # - Get the label values: image *= laplacian_mask if verbose: log.info(elapsed_time(t_start)) return image
def relabel_with_property(image, mapping): from timagetk.components.image import get_image_attributes dtype = "float32" attr = get_image_attributes(image, exclude=['dtype']) labels = image.labels() in_labels = set(mapping.keys()) & set(labels) off_labels = set(mapping.keys()) - in_labels n_in = len(in_labels) # -- Print a summary of this: n_mapped = len(mapping.keys()) s = f"Got an initial list of {n_mapped} mapped labels" if off_labels: n_off = len(off_labels) pc_in = n_in * 100 / n_mapped pc_off = 100 - pc_in s += f", {n_in} ({round(pc_in, 1)}%) are found in the image" s += f" and {n_off} ({round(pc_off, 1)}%) are not!" else: s += ", all are found in the image!" log.info(s) log.info(f"They will be remapped into {len(set(mapping.values()))} unique labels!") # - Get image template = image.get_array().copy() # - Get mask of the missing values unmapped_labels = list(set(image.labels()) - set(mapping)) mask = np.isin(template, unmapped_labels) def map_values(img, old_vals, new_vals): N = max(img.max(), max(old_vals)) + 1 mapar = np.empty(N, dtype=dtype) mapar[img] = img.astype(dtype) mapar[old_vals] = new_vals out = mapar[img] return out # - Replace the value in image k = np.array(list(mapping.keys())) v = np.array(list(mapping.values())) template = map_values(template, k, v) template[mask] = np.nan img = SpatialImage(template, **attr) return img
[docs] def relabel_from_mapping(image, mapping, clear_unmapped=True, **kwargs): """Relabel the image using a mapping. The mapping is a dictionary indicating the original label as keys and their new labels as values. Parameters ---------- image : timagetk.LabelledImage or timagetk.TissueImage2D or timagetk.TissueImage3D The labelled image to relabel. mapping : dict The dictionary indicating the original label as keys and their new labels as values. clear_unmapped : bool, optional If ``True`` (default), only the mapped labels are kept in the returned image. Other Parameters ---------------- dtype : str The dtype of returned array. Defaults to ``image.dtype``. rtype : Any The image type to return, *e.g.* ``SpatialImage``, ``LabelledImage``. Notes ----- It is possible to get rid of all other label by setting ``clear_unmapped`` to ``True``. Setting `clear_unmapped` to ``False``, there is no guaranty that the new label value is different from those of its neighbors, resulting in a label 'fusion'. Returns ------- timagetk.LabelledImage or timagetk.TissueImage2D or timagetk.TissueImage3D The relabelled image, image type will be the same as input. Examples -------- >>> import numpy as np >>> a = np.array([[1, 1, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]], dtype='uint8') >>> from timagetk import LabelledImage >>> from timagetk.components.labelled_image import relabel_from_mapping >>> im = LabelledImage(a, not_a_label=0) >>> mapping = {6:5, 5:6} >>> relab_im = relabel_from_mapping(im, mapping) >>> print(relab_im.get_array()) [[0 0 0 0 0 0] [0 5 6 0 0 0] [0 0 0 0 0 0] [0 0 0 0 0 0]] >>> relab_im = relabel_from_mapping(im, mapping, clear_unmapped=False) >>> print(relab_im.get_array()) [[1 1 7 7 1 1] [1 5 6 7 3 3] [2 2 1 7 3 3] [1 1 1 4 1 1]] """ from timagetk.components.image import image_class from timagetk.components.image import get_image_attributes Image = kwargs.get("rtype", image_class(image)) # - Get the `image` object attributes: attr = get_image_attributes(image, extra=['filename']) if not clear_unmapped: log.warning( "Relabelling without clearing unmapped labels may result in unwanted 'fusions' or duplicated labels!") # -- Check that mapping keys are known labels, and how many are unknown: labels = image.labels() in_labels = set(mapping.keys()) & set(labels) off_labels = set(mapping.keys()) - in_labels n_in = len(in_labels) # -- Print a summary of this: n_mapped = len(mapping.keys()) s = f"Got an initial list of {n_mapped} mapped labels" if off_labels: n_off = len(off_labels) pc_in = n_in * 100 / n_mapped pc_off = 100 - pc_in s += f", {n_in} ({round(pc_in, 1)}%) are found in the image" s += f" and {n_off} ({round(pc_off, 1)}%) are not!" else: s += ", all are found in the image!" log.info(s) log.info(f"They will be remapped into {len(set(mapping.values()))} unique labels!") # - Get image template = image.get_array().copy() # - Get mask of the missing values if clear_unmapped: unmapped_labels = list(set(image.labels()) - set(mapping)) mask = np.isin(template, unmapped_labels) def map_values(img, old_vals, new_vals): N = max(img.max(), max(old_vals)) + 1 mapar = np.empty(N, dtype=attr['dtype']) mapar[img] = img.astype(attr['dtype']) mapar[old_vals] = new_vals out = mapar[img] return out # - Replace the value in image k = np.array(list(mapping.keys())) v = np.array(list(mapping.values())) template = map_values(template, k, v) if clear_unmapped: template[mask] = image.not_a_label img = Image(template, **attr) log.info(f"The {clean_type(img)} image now has {len(img.labels())} labels!") return img
# - GLOBAL VARIABLES: MISS_LABEL = "The following label{} {} not found in the image: {}" # ''/'s'; 'is'/'are'; labels
[docs] class LabelledImage(SpatialImage): """Class to manipulate labelled image, aka. segmented image."""
[docs] def __new__(cls, image, **kwargs): """LabelledImage construction method. Parameters ---------- image : numpy.ndarray or timagetk.SpatialImage A numpy array or a SpatialImage containing a labelled array. Other Parameters ---------------- origin : list, optional Coordinates of the origin in the image, default: [0, 0] or [0, 0, 0]. voxelsize : list, optional Image voxelsize, default: [1.0, 1.0] or [1.0, 1.0, 1.0]. dtype : str, optional Image type. Defaults to the input `image` type. metadata : dict, optional Dictionary of image metadata. Defaults to an empty dict. Examples -------- >>> import numpy as np >>> from timagetk import SpatialImage >>> from timagetk import LabelledImage >>> from timagetk.array_util import DUMMY_SEG_2D >>> # Example #1 - Construct from a NumPy array: >>> lab_image = LabelledImage(DUMMY_SEG_2D, voxelsize=[0.5,0.5], not_a_label=0) >>> print(lab_image) LabelledImage object with following metadata: - shape: (13, 12) - ndim: 2 - dtype: uint8 - origin: [0, 0] - voxelsize: [0.5, 0.5] - unit: 1e-06 - acquisition_date: None - extent: [6.0, 5.5] - not_a_label: 0 >>> # Example #2 - Construct from a SpatialImage: >>> image_1 = SpatialImage(DUMMY_SEG_2D, voxelsize=[0.5,0.5]) >>> lab_image = LabelledImage(image_1, not_a_label=0) >>> isinstance(lab_image, np.ndarray) # show inheritance True >>> isinstance(lab_image, SpatialImage) # show inheritance True >>> isinstance(lab_image, LabelledImage) True >>> print(lab_image.voxelsize) [0.5, 0.5] >>> print(lab_image.not_a_label) 0 """ log.debug(f'LabelledImage.__new__ got a {clean_type(image)} instance!') log.debug(f'LabelledImage.__new__ got kwargs: {kwargs}.') # - Get variables for LabelledImage instantiation: if isinstance(image, SpatialImage): # -- Can be a SpatialImage or any class inheriting from it: kwargs.update({'axes_order': image.axes_order}) kwargs.update({'origin': image.origin}) kwargs.update({'voxelsize': image.voxelsize}) kwargs.update({'dtype': image.dtype}) kwargs.update({'metadata': image.metadata}) kwargs.update({'not_a_label': getattr(image, 'not_a_label', 0)}) return super(LabelledImage, cls).__new__(cls, image, **kwargs) elif isinstance(image, np.ndarray): # -- Case where constructing from a NumPy array: kwargs.update({'axes_order': kwargs.get('axes_order', None)}) kwargs.update({'origin': kwargs.get('origin', None)}) kwargs.update({'voxelsize': kwargs.get('voxelsize', None)}) kwargs.update({'dtype': kwargs.get('dtype', None)}) kwargs.update({'metadata': kwargs.get('metadata', {})}) kwargs.update({'not_a_label': kwargs.get('not_a_label', 0)}) return super(LabelledImage, cls).__new__(cls, image, **kwargs) else: msg = "Undefined construction method for type '{}'!" raise NotImplementedError(msg.format(type(image)))
[docs] def __init__(self, image, not_a_label=None, **kwargs): """LabelledImage initialisation method. Parameters ---------- image : numpy.ndarray or timagetk.SpatialImage An array or ``SpatialImage`` containing a labelled array. not_a_label : int, optional If given define the value that is not a label. Can be set later with the `not_a_label` property. """ # - In case a LabelledImage is constructed from a LabelledImage, get the attributes values: if isinstance(image, LabelledImage): attr_list = ["not_a_label"] attr_dict = get_attributes(image, attr_list) class_name = get_class_name(image) msg = "Overriding optional keyword arguments '{}' ({}) with defined attribute ({}) in given '{}'!" # -- Check necessity to override 'origin' with attribute value: if attr_dict['not_a_label'] is not None: if not_a_label is not None and not_a_label != attr_dict['not_a_label']: log.info(msg.format('not_a_label', not_a_label, attr_dict['not_a_label'], class_name)) not_a_label = attr_dict['not_a_label'] # -- Check 'class' definition in 'timagetk' metadata: # try: # md_class = image.metadata['timagetk']['class'] # except KeyError: # warn_msg = "Initializing from a 'LabelledImage' without 'class' entry in 'timagetk' metadata!" # log.warning(warn_msg) # self.metadata.update({'timagetk': {'class': 'LabelledImage'}}) # else: # if md_class != 'LabelledImage': # warn_msg = "Initializing from a 'LabelledImage' without correct 'class' definition in 'timagetk' metadata!" # warn_msg += "\n\{'timagetk': \{'class': {}\}\}".format(md_class) # log.warning(warn_msg) # self.metadata.update({'timagetk': {'class': 'LabelledImage'}}) else: # - Adding class to metadata: self.metadata.update({'timagetk': {'class': 'LabelledImage'}}) # - Initializing EMPTY hidden attributes: # -- Property hidden attributes: self._not_a_label = None # id referring to the absence of label # -- Topological element of order 3 are called 'labels': self._labels = None # list of labels self._label_bboxes = {} # dict of label bounding-boxes self._neighbors = {} # unfiltered neighborhood label-dict {vid_i: neighbors(vid_i)} # -- Topological element of order 2 are called 'surfels': self._surfels = None # list of surfels self._surfel_bboxes = {} # dict of surfel bounding-boxes self._surfel_voxels = {} # dict of surfel voxel coordinates # -- Topological element of order 1 are called 'linels': self._linels = None # list of linels self._linel_bboxes = {} # dict of linel bounding-boxes self._linel_voxels = {} # dict of linel voxel coordinates # -- Topological element of order 0 are called 'pointels': self._pointels = None # list of pointels self._pointel_bboxes = {} # dict of pointel bounding-boxes self._pointel_voxels = {} # dict of pointel voxel coordinates # - Initialise object property and most used hidden attributes: # -- Define the "not_a_label" value, if any (can be None): self.not_a_label = not_a_label # -- Get the list of labels found in the image: self.labels() n_lab = len(self.labels()) if kwargs.get('verbose', False): log.info(f"Initialized `LabelledImage` object with {n_lab} labels!") if n_lab <= 15: log.info(f"Found list of labels: {self.labels()}") # Used to cache the `vtCellProperties` instance: self._vt_ppty = None
[docs] def __str__(self): """Method called when printing the object.""" msg = "LabelledImage object with following metadata:\n" md = self.metadata msg += '\n'.join([' - {}: {}'.format(k, v) for k, v in md.items()]) return msg
@property def not_a_label(self): """Get the value associated to not a label state. This is used as "unknown label" or "erase value". Returns ------- int The value defined as not a label. Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a) >>> im.labels() [1, 2, 3, 4, 5, 6, 7] >>> im.not_a_label WARNING : no value defined for the 'not a label' id! >>> im = LabelledImage(a, not_a_label=1) >>> im.labels() [2, 3, 4, 5, 6, 7] >>> im.not_a_label 1 """ if self._not_a_label is None: log.warning("No value defined for the 'not a label' property!") return self._not_a_label @not_a_label.setter def not_a_label(self, value): """Set the value associated to not a label state. This is used as "unknown label" or "erase value". Parameters ---------- value : int The value defined as not a label. Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a) >>> im.labels() [1, 2, 3, 4, 5, 6, 7] >>> im.not_a_label WARNING : no value defined for the 'not a label' id! >>> im.not_a_label = 1 >>> im.labels() [2, 3, 4, 5, 6, 7] >>> im.not_a_label 1 """ if not isinstance(value, int) and value is not None: log.info("Provided value '{}' is not an integer!".format(value)) return else: self._not_a_label = value self.metadata = {'not_a_label': self.not_a_label} def _defined_not_a_label(self): """Tests if '_not_a_label' attribute is defined, if not raise a ValueError.""" try: assert self._not_a_label is not None except AssertionError: msg = "Attribute 'not_a_label' is not defined (None)." msg += "Please set it (integer) before calling this function!" raise ValueError(msg) return
[docs] def get_slice(self, slice_id, axis='z'): """Return a LabelledImage with only one slice for given axis. Parameters ---------- slice_id : int Slice to return. axis : int or str in {'x', 'y', 'z'}, optional Axis to use for slicing, default is 'z'. Returns ------- timagetk.LabelledImage 2D LabelledImage with only the required slice. Raises ------ ValueError If the image is not 3D and ``axis='z'``. If ``slice_id`` does not exist, *i.e.* should satisfy: ``0 < slice_id < max(len(axis))``. Examples -------- >>> from timagetk.array_util import dummy_labelled_image_3D >>> # Initialize a dummy 3D LabelledImage with a ZYX shape of 5x13x12: >>> img = dummy_labelled_image_3D([1., 0.5, 0.5]) >>> print(img.axes_order) {'Z': 0, 'Y': 1, 'X': 2} >>> print(img) # print a summary of the dummy labelled image LabelledImage object with following metadata: - shape: (5, 13, 12) - ndim: 3 - dtype: uint8 - origin: [0, 0, 0] - voxelsize: [1.0, 0.5, 0.5] - extent: [4.0, 6.0, 5.5] - not_a_label: 0 >>> # Taking an existing z-slice from a 3D image works fine: >>> img_z = img.get_slice(1, 'z') >>> print(img_z.axes_order) {'Y': 0, 'X': 1} >>> print(img_z) LabelledImage object with following metadata: - not_a_label: 0 - shape: (13, 12) - ndim: 2 - dtype: uint8 - origin: [0, 0] - voxelsize: [0.5, 0.5] - extent: [6.0, 5.5] >>> # Taking an existing x-slice from a 3D image works fine: >>> img_x = img.get_slice(3, 'x') >>> print(img_x.axes_order) {'Z': 0, 'Y': 1} >>> # Down-sampling x-axis of a 3D image: >>> nx = img.get_shape('x') >>> img_ds_x2 = img.get_slice(range(0, nx, 2), 'x') >>> print(img_ds_x2.axes_order) {'Z': 0, 'Y': 1, 'X': 2} >>> print(img_ds_x2) LabelledImage object with following metadata: - shape: (5, 13, 6) - ndim: 3 - dtype: uint8 - origin: [0, 0, 0] - unit: 1e-06 - acquisition_date: None - not_a_label: 0 - voxelsize: [1.0, 0.5, 1.0] - extent: [4.0, 6.0, 5.0] >>> # Taking an NON-existing z-slice from a 3D image raises an error: >>> img.get_slice(50, 'z') >>> # Taking a z-slice from a 2D image raises an error: >>> img_z.get_slice(5, 'z') """ return LabelledImage(SpatialImage.get_slice(self, slice_id, axis=axis), not_a_label=self.not_a_label)
[docs] def get_region(self, region): """Extract a region using list of start & stop indexes. There should be two values per dimension in `region`, *e.g.* ``region=[5, 8, 5, 8]`` for a 2D image. If the image is 3D and, in one dimension the start and stop indexes only differ by one (one layer of voxels), the returned image will be transformed to 2D! Parameters ---------- region : list Indexes as list of integers, *e.g.* ``[y-start, y-stop, x-start, x-stop]`` for a 2D image. Returns ------- timagetk.LabelledImage Output image. Raises ------ TypeError If the given `region` is not a list. ValueError If the number of indexes in `region` is wrong, should be twice the image dimensionality. If the `region` coordinates are not within the array boundaries. Example ------- >>> from timagetk import LabelledImage >>> from timagetk.array_util import dummy_labelled_image_2D >>> # Initialize a dummy (uint8) 2D LabelledImage with a YX shape of 13x12: >>> img = dummy_labelled_image_2D([0.5, 0.5]) >>> region = [1, 5, 1, 5] # y-start, y-stop, x-start, x-stop >>> out_img = img.get_region(region) >>> isinstance(out_img, LabelledImage) True >>> out_img LabelledImage([[2, 4, 4, 4], [2, 2, 4, 4], [2, 2, 2, 4], [2, 2, 2, 3]], dtype=uint8) >>> from timagetk.array_util import dummy_labelled_image_3D >>> # Initialize a dummy (uint8) 3D LabelledImage with a ZYX shape of 5x13x12: >>> img = dummy_labelled_image_3D([1.0, 0.5, 0.5]) >>> region = [2, 3, 1, 5, 1, 5] # z-start, z-stop, y-start, y-stop, x-start, x-stop >>> out_img = img.get_region(region) >>> isinstance(out_img, LabelledImage) True >>> out_img.is2D() True >>> out_img LabelledImage([[2, 4, 4, 4], [2, 2, 4, 4], [2, 2, 2, 4], [2, 2, 2, 3]], dtype=uint8) """ from timagetk.components.image import get_image_attributes attrs = get_image_attributes(self) return LabelledImage(SpatialImage.get_region(self, region), **attrs)
[docs] def transpose(self, *axes): """Permute image axes to given order, reverse by default. Parameters ---------- axes : list of int or list of str, optional By default, reverse the dimensions, otherwise permute the axes according to the values given. Returns ------- timagetk.LabelledImage The image with permuted axes. Examples -------- >>> from timagetk.array_util import dummy_labelled_image_3D >>> # -- Transpose works with 3D images: >>> # Initialize a dummy (uint8) 3D LabelledImage with a ZYX shape of 5x13x12: >>> img = dummy_labelled_image_3D([1.0, 0.5, 0.5]) >>> img_t = img.transpose() >>> # Transpose update the shape attribute of the image (here reversed): >>> print(img_t.shape) (12, 13, 5) >>> # Transpose update the voxelsize attribute of the image (here reversed): >>> print(img_t.voxelsize) [0.5, 0.5, 1.0] >>> # Transpose update the metadata dictionary of the image: >>> print(img_t.metadata) {'shape': (12, 13, 5), 'ndim': 3, 'dtype': dtype('uint8'), 'unit': 1e-06, 'acquisition_date': None, 'not_a_label': 0, 'origin': [0, 0, 0], 'voxelsize': [0.5, 0.5, 1.0], 'extent': [5.5, 6.0, 4.0]} >>> # -- Transpose accept axe names as input: >>> img_t = img.transpose('xyz') >>> print(img_t.shape) (12, 13, 5) >>> img_t = img.transpose('x', 'y', 'z') >>> print(img_t.shape) (5, 4, 3) >>> from timagetk.array_util import dummy_labelled_image_2D >>> # -- Transpose works with 2D images: >>> # Initialize a dummy (uint8) 2D LabelledImage with a YX shape of 13x12: >>> img = dummy_labelled_image_2D([0.5, 0.5]) >>> img_t = img.transpose() >>> # Transpose update the shape attribute of the image (here reversed): >>> print(img_t.shape) (5, 4) """ from timagetk.components.image import get_image_attributes attrs = get_image_attributes(self) return LabelledImage(SpatialImage.transpose(self, *axes), **attrs)
[docs] def invert_axis(self, axis): """Revert given axis. Parameters ---------- axis : {'x', 'y', 'z'} Axis to invert, can be either 'x', 'y' or 'z' (if 3D). Returns ------- timagetk.LabelledImage Image with reverted array for selected axis. Raises ------ ValueError If given ``axis`` is not in {'x', 'y', 'z'} for 3D images or not in {'x', 'y'} for 2D images. Example ------- >>> from timagetk.array_util import dummy_labelled_image_3D >>> # Initialize a dummy (uint8) 3D LabelledImage with a ZYX shape of 5x13x12: >>> img = dummy_labelled_image_3D([1.0, 0.5, 0.5]) >>> print(img.get_array()) >>> inv_img = img.invert_axis(axis='z') >>> print(inv_img.get_array()) """ from timagetk.components.image import get_image_attributes attrs = get_image_attributes(self) return LabelledImage(SpatialImage.invert_axis(self, axis), **attrs)
[docs] def topological_elements(self, element_order=None, verbose=True): """Extract the topological elements coordinates of a labelled image. Parameters ---------- element_order : int or list of int, optional List of dimensional order of the elements to return, should be in [2, 1, 0]. Defaults to ``None``, returns a dictionary with every order of topological elements. Returns ------- dict Dictionary with topological elements order as key, each containing dictionaries of n-uplets as keys and coordinates array as values. Notes ----- A "surfel" is a dimension 2 element with a neighborhood size equal to 2. A "linel" is a dimension 1 element with a neighborhood size equal to 3. A "pointel" is a dimension 0 element with a neighborhood size equal to 4. The order of the labels in the tuple defining the key is irrelevant, *i.e.* coordinates of surfel ``(2, 5)`` is the same as the one of ``(5, 2)``. Examples -------- >>> from timagetk.array_util import dummy_labelled_image_3D >>> im = dummy_labelled_image_3D() >>> topo = im.topological_elements() >>> topo[0] # access "pointel" positions {(1, 2, 3, 4): array([[0.5, 3.5, 3.5]]), (1, 2, 3, 7): array([[0.5, 7.5, 3.5]]), (1, 3, 4, 5): array([[0.5, 3.5, 8.5]]), (1, 3, 5, 6): array([[0.5, 7.5, 9.5]]), (1, 3, 6, 7): array([[ 0.5, 10.5, 6.5]])} >>> from timagetk.array_util import dummy_labelled_image_2D >>> im = dummy_labelled_image_2D() >>> topo = im.topological_elements() >>> topo[1] # access "pointel" positions """ import copy as cp if isinstance(element_order, int): element_order = [element_order] if element_order is None: if self.is2D(): element_order = [2, 1] else: element_order = list(range(3)) if 0 in element_order and self.is2D(): log.error("There is no elements of order 0 in a 2D image!") element_order.remove(0) if element_order == []: return None # - List missing order of topological element dictionary elem_order = cp.copy(element_order) if element_order is not None: # remove potential duplicates: element_order = list(set(element_order)) # remove already computed elements order: if 2 in element_order and self._surfel_voxels != {}: elem_order.remove(2) if 1 in element_order and self._linel_voxels != {}: elem_order.remove(1) if 0 in element_order and self._pointel_voxels != {}: elem_order.remove(0) # - If element are missing, compute them and save them to attributes: if elem_order != []: if self.is2D(): topo_elem = topological_elements_extraction2D(self, elem_order, verbose=verbose) else: topo_elem = topological_elements_extraction3D(self, elem_order, verbose=verbose) # - Get the surfel coordinates: if 2 in topo_elem: self._surfel_voxels = topo_elem[2] self._surfels = set(self._surfel_voxels.keys()) # - Get the linel coordinates: if 1 in topo_elem: self._linel_voxels = topo_elem[1] self._linels = set(self._linel_voxels.keys()) # - Get the pointel coordinates: if 0 in topo_elem: self._pointel_voxels = topo_elem[0] self._pointels = set(self._pointel_voxels.keys()) else: topo_elem = {} # - Get required but already computed dict of topological elements: if 2 in element_order and 2 not in topo_elem: topo_elem[2] = self._surfel_voxels if 1 in element_order and 1 not in topo_elem: topo_elem[1] = self._linel_voxels if 0 in element_order and 0 not in topo_elem: topo_elem[0] = self._pointel_voxels return topo_elem
# -------------------------------------------------------------------------- # LABEL based methods: # --------------------------------------------------------------------------
[docs] def labels(self, labels=None): """Get the list of labels found in the image, or filter given labels list by those. Parameters ---------- labels : int or list of int, optional If an integer or a list of integers, make sure they are in the image. Returns ------- list List of labels found in the image. Notes ----- If defined, the attribute ``self.not_a_label`` is excluded from the returned list of labels. Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a) >>> im.labels() [1, 2, 3, 4, 5, 6, 7] >>> im = LabelledImage(a, not_a_label=1) >>> im.labels() [2, 3, 4, 5, 6, 7] >>> im.labels(7) [7] """ # - If the hidden label attribute is None, list all labels in the array: if self._labels is None: self._labels = list(map(int, np.unique(self.get_array()))) # - Transform length-1 list to integers if isinstance(labels, list) and len(labels) == 1: labels = labels[0] # - Remove value attributed to 'not_a_label': unwanted_set = {self._not_a_label} label_set = set(self._labels) - unwanted_set # If an integer is given as label, return it if in the set of valid label else returns None if isinstance(labels, int): if not self.is_label_in_image(labels): log.critical(f"Requested label {labels} was not found in the image!") labels = None return [labels] # If a list of label is given, use set union to returns the valid ones if labels: label_set = list(label_set & set(labels)) # Map them as integers before returning them for further type checking... return list(map(int, label_set))
[docs] def nb_labels(self): """Return the number of labels found in the labelled image. Returns ------- int The number of labels found in the labelled image. Examples -------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a) >>> im.nb_labels() 7 >>> im = LabelledImage(a, not_a_label=1) >>> im.nb_labels() 6 """ return len(self.labels())
[docs] def is_label_in_image(self, label): """Test wheter the given label is in the image or not. Parameters ---------- label : int Value that should be present in the labelled image. Returns ------- bool ``True`` if the label is found in the image, else ``False``. Examples -------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a) >>> im.is_label_in_image(7) True >>> im.is_label_in_image(10) False """ return label in self.get_array()
[docs] def boundingbox(self, labels=None, real=False): """Return the bounding-box of a single or a list of labels. Parameters ---------- labels : int or list of int, optional If ``None`` (default), returns values for all known labels. If an integer or a list of integers, make sure they are in `self.labels()`. real : bool, optional If ``False`` (default), return the bounding-boxes in voxel units, else in real units. Returns ------- dict Label indexed bounding-boxes dictionary: ``{l: bounding-box(l)}`` for ``l`` in `labels`. Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a) >>> im.boundingbox(7) {7: (slice(0, 3, None), slice(2, 4, None))} >>> im.boundingbox([7, 2]) {2: (slice(0, 3, None), slice(0, 2, None)), 7: (slice(0, 3, None), slice(2, 4, None))} >>> im.boundingbox() {1: (slice(0, 4, None), slice(0, 6, None)), 2: (slice(0, 3, None), slice(0, 2, None)), 3: (slice(1, 3, None), slice(4, 6, None)), 4: (slice(3, 4, None), slice(3, 4, None)), 5: (slice(1, 2, None), slice(2, 3, None)), 6: (slice(1, 2, None), slice(1, 2, None)), 7: (slice(0, 3, None), slice(2, 4, None))} """ labels = self.labels(labels) if labels is None: return {} # - Starts with integer case since it is the easiest: if len(labels) == 1: labels = labels[0] try: assert labels in self._label_bboxes except AssertionError: image = self.get_array() bbox = nd.find_objects(image == labels, max_label=1)[0] self._label_bboxes[labels] = bbox return {labels: self._label_bboxes[labels]} # - Create a dict of bounding-boxes using 'scipy.ndimage.find_objects': known_bbox = [l in self._label_bboxes for l in labels] image = self.get_array() if self._label_bboxes is None or not all(known_bbox): max_lab = max(labels) log.info(f"Searching the bounding-box{'es' if max_lab > 1 else ''} of {max_lab} labels...") bbox = nd.find_objects(image, max_label=max_lab) # NB: scipy.ndimage.find_objects start at 1 (and python index at 0), hence to access i-th element, we have to use (i-1)-th index! self._label_bboxes = {n: bbox[n - 1] for n in range(1, max_lab + 1)} # - Filter returned bounding-boxes to the (cleaned) given list of labels bboxes = {l: self._label_bboxes[l] for l in labels} if real: vxs = self.voxelsize bboxes = {l: real_indices(bbox, vxs) for l, bbox in bboxes.items()} return bboxes
[docs] def label_coordinates(self, labels=None, real=True, axes_order=None): """Return the coordinates of each voxels representing a label. Parameters ---------- labels : int or list of int, optional If ``None`` (default), returns values for all known labels. If an integer or a list of integers, make sure they are in `self.labels()`. real : bool, optional If ``True`` (default), returns the coordinates in real world units, else in voxel units. axes_order : str, optional Order of the axes or dimension to use for returned coordinates. Returns ------- dict Label indexed coordinates dictionary: ``{l: coordinates(l)}`` for ``l`` in `labels`. Example ------- >>> from timagetk import LabelledImage >>> from timagetk.array_util import dummy_labelled_image_3D >>> im = LabelledImage(dummy_labelled_image_3D()) >>> im.label_coordinates(7) >>> im.label_coordinates([7, 2]) """ from timagetk.components.spatial_image import DEFAULT_AXES_2D from timagetk.components.spatial_image import DEFAULT_AXES_3D if axes_order is None: if self.is2D(): axes_order = DEFAULT_AXES_2D[::-1] else: axes_order = DEFAULT_AXES_3D[::-1] labels = self.labels(labels) # returns a list if labels is None: return {} if len(labels) == 1: labels = labels[0] coords = np.array(np.where(self == labels)).T if real: coords = coords * self.voxelsize coords = {labels: coords} else: # - Check we have all necessary bounding-boxes... bboxes = self.boundingbox(labels, real=False) log.info(f"Computing {len(labels)} labels coordinates:") coords = {} for label in tqdm(labels, unit='label'): try: crop = bboxes[label] crop_im = self.get_array()[crop] lcoords = np.array(np.where(crop_im == label)).T lcoords = np.array([lcoords[:, ax] + sl.start for ax, sl in enumerate(crop)]).T except ValueError: lcoords = np.array(np.where(self.get_array() == label)).T coords[label] = lcoords if real: coords = {l: lc * self.voxelsize for l, lc in coords.items()} # Check required axes order and re-order coordinates if necessary: idx_axes = {idx: ax for ax, idx in self.axes_order_dict.items()} if axes_order.lower() != ''.join([idx_axes[idx] for idx in range(self.ndim)]).lower(): coords = {l: c[:, self._new_order(axes_order)] for l, c in coords.items()} return coords
[docs] def label_array(self, label, dilation=0): """Return the array of the cropped labelled image by the label's bounding-box. Parameters ---------- label : int Label to use to crop out the labelled image. dilation : int, optional If defined (default is ``0``, no dilation), use this value as a dilation factor (in every direction) to be applied to the label bounding-box. Should be a strictly positive integer, or dilation will not be applied. Returns ------- timagetk.LabelledImage Labelled image cropped around the label bounding-box. Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a, not_a_label=0) >>> im.label_array(7) LabelledImage([[7, 7], [5, 7], [1, 7]]) >>> im.label_array(7, dilation=1) LabelledImage([[2, 7, 7, 1], [6, 5, 7, 3], [2, 1, 7, 3], [1, 1, 4, 1]]) """ # - Get the slice for given label: label_slice = self.boundingbox(label)[label] # - Create the cropped image when possible: if label_slice is None: # crop_img = self.get_array() # past behaviour... # not sure if it's right to return the whole array when label is not found... # indeed, if called by high cost computational methods on array it might do more arms than good! crop_img = None else: if dilation > 0: label_slice = dilation_by(label_slice, dilation) crop_img = self[label_slice].get_array() return LabelledImage(crop_img, origin=self.origin, voxelsize=self.voxelsize, metadata=self.metadata, not_a_label=self.not_a_label, axes_order=self.axes_order)
def _neighbors_with_mask(self, label): """Sub-function called when only one label is given to ``self.neighbors()``. Parameters ---------- label : int Compute the neighborhood for this label. Returns ------- list List of neighbors for given `label`. """ # - Compute the neighbors and update the unfiltered neighbors' dict: if label not in self._neighbors: crop_img = self.label_array(label, dilation=1) self._neighbors[label] = label_neighbors(crop_img, label) return self._neighbors[label] def _neighborhood_with_mask(self, labels): """Sub-function called when a list of labels is given to ``self.neighbors()``. Parameters ---------- label : list of int Compute the neighborhood for these labels. Returns ------- dict Label indexed neighborhood dictionary: ``{l: neighbors(l)}`` for ``l`` in `labels`. """ # - Check we have all necessary bounding-boxes... self.boundingbox(labels) # - Try a shortcut: 'self._neighbors' might have all required 'labels'... miss_labels = [l for l in labels if l not in self._neighbors] # - Compute the neighborhood for labels without (unfiltered) neighbors list: if miss_labels: log.info(f"Computing the neighbors list for {len(miss_labels)} labels...") # TODO: use MPIRE to speed-up with parallelization? for label in tqdm(miss_labels, unit='label'): # compute the neighborhood for the given label self._neighbors[label] = label_neighbors(self.label_array(label, dilation=1), label) neighborhood = {l: self._neighbors[l] for l in labels} return neighborhood
[docs] def neighbors(self, labels=None): """Return the neighbors list of labels. Parameters ---------- labels : None or int or list of int, optional If ``None`` (default), returns values for all known labels. If an integer or a list of integers, make sure they are in `self.labels()`. Returns ------- dict Label indexed neighborhood dictionary: ``{l: neighbors(l)}`` for ``l`` in `labels`. Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 2, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a) >>> im.neighbors(7) [1, 2, 3, 4, 5] >>> im.neighbors([7, 2]) {7: [1, 2, 3, 4, 5], 2: [1, 6, 7] } >>> im.neighbors() {1: [2, 3, 4, 5, 6, 7], 2: [1, 6, 7], 3: [1, 7], 4: [1, 7], 5: [1, 6, 7], 6: [1, 2, 5], 7: [1, 2, 3, 4, 5] } >>> im = LabelledImage(a, not_a_label=1) >>> im.neighbors(1) [2, 3, 4, 7] >>> im.neighbors([1, 2]) {1: [2, 3, 4, 7], 2: [1, 5, 7]} """ if labels is None: labels = self.labels() # - Neighborhood computing: if isinstance(labels, int): try: assert self.is_label_in_image(labels) except AssertionError: raise ValueError(MISS_LABEL.format('', 'is', labels)) return {labels: self._neighbors_with_mask(labels)} else: # list case: try: assert labels != [] except AssertionError: raise ValueError(MISS_LABEL.format('s', 'are', labels)) if self.is3D(): # Use `vt.vtCellProperties()` if self._vt_ppty is None: from vt import vtCellProperties self._vt_ppty = vtCellProperties(self.to_vtimage()) from timagetk.third_party.vt_features import _ppty_get_neighbors return _ppty_get_neighbors(self._vt_ppty, labels) else: return self._neighborhood_with_mask(labels)
# -------------------------------------------------------------------------- # SURFEL based methods: # --------------------------------------------------------------------------
[docs] def surfels(self, surfel_ids=None): """Get the list of surfels found in the image, or filter the list with those found in the image. Parameters ---------- surfel_ids : len-2 tuple or list(tuple), optional If given, filter the returned list of surfels. Else returns the list of all surfels found in the image (default). Returns ------- list of tuples List of surfel ids, expressed as len-2 tuples of integers Example ------- >>> from timagetk.array_util import dummy_labelled_image_3D >>> im = dummy_labelled_image_3D() >>> all_surfels = im.surfels() >>> print(all_surfels) [(1, 2), (2, 7), (1, 3), (6, 7), (4, 5), (5, 6), (1, 4), (1, 5), (1, 6), (2, 3), (3, 6), (1, 7), (3, 7), (3, 4), (2, 4), (3, 5)] """ err_msg = "Input 'surfel_ids' should be a list of length-2 tuples!" # - If the hidden label attribute is None, list all labels in the array: if self._surfels is None: self.topological_elements(element_order=2) if surfel_ids is None: surfel_ids = self._surfels elif isinstance(surfel_ids, tuple) and len(surfel_ids) == 2: surfel_ids = [surfel_ids] elif isinstance(surfel_ids, (list, set)): try: assert all(isinstance(f, tuple) and len(f) == 2 for f in surfel_ids) except AssertionError: raise TypeError(err_msg) else: raise TypeError(err_msg) # need to reorder given list of 'surfels', might not be label sorted: surfel_ids = set(map(stuple, surfel_ids)) return list(self._surfels & surfel_ids)
[docs] def surfel_coordinates(self, surfel_ids=None, real=False, axes_order=None): """Get a dictionary of surfel coordinates. Parameters ---------- surfel_ids : len-2 tuple or list(tuple), optional If given, filter the returned dictionary of surfel coordinates. Else returns it for all surfels found in the image (default). real : bool, optional If ``True`` (default), returns the coordinates in real world units, else in voxel units. axes_order : str, optional Order of the axes or dimension to use for returned coordinates. Returns ------- dict Surfel sorted dictionary of coordinates: ``{(i, j): coordinates(i, j)}`` for ``(i, j)`` in `surfel_ids`. Examples -------- >>> from timagetk.array_util import dummy_labelled_image_3D >>> im = dummy_labelled_image_3D() >>> surfel_coords = im.surfel_coordinates() >>> surfel_coords[(6, 7)] array([[ 6.5, 11. , 1. ], [ 6.5, 12. , 1. ], [ 6.5, 11. , 2. ], [ 6.5, 12. , 2. ], [ 6.5, 11. , 3. ], [ 6.5, 12. , 3. ], [ 6.5, 11. , 4. ], [ 6.5, 12. , 4. ]]) """ from timagetk.components.spatial_image import DEFAULT_AXES_2D from timagetk.components.spatial_image import DEFAULT_AXES_3D if axes_order is None: if self.is2D(): axes_order = DEFAULT_AXES_2D[::-1] else: axes_order = DEFAULT_AXES_3D[::-1] surfels_list = self.surfels(surfel_ids) coords = {f: self._surfel_voxels[f] for f in surfels_list} if real: coords = {f: np.multiply(c, self.voxelsize) for f, c in coords.items()} # Check required axes order and re-order coordinates if necessary: idx_axes = {idx: ax for ax, idx in self.axes_order_dict.items()} if axes_order.lower() != ''.join([idx_axes[idx] for idx in range(self.ndim)]).lower(): coords = {l: c[:, self._new_order(axes_order)] for l, c in coords.items()} return coords
# -------------------------------------------------------------------------- # LINEL based methods: # --------------------------------------------------------------------------
[docs] def linels(self, linel_ids=None): """Get the list of linels found in the image, or filter the list with those found in the image. Parameters ---------- linel_ids : len-3 tuple or list(tuple), optional If given, filter the returned list of linels. Else returns the list of all linels found in the image (default). Returns ------- list of tuples List of linel ids, expressed as len-3 tuples of integers. Example ------- >>> from timagetk.array_util import dummy_labelled_image_3D >>> im = dummy_labelled_image_3D() >>> im.linels() [(1, 3, 7), (1, 3, 6), (1, 2, 7), (1, 2, 3), (1, 3, 5), (1, 4, 5), (1, 5, 6), (1, 6, 7), (2, 3, 7), (3, 5, 6), (1, 2, 4), (1, 3, 4), (3, 4, 5), (2, 3, 4), (3, 6, 7)] """ err_msg = "Input 'linel_ids' should be a list of length-3 tuples!" # - If the hidden label attribute is None, list all labels in the array: if self._linels is None: self.topological_elements(element_order=1) if linel_ids is None: linel_ids = self._linels elif isinstance(linel_ids, tuple) and len(linel_ids) == 3: linel_ids = [linel_ids] elif isinstance(linel_ids, (list, set)): try: assert all(isinstance(e, tuple) and len(e) == 3 for e in linel_ids) except AssertionError: raise TypeError(err_msg) else: raise TypeError(err_msg) # need to reorder given list of 'linels', might not be label sorted: linel_ids = set(map(stuple, linel_ids)) return list(self._linels & linel_ids)
[docs] def linel_coordinates(self, linel_ids=None, real=False, axes_order=None): """Get a dictionary of linel coordinates. Parameters ---------- linel_ids : len-3 tuple or list(tuple), optional If given, filter the returned dictionary of linel coordinates. Else returns it for all linels found in the image (default). real : bool, optional If ``True`` (default), returns the coordinates in real world units, else in voxel units. axes_order : str, optional Order of the axes or dimension to use for returned coordinates. By default, use the same order as the image axes order. Returns ------- dict Linel sorted dictionary of coordinates: ``{(i, j, k): coordinates(i, j, k)}`` for ``(i, j, k)`` in `linel_ids`. Examples -------- >>> from timagetk.array_util import dummy_labelled_image_3D >>> im = dummy_labelled_image_3D() >>> im.linel_coordinates([(1, 3, 7)], True) {(1, 3, 7): array([[ 4.5, 8. , 0.5], [ 4.5, 9. , 0.5], [ 5.5, 10. , 0.5], [ 4. , 7.5, 0.5], [ 5. , 9.5, 0.5], [ 6. , 10.5, 0.5]])} """ # TODO: change to use the same order as the image axes order by default! # TODO: prpagate `axes_order` kwargs to `timagetk.features.cell_edges.median_coordinate` & `timagetk.features.cell_edges.Edges3D.geometric_median` from timagetk.components.spatial_image import DEFAULT_AXES_2D from timagetk.components.spatial_image import DEFAULT_AXES_3D if axes_order is None: if self.is2D(): axes_order = DEFAULT_AXES_2D[::-1] else: axes_order = DEFAULT_AXES_3D[::-1] linels_list = self.linels(linel_ids) coords = {e: self._linel_voxels[e] for e in linels_list} if real: coords = {e: c * self.voxelsize for e, c in coords.items()} # Check required axes order and re-order coordinates if necessary: idx_axes = {idx: ax for ax, idx in self.axes_order_dict.items()} if axes_order.lower() != ''.join([idx_axes[idx] for idx in range(self.ndim)]).lower(): coords = {l: c[:, self._new_order(axes_order)] for l, c in coords.items()} return coords
# -------------------------------------------------------------------------- # POINTEL based methods: # --------------------------------------------------------------------------
[docs] def pointels(self, pointel_ids=None): """Get the list of pointels found in the image, or filter the list with those found in the image. Parameters ---------- pointel_ids : len-4 tuple or list(tuple), optional If given, filter the returned list of pointels. Else returns the list of all pointels found in the image (default). Returns ------- list of tuples List of pointel ids, expressed as len-4 tuples of integers. Example ------- >>> from timagetk.array_util import dummy_labelled_image_3D >>> im = dummy_labelled_image_3D() >>> im.pointels() [(1, 3, 6, 7), (1, 3, 5, 6), (1, 2, 3, 4), (1, 2, 3, 7), (1, 3, 4, 5)] """ err_msg = "Input 'pointel_ids' should be a list of length-4 tuples!" # - If the hidden label attribute is None, list all labels in the array: if self._pointels is None: self.topological_elements(element_order=0) if pointel_ids is None: pointel_ids = self._pointels elif isinstance(pointel_ids, tuple) and len(pointel_ids) == 4: pointel_ids = [pointel_ids] elif isinstance(pointel_ids, (list, set)): try: assert all(isinstance(n, tuple) and len(n) == 4 for n in pointel_ids) except AssertionError: raise TypeError(err_msg) else: raise TypeError(err_msg) # need to reorder given list of 'pointels', might not be label-sorted: pointel_ids = set(map(stuple, pointel_ids)) return list(self._pointels & pointel_ids)
[docs] def pointel_coordinates(self, pointel_ids=None, real=False, axes_order='xyz'): """Get a dictionary of pointel coordinates. Parameters ---------- pointel_ids : len-4 tuple or list(tuple), optional If given, filter the returned dictionary of pointel coordinates. Else returns it for all pointels found in the image (default). real : bool, optional If ``True`` (default), returns the coordinates in real world units, else in voxel units. axes_order : str, optional Order of the axes or dimension to use for returned coordinates. Returns ------- dict Pointel sorted dictionary of coordinates: ``{(i, j, k, l): coordinates(i, j, k, l)}`` for ``(i, j, k, l)`` in `pointel_ids`. Examples -------- >>> from timagetk.array_util import dummy_labelled_image_3D >>> im = dummy_labelled_image_3D() >>> im.pointel_coordinates() {(1, 3, 6, 7): array([[6.5, 10.5, 0.5]]), (1, 3, 5, 6): array([[9.5, 7.5, 0.5]]), (1, 2, 3, 4): array([[3.5, 3.5, 0.5]]), (1, 2, 3, 7): array([[3.5, 7.5, 0.5]]), (1, 3, 4, 5): array([[8.5, 3.5, 0.5]])} """ pointels_list = self.pointels(pointel_ids) coords = {n: self._pointel_voxels[n] for n in pointels_list} if real: coords = {n: np.multiply(c, self.voxelsize) for n, c in coords.items()} # Check required axes order and re-order coordinates if necessary: idx_axes = {idx: ax for ax, idx in self.axes_order_dict.items()} if axes_order.lower() != ''.join([idx_axes[idx] for idx in range(self.ndim)]).lower(): coords = {l: c[:, self._new_order(axes_order)] for l, c in coords.items()} return coords
# -------------------------------------------------------------------------- # LabelledImage edition functions: # --------------------------------------------------------------------------
[docs] def get_image_with_labels(self, labels): """Return a copy of the labelled image with only the selected labels. Parameters ---------- labels : int or list of int A list of labels to keep in the returned copy. Returns ------- LabelledImage Labelled image with only the selected labels. Notes ----- Require the definition of the `not_a_label` property! Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a, not_a_label=0) >>> im.get_image_with_labels([2, 5]) LabelledImage([[0, 2, 0, 0, 0, 0], [0, 0, 5, 0, 0, 0], [2, 2, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]]) """ self._defined_not_a_label() all_labels = self.labels() labels = self.labels(labels) off_labels = list(set(all_labels) - set(labels)) if len(off_labels) == 0: log.warning("You selected ALL labels!") return self if len(labels) == 0: log.warning("You selected NO label!") return None if len(labels) < len(off_labels): template_im = image_with_labels(self, labels) else: template_im = image_without_labels(self, off_labels) return template_im
[docs] def get_image_without_labels(self, labels): """Return a copy of the labelled image without the selected labels. Parameters ---------- labels : int or list of int Label or list of labels to remove in the returned copy. Returns ------- LabelledImage Labelled image without the selected labels. Notes ----- Require the definition of the `not_a_label` property! Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a, not_a_label=0) >>> im.get_image_without_labels([2, 5]) LabelledImage([[1, 0, 7, 7, 1, 1], [1, 6, 0, 7, 3, 3], [0, 0, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) """ all_labels = self.labels() labels = self.labels(labels) off_labels = list(set(all_labels) - set(labels)) return self.get_image_with_labels(off_labels)
[docs] def get_label_margin_image(self, labels=None, **kwargs): """Return a hollow labelled image, *i.e.* with label margins only. Parameters ---------- labels : int or list of int, optional If ``None`` (default), returns values for all known labels. If an integer or a list of integers, make sure they are in `self.labels()`. Returns ------- LabelledImage The labelled margin image. Notes ----- The "inside" of each label is replaced with `self.not_a_label`. Keyword arguments are passed to `hollow_out_labelled_image`. See Also -------- timagetk.components.labelled_image.hollow_out_labelled_image Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 2, 2, 2, 3, 3, 3], [1, 2, 2, 2, 2, 3, 3, 3], [1, 2, 2, 2, 2, 3, 3, 3], [1, 2, 2, 2, 2, 3, 3, 3]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a, not_a_label=0) >>> im.get_label_margin_image([2, 3]) LabelledImage([[0, 2, 0, 0, 2, 3, 0, 0], [0, 2, 0, 0, 2, 3, 0, 0], [0, 2, 0, 0, 2, 3, 0, 0], [0, 2, 0, 0, 2, 3, 0, 0]]) """ if labels is not None: image = self.get_image_with_labels(labels) else: image = self return hollow_out_labelled_image(image, **kwargs)
[docs] def fuse_labels_in_image(self, labels, new_value='min'): """Fuse the provided list of labels to a given new_value, or the min or max of the list of labels. Parameters ---------- labels : list of int List of labels to fuse. new_value : int or {'min', 'max'}, optional Value used to replace the given list of labels. By default, 'min' use the min value of the ``labels`` list. Can also be the max value using 'max'. Notes ----- When manually specifing a `new_value`, beware that it is not already defined in the labelled image, except if that what you want. Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a, not_a_label=0) >>> im.fuse_labels_in_image([6, 7], new_value=8) LabelledImage([[1, 2, 8, 8, 1, 1], [1, 8, 5, 8, 3, 3], [2, 2, 1, 8, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> im.fuse_labels_in_image([6, 7], new_value='min') LabelledImage([[1, 2, 6, 6, 1, 1], [1, 6, 5, 6, 3, 3], [2, 2, 1, 6, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> im.fuse_labels_in_image([6, 7], new_value='max') LabelledImage([[1, 2, 7, 7, 1, 1], [1, 7, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) """ if isinstance(labels, np.ndarray): labels = labels.tolist() elif isinstance(labels, set): labels = list(labels) else: assert isinstance(labels, list) and len(labels) >= 2 # - Make sure 'labels' is correctly formatted: labels = self.labels(labels) nb_labels = len(labels) # - If no labels to remove, its over: if nb_labels == 0: log.warning('No labels to fuse!') return # - Define the integer value of 'new_value': if new_value == "min": new_value = min(labels) labels.remove(new_value) elif new_value == "max": new_value = max(labels) labels.remove(new_value) elif isinstance(new_value, int): if self.is_label_in_image(new_value) and new_value not in labels: msg = "Given new_value is in the image and not in the list of labels." raise ValueError(msg) if new_value in labels: labels.remove(new_value) else: raise NotImplementedError(f"Unknown 'new_value' definition for '{new_value}'") # - Label "fusion" loop: no_bbox = [] log.info(f"Fusing {nb_labels} labels: {labels} to new_value '{new_value}'.") for _n, label in tqdm(enumerate(labels), total=len(labels), unit='label'): # - Try to get the label's bounding-box: try: bbox = self.boundingbox(label)[label] except KeyError: no_bbox.append(label) bbox = None # - Performs value replacement: array_replace_label(self, label, new_value, bbox) # - If some bounding-boxes were missing, print about it: if no_bbox: n = len(no_bbox) log.warning( f"Could not find bounding-box{'es' if n > 1 else ''} for {n} label{'s' if n > 1 else ''}: {no_bbox}") # - RE-INITIALIZE the object attributes to match new labels: self.__init__(self) return
[docs] def remove_labels_from_image(self, labels): """Remove labels from image using by setting them to `not_a_label`. Parameters ---------- labels : list of int List of labels to remove from the image. Notes ----- Require the definition of the `not_a_label` attribute! Example ------- >>> import numpy as np >>> a = np.array([[1, 2, 7, 7, 1, 1], [1, 6, 5, 7, 3, 3], [2, 2, 1, 7, 3, 3], [1, 1, 1, 4, 1, 1]]) >>> from timagetk import LabelledImage >>> im = LabelledImage(a, not_a_label=0) >>> im.remove_labels_from_image([6, 7]) LabelledImage([[1, 2, 0, 0, 1, 1], [1, 0, 5, 0, 3, 3], [2, 2, 1, 0, 3, 3], [1, 1, 1, 4, 1, 1]]) """ if isinstance(labels, int): labels = [labels] elif isinstance(labels, np.ndarray): labels = labels.tolist() elif isinstance(labels, set): labels = list(labels) else: assert isinstance(labels, list) # - Make sure 'labels' is correctly formatted: labels = self.labels(labels) if isinstance(labels, int): labels = [labels] # may be converted back to integer with previous line nb_labels = len(labels) # - If no labels to remove, its over: if nb_labels == 0: log.warning('No labels to remove!') return # - Remove 'labels' using bounding-boxes to speed-up computation: no_bbox = [] for _n, label in tqdm(enumerate(labels), total=len(labels), unit='label'): # Try to get the label's bounding-box: try: bbox = self.boundingbox(label)[label] except KeyError: no_bbox.append(label) bbox = None # Performs value replacement: array_replace_label(self, label, self.not_a_label, bbox) # - If some bounding-boxes were missing, print about it: if no_bbox: n = len(no_bbox) log.warning( f"Could not find bounding-box{'es' if n > 1 else ''} for {n} label{'s' if n > 1 else ''}: {no_bbox}") # - RE-INITIALIZE the object attributes to match new labels: self.__init__(self) return