Mentions légales du service

Skip to content
Snippets Groups Projects
soma.py 12.2 KiB
Newer Older
DEBREUVE Eric's avatar
DEBREUVE Eric committed
# Copyright CNRS/Inria/UNS
# Contributor(s): Eric Debreuve (since 2019), Morgane Nadal (2020)
DEBREUVE Eric's avatar
DEBREUVE Eric committed
#
# 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.

DEBREUVE Eric's avatar
DEBREUVE Eric committed
from __future__ import annotations

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
DEBREUVE Eric's avatar
DEBREUVE Eric committed

from collections import namedtuple as namedtuple_t
DEBREUVE Eric's avatar
DEBREUVE Eric committed
import sys as sy_
from typing import Optional, Sequence, Tuple
DEBREUVE Eric's avatar
DEBREUVE Eric committed

import numpy as np_
import scipy.ndimage as im_
import skimage.filters as fl_
import skimage.measure as ms_
import skimage.morphology as mp_
import matplotlib.pyplot as pl_
DEBREUVE Eric's avatar
DEBREUVE Eric committed
som_ext_path_t = namedtuple_t("som_ext_path_t", "extension length path idx")
DEBREUVE Eric's avatar
DEBREUVE Eric committed
class soma_t(glial_cmp_t):
DEBREUVE Eric's avatar
DEBREUVE Eric committed
    #
NADAL Morgane's avatar
NADAL Morgane committed
    __slots__ = ("contour_points", "centroid", "skl_graph", "ext_roots", "graph_roots", "volume_soma_micron", "axes_ellipsoid")
DEBREUVE Eric's avatar
DEBREUVE Eric committed

    def __init__(self):
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        #
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        super().__init__()
        for slot in self.__class__.__slots__:
            setattr(self, slot, None)
DEBREUVE Eric's avatar
DEBREUVE Eric committed

    @classmethod
    def FromMap(cls, lmp: array_t, uid: int) -> soma_t:
        '''
        Initialize and create the soma object based on the labelled map.
        '''
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        #
        instance = cls()

        # Create a boolean map keeping only the soma number 'uid'.
        # Initialize the object with its different fields
        instance.InitializeFromMap(bmp, uid)
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        contour_map = cls.ContourMap(bmp)
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        instance.contour_points = tuple(zip(*contour_map.nonzero()))
        instance.centroid = np_.array([np_.median(instance.sites[i]) for i in range(3)])
DEBREUVE Eric's avatar
DEBREUVE Eric committed

        return instance

    def Extensions(self, max_level: int = sy_.maxsize) -> Tuple[glial_cmp_t]:
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        #
        # 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)
DEBREUVE Eric's avatar
DEBREUVE Eric committed

    def ContourPointsCloseTo(
        self, point: site_h, max_distance: float
    ) -> Tuple[Optional[Tuple[site_h, ...]], Optional[py_array_picker_h]]:
        '''
        Find the closest contour points of soma to an extension endpoint in the 3D map
        '''
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        #
        # Find the points in the contour points of the soma which euclidian distance to the endpoint is inferior to the
        # maximum distance allowed.
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        points = tuple(
            contour_point
            for contour_point in self.contour_points
            if (np_.subtract(point, contour_point) ** 2).sum() <= max_distance
        )
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        return __PointsCloseTo__(points)
DEBREUVE Eric's avatar
DEBREUVE Eric committed

DEBREUVE Eric's avatar
DEBREUVE Eric committed
    def ExtensionPointsCloseTo(
        self, point: site_h, max_distance: float
    ) -> Tuple[Optional[Tuple[site_h, ...]], Optional[py_array_picker_h]]:
        '''
        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.
        '''

DEBREUVE Eric's avatar
DEBREUVE Eric committed
        points = []

        # Find the points in the extensions sites which euclidian distance to the endpoint is inferior to the
        # maximum distance allowed.
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        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
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            )
DEBREUVE Eric's avatar
DEBREUVE Eric committed

DEBREUVE Eric's avatar
DEBREUVE Eric committed
        return __PointsCloseTo__(tuple(points))
DEBREUVE Eric's avatar
DEBREUVE Eric committed

    def BackReferenceSoma(self, glial_cmp: glial_cmp_t) -> None:
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        #
        raise NotImplementedError("Soma do not need to back-reference a soma")
DEBREUVE Eric's avatar
DEBREUVE Eric committed

    def __str__(self) -> str:
        #
        if self.extensions is None:
            n_extensions = 0
        else:
            n_extensions = self.extensions.__len__()

        return (
            f"Soma.{self.uid}, "
            f"area={self.sites[0].__len__()}, "
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            f"contour points={self.contour_points.__len__()}, "
            f"extensions={n_extensions}"
        )
