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://www.github.com/ChristophKirst/ClearMap2'

import gc
import multiprocessing

import numpy as np

import cv2
import scipy.ndimage.filters as ndf

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

###############################################################################
# ## 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 shape = clearmap_io.shape(source) order = clearmap_io.order(source) initialize_sinks(cell_detection_parameter, shape, order) cell_detection_parameter.update(verbose=processing_parameter.get('verbose', False)) n_processes = multiprocessing.cpu_count() if processing_parameter.get('processes') is None else processing_parameter.get('processes') n_threads = int(multiprocessing.cpu_count() / n_processes) # Number of threads so that * n_processes, fills CPUs results, blocks = bp.process(detect_cells_block, source, sink=None, function_type='block', return_result=True, return_blocks=True, parameter=cell_detection_parameter, workspace=workspace, **{**processing_parameter, **{'n_threads': n_threads}}) # merge results results = np.vstack([np.hstack(r) for r in results]) # create column headers # FIXME: use pd.DataFrame instead header = ['x', 'y', 'z'] dtypes = [int, int, int] if cell_detection_parameter['shape_detection'] is not None: header += ['size'] dtypes += [int] measures = cell_detection_parameter['intensity_detection']['measure'] header += measures dtypes += [float] * len(measures) dt = {'names': header, 'formats': dtypes} cells = np.zeros(len(results), dtype=dt) for i, h in enumerate(header): cells[h] = results[:, i] # save results return clearmap_io.write(sink, cells)
[docs] def detect_cells_block(source, parameter=default_cell_detection_parameter, n_threads=None): """Detect cells in a Block.""" # 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 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 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: centers = ap.where(maxima, processes=n_threads).array # WARNING: prange del maxima # correct for valid region if valid: 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 results = (centers,) else: results = () # WARNING: sd.detect_shape uses prange # cell shape detection # FIXME: may use centers without assignment shape = run_step('shape_detection', dog, sd.detect_shape, remove_previous_result=True, **default_step_params, args=[centers], extra_kwargs={'verbose': parameter.get('verbose'), 'processes': n_threads}) if parameter_shape: # size detection max_label = centers.shape[0] sizes = sd.find_size(shape, max_label=max_label) valid = sizes > 0 results += (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: 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 intensity = me.measure_expression(steps_to_measure[m], centers, search_radius=r, **parameter_intensity, processes=1, verbose=False) results += (intensity,) if parameter.get('verbose'): timer.print_elapsed_time('Shape detection') if valid is not None: results = tuple(r[valid] for r in results) # correct coordinate offsets of blocks results = (results[0] + lower,) + results[1:] # correct shapes for merging results = tuple(r[:, None] if r.ndim == 1 else r for r in results) 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.as_source(source) ids = np.ones(source.shape[0], dtype=bool) for filter_name, thrsh in thresholds.items(): if thrsh: if not isinstance(thrsh, (tuple, list)) and isinstance(thrsh, (int, float)): thrsh = (thrsh, None) if thrsh[0] is not None: # high pass ids = np.logical_and(ids, thrsh[0] <= source[filter_name]) if thrsh[1] is not None: # low pass ids = np.logical_and(ids, thrsh[1] > source[filter_name]) cells_filtered = source[ids] return clearmap_io.write(sink, cells_filtered)
############################################################################### # ## Tests ############################################################################### def _test(): """Tests."""