470 lines
17 KiB
Cython
470 lines
17 KiB
Cython
#cython: cdivision=True
|
|
#cython: boundscheck=False
|
|
#cython: nonecheck=False
|
|
#cython: wraparound=False
|
|
|
|
"""_max_tree.pyx - building a max-tree from an image.
|
|
|
|
This is an implementation of the max-tree, which is a morphological
|
|
representation of the image. Many morphological operators can be built
|
|
from this representation, namely attribute openings and closings.
|
|
|
|
This file also contains implementations of max-tree based filters and
|
|
functions to characterize the tree components.
|
|
"""
|
|
|
|
import numpy as np
|
|
cimport numpy as np
|
|
cimport cython
|
|
from .._shared.fused_numerics cimport np_real_numeric
|
|
|
|
np.import_array()
|
|
|
|
ctypedef np.float64_t DTYPE_FLOAT64_t
|
|
ctypedef np.int32_t DTYPE_INT32_t
|
|
ctypedef np.uint32_t DTYPE_UINT32_t
|
|
ctypedef np.uint64_t DTYPE_UINT64_t
|
|
ctypedef np.int64_t DTYPE_INT64_t
|
|
ctypedef np.uint8_t DTYPE_BOOL_t
|
|
ctypedef np.uint8_t DTYPE_UINT8_t
|
|
|
|
|
|
cdef DTYPE_INT64_t find_root_rec(DTYPE_INT64_t[::1] parent,
|
|
DTYPE_INT64_t index):
|
|
"""Get the root of the current tree through a recursive algorithm.
|
|
|
|
This function modifies the tree in-place through path compression, which
|
|
reduces the complexity from O(n*n) to O(n*log(n)). Despite path
|
|
compression, our tests showed that the non-recursive version
|
|
(:func:`find_root`) seems to perform better. We leave this version as
|
|
inspiration for future improvements.
|
|
|
|
Parameters
|
|
----------
|
|
parent : array of int
|
|
The array containing parent relationships.
|
|
index : int
|
|
The index of which we want to find the root.
|
|
|
|
Returns
|
|
-------
|
|
root : int
|
|
The root found from ``index``.
|
|
"""
|
|
if parent[index] != index:
|
|
parent[index] = find_root_rec(parent, parent[index])
|
|
return parent[index]
|
|
|
|
|
|
cdef inline DTYPE_INT64_t find_root(DTYPE_INT64_t[::1] parent,
|
|
DTYPE_INT64_t index):
|
|
"""Get the root of the current tree.
|
|
|
|
Here, we do without path compression and accept the higher complexity, but
|
|
the function is inline and avoids some overhead induced by its recursive
|
|
version.
|
|
|
|
Parameters
|
|
----------
|
|
parent : array of int
|
|
The array containing parent relationships.
|
|
index : int
|
|
The index of which we want to find the root.
|
|
|
|
Returns
|
|
-------
|
|
root : int
|
|
The root found from ``index``.
|
|
"""
|
|
while parent[index] != parent[parent[index]]:
|
|
parent[index] = parent[parent[index]]
|
|
return parent[index]
|
|
|
|
|
|
cdef void canonize(np_real_numeric[::1] image, DTYPE_INT64_t[::1] parent,
|
|
DTYPE_INT64_t[::1] sorted_indices):
|
|
"""Generate a max-tree for which every node's parent is a canonical node.
|
|
|
|
The parent of a non-canonical pixel is a canonical pixel.
|
|
The parent of a canonical pixel is also a canonical pixel with a different
|
|
value. There is exactly one canonical pixel for each component in the
|
|
component tree.
|
|
|
|
Parameters
|
|
----------
|
|
image : array
|
|
The raveled image intensity values.
|
|
parent : array of int
|
|
The array mapping image indices to their parents in the max-tree.
|
|
**This array will be modified in-place.**
|
|
sorted_indices : array of int
|
|
Array of image indices such that if i comes before j, then i cannot
|
|
be the parent of j.
|
|
"""
|
|
cdef DTYPE_INT64_t q = 0
|
|
cdef DTYPE_INT64_t p
|
|
for p in sorted_indices:
|
|
q = parent[p]
|
|
if image[q] == image[parent[q]]:
|
|
parent[p] = parent[q]
|
|
|
|
|
|
cdef np.ndarray[DTYPE_INT32_t, ndim = 2] unravel_offsets(
|
|
DTYPE_INT32_t[::1] offsets,
|
|
DTYPE_INT32_t[::1] center_point,
|
|
DTYPE_INT32_t[::1] shape):
|
|
"""Unravel a list of offset indices.
|
|
|
|
These offsets can be negative. The function generates an array of shape
|
|
(number of offsets, image dimensions), where each row corresponds
|
|
to the coordinates of each point.
|
|
|
|
See also
|
|
--------
|
|
unravel_index
|
|
"""
|
|
|
|
cdef DTYPE_INT32_t number_of_dimensions = len(shape)
|
|
cdef DTYPE_INT32_t number_of_points = len(offsets)
|
|
cdef np.ndarray[DTYPE_INT32_t, ndim = 2] points = np.zeros(
|
|
(number_of_points,
|
|
number_of_dimensions),
|
|
dtype=np.int32)
|
|
cdef DTYPE_INT32_t neg_shift = np.ravel_multi_index(center_point, shape)
|
|
|
|
cdef DTYPE_INT32_t i, offset, curr_index, coord
|
|
|
|
for i, offset in enumerate(offsets):
|
|
current_point = np.unravel_index(offset + neg_shift, shape)
|
|
for d in range(number_of_dimensions):
|
|
points[i, d] = current_point[d] - center_point[d]
|
|
|
|
return points
|
|
|
|
|
|
cdef DTYPE_UINT8_t _is_valid_neighbor(DTYPE_INT64_t index,
|
|
DTYPE_INT32_t[::1] coordinates,
|
|
DTYPE_INT32_t[::1] shape):
|
|
"""Check whether a neighbor of a given pixel is inside the image.
|
|
|
|
Parameters
|
|
----------
|
|
index : int
|
|
The pixel given as a linear index into the raveled image array.
|
|
coordinates : array of int, shape ``image.ndim``
|
|
The neighbor given as a list of offsets from `pixel` in each dimension.
|
|
shape : array of int, shape ``image.ndim`
|
|
The image shape.
|
|
|
|
Returns
|
|
-------
|
|
is_neighbor : uint8
|
|
0 if the neighbor falls outside the image, 1 otherwise.
|
|
"""
|
|
|
|
cdef DTYPE_INT64_t number_of_dimensions = len(shape)
|
|
cdef DTYPE_INT64_t res_coord = 0
|
|
cdef int i = 0
|
|
|
|
cdef np.ndarray[DTYPE_INT32_t, ndim = 1] p_coord = np.array(
|
|
np.unravel_index(index, shape),
|
|
dtype=np.int32)
|
|
|
|
# get the coordinates of the point from a 1D index
|
|
for i in range(number_of_dimensions):
|
|
res_coord = p_coord[i] + coordinates[i]
|
|
if res_coord < 0:
|
|
return 0
|
|
if res_coord >= shape[i]:
|
|
return 0
|
|
|
|
return 1
|
|
|
|
|
|
cpdef np.ndarray[DTYPE_FLOAT64_t, ndim = 1] _compute_area(np_real_numeric[::1] image,
|
|
DTYPE_INT64_t[::1] parent,
|
|
DTYPE_INT64_t[::1] sorted_indices):
|
|
"""Compute the area of all max-tree components.
|
|
|
|
This attribute is used for area opening and closing
|
|
"""
|
|
cdef DTYPE_INT64_t p_root = sorted_indices[0]
|
|
cdef DTYPE_INT64_t p, q
|
|
cdef DTYPE_UINT64_t number_of_pixels = len(image)
|
|
cdef np.ndarray[DTYPE_FLOAT64_t, ndim = 1] area = np.ones(number_of_pixels,
|
|
dtype=np.float64)
|
|
|
|
for p in sorted_indices[::-1]:
|
|
if p == p_root:
|
|
continue
|
|
q = parent[p]
|
|
area[q] = area[q] + area[p]
|
|
|
|
return area
|
|
|
|
|
|
cpdef np.ndarray[DTYPE_FLOAT64_t, ndim = 1] _compute_extension(
|
|
np_real_numeric[::1] image,
|
|
DTYPE_INT32_t[::1] shape,
|
|
DTYPE_INT64_t[::1] parent,
|
|
DTYPE_INT64_t[::1] sorted_indices):
|
|
"""Compute the bounding box extension of all max-tree components.
|
|
|
|
This attribute is used for diameter opening and closing.
|
|
"""
|
|
cdef DTYPE_INT64_t p_root = sorted_indices[0]
|
|
cdef DTYPE_INT64_t p, q
|
|
cdef DTYPE_UINT64_t number_of_pixels = len(image)
|
|
cdef np.ndarray[DTYPE_FLOAT64_t, ndim = 1] extension = np.ones(
|
|
number_of_pixels,
|
|
dtype=np.float64)
|
|
cdef np.ndarray[DTYPE_FLOAT64_t, ndim = 2] max_coord = np.array(
|
|
np.unravel_index(np.arange(number_of_pixels), shape),
|
|
dtype=np.float64).T
|
|
cdef np.ndarray[DTYPE_FLOAT64_t, ndim = 2] min_coord = np.array(
|
|
np.unravel_index(np.arange(number_of_pixels), shape),
|
|
dtype=np.float64).T
|
|
|
|
for p in sorted_indices[::-1]:
|
|
if p == p_root:
|
|
continue
|
|
q = parent[p]
|
|
max_coord[q] = np.maximum(max_coord[q], max_coord[p])
|
|
min_coord[q] = np.minimum(min_coord[q], min_coord[p])
|
|
extension[q] = np.max(max_coord[q] - min_coord[q]) + 1
|
|
|
|
return extension
|
|
|
|
|
|
# _max_tree_local_maxima cacluates the local maxima from the max-tree
|
|
# representation this is interesting if the max-tree representation has
|
|
# already been calculated for other reasons. Otherwise, it is not the most
|
|
# efficient method. If the parameter label is True, the minima are labeled.
|
|
cpdef void _max_tree_local_maxima(np_real_numeric[::1] image,
|
|
DTYPE_UINT64_t[::1] output,
|
|
DTYPE_INT64_t[::1] parent,
|
|
DTYPE_INT64_t[::1] sorted_indices
|
|
):
|
|
"""Find the local maxima in image from the max-tree representation.
|
|
|
|
Parameters
|
|
----------
|
|
|
|
image : array of arbitrary type
|
|
The flattened image pixels.
|
|
output : array of the same shape and type as image.
|
|
The output image must contain only ones.
|
|
parent : array of int
|
|
Image of the same shape as the input image. The value
|
|
at each pixel is the parent index of this pixel in the max-tree
|
|
reprentation.
|
|
sorted_indices : array of int
|
|
List of length = number of pixels. Each element
|
|
corresponds to one pixel index in the image. It encodes the order
|
|
of elements in the tree: a parent of a pixel always comes before
|
|
the element itself. More formally: i < j implies that j cannot be
|
|
the parent of i.
|
|
"""
|
|
|
|
cdef DTYPE_INT64_t p_root = sorted_indices[0]
|
|
cdef DTYPE_INT64_t p, q
|
|
cdef DTYPE_UINT64_t number_of_pixels = len(image)
|
|
cdef DTYPE_UINT64_t label = 1
|
|
|
|
for p in sorted_indices[::-1]:
|
|
if p == p_root:
|
|
continue
|
|
|
|
q = parent[p]
|
|
|
|
# if p is canonical (parent has a different value)
|
|
if image[p] != image[q]:
|
|
output[q] = 0
|
|
|
|
# if output[p] was the parent of some other canonical
|
|
# pixel, it has been set to zero. Only the leaves
|
|
# (local maxima) are thus > 0.
|
|
if output[p] > 0:
|
|
output[p] = label
|
|
label += 1
|
|
|
|
for p in sorted_indices[::-1]:
|
|
if p == p_root:
|
|
continue
|
|
|
|
q = parent[p]
|
|
|
|
# if p is not canonical (parent has the same value)
|
|
if image[p] == image[q]:
|
|
# in this case we propagate the value
|
|
output[p] = output[q]
|
|
continue
|
|
|
|
return
|
|
|
|
|
|
# direct filter (criteria based filter)
|
|
cpdef void _direct_filter(np_real_numeric[::1] image,
|
|
np_real_numeric[::1] output,
|
|
DTYPE_INT64_t[::1] parent,
|
|
DTYPE_INT64_t[::1] sorted_indices,
|
|
DTYPE_FLOAT64_t[::1] attribute,
|
|
DTYPE_FLOAT64_t attribute_threshold
|
|
):
|
|
"""Apply a direct filtering.
|
|
|
|
This produces an image in which for all possible thresholds, each connected
|
|
component has the specified attribute value greater than that threshold.
|
|
This is the basic function called by :func:`area_opening`,
|
|
:func:`diameter_opening`, and similar.
|
|
|
|
For :func:`area_opening`, for instance, the attribute is the area. In this
|
|
case, an image is produced for which all connected components for all
|
|
thresholds have at least an area (pixel count) of the threshold given by
|
|
the user.
|
|
|
|
Parameters
|
|
----------
|
|
|
|
image : array
|
|
The flattened image pixels.
|
|
output : array, same size and type as `image`
|
|
The array into which to write the output values. **This array will be
|
|
modified in-place.**
|
|
parent : array of int, same shape as `image`
|
|
Image of indices. The value at each pixel is the index of this pixel's
|
|
parent in the max-tree reprentation.
|
|
sorted_indices : array of int, same shape as `image`
|
|
"List" of pixel indices, which contains an ordering of elements in the
|
|
tree such that a parent of a pixel always comes before the element
|
|
itself. More formally: i < j implies that j cannot be the parent of i.
|
|
attribute : array of float
|
|
Contains the attributes computed for the max-tree.
|
|
attribute_threshold : float
|
|
The threshold to be applied to the attribute.
|
|
"""
|
|
|
|
cdef DTYPE_INT64_t p_root = sorted_indices[0]
|
|
cdef DTYPE_INT64_t p, q
|
|
cdef DTYPE_UINT64_t number_of_pixels = len(image)
|
|
|
|
if attribute[p_root] < attribute_threshold:
|
|
output[p_root] = 0
|
|
else:
|
|
output[p_root] = image[p_root]
|
|
|
|
for p in sorted_indices:
|
|
if p == p_root:
|
|
continue
|
|
|
|
q = parent[p]
|
|
|
|
# this means p is not canonical
|
|
# in other words, it has a parent that has the
|
|
# same image value.
|
|
if image[p] == image[q]:
|
|
output[p] = output[q]
|
|
continue
|
|
|
|
if attribute[p] < attribute_threshold:
|
|
# this corresponds to stopping
|
|
# as the level of the lower parent
|
|
# is propagated to the current level
|
|
output[p] = output[q]
|
|
else:
|
|
# here the image reconstruction continues.
|
|
# The level is maintained (original value).
|
|
output[p] = image[p]
|
|
|
|
return
|
|
|
|
|
|
# _max_tree is the main function. It allows to construct a max
|
|
# tree representation of the image.
|
|
cpdef void _max_tree(np_real_numeric[::1] image,
|
|
DTYPE_BOOL_t[::1] mask,
|
|
DTYPE_INT32_t[::1] structure,
|
|
DTYPE_INT32_t[::1] offset,
|
|
DTYPE_INT32_t[::1] shape,
|
|
DTYPE_INT64_t[::1] parent,
|
|
DTYPE_INT64_t[::1] sorted_indices
|
|
):
|
|
"""Build a max-tree.
|
|
|
|
Parameters
|
|
----------
|
|
image : array
|
|
The flattened image pixels.
|
|
mask : array of int
|
|
An array of the same shape as `image` where each pixel contains a
|
|
nonzero value if it is to be considered for the filtering. NOTE: it is
|
|
*essential* that the border pixels (those with neighbors falling
|
|
outside the volume) are all set to zero, or segfaults could occur.
|
|
structure : array of int
|
|
A list of coordinate offsets to compute the raveled coordinates of each
|
|
neighbor from the raveled coordinates of the current pixel.
|
|
parent : array of int
|
|
Output image of the same shape as the input image. The value at each
|
|
pixel is the parent index of this pixel in the max-tree reprentation.
|
|
**This array will be written to in-place.**
|
|
sorted_indices : array of int
|
|
Output "list" of pixel indices, which contains an ordering of elements
|
|
in the tree such that a parent of a pixel always comes before the
|
|
element itself. More formally: i < j implies that j cannot be the
|
|
the parent of i. **This array will be written to in-place.**
|
|
"""
|
|
|
|
cdef DTYPE_UINT64_t number_of_pixels = len(image)
|
|
cdef DTYPE_UINT64_t number_of_dimensions = len(shape)
|
|
|
|
cdef DTYPE_INT64_t i = 0
|
|
cdef DTYPE_INT64_t p = 0
|
|
cdef DTYPE_INT64_t root = 0
|
|
cdef DTYPE_INT64_t index = 0
|
|
|
|
cdef Py_ssize_t nneighbors = structure.shape[0]
|
|
|
|
cdef DTYPE_INT64_t[::1] zpar = parent.copy()
|
|
|
|
cdef np.ndarray[DTYPE_INT32_t, ndim = 2] points = unravel_offsets(
|
|
structure, offset, shape)
|
|
|
|
# initialization of the image parent.
|
|
for i in range(number_of_pixels):
|
|
parent[i] = -1
|
|
zpar[i] = -1
|
|
|
|
# traverse the array in reversed order (from highest value to lowest value)
|
|
for p in sorted_indices[::-1]:
|
|
parent[p] = p
|
|
zpar[p] = p
|
|
|
|
for i in range(nneighbors):
|
|
|
|
# get the ravelled index of the neighbor
|
|
index = p + structure[i]
|
|
|
|
if not mask[p]:
|
|
# in this case, p is at the border of the image.
|
|
# some neighbor point is not valid.
|
|
if not _is_valid_neighbor(p, points[i], shape):
|
|
# neighbor is not in the image.
|
|
continue
|
|
|
|
if parent[index] < 0:
|
|
# in this case the parent is not yet set: we ignore
|
|
continue
|
|
|
|
root = find_root(zpar, index)
|
|
if root != p:
|
|
zpar[root] = p
|
|
parent[root] = p
|
|
|
|
# In a canonized max-tree, each parent is a canonical pixel,
|
|
# i.e. for each connected component at a level l, all pixels point
|
|
# to the same representative which in turn points to the representative
|
|
# pixel at the next level.
|
|
canonize(image, parent, sorted_indices)
|
|
|
|
return
|