DEBREUVE Eric's avatar
DEBREUVE Eric committed

    @staticmethod
    def Map(image: array_t, low: float, high: float, selem: array_t) -> array_t:
        '''
        Perform hysteresis thresholding, dependant on the intensity of the input image, followed by closing/opening.
        '''

        result = __HysterisisImage__(image, low, high)
NADAL Morgane's avatar
NADAL Morgane committed
        # MaximumIntensityProjectionZ(result)
DEBREUVE Eric's avatar
DEBREUVE Eric committed

NADAL Morgane's avatar
NADAL Morgane committed
        result = __MorphologicalCleaning__(image, result, selem)
        # MaximumIntensityProjectionZ(result)
DEBREUVE Eric's avatar
DEBREUVE Eric committed

        return result

    @staticmethod
    def DeleteSmallSomas(map: array_t, min_area: int) -> array_t:

        result = map.copy()
DEBREUVE Eric's avatar
DEBREUVE Eric committed

        # Label the image region by measuring the connectivity
        lmp = ms_.label(map)
DEBREUVE Eric's avatar
DEBREUVE Eric 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
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            if region.area <= min_area:
DEBREUVE Eric's avatar
DEBREUVE Eric committed
                region_sites = (lmp == region.label).nonzero()
                result[region_sites] = 0
DEBREUVE Eric's avatar
DEBREUVE Eric committed

DEBREUVE Eric's avatar
DEBREUVE Eric committed

NADAL Morgane's avatar
NADAL Morgane committed
    @staticmethod
    def DeleteBigSomas(map: array_t, lmp: array_t, max_area: int) -> array_t:
        # 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
NADAL Morgane's avatar
NADAL Morgane committed
            if region.area >= max_area:
                region_sites = (lmp == region.label).nonzero()
                result[region_sites] = 0
                lmp[region_sites] = 0

        return result, lmp

DEBREUVE Eric's avatar
DEBREUVE Eric committed
    @staticmethod
    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.
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        result = part_map < 26

        return result.astype(np_.int8)

    @staticmethod
    def InfluenceMaps(map: array_t) -> Tuple[array_t, array_t]:
        '''
        Create a 3D influence map based on the somas' influence in the image.
        '''
DEBREUVE Eric's avatar
DEBREUVE Eric 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)
DEBREUVE Eric's avatar
DEBREUVE Eric committed

        # 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],
        #             ]

DEBREUVE Eric's avatar
DEBREUVE Eric committed

    @staticmethod
    def SomasLMap(somas: Sequence[soma_t], with_extensions: bool = True) -> array_t:
        '''
        Create a map containing the labelled somas and their extensions and connection paths
        with the label of their respective soma.
        '''
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        #
        if somas.__len__() == 0:
            raise AssertionError("No soma detected on the image.")

DEBREUVE Eric's avatar
DEBREUVE Eric committed
        shape = somas[0].img_shape
        result = np_.zeros(shape, dtype=np_.int64)
DEBREUVE Eric's avatar
DEBREUVE Eric committed

        # for each soma, put into the map the soma label at the sites of soma, connexion paths and extensions.
DEBREUVE Eric's avatar
DEBREUVE Eric committed
        for soma in somas:
DEBREUVE Eric's avatar
DEBREUVE Eric committed
            result[soma.sites] = soma.uid

            # if the soma has extensions, add the soma label at the site of the extensions and of the connection path
            if with_extensions:
                # For each existing connexion path soma-ext, add it on the result map with soma label
DEBREUVE Eric's avatar
DEBREUVE Eric committed
                for connection_path in filter(
                    lambda path: path is not None, soma.connection_path.values()
DEBREUVE Eric's avatar
DEBREUVE Eric committed
                ):
DEBREUVE Eric's avatar
DEBREUVE Eric committed
                    result[connection_path] = soma.uid

                # 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

                    # Also add the extensions on the result map with soma label
                    result[extension.sites] = soma.uid
DEBREUVE Eric's avatar
DEBREUVE Eric committed

        return result


DEBREUVE Eric's avatar
DEBREUVE Eric committed
def __PointsCloseTo__(
    points: Tuple[site_h, ...]
) -> Tuple[Optional[Tuple[site_h, ...]], Optional[py_array_picker_h]]:
DEBREUVE Eric's avatar
DEBREUVE Eric committed
    #
    if points.__len__() > 0:
        points_as_picker = tuple(zip(*points))
    else:
        points = None
        points_as_picker = None

    return points, points_as_picker
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


NADAL Morgane's avatar
NADAL Morgane committed
def __MorphologicalCleaning__(image: array_t, result: array_t, selem) -> array_t:

    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)