Source code for ClearMap.ImageProcessing.Experts.Cells

# -*- coding: utf-8 -*-
"""
Cells
=====

Expert cell image processing pipeline.

This module provides the basic routines for processing immediate early
gene data. 

The routines are used in the :mod:`ClearMap.Scripts.CellMap` pipeline.
"""
__author__ = 'Christoph Kirst <christoph.kirst.ck@gmail.com>'
__license__ = 'GPLv3 - GNU General Public License v3 (see LICENSE.txt)'
__copyright__ = 'Copyright © 2020 by Christoph Kirst'
__webpage__ = 'https://idisco.info'
__download__ = 'https://github.com/ClearAnatomics/ClearMap'

import gc
import multiprocessing
import shutil
import tempfile
import warnings
from pathlib import Path

import numpy as np

import cv2
import pandas as pd
import scipy.ndimage.filters as ndf
import scipy.ndimage as ndi

import ClearMap.IO.IO as clearmap_io

import ClearMap.ParallelProcessing.BlockProcessing as bp
import ClearMap.ParallelProcessing.DataProcessing.ArrayProcessing as ap

import ClearMap.ImageProcessing.IlluminationCorrection as ic
import ClearMap.ImageProcessing.Filter.StructureElement as se
import ClearMap.ImageProcessing.Filter.FilterKernel as fk

import ClearMap.Analysis.Measurements.maxima_detection as md
import ClearMap.Analysis.Measurements.shape_detection as sd
import ClearMap.Analysis.Measurements.MeasureExpression as me

import ClearMap.Utils.Timer as tmr
from ClearMap.ImageProcessing.Experts.utils import initialize_sinks, run_step, print_params
from ClearMap.ImageProcessing.LocalStatistics import local_percentile

from ClearMap.Utils.exceptions import ClearMapValueError
from ClearMap.Utils.utilities import sanitize_n_processes

###############################################################################
# ## Default parameter
###############################################################################

default_cell_detection_parameter = dict(
    # flatfield
    illumination_correction=dict(flatfield=None,
                                 scaling='mean'),

    # background removal
    background_correction=dict(shape=(10, 10),
                               form='Disk',
                               save=False),

    # equalization
    equalization=None,

    # difference of gaussians filter
    dog_filter=dict(shape=None,
                    sigma=None,
                    sigma2=None),

    # extended maxima detection
    maxima_detection=dict(h_max=None,
                          shape=5,
                          threshold=0,
                          valid=True,
                          save=False),

    # cell shape detection
    shape_detection=dict(threshold=700,
                         save=False),

    # cell intensity detection
    intensity_detection=dict(method='max',
                             shape=3,
                             measure=['source', 'background_correction']),
)
"""Parameter for the cell detection pipeline. 
See :func:`detect_cells` for details."""


default_cell_detection_processing_parameter = dict(
    size_max=100,
    size_min=50,
    overlap=32,
    axes=[2],
    optimization=True,
    optimization_fix='all',
    verbose=None,
    processes=None
)
"""Parallel processing parameter for the cell detection pipeline. 
See :func:`ClearMap.ParallelProcessing.BlockProcessing.process` for details."""       


###############################################################################
# ## Cell detection
###############################################################################

