# -*- coding: utf-8 -*-
import collections
import functools
import operator
import warnings
from dask import compute, delayed
import dask.array as da
import dask.bag as db
import dask.dataframe as dd
import numpy as np
from . import _utils
from ._utils import _label
from ._utils._find_objects import _array_chunk_location, _find_bounding_boxes, _find_objects
__all__ = [
"area",
"center_of_mass",
"extrema",
"histogram",
"label",
"labeled_comprehension",
"maximum",
"maximum_position",
"mean",
"median",
"minimum",
"minimum_position",
"standard_deviation",
"sum",
"sum_labels",
"variance",
]
[docs]def area(image, label_image=None, index=None):
"""Find the area of specified subregions in an image.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers.
If None (default), returns area of total image dimensions.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
area : ndarray
Area of ``index`` selected regions from ``label_image``.
Example
-------
>>> import dask.array as da
>>> image = da.random.random((3, 3))
>>> label_image = da.from_array(
[[1, 1, 0],
[1, 0, 3],
[0, 7, 0]], chunks=(1, 3))
>>> # No labels given, returns area of total image dimensions
>>> area(image)
9
>>> # Combined area of all non-zero labels
>>> area(image, label_image).compute()
5
>>> # Areas of selected labels selected with the ``index`` keyword argument
>>> area(image, label_image, index=[0, 1, 2, 3]).compute()
array([4, 3, 0, 1], dtype=int64)
"""
if label_image is None:
return da.prod(np.array([i for i in image.shape]))
else:
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
ones = da.ones(
label_image.shape, dtype=bool, chunks=label_image.chunks
)
area_lbl = labeled_comprehension(
ones, label_image, index, len, int, int(0)
)
return area_lbl
[docs]def center_of_mass(image, label_image=None, index=None):
"""
Find the center of mass over an image at specified subregions.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
center_of_mass : ndarray
Coordinates of centers-of-mass of ``image`` over the ``index`` selected
regions from ``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
# SciPy transposes these for some reason.
# So we do the same thing here.
# This only matters if index is some array.
index = index.T
out_dtype = np.dtype([("com", float, (image.ndim,))])
default_1d = np.full((1,), np.nan, dtype=out_dtype)
func = functools.partial(
_utils._center_of_mass, shape=image.shape, dtype=out_dtype
)
com_lbl = labeled_comprehension(
image, label_image, index,
func, out_dtype, default_1d[0], pass_positions=True
)
com_lbl = com_lbl["com"]
return com_lbl
[docs]def extrema(image, label_image=None, index=None):
"""
Find the min and max with positions over an image at specified subregions.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
minimums, maximums, min_positions, max_positions : tuple of ndarrays
Values and coordinates of minimums and maximums in each feature.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
out_dtype = np.dtype([
("min_val", image.dtype),
("max_val", image.dtype),
("min_pos", np.dtype(int), image.ndim),
("max_pos", np.dtype(int), image.ndim)
])
default_1d = np.zeros((1,), dtype=out_dtype)
func = functools.partial(
_utils._extrema, shape=image.shape, dtype=out_dtype
)
extrema_lbl = labeled_comprehension(
image, label_image, index,
func, out_dtype, default_1d[0], pass_positions=True
)
extrema_lbl = collections.OrderedDict([
(k, extrema_lbl[k])
for k in ["min_val", "max_val", "min_pos", "max_pos"]
])
for pos_key in ["min_pos", "max_pos"]:
pos_nd = extrema_lbl[pos_key]
if index.ndim == 0:
pos_nd = da.squeeze(pos_nd)
elif index.ndim > 1:
pos_nd = pos_nd.reshape(
(int(np.prod(pos_nd.shape[:-1])), pos_nd.shape[-1])
)
extrema_lbl[pos_key] = pos_nd
result = tuple(extrema_lbl.values())
return result
def find_objects(label_image):
"""Return bounding box slices for each object labelled by integers.
Parameters
----------
label_image : ndarray
Image features noted by integers.
Returns
-------
Dask dataframe
Each row respresents an indivdual integrer label. Columns contain the
slice information for the object boundaries in each dimension
(dimensions are named: 0, 1, ..., nd).
Notes
-----
You must have the optional dependency ``dask[dataframe]`` installed
to use the ``find_objects`` function.
"""
if label_image.dtype.char not in np.typecodes['AllInteger']:
raise ValueError("find_objects only accepts integer dtype arrays")
block_iter = zip(
np.ndindex(*label_image.numblocks),
map(functools.partial(operator.getitem, label_image),
da.core.slices_from_chunks(label_image.chunks))
)
arrays = []
for block_id, block in block_iter:
array_location = _array_chunk_location(block_id, label_image.chunks)
arrays.append(delayed(_find_bounding_boxes)(block, array_location))
bag = db.from_sequence(arrays)
result = bag.fold(functools.partial(_find_objects, label_image.ndim), split_every=2).to_delayed()
meta = dd.utils.make_meta([(i, object) for i in range(label_image.ndim)])
result = delayed(compute)(result)[0] # avoid the user having to call compute twice on result
result = dd.from_delayed(result, meta=meta, prefix="find-objects-", verify_meta=False)
return result
[docs]def histogram(image,
min,
max,
bins,
label_image=None,
index=None):
"""
Find the histogram over an image at specified subregions.
Histogram calculates the frequency of values in an array within bins
determined by ``min``, ``max``, and ``bins``. The ``label_image`` and
``index`` keywords can limit the scope of the histogram to specified
sub-regions within the array.
Parameters
----------
image : ndarray
N-D image data
min : int
Minimum value of range of histogram bins.
max : int
Maximum value of range of histogram bins.
bins : int
Number of bins.
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
histogram : ndarray
Histogram of ``image`` over the ``index`` selected regions from
``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
min = int(min)
max = int(max)
bins = int(bins)
func = functools.partial(_utils._histogram, min=min, max=max, bins=bins)
result = labeled_comprehension(
image, label_image, index, func, object, None
)
return result
[docs]def label(image, structure=None, wrap_axes=None):
"""
Label features in an array.
Parameters
----------
image : ndarray
An array-like object to be labeled. Any non-zero values in ``image``
are counted as features and zero values are considered the background.
structure : ndarray, optional
A structuring element that defines feature connections.
``structure`` must be symmetric. If no structuring element is
provided, one is automatically generated with a squared connectivity
equal to one. That is, for a 2-D ``image`` array, the default
structuring element is::
[[0,1,0],
[1,1,1],
[0,1,0]]
wrap_axes : tuple of int, optional
Whether labels should be wrapped across array boundaries, and if so which axes.
This feature is not present in `ndimage.label`.
Examples:
- (0,) only wrap across the 0th axis.
- (0, 1) wrap across the 0th and 1st axis.
- (0, 1, 3) wrap across 0th, 1st and 3rd axis.
Returns
-------
label : ndarray or int
An integer ndarray where each unique feature in ``image`` has a unique
label in the returned array.
num_features : int
How many objects were found.
"""
image = da.asarray(image)
labeled_blocks = np.empty(image.numblocks, dtype=object)
# First, label each block independently, incrementing the labels in that
# block by the total number of labels from previous blocks. This way, each
# block's labels are globally unique.
block_iter = zip(
np.ndindex(*image.numblocks),
map(functools.partial(operator.getitem, image),
da.core.slices_from_chunks(image.chunks))
)
index, input_block = next(block_iter)
labeled_blocks[index], total = _label.block_ndi_label_delayed(input_block,
structure)
for index, input_block in block_iter:
labeled_block, n = _label.block_ndi_label_delayed(input_block,
structure)
block_label_offset = da.where(labeled_block > 0,
total,
_label.LABEL_DTYPE.type(0))
labeled_block += block_label_offset
labeled_blocks[index] = labeled_block
total += n
# Put all the blocks together
block_labeled = da.block(labeled_blocks.tolist())
# Now, build a label connectivity graph that groups labels across blocks.
# We use this graph to find connected components and then relabel each
# block according to those.
label_groups = _label.label_adjacency_graph(
block_labeled, structure, total, wrap_axes=wrap_axes
)
new_labeling = _label.connected_components_delayed(label_groups)
relabeled = _label.relabel_blocks(block_labeled, new_labeling)
n = da.max(relabeled)
return (relabeled, n)
[docs]def labeled_comprehension(image,
label_image,
index,
func,
out_dtype,
default,
pass_positions=False):
"""
Compute a function over an image at specified subregions.
Roughly equivalent to [func(image[labels == i]) for i in index].
Sequentially applies an arbitrary function (that works on array_like image)
to subsets of an n-D image array specified by ``label_image`` and
``index``. The option exists to provide the function with positional
parameters as the second argument.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
func : callable
Python function to apply to ``label_image`` from ``image``.
out_dtype : dtype
Dtype to use for ``result``.
default : int, float or None
Default return value when a element of ``index`` does not exist
in ``label_image``.
pass_positions : bool, optional
If True, pass linear indices to ``func`` as a second argument.
Default is False.
Returns
-------
result : ndarray
Result of applying ``func`` on ``image`` over the ``index`` selected
regions from ``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
out_dtype = np.dtype(out_dtype)
default_1d = np.full((1,), default, dtype=out_dtype)
pass_positions = bool(pass_positions)
args = (image,)
if pass_positions:
positions = _utils._ravel_shape_indices(
image.shape, chunks=image.chunks
)
args = (image, positions)
result = np.empty(index.shape, dtype=object)
for i in np.ndindex(index.shape):
lbl_mtch_i = (label_image == index[i])
args_lbl_mtch_i = tuple(e[lbl_mtch_i] for e in args)
result[i] = _utils._labeled_comprehension_func(
func, out_dtype, default_1d, *args_lbl_mtch_i
)
for i in range(result.ndim - 1, -1, -1):
result2 = result[..., 0]
for j in np.ndindex(index.shape[:i]):
result2[j] = da.stack(result[j].tolist(), axis=0)
result = result2
result = result[()][..., 0]
return result
[docs]def maximum(image, label_image=None, index=None):
"""
Find the maxima over an image at specified subregions.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
maxima : ndarray
Maxima of ``image`` over the ``index`` selected regions from
``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
return labeled_comprehension(
image, label_image, index, np.max, image.dtype, image.dtype.type(0)
)
[docs]def maximum_position(image, label_image=None, index=None):
"""
Find the positions of maxima over an image at specified subregions.
For each region specified by ``label_image``, the position of the maximum
value of ``image`` within the region is returned.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
maxima_positions : ndarray
Maxima positions of ``image`` over the ``index`` selected regions from
``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
if index.shape:
index = index.flatten()
out_dtype = np.dtype([("pos", int, (image.ndim,))])
default_1d = np.zeros((1,), dtype=out_dtype)
func = functools.partial(
_utils._argmax, shape=image.shape, dtype=out_dtype
)
max_pos_lbl = labeled_comprehension(
image, label_image, index,
func, out_dtype, default_1d[0], pass_positions=True
)
max_pos_lbl = max_pos_lbl["pos"]
if index.shape == tuple():
max_pos_lbl = da.squeeze(max_pos_lbl)
return max_pos_lbl
[docs]def mean(image, label_image=None, index=None):
"""
Find the mean over an image at specified subregions.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
means : ndarray
Mean of ``image`` over the ``index`` selected regions from
``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
nan = np.float64(np.nan)
mean_lbl = labeled_comprehension(
image, label_image, index, np.mean, np.float64, nan
)
return mean_lbl
[docs]def minimum(image, label_image=None, index=None):
"""
Find the minima over an image at specified subregions.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
minima : ndarray
Minima of ``image`` over the ``index`` selected regions from
``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
return labeled_comprehension(
image, label_image, index, np.min, image.dtype, image.dtype.type(0)
)
[docs]def minimum_position(image, label_image=None, index=None):
"""
Find the positions of minima over an image at specified subregions.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
minima_positions : ndarray
Maxima positions of ``image`` over the ``index`` selected regions from
``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
if index.shape:
index = index.flatten()
out_dtype = np.dtype([("pos", int, (image.ndim,))])
default_1d = np.zeros((1,), dtype=out_dtype)
func = functools.partial(
_utils._argmin, shape=image.shape, dtype=out_dtype
)
min_pos_lbl = labeled_comprehension(
image, label_image, index,
func, out_dtype, default_1d[0], pass_positions=True
)
min_pos_lbl = min_pos_lbl["pos"]
if index.shape == tuple():
min_pos_lbl = da.squeeze(min_pos_lbl)
return min_pos_lbl
[docs]def standard_deviation(image, label_image=None, index=None):
"""
Find the standard deviation over an image at specified subregions.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
standard_deviation : ndarray
Standard deviation of ``image`` over the ``index`` selected regions
from ``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
nan = np.float64(np.nan)
std_lbl = labeled_comprehension(
image, label_image, index, np.std, np.float64, nan
)
return std_lbl
[docs]def sum_labels(image, label_image=None, index=None):
"""
Find the sum of all pixels over specified subregions of an image.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
sum_lbl : ndarray
Sum of ``image`` over the ``index`` selected regions from
``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
sum_lbl = labeled_comprehension(
image, label_image, index, np.sum, np.float64, np.float64(0)
)
return sum_lbl
[docs]def sum(image, label_image=None, index=None):
"""DEPRECATED FUNCTION. Use `sum_labels` instead."""
warnings.warn("DEPRECATED FUNCTION. Use `sum_labels` instead.",
DeprecationWarning)
return sum_labels(image, label_image=label_image, index=index)
[docs]def variance(image, label_image=None, index=None):
"""
Find the variance over an image at specified subregions.
Parameters
----------
image : ndarray
N-D image data
label_image : ndarray, optional
Image features noted by integers. If None (default), all values.
index : int or sequence of ints, optional
Labels to include in output. If None (default), all values where
non-zero ``label_image`` are used.
The ``index`` argument only works when ``label_image`` is specified.
Returns
-------
variance : ndarray
Variance of ``image`` over the ``index`` selected regions from
``label_image``.
"""
image, label_image, index = _utils._norm_input_labels_index(
image, label_image, index
)
nan = np.float64(np.nan)
var_lbl = labeled_comprehension(
image, label_image, index, np.var, np.float64, nan
)
return var_lbl