#!/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