[docs] def detect_cells(source, sink=None, cell_detection_parameter=default_cell_detection_parameter, processing_parameter=default_cell_detection_processing_parameter, workspace=None): """Cell detection pipeline. Arguments --------- source : source specification The source of the stitched raw data. sink : sink specification or None The sink to write the result to. If None, an array is returned. cell_detection_parameter : dict Parameter for the binarization. See below for details. processing_parameter : dict Parameter for the parallel processing. See :func:`ClearMap.ParallelProcessing.BlockProcessing.process` for description of all the parameter. workspace: Workspace The optional workspace object to have a handle to cancel the multiprocess Returns ------- sink : Source The result of the cell detection. Notes ----- Effectively this function performs the following steps: * illumination correction via :func:`~ClearMap.ImageProcessing.IlluminationCorrection.correct_illumination` * background removal * difference of Gaussians (DoG) filter * maxima detection via :func:`~ClearMap.Analysis.Measurements.MaximaDetection.find_extended_maxima` * cell shape detection via :func:`~ClearMap.Analysis.Measurements.ShapeDetection.detect_shape` * cell intensity and size measurements via: :func:`~ClearMap.ImageProcessing.Measurements.ShapeDetection.find_intensity`, :func:`~ClearMap.ImageProcessing.Measurements.ShapeDetection.find_size`. The parameters for each step are passed as sub-dictionaries to the cell_detection_parameter dictionary. * If None is passed for one of the steps this step is skipped. * Each step also has an additional parameter 'save' that enables saving of the result of that step to a file to inspect the pipeline. Illumination correction ----------------------- illumination_correction : dict or None Illumination correction step parameter. flatfield : array or str The flat field estimate for the image planes. background : array or None A background level to assume for the flatfield correction. scaling : float, 'max', 'mean' or None Optional scaling after the flat field correction. save : str or None Save the result of this step to the specified file if not None. See also :func:`ClearMap.ImageProcessing.IlluminationCorrection.correct_illumination` Background removal ------------------ background_correction : dict or None Background removal step parameter. shape : tuple The shape of the structure element to estimate the background. This should be larger than the typical cell size. form : str The form of the structure element (e.g. 'Disk') save : str or None Save the result of this step to the specified file if not None. Equalization ------------ equalization : dict or None Equalization step parameter. See also :func:`ClearMap.ImageProcessing.LocalStatistics.local_percentile` percentile : tuple The lower and upper percentiles used to estimate the equalization. The lower percentile is used for normalization, the upper to limit the maximal boost to a maximal intensity above this percentile. max_value : float The maximal intensity value in the equalized image. selem : tuple The structural element size to estimate the percentiles. Should be larger than the larger vessels. spacing : tuple The spacing used to move the structural elements. Larger spacings speed up processing but become locally less precise. interpolate : int The order of the interpolation used in constructing the full background estimate in case a non-trivial spacing is used. save : str or None Save the result of this step to the specified file if not None. DoG Filter ---------- dog_filter : dict or None Difference of Gaussian filter step parameter. shape : tuple The shape of the filter. This should be near the typical cell size. sigma : tuple or None The std of the inner Gaussian. If None, determined automatically from shape. sigma2 : tuple or None The std of the outer Gaussian. If None, determined automatically from shape. save : str or None Save the result of this step to the specified file if not None. Maxima detection ---------------- maxima_detection : dict or None Extended maxima detection step parameter. h_max : float or None The 'height' for the extended maxima. If None, simple local maxima detection is used. shape : tuple The shape of the structural element for extended maxima detection. This should be near the typical cell size. threshold : float or None Only maxima above this threshold are detected. If None, all maxima are detected. valid : bool If True, only detect cell centers in the valid range of the blocks with overlap. save : str or None Save the result of this step to the specified file if not None. Shape detection --------------- shape_detection : dict or None Shape detection step parameter. threshold : float Cell shape is expanded from maxima if pixels are above this threshold and not closer to another maxima. save : str or None Save the result of this step to the specified file if not None. Intensity detection ------------------- intensity_detection : dict or None Intensity detection step parameter. method : {'max'|'min','mean'|'sum'} The method to use to measure the intensity of a cell. shape : tuple or None If no cell shapes are detected a disk of this shape is used to measure the cell intensity. save : str or None Save the result of this step to the specified file if not None. References ---------- [1] Renier, Adams, Kirst, Wu et al., "Mapping of Brain Activity by Automated Volume Analysis of Immediate Early Genes.", Cell 165, 1789 (2016) [1] Kirst et al., "Mapping the Fine-Scale Organization and Plasticity of the Brain Vasculature", Cell 180, 780 (2020) """ # initialize sink initialize_sinks(cell_detection_parameter, clearmap_io.shape(source), clearmap_io.order(source)) cell_detection_parameter.update(verbose=processing_parameter.get('verbose', False)) n_processes = sanitize_n_processes(processing_parameter.get('processes')) n_threads = max(1, int(multiprocessing.cpu_count() / n_processes)) # Number of threads so that * n_processes, fills CPUs sink_location = clearmap_io.location(sink) if sink is not None else None sink_path = Path(sink_location) if sink_location is not None else None result_directory = Path(tempfile.mkdtemp(prefix='cells_blocks_', dir=sink_path.parent if sink_path is not None else None)) try: results = bp.process(detect_cells_block, source, sink=None, function_type='block', return_result=True, return_blocks=False, parameter=cell_detection_parameter, workspace=workspace, result_directory=result_directory, **{**processing_parameter, **{'n_threads': n_threads}}) # merge results results = pd.concat((pd.read_feather(r) for r in results), ignore_index=True) # save results if sink is None: return results else: results.to_feather(sink) return sink finally: shutil.rmtree(result_directory, ignore_errors=True)
[docs] def detect_cells_block(source, parameter=default_cell_detection_parameter, n_threads=None, result_directory=None): """Detect cells in a Block. Returns ------- pandas.DataFrame or str If ``result_directory`` is None, returns a DataFrame with explicit cell-property columns. If ``result_directory`` is provided, writes the block table to a per-block Feather file and returns the file path. """ # initialize parameter and slicing if parameter.get('verbose'): prefix = 'Block %s: ' % (source.info(),) total_time = tmr.Timer(prefix) else: prefix = '' base_slicing = source.valid.base_slicing valid_slicing = source.valid.slicing valid_lower = source.valid.lower valid_upper = source.valid.upper lower = source.lower results = {} centers = None steps_to_measure = {} # FIXME: rename parameter_intensity = parameter.get('intensity_detection') if parameter_intensity: parameter_intensity = parameter_intensity.copy() measure = parameter_intensity.pop('measure', []) measure = measure if measure else [] valid_measurement_keys = list(default_cell_detection_parameter.keys()) + ['source'] for m in measure: if m not in valid_measurement_keys: raise KeyError(f'Unknown measurement: {m}') steps_to_measure[m] = None else: measure = [] if 'source' in steps_to_measure: steps_to_measure['source'] = source default_step_params = {'parameter': parameter, 'steps_to_measure': steps_to_measure, 'prefix': prefix, 'base_slicing': base_slicing, 'valid_slicing': valid_slicing} # WARNING: if param_illumination: previous_step = source, not np.array(source.array) corrected = run_step('illumination_correction', np.array(source.array), ic.correct_illumination, **default_step_params) background = run_step('background_correction', corrected, remove_background, remove_previous_result=True, **default_step_params) equalized = run_step('equalization', background, equalize, remove_previous_result=True, extra_kwargs={'mask': None}, **default_step_params) dog = run_step('dog_filter', equalized, dog_filter, # TODO: DoG filter != .title() remove_previous_result=True, **default_step_params) # Maxima detection parameter_maxima = parameter.get('maxima_detection') parameter_shape = parameter.get('shape_detection') if parameter_shape or parameter_intensity: if not parameter_maxima: print(f'{prefix}Warning: maxima detection needed for shape and intensity detection!') parameter_maxima = {} if parameter_maxima: valid = parameter_maxima.pop('valid', None) maxima = run_step('maxima_detection', dog, md.find_maxima, extra_kwargs={'verbose': parameter.get('verbose')}, remove_previous_result=False, **default_step_params) # center of maxima if parameter_maxima['h_max']: # FIXME: check if source or dog centers = md.find_center_of_maxima(source, maxima=maxima, verbose=parameter.get('verbose')) else: if parameter_shape: threshold = parameter_shape.get('threshold', 0) print(f'{prefix}masking maxima centers by (dog > {threshold}) for shape detection') mask = dog > threshold maxima = maxima * mask # We treat the eventuality of connected components of size>1 in the mask (maxima>0); # the choice of the structure matrix for connectivity could need discussion. # with no further adaptation the maxima_labeling consumes too much memory maxima_labels, _ = ndi.label(maxima, structure=np.ones((3,) * 3, dtype='bool')) centers = np.vstack(md.label_representatives(maxima_labels)).transpose() # we could come back to the ancient version # centers = ap.where(maxima, processes=n_threads).array del maxima # correct for valid region if valid: print('Filtering centers for correct block processing.') ids = np.ones(len(centers), dtype=bool) for c, l, u in zip(centers.T, valid_lower, valid_upper): ids = np.logical_and(ids, np.logical_and(l <= c, c < u)) centers = centers[ids] del ids else: valid = None # cell shape detection # FIXME: may use centers without assignment if parameter_shape: if parameter.get('verbose'): timer = tmr.Timer(prefix) try: parser = (lambda t: t[0]) shape, sizes = run_step('shape_detection', dog, sd.detect_shape, remove_previous_result=True, **default_step_params, args = [centers], presave_parser=parser, extra_kwargs={'verbose': parameter.get('verbose'), 'processes': n_threads, 'return_sizes': True}) except ClearMapValueError as err: if str(err) == 'An uint array with 0 values will lead to inconsistent results, consider a histogram transform or dtype conversion.': warnings.warn('This block is likely to contain corrupted data, an empty output will be provided for this block.') centers = centers[:0] sizes = np.array([], dtype=int) shape = None else: raise if parameter.get('verbose'): timer.print_elapsed_time('Shape detection') valid = sizes > 0 results['size'] = sizes else: valid = None shape = None # cell intensity detection if parameter_intensity: parameter_intensity, timer = print_params(parameter_intensity, 'intensity_detection', prefix, parameter.get('verbose')) if shape is not None: r = parameter_intensity.pop('shape', 3) if isinstance(r, tuple): r = r[0] for m in measure: if shape is not None: max_label = centers.shape[0] intensity = sd.find_intensity(steps_to_measure[m], label=shape, max_label=max_label, **parameter_intensity) else: # WARNING: prange but me.measure_expression not parallel since processes=1 # FIXME : How can r be defined in this branch ??? intensity = me.measure_expression(steps_to_measure[m], centers, search_radius=r, **parameter_intensity, n_processes=1, verbose=False) results[m] = intensity if parameter.get('verbose'): timer.print_elapsed_time('Intensity detection') # FIXME: extract DF creation if centers is None: centers = np.zeros((0, 3), dtype=int) if valid is not None: centers = centers[valid] results = {k: v[valid] for k, v in results.items()} # correct coordinate offsets of blocks centers = centers + lower # create named result table results = {'x': centers[:, 0], 'y': centers[:, 1], 'z': centers[:, 2], **results} if parameter_shape and 'size' not in results: # TODO: check if required results['size'] = np.zeros(len(centers), dtype=int) for m in measure: if m not in results: results[m] = np.zeros(len(centers)) results = pd.DataFrame(results) if result_directory is not None: result_directory.mkdir(exist_ok=True) block_id = source.iteration if block_id is None: block_id = 0 result_path = result_directory / f'cells_block_{block_id:04d}.feather' results.to_feather(result_path) if parameter.get('verbose'): print(f'{prefix}Wrote block cell table: {result_path}, rows={len(results):d}, ' f'columns={list(results.columns)}', flush=True) results = result_path if parameter.get('verbose'): total_time.print_elapsed_time('Cell detection') gc.collect() return results
############################################################################### # ## Cell detection processing steps ###############################################################################
[docs] def remove_background(source, shape, form='Disk'): selem = se.structure_element(shape, form=form, ndim=2) # FIXME: use skimage kernel selem = np.array(selem).astype('uint8') removed = np.empty(source.shape, dtype=source.dtype) for z in range(source.shape[2]): # img[:,:,z] = img[:,:,z] - grey_opening(img[:,:,z], structure=structureElement('Disk', (30,30))) # img[:,:,z] = img[:,:,z] - morph.grey_opening(img[:,:,z], structure=structureElement('Disk', (150,150))) removed[:, :, z] = source[:, :, z] - np.minimum(source[:, :, z], cv2.morphologyEx(source[:, :, z], cv2.MORPH_OPEN, selem)) return removed
[docs] def dog_filter(source, shape, sigma=None, sigma2=None): if shape is not None: fdog = fk.filter_kernel(ftype='dog', shape=shape, sigma=sigma, sigma2=sigma2) fdog = fdog.astype('float32') filtered = ndf.correlate(source, fdog) filtered[filtered < 0] = 0 return filtered else: return source
[docs] def detect_maxima(source, h_max=None, shape=5, threshold=None, verbose=False): # FIXME: use to refactor # extended maxima maxima = md.find_maxima(source, h_max=h_max, shape=shape, threshold=threshold, verbose=verbose) # center of maxima if h_max: centers = md.find_center_of_maxima(source, maxima=maxima, verbose=verbose) else: centers = ap.where(maxima).array # FIXME: prange return centers
[docs] def equalize(source, percentile=(0.5, 0.95), max_value=1.5, selem=(200, 200, 5), spacing=(50, 50, 5), interpolate=1, mask=None): equalized = local_percentile(source, percentile=percentile, mask=mask, dtype=float, selem=selem, spacing=spacing, interpolate=interpolate) normalize = 1/np.maximum(equalized[..., 0], 1) maxima = equalized[..., 1] ids = maxima * normalize > max_value normalize[ids] = max_value / maxima[ids] equalized = np.array(source, dtype=float) * normalize return equalized
############################################################################### # ## Cell filtering ###############################################################################
[docs] def filter_cells(source, sink, thresholds): """ Filter an array of detected cells according to the thresholds. Arguments --------- source : str, array or Source The source for the cell data. sink : str, array or Source The sink for the results. thresholds : dict Dictionary of the form {name : threshold} where name refers to the column in the cell data and threshold can be None, a float indicating a minimal threshold or a tuple (min, max) where min, max can be None or a minimal and maximal threshold value. Returns ------- sink : str, array or Source The thresholded cell data. """ source = clearmap_io.open_ro(source) cells_mask = np.ones(source.shape[0], dtype=bool) for filter_name, thrsh in thresholds.items(): if thrsh is not None: if not isinstance(thrsh, (tuple, list)) and isinstance(thrsh, (int, float)): thrsh = (thrsh, None) if thrsh[0] is not None: # high pass cells_mask = np.logical_and(cells_mask, thrsh[0] <= source[filter_name]) if thrsh[1] is not None: # low pass cells_mask = np.logical_and(cells_mask, thrsh[1] > source[filter_name]) cells_filtered = source[cells_mask] return clearmap_io.write(sink, cells_filtered)
############################################################################### # ## Tests ############################################################################### def _test(): """Tests."""