# -*- coding: utf-8 -*-
"""
params
======
All the classes that define parameters or group thereof for the tabs of the graphical interface
"""
import os
import string
from itertools import permutations
from typing import List
import numpy as np
from ClearMap.config.atlas import ATLAS_NAMES_MAP
from ClearMap.gui.gui_utils import create_clearmap_widget, clear_layout
from ClearMap.gui.dialogs import get_directory_dlg
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import QInputDialog, QToolBox, QCheckBox, QPushButton
__author__ = 'Charly Rousseau <charly.rousseau@icm-institute.org>'
__license__ = 'GPLv3 - GNU General Public License v3 (see LICENSE.txt)'
__copyright__ = 'Copyright © 2022 by Charly Rousseau'
__webpage__ = 'https://idisco.info'
__download__ = 'https://www.github.com/ChristophKirst/ClearMap2'
from ClearMap.gui.params_interfaces import ParamLink, UiParameter, UiParameterCollection
[docs]
class ParamsOrientationError(ValueError):
pass
[docs]
class AlignmentParams(UiParameterCollection):
"""
Class that groups all the parameters related to the alignment of the sample
This includes stitching and registration
"""
def __init__(self, tab, src_folder=None):
super().__init__(tab, src_folder)
self.stitching_general = GeneralStitchingParams(tab, src_folder)
self.stitching_rigid = RigidStitchingParams(tab, src_folder)
self.stitching_wobbly = WobblyStitchingParams(tab, src_folder)
self.registration = RegistrationParams(tab, src_folder)
[docs]
def fix_cfg_file(self, f_path):
# cfg = ConfigLoader.get_cfg_from_path(f_path)
pipeline_name, ok = QInputDialog.getItem(self.tab, 'Please select pipeline type',
'Pipeline name:', ['CellMap', 'TubeMap', 'Both'], 0, False)
if not ok:
raise ValueError('Missing sample ID')
self.config['pipeline_name'] = pipeline_name
# WARNING: needs to be self.config
# to be sure that we are up to date
# (otherwise write but potentially no reload)
self.write_config()
@property
def params(self):
return self.stitching_general, self.stitching_rigid, self.stitching_wobbly, self.registration
@property
def all_stitching_params(self):
return self.stitching_general, self.stitching_rigid, self.stitching_wobbly
@property
def pipeline_name(self):
return self.config['pipeline_name']
@pipeline_name.setter
def pipeline_name(self, name):
self.config['pipeline_name'] = name
# @property
# def pipeline_is_cell_map(self):
# return self.pipeline_name.lower().replace('_', '') == 'cellmap'
#
# @property
# def pipeline_is_tube_map(self):
# return self.pipeline_name.lower().replace('_', '') == 'tubemap'
#
# @property
# def pipeline_is_both(self):
# return self.pipeline_name.lower().replace('_', '') == 'both'
[docs]
class SampleParameters(UiParameter):
"""
Class that links the sample params file to the UI
Attributes
----------
sample_id : str
The ID of the sample. This must be unique
use_id_as_prefix : bool
Whether to use the sample ID as a prefix for the output files
tile_extension : str
The extension of the tile files
raw_path : str
The path to the raw data. This is typically an *expression* that will be expanded by ClearMap
autofluo_path : str
The path to the autofluorescence data. This is typically an *expression* that will be expanded by ClearMap
arteries_path : str
The path to the arteries data. This is typically an *expression* that will be expanded by ClearMap
raw_resolution : List[float]
The resolution (sampling) of the raw data
arteries_resolution : List[float]
The resolution (sampling) of the arteries data
autofluorescence_resolution : List[float]
The resolution (sampling) of the autofluorescence data
slice_x : List[int]
The slice in the x dimension
slice_y : List[int]
The slice in the y dimension
slice_z : List[int]
The slice in the z dimension
orientation : List[int]
The orientation of the data (x, y, z) as a tuple of integers (1, 2, 3), (3, 1, 2) ...
Negative values are allowed to indicate a flip of the axis
"""
sample_id: str
use_id_as_prefix: bool
tile_extension: str
raw_path: str
autofluo_path: str
arteries_path: str
raw_resolution: List[float]
arteries_resolution: List[float]
autofluorescence_resolution: List[float]
slice_x: List[int]
slice_y: List[int]
slice_z: List[int]
orientation: List[int]
def __init__(self, tab, src_folder=None):
super().__init__(tab, src_folder)
self.params_dict = {
'sample_id': ['sample_id'],
'use_id_as_prefix': ParamLink(['use_id_as_prefix'], self.tab.useIdAsPrefixCheckBox),
'tile_extension': ParamLink(['src_paths', 'tile_extension'], self.tab.tileExtensionLineEdit),
'raw_path': ['src_paths', 'raw'],
'autofluo_path': ParamLink(['src_paths', 'autofluorescence'], self.tab.autofluoPathOptionalPlainTextEdit),
'arteries_path': ParamLink(['src_paths', 'arteries'], self.tab.arteriesPathOptionalPlainTextEdit),
'raw_resolution': ParamLink(['resolutions', 'raw'], self.tab.rawResolutionTriplet),
'arteries_resolution': ParamLink(['resolutions', 'arteries'], self.tab.arteriesResolutionTriplet),
'autofluorescence_resolution': ParamLink(['resolutions', 'autofluorescence'], self.tab.autofluorescenceResolutionTriplet),
'slice_x': ParamLink(['slice_x'], self.tab.sliceXDoublet),
'slice_y': ParamLink(['slice_y'], self.tab.sliceYDoublet),
'slice_z': ParamLink(['slice_z'], self.tab.sliceZDoublet),
'orientation': ['orientation'] # WARNING: Finish by orientation in case invalid,
}
self.connect()
if self.sample_id:
self.handle_sample_id_changed(self.sample_id)
[docs]
def connect(self):
self.tab.sampleIdTxt.editingFinished.connect(self.handle_sample_id_changed)
self.tab.rawPath.textChanged.connect(self.handle_raw_path_changed)
self.tab.orient_x.currentTextChanged.connect(self.handle_orientation_changed)
self.tab.orient_y.currentTextChanged.connect(self.handle_orientation_changed)
self.tab.orient_z.currentTextChanged.connect(self.handle_orientation_changed)
self.connect_simple_widgets()
def _ui_to_cfg(self):
self._config['base_directory'] = self.src_folder
[docs]
def cfg_to_ui(self):
self.reload()
super().cfg_to_ui()
[docs]
def fix_cfg_file(self, f_path): # FIXME: seems wrong to pass f_path just for that usage
# cfg = ConfigLoader.get_cfg_from_path(f_path)
self.config['base_directory'] = os.path.dirname(f_path) # WARNING: needs to be self.config
# to be sure that we are up to date
# (otherwise write but potentially no reload)
if not self.sample_id:
sample_id, ok = QInputDialog.getText(self.tab, 'Warning: missing ID',
'<b>Missing sample ID</b><br>Please input below')
self.sample_id = sample_id
if not ok:
raise ValueError('Missing sample ID')
self.config['sample_id'] = self.sample_id
self.config['use_id_as_prefix'] = self.use_id_as_prefix
self.config.write()
# Sample params
@property
def sample_id(self):
return self.tab.sampleIdTxt.text()
@sample_id.setter
def sample_id(self, id_):
self.tab.sampleIdTxt.setText(id_)
[docs]
def handle_sample_id_changed(self, id_=None):
if self.config is not None:
self.config['sample_id'] = self.sample_id
self.ui_to_cfg() # FIXME: check
@property
def raw_path(self):
f_path = self.tab.rawPath.toPlainText()
f_path = f_path if f_path else None
return f_path
@raw_path.setter
def raw_path(self, f_path):
f_path = f_path if f_path is not None else ''
self.tab.rawPath.setPlainText(f_path)
[docs]
def handle_raw_path_changed(self):
self.config['src_paths']['raw'] = self.raw_path
@property
def orientation(self):
x = int(self.tab.orient_x.currentText())
y = int(self.tab.orient_y.currentText())
z = int(self.tab.orient_z.currentText())
n_axes = len(set([abs(e) for e in (x, y, z)]))
if n_axes != 3:
raise ParamsOrientationError('Number of different axis is only {} instead of 3. '
'Please amend duplicate axes'.format(n_axes))
return x, y, z
@orientation.setter
def orientation(self, orientation):
n_axes = len(set([abs(e) for e in orientation]))
if n_axes != 3:
raise ParamsOrientationError('Number of different axis is only {} instead of 3. '
'Please amend duplicate axes'.format(n_axes))
self.tab.orient_x.setCurrentText(f'{orientation[0]}')
self.tab.orient_y.setCurrentText(f'{orientation[1]}')
self.tab.orient_z.setCurrentText(f'{orientation[2]}')
[docs]
def handle_orientation_changed(self, val): # WARNING: does not seem to move up the stack because of pyqtsignals
try:
orientation = self.orientation
except ParamsOrientationError as err:
print('Invalid orientation, keeping current')
return
self._config['orientation'] = orientation
[docs]
class RigidStitchingParams(UiParameter):
skip: bool
x_overlap: int
y_overlap: int
projection_thickness: List[int]
max_shifts_x: List[int]
max_shifts_y: List[int]
max_shifts_z: List[int]
background_level: int
background_pixels: int
def __init__(self, tab, src_folder=None):
super().__init__(tab, src_folder)
self.params_dict = {
'skip': ParamLink(['skip'], self.tab.skipRigidCheckbox),
'x_overlap': ParamLink(['overlap_x'], self.tab.xOverlapSinglet),
'y_overlap': ParamLink(['overlap_y'], self.tab.yOverlapSinglet),
'projection_thickness': ['project_thickness'], # FIXME: change to projection
'max_shifts_x': ParamLink(['max_shifts_x'], self.tab.rigidMaxShiftsXDoublet),
'max_shifts_y': ParamLink(['max_shifts_y'], self.tab.rigidMaxShiftsYDoublet),
'max_shifts_z': ParamLink(['max_shifts_z'], self.tab.rigidMaxShiftsZDoublet),
'background_level': ParamLink(['background_level'], self.tab.rigidBackgroundLevel),
'background_pixels': ParamLink(['background_pixels'], self.tab.rigidBackgroundPixels)
}
self.cfg_subtree = ['stitching', 'rigid']
self.connect()
[docs]
def connect(self):
self.tab.projectionThicknessDoublet.valueChangedConnect(self.handle_projection_thickness_changed)
self.connect_simple_widgets()
@property
def projection_thickness(self):
val = self.tab.projectionThicknessDoublet.getValue()
if val is not None:
val.append(None)
return val
@projection_thickness.setter
def projection_thickness(self, thickness):
if thickness is not None:
thickness = thickness[:2]
self.tab.projectionThicknessDoublet.setValue(thickness)
[docs]
def handle_projection_thickness_changed(self):
self.config['project_thickness'] = self.projection_thickness
[docs]
class WobblyStitchingParams(UiParameter):
skip: bool
max_shifts_x: List[int]
max_shifts_y: List[int]
max_shifts_z: List[int]
valid_range: list # FIXME: missing pixel size
slice_range: List[int]
slice_pixel_size: int
def __init__(self, tab, src_folder=None):
super().__init__(tab, src_folder)
self.params_dict = {
'skip': ParamLink(['skip'], self.tab.skipWobblyCheckBox),
'max_shifts_x': ParamLink(['max_shifts_x'], self.tab.wobblyMaxShiftsXDoublet),
'max_shifts_y': ParamLink(['max_shifts_y'], self.tab.wobblyMaxShiftsYDoublet),
'max_shifts_z': ParamLink(['max_shifts_z'], self.tab.wobblyMaxShiftsZDoublet),
'valid_range': ParamLink(['stack_valid_range'], self.tab.wobblyValidRangeDoublet),
'slice_range': ParamLink(['slice_valid_range'], self.tab.wobblySliceRangeDoublet),
'slice_pixel_size': ParamLink(['slice_pixel_size'], self.tab.wobblySlicePixelSizeSinglet)
}
self.cfg_subtree = ['stitching', 'wobbly']
self.connect()
[docs]
def connect(self):
self.connect_simple_widgets()
[docs]
class GeneralStitchingParams(UiParameter):
use_npy: bool
run_raw: bool
run_arteries: bool
preview_raw: bool
preview_arteries: bool
convert_output: bool
convert_raw: bool
convert_arteries: bool
conversion_fmt: str
# stitching_preview_step: str
def __init__(self, tab, src_folder=None):
super().__init__(tab, src_folder)
self.params_dict = {
'use_npy': ParamLink(['conversion', 'use_npy'], self.tab.stitchingUseNpyCheckBox),
'run_raw': ParamLink(['stitching', 'run', 'raw'], self.tab.stitchingRunRawCheckBox),
'run_arteries': ParamLink(['stitching', 'run', 'arteries'], self.tab.stitchingRunArteriesCheckBox),
'preview_raw': ParamLink(['stitching', 'preview', 'raw'], self.tab.stitchingPreviewRawCheckBox),
'preview_arteries': ParamLink(['stitching', 'preview', 'arteries'], self.tab.stitchingPreviewArteriesCheckBox),
'convert_output': ['stitching', 'output_conversion', 'skip'],
'convert_raw': ParamLink(['stitching', 'output_conversion', 'raw'], self.tab.stitchingConvertRawCheckBox),
'convert_arteries': ParamLink(['stitching', 'output_conversion', 'arteries'], self.tab.stitchingConvertArteriesCheckBox),
'conversion_fmt': ParamLink(['stitching', 'output_conversion', 'format'], self.tab.outputConversionFormat),
# 'stitching_preview_step': ParamLink([], self.tab.stitchingPreviewStep, connect=False)
}
self.attrs_to_invert = ['convert_output'] # FIXME: check
self.connect()
[docs]
def connect(self):
self.tab.skipOutputConversioncheckBox.stateChanged.connect(self.handle_convert_output_changed)
self.connect_simple_widgets()
@property
def convert_output(self):
return not self.tab.skipOutputConversioncheckBox.isChecked()
@convert_output.setter
def convert_output(self, skip):
self.set_check_state(self.tab.skipOutputConversioncheckBox, not skip)
[docs]
def handle_convert_output_changed(self, state):
self.config['stitching']['output_conversion']['skip'] = not self.convert_output
[docs]
class RegistrationParams(UiParameter):
atlas_id_changed = pyqtSignal(str)
atlas_structure_tree_id_changed = pyqtSignal(str)
skip_resampling: bool
atlas_resolution: List[float]
atlas_id: str
structure_tree_id: str
atlas_folder: str
channel_affine_file_path: str
ref_affine_file_path: str
ref_bspline_file_path: str
def __init__(self, tab, src_folder=None):
super().__init__(tab, src_folder)
self.params_dict = {
'skip_resampling': ParamLink(['resampling', 'skip'], self.tab.skipRegistrationResamplingCheckBox),
'atlas_resolution': ['resampling', 'raw_sink_resolution'],
'atlas_id': ['atlas', 'id'],
'structure_tree_id': ['atlas', 'structure_tree_id'],
'atlas_folder': ParamLink(['atlas', 'align_files_folder'], self.tab.atlasFolderPath, connect=False), # FIXME: ensure that set correctly by picking
'channel_affine_file_path': ParamLink(['atlas', 'align_channels_affine_file'], self.tab.channelAffineFilePath),
'ref_affine_file_path': ParamLink(['atlas', 'align_reference_affine_file'], self.tab.refAffineFilePath),
'ref_bspline_file_path': ParamLink(['atlas', 'align_reference_bspline_file'], self.tab.refBsplineFilePath),
}
self.atlas_info = ATLAS_NAMES_MAP
self.cfg_subtree = ['registration']
self.connect()
[docs]
def connect(self):
self.tab.atlasResolutionTriplet.valueChangedConnect(self.handle_atlas_resolution_changed)
self.tab.atlasIdComboBox.currentTextChanged.connect(self.handle_atlas_id_changed)
self.tab.structureTreeIdComboBox.currentTextChanged.connect(self.handle_structure_tree_id_changed)
self.connect_simple_widgets()
@property
def atlas_base_name(self):
return self.atlas_info[self.atlas_id]['base_name']
@property
def atlas_resolution(self):
return self.tab.atlasResolutionTriplet.getValue()
@atlas_resolution.setter
def atlas_resolution(self, res):
self.tab.atlasResolutionTriplet.setValue(res)
[docs]
def handle_atlas_resolution_changed(self, state):
self.config['resampling']['raw_sink_resolution'] = self.atlas_resolution
self.config['resampling']['autofluo_sink_resolution'] = self.atlas_resolution
@property
def atlas_id(self):
return self.tab.atlasIdComboBox.currentText()
@atlas_id.setter
def atlas_id(self, value):
self.tab.atlasIdComboBox.setCurrentText(value)
[docs]
def handle_atlas_id_changed(self):
self.config['atlas']['id'] = self.atlas_id
self.atlas_resolution = self.atlas_info[self.atlas_id]['resolution']
self.ui_to_cfg()
self.atlas_id_changed.emit(self.atlas_base_name)
@property
def structure_tree_id(self):
return self.tab.structureTreeIdComboBox.currentText()
@structure_tree_id.setter
def structure_tree_id(self, value):
self.tab.structureTreeIdComboBox.setCurrentText(value)
[docs]
def handle_structure_tree_id_changed(self):
self.config['atlas']['structure_tree_id'] = self.structure_tree_id
self.ui_to_cfg() # TODO: check if required
self.atlas_structure_tree_id_changed.emit(self.structure_tree_id)
[docs]
class CellMapParams(UiParameter):
background_correction_diameter: List[int]
maxima_shape: int
detection_threshold: int
cell_filter_size: List[int]
cell_filter_intensity: List[int]
voxelization_radii: List[int]
detect_cells: bool
filter_cells: bool
voxelize: bool
save_shape: bool
plot_when_finished: bool
plot_detected_cells: bool
crop_x_min: int
crop_x_max: int # TODO: if 99.9 % source put to 100% (None)
crop_y_min: int
crop_y_max: int # TODO: if 99.9 % source put to 100% (None)
crop_z_min: int
crop_z_max: int # TODO: if 99.9 % source put to 100% (None)
def __init__(self, tab, sample_params=None, preprocessing_params=None, src_folder=None):
super().__init__(tab, src_folder)
self.params_dict = {
'background_correction_diameter': ['detection', 'background_correction', 'diameter'],
'maxima_shape': ParamLink(['detection', 'maxima_detection', 'shape'], self.tab.maximaShape),
'detection_threshold': ParamLink(['detection', 'shape_detection', 'threshold'], self.tab.detectionThreshold),
'cell_filter_size': ParamLink(['cell_filtration', 'thresholds', 'size'], self.tab.cellFilterThresholdSizeDoublet),
'cell_filter_intensity': ParamLink(['cell_filtration', 'thresholds', 'intensity'],
self.tab.cellFilterThresholdIntensityDoublet,
connect=False),
'voxelization_radii': ParamLink(['voxelization', 'radii'], self.tab.voxelizationRadiusTriplet),
'detect_cells': ParamLink(None, self.tab.runCellMapDetectCellsCheckBox),
'filter_cells': ParamLink(None, self.tab.runCellMapFilterCellsCheckBox),
'voxelize': ParamLink(None, self.tab.runCellMapVoxelizeCheckBox),
'save_shape': ParamLink(None, self.tab.runCellMapSaveShapeCheckBox),
'plot_when_finished': ParamLink(['run', 'plot_when_finished'], self.tab.runCellMapPlotCheckBox),
'crop_x_min': ParamLink(['detection', 'test_set_slicing', 'dim_0', 0], self.tab.detectionSubsetXRangeMin),
'crop_x_max': ParamLink(['detection', 'test_set_slicing', 'dim_0', 1], self.tab.detectionSubsetXRangeMax),
'crop_y_min': ParamLink(['detection', 'test_set_slicing', 'dim_1', 0], self.tab.detectionSubsetYRangeMin),
'crop_y_max': ParamLink(['detection', 'test_set_slicing', 'dim_1', 1], self.tab.detectionSubsetYRangeMax),
'crop_z_min': ParamLink(['detection', 'test_set_slicing', 'dim_2', 0], self.tab.detectionSubsetZRangeMin),
'crop_z_max': ParamLink(['detection', 'test_set_slicing', 'dim_2', 1], self.tab.detectionSubsetZRangeMax)
}
self.sample_params = sample_params
self.preprocessing_params = preprocessing_params
self.connect()
[docs]
def connect(self):
self.tab.backgroundCorrectionDiameter.valueChanged.connect(self.handle_background_correction_diameter_changed)
self.tab.cellFilterThresholdIntensityDoublet.valueChangedConnect(self.handle_filter_intensity_changed)
self.connect_simple_widgets() # |TODO: automatise in parent class
[docs]
def cfg_to_ui(self):
self.reload()
super().cfg_to_ui()
@property
def ratios(self):
raw_res = np.array(self.sample_params.raw_resolution)
atlas_res = np.array(self.preprocessing_params.registration.atlas_resolution)
ratios = atlas_res / raw_res # to original
return ratios
@property
def background_correction_diameter(self):
return [self.tab.backgroundCorrectionDiameter.value()] * 2
@background_correction_diameter.setter
def background_correction_diameter(self, shape):
if isinstance(shape, (list, tuple)):
shape = shape[0]
self.tab.backgroundCorrectionDiameter.setValue(shape)
[docs]
def handle_background_correction_diameter_changed(self, val):
self.config['detection']['background_correction']['diameter'] = self.background_correction_diameter
@property
def cell_filter_intensity(self):
intensities = self.tab.cellFilterThresholdIntensityDoublet.getValue()
if intensities is None:
return
else:
intensities = list(intensities)
if intensities[-1] == -1:
intensities[-1] = 65536 # FIXME: hard coded, should read max
return intensities
@cell_filter_intensity.setter
def cell_filter_intensity(self, intensity):
self.tab.cellFilterThresholdIntensityDoublet.setValue(intensity)
[docs]
def handle_filter_intensity_changed(self, _):
self.config['cell_filtration']['thresholds']['intensity'] = self.cell_filter_intensity
[docs]
def scale_axis(self, val, axis='x'):
axis_ratio = self.ratios['xyz'.index(axis)]
return round(val * axis_ratio)
[docs]
def reverse_scale_axis(self, val, axis='x'):
axis_ratio = self.ratios['xyz'.index(axis)]
return round(val / axis_ratio)
@property
def slice_tuples(self):
return ((self.crop_x_min, self.crop_x_max),
(self.crop_y_min, self.crop_y_max),
(self.crop_z_min, self.crop_z_max))
@property
def slicing(self):
return tuple([slice(ax[0], ax[1]) for ax in self.slice_tuples])
[docs]
class VesselParams(UiParameterCollection):
def __init__(self, tab, sample_params, preprocessing_params, src_folder=None):
super().__init__(tab, src_folder)
# self.sample_params = sample_params # TODO: check if required
# self.preprocessing_params = preprocessing_params # TODO: check if required
self.binarization_params = VesselBinarizationParams(tab, src_folder)
self.graph_params = VesselGraphParams(tab, src_folder)
self.visualization_params = VesselVisualizationParams(tab, sample_params, preprocessing_params, src_folder)
@property
def params(self):
return self.binarization_params, self.graph_params, self.visualization_params
[docs]
class VesselBinarizationParams(UiParameter):
run_raw_binarization: bool
raw_binarization_clip_range: List[int]
raw_binarization_threshold: int
smooth_raw: bool
binary_fill_raw: bool
fill_main_channel: bool
run_arteries_binarization: bool
arteries_binarization_clip_range: List[int]
arteries_binarization_threshold: int
smooth_arteries: bool
binary_fill_arteries: bool
fill_secondary_channel: bool
fill_combined: bool
plot_step_1: str
plot_step_2: str
plot_channel_1: str
plot_channel_2: str
def __init__(self, tab, src_folder=None):
super().__init__(tab, src_folder)
self.params_dict = {
'run_raw_binarization': ParamLink(['raw', 'binarization', 'run'], self.tab.runRawBinarizationCheckBox),
'raw_binarization_clip_range': ParamLink(['raw', 'binarization', 'clip_range'],
self.tab.rawBinarizationClipRangeDoublet),
'raw_binarization_threshold': ['raw', 'binarization', 'threshold'],
'smooth_raw': ParamLink(['raw', 'smoothing', 'run'], self.tab.rawBinarizationSmoothingCheckBox),
'binary_fill_raw': ParamLink(['raw', 'binary_filling', 'run'], self.tab.rawBinarizationBinaryFillingCheckBox),
'fill_main_channel': ParamLink(['raw', 'deep_filling', 'run'], self.tab.binarizationRawDeepFillingCheckBox),
'run_arteries_binarization': ParamLink(['arteries', 'binarization', 'run'],
self.tab.runArteriesBinarizationCheckBox),
'arteries_binarization_clip_range': ParamLink(['arteries', 'binarization', 'clip_range'],
self.tab.arteriesBinarizationClipRangeDoublet),
'arteries_binarization_threshold': ['arteries', 'binarization', 'threshold'],
'smooth_arteries': ParamLink(['arteries', 'smoothing', 'run'],
self.tab.arteriesBinarizationSmoothingCheckBox),
'binary_fill_arteries': ParamLink(['arteries', 'binary_filling', 'run'],
self.tab.arteriesBinarizationBinaryFillingCheckBox),
'fill_secondary_channel': ParamLink(['arteries', 'deep_filling', 'run'],
self.tab.binarizationArteriesDeepFillingCheckBox),
'fill_combined': ParamLink(['combined', 'binary_fill'], self.tab.binarizationConbineBinaryFillingCheckBox),
'plot_step_1': ParamLink(None, self.tab.binarizationPlotStep1ComboBox),
'plot_step_2': ParamLink(None, self.tab.binarizationPlotStep2ComboBox),
'plot_channel_1': ParamLink(None, self.tab.binarizationPlotChannel1ComboBox),
'plot_channel_2': ParamLink(None, self.tab.binarizationPlotChannel2ComboBox),
}
self.cfg_subtree = ['binarization']
self.connect()
[docs]
def connect(self):
self.tab.rawBinarizationThresholdSpinBox.valueChanged.connect(self.handle_raw_binarization_threshold_changed)
self.tab.arteriesBinarizationThresholdSpinBox.valueChanged.connect(
self.handle_arteries_binarization_threshold_changed)
self.connect_simple_widgets()
@property
def n_steps(self):
n_steps = self.run_raw_binarization
n_steps += self.smooth_raw or self.binary_fill_raw
n_steps += self.fill_main_channel
n_steps += self.run_arteries_binarization
n_steps += self.smooth_arteries or self.binary_fill_arteries
n_steps += self.fill_secondary_channel
n_steps += self.fill_combined
return
@property
def raw_binarization_threshold(self):
return self.sanitize_neg_one(self.tab.rawBinarizationThresholdSpinBox.value())
@raw_binarization_threshold.setter
def raw_binarization_threshold(self, value):
self.tab.rawBinarizationThresholdSpinBox.setValue(self.sanitize_nones(value))
[docs]
def handle_raw_binarization_threshold_changed(self):
self.config['raw']['binarization']['threshold'] = self.raw_binarization_threshold
@property
def arteries_binarization_threshold(self):
return self.sanitize_neg_one(self.tab.arteriesBinarizationThresholdSpinBox.value())
@arteries_binarization_threshold.setter
def arteries_binarization_threshold(self, value):
self.tab.arteriesBinarizationThresholdSpinBox.setValue(self.sanitize_nones(value))
[docs]
def handle_arteries_binarization_threshold_changed(self):
self.config['arteries']['binarization']['threshold'] = self.arteries_binarization_threshold
[docs]
class VesselGraphParams(UiParameter):
skeletonize: bool
build: bool
clean: bool
reduce: bool
transform: bool
annotate: bool
use_arteries: bool
vein_intensity_range_on_arteries_channel: List[int]
restrictive_min_vein_radius: float
permissive_min_vein_radius: float
final_min_vein_radius: float
arteries_min_radius: float
max_arteries_tracing_iterations: int
max_veins_tracing_iterations: int
min_artery_size: int
min_vein_size: int
def __init__(self, tab, src_folder=None):
super().__init__(tab, src_folder)
self.params_dict = {
'skeletonize': ParamLink(['graph_construction', 'skeletonize'], self.tab.buildGraphSkeletonizeCheckBox),
'build': ParamLink(['graph_construction', 'build'], self.tab.buildGraphBuildCheckBox),
'clean': ParamLink(['graph_construction', 'clean'], self.tab.buildGraphCleanCheckBox),
'reduce': ParamLink(['graph_construction', 'reduce'], self.tab.buildGraphReduceCheckBox),
'transform': ParamLink(['graph_construction', 'transform'], self.tab.buildGraphTransformCheckBox),
'annotate': ParamLink(['graph_construction', 'annotate'], self.tab.buildGraphRegisterCheckBox),
'use_arteries': ParamLink(['graph_construction', 'use_arteries'], self.tab.buildGraphUseArteriesCheckBox),
'vein_intensity_range_on_arteries_channel': ParamLink(['vessel_type_postprocessing', 'pre_filtering', 'vein_intensity_range_on_arteries_ch'],
self.tab.veinIntensityRangeOnArteriesChannelDoublet),
'restrictive_min_vein_radius': ParamLink(['vessel_type_postprocessing', 'pre_filtering', 'restrictive_vein_radius'],
self.tab.restrictiveMinVeinRadiusSpinBox),
'permissive_min_vein_radius': ParamLink(['vessel_type_postprocessing', 'pre_filtering', 'permissive_vein_radius'],
self.tab.permissiveMinVeinRadiusSpinBox),
'final_min_vein_radius': ParamLink(['vessel_type_postprocessing', 'pre_filtering', 'final_vein_radius'],
self.tab.finalMinVeinRadiusSpinBox),
'arteries_min_radius': ParamLink(['vessel_type_postprocessing', 'pre_filtering', 'arteries_min_radius'],
self.tab.arteriesMinRadiusSpinBox),
'max_arteries_tracing_iterations': ParamLink(['vessel_type_postprocessing', 'tracing', 'max_arteries_iterations'],
self.tab.maxArteriesTracingIterationsSpinBox),
'max_veins_tracing_iterations': ParamLink(['vessel_type_postprocessing', 'tracing', 'max_veins_iterations'],
self.tab.maxVeinsTracingIterationsSpinBox),
'min_artery_size': ParamLink(['vessel_type_postprocessing', 'capillaries_removal', 'min_artery_size'],
self.tab.minArterySizeSpinBox),
'min_vein_size': ParamLink(['vessel_type_postprocessing', 'capillaries_removal', 'min_vein_size'],
self.tab.minVeinSizeSpinBox)
}
self.connect()
[docs]
def connect(self):
self.connect_simple_widgets()
[docs]
class VesselVisualizationParams(UiParameter):
crop_x_min: int
crop_x_max: int
crop_y_min: int
crop_y_max: int
crop_z_min: int
crop_z_max: int
graph_step: str
plot_type: str
voxelization_size: List[int]
vertex_degrees: str
weight_by_radius: bool
def __init__(self, tab, sample_params=None, preprocessing_params=None, src_folder=None):
super().__init__(tab, src_folder)
self.params_dict = { # TODO: if 99.9 % source put to 100% (None)
'crop_x_min': ParamLink(['slicing', 'dim_0', 0], self.tab.graphConstructionSlicerXRangeMin),
'crop_x_max': ParamLink(['slicing', 'dim_0', 1], self.tab.graphConstructionSlicerXRangeMax),
'crop_y_min': ParamLink(['slicing', 'dim_1', 0], self.tab.graphConstructionSlicerYRangeMin),
'crop_y_max': ParamLink(['slicing', 'dim_1', 1], self.tab.graphConstructionSlicerYRangeMax),
'crop_z_min': ParamLink(['slicing', 'dim_2', 0], self.tab.graphConstructionSlicerZRangeMin),
'crop_z_max': ParamLink(['slicing', 'dim_2', 1], self.tab.graphConstructionSlicerZRangeMax),
'graph_step': ParamLink(None, self.tab.graphSlicerStepComboBox, connect=False),
'plot_type': ParamLink(None, self.tab.graphPlotTypeComboBox, connect=False),
'voxelization_size': ParamLink(['voxelization', 'size'], self.tab.vasculatureVoxelizationRadiusTriplet),
'vertex_degrees': ParamLink(None, self.tab.vasculatureVoxelizationFilterDegreesSinglet, connect=False),
'weight_by_radius': ParamLink(None, self.tab.voxelizationWeightByRadiusCheckBox, connect=False)
# 'filter_name': ParamLink(None, self.tab.voxelizationFitlerNameComboBox, connect=False),
# 'weight_name': ParamLink(None, self.tab.voxelizationWeightNameComboBox, connect=False),
}
self.structure_id = None
self.cfg_subtree = ['visualization']
self.sample_params = sample_params
self.preprocessing_params = preprocessing_params
self.connect()
[docs]
def connect(self):
self.connect_simple_widgets()
[docs]
def set_structure_id(self, structure_widget):
self.structure_id = int(structure_widget.text(1))
@property
def ratios(self):
raw_res = np.array(self.sample_params.raw_resolution)
atlas_res = np.array(self.preprocessing_params.registration.atlas_resolution)
ratios = atlas_res / raw_res # to original
return ratios
[docs]
def scale_axis(self, val, axis='x'):
return round(val * self.ratios['xyz'.index(axis)])
[docs]
def reverse_scale_axis(self, val, axis='x'):
axis_ratio = self.ratios['xyz'.index(axis)]
return round(val / axis_ratio)
@property
def slice_tuples(self):
return ((self.crop_x_min, self.crop_x_max),
(self.crop_y_min, self.crop_y_max),
(self.crop_z_min, self.crop_z_max))
@property
def slicing(self):
return tuple([slice(ax[0], ax[1]) for ax in self.slice_tuples])
[docs]
class PreferencesParams(UiParameter):
verbosity: str
n_processes_file_conv: int
n_processes_resampling: int
n_processes_stitching: int
n_processes_cell_detection: int
n_processes_binarization: int
chunk_size_min: int
chunk_size_max: int
chunk_size_overlap: int
start_folder: str
start_full_screen: bool
lut: str
font_size: int
pattern_finder_min_n_files: int
three_d_plot_bg: str
def __init__(self, tab, src_folder=None):
super().__init__(tab, src_folder)
self.params_dict = {
'verbosity': ['verbosity'],
'n_processes_file_conv': ['n_processes_file_conv'],
'n_processes_resampling': ['n_processes_resampling'],
'n_processes_stitching': ['n_processes_stitching'],
'n_processes_cell_detection': ['n_processes_cell_detection'],
'n_processes_binarization': ['n_processes_binarization'],
'chunk_size_min': ParamLink(['detection_chunk_size_min'], self.tab.chunkSizeMinSpinBox, connect=False),
'chunk_size_max': ParamLink(['detection_chunk_size_max'], self.tab.chunkSizeMaxSpinBox, connect=False),
'chunk_size_overlap': ParamLink(['detection_chunk_overlap'], self.tab.chunkSizeOverlapSpinBox, connect=False),
'start_folder': ParamLink(['start_folder'], self.tab.startFolderLineEdit, connect=False),
'start_full_screen': ParamLink(['start_full_screen'], self.tab.startFullScreenCheckBox, connect=False),
'lut': ['default_lut'],
'font_size': ParamLink(['font_size'], self.tab.fontSizeSpinBox, connect=False),
'pattern_finder_min_n_files': ParamLink(['pattern_finder_min_n_files'],
self.tab.patternFinderMinFilesSpinBox, connect=False),
'three_d_plot_bg': ['three_d_plot_bg']
}
self.connect()
def _ui_to_cfg(self): # TODO: check if live update (i.e. connected handlers) or only on save
cfg = self._config
cfg['verbosity'] = self.verbosity
cfg['n_processes_file_conv'] = self.n_processes_file_conv
cfg['n_processes_resampling'] = self.n_processes_resampling
cfg['n_processes_stitching'] = self.n_processes_stitching
cfg['n_processes_cell_detection'] = self.n_processes_cell_detection
cfg['n_processes_binarization'] = self.n_processes_binarization
cfg['detection_chunk_size_min'] = self.chunk_size_min
cfg['detection_chunk_size_max'] = self.chunk_size_max
cfg['detection_chunk_overlap'] = self.chunk_size_overlap
cfg['start_folder'] = self.start_folder
cfg['start_full_screen'] = self.start_full_screen
cfg['default_lut'] = self.lut
cfg['font_size'] = self.font_size
cfg['pattern_finder_min_n_files'] = self.pattern_finder_min_n_files
cfg['three_d_plot_bg'] = self.three_d_plot_bg
[docs]
def cfg_to_ui(self):
self.reload()
super().cfg_to_ui()
@property
def three_d_plot_bg(self):
return self.tab.threeDPlotsBackgroundComboBox.currentText().lower()
@three_d_plot_bg.setter
def three_d_plot_bg(self, value):
self.tab.threeDPlotsBackgroundComboBox.setCurrentText(value)
@property
def verbosity(self):
return self.tab.verbosityComboBox.currentText().lower()
@verbosity.setter
def verbosity(self, lvl):
self.tab.verbosityComboBox.setCurrentText(lvl.capitalize())
@property
def n_processes_file_conv(self):
return self.sanitize_neg_one(self.tab.nProcessesFileConversionSpinBox.value())
@n_processes_file_conv.setter
def n_processes_file_conv(self, n_procs):
self.tab.nProcessesFileConversionSpinBox.setValue(self.sanitize_nones(n_procs))
@property
def n_processes_stitching(self):
return self.sanitize_neg_one(self.tab.nProcessesStitchingSpinBox.value())
@n_processes_stitching.setter
def n_processes_stitching(self, value):
self.tab.nProcessesStitchingSpinBox.setValue(self.sanitize_nones(value))
@property
def n_processes_resampling(self):
return self.sanitize_neg_one(self.tab.nProcessesResamplingSpinBox.value())
@n_processes_resampling.setter
def n_processes_resampling(self, value):
self.tab.nProcessesResamplingSpinBox.setValue(self.sanitize_nones(value))
@property
def n_processes_cell_detection(self):
return self.sanitize_neg_one(self.tab.nProcessesCellDetectionSpinBox.value())
@n_processes_cell_detection.setter
def n_processes_cell_detection(self, n_procs):
self.tab.nProcessesCellDetectionSpinBox.setValue(self.sanitize_nones(n_procs))
@property
def n_processes_binarization(self):
return self.sanitize_neg_one(self.tab.nProcessesBinarizationSpinBox.value())
@n_processes_binarization.setter
def n_processes_binarization(self, value):
self.tab.nProcessesBinarizationSpinBox.setValue(self.sanitize_nones(value))
@property
def lut(self):
return self.tab.lutComboBox.currentText().lower()
@lut.setter
def lut(self, lut_name):
self.tab.lutComboBox.setCurrentText(lut_name)
@property
def font_family(self):
return self.tab.fontComboBox.currentFont().family()
# @font_family.setter
# def font_family(self, font):
# self.tab.fontComboBox.setCurrentFont(font)
[docs]
class BatchParameters(UiParameter):
def __init__(self, tab, src_folder=None, preferences=None):
super().__init__(tab, src_folder)
self.group_concatenator = ' vs '
self.preferences = preferences
self.tab.sampleFoldersToolBox = QToolBox(parent=self.tab)
self.tab.sampleFoldersPageLayout.addWidget(self.tab.sampleFoldersToolBox, 3, 0)
def _ui_to_cfg(self):
self.config['paths']['results_folder'] = self.results_folder
self.config['groups'] = self.groups
[docs]
def cfg_to_ui(self):
self.reload()
self.results_folder = self.config['paths']['results_folder']
for i in range(len(self.config['groups'].keys())):
self.add_group()
self.group_names = self.config['groups'].keys() # FIXME: check that ordered
for i, paths in enumerate(self.config['groups'].values()):
self.set_paths(i+1, paths)
def __connect_btn(self, btn, callback):
try:
btn.clicked.connect(callback, type=Qt.UniqueConnection)
except TypeError as err:
if err.args[0] == 'connection is not unique':
btn.clicked.disconnect()
btn.clicked.connect(callback, type=Qt.UniqueConnection)
else:
raise err
def _connect_line_edit(self, ctrl, callback):
try:
ctrl.editingFinished.connect(callback, type=Qt.UniqueConnection)
except TypeError as err:
if err.args[0] == 'connection is not unique':
ctrl.editingFinished.disconnect()
ctrl.editingFinished.connect(callback, type=Qt.UniqueConnection)
else:
raise err
[docs]
def connect(self):
self.tab.addGroupPushButton.clicked.connect(self.add_group)
self.tab.removeGroupPushButton.clicked.connect(self.remove_group)
self.tab.resultsFolderLineEdit.textChanged.connect(self.handle_results_folder_changed)
# self.connect_simple_widgets()
[docs]
def connect_groups(self):
for btn in self.gp_add_folder_buttons:
self.__connect_btn(btn, self.handle_add_src_folder_clicked)
for btn in self.gp_remove_folder_buttons:
self.__connect_btn(btn, self.handle_remove_src_folder_clicked)
[docs]
def add_group(self): # REFACTOR: better in tab object
new_gp_id = self.n_groups + 1
group_controls = create_clearmap_widget('sample_group_controls.ui', patch_parent_class='QWidget')
self.tab.sampleFoldersToolBox.addItem(group_controls, f'Group {new_gp_id}')
self.connect_groups()
[docs]
def remove_group(self):
last_idx = self.tab.sampleFoldersToolBox.count() - 1
widg = self.tab.sampleFoldersToolBox.widget(last_idx)
self.tab.sampleFoldersToolBox.removeItem(last_idx)
widg.setParent(None)
widg.deleteLater()
@property
def n_groups(self):
return self.tab.sampleFoldersToolBox.count()
@property
def group_names(self):
return [lbl.text() for lbl in self.gp_group_name_ctrls]
@group_names.setter
def group_names(self, names):
for w, name in zip(self.gp_group_name_ctrls, names):
w.setText(name)
@property
def gp_group_name_ctrls(self):
return self.get_gp_ctrls('NameLineEdit')
@property
def gp_add_folder_buttons(self):
return self.get_gp_ctrls('AddSrcFolderBtn')
@property
def gp_remove_folder_buttons(self):
return self.get_gp_ctrls('RemoveSrcFolderBtn')
@property
def gp_list_widget(self):
return self.get_gp_ctrls('ListWidget')
[docs]
def get_gp_ctrls(self, ctrl_name):
return [getattr(self.tab.sampleFoldersToolBox.widget(i), f'gp{ctrl_name}') for i in range(self.n_groups)]
[docs]
def set_paths(self, gp, paths):
list_widget = self.gp_list_widget[gp - 1]
list_widget.clear()
list_widget.addItems(paths)
[docs]
def get_paths(self, gp): # TODO: should exist from group name
list_widget = self.gp_list_widget[gp - 1]
return [list_widget.item(i).text() for i in range(list_widget.count())]
[docs]
def get_all_paths(self):
return [self.get_paths(gp + 1) for gp in range(self.n_groups)]
@property
def groups(self):
return {gp: paths for gp, paths in zip(self.group_names, self.get_all_paths())}
[docs]
def handle_add_src_folder_clicked(self):
gp = self.tab.sampleFoldersToolBox.currentIndex()
folder_path = get_directory_dlg(self.preferences.start_folder, 'Select sample folder')
if folder_path:
self.gp_list_widget[gp].addItem(folder_path)
[docs]
def handle_remove_src_folder_clicked(self):
# print('call')
gp = self.tab.sampleFoldersToolBox.currentIndex()
sample_idx = self.gp_list_widget[gp].currentRow()
_ = self.gp_list_widget[gp].takeItem(sample_idx)
# print(_.text())
@property
def results_folder(self):
return self.tab.resultsFolderLineEdit.text()
@results_folder.setter
def results_folder(self, value):
self.tab.resultsFolderLineEdit.setText(value)
[docs]
def handle_results_folder_changed(self):
self.config['paths']['results_folder'] = self.results_folder
[docs]
class GroupAnalysisParams(BatchParameters):
"""
Essentially batch parameters with comparisons
"""
def __init__(self, tab, src_folder=None, preferences=None):
super().__init__(tab, src_folder, preferences)
self.comparison_checkboxes = []
def _ui_to_cfg(self):
super()._ui_to_cfg()
self.config['comparisons'] = {letter: pair for letter, pair in zip(string.ascii_lowercase,
self.selected_comparisons)}
[docs]
def cfg_to_ui(self):
super().cfg_to_ui()
self.update_comparisons()
for chk_bx in self.comparison_checkboxes:
if chk_bx.text().split(self.group_concatenator) in self.config['comparisons'].values():
self.set_check_state(chk_bx, True)
[docs]
def connect_groups(self):
super().connect_groups()
for ctrl in self.gp_group_name_ctrls:
self._connect_line_edit(ctrl, self.update_comparisons)
@property
def comparisons(self):
"""
Returns
-------
The list of all possible pairs of groups
"""
# return list(combinations(self.group_names, 2))
return list(permutations(self.group_names, 2))
@property
def selected_comparisons(self):
return [box.text().split(self.group_concatenator) for box in self.comparison_checkboxes if box.isChecked()]
[docs]
def update_comparisons(self):
clear_layout(self.tab.comparisonsVerticalLayout)
self.comparison_checkboxes = []
for pair in self.comparisons:
chk = QCheckBox(self.group_concatenator.join(pair))
chk.setChecked(False)
self.tab.comparisonsVerticalLayout.addWidget(chk)
self.comparison_checkboxes.append(chk)
self.tab.comparisonsVerticalLayout.addStretch()
self.plot_density_maps_buttons = []
for gp in self.group_names:
btn = QPushButton(f'Plot {gp} group density maps')
self.tab.comparisonsVerticalLayout.addWidget(btn)
self.plot_density_maps_buttons.append(btn)
[docs]
class BatchProcessingParams(BatchParameters):
"""
Essentially BatchParameters with processing steps
"""
def __init__(self, tab, src_folder=None, preferences=None):
super().__init__(tab, src_folder, preferences)
@property
def align(self):
return self.tab.batchAlignCheckBox.isChecked()
@align.setter
def align(self, value):
self.tab.batchAlignCheckBox.setChecked(value)
# def handle_align_changed(self):
# self.
@property
def count_cells(self):
return self.tab.batchCountCellsCheckBox.isChecked()
@count_cells.setter
def count_cells(self, value):
self.tab.batchCountCellsCheckBox.isChecked()
# def handle_count_cells_changed(self):
@property
def run_vaculature(self):
return self.tab.batchVasculatureCheckBox.isChecked()
@run_vaculature.setter
def run_vaculature(self, value):
self.tab.batchVasculatureCheckBox.setChecked(value)