Newer
Older
# Contributor(s): Eric Debreuve (since 2019), Morgane Nadal (2020)
#
# eric.debreuve@cnrs.fr
#
# This software is governed by the CeCILL license under French law and
# abiding by the rules of distribution of free software. You can use,
# modify and/ or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
#
# As a counterpart to the access to the source code and rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading, using, modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean that it is complicated to manipulate, and that also
# therefore means that it is reserved for developers and experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and, more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.
from brick.component.glial_cmp import glial_cmp_t
import brick.processing.map_labeling as ml_
from brick.general.type import array_t, py_array_picker_h, site_h
from typing import Optional, Sequence, Tuple
import numpy as np_
import scipy.ndimage as im_
import skimage.filters as fl_
import skimage.measure as ms_
import skimage.morphology as mp_
som_ext_path_t = namedtuple_t("som_ext_path_t", "extension length path idx")
__slots__ = ("contour_points", "centroid", "skl_graph", "ext_roots", "graph_roots", "volume_soma_micron", "axes_ellipsoid")
for slot in self.__class__.__slots__:
setattr(self, slot, None)
NADAL Morgane
committed
self.ext_roots = []
def FromMap(cls, lmp: array_t, uid: int) -> soma_t:
NADAL Morgane
committed
'''
Initialize and create the soma object based on the labelled map.
'''
NADAL Morgane
committed
# Create a boolean map keeping only the soma number 'uid'.
bmp = lmp == uid
NADAL Morgane
committed
# Initialize the object with its different fields
instance.InitializeFromMap(bmp, uid)
NADAL Morgane
committed
# Find the contour sites of the soma
instance.contour_points = tuple(zip(*contour_map.nonzero()))
NADAL Morgane
committed
# Find the centroid of the soma
instance.centroid = np_.array([np_.median(instance.sites[i]) for i in range(3)])
def Extensions(self, max_level: int = sy_.maxsize) -> Tuple[glial_cmp_t]:
#
# max_level=1: directly connected extensions
# ...
soma_ext = self.extensions.copy()
current_level_idx = 0
for level in range(2, max_level + 1):
next_level_idx = soma_ext.__len__()
if next_level_idx == current_level_idx:
break
for extension in soma_ext[current_level_idx:]:
soma_ext.extend(extension.extensions)
current_level_idx = next_level_idx
return tuple(soma_ext)
def ContourPointsCloseTo(
self, point: site_h, max_distance: float
) -> Tuple[Optional[Tuple[site_h, ...]], Optional[py_array_picker_h]]:
NADAL Morgane
committed
'''
Find the closest contour points of soma to an extension endpoint in the 3D map
'''
NADAL Morgane
committed
# Find the points in the contour points of the soma which euclidian distance to the endpoint is inferior to the
# maximum distance allowed.
points = tuple(
contour_point
for contour_point in self.contour_points
if (np_.subtract(point, contour_point) ** 2).sum() <= max_distance
)
NADAL Morgane
committed
# Return the reformatted closest points
def ExtensionPointsCloseTo(
self, point: site_h, max_distance: float
) -> Tuple[Optional[Tuple[site_h, ...]], Optional[py_array_picker_h]]:
NADAL Morgane
committed
'''
Find the closest extensions point to an extension endpoint in the 3D map.
Leave connection paths apart because only detected extension pieces
(as opposed to invented=connection paths) are considered valid connection targets.
'''
NADAL Morgane
committed
# Find the points in the extensions sites which euclidian distance to the endpoint is inferior to the
# maximum distance allowed.
for extension in self.Extensions():
ext_sites = tuple(zip(*extension.sites))
points.extend(
ext_point
for ext_point in ext_sites
if (np_.subtract(point, ext_point) ** 2).sum() <= max_distance
NADAL Morgane
committed
# Return the reformatted closest points
def BackReferenceSoma(self, glial_cmp: glial_cmp_t) -> None:
raise NotImplementedError("Soma do not need to back-reference a soma")
def __str__(self) -> str:
#
if self.extensions is None:
n_extensions = 0
else:
n_extensions = self.extensions.__len__()
return (
f"Soma.{self.uid}, "
f"contour points={self.contour_points.__len__()}, "
f"extensions={n_extensions}"
)
@staticmethod
def Map(image: array_t, low: float, high: float, selem: array_t) -> array_t:
NADAL Morgane
committed
'''
Perform hysteresis thresholding, dependant on the intensity of the input image, followed by closing/opening.
'''
result = __HysterisisImage__(image, low, high)
result = __MorphologicalCleaning__(image, result, selem)
# MaximumIntensityProjectionZ(result)
NADAL Morgane
committed
def DeleteSmallSomas(map: array_t, min_area: int) -> array_t:
result = map.copy()
NADAL Morgane
committed
# Label the image region by measuring the connectivity
lmp = ms_.label(map)
NADAL Morgane
committed
# Measure properties of labeled image regions
for region in ms_.regionprops(lmp):
# Delete regions (set pixel to 0) which area is smaller that the allowed minimum area
region_sites = (lmp == region.label).nonzero()
result[region_sites] = 0
lmp[region_sites] = 0
return result, lmp
NADAL Morgane
committed
def DeleteBigSomas(map: array_t, lmp: array_t, max_area: int) -> array_t:
NADAL Morgane
committed
result = map.copy()
NADAL Morgane
committed
# Measure properties of labeled image regions
for region in ms_.regionprops(lmp):
# Delete regions (set pixel to 0) which area is bigger that the allowed maximum area
# These detected big somas are probably an aggregation of multiple somas
if region.area >= max_area:
region_sites = (lmp == region.label).nonzero()
result[region_sites] = 0
lmp[region_sites] = 0
return result, lmp
NADAL Morgane
committed
def ContourMap(map: array_t) -> array_t:
'''
Create a map containing only the contour points of the soma.
'''
# Find the 26-connectivity of each voxel
part_map = ml_.PartLMap(map)
# The background is labeled with 27, and contour points have a connectivity of at most (<=) 25.
result = part_map < 26
return result.astype(np_.int8)
@staticmethod
NADAL Morgane
committed
def InfluenceMaps(map: array_t) -> Tuple[array_t, array_t]:
'''
Create a 3D influence map based on the somas' influence in the image.
'''
NADAL Morgane
committed
# Set background to 0
background = (map == 0).astype(np_.int8)
# Find the distance map and the influence map
dist_map, idx_map = im_.morphology.distance_transform_edt(background, return_indices=True)
# obj_map = np_.empty_like(map_)
# for row in range(0, obj_map.shape[0]):
# for col in range(0, obj_map.shape[1]):
# for dep in range(0, obj_map.shape[2]):
# obj_map[row, col, dep] = map_[
# idx_map[0, row, col, dep],
# idx_map[1, row, col, dep],
# idx_map[2, row, col, dep],
# ]
NADAL Morgane
committed
return dist_map, np_.array(map[tuple(idx_map)])
def SomasLMap(somas: Sequence[soma_t], with_extensions: bool = True) -> array_t:
NADAL Morgane
committed
'''
Create a map containing the labelled somas and their extensions and connection paths
with the label of their respective soma.
'''
NADAL Morgane
committed
# Initialization
NADAL Morgane
committed
if somas.__len__() == 0:
raise AssertionError("No soma detected on the image.")
shape = somas[0].img_shape
result = np_.zeros(shape, dtype=np_.int64)
NADAL Morgane
committed
# for each soma, put into the map the soma label at the sites of soma, connexion paths and extensions.
NADAL Morgane
committed
# if the soma has extensions, add the soma label at the site of the extensions and of the connection path
NADAL Morgane
committed
# For each existing connexion path soma-ext, add it on the result map with soma label
lambda path: path is not None, soma.connection_path.values()
NADAL Morgane
committed
# For each existing connexion path ext-ext, add it on the result map with soma label
for extension in soma.Extensions():
for connection_path in filter(
lambda path: path is not None,
extension.connection_path.values(),
):
result[connection_path] = soma.uid
NADAL Morgane
committed
# Also add the extensions on the result map with soma label
def __PointsCloseTo__(
points: Tuple[site_h, ...]
) -> Tuple[Optional[Tuple[site_h, ...]], Optional[py_array_picker_h]]:
NADAL Morgane
committed
'''
Reformat the 3D points coordinates.
'''
#
if points.__len__() > 0:
points_as_picker = tuple(zip(*points))
else:
points = None
points_as_picker = None
return points, points_as_picker
NADAL Morgane
committed
def __HysterisisImage__(image: array_t, low: float, high: float) -> array_t:
# Find the image max value
max_image = image.max()
# Find the image minimum non zero value
nonzero_sites = image.nonzero()
nonzero_values = image[nonzero_sites]
min_image = nonzero_values.min()
# Adapt the hysteresis thresholds to the max and min of the image
low = low * (max_image - min_image) + min_image
high = high * (max_image - min_image) + min_image
# Perform hysteresis
result = fl_.apply_hysteresis_threshold(image, low, high)
result = result.astype(np_.int8, copy=False)
return result
def __MorphologicalCleaning__(image: array_t, result: array_t, selem) -> array_t:
NADAL Morgane
committed
for dep in range(image.shape[0]):
result[dep, :, :] = mp_.closing(result[dep, :, :], selem)
result[dep, :, :] = mp_.opening(result[dep, :, :], selem)
return result
def MaximumIntensityProjectionZ(img: array_t, cmap: str ='tab20', axis: int = 0, output_image_file_name: str = None) -> None:
""" Maximum Image Projection on the Z axis. """
#
xy = np_.amax(img, axis=axis)
pl_.imshow(xy, cmap=cmap)
pl_.show(block=True)
if output_image_file_name is not None:
pl_.imsave(output_image_file_name, xy, cmap=cmap)
print('Image saved in', output_image_file_name)