diff --git a/.gitignore b/.gitignore index aa9a304dc..4c2ca6976 100644 --- a/.gitignore +++ b/.gitignore @@ -479,7 +479,6 @@ test_data/ /PartSegCore/segmentation/multiscale_opening/*.gch package/PartSegCore/multiscale_opening/\.vscode/ -package/PartSegCore/multiscale_opening/\.vscode/ /tutorials/tutorial-chromosome-1/tutorial-chromosome1.pdf !/package/tests/test_data/measurements_profile.json @@ -489,3 +488,6 @@ package/PartSegCore/multiscale_opening/\.vscode/ *.e6t !docs/Makefile + +!package/tests/test_data/old_saves/*.[0-9] +!package/tests/test_data/old_saves/*.[0-9][0-9] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1436fab8..ef9eb4e4b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_language_version: repos: - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black pass_filenames: true diff --git a/MANIFEST.in b/MANIFEST.in index 2f37c6cc8..b3a3c7e6c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,8 @@ global-include *.pyx global-include *.pxd include package/tests/test_data/ome.xsd.xml include package/tests/test_data/napari_measurements_profile.json +include package/tests/test_data/notebook/*.json +include package/tests/test_data/old_saves/*/*/*.json include Readme.md include changelog.md include pyproject.toml diff --git a/docs/PartSegCore/base.rst b/docs/PartSegCore/base.rst index 2fb62f789..89eec52b1 100644 --- a/docs/PartSegCore/base.rst +++ b/docs/PartSegCore/base.rst @@ -8,12 +8,6 @@ On this page there are described base classes of PartSegCore module .. automodule:: PartSegCore.algorithm_describe_base :members: -.channel_class --------------- -.. automodule:: PartSegCore.channel_class - :members: - :show-inheritance: - .image_operations ----------------- .. automodule:: PartSegCore.image_operations diff --git a/docs/PartSegCore/class_register.rst b/docs/PartSegCore/class_register.rst new file mode 100644 index 000000000..1c281c0a8 --- /dev/null +++ b/docs/PartSegCore/class_register.rst @@ -0,0 +1,9 @@ +PartSegCore.class_register +========================== + + + +.. automodule:: PartSegCore.class_register + :members: + :inherited-members: + :show-inheritance: diff --git a/docs/PartSegCore/index.rst b/docs/PartSegCore/index.rst index bed354d47..289d0dc85 100644 --- a/docs/PartSegCore/index.rst +++ b/docs/PartSegCore/index.rst @@ -8,6 +8,7 @@ This package contains calculation backend of PartSeg :caption: Contents: base + class_register color_image segmentation analysis/base diff --git a/docs/PartSegImage.rst b/docs/PartSegImage.rst index b17b43a3a..e3878201d 100644 --- a/docs/PartSegImage.rst +++ b/docs/PartSegImage.rst @@ -14,10 +14,3 @@ PartSegImage.image_reader :members: :exclude-members: ImageReader, MyTiffPage :show-inheritance: - - -PartSegImage.tifffile_fixes ---------------------------- - -.. autoclass:: PartSegImage.tifffile_fixes.MyTiffPage - :members: asarray diff --git a/docs/conf.py b/docs/conf.py index e15a1d47a..d29737246 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,12 +13,15 @@ # import os # import sys # sys.path.insert(0, os.path.abspath('.')) + +from datetime import date + import PartSeg # -- Project information ----------------------------------------------------- project = "PartSeg" -copyright = "2019, Laboratory of Functional and Structural Genomics" +copyright = f"{date.today().year}, Laboratory of Functional and Structural Genomics" author = "Grzegorz Bokota (LFSG)" # The full version, including alpha/beta/rc tags @@ -37,7 +40,9 @@ "sphinx_qt_documentation", "sphinx.ext.viewcode", "sphinx.ext.graphviz", + "sphinx_autodoc_typehints", "PartSegCore.sphinx.auto_parameters", + "sphinxcontrib.autodoc_pydantic", ] # Add any paths that contain templates here, relative to this directory. @@ -67,6 +72,7 @@ "python": ("https://docs.python.org/3", None), "PyQt5": ("https://www.riverbankcomputing.com/static/Docs/PyQt5", None), "Numpy": ("https://docs.scipy.org/doc/numpy/", None), + "packaging": ("https://packaging.pypa.io/en/latest/", None), } qt_documentation = "Qt5" diff --git a/docs/custom_application.rst b/docs/custom_application.rst deleted file mode 100644 index 8bb18ce54..000000000 --- a/docs/custom_application.rst +++ /dev/null @@ -1,6 +0,0 @@ -PartSeg.custom\_application -=========================== - -.. automodule:: PartSeg.custom_application.application - :members: - :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst index dacdc32ec..0ce21d7fb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,7 +15,6 @@ The documentation is incomplete. Many utilities are undocumented. PartSegCore/index common_gui/index common_backend/index - custom_application PartSegImage interface-overview/interface-overview diff --git a/docs/requirements.txt b/docs/requirements.txt index 42c2d9fba..a8725c5ba 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ -sphinx-autodoc-annotation==1.0.post1 +sphinx-autodoc-typehints==1.17.0 +autodoc_pydantic==1.6.1 sphinx-qt-documentation==0.3 sphinx!=3.0.0, !=3.5.0 diff --git a/package/PartSeg/_roi_analysis/advanced_window.py b/package/PartSeg/_roi_analysis/advanced_window.py index 15bd85d90..083beb401 100644 --- a/package/PartSeg/_roi_analysis/advanced_window.py +++ b/package/PartSeg/_roi_analysis/advanced_window.py @@ -2,7 +2,7 @@ import os from contextlib import suppress from copy import deepcopy -from typing import Optional, Tuple, Union +from typing import Optional, Tuple, Union, cast from qtpy.QtCore import QEvent, Qt, Slot from qtpy.QtGui import QIcon @@ -31,7 +31,7 @@ from superqt import QEnumComboBox, ensure_main_thread from PartSeg.common_gui.advanced_tabs import AdvancedWindow -from PartSegCore.analysis.algorithm_description import analysis_algorithm_dict +from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection from PartSegCore.analysis.measurement_base import AreaType, Leaf, MeasurementEntry, Node, PerComponent from PartSegCore.analysis.measurement_calculation import MEASUREMENT_DICT, MeasurementProfile from PartSegCore.universal_const import UNIT_SCALE, Units @@ -108,7 +108,7 @@ def __init__(self, settings: PartSettings): spacing_layout = QHBoxLayout() spacing_layout.addWidget(self.lock_spacing) for txt, el in zip(["x", "y", "z"], self.spacing[::-1]): - spacing_layout.addWidget(QLabel(txt + ":")) + spacing_layout.addWidget(QLabel(f"{txt}:")) spacing_layout.addWidget(el) spacing_layout.addWidget(self.units) spacing_layout.addStretch(1) @@ -167,14 +167,15 @@ def profile_chosen(self, text): # TODO update with knowledge from profile dict self.delete_btn.setEnabled(True) self.rename_btn.setEnabled(True) - self.info_label.setPlainText(profile.pretty_print(analysis_algorithm_dict)) + self.info_label.setPlainText(profile.pretty_print(AnalysisAlgorithmSelection.__register__)) def synchronize_spacing(self): if self.lock_spacing.isChecked(): self.spacing[1].setValue(self.spacing[2].value()) def image_spacing_change(self): - spacing = [el.value() / UNIT_SCALE[self.units.currentIndex()] for i, el in enumerate(self.spacing)] + spacing = [el.value() / UNIT_SCALE[self.units.currentIndex()] for el in self.spacing] + if not self.spacing[0].isEnabled(): spacing = spacing[1:] self._settings.image_spacing = spacing @@ -637,7 +638,7 @@ def get_parameters(self, node: Union[Node, Leaf], area: AreaType, component: Per node = node.replace_(per_component=component) with suppress(KeyError): arguments = MEASUREMENT_DICT[str(node.name)].get_fields() - if len(arguments) > 0 and len(node.dict) == 0: + if len(arguments) > 0 and len(dict(node.parameters)) == 0: dial = self.form_dialog(arguments) if dial.exec_(): node = node._replace(dict=dial.get_values()) @@ -697,9 +698,9 @@ def save_action(self): return selected_values = [] for i in range(self.profile_options_chosen.count()): - element: MeasurementListWidgetItem = self.profile_options_chosen.item(i) - selected_values.append(MeasurementEntry(element.text(), element.stat)) - stat_prof = MeasurementProfile(self.profile_name.text(), selected_values) + element = cast(MeasurementListWidgetItem, self.profile_options_chosen.item(i)) + selected_values.append(MeasurementEntry(name=element.text(), calculation_tree=element.stat)) + stat_prof = MeasurementProfile(name=self.profile_name.text(), chosen_fields=selected_values) self.settings.measurement_profiles[stat_prof.name] = stat_prof self.settings.dump() self.export_profiles_butt.setEnabled(True) @@ -722,9 +723,11 @@ def named_save_action(self): if val_dialog.exec_(): selected_values = [] for i in range(self.profile_options_chosen.count()): - element: MeasurementListWidgetItem = self.profile_options_chosen.item(i) - selected_values.append(MeasurementEntry(val_dialog.result[element.text()], element.stat)) - stat_prof = MeasurementProfile(self.profile_name.text(), selected_values) + element = cast(MeasurementListWidgetItem, self.profile_options_chosen.item(i)) + selected_values.append( + MeasurementEntry(name=val_dialog.result[element.text()], calculation_tree=element.stat) + ) + stat_prof = MeasurementProfile(name=self.profile_name.text(), chosen_fields=selected_values) self.settings.measurement_profiles[stat_prof.name] = stat_prof self.export_profiles_butt.setEnabled(True) diff --git a/package/PartSeg/_roi_analysis/main_window.py b/package/PartSeg/_roi_analysis/main_window.py index a071e1442..ca134b693 100644 --- a/package/PartSeg/_roi_analysis/main_window.py +++ b/package/PartSeg/_roi_analysis/main_window.py @@ -104,7 +104,7 @@ def __init__( self.choose_profile.textActivated.connect(self.change_profile) self.interactive_use.stateChanged.connect(self.execute_btn.setDisabled) self.interactive_use.stateChanged.connect(self.interactive_change) - self.algorithm_choose_widget = AlgorithmChoose(settings, algorithm_description.analysis_algorithm_dict) + self.algorithm_choose_widget = AlgorithmChoose(settings, algorithm_description.AnalysisAlgorithmSelection) self.algorithm_choose_widget.result.connect(self.execution_done) self.algorithm_choose_widget.finished.connect(self.calculation_finished) self.algorithm_choose_widget.value_changed.connect(self.interactive_algorithm_execute) @@ -627,7 +627,7 @@ def image_read(self): self.options_panel.interactive_algorithm_execute() def reload(self): - self.options_panel.algorithm_choose_widget.reload(algorithm_description.analysis_algorithm_dict) + self.options_panel.algorithm_choose_widget.reload(algorithm_description.AnalysisAlgorithmSelection) def closeEvent(self, event): self.settings.set_in_profile("main_window_geometry", self.saveGeometry().toHex().data().decode("ascii")) diff --git a/package/PartSeg/_roi_analysis/partseg_settings.py b/package/PartSeg/_roi_analysis/partseg_settings.py index f67287165..e72cefdc2 100644 --- a/package/PartSeg/_roi_analysis/partseg_settings.py +++ b/package/PartSeg/_roi_analysis/partseg_settings.py @@ -10,11 +10,11 @@ from PartSegCore.analysis.io_utils import MaskInfo, ProjectTuple from PartSegCore.analysis.load_functions import load_metadata from PartSegCore.analysis.measurement_calculation import MeasurementProfile -from PartSegCore.analysis.save_hooks import PartEncoder from PartSegCore.io_utils import PointsInfo -from PartSegCore.json_hooks import EventedDict, ProfileDict +from PartSegCore.json_hooks import PartSegEncoder from PartSegCore.project_info import HistoryElement from PartSegCore.roi_info import ROIInfo +from PartSegCore.utils import EventedDict, ProfileDict from ..common_backend.base_settings import BaseSettings, SaveSettingsDescription @@ -29,7 +29,7 @@ class PartSettings(BaseSettings): roi_pipelines_changed = Signal() measurement_profiles_changed = Signal() batch_plans_changed = Signal() - json_encoder_class = PartEncoder + json_encoder_class = PartSegEncoder load_metadata = staticmethod(load_metadata) last_executed_algorithm: str save_locations_keys = [ @@ -60,8 +60,8 @@ def fix_history(self, algorithm_name, algorithm_values): :param str algorithm_name: :param dict algorithm_values: """ - self.history[self.history_index + 1] = self.history[self.history_index + 1].replace_( - roi_extraction_parameters={"algorithm_name": algorithm_name, "values": algorithm_values} + self.history[self.history_index + 1] = self.history[self.history_index + 1].copy( + update=dict(roi_extraction_parameters={"algorithm_name": algorithm_name, "values": algorithm_values}) ) @staticmethod diff --git a/package/PartSeg/_roi_analysis/prepare_plan_widget.py b/package/PartSeg/_roi_analysis/prepare_plan_widget.py index d15379c5a..3d17fdcfa 100644 --- a/package/PartSeg/_roi_analysis/prepare_plan_widget.py +++ b/package/PartSeg/_roi_analysis/prepare_plan_widget.py @@ -34,7 +34,7 @@ from superqt import QEnumComboBox from PartSegCore.algorithm_describe_base import AlgorithmProperty, ROIExtractionProfile -from PartSegCore.analysis.algorithm_description import analysis_algorithm_dict +from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection from PartSegCore.analysis.calculation_plan import ( CalculationPlan, MaskBase, @@ -599,7 +599,7 @@ def create_mask(self): return names = dial.get_result() - mask_ob = MaskConstruct(text, *names) + mask_ob = MaskConstruct(name=text, mask1=names[0], mask2=names[1]) else: raise ValueError("Unknowsn widget") @@ -868,7 +868,7 @@ def show_segment_info(self, text=None): else: return profile = self.settings.roi_pipelines[text] - self.information.setText(profile.pretty_print(analysis_algorithm_dict)) + self.information.setText(profile.pretty_print(AnalysisAlgorithmSelection)) def show_segment(self): if self.update_element_chk.isChecked() and self.segment_stack.currentIndex() == 0: @@ -963,7 +963,7 @@ def explore_tree(self, up_widget, node_plan, deep=True): desc = QTreeWidgetItem(widget) desc.setText(0, "Description") if isinstance(node_plan.operation, ROIExtractionProfile): - txt = node_plan.operation.pretty_print(analysis_algorithm_dict) + txt = node_plan.operation.pretty_print(AnalysisAlgorithmSelection) else: txt = str(node_plan.operation) for line in txt.split("\n")[1:]: @@ -1015,7 +1015,7 @@ def update_view(self, reset=False): child = node.child(0) child.takeChildren() if isinstance(el.operation, ROIExtractionProfile): - txt = el.operation.pretty_print(analysis_algorithm_dict) + txt = el.operation.pretty_print(AnalysisAlgorithmSelection) else: txt = str(el.operation) for line in txt.split("\n")[1:]: diff --git a/package/PartSeg/_roi_analysis/profile_export.py b/package/PartSeg/_roi_analysis/profile_export.py index b4f32bd88..b63733d4b 100644 --- a/package/PartSeg/_roi_analysis/profile_export.py +++ b/package/PartSeg/_roi_analysis/profile_export.py @@ -19,7 +19,7 @@ from PartSeg.common_gui.searchable_list_widget import SearchableListWidget from PartSegCore.algorithm_describe_base import ROIExtractionProfile -from PartSegCore.analysis.algorithm_description import analysis_algorithm_dict +from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection class ExportDialog(QDialog): @@ -314,5 +314,5 @@ class ProfileDictViewer(ObjectPreview): """ def preview_object(self, ob: ROIExtractionProfile): - text = ob.pretty_print(analysis_algorithm_dict) + text = ob.pretty_print(AnalysisAlgorithmSelection.__register__) self.setText(text) diff --git a/package/PartSeg/_roi_mask/batch_proceed.py b/package/PartSeg/_roi_mask/batch_proceed.py index 2bf32fab1..f3bac3825 100644 --- a/package/PartSeg/_roi_mask/batch_proceed.py +++ b/package/PartSeg/_roi_mask/batch_proceed.py @@ -9,7 +9,7 @@ from PartSeg._roi_mask.stack_settings import StackSettings, get_mask from PartSegCore.algorithm_describe_base import ROIExtractionProfile -from PartSegCore.mask.algorithm_description import mask_algorithm_dict +from PartSegCore.mask.algorithm_description import MaskAlgorithmSelection from PartSegCore.mask.io_functions import LoadROIImage, LoadStackImage, MaskProjectTuple, SaveROI from PartSegCore.segmentation import StackAlgorithm from PartSegCore.segmentation.algorithm_base import ROIExtractionAlgorithm @@ -66,7 +66,7 @@ def run_calculation(self): try: name = path.basename(file_path) blank = get_mask(project_tuple.roi, project_tuple.mask, project_tuple.selected_components) - algorithm: StackAlgorithm = mask_algorithm_dict[task.parameters.algorithm]() + algorithm: StackAlgorithm = MaskAlgorithmSelection[task.parameters.algorithm]() algorithm.set_image(project_tuple.image) algorithm.set_mask(blank) algorithm.set_parameters(**task.parameters.values) @@ -81,7 +81,7 @@ def run_calculation(self): ) if isinstance(task.save_prefix, tuple): self.progress_info(name, "saving", algorithm.get_steps_num()) - name = path.splitext(path.basename(file_path))[0] + ".seg" + name = f"{path.splitext(path.basename(file_path))[0]}.seg" re_end = re.compile(r"(.*_version)(\d+)\.seg$") while path.exists(path.join(task.save_prefix[0], name)): match = re_end.match(name) diff --git a/package/PartSeg/_roi_mask/main_window.py b/package/PartSeg/_roi_mask/main_window.py index 3e799dba1..e1a798b27 100644 --- a/package/PartSeg/_roi_mask/main_window.py +++ b/package/PartSeg/_roi_mask/main_window.py @@ -30,7 +30,7 @@ from PartSegCore import UNIT_SCALE, Units, state_store from PartSegCore.io_utils import WrongFileTypeException from PartSegCore.mask import io_functions -from PartSegCore.mask.algorithm_description import mask_algorithm_dict +from PartSegCore.mask.algorithm_description import MaskAlgorithmSelection from PartSegCore.mask.history_utils import create_history_element_from_segmentation_tuple from PartSegCore.mask.io_functions import LoadROI, LoadROIFromTIFF, LoadROIParameters, MaskProjectTuple, SaveROI from PartSegCore.project_info import HistoryElement, HistoryProblem, calculate_mask_from_project @@ -168,7 +168,7 @@ def show_advanced_window(self): self.advanced_window.show() def reload(self): - self.parent().parent().options_panel.algorithm_options.algorithm_choose_widget.reload(mask_algorithm_dict) + self.parent().parent().options_panel.algorithm_options.algorithm_choose_widget.reload(MaskAlgorithmSelection) def load_image(self): # TODO move segmentation with image load to load_segmentaion @@ -467,8 +467,7 @@ def get_chosen(self): def get_mask(self): res = [0] - for _, check in sorted(self.check_box.items()): - res.append(check.isChecked()) + res.extend(check.isChecked() for _, check in sorted(self.check_box.items())) return np.array(res, dtype=np.uint8) @@ -500,7 +499,7 @@ def __init__(self, settings: StackSettings, image_view: StackImageView): self.execute_all_btn.setDisabled(True) self.save_parameters_btn = QPushButton("Save parameters") self.block_execute_all_btn = False - self.algorithm_choose_widget = AlgorithmChoose(settings, mask_algorithm_dict) + self.algorithm_choose_widget = AlgorithmChoose(settings, MaskAlgorithmSelection) self.algorithm_choose_widget.result.connect(self.execution_result_set) self.algorithm_choose_widget.finished.connect(self.execution_finished) self.algorithm_choose_widget.progress_signal.connect(self.progress_info) @@ -828,9 +827,7 @@ def set_image_path(self, value): self.update_spacing() def image_spacing_change(self): - self._settings.image_spacing = [ - el.value() / UNIT_SCALE[self.units.currentIndex()] for i, el in enumerate(self.spacing[::-1]) - ] + self._settings.image_spacing = [el.value() / UNIT_SCALE[self.units.currentIndex()] for el in self.spacing[::-1]] def showEvent(self, _a0): units_value = self._settings.get("units_value", Units.nm) diff --git a/package/PartSeg/_roi_mask/segmentation_info_dialog.py b/package/PartSeg/_roi_mask/segmentation_info_dialog.py index 8617ee953..c01dd96e9 100644 --- a/package/PartSeg/_roi_mask/segmentation_info_dialog.py +++ b/package/PartSeg/_roi_mask/segmentation_info_dialog.py @@ -4,7 +4,7 @@ from qtpy.QtWidgets import QGridLayout, QLabel, QListWidget, QPlainTextEdit, QPushButton, QWidget from PartSegCore.algorithm_describe_base import ROIExtractionProfile -from PartSegCore.mask.algorithm_description import mask_algorithm_dict +from PartSegCore.mask.algorithm_description import MaskAlgorithmSelection from .stack_settings import StackSettings @@ -53,9 +53,7 @@ def set_additional_text(self, text): @property def get_parameters(self): - if self.parameters_dict: - return self.parameters_dict - return self.settings.components_parameters_dict + return self.parameters_dict or self.settings.components_parameters_dict def change_component_info(self): if self.components.currentItem() is None: @@ -65,7 +63,7 @@ def change_component_info(self): if parameters is None: self.description.setPlainText("None") else: - self.description.setPlainText(f"Component {text}\n" + parameters.pretty_print(mask_algorithm_dict)) + self.description.setPlainText(f"Component {text}\n" + parameters.pretty_print(MaskAlgorithmSelection)) def set_parameter_action(self): if self.components.currentItem() is None: diff --git a/package/PartSeg/_roi_mask/simple_measurements.py b/package/PartSeg/_roi_mask/simple_measurements.py index 0e519eee0..4094e345f 100644 --- a/package/PartSeg/_roi_mask/simple_measurements.py +++ b/package/PartSeg/_roi_mask/simple_measurements.py @@ -20,6 +20,7 @@ from PartSeg.common_gui.universal_gui_part import ChannelComboBox from PartSeg.common_gui.waiting_dialog import ExecuteFunctionDialog from PartSegCore import Units +from PartSegCore.algorithm_describe_base import base_model_to_algorithm_property from PartSegCore.analysis.measurement_base import AreaType, Leaf, MeasurementEntry, PerComponent from PartSegCore.analysis.measurement_calculation import MEASUREMENT_DICT, MeasurementProfile, MeasurementResult @@ -84,7 +85,9 @@ def calculate(self): QMessageBox.warning(self, "No measurement", "Select at least one measurement") return - profile = MeasurementProfile("", [MeasurementEntry(name=x.name, calculation_tree=x) for x in to_calculate]) + profile = MeasurementProfile( + name="", chosen_fields=[MeasurementEntry(name=x.name, calculation_tree=x) for x in to_calculate] + ) dial = ExecuteFunctionDialog( profile.calculate, @@ -124,7 +127,9 @@ def refresh_measurements(self): area = val.get_starting_leaf().area pc = val.get_starting_leaf().per_component if ( - val.get_fields() + base_model_to_algorithm_property(val.__argument_class__) + if val.__new_style__ + else val.get_fields() or (area is not None and area != AreaType.ROI) or (pc is not None and pc != PerComponent.Yes) ): diff --git a/package/PartSeg/common_backend/base_settings.py b/package/PartSeg/common_backend/base_settings.py index b9f28132d..eba767e79 100644 --- a/package/PartSeg/common_backend/base_settings.py +++ b/package/PartSeg/common_backend/base_settings.py @@ -36,10 +36,11 @@ from PartSegCore.color_image import default_colormap_dict, default_label_dict from PartSegCore.color_image.base_colors import starting_colors from PartSegCore.io_utils import load_metadata_base -from PartSegCore.json_hooks import ProfileDict, ProfileEncoder, check_loaded_dict +from PartSegCore.json_hooks import PartSegEncoder from PartSegCore.project_info import AdditionalLayerDescription, HistoryElement, ProjectInfoBase from PartSegCore.roi_info import ROIInfo from PartSegCore.segmentation.algorithm_base import ROIExtractionResult +from PartSegCore.utils import ProfileDict, check_loaded_dict from PartSegImage import Image if hasattr(napari.utils.theme, "get_theme"): @@ -472,7 +473,7 @@ class BaseSettings(ViewSettings): points_changed = Signal() request_load_files = Signal(list) """:py:class:`~.Signal` mask changed signal""" - json_encoder_class = ProfileEncoder + json_encoder_class = PartSegEncoder load_metadata = staticmethod(load_metadata_base) algorithm_changed = Signal() """:py:class:`~.Signal` emitted when current algorithm should be changed""" @@ -753,20 +754,20 @@ def load(self, folder_path: Union[Path, str, None] = None): try: data: ProfileDict = self.load_metadata(file_path) if not data.verify_data(): - errors_list.append((file_path, data.filter_data())) + filtered = data.filter_data() + errors_list.append((file_path, filtered)) + logger.error(filtered) error = True el.values.update(data) except Exception as e: # pylint: disable=W0703 error = True + logger.error(e) errors_list.append((file_path, e)) finally: if error: timestamp = datetime.today().strftime("%Y-%m-%d_%H_%M_%S") base_path, ext = os.path.splitext(file_path) os.rename(file_path, f"{base_path}_{timestamp}{ext}") - - if errors_list: - logger.error(errors_list) return errors_list def get_project_info(self) -> ProjectInfoBase: diff --git a/package/PartSeg/common_gui/algorithms_description.py b/package/PartSeg/common_gui/algorithms_description.py index e95230f57..107ed0a74 100644 --- a/package/PartSeg/common_gui/algorithms_description.py +++ b/package/PartSeg/common_gui/algorithms_description.py @@ -5,12 +5,11 @@ from copy import deepcopy from enum import Enum -import magicgui import numpy as np from magicgui.widgets import ComboBox, Widget, create_widget from napari.layers.base import Layer -from packaging.version import parse as parse_version -from qtpy.QtCore import Signal +from pydantic import BaseModel +from qtpy.QtCore import QObject, Signal from qtpy.QtGui import QHideEvent, QPainter, QPaintEvent, QResizeEvent from qtpy.QtWidgets import ( QAbstractSpinBox, @@ -29,14 +28,19 @@ from superqt import QEnumComboBox from PartSeg.common_gui.error_report import ErrorDialog -from PartSegCore.algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty, ROIExtractionProfile -from PartSegCore.channel_class import Channel +from PartSegCore.algorithm_describe_base import ( + AlgorithmDescribeBase, + AlgorithmProperty, + AlgorithmSelection, + ROIExtractionProfile, + base_model_to_algorithm_property, +) from PartSegCore.segmentation.algorithm_base import ( ROIExtractionAlgorithm, ROIExtractionResult, SegmentationLimitException, ) -from PartSegImage import Image +from PartSegImage import Channel, Image from ..common_backend.base_settings import BaseSettings from ..common_backend.segmentation_thread import SegmentationThread @@ -129,7 +133,6 @@ def from_algorithm_property(cls, ob): user_name=ob.user_name, default_value=ob.default_value, options_range=ob.range, - single_steep=ob.single_step, value_type=ob.value_type, possible_values=ob.possible_values, help_text=ob.help_text, @@ -149,7 +152,7 @@ def _get_numeric_field(ap: AlgorithmProperty): ) else: # issubclass(ap.value_type, float): res = CustomDoubleSpinBox() - if not isinstance(ap.default_value, float): + if not isinstance(ap.default_value, (int, float)): raise ValueError( f"Incompatible types. default_value should be type of float. Is {type(ap.default_value)}" ) @@ -180,6 +183,8 @@ def _get_field_from_value_type(cls, ap: AlgorithmProperty): elif issubclass(ap.value_type, list): res = QComboBox() res.addItems(list(map(str, ap.possible_values))) + elif issubclass(ap.value_type, BaseModel): + res = FieldsList([cls.from_algorithm_property(x) for x in base_model_to_algorithm_property(ap.value_type)]) else: res = create_widget(annotation=ap.value_type, options={}) return res @@ -223,6 +228,8 @@ def get_change_signal(widget: typing.Union[QWidget, Widget]): return widget.values_changed if isinstance(widget, ListInput): return widget.change_signal + if isinstance(widget, FieldsList): + return widget.changed if hasattr(widget, "values_changed"): return widget.values_changed raise ValueError(f"Unsupported type: {type(widget)}") @@ -271,6 +278,38 @@ def get_getter_and_setter_function( raise ValueError(f"Unsupported type: {type(widget)}") +class FieldsList(QObject): + changed = Signal() + + def __init__(self, field_list: typing.List[QtAlgorithmProperty]): + super().__init__() + self.field_list = field_list + for el in field_list: + el.change_fun.connect(self._changed_wrap) + + def get_value(self): + return {el.name: el.get_value() for el in self.field_list} + + def _changed_wrap(self, val=None): + self.changed.emit() + + def set_value(self, val): + if isinstance(val, dict): + self._set_value_dkt(val) + else: + self._set_value_base_model(val) + + def _set_value_base_model(self, val): + for el in self.field_list: + if hasattr(val, el.name): + el.set_value(getattr(val, el.name)) + + def _set_value_dkt(self, val: dict): + for el in self.field_list: + if el.name in val: + el.set_value(val[el.name]) + + class ListInput(QWidget): change_signal = Signal() @@ -279,7 +318,7 @@ def __init__(self, property_el: QtAlgorithmProperty, length): self.input_list = [property_el.from_algorithm_property(property_el) for _ in range(length)] layout = QVBoxLayout() for el in self.input_list: - el.change_fun.connect(self.change_signal.emit) + el.change_fun.connect(_any_arguments(self.change_signal.emit)) layout.addWidget(el.get_field()) self.setLayout(layout) @@ -294,15 +333,8 @@ def set_value(self, value): def _any_arguments(fun): - if parse_version(magicgui.__version__) >= parse_version("0.3.0"): - - def _any(): - fun() - - else: - - def _any(*_): - fun() + def _any(): + fun() return _any @@ -312,7 +344,7 @@ class FormWidget(QWidget): def __init__( self, - fields: typing.List[AlgorithmProperty], + fields: typing.Union[typing.List[AlgorithmProperty], typing.Type[BaseModel]], start_values=None, dimension_num=1, settings: typing.Optional[BaseSettings] = None, @@ -325,6 +357,10 @@ def __init__( layout = QFormLayout() layout.setContentsMargins(10, 0, 10, 0) # layout.setVerticalSpacing(0) + self._model_class = None + if not isinstance(fields, list): + self._model_class = fields + fields = base_model_to_algorithm_property(fields) element_list = self._element_list(fields) for el in element_list: if isinstance(el, QLabel): @@ -334,11 +370,14 @@ def __init__( self.setLayout(layout) self.value_changed.connect(self.update_size) - def _add_to_layout(self, layout, ap: QtAlgorithmProperty, start_values: typing.MutableMapping, settings): + def _add_to_layout( + self, layout, ap: QtAlgorithmProperty, start_values: typing.MutableMapping, settings, add_to_widget_dict=True + ): label = QLabel(ap.user_name) if ap.help_text: label.setToolTip(ap.help_text) - self.widgets_dict[ap.name] = ap + if add_to_widget_dict: + self.widgets_dict[ap.name] = ap ap.change_fun.connect(_any_arguments(self.value_changed.emit)) if isinstance(ap.get_field(), SubAlgorithmWidget): layout.addRow(label, ap.get_field().choose) @@ -350,6 +389,11 @@ def _add_to_layout(self, layout, ap: QtAlgorithmProperty, start_values: typing.M if isinstance(ap.get_field(), Widget): layout.addRow(label, ap.get_field().native) return + if isinstance(ap.get_field(), FieldsList): + layout.addRow(label) + for el in ap.get_field().field_list: + self._add_to_layout(layout, el, start_values.get(ap.name, {}), settings, add_to_widget_dict=False) + return layout.addRow(label, ap.get_field()) # noinspection PyUnresolvedReferences if issubclass(ap.value_type, Channel): @@ -373,12 +417,17 @@ def update_size(self): self.setMinimumHeight(self.layout().minimumSize().height()) def get_values(self): - return {name: el.get_value() for name, el in self.widgets_dict.items()} + res = {name: el.get_value() for name, el in self.widgets_dict.items()} + if self._model_class is not None: + return self._model_class(**res) + return res def recursive_get_values(self): return {name: el.recursive_get_values() for name, el in self.widgets_dict.items()} - def set_values(self, values: dict): + def set_values(self, values: typing.Union[dict, BaseModel]): + if isinstance(values, BaseModel): + values = dict(values) for name, value in values.items(): if name in self.widgets_dict: self.widgets_dict[name].set_value(value) @@ -410,8 +459,7 @@ def __init__(self, algorithm_property: AlgorithmProperty): self.property = algorithm_property self.widgets_dict: typing.Dict[str, FormWidget] = {} # TODO protect for recursion - widget = FormWidget(algorithm_property.possible_values[algorithm_property.default_value].get_fields()) - widget.layout().setContentsMargins(0, 0, 0, 0) + widget = self._get_form_widget(algorithm_property) widget.value_changed.connect(self.values_changed) self.widgets_dict[algorithm_property.default_value] = widget @@ -435,6 +483,22 @@ def __init__(self, algorithm_property: AlgorithmProperty): self.tmp_widget = tmp_widget self.setLayout(layout) + @staticmethod + def _get_form_widget(algorithm_property, start_values=None): + if isinstance(algorithm_property, AlgorithmProperty): + calc_class = algorithm_property.possible_values[algorithm_property.default_value] + else: + calc_class = algorithm_property + if calc_class.__new_style__: + widget = FormWidget(calc_class.__argument_class__, start_values=start_values) + else: + widget = FormWidget( + algorithm_property.possible_values[algorithm_property.default_value].get_fields(), + start_values=start_values, + ) + widget.layout().setContentsMargins(0, 0, 0, 0) + return widget + def set_starting(self, starting_values): self.starting_values = starting_values # self.set_values(starting_values) @@ -467,10 +531,8 @@ def algorithm_choose(self, name): if name not in self.property.possible_values: return start_dict = {} if name not in self.starting_values else self.starting_values[name] - self.widgets_dict[name] = FormWidget( - self.property.possible_values[name].get_fields(), start_values=start_dict - ) - self.widgets_dict[name].layout().setContentsMargins(0, 0, 0, 0) + self.widgets_dict[name] = self._get_form_widget(self.property.possible_values[name], start_dict) + self.layout().addWidget(self.widgets_dict[name]) self.widgets_dict[name].value_changed.connect(self.values_changed) widget = self.widgets_dict[name] @@ -527,7 +589,10 @@ def __init__(self, settings: BaseSettings, name, algorithm: typing.Type[ROIExtra @staticmethod def _form_widget(algorithm, start_values) -> FormWidget: - return FormWidget(algorithm.get_fields(), start_values=start_values) + return FormWidget( + algorithm.__argument_class__ if algorithm.__new_style__ else algorithm.get_fields(), + start_values=start_values, + ) @staticmethod def exception_occurred(exc: Exception): @@ -576,7 +641,10 @@ def channel_num(self): def execute(self, exclude_mask=None): values = self.get_values() self.settings.set(f"algorithms.{self.name}", deepcopy(values)) - self.algorithm_thread.set_parameters(**values) + if isinstance(values, dict): + self.algorithm_thread.set_parameters(**values) + else: + self.algorithm_thread.set_parameters(values) self.algorithm_thread.start() def hideEvent(self, a0: QHideEvent): @@ -641,9 +709,7 @@ class AlgorithmChooseBase(QWidget): progress_signal = Signal(str, int) algorithm_changed = Signal(str) - def __init__( - self, settings: BaseSettings, algorithms: typing.Dict[str, typing.Type[ROIExtractionAlgorithm]], parent=None - ): + def __init__(self, settings: BaseSettings, algorithms: typing.Type[AlgorithmSelection], parent=None): super().__init__(parent) self.settings = settings self.algorithms = algorithms @@ -670,7 +736,7 @@ def _algorithm_widget(settings, name, val) -> InteractiveAlgorithmSettingsWidget def add_widgets_to_algorithm(self): self.algorithm_choose.blockSignals(True) self.algorithm_choose.clear() - for name, val in self.algorithms.items(): + for name, val in self.algorithms.__register__.items(): self.algorithm_choose.addItem(name) widget = self._algorithm_widget(self.settings, name, val) self.algorithm_dict[name] = widget @@ -742,9 +808,7 @@ def get_info_text(self): class AlgorithmChoose(AlgorithmChooseBase): - def __init__( - self, settings: BaseSettings, algorithms: typing.Dict[str, typing.Type[ROIExtractionAlgorithm]], parent=None - ): + def __init__(self, settings: BaseSettings, algorithms: typing.Type[AlgorithmSelection], parent=None): super().__init__(settings, algorithms, parent) self.settings.image_changed.connect(self.image_changed) @@ -768,4 +832,6 @@ def _value_set(self, value): if el.name == value: self.value = el return + if isinstance(value, Channel): + self.value = value.value self.value = value diff --git a/package/PartSeg/common_gui/main_window.py b/package/PartSeg/common_gui/main_window.py index 51d529b97..e29850c17 100644 --- a/package/PartSeg/common_gui/main_window.py +++ b/package/PartSeg/common_gui/main_window.py @@ -151,7 +151,8 @@ def __init__( self.setWindowTitle(title) self.title_base = title app = QApplication.instance() - if app is not None: + if app is not None and "PYTEST_CURRENT_TEST" not in os.environ: + # FIXME the PYTEST_CURRENT_TEST is used to prevent pytest session fail on CI. app.setStyleSheet(settings.get_style_sheet()) self.settings.theme_changed.connect(self.change_theme) self.channel_info = "" diff --git a/package/PartSeg/common_gui/napari_image_view.py b/package/PartSeg/common_gui/napari_image_view.py index fd2d43151..1566c7aa9 100644 --- a/package/PartSeg/common_gui/napari_image_view.py +++ b/package/PartSeg/common_gui/napari_image_view.py @@ -24,7 +24,7 @@ from vispy.geometry.rect import Rect from vispy.scene import BaseCamera -from PartSegCore.class_generator import enum_register +from PartSegCore.class_register import register_class from PartSegCore.image_operations import NoiseFilterType, bilateral, gaussian, median from PartSegCore.roi_info import ROIInfo from PartSegImage import Image @@ -110,6 +110,7 @@ def translated_coords(self, coords: Union[List[int], np.ndarray]) -> np.ndarray: return np.subtract(coords, fst_layer.translate).astype(int) +@register_class(old_paths=["PartSeg.common_gui.stack_image_view.LabelEnum"]) class LabelEnum(Enum): Not_show = 0 Show_results = 1 @@ -1007,6 +1008,3 @@ def _print_dict(dkt: MutableMapping, indent="") -> str: else: res.append(f"{indent}{k}: {v}") return "\n".join(res) - - -enum_register.register_class(LabelEnum) diff --git a/package/PartSeg/common_gui/universal_gui_part.py b/package/PartSeg/common_gui/universal_gui_part.py index 4851a25af..f2f2089b4 100644 --- a/package/PartSeg/common_gui/universal_gui_part.py +++ b/package/PartSeg/common_gui/universal_gui_part.py @@ -27,6 +27,7 @@ from superqt import QEnumComboBox from PartSegCore.universal_const import UNIT_SCALE, Units +from PartSegImage import Channel enum_type = Enum if PYQT5 else object @@ -34,13 +35,15 @@ class ChannelComboBox(QComboBox): """Combobox for selecting channel index. Channel numeration starts from 1 for user and from 0 for developer""" - def get_value(self) -> int: + def get_value(self) -> Channel: """Return current channel. Starting from 0""" - return self.currentIndex() + return Channel(self.currentIndex()) - def set_value(self, val: int): + def set_value(self, val: typing.Union[Channel, int]): """Set current channel . Starting from 0""" - self.setCurrentIndex(int(val)) + if isinstance(val, Channel): + self.setCurrentIndex(val.value) + self.setCurrentIndex(val) def change_channels_num(self, num: int): """Change number of channels""" diff --git a/package/PartSeg/plugins/modeling_save/save_modeling_data.py b/package/PartSeg/plugins/modeling_save/save_modeling_data.py index 0a7e93553..0e8e5e0a1 100644 --- a/package/PartSeg/plugins/modeling_save/save_modeling_data.py +++ b/package/PartSeg/plugins/modeling_save/save_modeling_data.py @@ -10,10 +10,10 @@ from PartSegCore.algorithm_describe_base import AlgorithmProperty from PartSegCore.analysis.io_utils import ProjectTuple from PartSegCore.analysis.save_functions import SaveCmap -from PartSegCore.channel_class import Channel from PartSegCore.io_utils import SaveBase, SaveROIAsNumpy, SaveROIAsTIFF from PartSegCore.roi_info import ROIInfo from PartSegCore.universal_const import Units +from PartSegImage import Channel class SaveModeling(SaveBase): diff --git a/package/PartSeg/plugins/napari_widgets/roi_extraction_algorithms.py b/package/PartSeg/plugins/napari_widgets/roi_extraction_algorithms.py index d7d7168ae..fec33f58b 100644 --- a/package/PartSeg/plugins/napari_widgets/roi_extraction_algorithms.py +++ b/package/PartSeg/plugins/napari_widgets/roi_extraction_algorithms.py @@ -24,11 +24,11 @@ from PartSeg import plugins from PartSegCore import UNIT_SCALE, Units -from PartSegCore.algorithm_describe_base import Register, ROIExtractionProfile -from PartSegCore.analysis.algorithm_description import analysis_algorithm_dict +from PartSegCore.algorithm_describe_base import AlgorithmSelection, ROIExtractionProfile +from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection from PartSegCore.analysis.load_functions import LoadProfileFromJSON from PartSegCore.analysis.save_functions import SaveProfilesToJSON -from PartSegCore.mask.algorithm_description import mask_algorithm_dict +from PartSegCore.mask.algorithm_description import MaskAlgorithmSelection from PartSegCore.segmentation import ROIExtractionResult from ..._roi_analysis.profile_export import ExportDialog, ImportDialog, ProfileDictViewer @@ -51,9 +51,14 @@ class NapariInteractiveAlgorithmSettingsWidget(InteractiveAlgorithmSettingsWidget): + form_widget: NapariFormWidgetWithMask + @staticmethod def _form_widget(algorithm, start_values) -> FormWidget: - return NapariFormWidgetWithMask(algorithm.get_fields(), start_values=start_values) + return NapariFormWidgetWithMask( + algorithm.__argument_class__ if algorithm.__new_style__ else algorithm.get_fields(), + start_values=start_values, + ) def reset_choices(self, event=None): self.form_widget.reset_choices(event) @@ -62,14 +67,18 @@ def get_layer_list(self) -> typing.List[str]: return [x.name for x in self.get_layers().values()] def get_values(self): + values = self.form_widget.get_values() + if not isinstance(values, dict): + values = dict(values) return { k: v.name if isinstance(v, NapariImage) else v - for k, v in self.form_widget.get_values().items() + for k, v in values.items() if not isinstance(v, Labels) and k != "mask" } def get_layers(self) -> typing.Dict[str, Layer]: - return {k: v for k, v in self.form_widget.get_values().items() if isinstance(v, Layer)} + values = self.form_widget.get_layers() + return {k: v for k, v in values.items() if isinstance(v, Layer)} class NapariAlgorithmChoose(AlgorithmChooseBase): @@ -84,7 +93,7 @@ def reset_choices(self, event=None): class ROIExtractionAlgorithms(QWidget): @staticmethod - def get_method_dict(): # pragma: no cover + def get_method_dict() -> AlgorithmSelection: # pragma: no cover raise NotImplementedError @staticmethod @@ -277,7 +286,7 @@ def refresh_profiles(self): class ROIAnalysisExtraction(ROIExtractionAlgorithms): @staticmethod def get_method_dict(): - return analysis_algorithm_dict + return AnalysisAlgorithmSelection @staticmethod def prefix() -> str: @@ -287,7 +296,7 @@ def prefix() -> str: class ROIMaskExtraction(ROIExtractionAlgorithms): @staticmethod def get_method_dict(): - return mask_algorithm_dict + return MaskAlgorithmSelection @staticmethod def prefix() -> str: @@ -298,13 +307,13 @@ class ProfilePreviewDialog(QDialog): def __init__( self, profile_dict: typing.Dict[str, ROIExtractionProfile], - algorithm_dict: Register, + algorithm_selection: typing.Type[AlgorithmSelection], settings: BaseSettings, parent=None, ): super().__init__(parent=parent) self.profile_dict = profile_dict - self.algorithm_dict = algorithm_dict + self.algorithm_selection = algorithm_selection self.settings = settings self.profile_list = SearchableListWidget() diff --git a/package/PartSeg/plugins/napari_widgets/search_label_widget.py b/package/PartSeg/plugins/napari_widgets/search_label_widget.py index bbfc7a363..d5ca8b563 100644 --- a/package/PartSeg/plugins/napari_widgets/search_label_widget.py +++ b/package/PartSeg/plugins/napari_widgets/search_label_widget.py @@ -11,6 +11,8 @@ from PartSeg.common_gui.napari_image_view import SearchType from PartSegCore.roi_info import ROIInfo +HIGHLIGHT_LABEL_NAME = ".Highlight" + class SearchLabel(Container): def __init__(self, napari_viewer: Viewer): @@ -51,9 +53,9 @@ def _update_roi_info(self): self._component_num_changed() def _stop(self): - if ".Highlight" in self.napari_viewer.layers: - self.napari_viewer.layers[".Highlight"].metadata["timer"].stop() - del self.napari_viewer.layers[".Highlight"] + if HIGHLIGHT_LABEL_NAME in self.napari_viewer.layers: + self.napari_viewer.layers[HIGHLIGHT_LABEL_NAME].metadata["timer"].stop() + del self.napari_viewer.layers[HIGHLIGHT_LABEL_NAME] def _highlight(self): num = self.component_selector.value @@ -65,12 +67,12 @@ def _highlight(self): slices = bound_info.get_slices() component_mark = self.roi_info.roi[tuple(slices)] == num translate_grid = labels.translate + bound_info.lower * labels.scale - if ".Highlight" in self.napari_viewer.layers: - self.napari_viewer.layers[".Highlight"].data = component_mark + if HIGHLIGHT_LABEL_NAME in self.napari_viewer.layers: + self.napari_viewer.layers[HIGHLIGHT_LABEL_NAME].data = component_mark else: layer = self.napari_viewer.add_labels( component_mark, - name=".Highlight", + name=HIGHLIGHT_LABEL_NAME, scale=labels.scale, blending="translucent", color={0: (0, 0, 0, 0), 1: "white"}, @@ -89,7 +91,7 @@ def flash_fun(layer_=layer): timer.start() layer.metadata["timer"] = timer - self.napari_viewer.layers[".Highlight"].translate = translate_grid + self.napari_viewer.layers[HIGHLIGHT_LABEL_NAME].translate = translate_grid self._shift_if_need(labels, bound_info) def _shift_if_need(self, labels, bound_info): diff --git a/package/PartSeg/plugins/napari_widgets/simple_measurement_widget.py b/package/PartSeg/plugins/napari_widgets/simple_measurement_widget.py index 20bf52e9d..59b6d1643 100644 --- a/package/PartSeg/plugins/napari_widgets/simple_measurement_widget.py +++ b/package/PartSeg/plugins/napari_widgets/simple_measurement_widget.py @@ -12,6 +12,7 @@ from napari.qt import create_worker from PartSegCore import UNIT_SCALE, Units +from PartSegCore.algorithm_describe_base import base_model_to_algorithm_property from PartSegCore.analysis.measurement_base import AreaType, Leaf, MeasurementEntry, PerComponent from PartSegCore.analysis.measurement_calculation import MEASUREMENT_DICT, MeasurementProfile, MeasurementResult from PartSegCore.roi_info import ROIInfo @@ -67,7 +68,9 @@ def _calculate(self, event=None): warnings.warn("No measurement. Select at least one measurement") return - profile = MeasurementProfile("", [MeasurementEntry(name=x.name, calculation_tree=x) for x in to_calculate]) + profile = MeasurementProfile( + name="", chosen_fields=[MeasurementEntry(name=x.name, calculation_tree=x) for x in to_calculate] + ) data_layer = self.image_choice.value or self.labels_choice.value @@ -121,7 +124,9 @@ def _refresh_measurements(self, event=None): area = val.get_starting_leaf().area pc = val.get_starting_leaf().per_component if ( - val.get_fields() + base_model_to_algorithm_property(val.__argument_class__) + if val.__new_style__ + else val.get_fields() or (area is not None and area != AreaType.ROI) or (pc is not None and pc != PerComponent.Yes) ): diff --git a/package/PartSeg/plugins/napari_widgets/utils.py b/package/PartSeg/plugins/napari_widgets/utils.py index 35ff86dbe..df3faefd5 100644 --- a/package/PartSeg/plugins/napari_widgets/utils.py +++ b/package/PartSeg/plugins/napari_widgets/utils.py @@ -5,15 +5,14 @@ from magicgui.widgets import Widget, create_widget from napari import Viewer from napari.layers import Image as NapariImage -from napari.layers import Labels +from napari.layers import Labels, Layer from qtpy.QtWidgets import QWidget from PartSeg.common_gui.algorithms_description import FormWidget, QtAlgorithmProperty from PartSeg.common_gui.custom_save_dialog import FormDialog from PartSegCore import UNIT_SCALE, Units from PartSegCore.algorithm_describe_base import AlgorithmProperty -from PartSegCore.channel_class import Channel -from PartSegImage import Image +from PartSegImage import Channel, Image class QtNapariAlgorithmProperty(QtAlgorithmProperty): @@ -40,6 +39,19 @@ def _element_list(self, fields) -> typing.Iterable[QtAlgorithmProperty]: mask = AlgorithmProperty("mask", "Mask", None, value_type=typing.Optional[Labels]) return super()._element_list(itertools.chain([mask], fields)) + def get_layers(self): + return { + name: el.get_value() + for name, el in self.widgets_dict.items() + if name != "mask" and isinstance(el.get_value(), Layer) + } + + def get_values(self): + res = {name: el.get_value() for name, el in self.widgets_dict.items() if name != "mask"} + if self._model_class is not None: + return self._model_class(**res) + return res + class NapariFormDialog(FormDialog): @staticmethod diff --git a/package/PartSegCore/_old_json_hooks.py b/package/PartSegCore/_old_json_hooks.py new file mode 100644 index 000000000..2f4042ccc --- /dev/null +++ b/package/PartSegCore/_old_json_hooks.py @@ -0,0 +1,155 @@ +import numpy as np +from napari.utils import Colormap + +from PartSegCore.algorithm_describe_base import ROIExtractionProfile +from PartSegCore.class_generator import SerializeClassEncoder +from PartSegCore.image_operations import RadiusType + + +class ProfileEncoder(SerializeClassEncoder): + """ + Json encoder for :py:class:`ProfileDict`, :py:class:`RadiusType`, + :py:class:`.SegmentationProfile` classes + + >>> import json + >>> data = ProfileDict() + >>> data.set("aa.bb.cc", 7) + >>> with open("some_file", 'w') as fp: + >>> json.dump(data, fp, cls=ProfileEncoder) + """ + + # pylint: disable=E0202 + def default(self, o): + """encoder implementation""" + if isinstance(o, RadiusType): + return {"__RadiusType__": True, "value": o.value} + if isinstance(o, ROIExtractionProfile): + return {"__SegmentationProfile__": True, "name": o.name, "algorithm": o.algorithm, "values": o.values} + if isinstance(o, Colormap): + return { + "__Colormap__": True, + "name": o.name, + "colors": o.colors.tolist(), + "interpolation": o.interpolation, + "controls": o.controls.tolist(), + } + if hasattr(o, "as_dict"): + dkt = o.as_dict() + dkt["__class__"] = f"{o.__module__}.{o.__class__.__name__}" + return dkt + if isinstance(o, np.integer): + return int(o) + if isinstance(o, np.floating): + return float(o) + return super().default(o) + + +class PartEncoder(ProfileEncoder): + # pylint: disable=E0202 + def default(self, o): + from PartSegCore.analysis.calculation_plan import CalculationPlan, CalculationTree + from PartSegCore.analysis.measurement_calculation import MeasurementProfile + + if isinstance(o, MeasurementProfile): + return {"__MeasurementProfile__": True, **o.as_dict()} + if isinstance(o, CalculationPlan): + return {"__CalculationPlan__": True, "tree": o.execution_tree, "name": o.name} + if isinstance(o, CalculationTree): + return {"__CalculationTree__": True, "operation": o.operation, "children": o.children} + return super().default(o) + + +def part_hook(dkt): + from PartSegCore.analysis.calculation_plan import CalculationPlan, CalculationTree + from PartSegCore.analysis.measurement_calculation import MeasurementProfile + + dkt2 = dkt.copy() + try: + if "__StatisticProfile__" in dkt: + del dkt["__StatisticProfile__"] + res = MeasurementProfile(**dkt) + return res + if "__MeasurementProfile__" in dkt: + del dkt["__MeasurementProfile__"] + res = MeasurementProfile(**dkt) + return res + if "__CalculationPlan__" in dkt: + del dkt["__CalculationPlan__"] + return CalculationPlan(**dkt) + if "__CalculationTree__" in dkt: + return CalculationTree(operation=dkt["operation"], children=dkt["children"]) + if ( + "__subtype__" in dkt + and "statistic_profile" in dkt + and dkt["__subtype__"] + in ( + "PartSegCore.analysis.calculation_plan.MeasurementCalculate", + "PartSeg.utils.analysis.calculation_plan.StatisticCalculate", + ) + ): + dkt["measurement_profile"] = dkt["statistic_profile"] + del dkt["statistic_profile"] + except Exception as e: # pylint: disable=W0703 + dkt = dkt2 + dkt["__error__"] = str(e) + + return profile_hook(dkt) + + +def profile_hook(dkt): + """ + hook for json loading + + >>> import json + >>> with open("some_file", 'r') as fp: + ... data = json.load(fp, object_hook=profile_hook) + + """ + dkt2 = dkt.copy() + try: + if "__ProfileDict__" in dkt: + from PartSegCore.utils import ProfileDict + + del dkt["__ProfileDict__"] + res = ProfileDict(**dkt) + return res + if "__RadiusType__" in dkt: + return RadiusType(dkt["value"]) + if "__SegmentationProperty__" in dkt: + del dkt["__SegmentationProperty__"] + res = ROIExtractionProfile(**dkt) + return res + if "__SegmentationProfile__" in dkt: + del dkt["__SegmentationProfile__"] + res = ROIExtractionProfile(**dkt) + return res + if ( + "__Serializable__" in dkt and dkt["__subtype__"] == "HistoryElement" and "algorithm_name" in dkt + ): # pragma: no cover + # old code fix + name = dkt["algorithm_name"] + par = dkt["algorithm_values"] + del dkt["algorithm_name"] + del dkt["algorithm_values"] + dkt["segmentation_parameters"] = {"algorithm_name": name, "values": par} + if "__Serializable__" in dkt and dkt["__subtype__"] == "PartSegCore.color_image.base_colors.ColorMap": + positions, colors = list(zip(*dkt["colormap"])) + return Colormap(colors, controls=positions) + if "__Serializable__" in dkt and dkt["__subtype__"] == "PartSegCore.color_image.base_colors.ColorPosition": + return (dkt["color_position"], dkt["color"]) + if "__Serializable__" in dkt and dkt["__subtype__"] == "PartSegCore.color_image.base_colors.Color": + return (dkt["red"] / 255, dkt["green"] / 255, dkt["blue"] / 255) + if "__Colormap__" in dkt: + del dkt["__Colormap__"] + if dkt["controls"][0] != 0: + dkt["controls"].insert(0, 0) + dkt["colors"].insert(0, dkt["colors"][0]) + if dkt["controls"][-1] != 1: + dkt["controls"].append(1) + dkt["colors"].append(dkt["colors"][-1]) + return Colormap(**dkt) + except Exception as e: # pylint: disable=W0703 + dkt = dkt2 + dkt["__error__"] = str(e) + + return dkt diff --git a/package/PartSegCore/algorithm_describe_base.py b/package/PartSegCore/algorithm_describe_base.py index fc7f3a4fc..6eeae70a2 100644 --- a/package/PartSegCore/algorithm_describe_base.py +++ b/package/PartSegCore/algorithm_describe_base.py @@ -1,11 +1,21 @@ import inspect +import textwrap import typing import warnings -from abc import ABC, abstractmethod -from collections import OrderedDict +from abc import ABC, ABCMeta, abstractmethod from enum import Enum -from PartSegCore.channel_class import Channel +from pydantic import BaseModel as PydanticBaseModel +from pydantic import create_model, validator +from pydantic.main import ModelMetaclass +from typing_extensions import Annotated + +from PartSegCore.class_register import REGISTER, class_to_str +from PartSegCore.utils import BaseModel +from PartSegImage import Channel + +if typing.TYPE_CHECKING: + from pydantic.fields import ModelField class AlgorithmDescribeNotFound(Exception): @@ -31,7 +41,6 @@ def __init__( user_name: str, default_value: typing.Union[str, int, float, object], options_range=None, - single_steep=None, possible_values=None, value_type=None, help_text="", @@ -56,7 +65,6 @@ def __init__( self.default_value = default_value self.range = options_range self.possible_values = possible_values - self.single_step = single_steep self.help_text = help_text self.per_dimension = per_dimension if self.value_type is list and default_value not in possible_values: @@ -71,17 +79,68 @@ def __repr__(self): ) -class AlgorithmDescribeBase(ABC): +class _GetDescriptionClass: + __slots__ = ("_name",) + + def __init__(self): + self._name = None + + def __set_name__(self, owner, name: str): + if self._name is None: + self._name = name + + def __get__(self, obj, klass): + if klass is None: + klass = type(obj) + + name = typing.cast(str, self._name) + fields_dkt = { + field.name: ( + Annotated[field.value_type, field.user_name, field.range, field.help_text], + field.default_value, + ) + for field in klass.get_fields() + if not isinstance(field, str) + } + + model = create_model(name, **fields_dkt) + model.__qualname__ = f"{klass.__qualname__}.{name}" + setattr(klass, name, model) + return model + + +def _partial_abstractmethod(funcobj): + funcobj.__is_partial_abstractmethod__ = True + return funcobj + + +class AlgorithmDescribeBaseMeta(ABCMeta): + def __new__(cls, name, bases, attrs, **kwargs): + cls2 = super().__new__(cls, name, bases, attrs, **kwargs) + if ( + not inspect.isabstract(cls2) + and hasattr(cls2.get_fields, "__is_partial_abstractmethod__") + and cls2.__argument_class__ is None + ): + raise RuntimeError("class need to have __argument_class__ set or get_fields functions defined") + cls2.__new_style__ = getattr(cls2.get_fields, "__is_partial_abstractmethod__", False) + return cls2 + + +class AlgorithmDescribeBase(ABC, metaclass=AlgorithmDescribeBaseMeta): """ This is abstract class for all algorithm exported to user interface. Based on get_name and get_fields methods the interface will be generated For each group of algorithm base abstract class will add additional methods """ + __argument_class__: typing.Optional[typing.Type[PydanticBaseModel]] = None + __new_style__: bool + @classmethod def get_doc_from_fields(cls): resp = "{\n" - for el in cls.get_fields(): + for el in cls._get_fields(): if isinstance(el, AlgorithmProperty): resp += f" {el.name}: {el.value_type} - " if el.help_text: @@ -102,21 +161,34 @@ def get_name(cls) -> str: raise NotImplementedError() @classmethod - @abstractmethod + @_partial_abstractmethod def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: """ This function return list of parameters needed by algorithm. It is used for generate form in User Interface :return: list of algorithm parameters and comments """ + if hasattr(cls, "__argument_class__") and cls.__argument_class__ is not None: + warnings.warn( + "Class has __argument_class__ defined, one should not use get_fields", + category=FutureWarning, + stacklevel=2, + ) + return base_model_to_algorithm_property(cls.__argument_class__) raise NotImplementedError() + @classmethod + def _get_fields(cls): + return base_model_to_algorithm_property(cls.__argument_class__) if cls.__new_style__ else cls.get_fields() + @classmethod def get_fields_dict(cls) -> typing.Dict[str, AlgorithmProperty]: - return {v.name: v for v in cls.get_fields() if isinstance(v, AlgorithmProperty)} + return {v.name: v for v in cls._get_fields() if isinstance(v, AlgorithmProperty)} @classmethod def get_default_values(cls): + if cls.__new_style__: + return cls.__argument_class__() # pylint: disable=E1102 return { el.name: { "name": el.default_value, @@ -136,10 +208,10 @@ def is_static(fun): return args[0] != "self" -AlgorithmType = typing.TypeVar("AlgorithmType", bound=type(AlgorithmDescribeBase)) +AlgorithmType = typing.TypeVar("AlgorithmType", bound=typing.Type[AlgorithmDescribeBase]) -class Register(OrderedDict, typing.Generic[AlgorithmType]): +class Register(typing.Dict, typing.Generic[AlgorithmType]): """ Dict used for register :class:`.AlgorithmDescribeBase` classes. All registers from `PartSeg.PartSegCore.register` are this @@ -160,12 +232,13 @@ def __init__(self, *args: AlgorithmType, class_methods=None, methods=None, sugge list(class_methods) if class_methods else getattr(suggested_base_class, "need_class_method", []) ) self.methods = list(methods) if methods else getattr(suggested_base_class, "need_method", []) + self._old_mapping = {} for el in args: self.register(el) - def values(self) -> typing.Iterable[AlgorithmType]: # pylint: disable=W0235 + def values(self) -> typing.Iterable[AlgorithmType]: # noinspection PyTypeChecker - return super().values() + return typing.cast(typing.Iterable[AlgorithmType], super().values()) def __eq__(self, other): return ( @@ -176,14 +249,24 @@ def __eq__(self, other): and self.suggested_base_class == other.suggested_base_class ) - def __getitem__(self, item) -> AlgorithmType: # pylint: disable=W0235 - return super().__getitem__(item) + def __getitem__(self, item) -> AlgorithmType: + # FIXME add better strategy to get proper class when there is conflict of names + try: + return typing.cast(AlgorithmType, super().__getitem__(item)) + except KeyError: + return typing.cast(AlgorithmType, super().__getitem__(self._old_mapping[item])) + + def __contains__(self, item): + return super().__contains__(item) or item in self._old_mapping - def register(self, value: AlgorithmType, replace=False): + def register( + self, value: AlgorithmType, replace: bool = False, old_names: typing.Optional[typing.List[str]] = None + ): """ Function for registering :class:`.AlgorithmDescribeBase` based algorithms :param value: algorithm to register - :param replace: replace existing algorithm, be patient with this + :param replace: replace existing algorithm, be patient with + :param old_names: list of old names for registered class """ self.check_function(value, "get_name", True) try: @@ -191,10 +274,22 @@ def register(self, value: AlgorithmType, replace=False): except NotImplementedError: raise ValueError(f"Class {value} need to implement get_name class method") if name in self and not replace: - raise ValueError(f"Object with this name: {name} already exist and register is not in replace mode") + raise ValueError( + f"Object {self[name]} with this name: {name} already exist and register is not in replace mode" + ) if not isinstance(name, str): raise ValueError(f"Function get_name of class {value} need return string not {type(name)}") self[name] = value + if old_names is not None: + # FIXME add better strategy to get proper class when there is conflict of names + for old_name in old_names: + if old_name in self._old_mapping and not replace: + raise ValueError( + f"Old value mapping for name {old_name} already registered." + f" Currently pointing to {self._old_mapping[name]}" + ) + self._old_mapping[old_name] = name + return value @staticmethod def check_function(ob, function_name, is_class): @@ -207,7 +302,7 @@ def check_function(ob, function_name, is_class): def __setitem__(self, key: str, value: AlgorithmType): if not issubclass(value, AlgorithmDescribeBase): raise ValueError( - f"Class {value} need to inherit from " f"{AlgorithmDescribeBase.__module__}.AlgorithmDescribeBase" + f"Class {value} need to inherit from {AlgorithmDescribeBase.__module__}.AlgorithmDescribeBase" ) self.check_function(value, "get_name", True) self.check_function(value, "get_fields", True) @@ -219,12 +314,13 @@ def __setitem__(self, key: str, value: AlgorithmType): raise ValueError(f"Function get_name of class {value} need return string not {type(val)}") if key != val: raise ValueError("Object need to be registered under name returned by gey_name function") - try: - val = value.get_fields() - except NotImplementedError: - raise ValueError(f"Method get_fields of class {value} need to be implemented") - if not isinstance(val, list): - raise ValueError(f"Function get_fields of class {value} need return list not {type(val)}") + if not value.__new_style__: + try: + val = value.get_fields() + if not isinstance(val, list): + raise ValueError(f"Function get_fields of class {value} need return list not {type(val)}") + except NotImplementedError: + raise ValueError(f"Method get_fields of class {value} need to be implemented") for el in self.class_methods: self.check_function(value, el, True) for el in self.methods: @@ -244,44 +340,138 @@ def get_default(self) -> str: raise ValueError("Register does not contain any algorithm.") -class ROIExtractionProfile: +class AddRegisterMeta(ModelMetaclass): + def __new__(cls, name, bases, attrs, **kwargs): + methods = kwargs.pop("methods", []) + suggested_base_class = kwargs.pop("suggested_base_class", None) + class_methods = kwargs.pop("class_methods", []) + cls2 = super().__new__(cls, name, bases, attrs, **kwargs) + cls2.__register__ = Register( + class_methods=class_methods, methods=methods, suggested_base_class=suggested_base_class + ) + return cls2 + + def __getitem__(self, item) -> AlgorithmType: + return self.__register__[item] + + def __contains__(self, item) -> bool: + return self.__register__.__contains__(item) + + def get(self, item, default=None): + return self.__register__.get(item, default) + + +class AlgorithmSelection(BaseModel, metaclass=AddRegisterMeta): # pylint: disable=E1139 """ + Base class for algorithm selection. + For given algorithm there should be Register instance set __register__ class variable. + """ + + name: str + values: typing.Union[PydanticBaseModel, typing.Dict[str, typing.Any]] + class_path: str = "" + if typing.TYPE_CHECKING: + __register__: Register + + @validator("name") + def check_name(cls, v): + if v not in cls.__register__: + raise ValueError(f"Missed algorithm {v}") + return v + + @validator("class_path", always=True) + def update_class_path(cls, v, values): + if v or "name" not in values: + return v + klass = cls.__register__[values["name"]] + return class_to_str(klass) + + @validator("values", pre=True) + def update_values(cls, v, values): + # FIXME add better strategy to get proper class when there is conflict of names + if "name" not in values or not isinstance(v, dict): + return v + klass = cls.__register__[values["name"]] + if not klass.__new_style__ or not klass.__argument_class__.__fields__: + return v + + dkt_migrated = REGISTER.migrate_data(class_to_str(klass.__argument_class__), {}, v) + return klass.__argument_class__(**dkt_migrated) + @classmethod + def register( + cls, value: AlgorithmType, replace=False, old_names: typing.Optional[typing.List[str]] = None + ) -> AlgorithmType: + """ + Function for registering :class:`.AlgorithmDescribeBase` based algorithms + :param value: algorithm to register + :param replace: replace existing algorithm, be patient with + :param old_names: list of old names for registered class + """ + return cls.__register__.register(value, replace, old_names) + + @classmethod + def get_default(cls): + name = cls.__register__.get_default() + return cls(name=name, values=cls[name].get_default_values()) + + +class ROIExtractionProfile(BaseModel): + """ :ivar str ~.name: name for segmentation profile :ivar str ~.algorithm: Name of algorithm :ivar dict ~.values: algorithm parameters """ - def __init__(self, name: str, algorithm: str, values: dict): - self.name = name - self.algorithm = algorithm - self.values = values + name: str + algorithm: str + values: typing.Any + + @validator("values") + def validate_values(cls, v, values): # pylint: disable=R0201 + if not isinstance(v, dict): + return v + if "algorithm" not in values: + return v + from PartSegCore.analysis import AnalysisAlgorithmSelection + from PartSegCore.mask.algorithm_description import MaskAlgorithmSelection + + name = values["algorithm"] + is_analysis = name in AnalysisAlgorithmSelection + is_mask = name in MaskAlgorithmSelection + if is_analysis == is_mask: + return v + algorithm = AnalysisAlgorithmSelection[name] if is_analysis else MaskAlgorithmSelection[name] + if not algorithm.__new_style__: + return v + dkt_migrated = REGISTER.migrate_data(class_to_str(algorithm.__argument_class__), {}, v) + return algorithm.__argument_class__(**dkt_migrated) def pretty_print(self, algorithm_dict): + if isinstance(algorithm_dict, AlgorithmSelection): + algorithm_dict = algorithm_dict.__register__ try: algorithm = algorithm_dict[self.algorithm] except KeyError: return str(self) + values = self.values if isinstance(self.values, dict) else self.values.dict() if self.name in {"", "Unknown"}: return ( "ROI extraction profile\nAlgorithm: " + self.algorithm + "\n" - + self._pretty_print(self.values, algorithm.get_fields_dict()) + + self._pretty_print(values, algorithm.get_fields_dict()) ) return ( - "ROI extraction profile name: " - + self.name - + "\nAlgorithm: " - + self.algorithm - + "\n" - + self._pretty_print(self.values, algorithm.get_fields_dict()) - ) + ((f"ROI extraction profile name: {self.name}" + "\nAlgorithm: ") + self.algorithm) + "\n" + ) + self._pretty_print(values, algorithm.get_fields_dict()) @classmethod def _pretty_print( cls, values: typing.MutableMapping, translate_dict: typing.Dict[str, AlgorithmProperty], indent=0 ): + if not isinstance(values, typing.MutableMapping): + return textwrap.indent(str(values), " " * indent) res = "" for k, v in values.items(): if k not in translate_dict: @@ -292,13 +482,19 @@ def _pretty_print( continue desc = translate_dict[k] res += " " * indent + desc.user_name + ": " - if issubclass(desc.value_type, Channel): + if issubclass(desc.value_type, Channel) and not isinstance(v, Channel): res += str(Channel(v)) elif issubclass(desc.value_type, AlgorithmDescribeBase): - res += desc.possible_values[v["name"]].get_name() - if v["values"]: + if isinstance(v, AlgorithmSelection): + name = v.name + values_ = v.values + else: + name = v["name"] + values_ = v["values"] + res += desc.possible_values[name].get_name() + if values_: res += "\n" - res += cls._pretty_print(v["values"], desc.possible_values[v["name"]].get_fields_dict(), indent + 2) + res += cls._pretty_print(values_, desc.possible_values[name].get_fields_dict(), indent + 2) elif isinstance(v, typing.MutableMapping): res += cls._pretty_print(v, {}, indent + 2) else: @@ -307,25 +503,21 @@ def _pretty_print( return res[:-1] @classmethod - def print_dict(cls, dkt, indent=0, name: str = ""): + def print_dict(cls, dkt, indent=0, name: str = "") -> str: if isinstance(dkt, Enum): return dkt.name if not isinstance(dkt, typing.MutableMapping): # FIXME update in future method of proper printing channel number if name.startswith("channel") and isinstance(dkt, int): - return dkt + 1 - return dkt + return str(dkt + 1) + return str(dkt) return "\n" + "\n".join( " " * indent + f"{k.replace('_', ' ')}: {cls.print_dict(v, indent + 2, k)}" for k, v in dkt.items() ) def __str__(self): - return ( - "ROI extraction profile name: " - + self.name - + "\nAlgorithm: " - + self.algorithm - + self.print_dict(self.values) + return ((f"ROI extraction profile name: {self.name}" + "\nAlgorithm: ") + self.algorithm) + self.print_dict( + self.values ) def __repr__(self): @@ -338,3 +530,54 @@ def __eq__(self, other): and self.algorithm == other.algorithm and self.values == other.values ) + + +def _field_to_algorithm_property(name: str, field: "ModelField"): + user_name = field.field_info.title + value_range = None + possible_values = None + value_type = field.type_ + default_value = field.field_info.default + help_text = field.field_info.description + if user_name is None: + user_name = name.replace("_", " ").capitalize() + if not hasattr(field.type_, "__origin__"): + if issubclass(field.type_, (int, float)): + value_range = ( + field.field_info.ge or field.field_info.gt or 0, + field.field_info.le or field.field_info.lt or 1000, + ) + if issubclass(field.type_, AlgorithmSelection): + value_type = AlgorithmDescribeBase + default_value = field.field_info.default.name + possible_values = field.type_.__register__ + + return AlgorithmProperty( + name=name, + user_name=user_name, + default_value=default_value, + options_range=value_range, + value_type=value_type, + possible_values=possible_values, + help_text=help_text, + ) + + +def base_model_to_algorithm_property(obj: typing.Type[BaseModel]) -> typing.List[AlgorithmProperty]: + res = [] + value: "ModelField" + if hasattr(obj, "header") and obj.header(): + res.append(obj.header()) + for name, value in obj.__fields__.items(): + ap = _field_to_algorithm_property(name, value) + + if "prefix" in value.field_info.extra: + res.append(value.field_info.extra["prefix"]) + + if "position" in value.field_info.extra: + res.insert(value.field_info.extra["position"], ap) + else: + res.append(ap) + if "suffix" in value.field_info.extra: + res.append(value.field_info.extra["suffix"]) + return res diff --git a/package/PartSegCore/analysis/__init__.py b/package/PartSegCore/analysis/__init__.py index d10424c8e..e21763264 100644 --- a/package/PartSegCore/analysis/__init__.py +++ b/package/PartSegCore/analysis/__init__.py @@ -1,17 +1,27 @@ -from .algorithm_description import analysis_algorithm_dict +import warnings + +from .algorithm_description import AnalysisAlgorithmSelection from .analysis_utils import SegmentationPipeline, SegmentationPipelineElement from .io_utils import ProjectTuple from .load_functions import load_metadata from .measurement_calculation import MEASUREMENT_DICT -from .save_hooks import PartEncoder, part_hook __all__ = ( + "AnalysisAlgorithmSelection", "ProjectTuple", - "analysis_algorithm_dict", "SegmentationPipeline", "SegmentationPipelineElement", - "part_hook", - "PartEncoder", "MEASUREMENT_DICT", "load_metadata", ) + + +def __getattr__(name): # pragma: no cover + if name == "analysis_algorithm_dict": + warnings.warn( + "analysis_algorithm_dict is deprecated. Please use AnalysisAlgorithmSelection instead", + category=FutureWarning, + stacklevel=2, + ) + return AnalysisAlgorithmSelection.__register__ + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/package/PartSegCore/analysis/algorithm_description.py b/package/PartSegCore/analysis/algorithm_description.py index 96196393f..81f952e6f 100644 --- a/package/PartSegCore/analysis/algorithm_description.py +++ b/package/PartSegCore/analysis/algorithm_description.py @@ -1,9 +1,42 @@ -from ..algorithm_describe_base import Register -from ..segmentation.restartable_segmentation_algorithms import final_algorithm_list +import warnings -analysis_algorithm_dict = Register( - *final_algorithm_list, +from ..algorithm_describe_base import AlgorithmSelection +from ..segmentation.restartable_segmentation_algorithms import ( + BorderRim, + LowerThresholdAlgorithm, + LowerThresholdFlowAlgorithm, + MaskDistanceSplit, + OtsuSegment, + RangeThresholdAlgorithm, + UpperThresholdAlgorithm, + UpperThresholdFlowAlgorithm, +) + + +class AnalysisAlgorithmSelection( + AlgorithmSelection, class_methods=["support_time", "support_z"], methods=["set_image", "set_mask", "get_info_text", "calculation_run"], -) -"""Register for segmentation method designed for separate specific areas.""" +): + """Register for segmentation method visible in PartSeg ROI Analysis.""" + + +AnalysisAlgorithmSelection.register(LowerThresholdAlgorithm) +AnalysisAlgorithmSelection.register(UpperThresholdAlgorithm) +AnalysisAlgorithmSelection.register(RangeThresholdAlgorithm) +AnalysisAlgorithmSelection.register(LowerThresholdFlowAlgorithm, old_names=["Lower threshold flow"]) +AnalysisAlgorithmSelection.register(UpperThresholdFlowAlgorithm, old_names=["Upper threshold flow"]) +AnalysisAlgorithmSelection.register(OtsuSegment) +AnalysisAlgorithmSelection.register(BorderRim) +AnalysisAlgorithmSelection.register(MaskDistanceSplit, old_names=["Split Mask on Part"]) + + +def __getattr__(name): # pragma: no cover + if name == "analysis_algorithm_dict": + warnings.warn( + "analysis_algorithm_dict is deprecated. Please use AnalysisAlgorithmSelection instead", + category=FutureWarning, + stacklevel=2, + ) + return AnalysisAlgorithmSelection.__register__ + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/package/PartSegCore/analysis/analysis_utils.py b/package/PartSegCore/analysis/analysis_utils.py index 109479ab2..0a5477e64 100644 --- a/package/PartSegCore/analysis/analysis_utils.py +++ b/package/PartSegCore/analysis/analysis_utils.py @@ -2,12 +2,12 @@ from textwrap import indent from PartSegCore.algorithm_describe_base import ROIExtractionProfile +from PartSegCore.utils import BaseModel -from ..class_generator import BaseSerializableClass from ..mask_create import MaskProperty -class SegmentationPipelineElement(BaseSerializableClass): +class SegmentationPipelineElement(BaseModel): segmentation: ROIExtractionProfile mask_property: MaskProperty @@ -28,7 +28,7 @@ def __repr__(self): ) -class SegmentationPipeline(BaseSerializableClass): +class SegmentationPipeline(BaseModel): name: str segmentation: ROIExtractionProfile mask_history: typing.List[SegmentationPipelineElement] diff --git a/package/PartSegCore/analysis/batch_processing/batch_backend.py b/package/PartSegCore/analysis/batch_processing/batch_backend.py index 25704eba8..3b981b8aa 100644 --- a/package/PartSegCore/analysis/batch_processing/batch_backend.py +++ b/package/PartSegCore/analysis/batch_processing/batch_backend.py @@ -40,7 +40,7 @@ import xlsxwriter from PartSegCore.algorithm_describe_base import ROIExtractionProfile -from PartSegCore.analysis.algorithm_description import analysis_algorithm_dict +from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection from PartSegCore.analysis.calculation_plan import ( BaseCalculation, Calculation, @@ -68,10 +68,10 @@ from PartSegImage import Image, TiffImageReader from ...io_utils import WrongFileTypeException +from ...json_hooks import PartSegEncoder from ...project_info import AdditionalLayerDescription, HistoryElement from ...roi_info import ROIInfo from ...segmentation import RestartableAlgorithm -from .. import PartEncoder from .parallel_backend import BatchManager, SubprocessOrder @@ -244,13 +244,16 @@ def step_segmentation(self, operation: ROIExtractionProfile, children: List[Calc :param ROIExtractionProfile operation: Specification of segmentation operation :param List[CalculationTree] children: list of nodes to iterate over after perform segmentation """ - segmentation_class = analysis_algorithm_dict.get(operation.algorithm, None) + segmentation_class = AnalysisAlgorithmSelection.get(operation.algorithm) if segmentation_class is None: # pragma: no cover raise ValueError(f"Segmentation class {operation.algorithm} do not found") segmentation_algorithm: RestartableAlgorithm = segmentation_class() segmentation_algorithm.set_image(self.image) segmentation_algorithm.set_mask(self.mask) - segmentation_algorithm.set_parameters(**operation.values) + if segmentation_algorithm.__new_style__: + segmentation_algorithm.set_parameters(operation.values) + else: + segmentation_algorithm.set_parameters(**operation.values) result = segmentation_algorithm.calculation_run(report_empty_fun) backup_data = self.roi_info, self.additional_layers, self.algorithm_parameters self.roi_info = ROIInfo(result.roi, result.roi_annotation, result.alternative_representation) @@ -348,12 +351,15 @@ def step_measurement(self, operation: MeasurementCalculate): """ channel = operation.channel if channel == -1: - segmentation_class: Type[ROIExtractionAlgorithm] = analysis_algorithm_dict.get( - self.algorithm_parameters["algorithm_name"], None + segmentation_class: Type[ROIExtractionAlgorithm] = AnalysisAlgorithmSelection.get( + self.algorithm_parameters["algorithm_name"] ) if segmentation_class is None: # pragma: no cover raise ValueError(f"Segmentation class {self.algorithm_parameters['algorithm_name']} do not found") - channel = self.algorithm_parameters["values"][segmentation_class.get_channel_parameter_name()] + if segmentation_class.__new_style__: + channel = getattr(self.algorithm_parameters["values"], segmentation_class.get_channel_parameter_name()) + else: + channel = self.algorithm_parameters["values"][segmentation_class.get_channel_parameter_name()] # FIXME use additional information old_mask = self.image.mask @@ -584,7 +590,9 @@ def __init__(self, calculation: BaseCalculation, write_threshold: int = 40): self.file_type = FileType.text_file self.writing = False data = SheetData("calculation_info", [("Description", "str"), ("JSON", "str")], raw=True) - data.add_data([str(calculation.calculation_plan), json.dumps(calculation.calculation_plan, cls=PartEncoder)], 0) + data.add_data( + [str(calculation.calculation_plan), json.dumps(calculation.calculation_plan, cls=PartSegEncoder)], 0 + ) self.sheet_dict = {} self.calculation_info = {} self.sheet_set = {"Errors"} @@ -811,7 +819,7 @@ def write_calculation_plan(writer: pd.ExcelWriter, calculation_plan: Calculation sheet.set_row(1, description.count("\n") * 12 + 10) sheet.set_column(0, 0, max(map(len, description.split("\n")))) sheet.set_column(1, 1, 15) - sheet.write("B2", json.dumps(calculation_plan, cls=PartEncoder, indent=2)) + sheet.write("B2", json.dumps(calculation_plan, cls=PartSegEncoder, indent=2)) def get_errors(self) -> List[ErrorInfo]: """ diff --git a/package/PartSegCore/analysis/calculate_pipeline.py b/package/PartSegCore/analysis/calculate_pipeline.py index 233f03449..d595a7d8b 100644 --- a/package/PartSegCore/analysis/calculate_pipeline.py +++ b/package/PartSegCore/analysis/calculate_pipeline.py @@ -4,7 +4,7 @@ import numpy as np from PartSegCore.algorithm_describe_base import ROIExtractionProfile -from PartSegCore.analysis.algorithm_description import analysis_algorithm_dict +from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection from PartSegCore.analysis.analysis_utils import SegmentationPipeline from PartSegCore.mask_create import calculate_mask from PartSegCore.project_info import HistoryElement @@ -53,9 +53,9 @@ def calculate_pipeline(image: Image, mask: typing.Optional[np.ndarray], pipeline def calculate_segmentation_step( profile: ROIExtractionProfile, image: Image, mask: typing.Optional[np.ndarray] ) -> typing.Tuple[ROIExtractionResult, str]: - algorithm: RestartableAlgorithm = analysis_algorithm_dict[profile.algorithm]() + algorithm: RestartableAlgorithm = AnalysisAlgorithmSelection[profile.algorithm]() algorithm.set_image(image) algorithm.set_mask(mask) parameters = profile.values - algorithm.set_parameters(**parameters) + algorithm.set_parameters(parameters) return algorithm.calculation_run(_empty_fun), algorithm.get_info_text() diff --git a/package/PartSegCore/analysis/calculation_plan.py b/package/PartSegCore/analysis/calculation_plan.py index b1aa9bceb..46e158db2 100644 --- a/package/PartSegCore/analysis/calculation_plan.py +++ b/package/PartSegCore/analysis/calculation_plan.py @@ -8,15 +8,19 @@ from copy import copy, deepcopy from enum import Enum +from pydantic import BaseModel as PydanticBaseModel + +from PartSegCore.utils import BaseModel + from ..algorithm_describe_base import ROIExtractionProfile -from ..class_generator import BaseSerializableClass, enum_register +from ..class_register import register_class, rename_key from ..mask_create import MaskProperty from ..universal_const import Units -from . import analysis_algorithm_dict +from . import AnalysisAlgorithmSelection from .measurement_calculation import MeasurementProfile -class MaskBase: +class MaskBase(BaseModel): """ Base class for mask in calculation plan. @@ -40,10 +44,8 @@ def __str__(self): return self.name.replace("_", " ") -enum_register.register_class(RootType) - - -class MaskCreate(MaskBase, BaseSerializableClass): +@register_class(old_paths=["PartSeg.utils.analysis.calculation_plan.MaskCreate"]) +class MaskCreate(MaskBase): """ Description of mask creation in calculation plan. @@ -57,14 +59,16 @@ def __str__(self): return f"Mask create: {self.name}\n" + str(self.mask_property).split("\n", 1)[1] -class MaskUse(MaskBase, BaseSerializableClass): +@register_class(old_paths=["PartSeg.utils.analysis.calculation_plan.MaskUse"]) +class MaskUse(MaskBase): """ Reuse of already defined mask Will be deprecated in short time """ -class MaskSum(MaskBase, BaseSerializableClass): +@register_class(old_paths=["PartSeg.utils.analysis.calculation_plan.MaskSum"]) +class MaskSum(MaskBase): """ Description of OR operation on mask @@ -77,7 +81,8 @@ class MaskSum(MaskBase, BaseSerializableClass): mask2: str -class MaskIntersection(MaskBase, BaseSerializableClass): +@register_class(old_paths=["PartSeg.utils.analysis.calculation_plan.MaskIntersection"]) +class MaskIntersection(MaskBase): """ Description of AND operation on mask @@ -90,7 +95,8 @@ class MaskIntersection(MaskBase, BaseSerializableClass): mask2: str -class Save(BaseSerializableClass): +@register_class(old_paths=["PartSeg.utils.analysis.calculation_plan.Save"]) +class Save(BaseModel): """ Save operation description @@ -108,7 +114,12 @@ class Save(BaseSerializableClass): values: dict -class MeasurementCalculate(BaseSerializableClass): +@register_class( + version="0.0.1", + migrations=[("0.0.1", rename_key("statistic_profile", "measurement_profile", optional=True))], + old_paths=["PartSeg.utils.analysis.calculation_plan.StatisticCalculate"], +) +class MeasurementCalculate(BaseModel): """ Measurement calculation description @@ -118,19 +129,10 @@ class MeasurementCalculate(BaseSerializableClass): :ivar str name_prefix: prefix of column names """ - __old_names__ = "StatisticCalculate" channel: int units: Units measurement_profile: MeasurementProfile name_prefix: str - # TODO rename statistic_profile to measurement_profile - - # noinspection PyOverloads,PyMissingConstructor - # pylint: disable=W0104 - # pragma: no cover - @typing.overload - def __init__(self, channel: int, units: Units, measurement_profile: MeasurementProfile, name_prefix: str): - ... @property def name(self): @@ -164,7 +166,8 @@ def get_save_path(op: Save, calculation: "FileCalculation") -> str: return os.path.join(calculation.result_prefix, rel_path + op.suffix + extension) -class MaskMapper: +@register_class(old_paths=["PartSeg.utils.analysis.calculation_plan.MaskMapper"]) +class MaskMapper(BaseModel): """ Base class for obtaining mask from computer disc @@ -191,7 +194,8 @@ def is_ready() -> bool: return True -class MaskSuffix(MaskMapper, BaseSerializableClass): +@register_class(old_paths=["PartSeg.utils.analysis.calculation_plan.MaskSuffix"]) +class MaskSuffix(MaskMapper): """ Description of mask form file obtained by adding suffix to image file path @@ -201,12 +205,6 @@ class MaskSuffix(MaskMapper, BaseSerializableClass): suffix: str - # noinspection PyMissingConstructor,PyOverloads - # pylint: disable=W0104 - @typing.overload - def __init__(self, name: str, suffix: str): # pragma: no cover - ... - def get_mask_path(self, file_path: str) -> str: base, ext = os.path.splitext(file_path) return base + self.suffix + ext @@ -215,7 +213,8 @@ def get_parameters(self): return {"name": self.name, "suffix": self.suffix} -class MaskSub(MaskMapper, BaseSerializableClass): +@register_class(old_paths=["PartSeg.utils.analysis.calculation_plan.MaskSub"]) +class MaskSub(MaskMapper): """ Description of mask form file obtained by substitution @@ -227,12 +226,6 @@ class MaskSub(MaskMapper, BaseSerializableClass): base: str rep: str - # noinspection PyMissingConstructor,PyOverloads - # pylint: disable=W0104 - @typing.overload - def __init__(self, name: str, base: str, rep: str): # pragma: no cover - ... - def get_mask_path(self, file_path: str) -> str: dir_name, filename = os.path.split(file_path) filename = filename.replace(self.base, self.rep) @@ -242,17 +235,12 @@ def get_parameters(self): return {"name": self.name, "base": self.base, "rep": self.rep} -class MaskFile(MaskMapper, BaseSerializableClass): +@register_class(old_paths=["PartSeg.utils.analysis.calculation_plan.MaskFile"]) +class MaskFile(MaskMapper): # TODO Check implementation path_to_file: str name_dict: typing.Optional[dict] = None - # noinspection PyMissingConstructor,PyOverloads - # pylint: disable=W0104 - @typing.overload - def __init__(self, name: str, path_to_file: str, name_dict: typing.Optional[dict] = None): # pragma: no cover - ... - def is_ready(self) -> bool: return os.path.exists(self.path_to_file) @@ -306,6 +294,7 @@ class PlanChanges(Enum): replace_node = 3 #: +@register_class(old_paths=["PartSeg.utils.analysis.calculation_plan.CalculationTree"]) class CalculationTree: """ Structure for describe calculation structure @@ -313,7 +302,7 @@ class CalculationTree: def __init__( self, - operation: typing.Union[BaseSerializableClass, ROIExtractionProfile, MeasurementCalculate, RootType], + operation: typing.Union[PydanticBaseModel, ROIExtractionProfile, MeasurementCalculate, RootType], children: typing.List["CalculationTree"], ): if operation == "root": @@ -327,6 +316,9 @@ def __str__(self): def __repr__(self): return f"CalculationTree(operation={repr(self.operation)}, children={self.children})" + def as_dict(self): + return {"operation": self.operation, "children": self.children} + class NodeType(Enum): """Type of node in calculation""" @@ -490,6 +482,9 @@ def __init__(self, tree: typing.Optional[CalculationTree] = None, name: str = "" self.changes = [] self.current_node = None + def as_dict(self): + return {"tree": self.execution_tree, "name": self.name} + def get_root_type(self): return self.execution_tree.operation @@ -541,7 +536,7 @@ def __copy__(self): def __deepcopy__(self, memo): return CalculationPlan(name=self.name, tree=deepcopy(self.execution_tree)) - def get_node(self, search_pos=None): + def get_node(self, search_pos: typing.Optional[typing.List[int]] = None) -> CalculationTree: """ :param search_pos: :return: CalculationTree @@ -638,7 +633,7 @@ def replace_name(self, name): if self.current_pos is None: return node = self.get_node() - node.operation = node.operation.replace_(name=name) + node.operation = node.operation.copy(update={"name": name}) self.changes.append((self.current_pos, node, PlanChanges.replace_node)) def has_children(self): @@ -767,7 +762,7 @@ def _pretty_print(self, elem: CalculationTree, indent) -> str: name = self.get_el_name(elem.operation) if isinstance(elem.operation, (MeasurementCalculate, ROIExtractionProfile, MaskCreate)): if isinstance(elem.operation, ROIExtractionProfile): - txt = elem.operation.pretty_print(analysis_algorithm_dict) + txt = elem.operation.pretty_print(AnalysisAlgorithmSelection) else: txt = str(elem.operation) txt = "\n".join(txt.split("\n")[1:]) diff --git a/package/PartSegCore/analysis/load_functions.py b/package/PartSegCore/analysis/load_functions.py index f54ca964a..7d3104f9a 100644 --- a/package/PartSegCore/analysis/load_functions.py +++ b/package/PartSegCore/analysis/load_functions.py @@ -23,24 +23,19 @@ LoadBase, LoadPoints, SegmentationType, - UpdateLoadedMetadataBase, WrongFileTypeException, check_segmentation_type, + load_metadata_base, open_tar_file, proxy_callback, tar_to_buff, ) -from ..json_hooks import ProfileDict, check_loaded_dict from ..mask.io_functions import LoadROIImage from ..project_info import HistoryElement from ..roi_info import ROIInfo from ..universal_const import UNIT_SCALE, Units -from .analysis_utils import SegmentationPipeline, SegmentationPipelineElement -from .calculation_plan import CalculationPlan, CalculationTree, MeasurementCalculate +from ..utils import ProfileDict, check_loaded_dict from .io_utils import MaskInfo, ProjectTuple, project_version_info -from .measurement_base import Leaf, MeasurementEntry, Node -from .measurement_calculation import MeasurementProfile -from .save_hooks import part_hook __all__ = [ "LoadStackImage", @@ -49,7 +44,6 @@ "LoadMask", "load_dict", "load_metadata", - "UpdateLoadedMetadataAnalysis", "LoadMaskSegmentation", ] @@ -317,99 +311,6 @@ def load( return res -class UpdateLoadedMetadataAnalysis(UpdateLoadedMetadataBase): - json_hook = part_hook - - @classmethod - def update_segmentation_profile(cls, profile_data: ROIExtractionProfile) -> ROIExtractionProfile: - replace_name_dict = { - "Split Mask on Part": "Mask Distance Splitting", - "Lower threshold flow": "Lower threshold with watershed", - "Upper threshold flow": "Upper threshold with watershed", - } - if profile_data.algorithm in replace_name_dict: - profile_data.algorithm = replace_name_dict[profile_data.algorithm] - return super().update_segmentation_profile(profile_data) - - @classmethod - def update_calculation_tree(cls, data: CalculationTree) -> CalculationTree: - data.operation = cls.recursive_update(data.operation) - data.children = [cls.update_calculation_tree(x) for x in data.children] - return data - - @classmethod - def update_calculation_plan(cls, data: CalculationPlan) -> CalculationPlan: - data.execution_tree = cls.update_calculation_tree(data.execution_tree) - return data - - @classmethod - def update_segmentation_pipeline_element(cls, data: SegmentationPipelineElement) -> SegmentationPipelineElement: - return SegmentationPipelineElement(cls.update_segmentation_profile(data.segmentation), data.mask_property) - - @classmethod - def update_segmentation_pipeline(cls, data: SegmentationPipeline) -> SegmentationPipeline: - return SegmentationPipeline( - name=data.name, - segmentaion=cls.update_segmentation_profile(data.segmentation), - mask_history=[cls.update_segmentation_pipeline_element(x) for x in data.mask_history], - ) - - @classmethod - def update_measurement_profile(cls, data: MeasurementProfile) -> MeasurementProfile: - data.chosen_fields = [cls.update_measurement_entry(x) for x in data.chosen_fields] - return data - - @classmethod - def update_measurement_entry(cls, data: MeasurementEntry) -> MeasurementEntry: - return data.replace_(calculation_tree=cls.update_measurement_calculation_tree(data.calculation_tree)) - - @classmethod - def update_measurement_calculation_tree(cls, data: typing.Union[Leaf, Node]) -> typing.Union[Leaf, Node]: - if isinstance(data, Leaf): - replace_name_dict = { - "Moment of inertia": "Moment", - "Components Number": "Components number", - "Pixel Brightness Sum": "Pixel brightness sum", - "Longest main axis length": "First principal axis length", - "Middle main axis length": "Second principal axis length", - "Shortest main axis length": "Third principal axis length", - "split on part volume": "distance splitting volume", - "split on part pixel brightness sum": "distance splitting pixel brightness sum", - "Rim Volume": "rim volume", - "Rim Pixel Brightness Sum": "rim pixel brightness sum", - "segmentation distance": "ROI distance", - } - - if data.name in replace_name_dict: - # noinspection PyUnresolvedReferences - return data._replace(name=replace_name_dict[data.name]) - return data - - return data.replace_( - left=cls.update_measurement_calculation_tree(data.left), - right=cls.update_measurement_calculation_tree(data.right), - ) - - @classmethod - def update_measurement_calculate(cls, data: MeasurementCalculate): - return data.replace_(measurement_profile=cls.update_measurement_profile(data.measurement_profile)) - - @classmethod - def recursive_update(cls, data): - if isinstance(data, CalculationPlan): - return cls.update_calculation_plan(data) - if isinstance(data, CalculationTree): - return cls.update_calculation_tree(data) - if isinstance(data, SegmentationPipeline): - return cls.update_segmentation_pipeline(data) - if isinstance(data, MeasurementProfile): - return cls.update_measurement_profile(data) - if isinstance(data, MeasurementCalculate): - return cls.update_measurement_calculate(data) - - return super().recursive_update(data) - - class LoadProfileFromJSON(LoadBase): @classmethod def get_short_name(cls): @@ -445,7 +346,7 @@ def load_metadata(data: typing.Union[str, Path]): :param data: path to json file, string with json, or opened file :return: restored structures """ - return UpdateLoadedMetadataAnalysis.load_json_data(data) + return load_metadata_base(data) def update_algorithm_dict(dkt): @@ -455,7 +356,6 @@ def update_algorithm_dict(dkt): profile = ROIExtractionProfile(name="", algorithm=dkt["algorithm_name"], values=dkt["values"]) else: return dkt - profile = UpdateLoadedMetadataAnalysis.recursive_update(profile) res = dict(dkt) res.update({"algorithm_name": profile.algorithm, "values": profile.values}) return res diff --git a/package/PartSegCore/analysis/measurement_base.py b/package/PartSegCore/analysis/measurement_base.py index 35b341ba3..2b984b059 100644 --- a/package/PartSegCore/analysis/measurement_base.py +++ b/package/PartSegCore/analysis/measurement_base.py @@ -1,20 +1,31 @@ import sys from abc import ABC from enum import Enum -from typing import Any, Dict, List, Optional, Set, Union +from typing import Any, Dict, ForwardRef, Optional, Set, Union import numpy as np +from pydantic import Field, validator from sympy import Symbol, symbols +from PartSegCore.utils import BaseModel +from PartSegImage import Channel from PartSegImage.image import Spacing -from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmDescribeNotFound, AlgorithmProperty -from ..channel_class import Channel -from ..class_generator import BaseSerializableClass, enum_register +from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmDescribeNotFound, base_model_to_algorithm_property +from ..class_register import REGISTER, class_to_str, register_class, rename_key from ..universal_const import Units +@register_class( + old_paths=[ + "PartSeg.utils.analysis.statistics_calculation.PerComponent", + "PartSeg.utils.analysis.measurement_base.PerComponent", + "segmentation_analysis.statistics_calculation.PerComponent", + ] +) class PerComponent(Enum): + """How measurement should be calculated""" + No = 1 Yes = 2 Mean = 3 @@ -23,7 +34,16 @@ def __str__(self): return self.name.replace("_", " ") +@register_class( + old_paths=[ + "PartSeg.utils.analysis.statistics_calculation.AreaType", + "PartSeg.utils.analysis.measurement_base.AreaType", + "segmentation_analysis.statistics_calculation.AreaType", + ] +) class AreaType(Enum): + """On which area type measurement should be calculated""" + ROI = 1 Mask = 2 Mask_without_ROI = 3 @@ -32,42 +52,53 @@ def __str__(self): return self.name.replace("_", " ") -enum_register.register_class(AreaType) -enum_register.register_class(PerComponent) +def _migrate_leaf_dict(dkt): + from .measurement_calculation import MEASUREMENT_DICT + + new_dkt = dkt.copy() + new_dkt["parameter_dict"] = new_dkt.pop("dict") + new_dkt["name"] = MEASUREMENT_DICT[new_dkt["name"]].get_name() + return new_dkt -class Leaf(BaseSerializableClass): + +@register_class( + version="0.0.2", + old_paths=[ + "PartSeg.utils.analysis.statistics_calculation.Leaf", + "PartSeg.utils.analysis.measurement_base.Leaf", + "segmentation_analysis.statistics_calculation.Leaf", + ], + migrations=[("0.0.1", _migrate_leaf_dict), ("0.0.2", rename_key("parameter_dict", "parameters"))], +) +class Leaf(BaseModel): """ Class for describe calculation of basic measurement - - :ivar str name: node name of method used to calculate - :ivar dict dict: additional parameters of method - :ivar float power: power to be applied to result of calculation methods - :ivar AreaType area: which type of ROI should be used for calculation - :ivar PerComponent per_component: if value should be calculated per component or for whole roi set - :ivar Channel channel: probably not used TODO Check """ - # noinspection PyMissingConstructor,PyShadowingBuiltins, PyUnusedLocal - # pylint: disable=W0104,W0221,W0622,W0231 - def __init__( - self, - name: str, - dict: Dict = None, - power: float = 1.0, - area: Optional[AreaType] = None, - per_component: Optional[PerComponent] = None, - channel: Optional[Channel] = None, - ): - ... - name: str - dict: Dict = {} + parameters: Any = Field(default_factory=dict) power: float = 1.0 area: Optional[AreaType] = None per_component: Optional[PerComponent] = None channel: Optional[Channel] = None + @validator("parameters") + def _validate_parameters(cls, v, values): # pylint: disable=R0201 + if not isinstance(v, dict) or "name" not in values: + return v + from .measurement_calculation import MEASUREMENT_DICT + + if values["name"] not in MEASUREMENT_DICT: + return v + + method = MEASUREMENT_DICT[values["name"]] + if not method.__new_style__ or not method.__argument_class__.__fields__: + return v + + v = REGISTER.migrate_data(class_to_str(method.__argument_class__), {}, v) + return method.__argument_class__(**v) + def get_channel_num(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) -> Set[Channel]: """ Get set with number of channels needed for calculate this measurement @@ -76,31 +107,40 @@ def get_channel_num(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) :return: set of channels num """ resp = set() - if self.channel is not None and self.channel >= 0: + if self.channel is not None and self.channel.value != -1: resp.add(self.channel) try: measurement_method = measurement_dict[self.name] - for el in measurement_method.get_fields(): + if measurement_method.__new_style__: + fields = base_model_to_algorithm_property(measurement_method.__argument_class__) + else: + fields = measurement_method.get_fields() + for el in fields: if isinstance(el, str): continue - if issubclass(el.value_type, Channel) and el.name in self.dict: - resp.add(self.dict[el.name]) + if el.value_type is Channel: + if isinstance(self.parameters, dict): + if el.name in self.parameters: + resp.add(Channel(self.parameters[el.name])) + elif hasattr(self.parameters, el.name): + resp.add(getattr(self.parameters, el.name)) except KeyError as e: raise AlgorithmDescribeNotFound(self.name) from e return resp def _parameters_string(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) -> str: - if len(self.dict) == 0 and self.channel is None: + parameters = dict(self.parameters) + if not parameters and self.channel is None: return "" arr = [] - if self.channel is not None and self.channel >= 0: - arr.append(f"channel={self.channel+1}") + if self.channel is not None and self.channel.value != -1: + arr.append(f"channel={self.channel}") if self.name in measurement_dict: measurement_method = measurement_dict[self.name] fields_dict = measurement_method.get_fields_dict() - arr.extend(f"{fields_dict[k].user_name}={v}" for k, v in self.dict.items()) + arr.extend(f"{fields_dict[k].user_name}={v}" for k, v in parameters.items()) else: - arr.extend(f"{k.replace('_', ' ')}={v}" for k, v in self.dict.items()) + arr.extend(f"{k.replace('_', ' ')}={v}" for k, v in parameters.items()) return "[" + ", ".join(arr) + "]" def _plugin_info(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) -> str: @@ -117,9 +157,16 @@ def _plugin_info(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) -> return "" def pretty_print(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) -> str: + """ + Pretty print for presentation in user interface. + + :param measurement_dict: dict with additional information used for more detailed description + :return: string with indentation + """ + resp = self.name if self.area is not None: - resp = str(self.area) + " " + resp + resp = f"{self.area} {resp}" resp = self._plugin_info(measurement_dict) + resp if self.per_component is not None: if self.per_component == PerComponent.Yes: @@ -134,7 +181,12 @@ def pretty_print(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) -> def __str__(self): # pragma: no cover return self.pretty_print({}) - def get_unit(self, ndim) -> Symbol: + def get_unit(self, ndim: int) -> Symbol: + """ + Return unit of selected measurement reflecting dimensionality. + + :param ndim: data dimensionality + """ from PartSegCore.analysis import MEASUREMENT_DICT method = MEASUREMENT_DICT[self.name] @@ -143,8 +195,13 @@ def get_unit(self, ndim) -> Symbol: return method.get_units(ndim) def is_per_component(self) -> bool: + """If measurement return list of result or single value.""" return self.per_component == PerComponent.Yes + def need_mask(self) -> bool: + """If this measurement need mast for proper calculation.""" + return self.area in [AreaType.Mask, AreaType.Mask_without_ROI] + def replace(self, **kwargs) -> Leaf: for key in list(kwargs.keys()): @@ -152,46 +209,58 @@ def replace(self, **kwargs) -> Leaf: continue if not hasattr(self, key): raise ValueError(f"Unknown parameter {key}") - if getattr(self, key) is not None and (key != "dict" or self.dict): + if getattr(self, key) is not None and (key != "parameters" or dict(self.parameters)): del kwargs[key] - dkt = self.asdict() - dkt.update(kwargs) - return Leaf(**dkt) + return self.copy(update=kwargs) Leaf.replace_ = replace +Node = ForwardRef("Node") -class Node(BaseSerializableClass): - left: Union["Node", Leaf] - op: str - right: Union["Node", Leaf] - # noinspection PyMissingConstructor, PyUnusedLocal - # pylint: disable=W0104,W0231 - def __init__(self, left: Union["Node", Leaf], op: str, right: Union["Node", Leaf]): - ... +@register_class( + old_paths=[ + "PartSeg.utils.analysis.statistics_calculation.Node", + "PartSeg.utils.analysis.measurement_base.Node", + "segmentation_analysis.statistics_calculation.Node", + ] +) +class Node(BaseModel): + """ + Class for describe operation between two measurements + """ + + left: Union[Node, Leaf] + op: str = Field( + description="Operation to perform between left and right child. Currently only division (`/`) supported" + ) + right: Union[Node, Leaf] def get_channel_num(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) -> Set[Channel]: return self.left.get_channel_num(measurement_dict) | self.right.get_channel_num(measurement_dict) def __str__(self): # pragma: no cover - left_text = "(" + str(self.left) + ")" if isinstance(self.left, Node) else str(self.left) - right_text = "(" + str(self.right) + ")" if isinstance(self.right, Node) else str(self.right) + left_text = f"({str(self.left)})" if isinstance(self.left, Node) else str(self.left) + + right_text = f"({str(self.right)})" if isinstance(self.right, Node) else str(self.right) + return left_text + self.op + right_text def pretty_print(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) -> str: # pragma: no cover left_text = ( - "(" + self.left.pretty_print(measurement_dict) + ")" + f"({self.left.pretty_print(measurement_dict)})" if isinstance(self.left, Node) else self.left.pretty_print(measurement_dict) ) + right_text = ( - "(" + self.right.pretty_print(measurement_dict) + ")" + f"({self.right.pretty_print(measurement_dict)})" if isinstance(self.right, Node) else self.right.pretty_print(measurement_dict) ) + return left_text + self.op + right_text def get_unit(self, ndim) -> Symbol: @@ -202,16 +271,22 @@ def get_unit(self, ndim) -> Symbol: def is_per_component(self) -> bool: return self.left.is_per_component() or self.right.is_per_component() + def need_mask(self): + return self.left.need_mask() or self.right.need_mask() -class MeasurementEntry(BaseSerializableClass): - # noinspection PyUnusedLocal - # noinspection PyMissingConstructor - __old_names__ = "StatisticEntry" - # noinspection PyMissingConstructor,PyUnusedLocal - # pylint: disable=W0104,W0231 - def __init__(self, name: str, calculation_tree: Union[Node, Leaf]): - ... +Node.update_forward_refs() + + +@register_class( + old_paths=[ + "PartSeg.utils.analysis.statistics_calculation.StatisticEntry", + "PartSeg.utils.analysis.measurement_base.StatisticEntry", + "segmentation_analysis.statistics_calculation.StatisticEntry", + ] +) +class MeasurementEntry(BaseModel): + """Describe single measurement in measurement set""" name: str calculation_tree: Union[Node, Leaf] @@ -229,6 +304,8 @@ class MeasurementMethodBase(AlgorithmDescribeBase, ABC): based on text_info[0] the measurement name wil be generated, based_on text_info[1] the description is generated """ + __argument_class__ = BaseModel + text_info = "", "" need_class_method = [ @@ -242,6 +319,7 @@ class MeasurementMethodBase(AlgorithmDescribeBase, ABC): @classmethod def get_name(cls) -> str: + """Name of measurement""" return str(cls.get_starting_leaf().name) @classmethod @@ -254,11 +332,6 @@ def is_component(cls) -> bool: """Return information if Need information about components""" return False - @classmethod - def get_fields(cls) -> List[Union[str, AlgorithmProperty]]: - """Additional fields needed by algorithm. like radius of dilation""" - return [] - @staticmethod def calculate_property( # image: Image, @@ -291,7 +364,7 @@ def calculate_property( @classmethod def get_starting_leaf(cls) -> Leaf: - """This leaf is putted on default list""" + """This leaf is put on default list""" return Leaf(name=cls._display_name()) @classmethod diff --git a/package/PartSegCore/analysis/measurement_calculation.py b/package/PartSegCore/analysis/measurement_calculation.py index 679452a2f..44ee35257 100644 --- a/package/PartSegCore/analysis/measurement_calculation.py +++ b/package/PartSegCore/analysis/measurement_calculation.py @@ -1,3 +1,4 @@ +import warnings from collections import OrderedDict from contextlib import suppress from enum import Enum @@ -23,16 +24,17 @@ import pandas as pd import SimpleITK from mahotas.features import haralick +from pydantic import Field from scipy.spatial.distance import cdist from sympy import symbols from PartSegCore.segmentation.restartable_segmentation_algorithms import LowerThresholdAlgorithm -from PartSegImage import Image +from PartSegCore.utils import BaseModel +from PartSegImage import Channel, Image from .. import autofit as af -from ..algorithm_describe_base import AlgorithmProperty, Register, ROIExtractionProfile -from ..channel_class import Channel -from ..class_generator import enum_register +from ..algorithm_describe_base import Register, ROIExtractionProfile +from ..class_register import register_class, rename_key from ..mask_partition_utils import BorderRim, MaskDistanceSplit from ..roi_info import ROIInfo from ..universal_const import UNIT_SCALE, Units @@ -261,24 +263,22 @@ def get_separated(self, all_components=False) -> List[List[MeasurementValueType] return res -class MeasurementProfile: - PARAMETERS = ["name", "chosen_fields", "reversed_brightness", "use_gauss_image", "name_prefix"] +class MeasurementProfile(BaseModel): + name: str + chosen_fields: List[MeasurementEntry] + name_prefix: str = "" - def __init__(self, name, chosen_fields: List[MeasurementEntry], name_prefix=""): - self.name = name - self.chosen_fields: List[MeasurementEntry] = chosen_fields - self._need_mask = False - for cf_val in chosen_fields: - self._need_mask = self._need_mask or self.need_mask(cf_val.calculation_tree) - self.name_prefix = name_prefix + @property + def _need_mask(self): + return any(cf_val.calculation_tree.need_mask() for cf_val in self.chosen_fields) - def to_dict(self): - return {"name": self.name, "chosen_fields": self.chosen_fields, "name_prefix": self.name_prefix} - - def need_mask(self, tree): - if isinstance(tree, Leaf): - return tree.area in [AreaType.Mask, AreaType.Mask_without_ROI] - return self.need_mask(tree.left) or self.need_mask(tree.right) + def to_dict(self): # pragma: no cover + warnings.warn( + f"{self.__class__.__name__}.to_dict is deprecated. Use as_dict instead", + category=FutureWarning, + stacklevel=2, + ) + return dict(self) def _need_mask_without_segmentation(self, tree): if isinstance(tree, Leaf): @@ -337,7 +337,7 @@ def get_component_info(self, unit: Units): ] def is_any_mask_measurement(self): - return any(self.need_mask(el.calculation_tree) for el in self.chosen_fields) + return any(el.calculation_tree.need_mask() for el in self.chosen_fields) def _is_component_measurement(self, node): if isinstance(node, Leaf): @@ -353,7 +353,7 @@ def _prepare_leaf_kw(node, kwargs, method, area_type): AreaType.ROI: "segmentation", } kw = dict(kwargs) - kw.update(node.dict) + kw.update(dict(node.parameters)) if node.channel is not None: kw["channel"] = kw[f"channel_{node.channel}"] @@ -416,7 +416,9 @@ def _calculate_leaf( ) -> Tuple[Union[float, np.ndarray], symbols, AreaType]: method: MeasurementMethodBase = MEASUREMENT_DICT[node.name] - hash_str = hash_fun_call_name(method, node.dict, node.area, node.per_component, node.channel, NO_COMPONENT) + hash_str = hash_fun_call_name( + method, node.parameters, node.area, node.per_component, node.channel, NO_COMPONENT + ) area_type = method.area_type(node.area) if hash_str in help_dict: val = help_dict[hash_str] @@ -1146,10 +1148,7 @@ def get_units(cls, ndim): class RimVolume(MeasurementMethodBase): text_info = "rim volume", "Calculate volumes for elements in radius (in physical units) from mask" - - @classmethod - def get_fields(cls): - return BorderRim.get_fields() + __argument_class__ = BorderRim.__argument_class__ @classmethod def get_starting_leaf(cls): @@ -1177,10 +1176,7 @@ class RimPixelBrightnessSum(MeasurementMethodBase): "rim pixel brightness sum", "Calculate mass for components located within rim (in physical units) from mask", ) - - @classmethod - def get_fields(cls): - return BorderRim.get_fields() + __argument_class__ = BorderRim.__argument_class__ @classmethod def get_starting_leaf(cls): @@ -1213,6 +1209,7 @@ def area_type(area: AreaType): return AreaType.ROI +@register_class(old_paths=["PartSeg.utils.analysis.statistics_calculation.DistancePoint"]) class DistancePoint(Enum): Border = 1 Mass_center = 2 @@ -1222,23 +1219,15 @@ def __str__(self): return self.name.replace("_", " ") -try: - # noinspection PyUnresolvedReferences,PyUnboundLocalVariable - reloading -except NameError: - reloading = False - enum_register.register_class(DistancePoint) +@register_class(version="0.0.1", migrations=[("0.0.1", rename_key("distance_to_segmentation", "distance_to_roi"))]) +class DistanceMaskROIParameters(BaseModel): + distance_from_mask: DistancePoint = DistancePoint.Border + distance_to_roi: DistancePoint = Field(DistancePoint.Border, title="Distance to ROI") class DistanceMaskROI(MeasurementMethodBase): text_info = "ROI distance", "Calculate distance between ROI and mask" - - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("distance_from_mask", "Distance from mask", DistancePoint.Border), - AlgorithmProperty("distance_to_segmentation", "Distance to ROI", DistancePoint.Border), - ] + __argument_class__ = DistanceMaskROIParameters @staticmethod def calculate_points(channel, area_array, voxel_size, result_scalar, point_type: DistancePoint) -> np.ndarray: @@ -1264,7 +1253,7 @@ def calculate_property( voxel_size, result_scalar, distance_from_mask: DistancePoint, - distance_to_segmentation: DistancePoint, + distance_to_roi: DistancePoint, *args, **kwargs, ): # pylint: disable=W0221 @@ -1275,7 +1264,7 @@ def calculate_property( if not (np.any(mask) and np.any(area_array)): return 0 mask_pos = cls.calculate_points(channel, mask, voxel_size, result_scalar, distance_from_mask) - seg_pos = cls.calculate_points(channel, area_array, voxel_size, result_scalar, distance_to_segmentation) + seg_pos = cls.calculate_points(channel, area_array, voxel_size, result_scalar, distance_to_roi) if 1 in {mask_pos.shape[0], seg_pos.shape[0]}: return np.min(cdist(mask_pos, seg_pos)) @@ -1301,28 +1290,27 @@ def area_type(area: AreaType): return AreaType.ROI +class DistanceROIROIParameters(BaseModel): + profile: ROIExtractionProfile = Field( + ROIExtractionProfile( + name="default", + algorithm=LowerThresholdAlgorithm.get_name(), + values=LowerThresholdAlgorithm.get_default_values(), + ), + title="ROI extraction profile", + ) + distance_from_new_roi: DistancePoint = Field(DistancePoint.Border, title="Distance new ROI") + distance_to_roi: DistancePoint = Field(DistancePoint.Border, title="Distance to ROI") + + class DistanceROIROI(DistanceMaskROI): text_info = "to new ROI distance", "Calculate distance between ROI and new ROI" + __argument_class__ = DistanceROIROIParameters @classmethod def get_starting_leaf(cls): return Leaf(name=cls.text_info[0], area=AreaType.ROI) - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty( - "profile", - "ROI extraction profile", - ROIExtractionProfile( - "default", LowerThresholdAlgorithm.get_name(), LowerThresholdAlgorithm.get_default_values() - ), - value_type=ROIExtractionProfile, - ), - AlgorithmProperty("distance_from_new_roi", "Distance new ROI", DistancePoint.Border), - AlgorithmProperty("distance_to_roi", "Distance to ROI", DistancePoint.Border), - ] - # noinspection PyMethodOverriding @classmethod def calculate_property( @@ -1369,7 +1357,7 @@ def calculate_property( tuple(voxel_size), result_scalar, distance_from_mask=distance_from_new_roi, - distance_to_segmentation=distance_to_roi, + distance_to_roi=distance_to_roi, ) @staticmethod @@ -1377,28 +1365,27 @@ def need_full_data(): return True +class ROINeighbourhoodROIParameters(BaseModel): + profile: ROIExtractionProfile = Field( + ROIExtractionProfile( + name="default", + algorithm=LowerThresholdAlgorithm.get_name(), + values=LowerThresholdAlgorithm.get_default_values(), + ), + title="ROI extraction profile", + ) + distance: float = Field(500, ge=0, le=10000, title="Distance") + units: Units = Units.nm + + class ROINeighbourhoodROI(DistanceMaskROI): text_info = "Neighbourhood new ROI presence", "Count how many of new roi are present in neighbourhood of new ROI" + __argument_class__ = ROINeighbourhoodROIParameters @classmethod def get_starting_leaf(cls): return Leaf(name=cls.text_info[0], area=AreaType.ROI) - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty( - "profile", - "ROI extraction profile", - ROIExtractionProfile( - "default", LowerThresholdAlgorithm.get_name(), LowerThresholdAlgorithm.get_default_values() - ), - value_type=ROIExtractionProfile, - ), - AlgorithmProperty("distance", "Distance", 500.0, options_range=(0, 10000), value_type=float), - AlgorithmProperty("units", "Units", Units.nm, value_type=Units), - ] - # noinspection PyMethodOverriding @classmethod def calculate_property( @@ -1451,17 +1438,16 @@ def need_full_data(): return True +class SplitOnPartParameters(MaskDistanceSplit.__argument_class__): + part_selection: int = Field(2, title="Which part (from border)", ge=1, le=1024) + + class SplitOnPartVolume(MeasurementMethodBase): text_info = ( "distance splitting volume", "Split mask on parts and then calculate volume of cross of segmentation and mask part", ) - - @classmethod - def get_fields(cls): - return MaskDistanceSplit.get_fields() + [ - AlgorithmProperty("part_selection", "Which part (from border)", 2, (1, 1024)) - ] + __argument_class__ = SplitOnPartParameters @staticmethod def calculate_property(part_selection, area_array, voxel_size, result_scalar, **kwargs): # pylint: disable=W0221 @@ -1487,12 +1473,7 @@ class SplitOnPartPixelBrightnessSum(MeasurementMethodBase): "distance splitting pixel brightness sum", "Split mask on parts and then calculate pixel brightness sum of cross of segmentation and mask part", ) - - @classmethod - def get_fields(cls): - return MaskDistanceSplit.get_fields() + [ - AlgorithmProperty("part_selection", "Which part (from border)", 2, (1, 1024)) - ] + __argument_class__ = SplitOnPartParameters @staticmethod def calculate_property(part_selection, channel, area_array, **kwargs): # pylint: disable=W0221 @@ -1524,6 +1505,25 @@ def need_channel(cls): DifferenceVariance DifferenceEntropy InfoMeas1 InfoMeas2""".split() +class HaralickEnum(Enum): + AngularSecondMoment = "AngularSecondMoment" + Contrast = "Contrast" + Correlation = "Correlation" + Variance = "Variance" + InverseDifferenceMoment = "InverseDifferenceMoment" + SumAverage = "SumAverage" + SumVariance = "SumVariance" + SumEntropy = "SumEntropy" + Entropy = "Entropy" + DifferenceVariance = "DifferenceVariance" + DifferenceEntropy = "DifferenceEntropy" + InfoMeas1 = "InfoMeas1" + InfoMeas2 = "InfoMeas2" + + def index(self) -> int: + return list(self.__class__).index(self) + + def _rescale_image(data: np.ndarray): if data.dtype == np.uint8: return data @@ -1532,20 +1532,20 @@ def _rescale_image(data: np.ndarray): return ((data - min_val) / ((max_val - min_val) / 255)).astype(np.uint8) +class HaralickParameters(BaseModel): + feature: HaralickEnum = HaralickEnum.AngularSecondMoment + distance: int = Field(1, ge=1, le=10) + + class Haralick(MeasurementMethodBase): + __argument_class__ = HaralickParameters + @classmethod def get_units(cls, ndim) -> symbols: return "1" text_info = "Haralick", "Calculate Haralick features" - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("feature", "Feature", HARALIC_FEATURES[0], possible_values=HARALIC_FEATURES), - AlgorithmProperty("distance", "Distance", 1, options_range=(1, 10)), - ] - @classmethod def need_channel(cls): return True @@ -1554,6 +1554,8 @@ def need_channel(cls): def calculate_property( cls, area_array, channel, distance, feature, _cache=False, **kwargs ): # pylint: disable=W0221 + if isinstance(feature, str): + feature = HaralickEnum(feature) _cache = _cache and "_area" in kwargs and "_per_component" in kwargs if _cache: help_dict: Dict = kwargs["help_dict"] @@ -1564,10 +1566,10 @@ def calculate_property( ) if hash_name not in help_dict: help_dict[hash_name] = cls.calculate_haralick(channel, area_array, distance) - return help_dict[hash_name][HARALIC_FEATURES.index(feature)] + return help_dict[hash_name][feature.index()] res = cls.calculate_haralick(channel, area_array, distance) - return res[HARALIC_FEATURES.index(feature)] + return res[feature.index()] @staticmethod def calculate_haralick(channel, area_array, distance): @@ -1593,12 +1595,13 @@ def get_starting_leaf(cls): return super().get_starting_leaf().replace_(area=AreaType.ROI, per_component=PerComponent.Yes) +class GetROIAnnotationTypeParameters(BaseModel): + name: str = "" + + class GetROIAnnotationType(MeasurementMethodBase): text_info = "annotation by name", "Get roi annotation by name" - - @classmethod - def get_fields(cls): - return [AlgorithmProperty("name", "Name", "")] + __argument_class__ = GetROIAnnotationTypeParameters @classmethod def get_starting_leaf(cls): @@ -1613,26 +1616,19 @@ def get_units(cls, ndim): return "str" +class ColocalizationMeasurementParameters(BaseModel): + channel_fst: Channel = Field(0, title="Channel 1") + channel_scd: Channel = Field(1, title="Channel 2") + colocalization: CorrelationEnum = CorrelationEnum.pearson + randomize: bool = Field( + False, description="If randomize orders of pixels in one channel", title="Randomize channel" + ) + randomize_repeat: int = Field(10, description="Number of repetitions for mean_calculate", title="Randomize num") + + class ColocalizationMeasurement(MeasurementMethodBase): text_info = "Colocalization", "Measurement of colocalization of two channels." - - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("channel_fst", "Channel 1", 0, value_type=Channel), - AlgorithmProperty("channel_scd", "Channel 2", 1, value_type=Channel), - AlgorithmProperty( - "colocalization", - "Colocalization", - CorrelationEnum.pearson, - ), - AlgorithmProperty( - "randomize", "Randomize channel", False, help_text="If randomize orders of pixels in one channel" - ), - AlgorithmProperty( - "randomize_repeat", "Randomize num", 10, help_text="Number of repetitions for mean_calculate" - ), - ] + __argument_class__ = ColocalizationMeasurementParameters @staticmethod def _calculate_masked(data_1, data_2, colocalization): @@ -1717,35 +1713,34 @@ def calc_diam(array, voxel_size): # pragma: no cover return np.sqrt(diam) -MEASUREMENT_DICT = Register( - Volume, - Diameter, - PixelBrightnessSum, - ComponentBoundingBox, - GetROIAnnotationType, - ComponentsNumber, - MaximumPixelBrightness, - MinimumPixelBrightness, - MeanPixelBrightness, - MedianPixelBrightness, - StandardDeviationOfPixelBrightness, - ColocalizationMeasurement, - Moment, - FirstPrincipalAxisLength, - SecondPrincipalAxisLength, - ThirdPrincipalAxisLength, - Compactness, - Sphericity, - Surface, - RimVolume, - RimPixelBrightnessSum, - ROINeighbourhoodROI, - DistanceMaskROI, - DistanceROIROI, - SplitOnPartVolume, - SplitOnPartPixelBrightnessSum, - Voxels, - Haralick, - suggested_base_class=MeasurementMethodBase, -) +MEASUREMENT_DICT = Register(suggested_base_class=MeasurementMethodBase) """Register with all measurements algorithms""" + +MEASUREMENT_DICT.register(Volume) +MEASUREMENT_DICT.register(Diameter) +MEASUREMENT_DICT.register(PixelBrightnessSum, old_names=["Pixel Brightness Sum"]) +MEASUREMENT_DICT.register(ComponentBoundingBox) +MEASUREMENT_DICT.register(GetROIAnnotationType) +MEASUREMENT_DICT.register(ComponentsNumber, old_names=["Components Number"]) +MEASUREMENT_DICT.register(MaximumPixelBrightness) +MEASUREMENT_DICT.register(MinimumPixelBrightness) +MEASUREMENT_DICT.register(MeanPixelBrightness) +MEASUREMENT_DICT.register(MedianPixelBrightness) +MEASUREMENT_DICT.register(StandardDeviationOfPixelBrightness) +MEASUREMENT_DICT.register(ColocalizationMeasurement) +MEASUREMENT_DICT.register(Moment, old_names=["Moment of inertia"]) +MEASUREMENT_DICT.register(FirstPrincipalAxisLength, old_names=["Longest main axis length"]) +MEASUREMENT_DICT.register(SecondPrincipalAxisLength, old_names=["Middle main axis length"]) +MEASUREMENT_DICT.register(ThirdPrincipalAxisLength, old_names=["Shortest main axis length"]) +MEASUREMENT_DICT.register(Compactness) +MEASUREMENT_DICT.register(Sphericity) +MEASUREMENT_DICT.register(Surface) +MEASUREMENT_DICT.register(RimVolume, old_names=["Rim Volume"]) +MEASUREMENT_DICT.register(RimPixelBrightnessSum, old_names=["Rim Pixel Brightness Sum"]) +MEASUREMENT_DICT.register(ROINeighbourhoodROI) +MEASUREMENT_DICT.register(DistanceMaskROI, old_names=["segmentation distance"]) +MEASUREMENT_DICT.register(DistanceROIROI, old_names=["to ROI distance"]) +MEASUREMENT_DICT.register(SplitOnPartVolume, old_names=["split on part volume"]) +MEASUREMENT_DICT.register(SplitOnPartPixelBrightnessSum, old_names=["split on part pixel brightness sum"]) +MEASUREMENT_DICT.register(Voxels) +MEASUREMENT_DICT.register(Haralick) diff --git a/package/PartSegCore/analysis/save_functions.py b/package/PartSegCore/analysis/save_functions.py index 1ca94f040..44f219486 100644 --- a/package/PartSegCore/analysis/save_functions.py +++ b/package/PartSegCore/analysis/save_functions.py @@ -9,16 +9,15 @@ import numpy as np import tifffile -from PartSegImage import Image, ImageWriter +from PartSegImage import Channel, Image, ImageWriter from ..algorithm_describe_base import AlgorithmProperty, Register -from ..channel_class import Channel from ..io_utils import NotSupportedImage, SaveBase, SaveMaskAsTiff, SaveROIAsNumpy, SaveROIAsTIFF, get_tarinfo +from ..json_hooks import PartSegEncoder from ..project_info import HistoryElement from ..roi_info import ROIInfo from ..universal_const import UNIT_SCALE, Units from .io_utils import ProjectTuple, project_version_info -from .save_hooks import PartEncoder __all__ = [ "SaveProject", @@ -65,13 +64,13 @@ def save_project( ImageWriter.save(image, image_buff, compression=None) tar_image = get_tarinfo("image.tif", image_buff) tar.addfile(tarinfo=tar_image, fileobj=image_buff) - para_str = json.dumps(algorithm_parameters, cls=PartEncoder) + para_str = json.dumps(algorithm_parameters, cls=PartSegEncoder) parameters_buff = BytesIO(para_str.encode("utf-8")) tar_algorithm = get_tarinfo("algorithm.json", parameters_buff) tar.addfile(tar_algorithm, parameters_buff) meta_str = json.dumps( {"project_version_info": str(project_version_info), "roi_annotations": roi_info.annotations}, - cls=PartEncoder, + cls=PartSegEncoder, ) meta_buff = BytesIO(meta_str.encode("utf-8")) tar_meta = get_tarinfo("metadata.json", meta_buff) @@ -92,7 +91,7 @@ def save_project( el.arrays.seek(0) tar.addfile(hist_info, el.arrays) if el_info: - hist_str = json.dumps(el_info, cls=PartEncoder) + hist_str = json.dumps(el_info, cls=PartSegEncoder) hist_buff = BytesIO(hist_str.encode("utf-8")) tar_algorithm = get_tarinfo("history/history.json", hist_buff) tar.addfile(tar_algorithm, hist_buff) @@ -230,7 +229,7 @@ def save( seg = (segmentation == i).astype(np.uint8) if np.any(seg): base, ext = os.path.splitext(save_location) - save_loc = base + f"_comp{i}" + ext + save_loc = f"{base}_comp{i}{ext}" save_cmap(save_loc, data, spacing, seg, reverse_base, parameters) else: save_cmap(save_location, data, spacing, segmentation, reverse_base, parameters) @@ -299,7 +298,7 @@ def save( if size > 0: segmentation_mask = np.array(project_info.roi_info.roi == i)[parameters.get("time", 0)] base_path, ext = os.path.splitext(save_location) - new_save_location = base_path + f"_part{i}" + ext + new_save_location = f"{base_path}_part{i}{ext}" cls._save(new_save_location, channel_image, segmentation_mask, shift) @@ -378,7 +377,7 @@ def save( step_changed=None, ): with open(save_location, "w", encoding="utf-8") as ff: - json.dump(project_info, ff, cls=PartEncoder, indent=2) + json.dump(project_info, ff, cls=PartSegEncoder, indent=2) @classmethod def get_name(cls) -> str: diff --git a/package/PartSegCore/analysis/save_hooks.py b/package/PartSegCore/analysis/save_hooks.py deleted file mode 100644 index 3df30d552..000000000 --- a/package/PartSegCore/analysis/save_hooks.py +++ /dev/null @@ -1,43 +0,0 @@ -from ..json_hooks import ProfileEncoder, profile_hook -from .calculation_plan import CalculationPlan, CalculationTree -from .measurement_calculation import MeasurementProfile - - -class PartEncoder(ProfileEncoder): - # pylint: disable=E0202 - def default(self, o): - if isinstance(o, MeasurementProfile): - return {"__MeasurementProfile__": True, **o.to_dict()} - if isinstance(o, CalculationPlan): - return {"__CalculationPlan__": True, "tree": o.execution_tree, "name": o.name} - if isinstance(o, CalculationTree): - return {"__CalculationTree__": True, "operation": o.operation, "children": o.children} - return super().default(o) - - -def part_hook(dkt): - if "__StatisticProfile__" in dkt: - del dkt["__StatisticProfile__"] - res = MeasurementProfile(**dkt) - return res - if "__MeasurementProfile__" in dkt: - del dkt["__MeasurementProfile__"] - res = MeasurementProfile(**dkt) - return res - if "__CalculationPlan__" in dkt: - del dkt["__CalculationPlan__"] - return CalculationPlan(**dkt) - if "__CalculationTree__" in dkt: - return CalculationTree(operation=dkt["operation"], children=dkt["children"]) - if ( - "__subtype__" in dkt - and "statistic_profile" in dkt - and dkt["__subtype__"] - in ( - "PartSegCore.analysis.calculation_plan.MeasurementCalculate", - "PartSeg.utils.analysis.calculation_plan.StatisticCalculate", - ) - ): - dkt["measurement_profile"] = dkt["statistic_profile"] - del dkt["statistic_profile"] - return profile_hook(dkt) diff --git a/package/PartSegCore/channel_class.py b/package/PartSegCore/channel_class.py index e7c57019e..c5da60792 100644 --- a/package/PartSegCore/channel_class.py +++ b/package/PartSegCore/channel_class.py @@ -1,8 +1,3 @@ -class Channel(int): - """ - This class is introduced to distinguish numerical algorithm parameter from choose chanel. - In autogenerated interface field with this type limits input values to number of current image channels - """ +from PartSegImage import Channel - def __str__(self): - return str(self + 1) +__all__ = ("Channel",) diff --git a/package/PartSegCore/class_generator.py b/package/PartSegCore/class_generator.py index 144a5fc22..d387babb3 100644 --- a/package/PartSegCore/class_generator.py +++ b/package/PartSegCore/class_generator.py @@ -71,6 +71,10 @@ def asdict(self): 'Return a new OrderedDict which maps field names to their values.' return OrderedDict(zip(self._fields, _attrgetter(*self.__slots__)(self))) + def as_dict(self): + 'Return a new OrderedDict which maps field names to their values.' + return dict(zip(self._fields, _attrgetter(*self.__slots__)(self))) + def as_tuple(self): return {tuple_fields} @@ -188,7 +192,7 @@ def add_classes(types_list, translate_dict, global_state): if type_._name is None: # pylint: disable=W0212 type_str = str(type_.__origin__) else: - type_str = "typing." + str(type_._name) # pylint: disable=W0212 + type_str = f"typing.{str(type_._name)}" # pylint: disable=W0212 type_str += "[" + ", ".join(translate_dict[x] for x in sub_types) + "]" translate_dict[type_] = type_str continue diff --git a/package/PartSegCore/class_register.py b/package/PartSegCore/class_register.py new file mode 100644 index 000000000..b32525a41 --- /dev/null +++ b/package/PartSegCore/class_register.py @@ -0,0 +1,310 @@ +""" +This module contains utility for registration migration information for class. +""" +import importlib +import inspect +from dataclasses import dataclass +from functools import wraps +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union + +from packaging.version import Version +from packaging.version import parse as parse_version + + +def class_to_str(cls) -> str: + """Get full qualified name for e given class.""" + if cls.__module__.startswith("pydantic.dataclass"): + cls = cls.__mro__[1] + return class_to_str(cls) + return f"{cls.__module__}.{cls.__qualname__}" + + +def get_super_class(cls: Type) -> Optional[Type]: + """Get parent class for a given class""" + if len(cls.__mro__) < 2: + return None + if cls.__module__.startswith("pydantic.dataclass"): + return get_super_class(cls.__mro__[1]) + return cls.__mro__[1] + + +def str_to_version(version: Union[str, Version]) -> Version: + """If version passed as sting then convert it to Version object, otherwise return untouched.""" + return parse_version(version) if isinstance(version, str) else version + + +MigrationCallable = Callable[[Dict[str, Any]], Dict[str, Any]] +MigrationInfo = Tuple[Version, MigrationCallable] +"""Type describing single migration entry. For given class Version number should be unique.""" +MigrationStartInfo = Tuple[Union[str, Version], MigrationCallable] + +T = TypeVar("T") + +RegisterReturnType = Union[Type[T], Callable[[Type[T]], Type[T]]] + + +@dataclass(frozen=True) +class TypeInfo: + """ + Class for storing information in :py:class:`~.MigrationRegistration` + + :ivar str base_path: full qualified path to current class module and name + :ivar typing.Type type_: class itself + :ivar packaging.version.Version version: current clas version + :ivar typing.List[~.MigrationInfo] migrations: list of migrations for deserialize old version + :ivar bool use_parent_migrations: if migrations from parent class should be applied when deserialized object. + """ + + base_path: str + type_: Type + version: Version + migrations: List[MigrationInfo] + use_parent_migrations: bool + + +class MigrationRegistration: + """ + Implementation of class register to storage information needed for migration from previous version. + """ + + def __init__(self): + self._data_dkt: Dict[str, TypeInfo] = {} + self._parent_migrations: Dict[str, bool] = {} + + def register( + self, + cls: Type = None, + version: Union[str, Version] = "0.0.0", + migrations: List[MigrationStartInfo] = None, + old_paths: List[str] = None, + use_parent_migrations: bool = True, + ) -> RegisterReturnType: + """ + Register class instance for storage information needed for deserialization of object from older version. + + + :param cls: class to be registered + :param version: current version of class + :param typing.List[MigrationInfo] migrations: list of migrations for deserialize old version + :param old_paths: old name of class with modules + :param use_parent_migrations: if migrations from parent class should be applied when deserialized object + :return: class itself if cls parameter is provided. Otherwise, + one argument function which will consume Type to be registered. + """ + if migrations is None: + migrations = [] + else: + migrations = list(sorted((str_to_version(x), y) for x, y in migrations)) + if old_paths is None: + old_paths = [] + version = str_to_version(version) + + if migrations and max(x for x, _ in migrations) > version: + raise ValueError("class version lower than in migrations") + + def _register(cls_): + base_path = class_to_str(cls_) + type_info = TypeInfo( + base_path=base_path, + type_=cls_, + version=version, + migrations=migrations, + use_parent_migrations=use_parent_migrations, + ) + if base_path in self._data_dkt: + raise RuntimeError(f"Class name {base_path} already taken by {self._data_dkt[base_path].base_path}") + self._data_dkt[base_path] = type_info + for name in old_paths: + if name in self._data_dkt and self._data_dkt[name].base_path != base_path: + raise RuntimeError(f"Class name {name} already taken by {self._data_dkt[name].base_path}") + self._data_dkt[name] = type_info + return cls_ + + if cls is None: + return _register + return _register(cls) + + def use_parent_migrations(self, name: str) -> bool: + """ + Check if parent migrations should be used. + + :param name: full qualified path to class + :return: information if parent class migrations should be applied. + """ + self._register_missed(class_str=name) + return self._data_dkt[name].use_parent_migrations + + def get_version(self, cls: Type) -> Version: + """For a given class return version with which given class was registered using :py:meth:`register`""" + class_str = class_to_str(cls) + self._register_missed(class_str=class_str) + return self._data_dkt[class_str].version + + def get_class(self, class_str: str) -> Type: + """ + Get class base of qualified name. Could be done using current or old path. + + Qualified name is determined using :py:func:`class_to_str` and old path comes from the ``old_paths`` argument + of :py:meth:`register` method. + """ + self._register_missed(class_str=class_str) + return self._data_dkt[class_str].type_ + + def migrate_data( + self, class_str: str, class_str_to_version_dkt: Dict[str, Union[str, Version]], data: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Apply migrations base on register state. Current implementation does not support multiple inheritance. + + :param class_str: fully qualified class path + :param class_str_to_version_dkt: for each parent class information about version during serialization. + If class is absent from this dict then assumed version is "0.0.0" + :param data: dict of kwargs to constructor of class + """ + if self.use_parent_migrations(class_str): + super_klass = get_super_class(self.get_class(class_str)) + if super_klass is not None: + data = self.migrate_data(class_to_str(super_klass), class_str_to_version_dkt, data) + version = str_to_version(class_str_to_version_dkt.get(class_str, "0.0.0")) + for version_, migration in self._data_dkt[class_str].migrations: + if version < version_: + data = migration(data) + return data + + def _register_missed(self, class_str): + """Register class if missed from register""" + if class_str in self._data_dkt: + return + module_name, class_name = class_str.rsplit(".", maxsplit=1) + class_path = [class_name] + while True: + try: + module = importlib.import_module(module_name) + break + except ModuleNotFoundError: + module_name, class_name_ = module_name.rsplit(".", maxsplit=1) + class_path.append(class_name_) + if class_str in self._data_dkt: + return + class_ = module + for name in class_path[::-1]: + class_ = getattr(class_, name) + self.register(class_) + + +# THe global instance of register is use because registration is performed on import time. +# There should no information storage for objects. +REGISTER = MigrationRegistration() +"""Default register to storage class information""" + + +def rename_key(from_key: str, to_key: str, optional=False) -> MigrationCallable: + """ + simple migration function for rename fields + + :param from_key: original name + :param to_key: destination name + :param optional: if migration is required (for backward compatibility) + :return: migration function + """ + + def _migrate(dkt: Dict[str, Any]) -> Dict[str, Any]: + if optional and from_key not in dkt: + return dkt + res_dkt = dkt.copy() + res_dkt[to_key] = res_dkt.pop(from_key) + return res_dkt + + return _migrate + + +def update_argument(argument_name): + """ + This is decorator for move conversion of dict to class outside function code. + It first inspects function signature to determine type th which argument should be converted. + Then, if argument is passed as dict then all migrations from :py:attr:`REGISTER` all applied, + then object is constructed and replace base one. + + :param argument_name: name of argument which should be converted + + Example:: + + @register_class(version="0.0.1", migrations=[("0.0.1", rename_key("value", "value1"))]) + class DataClass: + def __init__(value1, value2) + self.value1 = value1 + self.value2 = value2 + + @update_argument("arg") + def some_func(arg: DataClass): + print(arg.value1) + + some_func({"value": 1, "value2": 5}) + + """ + + def _wrapper(func): + signature = inspect.signature(func) + if argument_name not in signature.parameters: # pragma: no cover + raise RuntimeError("Argument should be accessible using inspect module.") + arg_index = list(signature.parameters).index(argument_name) + klass = signature.parameters[argument_name].annotation + if not inspect.isclass(klass): # pragma: no cover + raise ValueError(f"Annotation {klass} of {argument_name} parameter is not a class") + + @wraps(func) + def _update_from_dict(*args, **kwargs): + if argument_name in kwargs and isinstance(kwargs[argument_name], dict): + kwargs = kwargs.copy() + kw = REGISTER.migrate_data(class_to_str(klass), {}, kwargs[argument_name]) + kwargs[argument_name] = klass(**kw) + elif len(args) > arg_index and isinstance(args[arg_index], dict): + args = list(args) + kw = REGISTER.migrate_data(class_to_str(klass), {}, args[arg_index]) + args[arg_index] = klass(**kw) + return func(*args, **kwargs) + + return _update_from_dict + + return _wrapper + + +def register_class( + cls: Optional[Type[T]] = None, + version: Union[str, Version] = "0.0.0", + migrations: List[MigrationStartInfo] = None, + old_paths: List[str] = None, + use_parent_migrations: bool = True, +) -> RegisterReturnType: + """ + This is wrapper for call :py:meth:`MigrationRegistration.register` of default register instance. + Please see its documentation for details. + + :param cls: class to be registered + :param version: current version of class + :param typing.List[MigrationInfo] migrations: list of migrations for deserialize old version + :param old_paths: old name of class with modules + :param use_parent_migrations: if migrations from parent class should be applied when deserialized object + :return: class itself if cls parameter is provided. Otherwise, + one argument function which will consume Type to be registered. + + Examples:: + + @register_class(version="0.0.1", migrations=[("0.0.1", rename_key("value", "value1"))]) + class DataClass: + def __init__(value1, value2) + self.value1 = value1 + self.value2 = value2 + + or:: + + + class DataClass2: + def __init__(value1, value2) + self.value1 = value1 + self.value2 = value2 + + register_class(DataClass2, version="0.0.1", migrations=[("0.0.1", rename_key("value", "value1"))]) + + """ + return REGISTER.register(cls, version, migrations, old_paths, use_parent_migrations) diff --git a/package/PartSegCore/color_image/color_image_base.py b/package/PartSegCore/color_image/color_image_base.py deleted file mode 100644 index 4439705da..000000000 --- a/package/PartSegCore/color_image/color_image_base.py +++ /dev/null @@ -1,111 +0,0 @@ -import typing -from concurrent.futures import ThreadPoolExecutor, as_completed - -import numpy as np - -from PartSegCore_compiled_backend.color_image_cython import color_grayscale, resolution - -from .base_colors import BaseColormap - -color_array_dict: typing.Dict[BaseColormap, np.ndarray] = {} -# TODO maybe replace with better cache structure - - -def create_color_map(colormap_definition: BaseColormap, power: float = 1.0) -> np.ndarray: - """ - Calculate array with approximation of colormap used by :py:func`.color_image_fun` function. - If first or last color do not have position 0 or 1 respectively then begin or end will be filled with given color - - Greyscale colormap - ``res = create_color_map([ColorPosition(0, Color(0, 0, 0)), ColorPosition(1, Color(255, 255, 255))])`` - - Black in first 25% of colormap then Greyscale from 25% to 75% and then white from 75% of colormap - ``res = create_color_map([ColorPosition(0.25, Color(0, 0, 0)), ColorPosition(0.75, Color(255, 255, 255))])`` - - :param colormap_definition: list defining base colormap - :param power: power normalization of colormap. Will be ignored if points_and_colors.points be non empty - :return: array of size (1024, 3) - """ - # TODO Rethink - colormap = np.zeros((1024, 3), dtype=np.uint8) - bounds = colormap_definition.bounds() - values = colormap_definition.color_values() - if len(bounds) == 0: - return colormap - if len(bounds) == 1: - colormap[:] = values[0] - return colormap - _bounds = [x**power * ((resolution - 1) / resolution) for x in bounds] - bounds = [_bounds[0]] - _values = values - values = [_values[0]] - if len(bounds) < 10: - for i, (x, y) in enumerate(zip(_bounds, _bounds[1:]), start=1): - dist = (y - x) * resolution / 256 - if dist > 1: - bounds.append(y - dist / resolution) - values.append(_values[i]) - bounds.append(y) - values.append(_values[i]) - values = np.array(values) - points = list(colormap_definition.get_points()) - if not points: - points = np.linspace(0, 1, resolution, endpoint=False) - for i in range(3): - colormap[:, i] = np.interp(points, bounds, values[:, i]) - return colormap - - -def color_bar_fun(bar: np.ndarray, colormap: typing.Union[BaseColormap, np.ndarray]): - if isinstance(colormap, BaseColormap): - if colormap not in color_array_dict: - color_array_dict[colormap] = create_color_map(colormap) - colormap = color_array_dict[colormap] - if not isinstance(colormap, np.ndarray) or colormap.shape != ( - resolution, - 3, - ): - raise ValueError(f"colormap should be passed as numpy array with shape ({resolution},3)") - min_val = np.min(bar) - cords = ((bar - min_val) * ((colormap.shape[0] - 1) / (np.max(bar) - min_val))).astype(np.uint16) - return colormap[cords] - - -def color_image_fun( - image: np.ndarray, - colors: typing.List[typing.Union[BaseColormap, np.ndarray]], - min_max: typing.List[typing.Tuple[float, float]], -) -> np.ndarray: - """ - Color given image layer. - :param image: Single image layer (array of size (width, height, channels) - :param colors: list of colormaps by name (from ``.color_maps`` dict) or array (its size ned to be size (1024, 3)) - array can be created by :py:func:`.create_color_map` - :param min_max: bounds for each channel separately - :return: colored image (array of size (width, height, 3) as RGB image - """ - new_shape = image.shape[:-1] + (3,) - - result_images = [] # = np.zeros(new_shape, dtype=np.uint8) - colored_channels = {} - with ThreadPoolExecutor(max_workers=8) as executor: - for i, colormap in enumerate(colors): - if colormap is None: - continue - if isinstance(colormap, BaseColormap): - if colormap not in color_array_dict: - color_array_dict[colormap] = create_color_map(colormap) - colormap = color_array_dict[colormap] - if not isinstance(colormap, np.ndarray) or not colormap.shape == (resolution, 3): - raise ValueError(f"colormap should be passed as numpy array with shape ({resolution},3)") - min_val, max_val = min_max[i] - - colored_channels[executor.submit(color_grayscale, colormap, image[..., i], min_val, max_val)] = i - for res in as_completed(colored_channels): - result_images.append(res.result()) - if len(result_images) > 0: - if len(result_images) == 1: - return result_images[0] - # TODO use ColorMap additional information - return np.max(result_images, axis=0) - return np.zeros(new_shape, dtype=np.uint8) diff --git a/package/PartSegCore/image_operations.py b/package/PartSegCore/image_operations.py index b8d4c7800..ac90be488 100644 --- a/package/PartSegCore/image_operations.py +++ b/package/PartSegCore/image_operations.py @@ -4,9 +4,10 @@ import numpy as np import SimpleITK as sitk -from .class_generator import enum_register +from .class_register import register_class +@register_class class RadiusType(Enum): """ If operation should be performed and if on each layer separately on globally @@ -20,6 +21,7 @@ def __str__(self): return ["No", "2d", "3d"][self.value] +@register_class class NoiseFilterType(Enum): No = 0 Gauss = 1 @@ -30,10 +32,6 @@ def __str__(self): return self.name -enum_register.register_class(RadiusType) -enum_register.register_class(NoiseFilterType) - - def _generic_image_operation(image, radius, fun, layer): if image.ndim == 3 and image.shape[0] == 1: layer = True diff --git a/package/PartSegCore/io_utils.py b/package/PartSegCore/io_utils.py index 19c130266..11048cc93 100644 --- a/package/PartSegCore/io_utils.py +++ b/package/PartSegCore/io_utils.py @@ -13,13 +13,12 @@ import numpy as np import pandas as pd import tifffile -from napari.utils import Colormap -from PartSegCore.json_hooks import ProfileDict, profile_hook +from PartSegCore.json_hooks import partseg_object_hook from PartSegImage import ImageWriter from PartSegImage.image import minimal_dtype -from .algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty, ROIExtractionProfile +from .algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty from .project_info import ProjectInfoBase @@ -190,88 +189,22 @@ def partial(cls): return False -class UpdateLoadedMetadataBase: - json_hook = staticmethod(profile_hook) - - @classmethod - def load_json_data(cls, data: typing.Union[str, Path, typing.TextIO]): +def load_metadata_base(data: typing.Union[str, Path]): + try: + if isinstance(data, typing.TextIO): + decoded_data = json.load(data, object_hook=partseg_object_hook) + elif os.path.exists(data): + with open(data, encoding="utf-8") as ff: + decoded_data = json.load(ff, object_hook=partseg_object_hook) + else: + decoded_data = json.loads(data, object_hook=partseg_object_hook) + except ValueError as e: try: - if isinstance(data, typing.TextIO): - decoded_data = json.load(data, object_hook=cls.json_hook) - elif os.path.exists(data): - with open(data, encoding="utf-8") as ff: - decoded_data = json.load(ff, object_hook=cls.json_hook) - else: - decoded_data = json.loads(data, object_hook=cls.json_hook) - except ValueError as e: - try: - decoded_data = json.loads(data, object_hook=cls.json_hook) - except Exception: - raise e - - return cls.recursive_update(decoded_data) - - @classmethod - def recursive_update(cls, data): - if isinstance(data, (tuple, list)): - return type(data)([cls.recursive_update(x) for x in data]) - if isinstance(data, ROIExtractionProfile): - return cls.update_segmentation_profile(data) - if isinstance(data, Enum): - return cls.update_enum(data) - if isinstance(data, typing.MutableMapping): - for key in data.keys(): - data[key] = cls.recursive_update(data[key]) - if key == "custom_colormap": - cls.update_colormaps(data[key]) - if isinstance(data, ProfileDict): - data.my_dict = cls.recursive_update(data.my_dict) - return data - - @staticmethod - def update_colormaps(dkt: dict): - for key, val in dkt.items(): - if isinstance(val, Colormap): - val.name = key - - @classmethod - def update_enum(cls, enum_data: Enum): - return enum_data - - # noinspection PyUnusedLocal - @classmethod - def update_segmentation_sub_dict(cls, name: str, dkt: typing.MutableMapping) -> typing.MutableMapping: - if "values" not in dkt: - return dkt - if name == "sprawl_type" and dkt["name"].endswith(" sprawl"): - dkt["name"] = dkt["name"][: -len(" sprawl")] - for key in dkt["values"].keys(): - item = dkt["values"][key] - if isinstance(item, Enum): - dkt["values"][key] = cls.update_enum(item) - elif isinstance(item, typing.MutableMapping): - dkt["values"][key] = cls.update_segmentation_sub_dict(key, item) - return dkt - - @classmethod - def update_segmentation_profile(cls, profile_data: ROIExtractionProfile) -> ROIExtractionProfile: - for key in list(profile_data.values.keys()): - item = profile_data.values[key] - if isinstance(item, Enum): - profile_data.values[key] = cls.update_enum(item) - elif isinstance(item, typing.MutableMapping): - if key == "noise_removal": - del profile_data.values[key] - key = "noise_filtering" - if "values" in item and "gauss_type" in item["values"]: - item["values"]["dimension_type"] = item["values"]["gauss_type"] - del item["values"]["gauss_type"] - profile_data.values[key] = cls.update_segmentation_sub_dict(key, item) - return profile_data + decoded_data = json.loads(str(data), object_hook=partseg_object_hook) + except Exception: + raise e - -def load_metadata_base(data: typing.Union[str, Path]): - return UpdateLoadedMetadataBase.load_json_data(data) + return decoded_data def proxy_callback( diff --git a/package/PartSegCore/json_hooks.py b/package/PartSegCore/json_hooks.py index fc8757e3e..db1f1c92c 100644 --- a/package/PartSegCore/json_hooks.py +++ b/package/PartSegCore/json_hooks.py @@ -1,392 +1,102 @@ -import copy -import importlib -import itertools -import typing -from collections import defaultdict -from collections.abc import MutableMapping -from contextlib import suppress +import dataclasses +import enum +import json +from pathlib import Path import numpy as np -from napari.utils import Colormap -from psygnal import Signal - -from PartSegCore.algorithm_describe_base import ROIExtractionProfile - -from .class_generator import SerializeClassEncoder, serialize_hook -from .image_operations import RadiusType -from .utils import CallbackBase, get_callback - - -class EventedDict(MutableMapping): - setted = Signal(str) - deleted = Signal(str) - - def __init__(self, **kwargs): - # TODO add positional only argument when drop python 3.7 - super().__init__() - self._dict = {} - for key, dkt in kwargs.items(): - if isinstance(dkt, dict): - dkt = EventedDict(**dkt) - if isinstance(dkt, EventedDict): - dkt.base_key = key - dkt.setted.connect(self._propagate_setitem) - dkt.deleted.connect(self._propagate_del) - self._dict[key] = dkt - self.base_key = "" - - def __setitem__(self, k, v) -> None: - if isinstance(v, dict): - v = EventedDict(**v) - if isinstance(v, EventedDict): - v.base_key = k - v.setted.connect(self._propagate_setitem) - v.deleted.connect(self._propagate_del) - if k in self._dict and isinstance(self._dict[k], EventedDict): - self._dict[k].setted.disconnect(self._propagate_setitem) - self._dict[k].deleted.disconnect(self._propagate_del) - old_value = self._dict[k] if k in self._dict else None - with suppress(ValueError): - if old_value == v: - return - self._dict[k] = v - self.setted.emit(k) - - def __delitem__(self, k) -> None: - if k in self._dict and isinstance(self._dict[k], EventedDict): - self._dict[k].setted.disconnect(self._propagate_setitem) - self._dict[k].deleted.disconnect(self._propagate_del) - del self._dict[k] - self.deleted.emit(k) - - def __getitem__(self, k): - return self._dict[k] - - def __len__(self) -> int: - return len(self._dict) - - def __iter__(self) -> typing.Iterator: - return iter(self._dict) - - def as_dict(self): - return copy.copy(self._dict) - - def as_dict_deep(self): - return {k: v.as_dict_deep() if isinstance(v, EventedDict) else v for k, v in self._dict.items()} - - def __str__(self): - return f"EventedDict({self._dict})" - - def __repr__(self): - return f"EventedDict({repr(self._dict)})" - - def _propagate_setitem(self, key): - # Fixme when partial disconnect will work - sender: EventedDict = Signal.sender() - if sender.base_key: - self.setted.emit(f"{sender.base_key}.{key}") - else: - self.setted.emit(key) - - def _propagate_del(self, key): - # Fixme when partial disconnect will work - sender: EventedDict = Signal.sender() - if sender.base_key: - self.deleted.emit(f"{sender.base_key}.{key}") - else: - self.deleted.emit(key) - - -def recursive_update_dict(main_dict: typing.Union[dict, EventedDict], other_dict: typing.Union[dict, EventedDict]): - """ - recursive update main_dict with elements of other_dict - - :param main_dict: dict to be updated recursively - :param other_dict: source dict - - >>> dkt1 = {"test": {"test": 1, "test2": {"test4": 1}}} - >>> dkt2 = {"test": {"test": 4, "test2": {"test2": 1}}} - >>> recursive_update_dict(dkt1, dkt2) - >>> dkt1 - {"test": {"test": 1, "test2": {"test4": 1, "test2": 1}}} - """ - for key, val in other_dict.items(): - if key in main_dict and isinstance(main_dict[key], dict) and isinstance(val, dict): - recursive_update_dict(main_dict[key], val) - else: - main_dict[key] = val - - -class ProfileDict: - """ - Dict for storing recursive data. The path are dot separated. - - >>> dkt = ProfileDict() - >>> dkt.set(["aa", "bb", "c1"], 7) - >>> dkt.get(["aa", "bb", "c1"]) - 7 - >>> dkt.get("aa.bb.c2", 8) - 8 - >>> dkt.get("aa.bb") - {'c1': 7, 'c2': 8} - """ - - def __init__(self, **kwargs): - self._my_dict = EventedDict(**kwargs) - self._callback_dict: typing.Dict[str, typing.List[CallbackBase]] = defaultdict(list) - - self._my_dict.setted.connect(self._call_callback) - self._my_dict.deleted.connect(self._call_callback) - - def as_dict(self): - return self.my_dict.as_dict() - - @property - def my_dict(self) -> EventedDict: - return self._my_dict - - @my_dict.setter - def my_dict(self, value: typing.Union[dict, EventedDict]): - if isinstance(value, dict): - value = EventedDict(**value) - - self._my_dict.setted.disconnect(self._call_callback) - self._my_dict.deleted.disconnect(self._call_callback) - self._my_dict = value - self._my_dict.setted.connect(self._call_callback) - self._my_dict.deleted.connect(self._call_callback) - - def update(self, ob: typing.Union["ProfileDict", dict, None] = None, **kwargs): - """ - Update dict recursively. Use :py:func:`~.recursive_update_dict` - - :param ob: data source - :param kwargs: data source, as keywords - """ - if isinstance(ob, ProfileDict): - recursive_update_dict(self.my_dict, ob.my_dict) - recursive_update_dict(self.my_dict, kwargs) - elif isinstance(ob, dict): - recursive_update_dict(self.my_dict, ob) - recursive_update_dict(self.my_dict, kwargs) - elif ob is None: - recursive_update_dict(self.my_dict, kwargs) - - def profile_change(self): - for callback in itertools.chain(*self._callback_dict.values()): - callback() - - def connect( - self, key_path: typing.Union[typing.Sequence[str], str], callback: typing.Callable[[], typing.Any], maxargs=None - ) -> typing.Callable: - """ - Connect function to receive information when object on path was changed using :py:meth:`.set` - - :param key_path: path for which signal should be emitted - :param callback: parameterless function which should be called - - :return: callback function itself. - """ - if not isinstance(key_path, str): - key_path = ".".join(key_path) - - self._callback_dict[key_path].append(get_callback(callback, maxargs)) - return callback - - def set(self, key_path: typing.Union[typing.Sequence[str], str], value): - """ - Set value from dict - - :param key_path: Path to element. If is string then will be split on '.' (`key_path.split('.')`) - :param value: Value to set. - """ - if isinstance(key_path, str): - key_path = key_path.split(".") - curr_dict = self.my_dict - - for i, key in enumerate(key_path[:-1]): - try: - # TODO add check if next step element is dict and create custom information - curr_dict = curr_dict[key] - except KeyError: - for key2 in key_path[i:-1]: - with curr_dict.setted.blocked(): - curr_dict[key2] = EventedDict() - curr_dict = curr_dict[key2] - break - if isinstance(value, dict): - value = EventedDict(**value) - curr_dict[key_path[-1]] = value - return value - - def _call_callback(self, key_path: typing.Union[typing.Sequence[str], str]): - if isinstance(key_path, str): - key_path = key_path.split(".") - full_path = ".".join(key_path[1:]) - callback_path = "" - callback_list = [] - if callback_path in self._callback_dict: - callback_list = self._callback_dict[callback_path] - - for callback_path in itertools.accumulate(key_path[1:], lambda x, y: f"{x}.{y}"): - if callback_path in self._callback_dict: - li = self._callback_dict[callback_path] - li = [x for x in li if x.is_alive()] - self._callback_dict[callback_path] = li - callback_list.extend(li) - for callback in callback_list: - callback(full_path) - - def get(self, key_path: typing.Union[list, str], default=None): - """ - Get value from dict. - - :param key_path: Path to element. If is string then will be split on . (`key_path.split('.')`). - :param default: default value if element missed in dict. - :raise KeyError: on missed element if default is not provided. - :return: requested value - """ - if isinstance(key_path, str): - key_path = key_path.split(".") - curr_dict = self.my_dict - for key in key_path: +import pydantic + +from PartSegImage import Channel + +from ._old_json_hooks import part_hook +from .class_register import REGISTER, class_to_str + + +def add_class_info(obj, dkt): + dkt["__class__"] = class_to_str(obj.__class__) + dkt["__class_version_dkt__"] = { + class_to_str(sup_obj): str(REGISTER.get_version(sup_obj)) + for sup_obj in obj.__class__.__mro__ + if class_to_str(sup_obj) + not in { + "object", + "pydantic.main.BaseModel", + "pydantic.utils.Representation", + "enum.Enum", + "builtins.object", + "PartSegCore.utils.BaseModel", + "typing.Generic", + } + and not class_to_str(sup_obj).startswith("collections.abc") + } + return dkt + + +class PartSegEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, enum.Enum): + dkt = {"value": o.value} + return add_class_info(o, dkt) + if dataclasses.is_dataclass(o): + fields = dataclasses.fields(o) + dkt = {x.name: getattr(o, x.name) for x in fields} + return add_class_info(o, dkt) + + if isinstance(o, np.ndarray): + return o.tolist() + + if isinstance(o, pydantic.BaseModel): try: - curr_dict = curr_dict[key] - except KeyError as e: - if default is None: - raise e - - val = copy.deepcopy(default) - return self.set(key_path, val) - - return curr_dict - - def verify_data(self) -> bool: - """ - Call :py:func:`~.check_loaded_dict` on inner structures - """ - return check_loaded_dict(self.my_dict) + dkt = dict(o) + except (ValueError, TypeError): + dkt = o.dict() + return add_class_info(o, dkt) - def filter_data(self): - error_list = [] - for group, up_dkt in list(self.my_dict.items()): - if not isinstance(up_dkt, (dict, EventedDict)): - continue - for key, dkt in list(up_dkt.items()): - if not check_loaded_dict(dkt): - error_list.append(f"{group}.{key}") - del up_dkt[key] - return error_list - - -class ProfileEncoder(SerializeClassEncoder): - """ - Json encoder for :py:class:`ProfileDict`, :py:class:`RadiusType`, - :py:class:`.SegmentationProfile` classes - - >>> import json - >>> data = ProfileDict() - >>> data.set("aa.bb.cc", 7) - >>> with open("some_file", 'w') as fp: - >>> json.dump(data, fp, cls=ProfileEncoder) - """ - - # pylint: disable=E0202 - def default(self, o): - """encoder implementation""" - if isinstance(o, RadiusType): - return {"__RadiusType__": True, "value": o.value} - if isinstance(o, ROIExtractionProfile): - return {"__SegmentationProfile__": True, "name": o.name, "algorithm": o.algorithm, "values": o.values} - if isinstance(o, Colormap): - return { - "__Colormap__": True, - "name": o.name, - "colors": o.colors.tolist(), - "interpolation": o.interpolation, - "controls": o.controls.tolist(), - } if hasattr(o, "as_dict"): dkt = o.as_dict() - dkt["__class__"] = o.__module__ + "." + o.__class__.__name__ - return dkt + return add_class_info(o, dkt) + if isinstance(o, np.integer): return int(o) if isinstance(o, np.floating): return float(o) + if isinstance(o, Path): + return str(o) + if isinstance(o, Channel): + return o.value + if isinstance(o, dict) and "__error__" in o: + del o["__error__"] # different environments without same plugins installed return super().default(o) -def profile_hook(dkt): - """ - hook for json loading - - >>> import json - >>> with open("some_file", 'r') as fp: - ... data = json.load(fp, object_hook=profile_hook) - - """ +def partseg_object_hook(dkt: dict): + if "__error__" in dkt: + dkt.pop("__error__") # different environments without same plugins installed if "__class__" in dkt: - module_name, class_name = dkt["__class__"].rsplit(".", maxsplit=1) # the migration code should be called here + cls_str = dkt.pop("__class__") + version_dkt = dkt.pop("__class_version_dkt__") if "__class_version_dkt__" in dkt else {cls_str: "0.0.0"} try: - del dkt["__class__"] - module = importlib.import_module(module_name) - return getattr(module, class_name)(**dkt) - except Exception as e: # skipcq: PTC-W0703` # pylint: disable=W0703 # pragma: no cover - dkt["__class__"] = module_name + "." + class_name - dkt["__error__"] = e - if "__ProfileDict__" in dkt: - del dkt["__ProfileDict__"] - res = ProfileDict(**dkt) - return res - if "__RadiusType__" in dkt: - return RadiusType(dkt["value"]) - if "__SegmentationProperty__" in dkt: - del dkt["__SegmentationProperty__"] - res = ROIExtractionProfile(**dkt) - return res - if "__SegmentationProfile__" in dkt: - del dkt["__SegmentationProfile__"] - res = ROIExtractionProfile(**dkt) - return res - if ( - "__Serializable__" in dkt and dkt["__subtype__"] == "HistoryElement" and "algorithm_name" in dkt - ): # pragma: no cover - # old code fix - name = dkt["algorithm_name"] - par = dkt["algorithm_values"] - del dkt["algorithm_name"] - del dkt["algorithm_values"] - dkt["segmentation_parameters"] = {"algorithm_name": name, "values": par} - if "__Serializable__" in dkt and dkt["__subtype__"] == "PartSegCore.color_image.base_colors.ColorMap": - positions, colors = list(zip(*dkt["colormap"])) - return Colormap(colors, controls=positions) - if "__Serializable__" in dkt and dkt["__subtype__"] == "PartSegCore.color_image.base_colors.ColorPosition": - return (dkt["color_position"], dkt["color"]) - if "__Serializable__" in dkt and dkt["__subtype__"] == "PartSegCore.color_image.base_colors.Color": - return (dkt["red"] / 255, dkt["green"] / 255, dkt["blue"] / 255) - if "__Colormap__" in dkt: - del dkt["__Colormap__"] - if dkt["controls"][0] != 0: - dkt["controls"].insert(0, 0) - dkt["colors"].insert(0, dkt["colors"][0]) - if dkt["controls"][-1] != 1: - dkt["controls"].append(1) - dkt["colors"].append(dkt["colors"][-1]) - return Colormap(**dkt) - - return serialize_hook(dkt) - - -def check_loaded_dict(dkt) -> bool: - """ - Recursive check if dict `dkt` or any sub dict contains '__error__' key. - - :param dkt: dict to check - """ - if not isinstance(dkt, (dict, EventedDict)): - return True - if "__error__" in dkt: - return False - return all(check_loaded_dict(val) for val in dkt.values()) + dkt_migrated = REGISTER.migrate_data(cls_str, version_dkt, dkt) + cls = REGISTER.get_class(cls_str) + return cls(**dkt_migrated) + except Exception as e: # pylint: disable=W0703 + dkt["__class__"] = cls_str + dkt["__class_version_dkt__"] = version_dkt + dkt["__error__"] = str(e) + + if "__ReadOnly__" in dkt or "__Serializable__" in dkt or "__Enum__" in dkt: + is_enum = "__Enum__" in dkt + for el in ("__Enum__", "__Serializable__", "__ReadOnly__"): + dkt.pop(el, None) + cls_str = dkt["__subtype__"] + del dkt["__subtype__"] + try: + dkt_migrated = REGISTER.migrate_data(cls_str, {}, dkt) + cls = REGISTER.get_class(cls_str) + return cls(**dkt_migrated) + except Exception: # pylint: disable=W0703 + dkt["__subtype__"] = cls_str + dkt["__Enum__" if is_enum else "__Serializable__"] = True + + return part_hook(dkt) diff --git a/package/PartSegCore/mask/algorithm_description.py b/package/PartSegCore/mask/algorithm_description.py index 877a6df71..dc4c3a7b6 100644 --- a/package/PartSegCore/mask/algorithm_description.py +++ b/package/PartSegCore/mask/algorithm_description.py @@ -1,6 +1,38 @@ -from ..algorithm_describe_base import Register -from ..segmentation.segmentation_algorithm import final_algorithm_list +import warnings -mask_algorithm_dict = Register() -for el in final_algorithm_list: - mask_algorithm_dict.register(el) +from ..algorithm_describe_base import AlgorithmSelection +from ..segmentation.segmentation_algorithm import ( + AutoThresholdAlgorithm, + CellFromNucleusFlow, + MorphologicalWatershed, + ThresholdAlgorithm, + ThresholdFlowAlgorithm, + ThresholdPreview, +) + + +class MaskAlgorithmSelection( + AlgorithmSelection, + class_methods=["support_time", "support_z"], + methods=["set_image", "set_mask", "get_info_text", "calculation_run"], +): + """Register for segmentation method visible in PartSeg ROI Mask.""" + + +MaskAlgorithmSelection.register(AutoThresholdAlgorithm) +MaskAlgorithmSelection.register(CellFromNucleusFlow) +MaskAlgorithmSelection.register(MorphologicalWatershed) +MaskAlgorithmSelection.register(ThresholdAlgorithm) +MaskAlgorithmSelection.register(ThresholdFlowAlgorithm) +MaskAlgorithmSelection.register(ThresholdPreview) + + +def __getattr__(name): # pragma: no cover + if name == "mask_algorithm_dict": + warnings.warn( + "mask_algorithm_dict is deprecated. Please use MaskAlgorithmSelection instead", + category=FutureWarning, + stacklevel=2, + ) + return MaskAlgorithmSelection.__register__ + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/package/PartSegCore/mask/io_functions.py b/package/PartSegCore/mask/io_functions.py index 77883f5c1..13db4e825 100644 --- a/package/PartSegCore/mask/io_functions.py +++ b/package/PartSegCore/mask/io_functions.py @@ -27,15 +27,15 @@ SaveROIAsNumpy, SaveROIAsTIFF, SegmentationType, - UpdateLoadedMetadataBase, WrongFileTypeException, check_segmentation_type, get_tarinfo, + load_metadata_base, open_tar_file, proxy_callback, tar_to_buff, ) -from ..json_hooks import ProfileEncoder +from ..json_hooks import PartSegEncoder from ..project_info import AdditionalLayerDescription, HistoryElement, ProjectInfoBase from ..roi_info import ROIInfo @@ -144,7 +144,7 @@ def save_stack_segmentation( metadata["base_file"] = os.path.relpath(file_path, os.path.dirname(file_data)) else: metadata["base_file"] = file_path - metadata_buff = BytesIO(json.dumps(metadata, cls=ProfileEncoder).encode("utf-8")) + metadata_buff = BytesIO(json.dumps(metadata, cls=PartSegEncoder).encode("utf-8")) metadata_tar = get_tarinfo("metadata.json", metadata_buff) tar_file.addfile(metadata_tar, metadata_buff) step_changed(4) @@ -177,7 +177,7 @@ def save_stack_segmentation( hist.arrays.seek(0) tar_file.addfile(hist_info, hist.arrays) if el_info: - hist_str = json.dumps(el_info, cls=ProfileEncoder) + hist_str = json.dumps(el_info, cls=PartSegEncoder) hist_buff = BytesIO(hist_str.encode("utf-8")) tar_algorithm = get_tarinfo("history/history.json", hist_buff) tar_file.addfile(tar_algorithm, hist_buff) @@ -284,21 +284,6 @@ def get_name(cls): def get_short_name(cls): return "seg" - @staticmethod - def fix_parameters(profile: ROIExtractionProfile): - if profile is None: - return - if (profile.algorithm in {"Threshold", "Auto Threshold"}) and isinstance(profile.values["smooth_border"], bool): - if profile.values["smooth_border"] and "smooth_border_radius" in profile.values: - profile.values["smooth_border"] = { - "name": "Opening", - "values": {"smooth_border_radius": profile.values["smooth_border_radius"]}, - } - del profile.values["smooth_border_radius"] - else: - profile.values["smooth_border"] = {"name": "None", "values": {}} - return profile - @classmethod def load( cls, @@ -315,7 +300,7 @@ def load( else: parameters = defaultdict( lambda: None, - [(int(k), cls.fix_parameters(v)) for k, v in segmentation_tuple.roi_extraction_parameters.items()], + [(int(k), v) for k, v in segmentation_tuple.roi_extraction_parameters.items()], ) return dataclasses.replace(segmentation_tuple, roi_extraction_parameters=parameters) @@ -356,7 +341,7 @@ def load( else: parameters = defaultdict( lambda: None, - [(int(k), LoadROI.fix_parameters(v)) for k, v in project_metadata["parameters"].items()], + [(int(k), v) for k, v in project_metadata["parameters"].items()], ) return MaskProjectTuple(file_path=file_data, image=None, roi_extraction_parameters=parameters) @@ -365,7 +350,7 @@ def load( project_metadata = load_metadata(tar_file.extractfile("metadata.json").read().decode("utf8")) parameters = defaultdict( lambda: None, - [(int(k), LoadROI.fix_parameters(v)) for k, v in project_metadata["parameters"].items()], + [(int(k), v) for k, v in project_metadata["parameters"].items()], ) finally: if isinstance(file_data, (str, Path)): @@ -464,7 +449,7 @@ def get_short_name(cls): @classmethod def get_next_file(cls, file_paths: typing.List[str]): base, ext = os.path.splitext(file_paths[0]) - return base + "_mask" + ext + return f"{base}_mask{ext}" @classmethod def number_of_files(cls): @@ -671,7 +656,7 @@ def save( :return: """ with open(save_location, "w", encoding="utf-8") as ff: - json.dump({"parameters": project_info.roi_extraction_parameters}, ff, cls=ProfileEncoder) + json.dump({"parameters": project_info.roi_extraction_parameters}, ff, cls=PartSegEncoder) @classmethod def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: @@ -719,28 +704,7 @@ def load_metadata(data: typing.Union[str, Path, typing.TextIO]): :param data: path to json file, string with json, or opened file :return: restored structures """ - return UpdateLoadedMetadataMask.load_json_data(data) - - -class UpdateLoadedMetadataMask(UpdateLoadedMetadataBase): - @classmethod - def update_segmentation_profile(cls, profile_data: ROIExtractionProfile) -> ROIExtractionProfile: - profile_data = super().update_segmentation_profile(profile_data) - if profile_data.algorithm in {"Threshold", "Auto Threshold"}: - if isinstance(profile_data.values["smooth_border"], bool): - if profile_data.values["smooth_border"]: - profile_data.values["smooth_border"] = { - "name": "Opening", - "values": {"smooth_border_radius": profile_data.values["smooth_border_radius"]}, - } - else: - profile_data.values["smooth_border"] = {"name": "None", "values": {}} - if "smooth_border_radius" in profile_data.values: - del profile_data.values["smooth_border_radius"] - if "noise_removal" in profile_data.values: - profile_data.values["noise_filtering"] = profile_data.values["noise_removal"] - del profile_data.values["noise_removal"] - return profile_data + return load_metadata_base(data) load_dict = Register( diff --git a/package/PartSegCore/mask_create.py b/package/PartSegCore/mask_create.py index a1b3ed1d5..28962ccaf 100644 --- a/package/PartSegCore/mask_create.py +++ b/package/PartSegCore/mask_create.py @@ -3,13 +3,15 @@ import numpy as np import SimpleITK as sitk +from PartSegCore.utils import BaseModel from PartSegImage.image import minimal_dtype -from .class_generator import BaseSerializableClass +from .class_register import register_class from .image_operations import RadiusType, dilate, erode -class MaskProperty(BaseSerializableClass): +@register_class(old_paths=["PartSeg.utils.mask_create.MaskProperty"]) +class MaskProperty(BaseModel): """ Description of creation mask from segmentation @@ -69,7 +71,14 @@ def simple_mask(cls) -> "MaskProperty": :rtype: MaskProperty """ - return cls(RadiusType.NO, 0, RadiusType.NO, 0, False, False) + return cls( + dilate=RadiusType.NO, + dilate_radius=0, + fill_holes=RadiusType.NO, + max_holes_size=0, + save_components=False, + clip_to_mask=False, + ) def mp_eq(self: MaskProperty, other: MaskProperty): @@ -192,7 +201,7 @@ def _fill_holes(mask_description: MaskProperty, mask: np.ndarray) -> np.ndarray: if mask_description.save_components: border = 1 res_slice = tuple(slice(border, -border) for _ in range(mask.ndim)) - mask_description_copy = mask_description.replace_(save_components=False) + mask_description_copy = mask_description.copy(update={"save_components": False}) mask_prohibited = mask > 0 for component, slice_arr, cmp_num in _cut_components(mask, mask, border): mask_prohibited_component = mask_prohibited[slice_arr] diff --git a/package/PartSegCore/mask_partition_utils.py b/package/PartSegCore/mask_partition_utils.py index eb7c4bdfc..b66336e8a 100644 --- a/package/PartSegCore/mask_partition_utils.py +++ b/package/PartSegCore/mask_partition_utils.py @@ -12,13 +12,20 @@ import numpy as np import SimpleITK +from pydantic import Field from scipy.ndimage import distance_transform_edt -from PartSegCore.algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty +from PartSegCore.algorithm_describe_base import AlgorithmDescribeBase +from PartSegCore.utils import BaseModel from .universal_const import UNIT_SCALE, Units +class BorderRimParameters(BaseModel): + distance: float = Field(500, ge=0, le=10**6) + units: Units = Units.nm + + class BorderRim(AlgorithmDescribeBase): """ This class implement border rim (Annulus like) calculation. @@ -29,15 +36,10 @@ class BorderRim(AlgorithmDescribeBase): The algorithm is: 1. For each image voxel calculate distance from background (0 labeled voxels in mask) with respect of voxel size - 2. Select this voxels which are closer than gvien distance. + 2. Select these voxels which are closer than given distance. """ - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("distance", "Distance", 500.0, options_range=(0, 10000), value_type=float), - AlgorithmProperty("units", "Units", Units.nm, value_type=Units), - ] + __argument_class__ = BorderRimParameters @classmethod def get_name(cls) -> str: @@ -69,6 +71,15 @@ def border_mask(mask: np.ndarray, distance: float, units: Units, voxel_size, **_ return mask +class MaskDistanceSplitParameters(BaseModel): + num_of_parts: int = Field(2, title="Number of Parts", ge=1, le=1024) + equal_volume: bool = Field( + False, + title="Equal Volume", + description="If split should be done in respect of parts volume of parts thickness.", + ) + + class MaskDistanceSplit(AlgorithmDescribeBase): """ This class contains implementation of splitting mask on parts based on distance from borders. @@ -96,22 +107,12 @@ class MaskDistanceSplit(AlgorithmDescribeBase): """ + __argument_class__ = MaskDistanceSplitParameters + @classmethod def get_name(cls) -> str: return "Mask Distance Split" - @classmethod - def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: - return [ - AlgorithmProperty("num_of_parts", "Number of Parts", 2, (1, 1024)), - AlgorithmProperty( - "equal_volume", - "Equal Volume", - False, - help_text="If split should be done in respect of parts volume of parts thickness.", - ), - ] - @staticmethod def split(mask: np.ndarray, num_of_parts: int, equal_volume: bool, voxel_size, **_): """ diff --git a/package/PartSegCore/project_info.py b/package/PartSegCore/project_info.py index dceb9e11e..9c715ac3e 100644 --- a/package/PartSegCore/project_info.py +++ b/package/PartSegCore/project_info.py @@ -5,10 +5,9 @@ import numpy as np -from PartSegCore.class_generator import BaseSerializableClass from PartSegCore.mask_create import MaskProperty, calculate_mask from PartSegCore.roi_info import ROIInfo -from PartSegCore.utils import numpy_repr +from PartSegCore.utils import BaseModel, numpy_repr from PartSegImage import Image if sys.version_info.minor < 8: @@ -38,12 +37,15 @@ def __repr__(self): ) -class HistoryElement(BaseSerializableClass): +class HistoryElement(BaseModel): roi_extraction_parameters: Dict[str, Any] annotations: Optional[Dict[int, Any]] mask_property: MaskProperty arrays: BytesIO + class Config: + arbitrary_types_allowed = True + @classmethod def create( cls, diff --git a/package/PartSegCore/register.py b/package/PartSegCore/register.py index 7fa95ada8..292c7790f 100644 --- a/package/PartSegCore/register.py +++ b/package/PartSegCore/register.py @@ -61,12 +61,12 @@ class RegisterEnum(Enum): # noinspection DuplicatedCode register_dict = { - RegisterEnum.flow: watershed.flow_dict, - RegisterEnum.sprawl: watershed.flow_dict, - RegisterEnum.threshold: threshold.threshold_dict, - RegisterEnum.noise_filtering: noise_filtering.noise_filtering_dict, - RegisterEnum.analysis_algorithm: analysis_algorithm_description.analysis_algorithm_dict, - RegisterEnum.mask_algorithm: mask_algorithm_description.mask_algorithm_dict, + RegisterEnum.flow: watershed.FlowMethodSelection.__register__, + RegisterEnum.sprawl: watershed.FlowMethodSelection.__register__, + RegisterEnum.threshold: threshold.ThresholdSelection.__register__, + RegisterEnum.noise_filtering: noise_filtering.NoiseFilterSelection.__register__, + RegisterEnum.analysis_algorithm: analysis_algorithm_description.AnalysisAlgorithmSelection.__register__, + RegisterEnum.mask_algorithm: mask_algorithm_description.MaskAlgorithmSelection.__register__, RegisterEnum.analysis_save: save_functions.save_dict, RegisterEnum.analysis_load: load_functions.load_dict, RegisterEnum.mask_load: io_functions.load_dict, @@ -75,8 +75,8 @@ class RegisterEnum(Enum): RegisterEnum.mask_save_components: io_functions.save_components_dict, RegisterEnum.mask_save_segmentation: io_functions.save_segmentation_dict, RegisterEnum.analysis_measurement: measurement_calculation.MEASUREMENT_DICT, - RegisterEnum.roi_analysis_segmentation_algorithm: analysis_algorithm_description.analysis_algorithm_dict, - RegisterEnum.roi_mask_segmentation_algorithm: mask_algorithm_description.mask_algorithm_dict, + RegisterEnum.roi_analysis_segmentation_algorithm: analysis_algorithm_description.AnalysisAlgorithmSelection.__register__, # noqa: E501 + RegisterEnum.roi_mask_segmentation_algorithm: mask_algorithm_description.MaskAlgorithmSelection.__register__, } # noinspection DuplicatedCode diff --git a/package/PartSegCore/segmentation/algorithm_base.py b/package/PartSegCore/segmentation/algorithm_base.py index c40d63810..a1e3153c8 100644 --- a/package/PartSegCore/segmentation/algorithm_base.py +++ b/package/PartSegCore/segmentation/algorithm_base.py @@ -5,12 +5,17 @@ import numpy as np -from PartSegCore.channel_class import Channel -from PartSegImage import Image - -from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty, ROIExtractionProfile +from PartSegImage import Channel, Image + +from ..algorithm_describe_base import ( + AlgorithmDescribeBase, + AlgorithmProperty, + ROIExtractionProfile, + base_model_to_algorithm_property, +) +from ..class_register import REGISTER, class_to_str from ..image_operations import RadiusType -from ..project_info import AdditionalLayerDescription, ProjectInfoBase +from ..project_info import AdditionalLayerDescription from ..roi_info import ROIInfo from ..utils import numpy_repr @@ -147,11 +152,6 @@ def clean(self): self.channel = None self.mask = None - @staticmethod - def single_channel(): - """Check if algorithm run on single channel""" - return True - @property def mask(self) -> Optional[np.ndarray]: if self._mask is not None and not self.support_time(): @@ -193,21 +193,6 @@ def calculation_run_wrap(self, report_fun: Callable[[str, int], None]) -> ROIExt def calculation_run(self, report_fun: Callable[[str, int], None]) -> ROIExtractionResult: raise NotImplementedError() - @classmethod - def segment_project(cls, project: ProjectInfoBase, parameters: dict) -> ROIExtractionResult: - """ - - :param ProjectInfoBase project: - :param dict parameters: - :return: - :rtype: - """ - instance = cls() - instance.set_image(project.image) - instance.set_mask(project.mask) - instance.set_parameters(**parameters) - return instance.calculation_run(report_empty_fun) - @abstractmethod def get_info_text(self): raise NotImplementedError() @@ -232,7 +217,19 @@ def set_image(self, image): self.channel = None self.mask = None - def set_parameters(self, **kwargs): + def set_parameters(self, _params=None, **kwargs): + # FIXME when drop python 3.7 use postional only argument + if _params is not None: + if isinstance(_params, dict): + kwargs = _params + else: + self.new_parameters = _params + return + if self.__new_style__: + kwargs = REGISTER.migrate_data(class_to_str(self.__argument_class__), {}, kwargs) + self.new_parameters = self.__argument_class__(**kwargs) # pylint: disable=E1102 + return + base_names = [x.name for x in self.get_fields() if isinstance(x, AlgorithmProperty)] if set(base_names) != set(kwargs.keys()): missed_arguments = ", ".join(set(base_names).difference(set(kwargs.keys()))) @@ -241,7 +238,7 @@ def set_parameters(self, **kwargs): self.new_parameters = deepcopy(kwargs) def get_segmentation_profile(self) -> ROIExtractionProfile: - return ROIExtractionProfile("", self.get_name(), deepcopy(self.new_parameters)) + return ROIExtractionProfile(name="", algorithm=self.get_name(), values=deepcopy(self.new_parameters)) @staticmethod def get_steps_num(): @@ -250,7 +247,11 @@ def get_steps_num(): @classmethod def get_channel_parameter_name(cls): - for el in cls.get_fields(): + if cls.__new_style__: + fields = base_model_to_algorithm_property(cls.__argument_class__) + else: + fields = cls.get_fields() + for el in fields: if el.value_type == Channel: return el.name raise ValueError("No channel defined") diff --git a/package/PartSegCore/segmentation/border_smoothing.py b/package/PartSegCore/segmentation/border_smoothing.py index 964e8d827..ac15d1c1a 100644 --- a/package/PartSegCore/segmentation/border_smoothing.py +++ b/package/PartSegCore/segmentation/border_smoothing.py @@ -1,16 +1,17 @@ +import warnings from abc import ABC import numpy as np import SimpleITK as sitk +from pydantic import Field -from PartSegCore.algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty, Register +from PartSegCore.algorithm_describe_base import AlgorithmDescribeBase, AlgorithmSelection from PartSegCore.segmentation.watershed import NeighType, get_neighbourhood +from PartSegCore.utils import BaseModel class BaseSmoothing(AlgorithmDescribeBase, ABC): - @classmethod - def get_fields(cls): - return [] + __argument_class__ = BaseModel @classmethod def smooth(cls, segmentation: np.ndarray, arguments: dict) -> np.ndarray: @@ -27,15 +28,17 @@ def smooth(cls, segmentation: np.ndarray, arguments: dict) -> np.ndarray: return segmentation +class OpeningSmoothingParams(BaseModel): + smooth_border_radius: int = Field(2, title="Smooth borders radius", ge=1, le=20) + + class OpeningSmoothing(BaseSmoothing): + __argument_class__ = OpeningSmoothingParams + @classmethod def get_name(cls) -> str: return "Opening" - @classmethod - def get_fields(cls): - return [AlgorithmProperty("smooth_border_radius", "Smooth borders radius", 2, (1, 20), 1)] - @classmethod def smooth(cls, segmentation: np.ndarray, arguments: dict) -> np.ndarray: radius = arguments["smooth_border_radius"] @@ -44,30 +47,26 @@ def smooth(cls, segmentation: np.ndarray, arguments: dict) -> np.ndarray: return sitk.GetArrayFromImage(sitk.BinaryMorphologicalOpening(sitk.GetImageFromArray(segmentation), radius)) +class VoteSmoothingParams(BaseModel): + neighbourhood_type: NeighType = Field( + NeighType.edges, title="Side Neighbourhood", description="use 6, 18 or 26 neighbourhood (5, 8, 8 for 2d data)" + ) + support_level: int = Field( + 1, + title="Support level", + ge=1, + le=27, + description="How many voxels in neighbourhood need to be labeled to preserve pixel", + ) + + class VoteSmoothing(BaseSmoothing): + __argument_class__ = VoteSmoothingParams + @classmethod def get_name(cls) -> str: return "Vote" - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty( - "neighbourhood_type", - "Side Neighbourhood", - NeighType.edges, - help_text="use 6, 18 or 26 neighbourhood (5, 8, 8 for 2d data)", - ), - AlgorithmProperty( - "support_level", - "Support level", - 1, - (1, 27), - 1, - help_text="How many voxels in neighbourhood need to be labeled to preserve pixel", - ), - ] - @classmethod def smooth(cls, segmentation: np.ndarray, arguments: dict) -> np.ndarray: segmentation_bin = (segmentation > 0).astype(np.uint8) @@ -82,35 +81,19 @@ def smooth(cls, segmentation: np.ndarray, arguments: dict) -> np.ndarray: return segmentation +class IterativeSmoothingParams(VoteSmoothingParams): + max_steps: int = Field( + 1, title="Max steps", description="How many voxels in neighbourhood need to be labeled to preserve pixel" + ) + + class IterativeVoteSmoothing(BaseSmoothing): + __argument_class__ = IterativeSmoothingParams + @classmethod def get_name(cls) -> str: return "Iterative Vote" - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty( - "neighbourhood_type", "Side Neighbourhood", NeighType.edges, help_text="use 6, 18 or 26 neighbourhood" - ), - AlgorithmProperty( - "support_level", - "Support level", - 1, - (1, 26), - 1, - help_text="How many voxels in neighbourhood need to be labeled to preserve pixel", - ), - AlgorithmProperty( - "max_steps", - "Max steps", - 1, - (1, 100), - 1, - help_text="How many voxels in neighbourhood need to be labeled to preserve pixel", - ), - ] - @classmethod def smooth(cls, segmentation: np.ndarray, arguments: dict) -> np.ndarray: segmentation_bin = (segmentation > 0).astype(np.uint8) @@ -132,4 +115,23 @@ def smooth(cls, segmentation: np.ndarray, arguments: dict) -> np.ndarray: return segmentation -smooth_dict = Register(NoneSmoothing, OpeningSmoothing, VoteSmoothing, IterativeVoteSmoothing) +class SmoothAlgorithmSelection(AlgorithmSelection, class_methods=["smooth"], suggested_base_class=BaseSmoothing): + pass + + +SmoothAlgorithmSelection.register(NoneSmoothing) +SmoothAlgorithmSelection.register(OpeningSmoothing) +SmoothAlgorithmSelection.register(VoteSmoothing) +SmoothAlgorithmSelection.register(IterativeVoteSmoothing) + + +def __getattr__(name): # pragma: no cover + if name == "smooth_dict": + warnings.warn( + "threshold_dict is deprecated. Please use SmoothAlgorithmSelection instead", + category=FutureWarning, + stacklevel=2, + ) + return SmoothAlgorithmSelection.__register__ + + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/package/PartSegCore/segmentation/mu_mid_point.py b/package/PartSegCore/segmentation/mu_mid_point.py index 6fd6f5163..8a6e80c33 100644 --- a/package/PartSegCore/segmentation/mu_mid_point.py +++ b/package/PartSegCore/segmentation/mu_mid_point.py @@ -1,8 +1,9 @@ +import warnings from abc import ABC import numpy as np -from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty, Register +from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty, AlgorithmSelection class BaseMuMid(AlgorithmDescribeBase, ABC): @@ -81,4 +82,21 @@ def value(cls, sprawl_area: np.ndarray, data: np.ndarray, lower_bound, upper_bou return np.quantile[data[sprawl_area > 0], arguments["quantile"] / 100] -mu_mid_dict = Register(MeanBound, PercentBound, MeanPixelValue, MedianPixelValue, QuantilePixelValue) +class MuMidSelection(AlgorithmSelection, class_methods=["value"], suggested_base_class=BaseMuMid): + pass + + +MuMidSelection.register(MeanBound) +MuMidSelection.register(PercentBound) +MuMidSelection.register(MeanPixelValue) +MuMidSelection.register(MedianPixelValue) +MuMidSelection.register(QuantilePixelValue) + + +def __getattr__(name): # pragma: no cover + if name == "mu_mid_dict": + warnings.warn( + "mu_mid_dict is deprecated. Please use MuMidSelection instead", category=FutureWarning, stacklevel=2 + ) + return MuMidSelection.__register__ + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/package/PartSegCore/segmentation/noise_filtering.py b/package/PartSegCore/segmentation/noise_filtering.py index bd0c61f05..2a44c10f4 100644 --- a/package/PartSegCore/segmentation/noise_filtering.py +++ b/package/PartSegCore/segmentation/noise_filtering.py @@ -1,15 +1,20 @@ import typing +import warnings from abc import ABC from enum import Enum import numpy as np +from pydantic import Field -from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty, Register -from ..class_generator import enum_register +from PartSegCore.utils import BaseModel + +from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmSelection +from ..class_register import register_class, rename_key, update_argument from ..image_operations import bilateral, gaussian, median from .algorithm_base import calculate_operation_radius as _calculate_operation_radius +@register_class(old_paths=["PartSeg.utils.segmentation.noise_filtering.GaussType"]) class DimensionType(Enum): Layer = 1 Stack = 2 @@ -18,14 +23,6 @@ def __str__(self): return self.name.replace("_", " ") -try: - # noinspection PyUnresolvedReferences,PyUnboundLocalVariable - reloading -except NameError: - reloading = False # means the module is being imported - enum_register.register_class(DimensionType, old_name="GaussType") - - class NoiseFilteringBase(AlgorithmDescribeBase, ABC): """Base class for noise filtering operations""" @@ -43,54 +40,57 @@ def noise_filter(cls, channel: np.ndarray, spacing: typing.Iterable[float], argu class NoneNoiseFiltering(NoiseFilteringBase): + __argument_class__ = BaseModel + @classmethod def get_name(cls): return "None" - @classmethod - def get_fields(cls): - return [] - @classmethod def noise_filter(cls, channel: np.ndarray, spacing: typing.Iterable[float], arguments: dict): return channel +@register_class(version="0.0.1", migrations=[("0.0.1", rename_key("gauss_type", "dimension_type", optional=True))]) +class GaussNoiseFilteringParams(BaseModel): + dimension_type: DimensionType = Field(DimensionType.Layer, title="Gauss type") + radius: float = Field(1.0, title="Gauss radius", ge=0, le=100) + + class GaussNoiseFiltering(NoiseFilteringBase): + __argument_class__ = GaussNoiseFilteringParams + @classmethod def get_name(cls): return "Gauss" @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("dimension_type", "Gauss type", DimensionType.Layer), - AlgorithmProperty("radius", "Gauss radius", 1.0, value_type=float), - ] - - @classmethod - def noise_filter(cls, channel: np.ndarray, spacing: typing.Iterable[float], arguments: dict): - gauss_radius = calculate_operation_radius(arguments["radius"], spacing, arguments["dimension_type"]) - layer = arguments["dimension_type"] == DimensionType.Layer + @update_argument("arguments") + def noise_filter(cls, channel: np.ndarray, spacing: typing.Iterable[float], arguments: GaussNoiseFilteringParams): + gauss_radius = calculate_operation_radius(arguments.radius, spacing, arguments.dimension_type) + layer = arguments.dimension_type == DimensionType.Layer return gaussian(channel, gauss_radius, layer=layer) +class BilateralNoiseFilteringParams(BaseModel): + dimension_type: DimensionType = Field(DimensionType.Layer, title="Bilateral type") + radius: float = Field(1.0, title="Bilateral radius", ge=0, le=100) + + class BilateralNoiseFiltering(NoiseFilteringBase): + __argument_class__ = BilateralNoiseFilteringParams + @classmethod def get_name(cls): return "Bilateral" @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("dimension_type", "Gauss type", DimensionType.Layer), - AlgorithmProperty("radius", "Gauss radius", 1.0, value_type=float), - ] - - @classmethod - def noise_filter(cls, channel: np.ndarray, spacing: typing.Iterable[float], arguments: dict): - gauss_radius = calculate_operation_radius(arguments["radius"], spacing, arguments["dimension_type"]) - layer = arguments["dimension_type"] == DimensionType.Layer + @update_argument("arguments") + def noise_filter( + cls, channel: np.ndarray, spacing: typing.Iterable[float], arguments: BilateralNoiseFilteringParams + ): + gauss_radius = calculate_operation_radius(arguments.radius, spacing, arguments.dimension_type) + layer = arguments.dimension_type == DimensionType.Layer return bilateral(channel, max(gauss_radius), layer=layer) @@ -101,30 +101,43 @@ def calculate_operation_radius(radius, spacing, gauss_type): return res +class MedianNoiseFilteringParams(BaseModel): + dimension_type: DimensionType = Field(DimensionType.Layer, title="Median type") + radius: int = Field(1, title="Median radius", ge=0, le=100) + + class MedianNoiseFiltering(NoiseFilteringBase): + __argument_class__ = MedianNoiseFilteringParams + @classmethod def get_name(cls): return "Median" @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("dimension_type", "Median type", DimensionType.Layer), - AlgorithmProperty("radius", "Median radius", 1, value_type=int), - ] - - @classmethod - def noise_filter(cls, channel: np.ndarray, spacing: typing.Iterable[float], arguments: dict): - gauss_radius = calculate_operation_radius(arguments["radius"], spacing, arguments["dimension_type"]) - layer = arguments["dimension_type"] == DimensionType.Layer + @update_argument("arguments") + def noise_filter(cls, channel: np.ndarray, spacing: typing.Iterable[float], arguments: MedianNoiseFilteringParams): + gauss_radius = calculate_operation_radius(arguments.radius, spacing, arguments.dimension_type) + layer = arguments.dimension_type == DimensionType.Layer gauss_radius = [int(x) for x in gauss_radius] return median(channel, gauss_radius, layer=layer) -noise_filtering_dict = Register( - NoneNoiseFiltering, - GaussNoiseFiltering, - MedianNoiseFiltering, - BilateralNoiseFiltering, - class_methods=["noise_filter"], -) +class NoiseFilterSelection(AlgorithmSelection, class_methods=["noise_filter"], suggested_base_class=NoiseFilteringBase): + pass + + +NoiseFilterSelection.register(NoneNoiseFiltering) +NoiseFilterSelection.register(GaussNoiseFiltering) +NoiseFilterSelection.register(MedianNoiseFiltering) +NoiseFilterSelection.register(BilateralNoiseFiltering) + + +def __getattr__(name): # pragma: no cover + if name == "noise_filtering_dict": + warnings.warn( + "noise_filtering_dict is deprecated. Please use NoiseFilterSelection instead", + category=FutureWarning, + stacklevel=2, + ) + return NoiseFilterSelection.__register__ + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/package/PartSegCore/segmentation/restartable_segmentation_algorithms.py b/package/PartSegCore/segmentation/restartable_segmentation_algorithms.py index 1e4942ef4..9c6aacb6b 100644 --- a/package/PartSegCore/segmentation/restartable_segmentation_algorithms.py +++ b/package/PartSegCore/segmentation/restartable_segmentation_algorithms.py @@ -7,21 +7,26 @@ import numpy as np import SimpleITK +from pydantic import Field, validator +from PartSegCore.utils import BaseModel from PartSegCore_compiled_backend.multiscale_opening import PyMSO, calculate_mu_mid +from PartSegImage import Channel -from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty, ROIExtractionProfile -from ..channel_class import Channel +from ..algorithm_describe_base import ROIExtractionProfile +from ..class_register import REGISTER, class_to_str, register_class, rename_key from ..mask_partition_utils import BorderRim as BorderRimBase from ..mask_partition_utils import MaskDistanceSplit as MaskDistanceSplitBase from ..project_info import AdditionalLayerDescription from ..universal_const import Units from ..utils import bisect from .algorithm_base import ROIExtractionAlgorithm, ROIExtractionResult, SegmentationLimitException -from .mu_mid_point import BaseMuMid, mu_mid_dict -from .noise_filtering import noise_filtering_dict -from .threshold import BaseThreshold, double_threshold_dict, threshold_dict -from .watershed import BaseWatershed, calculate_distances_array, flow_dict, get_neigh +from .mu_mid_point import BaseMuMid, MuMidSelection +from .noise_filtering import NoiseFilterSelection +from .threshold import BaseThreshold, DoubleThresholdSelection, ThresholdSelection +from .watershed import BaseWatershed, FlowMethodSelection, calculate_distances_array, get_neigh + +REQUIRE_MASK_STR = "Need mask" def blank_operator(_x, _y): @@ -39,8 +44,8 @@ class RestartableAlgorithm(ROIExtractionAlgorithm, ABC): def __init__(self, **kwargs): super().__init__() - self.parameters = defaultdict(lambda: None) - self.new_parameters = {} + self.parameters: typing.Dict[str, typing.Optional[typing.Any]] = defaultdict(lambda: None) + self.new_parameters = self.__argument_class__() if self.__new_style__ else {} # pylint: disable=E1102 def set_image(self, image): self.parameters = defaultdict(lambda: None) @@ -65,12 +70,20 @@ def support_z(cls): return True +class BorderRimParameters(BorderRimBase.__argument_class__): + @staticmethod + def header(): + return REQUIRE_MASK_STR + + class BorderRim(RestartableAlgorithm): """ This class wrap the :py:class:`PartSegCore.mask_partition_utils.BorderRim`` class in segmentation algorithm interface. It allow user to check how rim look with given set of parameters """ + __argument_class__ = BorderRimParameters + @classmethod def get_name(cls): return "Border Rim" @@ -80,30 +93,36 @@ def __init__(self): self.distance = 0 self.units = Units.nm - @classmethod - def get_fields(cls): - return ["Need mask"] + BorderRimBase.get_fields() - def get_info_text(self): - return "Need mask" if self.mask is None else "" + return REQUIRE_MASK_STR if self.mask is None else "" def calculation_run(self, _report_fun) -> ROIExtractionResult: if self.mask is not None: - result = BorderRimBase.border_mask(mask=self.mask, voxel_size=self.image.spacing, **self.new_parameters) + result = BorderRimBase.border_mask( + mask=self.mask, voxel_size=self.image.spacing, **self.new_parameters.dict() + ) return ROIExtractionResult(roi=result, parameters=self.get_segmentation_profile()) raise SegmentationLimitException("Border Rim needs mask") +class MaskDistanceSplitParameters(MaskDistanceSplitBase.__argument_class__): + @staticmethod + def header(): + return REQUIRE_MASK_STR + + class MaskDistanceSplit(RestartableAlgorithm): """ This class wrap the :py:class:`PartSegCore.mask_partition_utils.SplitMaskOnPart` class in segmentation algorithm interface. It allow user to check how split look with given set of parameters """ + __argument_class__ = MaskDistanceSplitParameters + def calculation_run(self, report_fun: typing.Callable[[str, int], None]) -> ROIExtractionResult: if self.mask is not None: result = MaskDistanceSplitBase.split( - mask=self.mask, voxel_size=self.image.voxel_size, **self.new_parameters + mask=self.mask, voxel_size=self.image.voxel_size, **self.new_parameters.dict() ) return ROIExtractionResult(roi=result, parameters=self.get_segmentation_profile()) raise SegmentationLimitException("Mask Distance Split needs mask") @@ -112,9 +131,30 @@ def calculation_run(self, report_fun: typing.Callable[[str, int], None]) -> ROIE def get_name(cls) -> str: return "Mask Distance Splitting" - @classmethod - def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: - return ["Need mask"] + MaskDistanceSplitBase.get_fields() + +@register_class(version="0.0.1", migrations=[("0.0.1", rename_key("noise_removal", "noise_filtering", optional=True))]) +class ThresholdBaseAlgorithmParameters(BaseModel): + channel: Channel = 0 + noise_filtering: NoiseFilterSelection = Field(NoiseFilterSelection.get_default(), title="Filter") + minimum_size: int = Field(8000, title="Minimum size (px)", ge=0, le=10**6) + side_connection: bool = Field( + False, + title="Connect only sides", + description="During calculation of connected components includes only side by side connected pixels", + ) + + @validator("noise_filtering") + def _noise_filter_validate(cls, v): # pylint: disable=R0201 + if not isinstance(v, dict): + return v + algorithm = NoiseFilterSelection[v["name"]] + if not algorithm.__new_style__ or not algorithm.__argument_class__.__fields__: + return v + return algorithm.__argument_class__(**REGISTER.migrate_data(class_to_str(algorithm.__argument_class__), {}, v)) + + +class ThresholdBaseAlgorithmParametersAnnot(ThresholdBaseAlgorithmParameters): + threshold: typing.Any = None class ThresholdBaseAlgorithm(RestartableAlgorithm, ABC): @@ -123,28 +163,11 @@ class ThresholdBaseAlgorithm(RestartableAlgorithm, ABC): Created for reduce code repetition. """ - threshold_operator = staticmethod(blank_operator) + __argument_class__ = ThresholdBaseAlgorithmParameters - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("channel", "Channel", 0, value_type=Channel), - AlgorithmProperty( - "noise_filtering", - "Filter", - next(iter(noise_filtering_dict.keys())), - possible_values=noise_filtering_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty("minimum_size", "Minimum size (px)", 8000, (0, 10**6), 1000), - AlgorithmProperty( - "side_connection", - "Connect only sides", - False, - (True, False), - help_text="During calculation of connected components includes" " only side by side connected pixels", - ), - ] + new_parameters: ThresholdBaseAlgorithmParametersAnnot + + threshold_operator = staticmethod(blank_operator) def __init__(self, **kwargs): super().__init__() @@ -203,29 +226,29 @@ def calculation_run(self, report_fun: typing.Callable[[str, int], typing.Any]) - :param report_fun: function used to trace progress """ + # TODO Refactor self.old_threshold_info = self.threshold_info restarted = False - if self.channel is None or self.parameters["channel"] != self.new_parameters["channel"]: - self.parameters["channel"] = self.new_parameters["channel"] - self.channel = self.get_channel(self.new_parameters["channel"]) + if self.channel is None or self.parameters["channel"] != self.new_parameters.channel: + self.parameters["channel"] = self.new_parameters.channel + self.channel = self.get_channel(self.new_parameters.channel) restarted = True - if restarted or self.parameters["noise_filtering"] != self.new_parameters["noise_filtering"]: - self.parameters["noise_filtering"] = deepcopy(self.new_parameters["noise_filtering"]) - noise_filtering_parameters = self.new_parameters["noise_filtering"] - self.cleaned_image = noise_filtering_dict[noise_filtering_parameters["name"]].noise_filter( - self.channel, self.image.spacing, noise_filtering_parameters["values"] + if restarted or self.parameters["noise_filtering"] != self.new_parameters.noise_filtering: + self.parameters["noise_filtering"] = deepcopy(self.new_parameters.noise_filtering) + noise_filtering_parameters = self.new_parameters.noise_filtering + self.cleaned_image = NoiseFilterSelection[noise_filtering_parameters.name].noise_filter( + self.channel, self.image.spacing, noise_filtering_parameters.values ) restarted = True - if restarted or self.new_parameters["threshold"] != self.parameters["threshold"]: + if restarted or self.new_parameters.threshold != self.parameters["threshold"]: if self.parameters["threshold"] is None: restarted = True - self.parameters["threshold"] = deepcopy(self.new_parameters["threshold"]) + self.parameters["threshold"] = deepcopy(self.new_parameters.threshold) self.threshold_image = self._threshold(self.cleaned_image) - if isinstance(self.threshold_info, (list, tuple)) and ( - self.old_threshold_info is None or self.old_threshold_info[0] != self.threshold_info[0] - ): - restarted = True - elif self.old_threshold_info != self.threshold_info: + if ( + isinstance(self.threshold_info, (list, tuple)) + and (self.old_threshold_info is None or self.old_threshold_info[0] != self.threshold_info[0]) + ) or self.old_threshold_info != self.threshold_info: restarted = True if self.threshold_image.max() == 0: res = self.prepare_result(self.threshold_image.astype(np.uint8)) @@ -236,17 +259,17 @@ def calculation_run(self, report_fun: typing.Callable[[str, int], typing.Any]) - f"and chosen threshold is {self.threshold_info}" ) return dataclasses.replace(res, info_text=info_text) - if restarted or self.new_parameters["side_connection"] != self.parameters["side_connection"]: - self.parameters["side_connection"] = self.new_parameters["side_connection"] + if restarted or self.new_parameters.side_connection != self.parameters["side_connection"]: + self.parameters["side_connection"] = self.new_parameters.side_connection connect = SimpleITK.ConnectedComponent( - SimpleITK.GetImageFromArray(self.threshold_image), not self.new_parameters["side_connection"] + SimpleITK.GetImageFromArray(self.threshold_image), not self.new_parameters.side_connection ) self.segmentation = SimpleITK.GetArrayFromImage(SimpleITK.RelabelComponent(connect)) self._sizes_array = np.bincount(self.segmentation.flat) restarted = True - if restarted or self.new_parameters["minimum_size"] != self.parameters["minimum_size"]: - self.parameters["minimum_size"] = self.new_parameters["minimum_size"] - minimum_size = self.new_parameters["minimum_size"] + if restarted or self.new_parameters.minimum_size != self.parameters["minimum_size"]: + self.parameters["minimum_size"] = self.new_parameters.minimum_size + minimum_size = self.new_parameters.minimum_size ind = bisect(self._sizes_array[1:], minimum_size, lambda x, y: x > y) finally_segment = np.copy(self.segmentation) finally_segment[finally_segment > ind] = 0 @@ -262,37 +285,28 @@ def calculation_run(self, report_fun: typing.Callable[[str, int], typing.Any]) - def clean(self): super().clean() - self.parameters = defaultdict(lambda: None) + self.parameters: typing.Dict[str, typing.Optional[typing.Any]] = defaultdict(lambda: None) self.cleaned_image = None self.mask = None def _threshold(self, image, thr=None): if thr is None: - thr: BaseThreshold = threshold_dict[self.new_parameters["threshold"]["name"]] + thr: BaseThreshold = ThresholdSelection[self.new_parameters.threshold.name] mask, thr_val = thr.calculate_mask( - image, self.mask, self.new_parameters["threshold"]["values"], self.threshold_operator + image, self.mask, self.new_parameters.threshold.values, self.threshold_operator ) self.threshold_info = thr_val return mask +class OneThresholdAlgorithmParameters(ThresholdBaseAlgorithmParameters): + threshold: ThresholdSelection = Field(ThresholdSelection.get_default(), position=2) + + class OneThresholdAlgorithm(ThresholdBaseAlgorithm, ABC): """Base class for PartSeg analysis algorithm which apply one threshold. Created for reduce code repetition.""" - @classmethod - def get_fields(cls): - fields = super().get_fields() - fields.insert( - 2, - AlgorithmProperty( - "threshold", - "Threshold", - next(iter(threshold_dict.keys())), - possible_values=threshold_dict, - value_type=AlgorithmDescribeBase, - ), - ) - return fields + __argument_class__ = OneThresholdAlgorithmParameters class LowerThresholdAlgorithm(OneThresholdAlgorithm): @@ -323,6 +337,23 @@ def get_name(cls): return "Upper threshold" +class TwoThreshold(BaseModel): + lower_threshold: float = Field(1000, ge=0, le=10**6) + upper_threshold: float = Field(10000, ge=0, le=10**6) + + +def _to_two_thresholds(dkt): + dkt["threshold"] = TwoThreshold( + lower_threshold=dkt.pop("lower_threshold"), upper_threshold=dkt.pop("upper_threshold") + ) + return dkt + + +@register_class(version="0.0.1", migrations=[("0.0.1", _to_two_thresholds)]) +class RangeThresholdAlgorithmParameters(ThresholdBaseAlgorithmParameters): + threshold: TwoThreshold = Field(TwoThreshold(), position=2) + + class RangeThresholdAlgorithm(ThresholdBaseAlgorithm): """ Implementation of upper threshold algorithm. @@ -330,34 +361,15 @@ class RangeThresholdAlgorithm(ThresholdBaseAlgorithm): The area of interest are voxels from filtered channel with value between the lower and upper threshold """ - def set_parameters(self, **kwargs): - super().set_parameters(**kwargs) - self.new_parameters["threshold"] = ( - self.new_parameters["lower_threshold"], - self.new_parameters["upper_threshold"], - ) - - def get_segmentation_profile(self) -> ROIExtractionProfile: - resp = super().get_segmentation_profile() - low, upp = resp.values["threshold"] - del resp.values["threshold"] - resp.values["lower_threshold"] = low - resp.values["upper_threshold"] = upp - return resp + __argument_class__ = RangeThresholdAlgorithmParameters def _threshold(self, image, thr=None): - self.threshold_info = self.new_parameters["threshold"] + self.threshold_info = deepcopy(self.new_parameters.threshold) return ( - (image > self.new_parameters["threshold"][0]) * np.array(image < self.new_parameters["threshold"][1]) + (image > self.new_parameters.threshold.lower_threshold) + * np.array(image < self.new_parameters.threshold.upper_threshold) ).astype(np.uint8) - @classmethod - def get_fields(cls): - fields = super().get_fields() - fields.insert(2, AlgorithmProperty("lower_threshold", "Lower threshold", 10000, (0, 10**6), 100)) - fields.insert(3, AlgorithmProperty("upper_threshold", "Upper threshold", 10000, (0, 10**6), 100)) - return fields - @classmethod def get_name(cls): return "Range threshold" @@ -370,47 +382,25 @@ def __init__(self): def _threshold(self, image, thr=None): if thr is None: - thr: BaseThreshold = double_threshold_dict[self.new_parameters["threshold"]["name"]] + thr: BaseThreshold = DoubleThresholdSelection[self.new_parameters.threshold.name] mask, thr_val = thr.calculate_mask( - image, self.mask, self.new_parameters["threshold"]["values"], self.threshold_operator + image, self.mask, self.new_parameters.threshold.values, self.threshold_operator ) self.threshold_info = thr_val self.sprawl_area = (mask >= 1).astype(np.uint8) return (mask == 2).astype(np.uint8) +@register_class(version="0.0.1", migrations=[("0.0.1", rename_key("sprawl_type", "flow_type"))]) +class BaseThresholdFlowAlgorithmParameters(ThresholdBaseAlgorithmParameters): + threshold: DoubleThresholdSelection = Field(DoubleThresholdSelection.get_default(), position=2) + flow_type: FlowMethodSelection = Field(FlowMethodSelection.get_default(), position=3) + minimum_size: int = Field(8000, title="Minimum core\nsize (px)", ge=0, le=10**6) + + class BaseThresholdFlowAlgorithm(TwoLevelThresholdBaseAlgorithm, ABC): - @classmethod - def get_fields(cls): - fields = super().get_fields() - fields.insert( - 2, - AlgorithmProperty( - "threshold", - "Threshold", - next(iter(double_threshold_dict.keys())), - possible_values=double_threshold_dict, - value_type=AlgorithmDescribeBase, - ), - ) - fields.insert( - 3, - AlgorithmProperty( - "sprawl_type", - "Flow type", - next(iter(flow_dict.keys())), - possible_values=flow_dict, - value_type=AlgorithmDescribeBase, - ), - ) - for i, el in enumerate(fields): - if el.name == "minimum_size": - index = i - break - else: - raise ValueError("No minimum size field") - fields[index] = AlgorithmProperty("minimum_size", "Minimum core\nsize (px)", 8000, (0, 10**6), 1000) - return fields + __argument_class__ = BaseThresholdFlowAlgorithmParameters + new_parameters: BaseThresholdFlowAlgorithmParameters def get_info_text(self): return ( @@ -453,22 +443,22 @@ def calculation_run(self, report_fun) -> ROIExtractionResult: if ( restarted or self.old_threshold_info[1] != self.threshold_info[1] - or self.new_parameters["sprawl_type"] != self.parameters["sprawl_type"] + or self.new_parameters.flow_type != self.parameters["flow_type"] ): if self.threshold_operator(self.threshold_info[1], self.threshold_info[0]): self.final_sizes = np.bincount(finally_segment.flat) return self.prepare_result(self.finally_segment) - path_sprawl: BaseWatershed = flow_dict[self.new_parameters["sprawl_type"]["name"]] - self.parameters["sprawl_type"] = self.new_parameters["sprawl_type"] + path_sprawl: BaseWatershed = FlowMethodSelection[self.new_parameters.flow_type.name] + self.parameters["flow_type"] = self.new_parameters.flow_type new_segment = path_sprawl.sprawl( self.sprawl_area, np.copy(finally_segment), # TODO add tests for discover this problem self.channel, self.components_num, self.image.spacing, - self.new_parameters["side_connection"], + self.new_parameters.side_connection, self.threshold_operator, - self.new_parameters["sprawl_type"]["values"], + self.new_parameters.flow_type.values, self.threshold_info[1], self.threshold_info[0], ) @@ -501,52 +491,47 @@ def get_name(cls): return "Upper threshold with watershed" +@register_class(version="0.0.1", migrations=[("0.0.1", rename_key("noise_removal", "noise_filtering", optional=True))]) +class OtsuSegmentParameters(BaseModel): + channel: Channel = 0 + noise_filtering: NoiseFilterSelection = Field(NoiseFilterSelection.get_default(), title="Noise Removal") + components: int = Field(2, title="Number of Components", ge=0, lt=100) + valley: bool = Field(True, title="Valley emphasis") + hist_num: int = Field(128, title="Number of histogram bins", ge=8, le=2**16) + + class OtsuSegment(RestartableAlgorithm): + __argument_class__ = OtsuSegmentParameters + new_parameters: OtsuSegmentParameters + @classmethod def get_name(cls): return "Multiple Otsu" - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("channel", "Channel", 0, value_type=Channel), - AlgorithmProperty( - "noise_filtering", - "Noise Removal", - next(iter(noise_filtering_dict.keys())), - possible_values=noise_filtering_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty("components", "Number of Components", 2, (0, 100)), - # AlgorithmProperty("mask", "Use mask in calculation", True), - AlgorithmProperty("valley", "Valley emphasis", True), - AlgorithmProperty("hist_num", "Number of histogram bins", 128, (8, 2**16)), - ] - def __init__(self): super().__init__() self._sizes_array = [] self.threshold_info = [] def calculation_run(self, report_fun): - channel = self.get_channel(self.new_parameters["channel"]) - noise_filtering_parameters = self.new_parameters["noise_filtering"] - cleaned_image = noise_filtering_dict[noise_filtering_parameters["name"]].noise_filter( - channel, self.image.spacing, noise_filtering_parameters["values"] + channel = self.get_channel(self.new_parameters.channel) + noise_filtering_parameters = self.new_parameters.noise_filtering + cleaned_image = NoiseFilterSelection[noise_filtering_parameters.name].noise_filter( + channel, self.image.spacing, noise_filtering_parameters.values ) cleaned_image_sitk = SimpleITK.GetImageFromArray(cleaned_image) res = SimpleITK.OtsuMultipleThresholds( cleaned_image_sitk, - self.new_parameters["components"], + self.new_parameters.components, 0, - self.new_parameters["hist_num"], - self.new_parameters["valley"], + self.new_parameters.hist_num, + self.new_parameters.valley, ) res = SimpleITK.GetArrayFromImage(res) self._sizes_array = np.bincount(res.flat)[1:] self.threshold_info = [] annotations = {} - for i in range(1, self.new_parameters["components"] + 1): + for i in range(1, self.new_parameters.components + 1): val = cleaned_image[res == i] if val.size: self.threshold_info.append(np.min(val)) @@ -557,7 +542,7 @@ def calculation_run(self, report_fun): annotations[i] = {"lower threshold": self.threshold_info[-1]} if i > 1: annotations[i - 1]["upper threshold"] = self.threshold_info[-1] - annotations[self.new_parameters["components"]]["upper threshold"] = np.max(cleaned_image) + annotations[self.new_parameters.components]["upper threshold"] = np.max(cleaned_image) return ROIExtractionResult( roi=res, parameters=self.get_segmentation_profile(), @@ -574,26 +559,15 @@ def get_info_text(self): ) +class BaseMultiScaleOpeningParameters(TwoLevelThresholdBaseAlgorithm.__argument_class__): + threshold: DoubleThresholdSelection = Field(DoubleThresholdSelection.get_default()) + mu_mid: MuMidSelection = Field(MuMidSelection.get_default(), title="Mu mid value") + step_limits: int = Field(100, title="Limits of Steps", ge=1, le=1000) + + class BaseMultiScaleOpening(TwoLevelThresholdBaseAlgorithm, ABC): # pragma: no cover - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty( - "threshold", - "Threshold", - next(iter(double_threshold_dict.keys())), - possible_values=double_threshold_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty( - "mu_mid", - "Mu mid value", - next(iter(mu_mid_dict.keys())), - possible_values=mu_mid_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty("step_limits", "Limits of Steps", 100, options_range=(1, 1000), value_type=int), - ] + super().get_fields() + __argument_class__ = BaseMultiScaleOpeningParameters + new_parameters: BaseMultiScaleOpeningParameters def get_info_text(self): return ( @@ -610,7 +584,7 @@ def __init__(self): super().__init__() self.finally_segment = None self.final_sizes = [] - self.threshold_info = [None, None] + self.threshold_info = [float("nan"), float("nan")] self.steps = 0 self.mso = PyMSO() self.mso.set_use_background(True) @@ -623,13 +597,11 @@ def clean(self): def set_image(self, image): super().set_image(image) - self.threshold_info = [None, None] + self.threshold_info = [float("nan"), float("nan")] def calculation_run(self, report_fun) -> ROIExtractionResult: - if self.new_parameters["side_connection"] != self.parameters["side_connection"]: - neigh, dist = calculate_distances_array( - self.image.spacing, get_neigh(self.new_parameters["side_connection"]) - ) + if self.new_parameters.side_connection != self.parameters["side_connection"]: + neigh, dist = calculate_distances_array(self.image.spacing, get_neigh(self.new_parameters.side_connection)) self.mso.set_neighbourhood(neigh, dist) segment_data = super().calculation_run(report_fun) if segment_data is not None and self.components_num == 0: @@ -655,13 +627,13 @@ def calculation_run(self, report_fun) -> ROIExtractionResult: if ( restarted or self.old_threshold_info[1] != self.threshold_info[1] - or self.new_parameters["mu_mid"] != self.parameters["mu_mid"] + or self.new_parameters.mu_mid != self.parameters["mu_mid"] ): if self.threshold_operator(self.threshold_info[1], self.threshold_info[0]): self.final_sizes = np.bincount(finally_segment.flat) return self.prepare_result(self.finally_segment) - mu_calc: BaseMuMid = mu_mid_dict[self.new_parameters["mu_mid"]["name"]] - self.parameters["mu_mid"] = self.new_parameters["mu_mid"] + mu_calc: BaseMuMid = MuMidSelection[self.new_parameters.mu_mid.name] + self.parameters["mu_mid"] = self.new_parameters.mu_mid sprawl_area = (self.sprawl_area > 0).astype(np.uint8) sprawl_area[finally_segment > 0] = 0 mid_val = mu_calc.value( @@ -669,16 +641,16 @@ def calculation_run(self, report_fun) -> ROIExtractionResult: self.channel, self.threshold_info[0], self.threshold_info[1], - self.new_parameters["mu_mid"]["values"], + self.new_parameters.mu_mid.values, ) mu_array = calculate_mu_mid(self.channel, self.threshold_info[0], mid_val, self.threshold_info[1]) self.mso.set_mu_array(mu_array) restarted = True - if restarted or self.new_parameters["step_limits"] != self.parameters["step_limits"]: - self.parameters["step_limits"] = self.new_parameters["step_limits"] + if restarted or self.new_parameters.step_limits != self.parameters["step_limits"]: + self.parameters["step_limits"] = self.new_parameters.step_limits count_steps_factor = 20 if self.image.is_2d else 3 - self.mso.run_MSO(self.new_parameters["step_limits"], count_steps_factor) + self.mso.run_MSO(self.new_parameters.step_limits, count_steps_factor) self.steps = self.mso.steps_done() new_segment = self.mso.get_result_catted() new_segment[new_segment > 0] -= 1 @@ -707,8 +679,7 @@ def get_name(cls): # pragma: no cover UpperThresholdAlgorithm, RangeThresholdAlgorithm, LowerThresholdFlowAlgorithm, - UpperThresholdFlowAlgorithm, # LowerThresholdMultiScaleOpening, - # UpperThresholdMultiScaleOpening, + UpperThresholdFlowAlgorithm, OtsuSegment, BorderRim, MaskDistanceSplit, diff --git a/package/PartSegCore/segmentation/segmentation_algorithm.py b/package/PartSegCore/segmentation/segmentation_algorithm.py index 27eefd919..bc83443ea 100644 --- a/package/PartSegCore/segmentation/segmentation_algorithm.py +++ b/package/PartSegCore/segmentation/segmentation_algorithm.py @@ -4,18 +4,20 @@ import numpy as np import SimpleITK as sitk +from pydantic import Field -from PartSegCore.segmentation.border_smoothing import smooth_dict -from PartSegCore.segmentation.watershed import BaseWatershed, flow_dict +from PartSegCore.utils import BaseModel +from PartSegImage import Channel -from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty -from ..channel_class import Channel +from ..class_register import register_class, rename_key from ..convex_fill import convex_fill from ..project_info import AdditionalLayerDescription from ..segmentation.algorithm_base import ROIExtractionAlgorithm, ROIExtractionResult from ..utils import bisect -from .noise_filtering import noise_filtering_dict -from .threshold import BaseThreshold, double_threshold_dict, threshold_dict +from .border_smoothing import NoneSmoothing, OpeningSmoothing, SmoothAlgorithmSelection +from .noise_filtering import NoiseFilterSelection +from .threshold import BaseThreshold, DoubleThresholdSelection, ThresholdSelection +from .watershed import BaseWatershed, FlowMethodSelection class StackAlgorithm(ROIExtractionAlgorithm, ABC): @@ -33,34 +35,29 @@ def support_z(cls): def get_noise_filtered_channel(self, channel_idx, noise_removal): channel = self.get_channel(channel_idx) - return noise_filtering_dict[noise_removal["name"]].noise_filter( - channel, self.image.spacing, noise_removal["values"] - ) + return NoiseFilterSelection[noise_removal.name].noise_filter(channel, self.image.spacing, noise_removal.values) + + +@register_class(version="0.0.1", migrations=[("0.0.1", rename_key("noise_removal", "noise_filtering", optional=True))]) +class ThresholdPreviewParameters(BaseModel): + channel: Channel = 0 + noise_filtering: NoiseFilterSelection = Field(NoiseFilterSelection.get_default(), title="Filter") + threshold: int = Field(1000, ge=0, le=10**6) class ThresholdPreview(StackAlgorithm): - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("channel", "Channel", 0, value_type=Channel), - AlgorithmProperty( - "noise_filtering", - "Filter", - noise_filtering_dict.get_default(), - possible_values=noise_filtering_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty("threshold", "Threshold", 1000, (0, 10**6), 100), - ] + __argument_class__ = ThresholdPreviewParameters + + new_parameters: ThresholdPreviewParameters @classmethod def get_name(cls): return "Only Threshold" def calculation_run(self, report_fun) -> ROIExtractionResult: - image = self.get_noise_filtered_channel(self.new_parameters["channel"], self.new_parameters["noise_filtering"]) + image = self.get_noise_filtered_channel(self.new_parameters.channel, self.new_parameters.noise_filtering) report_fun("threshold", 0) - res = (image > self.new_parameters["threshold"]).astype(np.uint8) + res = (image > self.new_parameters.threshold).astype(np.uint8) report_fun("mask", 1) if self.mask is not None: res[self.mask == 0] = 0 @@ -81,7 +78,51 @@ def get_steps_num(): return 3 +def _migrate_smooth_border(dkt: dict): + if isinstance(dkt["smooth_border"], bool): + dkt = dkt.copy() + if dkt["smooth_border"] and "smooth_border_radius" in dkt: + dkt["smooth_border"] = SmoothAlgorithmSelection( + name=OpeningSmoothing.get_name(), + values=OpeningSmoothing.__argument_class__(smooth_border_radius=dkt.pop("smooth_border_radius")), + ) + else: + dkt["smooth_border"] = SmoothAlgorithmSelection( + name=NoneSmoothing.get_name(), values=NoneSmoothing.__argument_class__() + ) + if "smooth_border_radius" in dkt: + del dkt["smooth_border_radius"] + return dkt + + +@register_class( + version="0.0.2", + migrations=[ + ("0.0.1", _migrate_smooth_border), + ("0.0.2", rename_key("noise_removal", "noise_filtering", optional=True)), + ], +) +class BaseThresholdAlgorithmParameters(BaseModel): + channel: Channel = 0 + noise_filtering: NoiseFilterSelection = Field(NoiseFilterSelection.get_default(), title="Filter") + threshold: ThresholdSelection = Field(ThresholdSelection.get_default(), title="Threshold") + close_holes: bool = Field(True, title="Fill holes") + close_holes_size: int = Field(200, title="Maximum holes size (px)", ge=0, le=10**5) + smooth_border: SmoothAlgorithmSelection = Field(SmoothAlgorithmSelection.get_default(), title="Smooth borders") + side_connection: bool = Field( + False, + title="Side by Side connections", + description="During calculation of connected components includes only side by side connected pixels", + ) + minimum_size: int = Field(8000, ge=20, le=10**6) + use_convex: int = Field(False, title="Use convex hull") + + class BaseThresholdAlgorithm(StackAlgorithm, ABC): + __argument_class__ = BaseThresholdAlgorithmParameters + + new_parameters: BaseThresholdAlgorithmParameters + def __init__(self): super().__init__() self.sizes = [0] @@ -91,46 +132,8 @@ def get_info_text(self): return f"ROI sizes: {', '.join(map(str, self.sizes[1:]))}" return "" - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("channel", "Channel", 0, value_type=Channel), - AlgorithmProperty( - "noise_filtering", - "Filter", - noise_filtering_dict.get_default(), - possible_values=noise_filtering_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty( - "threshold", - "Threshold", - threshold_dict.get_default(), - possible_values=threshold_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty("close_holes", "Fill holes", True, (True, False)), - AlgorithmProperty("close_holes_size", "Maximum holes size (px)", 200, (0, 10**5), 10), - AlgorithmProperty( - "smooth_border", - "Smooth borders", - smooth_dict.get_default(), - possible_values=smooth_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty( - "side_connection", - "Side by Side connections", - False, - (True, False), - help_text="During calculation of connected components includes" " only side by side connected pixels", - ), - AlgorithmProperty("minimum_size", "Minimum size", 8000, (20, 10**6), 1000), - AlgorithmProperty("use_convex", "Use convex hull", False, (True, False)), - ] - - -class MorphologicalWatersheed(BaseThresholdAlgorithm): + +class MorphologicalWatershed(BaseThresholdAlgorithm): def __init__(self): super().__init__() self.base_sizes = [0] @@ -145,23 +148,23 @@ def get_steps_num(): def _threshold_and_exclude(self, image, report_fun): report_fun("Threshold calculation", 1) - threshold_algorithm: BaseThreshold = threshold_dict[self.new_parameters["threshold"]["name"]] + threshold_algorithm: BaseThreshold = ThresholdSelection[self.new_parameters.threshold.name] mask, _thr_val = threshold_algorithm.calculate_mask( - image, self.mask, self.new_parameters["threshold"]["values"], operator.ge + image, self.mask, self.new_parameters.threshold.values, operator.ge ) report_fun("Threshold calculated", 2) return mask def calculation_run(self, report_fun): report_fun("Noise removal", 0) - image = self.get_noise_filtered_channel(self.new_parameters["channel"], self.new_parameters["noise_filtering"]) + image = self.get_noise_filtered_channel(self.new_parameters.channel, self.new_parameters.noise_filtering) mask = self._threshold_and_exclude(image, report_fun) - if self.new_parameters["close_holes"]: + if self.new_parameters.close_holes: report_fun("Filing holes", 3) - mask = close_small_holes(mask, self.new_parameters["close_holes_size"]) + mask = close_small_holes(mask, self.new_parameters.close_holes_size) report_fun("Smooth border", 4) - self.segmentation = smooth_dict[self.new_parameters["smooth_border"]["name"]].smooth( - mask, self.new_parameters["smooth_border"]["values"] + self.segmentation = SmoothAlgorithmSelection[self.new_parameters.smooth_border.name].smooth( + mask, self.new_parameters.smooth_border.values ) report_fun("Components calculating", 5) @@ -176,7 +179,7 @@ def calculation_run(self, report_fun): ) self.base_sizes = np.bincount(self.segmentation.flat) - ind = bisect(self.base_sizes[1:], self.new_parameters["minimum_size"], lambda x, y: x > y) + ind = bisect(self.base_sizes[1:], self.new_parameters.minimum_size, lambda x, y: x > y) resp = np.copy(self.segmentation) resp[resp > ind] = 0 @@ -187,7 +190,7 @@ def calculation_run(self, report_fun): else: info_text = "" self.sizes = self.base_sizes[: ind + 1] - if self.new_parameters["use_convex"]: + if self.new_parameters.use_convex: report_fun("convex hull", 6) resp = convex_fill(resp) self.sizes = np.bincount(resp.flat) @@ -229,28 +232,28 @@ def _threshold_and_exclude(self, image, report_fun): def calculation_run(self, report_fun): report_fun("Noise removal", 0) - image = self.get_noise_filtered_channel(self.new_parameters["channel"], self.new_parameters["noise_filtering"]) + image = self.get_noise_filtered_channel(self.new_parameters.channel, self.new_parameters.noise_filtering) mask = self._threshold_and_exclude(image, report_fun) - if self.new_parameters["close_holes"]: + if self.new_parameters.close_holes: report_fun("Filing holes", 3) - mask = close_small_holes(mask, self.new_parameters["close_holes_size"]) + mask = close_small_holes(mask, self.new_parameters.close_holes_size) report_fun("Smooth border", 4) - self.segmentation = smooth_dict[self.new_parameters["smooth_border"]["name"]].smooth( - mask, self.new_parameters["smooth_border"]["values"] + self.segmentation = SmoothAlgorithmSelection[self.new_parameters.smooth_border.name].smooth( + mask, self.new_parameters.smooth_border.values ) report_fun("Components calculating", 5) self.segmentation = sitk.GetArrayFromImage( sitk.RelabelComponent( sitk.ConnectedComponent( - sitk.GetImageFromArray(self.segmentation), not self.new_parameters["side_connection"] + sitk.GetImageFromArray(self.segmentation), not self.new_parameters.side_connection ), 20, ) ) self.base_sizes = np.bincount(self.segmentation.flat) - ind = bisect(self.base_sizes[1:], self.new_parameters["minimum_size"], lambda x, y: x > y) + ind = bisect(self.base_sizes[1:], self.new_parameters.minimum_size, lambda x, y: x > y) resp = np.copy(self.segmentation) resp[resp > ind] = 0 @@ -261,7 +264,7 @@ def calculation_run(self, report_fun): else: info_text = "" self.sizes = self.base_sizes[: ind + 1] - if self.new_parameters["use_convex"]: + if self.new_parameters.use_convex: report_fun("convex hull", 6) resp = convex_fill(resp) self.sizes = np.bincount(resp.flat) @@ -296,113 +299,76 @@ def _threshold_image(self, image: np.ndarray) -> Optional[np.ndarray]: def _threshold_and_exclude(self, image, report_fun): report_fun("Threshold calculation", 1) - threshold_algorithm: BaseThreshold = threshold_dict[self.new_parameters["threshold"]["name"]] + threshold_algorithm: BaseThreshold = ThresholdSelection[self.new_parameters.threshold.name] mask, _thr_val = threshold_algorithm.calculate_mask( - image, self.mask, self.new_parameters["threshold"]["values"], operator.ge + image, self.mask, self.new_parameters.threshold.values, operator.ge ) report_fun("Threshold calculated", 2) return mask +@register_class(version="0.0.1", migrations=[("0.0.1", rename_key("sprawl_type", "flow_type"))]) +class ThresholdFlowAlgorithmParameters(BaseThresholdAlgorithmParameters): + threshold: DoubleThresholdSelection = Field(DoubleThresholdSelection.get_default()) + flow_type: FlowMethodSelection = Field(FlowMethodSelection.get_default()) + + class ThresholdFlowAlgorithm(BaseThresholdAlgorithm): + __argument_class__ = ThresholdFlowAlgorithmParameters + + new_parameters: ThresholdFlowAlgorithmParameters + @classmethod def get_name(cls) -> str: return "Threshold Flow" - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("channel", "Channel", 0, value_type=Channel), - AlgorithmProperty( - "noise_filtering", - "Filter", - noise_filtering_dict.get_default(), - possible_values=noise_filtering_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty( - "threshold", - "Threshold", - double_threshold_dict.get_default(), - possible_values=double_threshold_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty("close_holes", "Fill holes", True, (True, False)), - AlgorithmProperty("close_holes_size", "Maximum holes size (px)", 200, (0, 10**5), 10), - AlgorithmProperty( - "smooth_border", - "Smooth borders", - smooth_dict.get_default(), - possible_values=smooth_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty( - "side_connection", - "Side by Side connections", - False, - (True, False), - help_text="During calculation of connected components includes only side by side connected pixels", - ), - AlgorithmProperty("minimum_size", "Minimum size", 8000, (20, 10**6), 1000), - AlgorithmProperty( - "sprawl_type", - "Flow type", - flow_dict.get_default(), - possible_values=flow_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty("use_convex", "Use convex hull", False, (True, False)), - ] - def calculation_run(self, report_fun: Callable[[str, int], None]) -> ROIExtractionResult: report_fun("Noise removal", 0) noise_filtered = self.get_noise_filtered_channel( - self.new_parameters["channel"], self.new_parameters["noise_filtering"] + self.new_parameters.channel, self.new_parameters.noise_filtering ) report_fun("Threshold apply", 1) - mask, thr = double_threshold_dict[self.new_parameters["threshold"]["name"]].calculate_mask( - noise_filtered, self.mask, self.new_parameters["threshold"]["values"], operator.ge + mask, thr = DoubleThresholdSelection[self.new_parameters.threshold.name].calculate_mask( + noise_filtered, self.mask, self.new_parameters.threshold.values, operator.ge ) core_objects = np.array(mask == 2).astype(np.uint8) report_fun("Core components calculating", 2) core_objects = sitk.GetArrayFromImage( sitk.RelabelComponent( - sitk.ConnectedComponent( - sitk.GetImageFromArray(core_objects), not self.new_parameters["side_connection"] - ), + sitk.ConnectedComponent(sitk.GetImageFromArray(core_objects), not self.new_parameters.side_connection), 20, ) ) self.base_sizes = np.bincount(core_objects.flat) - ind = bisect(self.base_sizes[1:], self.new_parameters["minimum_size"], lambda x, y: x > y) + ind = bisect(self.base_sizes[1:], self.new_parameters.minimum_size, lambda x, y: x > y) core_objects[core_objects > ind] = 0 - if self.new_parameters["close_holes"]: + if self.new_parameters.close_holes: report_fun("Filing holes", 3) - mask = close_small_holes(mask, self.new_parameters["close_holes_size"]) + mask = close_small_holes(mask, self.new_parameters.close_holes_size) report_fun("Smooth border", 4) - mask = smooth_dict[self.new_parameters["smooth_border"]["name"]].smooth( - mask, self.new_parameters["smooth_border"]["values"] + mask = SmoothAlgorithmSelection[self.new_parameters.smooth_border.name].smooth( + mask, self.new_parameters.smooth_border.values ) report_fun("Flow calculation", 5) - sprawl_algorithm: BaseWatershed = flow_dict[self.new_parameters["sprawl_type"]["name"]] + sprawl_algorithm: BaseWatershed = FlowMethodSelection[self.new_parameters.flow_type.name] segmentation = sprawl_algorithm.sprawl( mask, core_objects, noise_filtered, ind, self.image.spacing, - self.new_parameters["side_connection"], + self.new_parameters.side_connection, operator.gt, - self.new_parameters["sprawl_type"]["values"], + self.new_parameters.flow_type.values, thr[1], thr[0], ) - if self.new_parameters["use_convex"]: + if self.new_parameters.use_convex: report_fun("convex hull", 6) segmentation = convex_fill(segmentation) report_fun("Calculation done", 7) @@ -420,12 +386,13 @@ def get_steps_num(): return 7 +class AutoThresholdAlgorithmParams(BaseThresholdAlgorithmParameters): + suggested_size: int = Field(200000, ge=0, le=10**6) + + class AutoThresholdAlgorithm(BaseSingleThresholdAlgorithm): - @classmethod - def get_fields(cls): - res = super().get_fields() - res.insert(-1, AlgorithmProperty("suggested_size", "Suggested size", 200000, (0, 10**6), 1000)) - return res + __argument_class__ = AutoThresholdAlgorithmParams + new_parameters: AutoThresholdAlgorithmParams @classmethod def get_name(cls): @@ -433,13 +400,13 @@ def get_name(cls): def _threshold_image(self, image: np.ndarray) -> np.ndarray: sitk_image = sitk.GetImageFromArray(image) - sitk_mask = sitk.ThresholdMaximumConnectedComponents(sitk_image, self.new_parameters["suggested_size"]) + sitk_mask = sitk.ThresholdMaximumConnectedComponents(sitk_image, self.new_parameters.suggested_size) # TODO what exactly it returns. Maybe it is already segmented. mask = sitk.GetArrayFromImage(sitk_mask) min_val = np.min(image[mask > 0]) - threshold_algorithm: BaseThreshold = threshold_dict[self.new_parameters["threshold"]["name"]] + threshold_algorithm: BaseThreshold = ThresholdSelection[self.new_parameters.threshold.name] mask2, thr_val = threshold_algorithm.calculate_mask( - image, None, self.new_parameters["threshold"]["values"], operator.le + image, None, self.new_parameters.threshold.values, operator.le ) if thr_val < min_val: return mask @@ -453,39 +420,60 @@ def _threshold_and_exclude(self, image, report_fun): return self._threshold_image(image) +class CellFromNucleusFlowParameters(BaseModel): + nucleus_channel: Channel = Field(0, title="Nucleus Channel") + nucleus_noise_filtering: NoiseFilterSelection = Field(NoiseFilterSelection.get_default(), title="Filter") + nucleus_threshold: ThresholdSelection = Field(ThresholdSelection.get_default(), title="Threshold") + cell_channel: Channel = Field(0, title="Cell Channel") + cell_noise_filtering: NoiseFilterSelection = Field(NoiseFilterSelection.get_default(), title="Filter") + cell_threshold: ThresholdSelection = Field(ThresholdSelection.get_default(), title="Threshold") + flow_type: FlowMethodSelection = Field(FlowMethodSelection.get_default(), title="Flow type") + close_holes: bool = Field(True, title="Fill holes") + close_holes_size: int = Field(200, title="Maximum holes size (px)", ge=0, le=10**5) + smooth_border: SmoothAlgorithmSelection = Field(SmoothAlgorithmSelection.get_default(), title="Smooth borders") + side_connection: bool = Field( + False, + title="Side by Side connections", + description="During calculation of connected components includes only side by side connected pixels", + ) + minimum_size: int = Field(8000, ge=20, le=10**6) + use_convex: int = Field(False, title="Use convex hull") + + class CellFromNucleusFlow(StackAlgorithm): + __argument_class__ = CellFromNucleusFlowParameters + new_parameters: CellFromNucleusFlowParameters + def calculation_run(self, report_fun: Callable[[str, int], None]) -> ROIExtractionResult: report_fun("Nucleus noise removal", 0) nucleus_channel = self.get_noise_filtered_channel( - self.new_parameters["nucleus_channel"], self.new_parameters["nucleus_noise_filtering"] + self.new_parameters.nucleus_channel, self.new_parameters.nucleus_noise_filtering ) report_fun("Nucleus threshold apply", 1) - nucleus_mask, _nucleus_thr = threshold_dict[self.new_parameters["nucleus_threshold"]["name"]].calculate_mask( - nucleus_channel, self.mask, self.new_parameters["nucleus_threshold"]["values"], operator.ge + nucleus_mask, _nucleus_thr = ThresholdSelection[self.new_parameters.nucleus_threshold.name].calculate_mask( + nucleus_channel, self.mask, self.new_parameters.nucleus_threshold.values, operator.ge ) report_fun("Nucleus calculate", 2) nucleus_objects = sitk.GetArrayFromImage( sitk.RelabelComponent( - sitk.ConnectedComponent( - sitk.GetImageFromArray(nucleus_mask), not self.new_parameters["side_connection"] - ), + sitk.ConnectedComponent(sitk.GetImageFromArray(nucleus_mask), not self.new_parameters.side_connection), 20, ) ) sizes = np.bincount(nucleus_objects.flat) - ind = bisect(sizes[1:], self.new_parameters["minimum_size"], lambda x, y: x > y) + ind = bisect(sizes[1:], self.new_parameters.minimum_size, lambda x, y: x > y) nucleus_objects[nucleus_objects > ind] = 0 report_fun("Cell noise removal", 3) cell_channel = self.get_noise_filtered_channel( - self.new_parameters["cell_channel"], self.new_parameters["cell_noise_filtering"] + self.new_parameters.cell_channel, self.new_parameters.cell_noise_filtering ) report_fun("Cell threshold apply", 4) - cell_mask, cell_thr = threshold_dict[self.new_parameters["cell_threshold"]["name"]].calculate_mask( - cell_channel, self.mask, self.new_parameters["cell_threshold"]["values"], operator.ge + cell_mask, cell_thr = ThresholdSelection[self.new_parameters.cell_threshold.name].calculate_mask( + cell_channel, self.mask, self.new_parameters.cell_threshold.values, operator.ge ) report_fun("Flow calculation", 5) - sprawl_algorithm: BaseWatershed = flow_dict[self.new_parameters["flow_type"]["name"]] + sprawl_algorithm: BaseWatershed = FlowMethodSelection[self.new_parameters.flow_type.name] mean_brightness = np.mean(cell_channel[cell_mask > 0]) if mean_brightness < cell_thr: mean_brightness = cell_thr + 10 @@ -495,17 +483,17 @@ def calculation_run(self, report_fun: Callable[[str, int], None]) -> ROIExtracti cell_channel, ind, self.image.spacing, - self.new_parameters["side_connection"], + self.new_parameters.side_connection, operator.gt, - self.new_parameters["flow_type"]["values"], + self.new_parameters.flow_type.values, cell_thr, mean_brightness, ) report_fun("Smooth border", 6) - segmentation = smooth_dict[self.new_parameters["smooth_border"]["name"]].smooth( - segmentation, self.new_parameters["smooth_border"]["values"] + segmentation = SmoothAlgorithmSelection[self.new_parameters.smooth_border.name].smooth( + segmentation, self.new_parameters.smooth_border.values ) - if self.new_parameters["use_convex"]: + if self.new_parameters.use_convex: report_fun("convex hull", 7) segmentation = convex_fill(segmentation) report_fun("Calculation done", 8) @@ -524,66 +512,6 @@ def get_info_text(self): def get_steps_num(): return 9 - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("nucleus_channel", "Nucleus Channel", 0, value_type=Channel), - AlgorithmProperty( - "nucleus_noise_filtering", - "Filter", - noise_filtering_dict.get_default(), - possible_values=noise_filtering_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty( - "nucleus_threshold", - "Threshold", - threshold_dict.get_default(), - possible_values=threshold_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty("cell_channel", "Cell Channel", 0, value_type=Channel), - AlgorithmProperty( - "cell_noise_filtering", - "Filter", - noise_filtering_dict.get_default(), - possible_values=noise_filtering_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty( - "cell_threshold", - "Threshold", - threshold_dict.get_default(), - possible_values=threshold_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty( - "flow_type", - "Flow type", - flow_dict.get_default(), - possible_values=flow_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty("close_holes", "Fill holes", True, (True, False)), - AlgorithmProperty("close_holes_size", "Maximum holes size (px)", 200, (0, 10**5), 10), - AlgorithmProperty( - "smooth_border", - "Smooth borders", - smooth_dict.get_default(), - possible_values=smooth_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty( - "side_connection", - "Side by Side connections", - False, - (True, False), - help_text="During calculation of connected components includes only side by side connected pixels", - ), - AlgorithmProperty("minimum_size", "Minimum size", 8000, (20, 10**6), 1000), - AlgorithmProperty("use_convex", "Use convex hull", False, (True, False)), - ] - @classmethod def get_name(cls) -> str: return "Cell from nucleus flow" @@ -592,7 +520,7 @@ def get_name(cls) -> str: final_algorithm_list = [ ThresholdAlgorithm, ThresholdFlowAlgorithm, - MorphologicalWatersheed, + MorphologicalWatershed, ThresholdPreview, AutoThresholdAlgorithm, CellFromNucleusFlow, diff --git a/package/PartSegCore/segmentation/threshold.py b/package/PartSegCore/segmentation/threshold.py index e644f7ad2..ad14c0584 100644 --- a/package/PartSegCore/segmentation/threshold.py +++ b/package/PartSegCore/segmentation/threshold.py @@ -1,63 +1,81 @@ import typing +import warnings from abc import ABC import numpy as np import SimpleITK as sitk +from pydantic import Field -from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty, Register +from PartSegCore.class_register import register_class, rename_key, update_argument +from PartSegCore.utils import BaseModel + +from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmSelection from .algorithm_base import SegmentationLimitException +class SingleThresholdParams(BaseModel): + threshold: float = Field(8000.0, ge=-100000, le=100000, title="Threshold", description="Threshold values") + + +@register_class(version="0.0.1", migrations=[("0.0.1", rename_key("masked", "apply_mask"))]) +class SimpleITKThresholdParams128(BaseModel): + apply_mask: bool = Field(True, description="If apply mask before calculate threshold") + bins: int = Field(128, title="Histogram bins", ge=8, le=2**16) + + +@register_class(version="0.0.1", migrations=[("0.0.1", rename_key("masked", "apply_mask"))]) +class SimpleITKThresholdParams256(BaseModel): + apply_mask: bool = Field(True, description="If apply mask before calculate threshold") + bins: int = Field(128, title="Histogram bins", ge=8, le=2**16) + + class BaseThreshold(AlgorithmDescribeBase, ABC): @classmethod def calculate_mask( cls, data: np.ndarray, mask: typing.Optional[np.ndarray], - arguments: dict, + arguments: BaseModel, operator: typing.Callable[[object, object], bool], ): raise NotImplementedError() class ManualThreshold(BaseThreshold): + __argument_class__ = SingleThresholdParams + @classmethod def get_name(cls): return "Manual" @classmethod - def get_fields(cls): - return [AlgorithmProperty("threshold", "Threshold", 8000.0, (-100000, 100000))] - - @classmethod - def calculate_mask(cls, data: np.ndarray, mask: typing.Optional[np.ndarray], arguments: dict, operator): - result = np.array(operator(data, arguments["threshold"])).astype(np.uint8) + @update_argument("arguments") + def calculate_mask( + cls, data: np.ndarray, mask: typing.Optional[np.ndarray], arguments: SingleThresholdParams, operator + ): + result = np.array(operator(data, arguments.threshold)).astype(np.uint8) if mask is not None: result[mask == 0] = 0 - return result, arguments["threshold"] + return result, arguments.threshold class SitkThreshold(BaseThreshold, ABC): - bins_num = 128 - - @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("masked", "Apply mask", True), - AlgorithmProperty("bins", "histogram bins", cls.bins_num, (8, 2**16)), - ] + __argument_class__ = SimpleITKThresholdParams128 @classmethod - def calculate_mask(cls, data: np.ndarray, mask: typing.Optional[np.ndarray], arguments: dict, operator): + @update_argument("arguments") + def calculate_mask( + cls, data: np.ndarray, mask: typing.Optional[np.ndarray], arguments: SimpleITKThresholdParams128, operator + ): if mask is not None and mask.dtype != np.uint8: mask = (mask > 0).astype(np.uint8) ob, bg, th_op = (0, 1, np.min) if operator(1, 0) else (1, 0, np.max) image_sitk = sitk.GetImageFromArray(data) - if arguments["masked"] and mask is not None: + if arguments.apply_mask and mask is not None: mask_sitk = sitk.GetImageFromArray(mask) - calculated = cls.calculate_threshold(image_sitk, mask_sitk, ob, bg, arguments["bins"], True, 1) + calculated = cls.calculate_threshold(image_sitk, mask_sitk, ob, bg, arguments.bins, True, 1) else: - calculated = cls.calculate_threshold(image_sitk, ob, bg, arguments["bins"]) + calculated = cls.calculate_threshold(image_sitk, ob, bg, arguments.bins) result = sitk.GetArrayFromImage(calculated) if mask is not None: result[mask == 0] = 0 @@ -80,7 +98,7 @@ def calculate_threshold(*args, **kwargs): class LiThreshold(SitkThreshold): - bins_num = 256 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -92,7 +110,7 @@ def calculate_threshold(*args, **kwargs): class MaximumEntropyThreshold(SitkThreshold): - bins_num = 256 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -104,7 +122,7 @@ def calculate_threshold(*args, **kwargs): class RenyiEntropyThreshold(SitkThreshold): - bins_num = 256 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -116,7 +134,7 @@ def calculate_threshold(*args, **kwargs): class ShanbhagThreshold(SitkThreshold): - bins_num = 256 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -128,7 +146,7 @@ def calculate_threshold(*args, **kwargs): class TriangleThreshold(SitkThreshold): - bins_num = 256 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -140,7 +158,7 @@ def calculate_threshold(*args, **kwargs): class YenThreshold(SitkThreshold): - bins_num = 256 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -152,7 +170,7 @@ def calculate_threshold(*args, **kwargs): class HuangThreshold(SitkThreshold): - bins_num = 128 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -164,7 +182,7 @@ def calculate_threshold(*args, **kwargs): class IntermodesThreshold(SitkThreshold): - bins_num = 256 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -181,7 +199,7 @@ def calculate_threshold(*args, **kwargs): class IsoDataThreshold(SitkThreshold): - bins_num = 256 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -193,7 +211,7 @@ def calculate_threshold(*args, **kwargs): class KittlerIllingworthThreshold(SitkThreshold): - bins_num = 256 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -210,7 +228,7 @@ def calculate_threshold(*args, **kwargs): class MomentsThreshold(SitkThreshold): - bins_num = 256 + __argument_class__ = SimpleITKThresholdParams256 @classmethod def get_name(cls): @@ -221,88 +239,109 @@ def calculate_threshold(*args, **kwargs): return sitk.MomentsThreshold(*args) -threshold_dict = Register() -threshold_dict.register(ManualThreshold) -threshold_dict.register(OtsuThreshold) -threshold_dict.register(LiThreshold) -threshold_dict.register(RenyiEntropyThreshold) -threshold_dict.register(ShanbhagThreshold) -threshold_dict.register(TriangleThreshold) -threshold_dict.register(YenThreshold) -threshold_dict.register(HuangThreshold) -threshold_dict.register(IntermodesThreshold) -threshold_dict.register(IsoDataThreshold) -threshold_dict.register(KittlerIllingworthThreshold) -threshold_dict.register(MomentsThreshold) -threshold_dict.register(MaximumEntropyThreshold) +class ThresholdSelection(AlgorithmSelection, class_methods=["calculate_mask"], suggested_base_class=BaseThreshold): + pass + + +ThresholdSelection.register(ManualThreshold) +ThresholdSelection.register(OtsuThreshold) +ThresholdSelection.register(LiThreshold) +ThresholdSelection.register(RenyiEntropyThreshold) +ThresholdSelection.register(ShanbhagThreshold) +ThresholdSelection.register(TriangleThreshold) +ThresholdSelection.register(YenThreshold) +ThresholdSelection.register(HuangThreshold) +ThresholdSelection.register(IntermodesThreshold) +ThresholdSelection.register(IsoDataThreshold) +ThresholdSelection.register(KittlerIllingworthThreshold) +ThresholdSelection.register(MomentsThreshold) +ThresholdSelection.register(MaximumEntropyThreshold) + + +class DoubleThresholdParams(BaseModel): + core_threshold: ThresholdSelection = ThresholdSelection.get_default() + base_threshold: ThresholdSelection = ThresholdSelection.get_default() class DoubleThreshold(BaseThreshold): + __argument_class__ = DoubleThresholdParams + @classmethod def get_name(cls): # return "Double Choose" return "Base/Core" @classmethod - def get_fields(cls): - return [ - AlgorithmProperty( - "core_threshold", - "Core threshold", - threshold_dict.get_default(), - possible_values=threshold_dict, - value_type=AlgorithmDescribeBase, - ), - AlgorithmProperty( - "base_threshold", - "Base threshold", - threshold_dict.get_default(), - possible_values=threshold_dict, - value_type=AlgorithmDescribeBase, - ), - ] - - @classmethod - def calculate_mask(cls, data: np.ndarray, mask: typing.Optional[np.ndarray], arguments: dict, operator): - thr: BaseThreshold = threshold_dict[arguments["core_threshold"]["name"]] - mask1, thr_val1 = thr.calculate_mask(data, mask, arguments["core_threshold"]["values"], operator) + @update_argument("arguments") + def calculate_mask( + cls, data: np.ndarray, mask: typing.Optional[np.ndarray], arguments: DoubleThresholdParams, operator + ): + thr: BaseThreshold = threshold_dict[arguments.core_threshold.name] + mask1, thr_val1 = thr.calculate_mask(data, mask, arguments.core_threshold.values, operator) - thr: BaseThreshold = threshold_dict[arguments["base_threshold"]["name"]] - mask2, thr_val2 = thr.calculate_mask(data, mask, arguments["base_threshold"]["values"], operator) + thr: BaseThreshold = threshold_dict[arguments.base_threshold.name] + mask2, thr_val2 = thr.calculate_mask(data, mask, arguments.base_threshold.values, operator) mask2[mask2 > 0] = 1 mask2[mask1 > 0] = 2 return mask2, (thr_val1, thr_val2) +@register_class(version="0.0.1", migrations=[("0.0.1", rename_key("hist_num", "bins"))]) +class DoubleOtsuParams(BaseModel): + valley: bool = Field(True, title="Valley emphasis") + bins: int = Field(128, title="Histogram bins", ge=8, le=2**16) + + class DoubleOtsu(BaseThreshold): + __argument_class__ = DoubleOtsuParams + @classmethod def get_name(cls): return "Double Otsu" @classmethod - def get_fields(cls): - return [ # AlgorithmProperty("mask", "Use mask in calculation", True), - AlgorithmProperty("valley", "Valley emphasis", True), - AlgorithmProperty("hist_num", "Histogram bins", 128, (8, 2**16)), - ] - - @classmethod + @update_argument("arguments") def calculate_mask( cls, data: np.ndarray, mask: typing.Optional[np.ndarray], - arguments: dict, + arguments: DoubleOtsuParams, operator: typing.Callable[[object, object], bool], ): cleaned_image_sitk = sitk.GetImageFromArray(data) - res = sitk.OtsuMultipleThresholds(cleaned_image_sitk, 2, 0, arguments["hist_num"], arguments["valley"]) + res = sitk.OtsuMultipleThresholds(cleaned_image_sitk, 2, 0, arguments.bins, arguments.valley) res = sitk.GetArrayFromImage(res) thr1 = data[res == 2].min() thr2 = data[res == 1].min() return res, (thr1, thr2) -double_threshold_dict = Register() +class DoubleThresholdSelection( + AlgorithmSelection, class_methods=["calculate_mask"], suggested_base_class=BaseThreshold +): + pass + + +DoubleThresholdSelection.register(DoubleThreshold) +DoubleThresholdSelection.register(DoubleOtsu) + +double_threshold_dict = DoubleThresholdSelection.__register__ +threshold_dict = ThresholdSelection.__register__ + + +def __getattr__(name): # pragma: no cover + if name == "threshold_dict": + warnings.warn( + "threshold_dict is deprecated. Please use ThresholdSelection instead", category=FutureWarning, stacklevel=2 + ) + return ThresholdSelection.__register__ + + if name == "double_threshold_dict": + warnings.warn( + "double_threshold_dict is deprecated. Please use DoubleThresholdSelection instead", + category=FutureWarning, + stacklevel=2, + ) + return DoubleThresholdSelection.__register__ -double_threshold_dict.register(DoubleThreshold) -double_threshold_dict.register(DoubleOtsu) + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/package/PartSegCore/segmentation/watershed.py b/package/PartSegCore/segmentation/watershed.py index 10aa49d5a..7191b5a39 100644 --- a/package/PartSegCore/segmentation/watershed.py +++ b/package/PartSegCore/segmentation/watershed.py @@ -1,13 +1,15 @@ """ -This module contains PartSeg wrapers for function for :py:mod:`..sprawl_utils.find_split`. +This module contains PartSeg wrappers for function for :py:mod:`..sprawl_utils.find_split`. """ +import warnings from abc import ABC from enum import Enum from typing import Any, Callable import numpy as np +from pydantic import Field -from PartSegCore.class_generator import enum_register +from PartSegCore.utils import BaseModel from PartSegCore_compiled_backend.multiscale_opening import MuType, PyMSO, calculate_mu from PartSegCore_compiled_backend.sprawl_utils.find_split import ( euclidean_sprawl, @@ -16,16 +18,15 @@ path_minimum_sprawl, ) -from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmProperty, Register +from ..algorithm_describe_base import AlgorithmDescribeBase, AlgorithmSelection +from ..class_register import update_argument from .algorithm_base import SegmentationLimitException class BaseWatershed(AlgorithmDescribeBase, ABC): """base class for all sprawl interface""" - @classmethod - def get_fields(cls): - return [] + __argument_class__ = BaseModel @classmethod def sprawl( @@ -52,8 +53,8 @@ def sprawl( :param side_connection: :param operator: :param arguments: dict with parameters reported by function :py:meth:`get_fields` - :param lower_bound: data value lower boud - :param upper_bound: data value upper boud + :param lower_bound: data value lower bound + :param upper_bound: data value upper bound :return: """ raise NotImplementedError() @@ -88,7 +89,7 @@ def sprawl( class DistanceWatershed(BaseWatershed): - """Calculate Euclidean sprawl (watersheed) with respect to image spacing""" + """Calculate Euclidean sprawl (watershed) with respect to image spacing""" @classmethod def get_name(cls): @@ -182,19 +183,20 @@ def sprawl( ) +class MSOWatershedParams(BaseModel): + step_limits: int = Field(100, ge=1, le=1000, title="Threshold", description="Limits of Steps") + reflective: bool = False + + class MSOWatershed(BaseWatershed): + __argument_class__ = MSOWatershedParams + @classmethod def get_name(cls): return "MultiScale Opening" @classmethod - def get_fields(cls): - return [ - AlgorithmProperty("step_limits", "Limits of Steps", 100, options_range=(1, 1000), value_type=int), - AlgorithmProperty("reflective", "Reflective", False, value_type=bool), - ] - - @classmethod + @update_argument("arguments") def sprawl( cls, sprawl_area: np.ndarray, @@ -204,7 +206,7 @@ def sprawl( spacing, side_connection: bool, operator: Callable[[Any, Any], bool], - arguments: dict, + arguments: MSOWatershedParams, lower_bound, upper_bound, ): @@ -222,11 +224,11 @@ def sprawl( mu_array = calculate_mu(data.copy("C"), lower_bound, upper_bound, MuType.base_mu) except OverflowError: raise SegmentationLimitException("Wrong range for ") - if arguments["reflective"]: + if arguments.reflective: mu_array[mu_array < 0.5] = 1 - mu_array[mu_array < 0.5] mso.set_mu_array(mu_array) try: - mso.run_MSO(arguments["step_limits"]) + mso.run_MSO(arguments.step_limits) except RuntimeError as e: if e.args[0] == "to many steps: constrained dilation": raise SegmentationLimitException(*e.args) @@ -238,18 +240,33 @@ def sprawl( return result -flow_dict = Register( - MSOWatershed, PathWatershed, DistanceWatershed, PathDistanceWatershed, FDTWatershed, class_methods=["sprawl"] -) -"""This register contains algorithms for sprawl area from core object.""" +class FlowMethodSelection(AlgorithmSelection, class_methods=["sprawl"], suggested_base_class=BaseWatershed): + """This register contains algorithms for sprawl area from core object.""" -sprawl_dict = flow_dict + +FlowMethodSelection.register(MSOWatershed, old_names=["MultiScale Opening sprawl"]) +FlowMethodSelection.register(PathWatershed, old_names=["Path sprawl"]) +FlowMethodSelection.register(DistanceWatershed, old_names=["Euclidean sprawl"]) +FlowMethodSelection.register(PathDistanceWatershed, old_names=["Path euclidean sprawl"]) +FlowMethodSelection.register(FDTWatershed, old_names=["Fuzzy distance sprawl"]) + + +def __getattr__(name): # pragma: no cover + if name == "flow_dict": + warnings.warn( + "flow_dict is deprecated. Please use FlowMethodSelection instead", category=FutureWarning, stacklevel=2 + ) + return FlowMethodSelection.__register__ + if name == "sprawl_dict": + warnings.warn( + "sprawl_dict is deprecated. Please use FlowMethodSelection instead", category=FutureWarning, stacklevel=2 + ) + return FlowMethodSelection.__register__ + raise AttributeError(f"module {__name__} has no attribute {name}") def get_neigh(sides): - if sides: - return NeighType.sides - return NeighType.edges + return NeighType.sides if sides else NeighType.edges class NeighType(Enum): @@ -261,18 +278,10 @@ def __str__(self): return self.name -try: - # noinspection PyUnresolvedReferences,PyUnboundLocalVariable - reloading -except NameError: - reloading = False # means the module is being imported firs time - enum_register.register_class(NeighType) - - def calculate_distances_array(spacing, neigh_type: NeighType): """ :param spacing: image spacing - :param neigh_type: neigbourhood type + :param neigh_type: neighbourhood type :return: neighbourhood array, distance array """ min_dist = min(spacing) diff --git a/package/PartSegCore/sphinx/auto_parameters.py b/package/PartSegCore/sphinx/auto_parameters.py index c2a133db5..78fc33672 100644 --- a/package/PartSegCore/sphinx/auto_parameters.py +++ b/package/PartSegCore/sphinx/auto_parameters.py @@ -14,16 +14,13 @@ # noinspection PyUnusedLocal def algorithm_parameters_doc(app: Sphinx, what, name: str, obj, options, lines: list): if inspect.isclass(obj) and issubclass(obj, AlgorithmDescribeBase) and not inspect.isabstract(obj): - fields = [x for x in obj.get_fields() if isinstance(x, AlgorithmProperty)] + fields = [x for x in obj._get_fields() if isinstance(x, AlgorithmProperty)] # pylint: disable=W0212 if fields: lines.extend(["", "This algorithm has following parameters:", ""]) for el in fields: if el.help_text: - lines.append( - "- **{}** ({})- {}, {}".format( - el.name, extract_type_name(el.value_type), el.user_name, el.help_text - ) - ) + lines.append(f"- **{el.name}** ({extract_type_name(el.value_type)})- {el.user_name}, {el.help_text}") + else: lines.append(f"- **{el.name}** ({extract_type_name(el.value_type)})- {el.user_name}") @@ -47,7 +44,7 @@ def add_content(self, more_content: Any, no_docstring: bool = False) -> None: k = 0 if self.object.methods: self.add_line( - "Need methods: {}".format(", ".join("``" + x + "``" for x in self.object.methods)), + f'Need methods: {", ".join(f"``{x}``" for x in self.object.methods)}', source, k, ) @@ -56,7 +53,7 @@ def add_content(self, more_content: Any, no_docstring: bool = False) -> None: k += 2 if self.object.class_methods: self.add_line( - "Need class methods: {}".format(", ".join("``" + x + "``" for x in self.object.class_methods)), + f'Need class methods: {", ".join(f"``{x}``" for x in self.object.class_methods)}', source, k, ) diff --git a/package/PartSegCore/universal_const.py b/package/PartSegCore/universal_const.py index c9c8f29d1..0e12e4e21 100644 --- a/package/PartSegCore/universal_const.py +++ b/package/PartSegCore/universal_const.py @@ -1,9 +1,10 @@ from enum import Enum -from PartSegCore.class_generator import enum_register +from PartSegCore.class_register import register_class # noinspection NonAsciiCharacters +@register_class(old_paths=["PartSeg.utils.universal_const.Units"]) class Units(Enum): mm = 0 µm = 1 @@ -14,7 +15,5 @@ def __str__(self): return self.name.replace("_", " ") -enum_register.register_class(Units) - _UNITS_LIST = ["mm", "µm", "nm", "pm"] UNIT_SCALE = [10**3, 10**6, 10**9, 10**12] diff --git a/package/PartSegCore/utils.py b/package/PartSegCore/utils.py index 08adee1e5..eee4ec9ce 100644 --- a/package/PartSegCore/utils.py +++ b/package/PartSegCore/utils.py @@ -1,13 +1,22 @@ +import copy import inspect +import itertools import typing import weakref from abc import ABC, abstractmethod +from collections import defaultdict +from contextlib import suppress from types import MethodType import numpy as np +from pydantic import BaseModel as PydanticBaseModel __author__ = "Grzegorz Bokota" +from psygnal import Signal + +from PartSegCore.class_register import register_class + def bisect(arr, val, comp): left = -1 @@ -108,3 +117,294 @@ def get_callback(callback: typing.Union[typing.Callable, MethodType], max_args=N return CallbackMethod(callback, max_args) return CallbackFun(callback, max_args) + + +@register_class(old_paths=["PartSegCore.json_hooks.EventedDict"]) +class EventedDict(typing.MutableMapping): + setted = Signal(str) + deleted = Signal(str) + + def __deepcopy__(self, memodict=None): + if memodict is None: + memodict = {} + cls = self.__class__ + result = cls(**copy.deepcopy(self._dict)) + result.base_key = self.base_key + memodict[id(self)] = result + return result + + def __init__(self, **kwargs): + # TODO add positional only argument when drop python 3.7 + super().__init__() + self._dict = {} + for key, dkt in kwargs.items(): + if isinstance(dkt, dict): + dkt = EventedDict(**dkt) + if isinstance(dkt, EventedDict): + dkt.base_key = key + dkt.setted.connect(self._propagate_setitem) + dkt.deleted.connect(self._propagate_del) + self._dict[key] = dkt + self.base_key = "" + + def __setitem__(self, k, v) -> None: + if isinstance(v, dict): + v = EventedDict(**v) + if isinstance(v, EventedDict): + v.base_key = k + v.setted.connect(self._propagate_setitem) + v.deleted.connect(self._propagate_del) + if k in self._dict and isinstance(self._dict[k], EventedDict): + self._dict[k].setted.disconnect(self._propagate_setitem) + self._dict[k].deleted.disconnect(self._propagate_del) + old_value = self._dict[k] if k in self._dict else None + with suppress(ValueError): + if old_value == v: + return + self._dict[k] = v + self.setted.emit(k) + + def __delitem__(self, k) -> None: + if k in self._dict and isinstance(self._dict[k], EventedDict): + self._dict[k].setted.disconnect(self._propagate_setitem) + self._dict[k].deleted.disconnect(self._propagate_del) + del self._dict[k] + self.deleted.emit(k) + + def __getitem__(self, k): + return self._dict[k] + + def __len__(self) -> int: + return len(self._dict) + + def __iter__(self) -> typing.Iterator: + return iter(self._dict) + + def as_dict(self): + return copy.copy(self._dict) + + def as_dict_deep(self): + return {k: v.as_dict_deep() if isinstance(v, EventedDict) else v for k, v in self._dict.items()} + + def __str__(self): + return f"EventedDict({self._dict})" + + def __repr__(self): + return f"EventedDict({repr(self._dict)})" + + def _propagate_setitem(self, key): + # Fixme when partial disconnect will work + sender: EventedDict = Signal.sender() + if sender.base_key: + self.setted.emit(f"{sender.base_key}.{key}") + else: + self.setted.emit(key) + + def _propagate_del(self, key): + # Fixme when partial disconnect will work + sender: EventedDict = Signal.sender() + if sender.base_key: + self.deleted.emit(f"{sender.base_key}.{key}") + else: + self.deleted.emit(key) + + +def recursive_update_dict(main_dict: typing.Union[dict, EventedDict], other_dict: typing.Union[dict, EventedDict]): + """ + recursive update main_dict with elements of other_dict + + :param main_dict: dict to be updated recursively + :param other_dict: source dict + + >>> dkt1 = {"test": {"test": 1, "test2": {"test4": 1}}} + >>> dkt2 = {"test": {"test": 4, "test2": {"test2": 1}}} + >>> recursive_update_dict(dkt1, dkt2) + >>> dkt1 + {"test": {"test": 1, "test2": {"test4": 1, "test2": 1}}} + """ + for key, val in other_dict.items(): + if key in main_dict and isinstance(main_dict[key], dict) and isinstance(val, dict): + recursive_update_dict(main_dict[key], val) + else: + main_dict[key] = val + + +@register_class(old_paths=["PartSegCore.json_hooks.ProfileDict"]) +class ProfileDict: + """ + Dict for storing recursive data. The path are dot separated. + + >>> dkt = ProfileDict() + >>> dkt.set(["aa", "bb", "c1"], 7) + >>> dkt.get(["aa", "bb", "c1"]) + 7 + >>> dkt.get("aa.bb.c2", 8) + 8 + >>> dkt.get("aa.bb") + {'c1': 7, 'c2': 8} + """ + + def __init__(self, **kwargs): + self._my_dict = EventedDict(**kwargs) + self._callback_dict: typing.Dict[str, typing.List[CallbackBase]] = defaultdict(list) + + self._my_dict.setted.connect(self._call_callback) + self._my_dict.deleted.connect(self._call_callback) + + def as_dict(self): + return self.my_dict.as_dict() + + @property + def my_dict(self) -> EventedDict: + return self._my_dict + + @my_dict.setter + def my_dict(self, value: typing.Union[dict, EventedDict]): + if isinstance(value, dict): + value = EventedDict(**value) + + self._my_dict.setted.disconnect(self._call_callback) + self._my_dict.deleted.disconnect(self._call_callback) + self._my_dict = value + self._my_dict.setted.connect(self._call_callback) + self._my_dict.deleted.connect(self._call_callback) + + def update(self, ob: typing.Union["ProfileDict", dict, None] = None, **kwargs): + """ + Update dict recursively. Use :py:func:`~.recursive_update_dict` + + :param ob: data source + :param kwargs: data source, as keywords + """ + if isinstance(ob, ProfileDict): + recursive_update_dict(self.my_dict, ob.my_dict) + recursive_update_dict(self.my_dict, kwargs) + elif isinstance(ob, dict): + recursive_update_dict(self.my_dict, ob) + recursive_update_dict(self.my_dict, kwargs) + elif ob is None: + recursive_update_dict(self.my_dict, kwargs) + + def profile_change(self): + for callback in itertools.chain(*self._callback_dict.values()): + callback() + + def connect( + self, key_path: typing.Union[typing.Sequence[str], str], callback: typing.Callable[[], typing.Any], maxargs=None + ) -> typing.Callable: + """ + Connect function to receive information when object on path was changed using :py:meth:`.set` + + :param key_path: path for which signal should be emitted + :param callback: parameterless function which should be called + + :return: callback function itself. + """ + if not isinstance(key_path, str): + key_path = ".".join(key_path) + + self._callback_dict[key_path].append(get_callback(callback, maxargs)) + return callback + + def set(self, key_path: typing.Union[typing.Sequence[str], str], value): + """ + Set value from dict + + :param key_path: Path to element. If is string then will be split on '.' (`key_path.split('.')`) + :param value: Value to set. + """ + if isinstance(key_path, str): + key_path = key_path.split(".") + curr_dict = self.my_dict + + for i, key in enumerate(key_path[:-1]): + try: + # TODO add check if next step element is dict and create custom information + curr_dict = curr_dict[key] + except KeyError: + for key2 in key_path[i:-1]: + with curr_dict.setted.blocked(): + curr_dict[key2] = EventedDict() + curr_dict = curr_dict[key2] + break + if isinstance(value, dict): + value = EventedDict(**value) + curr_dict[key_path[-1]] = value + return value + + def _call_callback(self, key_path: typing.Union[typing.Sequence[str], str]): + if isinstance(key_path, str): + key_path = key_path.split(".") + full_path = ".".join(key_path[1:]) + callback_path = "" + callback_list = [] + if callback_path in self._callback_dict: + callback_list = self._callback_dict[callback_path] + + for callback_path in itertools.accumulate(key_path[1:], lambda x, y: f"{x}.{y}"): + if callback_path in self._callback_dict: + li = self._callback_dict[callback_path] + li = [x for x in li if x.is_alive()] + self._callback_dict[callback_path] = li + callback_list.extend(li) + for callback in callback_list: + callback(full_path) + + def get(self, key_path: typing.Union[list, str], default=None): + """ + Get value from dict. + + :param key_path: Path to element. If is string then will be split on . (`key_path.split('.')`). + :param default: default value if element missed in dict. + :raise KeyError: on missed element if default is not provided. + :return: requested value + """ + if isinstance(key_path, str): + key_path = key_path.split(".") + curr_dict = self.my_dict + for key in key_path: + try: + curr_dict = curr_dict[key] + except KeyError as e: + if default is None: + raise e + + val = copy.deepcopy(default) + return self.set(key_path, val) + + return curr_dict + + def verify_data(self) -> bool: + """ + Call :py:func:`~.check_loaded_dict` on inner structures + """ + return check_loaded_dict(self.my_dict) + + def filter_data(self): + error_list = [] + for group, up_dkt in list(self.my_dict.items()): + if not isinstance(up_dkt, (dict, EventedDict)): + continue + for key, dkt in list(up_dkt.items()): + if not check_loaded_dict(dkt): + error_list.append(f"{group}.{key}") + del up_dkt[key] + return error_list + + +def check_loaded_dict(dkt) -> bool: + """ + Recursive check if dict `dkt` or any sub dict contains '__error__' key. + + :param dkt: dict to check + """ + if not isinstance(dkt, (dict, EventedDict)): + return True + if "__error__" in dkt: + return False + return all(check_loaded_dict(val) for val in dkt.values()) + + +class BaseModel(PydanticBaseModel): + class Config: + extra = "forbid" diff --git a/package/PartSegImage/__init__.py b/package/PartSegImage/__init__.py index b6f9c7acc..8bc8ca24f 100644 --- a/package/PartSegImage/__init__.py +++ b/package/PartSegImage/__init__.py @@ -2,6 +2,7 @@ import sys from . import tifffile_fixes # noqa: F401 +from .channel_class import Channel from .image import Image from .image_reader import ( CziImageReader, @@ -15,6 +16,7 @@ __all__ = ( "BaseImageWriter", + "Channel", "Image", "TiffImageReader", "IMAGEJImageWriter", diff --git a/package/PartSegImage/channel_class.py b/package/PartSegImage/channel_class.py new file mode 100644 index 000000000..779b53851 --- /dev/null +++ b/package/PartSegImage/channel_class.py @@ -0,0 +1,51 @@ +from typing import Union + + +def check_type(value): + if isinstance(value, Channel): + return value + if value.__class__.__module__.startswith("napari"): + value = value.name + if not isinstance(value, (str, int)): + raise TypeError(f"Channel need to be int or str, provided {type(value)}") + return Channel(value) + + +class Channel: + """ + This class is introduced to distinguish numerical algorithm parameter from choose chanel. + In autogenerated interface field with this type limits input values to number of current image channels + """ + + def __init__(self, value: Union[str, int]): + if isinstance(value, Channel): + value = value.value + assert isinstance(value, (str, int)), f"wrong type {value} {type(value)}" # nosec + self._value: Union[str, int] = value + + @property + def value(self) -> Union[str, int]: + """Value stored in this class""" + return self._value + + def __str__(self): + return str(self._value + 1) if isinstance(self._value, int) else self._value + + def __repr__(self): + return f"<{self.__class__.__module__}.{self.__class__.__name__}(value={repr(self._value)})>" + + def __eq__(self, other): + return isinstance(other, Channel) and self._value == other.value + + def __hash__(self): + return hash(self._value) + + @classmethod + def __get_validators__(cls): + yield check_type + + @classmethod + def __modify_schema__(cls, field_schema): + field_schema["title"] = "Channel" + field_schema["type"] = "object" + field_schema["properties"] = {"value": {"title": "value", "anyOf": [{"type": "string"}, {"type": "integer"}]}} diff --git a/package/PartSegImage/image.py b/package/PartSegImage/image.py index 54bd04987..a28ea1996 100644 --- a/package/PartSegImage/image.py +++ b/package/PartSegImage/image.py @@ -7,6 +7,8 @@ import numpy as np +from PartSegImage import Channel + Spacing = typing.Tuple[typing.Union[float, int], ...] _IMAGE_DATA = typing.Union[typing.List[np.ndarray], np.ndarray] @@ -520,6 +522,8 @@ def get_data_by_axis(self, **kwargs) -> np.ndarray: kwargs["C"] = kwargs.pop("c") channel = kwargs.pop("C", slice(None) if "C" in self.axis_order else 0) + if isinstance(channel, Channel): + channel = channel.value axis_order = self.axis_order for name, value in kwargs.items(): @@ -529,6 +533,8 @@ def get_data_by_axis(self, **kwargs) -> np.ndarray: axis_order = axis_order.replace(name.upper(), "") slices_t = tuple(slices) + if isinstance(channel, str): + channel = self._channel_names.index(channel) if isinstance(channel, int): return self._channel_arrays[channel][slices_t] return np.stack([x[slices_t] for x in self._channel_arrays[channel]], axis=axis_order.index("C")) diff --git a/package/tests/conftest.py b/package/tests/conftest.py index 7982aa443..f8aedd547 100644 --- a/package/tests/conftest.py +++ b/package/tests/conftest.py @@ -14,7 +14,6 @@ from PartSegCore.analysis import ProjectTuple from PartSegCore.analysis.measurement_base import AreaType, MeasurementEntry, PerComponent from PartSegCore.analysis.measurement_calculation import ComponentsNumber, MeasurementProfile, Volume -from PartSegCore.image_operations import RadiusType from PartSegCore.mask.io_functions import MaskProjectTuple from PartSegCore.mask_create import MaskProperty from PartSegCore.roi_info import ROIInfo @@ -161,7 +160,7 @@ def stack_segmentation2(stack_image: MaskProjectTuple, mask_segmentation_paramet for i, (x, y) in enumerate(itertools.product([0, 20], repeat=2), start=1): data[3:-3, x + 4 : x + 16, y + 4 : y + 16] = i data = ROIInfo(stack_image.image.fit_array_to_image(data)) - mask_segmentation_parameters.values["threshold"]["values"]["threshold"] = 110 + mask_segmentation_parameters.values.threshold.values.threshold = 110 parameters = {i: deepcopy(mask_segmentation_parameters) for i in range(1, 5)} return dataclasses.replace( stack_image, roi_info=data, roi_extraction_parameters=parameters, selected_components=[1, 3] @@ -170,27 +169,42 @@ def stack_segmentation2(stack_image: MaskProjectTuple, mask_segmentation_paramet @pytest.fixture def mask_property(): - return MaskProperty(RadiusType.NO, 0, RadiusType.NO, 0, False, False, False) + return MaskProperty.simple_mask() @pytest.fixture def measurement_profiles(): statistics = [ MeasurementEntry( - "Segmentation Volume", - Volume.get_starting_leaf().replace_(area=AreaType.ROI, per_component=PerComponent.No), + name="Segmentation Volume", + calculation_tree=Volume.get_starting_leaf().replace_(area=AreaType.ROI, per_component=PerComponent.No), ), MeasurementEntry( - "ROI Components Number", - ComponentsNumber.get_starting_leaf().replace_(area=AreaType.ROI, per_component=PerComponent.No), + name="ROI Components Number", + calculation_tree=ComponentsNumber.get_starting_leaf().replace_( + area=AreaType.ROI, per_component=PerComponent.No + ), ), ] statistics2 = [ MeasurementEntry( - "Mask Volume", Volume.get_starting_leaf().replace_(area=AreaType.Mask, per_component=PerComponent.No) + name="Mask Volume", + calculation_tree=Volume.get_starting_leaf().replace_(area=AreaType.Mask, per_component=PerComponent.No), ), ] - return MeasurementProfile("statistic1", statistics), MeasurementProfile("statistic2", statistics + statistics2) + return MeasurementProfile(name="statistic1", chosen_fields=statistics), MeasurementProfile( + name="statistic2", chosen_fields=statistics + statistics2 + ) + + +@pytest.fixture +def clean_register(): + from PartSegCore.json_hooks import REGISTER + + old_dict = REGISTER._data_dkt + REGISTER._data_dkt = {} + yield + REGISTER._data_dkt = old_dict def pytest_collection_modifyitems(session, config, items): diff --git a/package/tests/test_PartSeg/roi_analysis/test_advanced_window.py b/package/tests/test_PartSeg/roi_analysis/test_advanced_window.py index 1c31c817f..e58e40321 100644 --- a/package/tests/test_PartSeg/roi_analysis/test_advanced_window.py +++ b/package/tests/test_PartSeg/roi_analysis/test_advanced_window.py @@ -1,7 +1,7 @@ from PartSeg._roi_analysis.advanced_window import Properties from PartSeg._roi_analysis.advanced_window import QInputDialog as advanced_module_input from PartSeg._roi_analysis.advanced_window import QMessageBox as advanced_message_box -from PartSegCore.analysis import analysis_algorithm_dict +from PartSegCore.analysis import AnalysisAlgorithmSelection class TestProperties: @@ -33,9 +33,9 @@ def test_pipeline_profile_show_info( with qtbot.waitSignal(widget.profile_list.currentItemChanged, timeout=10**4): widget.profile_list.setCurrentRow(1) profile = part_settings.roi_profiles[widget.profile_list.item(1).text()] - assert widget.info_label.toPlainText() == profile.pretty_print(analysis_algorithm_dict) + assert widget.info_label.toPlainText() == profile.pretty_print(AnalysisAlgorithmSelection) widget.pipeline_list.setCurrentRow(0) - assert widget.info_label.toPlainText() == sample_pipeline.pretty_print(analysis_algorithm_dict) + assert widget.info_label.toPlainText() == sample_pipeline.pretty_print(AnalysisAlgorithmSelection) widget.hide() def test_delete_profile(self, qtbot, part_settings, border_rim_profile, lower_threshold_profile): diff --git a/package/tests/test_PartSeg/test_common_backend.py b/package/tests/test_PartSeg/test_common_backend.py index 34ffdf3e9..e8ab98ef2 100644 --- a/package/tests/test_PartSeg/test_common_backend.py +++ b/package/tests/test_PartSeg/test_common_backend.py @@ -581,9 +581,14 @@ def test_base_settings_history(self, tmp_path, qtbot, monkeypatch): settings = base_settings.BaseSettings(tmp_path) assert settings.history_size() == 0 assert settings.history_redo_size() == 0 - hist_elem = HistoryElement({"a": 1}, None, MaskProperty.simple_mask(), BytesIO()) - hist_elem2 = HistoryElement({"a": 2}, None, MaskProperty.simple_mask(), BytesIO()) - hist_elem3 = HistoryElement({"a": 3}, None, MaskProperty.simple_mask(), BytesIO()) + hist_elem = HistoryElement( + roi_extraction_parameters={"a": 1}, + annotations=None, + mask_property=MaskProperty.simple_mask(), + arrays=BytesIO(), + ) + hist_elem2 = hist_elem.copy(update={"roi_extraction_parameters": {"a": 2}, "arrays": BytesIO()}) + hist_elem3 = hist_elem.copy(update={"roi_extraction_parameters": {"a": 3}, "arrays": BytesIO()}) settings.add_history_element(hist_elem) assert settings.history_size() == 1 assert settings.history_redo_size() == 0 diff --git a/package/tests/test_PartSeg/test_common_gui.py b/package/tests/test_PartSeg/test_common_gui.py index eb7cfbfdf..213257c03 100644 --- a/package/tests/test_PartSeg/test_common_gui.py +++ b/package/tests/test_PartSeg/test_common_gui.py @@ -1,7 +1,9 @@ # pylint: disable=R0201 +import datetime import os import platform import sys +import typing from enum import Enum from pathlib import Path from unittest.mock import MagicMock @@ -9,8 +11,11 @@ import numpy as np import pytest import qtpy +from magicgui.widgets import Widget +from pydantic import Field from qtpy.QtCore import QSize, Qt -from qtpy.QtWidgets import QFileDialog, QMainWindow, QWidget +from qtpy.QtWidgets import QCheckBox, QComboBox, QFileDialog, QLabel, QLineEdit, QMainWindow, QWidget +from superqt import QEnumComboBox from PartSeg.common_gui import select_multiple_files from PartSeg.common_gui.about_dialog import AboutDialog @@ -21,6 +26,7 @@ AdvancedWindow, Appearance, ) +from PartSeg.common_gui.algorithms_description import FieldsList, FormWidget, ListInput, QtAlgorithmProperty from PartSeg.common_gui.custom_load_dialog import CustomLoadDialog, IOMethodMock, LoadProperty, PLoadDialog from PartSeg.common_gui.custom_save_dialog import CustomSaveDialog, FormDialog, PSaveDialog from PartSeg.common_gui.equal_column_layout import EqualColumnLayout @@ -28,14 +34,22 @@ from PartSeg.common_gui.multiple_file_widget import LoadRecentFiles, MultipleFileWidget, MultipleLoadDialog from PartSeg.common_gui.qt_modal import QtPopup from PartSeg.common_gui.searchable_combo_box import SearchComboBox -from PartSeg.common_gui.universal_gui_part import EnumComboBox +from PartSeg.common_gui.universal_gui_part import ChannelComboBox, CustomDoubleSpinBox, CustomSpinBox, EnumComboBox from PartSegCore import state_store -from PartSegCore.algorithm_describe_base import AlgorithmProperty, Register +from PartSegCore.algorithm_describe_base import ( + AlgorithmDescribeBase, + AlgorithmProperty, + AlgorithmSelection, + Register, + base_model_to_algorithm_property, +) from PartSegCore.analysis.calculation_plan import MaskSuffix from PartSegCore.analysis.load_functions import LoadProject, LoadStackImage, load_dict from PartSegCore.analysis.save_functions import SaveAsTiff, SaveProject, save_dict +from PartSegCore.class_register import register_class from PartSegCore.io_utils import SaveBase -from PartSegImage import Image, ImageWriter +from PartSegCore.utils import BaseModel +from PartSegImage import Channel, Image, ImageWriter pyside_skip = pytest.mark.skipif(qtpy.API_NAME == "PySide2" and platform.system() == "Linux", reason="PySide2 problem") IS_MACOS = sys.platform == "darwin" @@ -547,7 +561,7 @@ def test_move_to_error_no_parent(self, qtbot): with pytest.raises(ValueError): popup.move_to() - @pytest.mark.parametrize("pos", ["top", "bottom", "left", "right"]) + @pytest.mark.parametrize("pos", ["top", "bottom", "left", "right", (10, 10, 10, 10), (15, 10, 10, 10)]) def test_move_to(self, pos, qtbot): window = QMainWindow() qtbot.addWidget(window) @@ -568,15 +582,6 @@ def test_move_to_error_wrong_params(self, qtbot): with pytest.raises(ValueError): popup.move_to({}) - @pytest.mark.parametrize("pos", [[10, 10, 10, 10], (15, 10, 10, 10)]) - def test_move_to_cords(self, pos, qtbot): - window = QMainWindow() - qtbot.addWidget(window) - widget = QWidget() - window.setCentralWidget(widget) - popup = QtPopup(widget) - popup.move_to(pos) - def test_click(self, qtbot, monkeypatch): popup = QtPopup(None) monkeypatch.setattr(popup, "close", MagicMock()) @@ -653,3 +658,260 @@ def test_theme_select(self, qtbot, base_settings, monkeypatch): app.layout_list.setCurrentIndex(0) assert app.layout_list.currentText() == "aaa" assert base_settings.theme_name == "aaa" + + +class TestFormWidget: + def test_create(self, qtbot): + form = FormWidget([]) + qtbot.add_widget(form) + assert not form.has_elements() + assert form.get_values() == {} + + def test_single_field_widget(self, qtbot): + form = FormWidget([AlgorithmProperty("test", "Test", 1)]) + qtbot.add_widget(form) + assert form.has_elements() + assert form.get_values() == {"test": 1} + + def test_single_field_widget_start_values(self, qtbot): + form = FormWidget([AlgorithmProperty("test", "Test", 1)], start_values={"test": 2}) + qtbot.add_widget(form) + assert form.has_elements() + assert form.get_values() == {"test": 2} + form.set_values({"test": 5}) + assert form.get_values() == {"test": 5} + + def test_single_field_widget_wrong_start_values(self, qtbot): + form = FormWidget([AlgorithmProperty("test", "Test", 1)], start_values={"test": "aaa"}) + qtbot.add_widget(form) + assert form.has_elements() + assert form.get_values() == {"test": 1} + + def test_base_model_simple_create(self, qtbot): + class Fields(BaseModel): + test: int = 5 + + form = FormWidget(Fields) + qtbot.add_widget(form) + assert form.has_elements() + assert isinstance(form.get_values(), Fields) + assert form.get_values() == Fields(test=5) + form.set_values(Fields(test=8)) + assert form.get_values() == Fields(test=8) + + def test_base_model_nested_create(self, qtbot): + class SubFields(BaseModel): + field1: int = 0 + field2: float = 0 + + class Fields(BaseModel): + test1: SubFields = SubFields(field1=5, field2=7) + test2: SubFields = SubFields(field1=15, field2=41) + + form = FormWidget(Fields) + qtbot.add_widget(form) + assert form.has_elements() + assert isinstance(form.get_values(), Fields) + assert form.get_values() == Fields(test1=SubFields(field1=5, field2=7), test2=SubFields(field1=15, field2=41)) + + def test_base_model_register_create(self, qtbot): + class SampleSelection(AlgorithmSelection): + pass + + class SampleClass1(AlgorithmDescribeBase): + @classmethod + def get_name(cls) -> str: + return "1" + + @classmethod + def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: + return [AlgorithmProperty("field", "Field", 1)] + + class SampleClass2(AlgorithmDescribeBase): + @classmethod + def get_name(cls) -> str: + return "2" + + @classmethod + def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: + return [AlgorithmProperty("field_", "Field", 2)] + + SampleSelection.register(SampleClass1) + SampleSelection.register(SampleClass2) + + class SampleModel(BaseModel): + field1: int = Field(10, le=100, ge=0, title="Field 1") + check_selection: SampleSelection = Field(SampleSelection(name="1", values={}), title="Class selection") + + form = FormWidget(SampleModel) + qtbot.add_widget(form) + assert form.has_elements() + assert isinstance(form.get_values(), SampleModel) + assert form.get_values() == SampleModel( + field1=10, check_selection=SampleSelection(name="1", values={"field": 1}) + ) + + def test_base_model_register_nested_create(self, qtbot, clean_register): + class SampleSelection(AlgorithmSelection): + pass + + @register_class + class SubModel1(BaseModel): + field1: int = 3 + + @register_class + class SubModel2(BaseModel): + field2: int = 5 + + class SampleClass1(AlgorithmDescribeBase): + __argument_class__ = SubModel1 + + @classmethod + def get_name(cls) -> str: + return "1" + + class SampleClass2(AlgorithmDescribeBase): + __argument_class__ = SubModel2 + + @classmethod + def get_name(cls) -> str: + return "2" + + SampleSelection.register(SampleClass1) + SampleSelection.register(SampleClass2) + + @register_class + class SampleModel(BaseModel): + field1: int = Field(10, le=100, ge=0, title="Field 1") + check_selection: SampleSelection = Field(SampleSelection(name="1", values={}), title="Class selection") + + form = FormWidget(SampleModel) + qtbot.add_widget(form) + assert form.has_elements() + assert isinstance(form.get_values(), SampleModel) + assert isinstance(form.get_values().check_selection.values, SubModel1) + assert form.get_values() == SampleModel( + field1=10, check_selection=SampleSelection(name="1", values=SubModel1(field1=3)) + ) + + +class TestFieldsList: + def test_simple(self): + li = FieldsList([]) + assert li.get_value() == {} + + def test_setting_values(self, qtbot): + class Fields(BaseModel): + field1: int = 1 + field2: float = 3 + + ap_li = [QtAlgorithmProperty.from_algorithm_property(x) for x in base_model_to_algorithm_property(Fields)] + for el in ap_li: + qtbot.add_widget(el.get_field()) + + li = FieldsList(ap_li) + assert li.get_value() == {"field1": 1, "field2": 3} + + li.set_value(Fields(field1=2, field2=1)) + assert li.get_value() == {"field1": 2, "field2": 1} + + li.set_value({"field1": 5, "field2": 8}) + assert li.get_value() == {"field1": 5, "field2": 8} + + def test_signal(self, qtbot): + ap = QtAlgorithmProperty(name="test", user_name="Test", default_value=1) + qtbot.add_widget(ap.get_field()) + li = FieldsList([ap]) + with qtbot.wait_signal(li.changed): + ap.set_value(3) + assert li.get_value() == {"test": 3} + + +class EnumQtAl(Enum): + test1 = 1 + test2 = 2 + + +class ModelQtAl(BaseModel): + field1 = 1 + field2 = 2.0 + + +class TestQtAlgorithmProperty: + def test_from_algorithm_property(self, qtbot): + res = QtAlgorithmProperty.from_algorithm_property("aaaa") + assert isinstance(res, QLabel) + + ap = AlgorithmProperty(name="test", user_name="Test", default_value=1) + res = QtAlgorithmProperty.from_algorithm_property(ap) + assert isinstance(res, QtAlgorithmProperty) + qtbot.add_widget(res.get_field()) + + with pytest.raises(ValueError): + QtAlgorithmProperty.from_algorithm_property(1) + + @pytest.mark.parametrize( + "data_type,default_value,expected_type,next_value", + [ + (Channel, Channel(1), ChannelComboBox, Channel(2)), + (bool, True, QCheckBox, False), + (int, 1, CustomSpinBox, 2), + (float, 1, CustomDoubleSpinBox, 3), + (EnumQtAl, EnumQtAl.test1, QEnumComboBox, EnumQtAl.test2), + (str, "a", QLineEdit, "b"), + (ModelQtAl, ModelQtAl(), FieldsList, ModelQtAl(field1=3, field2=4.5)), + (datetime.date, datetime.date(2022, 3, 24), Widget, datetime.date(2022, 3, 25)), + ], + ) + def test_types(self, qtbot, data_type, default_value, expected_type, next_value): + ap = AlgorithmProperty(name="test", user_name="Test", default_value=default_value, value_type=data_type) + res = QtAlgorithmProperty.from_algorithm_property(ap) + if isinstance(res.get_field(), FieldsList): + for el in res.get_field().field_list: + qtbot.add_widget(el.get_field()) + elif isinstance(res.get_field(), QWidget): + qtbot.add_widget(res.get_field()) + assert isinstance(res.get_field(), expected_type) + assert res.get_value() == default_value + with qtbot.wait_signal(res.change_fun): + res.set_value(next_value) + assert res.get_value() == next_value + + def test_list_type(self, qtbot): + ap = AlgorithmProperty( + name="test", user_name="Test", default_value="aaa", value_type=list, possible_values=["aaa", "bbb"] + ) + res = QtAlgorithmProperty.from_algorithm_property(ap) + qtbot.add_widget(res.get_field()) + assert isinstance(res.get_field(), QComboBox) + assert res.get_value() == "aaa" + + with qtbot.wait_signal(res.change_fun): + res.set_value("bbb") + assert res.get_value() == "bbb" + + def test_numeric_type_default_value_error(self): + ap = AlgorithmProperty(name="test", user_name="Test", default_value="a", value_type=int) + with pytest.raises(ValueError): + QtAlgorithmProperty.from_algorithm_property(ap) + ap.default_value = 1.0 + with pytest.raises(ValueError): + QtAlgorithmProperty.from_algorithm_property(ap) + ap = AlgorithmProperty(name="test", user_name="Test", default_value="a", value_type=float) + with pytest.raises(ValueError): + QtAlgorithmProperty.from_algorithm_property(ap) + + def test_per_dimension(self, qtbot): + ap = AlgorithmProperty(name="test", user_name="Test", default_value=1, per_dimension=True) + res = QtAlgorithmProperty.from_algorithm_property(ap) + qtbot.add_widget(res.get_field()) + assert isinstance(res.get_field(), ListInput) + assert res.get_value() == [1, 1, 1] + with qtbot.wait_signal(res.change_fun): + res.set_value([2, 4, 6]) + assert res.get_value() == [2, 4, 6] + + with qtbot.wait_signal(res.change_fun): + res.set_value(1) + + assert res.get_value() == [1, 1, 1] diff --git a/package/tests/test_PartSeg/test_napari_widgets.py b/package/tests/test_PartSeg/test_napari_widgets.py index a7bc3cfaa..d5d9bb8fb 100644 --- a/package/tests/test_PartSeg/test_napari_widgets.py +++ b/package/tests/test_PartSeg/test_napari_widgets.py @@ -9,12 +9,13 @@ from PartSeg.common_gui.napari_image_view import SearchType from PartSeg.plugins.napari_widgets import MaskCreate, ROIAnalysisExtraction, ROIMaskExtraction, SearchLabel, _settings from PartSeg.plugins.napari_widgets.roi_extraction_algorithms import ProfilePreviewDialog, QInputDialog +from PartSeg.plugins.napari_widgets.search_label_widget import HIGHLIGHT_LABEL_NAME from PartSegCore.algorithm_describe_base import ROIExtractionProfile -from PartSegCore.analysis.algorithm_description import analysis_algorithm_dict +from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection from PartSegCore.analysis.load_functions import LoadProfileFromJSON from PartSegCore.analysis.measurement_calculation import Volume, Voxels from PartSegCore.analysis.save_functions import SaveProfilesToJSON -from PartSegCore.mask.algorithm_description import mask_algorithm_dict +from PartSegCore.mask.algorithm_description import MaskAlgorithmSelection from PartSegCore.segmentation import ROIExtractionResult napari_skip = pytest.mark.skipif( @@ -30,7 +31,6 @@ def clean_settings(tmp_path): old_settings = _settings._settings _settings._settings = None - # _settings._settings = BaseSettings(tmp_path) yield _settings._settings = old_settings @@ -92,16 +92,12 @@ def func(*_args, **_kwargs): return func -@pytest.mark.parametrize("register", [analysis_algorithm_dict, mask_algorithm_dict]) +@pytest.mark.parametrize("register", [AnalysisAlgorithmSelection, MaskAlgorithmSelection]) def test_profile_preview_dialog(part_settings, register, qtbot, monkeypatch, tmp_path): - elem_name = next(iter(register)) + alg = register.get_default() profiles = { - "prof1": ROIExtractionProfile( - name="prof1", algorithm=elem_name, values=register[elem_name].get_default_values() - ), - "prof2": ROIExtractionProfile( - name="prof2", algorithm=elem_name, values=register[elem_name].get_default_values() - ), + "prof1": ROIExtractionProfile(name="prof1", algorithm=alg.name, values=alg.values), + "prof2": ROIExtractionProfile(name="prof2", algorithm=alg.name, values=alg.values), } dialog = ProfilePreviewDialog(profiles, register, part_settings) qtbot.add_widget(dialog) @@ -224,15 +220,15 @@ def test_search_labels(make_napari_viewer, qtbot): search.search_type.value = SearchType.Highlight search.component_selector.value = 1 - assert ".Highlight" in viewer.layers + assert HIGHLIGHT_LABEL_NAME in viewer.layers qtbot.wait(500) - assert ".Highlight" in viewer.layers + assert HIGHLIGHT_LABEL_NAME in viewer.layers search._stop() - assert ".Highlight" not in viewer.layers + assert HIGHLIGHT_LABEL_NAME not in viewer.layers search.search_type.value = SearchType.Zoom_in search.search_type.value = SearchType.Highlight - assert ".Highlight" in viewer.layers + assert HIGHLIGHT_LABEL_NAME in viewer.layers search.search_type.value = SearchType.Zoom_in - assert ".Highlight" not in viewer.layers + assert HIGHLIGHT_LABEL_NAME not in viewer.layers search.search_type.value = SearchType.Highlight search.component_selector.value = 2 diff --git a/package/tests/test_PartSeg/test_settings.py b/package/tests/test_PartSeg/test_settings.py index b20196c6b..81875a59f 100644 --- a/package/tests/test_PartSeg/test_settings.py +++ b/package/tests/test_PartSeg/test_settings.py @@ -11,7 +11,7 @@ from PartSeg._roi_mask.stack_settings import StackSettings, get_mask from PartSeg.common_backend.base_settings import BaseSettings, SwapTimeStackException, TimeAndStackException from PartSegCore.algorithm_describe_base import ROIExtractionProfile -from PartSegCore.analysis import analysis_algorithm_dict +from PartSegCore.analysis import AnalysisAlgorithmSelection from PartSegCore.analysis.io_utils import MaskInfo, create_history_element_from_project from PartSegCore.analysis.load_functions import LoadProject from PartSegCore.analysis.save_functions import SaveProject @@ -354,7 +354,7 @@ def test_pipeline_saving(self, qtbot, tmp_path, image, algorithm_parameters, mas settings = PartSettings(tmp_path) settings.image = image settings.last_executed_algorithm = algorithm_parameters["algorithm_name"] - algorithm = analysis_algorithm_dict[algorithm_parameters["algorithm_name"]]() + algorithm = AnalysisAlgorithmSelection[algorithm_parameters["algorithm_name"]]() algorithm.set_image(settings.image) algorithm.set_parameters(**algorithm_parameters["values"]) result = algorithm.calculation_run(lambda x, y: None) diff --git a/package/tests/test_PartSegCore/class_register_util.py b/package/tests/test_PartSegCore/class_register_util.py new file mode 100644 index 000000000..d9f446ab4 --- /dev/null +++ b/package/tests/test_PartSegCore/class_register_util.py @@ -0,0 +1,6 @@ +from PartSegCore.class_register import register_class + + +@register_class +class SampleClass5: + pass diff --git a/package/tests/test_PartSegCore/segmentation/test_segmentation_algorithm.py b/package/tests/test_PartSegCore/segmentation/test_segmentation_algorithm.py index 8dc243ae4..98d843fec 100644 --- a/package/tests/test_PartSegCore/segmentation/test_segmentation_algorithm.py +++ b/package/tests/test_PartSegCore/segmentation/test_segmentation_algorithm.py @@ -3,6 +3,7 @@ import numpy as np import pytest +from PartSegCore.algorithm_describe_base import base_model_to_algorithm_property from PartSegCore.segmentation import ROIExtractionAlgorithm from PartSegCore.segmentation.algorithm_base import ROIExtractionResult, SegmentationLimitException from PartSegCore.segmentation.restartable_segmentation_algorithms import final_algorithm_list as restartable_list @@ -21,8 +22,8 @@ def empty(*args): @pytest.fixture(autouse=True) def fix_threshold_flow(monkeypatch): values = ThresholdFlowAlgorithm.get_default_values() - values["threshold"]["values"]["core_threshold"]["values"]["threshold"] = 10 - values["threshold"]["values"]["base_threshold"]["values"]["threshold"] = 5 + values.threshold.values.core_threshold.values.threshold = 10 + values.threshold.values.base_threshold.values.threshold = 5 def _param(self): return values @@ -30,8 +31,8 @@ def _param(self): monkeypatch.setattr(ThresholdFlowAlgorithm, "get_default_values", _param) values2 = CellFromNucleusFlow.get_default_values() - values2["nucleus_threshold"]["values"]["threshold"] = 10 - values2["cell_threshold"]["values"]["threshold"] = 5 + values2.nucleus_threshold.values.threshold = 10 + values2.cell_threshold.values.threshold = 5 def _param2(self): return values2 @@ -49,8 +50,12 @@ def test_segmentation_algorithm(image, algorithm: Type[ROIExtractionAlgorithm], instance.set_image(image) if masking: instance.set_mask(image.get_channel(0) > 0) - instance.set_parameters(**instance.get_default_values()) - if not masking and "Need mask" in algorithm.get_fields(): + if instance.__new_style__: + # FIXME when migrate whole code + instance.set_parameters(instance.get_default_values()) + else: + instance.set_parameters(**instance.get_default_values()) + if not masking and "Need mask" in base_model_to_algorithm_property(instance.__argument_class__): with pytest.raises(SegmentationLimitException): instance.calculation_run(empty) else: diff --git a/package/tests/test_PartSegCore/test_algorithm_describe_base.py b/package/tests/test_PartSegCore/test_algorithm_describe_base.py new file mode 100644 index 000000000..c0fdcd9d3 --- /dev/null +++ b/package/tests/test_PartSegCore/test_algorithm_describe_base.py @@ -0,0 +1,320 @@ +# pylint: disable=R0201 +import typing +from enum import Enum + +import pytest +from pydantic import BaseModel as PydanticBaseModel +from pydantic import Field, ValidationError + +from PartSegCore.algorithm_describe_base import ( + AlgorithmDescribeBase, + AlgorithmProperty, + AlgorithmSelection, + _GetDescriptionClass, + base_model_to_algorithm_property, +) +from PartSegCore.class_register import class_to_str, register_class +from PartSegCore.utils import BaseModel +from PartSegImage import Channel + + +def test_get_description_class(): + class SampleClass: + __test_class__ = _GetDescriptionClass() + + @classmethod + def get_fields(self): + return [AlgorithmProperty("test1", "Test 1", 1), AlgorithmProperty("test2", "Test 2", 2.0)] + + val = SampleClass.__test_class__ + assert val.__name__ == "__test_class__" + assert val.__qualname__.endswith("SampleClass.__test_class__") + assert issubclass(val, PydanticBaseModel) + assert val.__fields__.keys() == {"test1", "test2"} + + +def test_algorithm_selection(): + class TestSelection(AlgorithmSelection): + pass + + class TestSelection2(AlgorithmSelection): + pass + + class Class1(AlgorithmDescribeBase): + @classmethod + def get_name(cls) -> str: + return "test1" + + @classmethod + def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: + return [] + + class Class2(AlgorithmDescribeBase): + @classmethod + def get_name(cls) -> str: + return "test2" + + @classmethod + def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: + return [] + + TestSelection.register(Class1) + TestSelection.register(Class2) + + assert "test1" in TestSelection.__register__ + assert "test1" not in TestSelection2.__register__ + + v = TestSelection(name="test1", values={}) + assert v.name == "test1" + assert v.class_path == class_to_str(Class1) + assert v.values == {} + + with pytest.raises(ValidationError): + TestSelection(name="test3", values={}) + + assert TestSelection["test1"] is Class1 + + +def test_algorithm_selection_convert_subclass(clean_register): + class TestSelection(AlgorithmSelection): + pass + + @register_class + class TestModel1(BaseModel): + field1: int = 0 + + @register_class(version="0.0.1", migrations=[("0.0.1", lambda x: {"field2": x["field"]})]) + class TestModel2(BaseModel): + field2: int = 7 + + class Class1(AlgorithmDescribeBase): + __argument_class__ = TestModel1 + + @classmethod + def get_name(cls) -> str: + return "test1" + + class Class2(AlgorithmDescribeBase): + __argument_class__ = TestModel2 + + @classmethod + def get_name(cls) -> str: + return "test2" + + TestSelection.register(Class1) + TestSelection.register(Class2) + + ob = TestSelection(name="test1", values={"field1": 4}) + assert isinstance(ob.values, TestModel1) + assert ob.values.field1 == 4 + + ob = TestSelection(name="test2", values={"field": 5}) + assert isinstance(ob.values, TestModel2) + assert ob.values.field2 == 5 + + +def test_algorithm_selection_register_old(clean_register): + class TestSelection(AlgorithmSelection): + pass + + @register_class + class TestModel1(BaseModel): + field1: int = 0 + + class Class1(AlgorithmDescribeBase): + __argument_class__ = TestModel1 + + @classmethod + def get_name(cls) -> str: + return "test3" + + TestSelection.register(Class1, old_names=["test"]) + + ob = TestSelection(name="test3", values={"field1": 4}) + assert isinstance(ob.values, TestModel1) + ob = TestSelection(name="test", values={"field1": 4}) + assert isinstance(ob.values, TestModel1) + with pytest.raises(ValidationError): + TestSelection(name="test4", values={"field1": 4}) + + +def test_base_model_to_algorithm_property_base(): + class SampleEnum(Enum): + a = 1 + b = 2 + + class Sample(BaseModel): + field1: int = Field(0, le=100, ge=0, title="Field 1") + field2: SampleEnum = SampleEnum.a + field_3: float = Field(0, le=55, ge=-7) + channel: Channel = Field(0, title="Channel") + + s = Sample(field1=1, field_3=1.5) + assert s.field_3 == 1.5 + + converted = base_model_to_algorithm_property(Sample) + assert len(converted) == 4 + assert converted[0].name == "field1" + assert converted[0].user_name == "Field 1" + assert issubclass(converted[0].value_type, int) + assert converted[0].range == (0, 100) + assert converted[1].name == "field2" + assert converted[1].user_name == "Field2" + assert converted[1].value_type is SampleEnum + assert converted[2].name == "field_3" + assert converted[2].user_name == "Field 3" + assert issubclass(converted[2].value_type, float) + assert converted[2].range == (-7, 55) + + assert converted[3].value_type is Channel + assert converted[3].name == "channel" + assert converted[3].user_name == "Channel" + + +def test_base_model_to_algorithm_property_algorithm_describe_base(): + class SampleSelection(AlgorithmSelection): + pass + + class SampleClass1(AlgorithmDescribeBase): + @classmethod + def get_name(cls) -> str: + return "1" + + @classmethod + def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: + return [] + + class SampleClass2(AlgorithmDescribeBase): + @classmethod + def get_name(cls) -> str: + return "2" + + @classmethod + def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: + return [] + + SampleSelection.register(SampleClass1) + SampleSelection.register(SampleClass2) + + d_text = "description text" + + class SampleModel(BaseModel): + field1: int = Field(10, le=100, ge=0, title="Field 1", description=d_text) + check_selection: SampleSelection = Field(SampleSelection(name="1", values={}), title="Class selection") + + converted = base_model_to_algorithm_property(SampleModel) + assert len(converted) == 2 + assert issubclass(converted[0].value_type, int) + assert converted[0].help_text == d_text + assert issubclass(converted[1].value_type, AlgorithmDescribeBase) + assert converted[1].default_value == "1" + assert converted[1].possible_values is SampleSelection.__register__ + + +def test_base_model_to_algorithm_property_algorithm_describe_empty(): + assert base_model_to_algorithm_property(BaseModel) == [] + + +def test_text_addition_model_to_algorithm_property(): + class ModelWithText(BaseModel): + field1: int = 1 + field2: int = Field(1, prefix="aaaa") + field3: int = Field(1, suffix="bbbb") + field4: int = 1 + + @staticmethod + def header(): + return "cccc" + + property_list = base_model_to_algorithm_property(ModelWithText) + assert property_list[0] == "cccc" + assert property_list[2] == "aaaa" + assert property_list[5] == "bbbb" + + +def test_base_model_to_algorithm_property_position(): + class BBaseModel(BaseModel): + field1: int = 1 + field2: int = 1 + + class ModelWithPosition(BBaseModel): + field3: int = Field(1, position=1) + + property_list = base_model_to_algorithm_property(ModelWithPosition) + assert property_list[0].name == "field1" + assert property_list[1].name == "field3" + assert property_list[2].name == "field2" + + +class TestAlgorithmDescribeBase: + def test_old_style_algorithm(self): + class SampleAlgorithm(AlgorithmDescribeBase): + @classmethod + def get_name(cls) -> str: + return "sample" + + @classmethod + def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: + return ["aaaa", AlgorithmProperty("name", "Name", 1, options_range=(1, 10), help_text="ceeeec")] + + assert SampleAlgorithm.get_name() == "sample" + assert len(SampleAlgorithm.get_fields()) == 2 + assert "ceeeec" in SampleAlgorithm.get_doc_from_fields() + assert "(default values: 1)" in SampleAlgorithm.get_doc_from_fields() + assert len(SampleAlgorithm.get_fields_dict()) == 1 + assert SampleAlgorithm.get_default_values() == {"name": 1} + + def test_new_style_algorithm(self): + class DataModel(BaseModel): + name: int = Field(1, ge=1, le=10, description="ceeeec", prefix="aaaa") + + class SampleAlgorithm(AlgorithmDescribeBase): + __argument_class__ = DataModel + + @classmethod + def get_name(cls) -> str: + return "sample" + + assert SampleAlgorithm.get_name() == "sample" + with pytest.warns(FutureWarning, match=r"Class has __argument_class__ defined"): + assert len(SampleAlgorithm.get_fields()) == 2 + assert "ceeeec" in SampleAlgorithm.get_doc_from_fields() + assert "(default values: 1)" in SampleAlgorithm.get_doc_from_fields() + assert len(SampleAlgorithm.get_fields_dict()) == 1 + assert SampleAlgorithm.get_default_values() == {"name": 1} + + def test_new_style_algorithm_with_old_style_subclass(self): + class DataModel(BaseModel): + name: int = Field(1, ge=1, le=10, description="ceeeec", prefix="aaaa") + + class SampleAlgorithm(AlgorithmDescribeBase): + __argument_class__ = DataModel + + @classmethod + def get_name(cls) -> str: + return "sample" + + class SampleSubAlgorithm(SampleAlgorithm): + @classmethod + def get_name(cls) -> str: + return "sample2" + + @classmethod + def get_fields(cls) -> typing.List[typing.Union[AlgorithmProperty, str]]: + return super().get_fields() + [ + AlgorithmProperty("name2", "Name 2", 3.0, options_range=(1, 10), help_text="deeeed") + ] + + assert SampleSubAlgorithm.get_name() == "sample2" + with pytest.warns(FutureWarning, match=r"Class has __argument_class__ defined"): + assert len(SampleSubAlgorithm.get_fields()) == 3 + with pytest.warns(FutureWarning, match=r"Class has __argument_class__ defined"): + doc_text = SampleSubAlgorithm.get_doc_from_fields() + assert "ceeeec" in doc_text + assert "deeeed" in doc_text + assert "(default values: 1)" in doc_text + assert "(default values: 3.0)" in doc_text + with pytest.warns(FutureWarning, match=r"Class has __argument_class__ defined"): + assert len(SampleSubAlgorithm.get_fields_dict()) == 2 + with pytest.warns(FutureWarning, match=r"Class has __argument_class__ defined"): + assert SampleSubAlgorithm.get_default_values() == {"name": 1, "name2": 3.0} diff --git a/package/tests/test_PartSegCore/test_analysis_batch.py b/package/tests/test_PartSegCore/test_analysis_batch.py index ee8edb795..a370ff3a6 100644 --- a/package/tests/test_PartSegCore/test_analysis_batch.py +++ b/package/tests/test_PartSegCore/test_analysis_batch.py @@ -39,6 +39,7 @@ from PartSegCore.io_utils import SaveBase from PartSegCore.mask_create import MaskProperty from PartSegCore.segmentation.noise_filtering import DimensionType +from PartSegCore.segmentation.restartable_segmentation_algorithms import LowerThresholdFlowAlgorithm from PartSegCore.universal_const import UNIT_SCALE, Units from PartSegImage import Image, ImageWriter, TiffImageReader @@ -75,20 +76,22 @@ def create_test_data(tmpdir): class TestCalculationProcess: @staticmethod def create_calculation_plan(): - parameters = { - "channel": 1, - "minimum_size": 200, - "threshold": { - "name": "Base/Core", - "values": { - "core_threshold": {"name": "Manual", "values": {"threshold": 30000}}, - "base_threshold": {"name": "Manual", "values": {"threshold": 13000}}, + parameters = LowerThresholdFlowAlgorithm.__argument_class__( + **{ + "channel": 1, + "minimum_size": 200, + "threshold": { + "name": "Base/Core", + "values": { + "core_threshold": {"name": "Manual", "values": {"threshold": 30000}}, + "base_threshold": {"name": "Manual", "values": {"threshold": 13000}}, + }, }, - }, - "noise_filtering": {"name": "Gauss", "values": {"dimension_type": DimensionType.Layer, "radius": 1.0}}, - "side_connection": False, - "sprawl_type": {"name": "Euclidean", "values": {}}, - } + "noise_filtering": {"name": "Gauss", "values": {"dimension_type": DimensionType.Layer, "radius": 1.0}}, + "side_connection": False, + "flow_type": {"name": "Euclidean", "values": {}}, + } + ) segmentation = ROIExtractionProfile(name="test", algorithm="Lower threshold with watershed", values=parameters) mask_suffix = MaskSuffix(name="", suffix="_mask") @@ -122,20 +125,22 @@ def create_calculation_plan(): @staticmethod def create_calculation_plan2(): - parameters = { - "channel": 0, - "minimum_size": 200, - "threshold": { - "name": "Base/Core", - "values": { - "core_threshold": {"name": "Manual", "values": {"threshold": 30000}}, - "base_threshold": {"name": "Manual", "values": {"threshold": 13000}}, + parameters = LowerThresholdFlowAlgorithm.__argument_class__( + **{ + "channel": 0, + "minimum_size": 200, + "threshold": { + "name": "Base/Core", + "values": { + "core_threshold": {"name": "Manual", "values": {"threshold": 30000}}, + "base_threshold": {"name": "Manual", "values": {"threshold": 13000}}, + }, }, - }, - "noise_filtering": {"name": "Gauss", "values": {"dimension_type": DimensionType.Layer, "radius": 1.0}}, - "side_connection": False, - "sprawl_type": {"name": "Euclidean", "values": {}}, - } + "noise_filtering": {"name": "Gauss", "values": {"dimension_type": DimensionType.Layer, "radius": 1.0}}, + "side_connection": False, + "flow_type": {"name": "Euclidean", "values": {}}, + } + ) segmentation = ROIExtractionProfile(name="test", algorithm="Lower threshold with watershed", values=parameters) chosen_fields = [ @@ -193,20 +198,22 @@ def create_simple_plan(root_type: RootType, save: Save): @staticmethod def create_calculation_plan3(): - parameters = { - "channel": 1, - "minimum_size": 200, - "threshold": { - "name": "Base/Core", - "values": { - "core_threshold": {"name": "Manual", "values": {"threshold": 30000}}, - "base_threshold": {"name": "Manual", "values": {"threshold": 13000}}, + parameters = LowerThresholdFlowAlgorithm.__argument_class__( + **{ + "channel": 1, + "minimum_size": 200, + "threshold": { + "name": "Base/Core", + "values": { + "core_threshold": {"name": "Manual", "values": {"threshold": 30000}}, + "base_threshold": {"name": "Manual", "values": {"threshold": 13000}}, + }, }, - }, - "noise_filtering": {"name": "Gauss", "values": {"dimension_type": DimensionType.Layer, "radius": 1.0}}, - "side_connection": False, - "sprawl_type": {"name": "Euclidean", "values": {}}, - } + "noise_filtering": {"name": "Gauss", "values": {"dimension_type": DimensionType.Layer, "radius": 1.0}}, + "side_connection": False, + "flow_type": {"name": "Euclidean", "values": {}}, + } + ) segmentation = ROIExtractionProfile(name="test", algorithm="Lower threshold with watershed", values=parameters) mask_suffix = MaskSuffix(name="", suffix="_mask") @@ -462,17 +469,19 @@ def test_full_pipeline(self, tmpdir, data_test_dir, monkeypatch): assert os.path.basename(df.name.units[i]) == f"stack1_component{i+1}.tif" @pytest.mark.filterwarnings("ignore:This method will be removed") - def test_full_pipeline_error(self, tmp_path_factory, data_test_dir, monkeypatch): + def test_full_pipeline_error(self, tmp_path, data_test_dir, monkeypatch): plan = self.create_calculation_plan() - data_dir = tmp_path_factory.mktemp("data") + data_dir = tmp_path / "data" + data_dir.mkdir() file_pattern_copy = os.path.join(data_test_dir, "stack1_components", "stack1_component*.tif") file_paths = sorted(glob(file_pattern_copy)) for el in file_paths: shutil.copy(el, data_dir) - shutil.copy(data_dir / "stack1_component1.tif", data_dir / "stack1_component10.tif") + shutil.copy(data_dir / "stack1_component1.tif", data_dir / "stack1_component10.tif") file_pattern = os.path.join(data_dir, "stack1_component*[0-9].tif") file_paths = sorted(glob(file_pattern)) - result_dir = tmp_path_factory.mktemp("result") + result_dir = tmp_path / "result" + result_dir.mkdir() assert os.path.basename(file_paths[0]) == "stack1_component1.tif" calc = Calculation( diff --git a/package/tests/test_PartSegCore/test_class_register.py b/package/tests/test_PartSegCore/test_class_register.py new file mode 100644 index 000000000..2342b3f70 --- /dev/null +++ b/package/tests/test_PartSegCore/test_class_register.py @@ -0,0 +1,113 @@ +from typing import Any, Dict + +import pytest + +from PartSegCore.class_register import REGISTER, class_to_str, register_class, rename_key, update_argument + + +@register_class +class SampleClass1: + pass + + +def rename_a_to_c(dkt: Dict[str, Any]) -> Dict[str, Any]: + dkt = dict(dkt) + dkt["c"] = dkt["a"] + del dkt["a"] + return dkt + + +@register_class(version="0.0.1", migrations=[("0.0.1", rename_a_to_c)]) +class SampleClass2: + pass + + +class SampleClass3: + pass + + +@register_class(old_paths=["test.test.BBase"], version="0.0.2") +class SampleClass4: + pass + + +class SampleClass6: + pass + + +def test_migrate(): + assert REGISTER.migrate_data(class_to_str(SampleClass1), {}, {"a": 1, "b": 2}) == {"a": 1, "b": 2} + assert REGISTER.migrate_data( + class_to_str(SampleClass2), {class_to_str(SampleClass2): "0.0.1"}, {"a": 1, "b": 2} + ) == {"a": 1, "b": 2} + assert REGISTER.migrate_data(class_to_str(SampleClass2), {}, {"a": 1, "b": 2}) == {"c": 1, "b": 2} + + +def test_unregistered_class(): + assert REGISTER.get_class(class_to_str(SampleClass3)) is SampleClass3 + + +def test_old_paths(): + assert REGISTER.get_class("test.test.BBase") is SampleClass4 + + +def test_import_part(): + obj = REGISTER.get_class("test_PartSegCore.class_register_util.SampleClass5") + from .class_register_util import SampleClass5 + + assert SampleClass5 is obj + + +def test_old_class_error(): + with pytest.raises(RuntimeError): + register_class(SampleClass6, old_paths=[class_to_str(SampleClass4)]) + + +def test_get_version(): + assert str(REGISTER.get_version(SampleClass1)) == "0.0.0" + assert str(REGISTER.get_version(SampleClass2)) == "0.0.1" + assert str(REGISTER.get_version(SampleClass4)) == "0.0.2" + + +def test_rename_key(): + dkt = {"aaa": 1, "bbb": 2} + assert rename_key(from_key="aaa", to_key="ccc")(dkt) == {"bbb": 2, "ccc": 1} + with pytest.raises(KeyError): + rename_key("ccc", "ddd")(dkt) + + assert rename_key("ccc", "ddd", optional=True)(dkt) == dkt + + +def test_update_argument(clean_register): + @REGISTER.register(version="0.0.1") + class MigrateClass: + def __init__(self, a, b): + self.a = a + self.b = b + + class ClassToCall: + __argument_class__ = MigrateClass + + @classmethod + @update_argument("arg") + def call_func(cls, aa, arg: MigrateClass): + print(aa, arg.a) + + ClassToCall.call_func(aa=1, arg={"a": 1, "b": 2}) + ClassToCall.call_func(1, {"a": 1, "b": 2}) + + +def test_migrate_parent_class(clean_register): + @register_class(version="0.0.1", migrations=[("0.0.1", rename_key("field", "field1"))]) + class BaseMigrateClass: + def __init__(self, field1): + self.field1 = field1 + + @register_class + class MigrateClass(BaseMigrateClass): + def __init__(self, field2, **kwargs): + super().__init__(**kwargs) + self.field2 = field2 + + migrated = REGISTER.migrate_data(class_to_str(MigrateClass), {}, {"field2": 1, "field": 5}) + assert "field1" in migrated diff --git a/package/tests/test_PartSegCore/test_io.py b/package/tests/test_PartSegCore/test_io.py index 69c6bbca9..092b7323e 100644 --- a/package/tests/test_PartSegCore/test_io.py +++ b/package/tests/test_PartSegCore/test_io.py @@ -8,6 +8,7 @@ from copy import deepcopy from enum import Enum from glob import glob +from pathlib import Path from typing import Type import h5py @@ -20,15 +21,12 @@ from PartSegCore.algorithm_describe_base import ROIExtractionProfile from PartSegCore.analysis import ProjectTuple from PartSegCore.analysis.calculation_plan import CalculationPlan, MaskSuffix, MeasurementCalculate -from PartSegCore.analysis.load_functions import LoadProject, UpdateLoadedMetadataAnalysis +from PartSegCore.analysis.load_functions import LoadProject from PartSegCore.analysis.measurement_base import Leaf, MeasurementEntry from PartSegCore.analysis.measurement_calculation import MEASUREMENT_DICT, MeasurementProfile from PartSegCore.analysis.save_functions import SaveAsNumpy, SaveAsTiff, SaveCmap, SaveProject, SaveXYZ -from PartSegCore.analysis.save_hooks import PartEncoder, part_hook -from PartSegCore.class_generator import enum_register -from PartSegCore.image_operations import RadiusType -from PartSegCore.io_utils import LoadBase, SaveBase, SaveROIAsNumpy, UpdateLoadedMetadataBase -from PartSegCore.json_hooks import check_loaded_dict +from PartSegCore.io_utils import LoadBase, SaveBase, SaveROIAsNumpy, load_metadata_base +from PartSegCore.json_hooks import PartSegEncoder, partseg_object_hook from PartSegCore.mask.history_utils import create_history_element_from_segmentation_tuple from PartSegCore.mask.io_functions import ( LoadROI, @@ -48,6 +46,7 @@ from PartSegCore.segmentation.algorithm_base import AdditionalLayerDescription from PartSegCore.segmentation.noise_filtering import DimensionType from PartSegCore.segmentation.segmentation_algorithm import ThresholdAlgorithm +from PartSegCore.utils import ProfileDict, check_loaded_dict from PartSegImage import Image @@ -116,7 +115,13 @@ def analysis_project_reversed() -> ProjectTuple: @pytest.fixture def mask_prop(): - return MaskProperty(RadiusType.NO, 0, RadiusType.NO, 0, False, False) + return MaskProperty.simple_mask() + + +class SampleEnumClass(Enum): + test0 = 0 + test1 = 1 + test2 = 2 class TestHistoryElement: @@ -251,7 +256,7 @@ def test_profile_load(self, data_test_dir): # noinspection PyBroadException try: with open(profile_path) as ff: - data = json.load(ff, object_hook=part_hook) + data = json.load(ff, object_hook=partseg_object_hook) assert check_loaded_dict(data) except Exception: # pylint: disable=W0703 pytest.fail("Fail in loading profile") @@ -261,7 +266,7 @@ def test_measure_load(self, data_test_dir): # noinspection PyBroadException try: with open(profile_path) as ff: - data = json.load(ff, object_hook=part_hook) + data = json.load(ff, object_hook=partseg_object_hook) assert check_loaded_dict(data) except Exception: # pylint: disable=W0703 pytest.fail("Fail in loading profile") @@ -269,36 +274,25 @@ def test_measure_load(self, data_test_dir): def test_json_dump(self): with pytest.raises(TypeError): json.dumps(DimensionType.Layer) - data_string = json.dumps(DimensionType.Layer, cls=PartEncoder) - assert re.search('"__Enum__":[^,}]+[,}]', data_string) is not None - assert re.search('"__subtype__":[^,}]+[,}]', data_string) is not None + data_string = json.dumps(DimensionType.Layer, cls=PartSegEncoder) + assert re.search('"__class__":[^,}]+[,}]', data_string) is not None assert re.search('"value":[^,}]+[,}]', data_string) is not None def test_json_load(self): - class Test(Enum): - test0 = 0 - test1 = 1 - test2 = 2 - - test_json = json.dumps(Test.test0, cls=PartEncoder) - - assert not check_loaded_dict(json.loads(test_json, object_hook=part_hook)) - - enum_register.register_class(Test) - assert isinstance(json.loads(test_json, object_hook=part_hook), Test) + test_json = json.dumps(SampleEnumClass.test0, cls=PartSegEncoder) + assert isinstance(json.loads(test_json, object_hook=partseg_object_hook), SampleEnumClass) def test_modernize_0_9_2_3(self, bundle_test_dir): file_path = os.path.join(bundle_test_dir, "segment_profile_0.9.2.3.json") assert os.path.exists(file_path) - data = UpdateLoadedMetadataBase.load_json_data(file_path) - assert "noise_filtering" in data["test_0.9.2.3"].values - assert "dimension_type" in data["test_0.9.2.3"].values["noise_filtering"]["values"] + data = load_metadata_base(file_path) + assert hasattr(data["test_0.9.2.3"].values, "noise_filtering") + assert hasattr(data["test_0.9.2.3"].values.noise_filtering.values, "dimension_type") file_path = os.path.join(bundle_test_dir, "calculation_plan_0.9.2.3.json") - data = UpdateLoadedMetadataAnalysis.load_json_data(file_path) + data = load_metadata_base(file_path) def test_update_name(self): - data = UpdateLoadedMetadataAnalysis.load_json_data(update_name_json) - print(data) + data = load_metadata_base(update_name_json) mp = data["problematic set"] assert isinstance(mp, MeasurementProfile) assert isinstance(mp.chosen_fields[0], MeasurementEntry) @@ -307,7 +301,7 @@ def test_update_name(self): assert mp.chosen_fields[1].calculation_tree.name == "Components number" def test_load_workflow(self, bundle_test_dir): - data = UpdateLoadedMetadataAnalysis.load_json_data(os.path.join(bundle_test_dir, "workflow.json")) + data = load_metadata_base(os.path.join(bundle_test_dir, "workflow.json")) plan = data["workflow"] assert isinstance(plan, CalculationPlan) mask_step = plan.execution_tree.children[0] @@ -407,8 +401,8 @@ def test_loading_new_segmentation(self, tmpdir, data_test_dir): algorithm = ThresholdAlgorithm() algorithm.set_image(image_data.image) param = algorithm.get_default_values() - param["channel"] = 0 - algorithm.set_parameters(**param) + param.channel = 0 + algorithm.set_parameters(param) res = algorithm.calculation_run(lambda x, y: None) num = np.max(res.roi) + 1 data_dict = {str(i): deepcopy(res.parameters) for i in range(1, num)} @@ -616,6 +610,19 @@ def test_json_parameters_mask(stack_segmentation1, tmp_path): assert len(load_param.roi_extraction_parameters) == 4 +@pytest.mark.parametrize("file_path", (Path(__file__).parent.parent / "test_data" / "notebook").glob("*.json")) +def test_load_notebook_json(file_path): + load_metadata_base(file_path) + + +@pytest.mark.parametrize( + "file_path", list((Path(__file__).parent.parent / "test_data" / "old_saves").glob(os.path.join("*", "*", "*.json"))) +) +def test_old_saves_load(file_path): + data: ProfileDict = load_metadata_base(file_path) + assert data.verify_data(), data.filter_data() + + update_name_json = """ {"problematic set": { "__MeasurementProfile__": true, diff --git a/package/tests/test_PartSegCore/test_json_hooks.py b/package/tests/test_PartSegCore/test_json_hooks.py index f05b65eb2..7251fc2a9 100644 --- a/package/tests/test_PartSegCore/test_json_hooks.py +++ b/package/tests/test_PartSegCore/test_json_hooks.py @@ -1,227 +1,53 @@ # pylint: disable=R0201 import json -from unittest.mock import MagicMock +from dataclasses import dataclass +from enum import Enum import numpy as np import pytest from napari.utils import Colormap +from napari.utils.notifications import NotificationSeverity +from PartSegCore.class_register import class_to_str, register_class, rename_key from PartSegCore.image_operations import RadiusType -from PartSegCore.json_hooks import EventedDict, ProfileDict, ProfileEncoder, profile_hook, recursive_update_dict - - -def test_recursive_update_dict_basic(): - dict1 = {"a": 1, "b": 2} - dict2 = {"b": 3, "c": 4} - dict1_copy = dict1.copy() - dict1.update(dict2) - recursive_update_dict(dict1_copy, dict2) - assert dict1 == dict1_copy - - -def test_recursive_update_dict(): - dict1 = {"a": {"k": 1, "l": 2}, "b": {"k": 1, "l": 2}} - dict2 = {"a": {"m": 3, "l": 4}, "b": 3, "c": 4} - recursive_update_dict(dict1, dict2) - assert dict1 == {"a": {"k": 1, "l": 4, "m": 3}, "b": 3, "c": 4} - - -class TestEventedDict: - def test_simple_add(self): - receiver = MagicMock() - - dkt = EventedDict() - dkt.setted.connect(receiver.set) - dkt.deleted.connect(receiver.delete) - dkt["a"] = 1 - assert dkt["a"] == 1 - assert receiver.set.call_count == 1 - assert "'a': 1" in str(dkt) - assert "'a': 1" in repr(dkt) - dkt["a"] = 2 - assert dkt["a"] == 2 - assert receiver.set.call_count == 2 - assert len(dkt) == 1 - del dkt["a"] - assert receiver.set.call_count == 2 - assert receiver.delete.call_count == 1 - assert len(dkt) == 0 - - def test_simple_add_remove(self): - callback_list = [] - - def callback_add(): - callback_list.append(1) - - def callback_delete(): - callback_list.append(2) - - dkt = EventedDict() - dkt.setted.connect(callback_add) - dkt.deleted.connect(callback_delete) - - dkt[1] = 1 - dkt[2] = 1 - assert len(dkt) == 2 - assert callback_list == [1, 1] - del dkt[1] - assert len(dkt) == 1 - assert callback_list == [1, 1, 2] - - def test_nested_evented(self): - dkt = EventedDict(bar={"foo": {"baz": 1}}) - assert isinstance(dkt["bar"], EventedDict) - assert isinstance(dkt["bar"]["foo"], EventedDict) - assert dkt["bar"]["foo"]["baz"] == 1 - - dkt["baz"] = {"bar": {"foo": 1}} - assert isinstance(dkt["baz"], EventedDict) - assert isinstance(dkt["baz"]["bar"], EventedDict) - assert dkt["baz"]["bar"]["foo"] == 1 - - def test_serialize(self, tmp_path): - dkt = EventedDict( - **{"a": {"b": {"c": 1, "d": 2, "e": 3}, "f": 1}, "g": {"h": {"i": 1, "j": 2}, "k": [6, 7, 8]}} - ) - with (tmp_path / "test_dict.json").open("w") as f_p: - json.dump(dkt, f_p, cls=ProfileEncoder) - - with (tmp_path / "test_dict.json").open("r") as f_p: - dkt2 = json.load(f_p, object_hook=profile_hook) - - assert isinstance(dkt2, EventedDict) - assert isinstance(dkt2["a"], EventedDict) - assert dkt["g"]["k"] == [6, 7, 8] - - def test_signal_names(self): - receiver = MagicMock() - dkt = EventedDict(baz={"foo": 1}) - dkt.setted.connect(receiver.set) - dkt.deleted.connect(receiver.deleted) - dkt["foo"] = 1 - assert receiver.set.call_count == 1 - receiver.set.assert_called_with("foo") - dkt["bar"] = EventedDict() - assert receiver.set.call_count == 2 - receiver.set.assert_called_with("bar") - dkt["bar"]["baz"] = 1 - assert receiver.set.call_count == 3 - receiver.set.assert_called_with("bar.baz") - dkt["baz"]["foo"] = 2 - assert receiver.set.call_count == 4 - receiver.set.assert_called_with("baz.foo") - - del dkt["bar"]["baz"] - assert receiver.deleted.call_count == 1 - receiver.deleted.assert_called_with("bar.baz") - del dkt["bar"] - assert receiver.deleted.call_count == 2 - receiver.deleted.assert_called_with("bar") - - def test_propagate_signal(self): - receiver = MagicMock() - dkt = EventedDict(baz={"foo": 1}) - dkt.setted.connect(receiver.set) - dkt.deleted.connect(receiver.deleted) - dkt["baz"].base_key = "" - dkt["baz"]["foo"] = 2 - receiver.set.assert_called_with("foo") - receiver.set.assert_called_once() - del dkt["baz"]["foo"] - receiver.deleted.assert_called_with("foo") - receiver.deleted.assert_called_once() - - -class TestProfileDict: - def test_simple(self): - dkt = ProfileDict() - dkt.set("a.b.c", 1) - dkt.set("a.b.a", 2) - assert dkt.get("a.b.c") == 1 - with pytest.raises(KeyError): - dkt.get("a.b.d") - dkt.get("a.b.d", 3) - assert dkt.get("a.b.d") == 3 - assert dkt.get("a.b") == {"a": 2, "c": 1, "d": 3} - with pytest.raises(TypeError): - dkt.set("a.b.c.d", 3) - - def test_update(self): - dkt = ProfileDict() - dkt.update(a=1, b=2, c=3) - assert dkt.my_dict == {"a": 1, "b": 2, "c": 3} - dkt2 = ProfileDict() - dkt2.update(c=4, d={"a": 2, "e": 7}) - assert dkt2.get("d.e") == 7 - dkt.update(dkt2) - assert dkt.get("d.e") == 7 - assert dkt.get("c") == 4 - dkt.update({"g": 1, "h": 4}) - assert dkt.get("g") == 1 - dkt.update({"w": 1, "z": 4}, w=3) - assert dkt.get("w") == 3 - assert dkt.verify_data() - assert dkt.filter_data() == [] - dkt.set("e.h.l", {"aaa": 1, "__error__": True}) - assert not dkt.verify_data() - assert dkt.filter_data() == ["e.h"] - - def test_serialize(self, tmp_path): - dkt = ProfileDict() - dkt.set("a.b.c", 1) - dkt.set("a.b.a", 2) - with open(tmp_path / "test.json", "w") as f_p: - json.dump(dkt, f_p, cls=ProfileEncoder) - with open(tmp_path / "test.json") as f_p: - dkt2 = json.load(f_p, object_hook=profile_hook) - - assert dkt.my_dict == dkt2.my_dict - - def test_callback(self): - def dummy_call(): - receiver.dummy() - - receiver = MagicMock() - - dkt = ProfileDict() - dkt.connect("", receiver.empty) - dkt.connect("", dummy_call) - dkt.connect("b", receiver.b) - dkt.connect(["d", "c"], receiver.dc) - - dkt.set("test.a", 1) - assert receiver.empty.call_count == 1 - assert receiver.dummy.call_count == 1 - receiver.empty.assert_called_with("a") - receiver.dummy.assert_called_with() - dkt.set("test.a", 1) - assert receiver.empty.call_count == 1 - receiver.b.assert_not_called() - dkt.set("test2.a", 1) - assert receiver.empty.call_count == 2 - receiver.b.assert_not_called() - dkt.set(["test", "b"], 1) - assert receiver.empty.call_count == 3 - assert receiver.b.call_count == 1 - dkt.set("test.d.c", 1) - receiver.dc.assert_called_once() - dkt.set("test.a", 2) - assert receiver.empty.call_count == 5 +from PartSegCore.json_hooks import PartSegEncoder, add_class_info, partseg_object_hook +from PartSegCore.utils import BaseModel, ProfileDict + + +@dataclass +class SampleDataclass: + filed1: int + field2: str + + +class SamplePydantic(BaseModel): + sample_int: int + sample_str: str + sample_dataclass: SampleDataclass + + +class SampleAsDict: + def __init__(self, value1, value2): + self.value1 = value1 + self.value2 = value2 + + def as_dict(self): + return {"value1": self.value1, "value2": self.value2} def test_profile_hook_colormap_load(bundle_test_dir): with open(bundle_test_dir / "view_settings_v0.12.6.json") as f_p: - json.load(f_p, object_hook=profile_hook) + json.load(f_p, object_hook=partseg_object_hook) def test_colormap_dump(tmp_path): cmap_list = [Colormap([(0, 0, 0), (1, 1, 1)]), Colormap([(0, 0, 0), (1, 1, 1)], controls=[0, 1])] with open(tmp_path / "test.json", "w") as f_p: - json.dump(cmap_list, f_p, cls=ProfileEncoder) + json.dump(cmap_list, f_p, cls=PartSegEncoder) with open(tmp_path / "test.json") as f_p: - cmap_list2 = json.load(f_p, object_hook=profile_hook) + cmap_list2 = json.load(f_p, object_hook=partseg_object_hook) assert np.array_equal(cmap_list[0].colors, cmap_list2[0].colors) assert np.array_equal(cmap_list[0].controls, cmap_list2[0].controls) @@ -233,10 +59,10 @@ def test_colormap_dump(tmp_path): Colormap([(0, 0, 0), (0, 0, 0), (1, 1, 1), (1, 1, 1)], controls=[0, 0.1, 0.8, 1]), ] with open(tmp_path / "test2.json", "w") as f_p: - json.dump(cmap_list, f_p, cls=ProfileEncoder) + json.dump(cmap_list, f_p, cls=PartSegEncoder) with open(tmp_path / "test2.json") as f_p: - cmap_list2 = json.load(f_p, object_hook=profile_hook) + cmap_list2 = json.load(f_p, object_hook=partseg_object_hook) assert np.array_equal(cmap_list[0].colors, cmap_list2[0].colors) assert np.array_equal(cmap_list[0].controls, cmap_list2[0].controls) @@ -252,7 +78,7 @@ class TestProfileEncoder: @pytest.mark.parametrize("dtype", [np.uint8, np.uint16, np.uint32, np.float32, np.float64]) def test_dump_numpy_types(self, dtype): data = {"a": dtype(2)} - text = json.dumps(data, cls=ProfileEncoder) + text = json.dumps(data, cls=PartSegEncoder) loaded = json.loads(text) assert loaded["a"] == 2 @@ -260,7 +86,132 @@ def test_dump_custom_types(self): prof_dict = ProfileDict() prof_dict.set("a.b.c", 1) data = {"a": RadiusType.R2D, "b": prof_dict} - text = json.dumps(data, cls=ProfileEncoder) - loaded = json.loads(text, object_hook=profile_hook) + text = json.dumps(data, cls=PartSegEncoder) + loaded = json.loads(text, object_hook=partseg_object_hook) assert loaded["a"] == RadiusType.R2D assert loaded["b"].get("a.b.c") == 1 + + +class TestPartSegEncoder: + def test_enum_serialize(self, tmp_path): + data = {"value1": RadiusType.R2D, "value2": RadiusType.NO, "value3": NotificationSeverity.ERROR} + with (tmp_path / "test.json").open("w") as f_p: + json.dump(data, f_p, cls=PartSegEncoder) + with (tmp_path / "test.json").open("r") as f_p: + data2 = json.load(f_p, object_hook=partseg_object_hook) + assert data2["value1"] == RadiusType.R2D + assert data2["value2"] == RadiusType.NO + assert data2["value3"] == NotificationSeverity.ERROR + + def test_dataclass_serialze(self, tmp_path): + data = {"value": SampleDataclass(1, "text")} + with (tmp_path / "test.json").open("w") as f_p: + json.dump(data, f_p, cls=PartSegEncoder) + with (tmp_path / "test.json").open("r") as f_p: + data2 = json.load(f_p, object_hook=partseg_object_hook) + + assert isinstance(data2["value"], SampleDataclass) + assert data2["value"] == SampleDataclass(1, "text") + + def test_pydantic_serialize(self, tmp_path): + data = { + "color1": Colormap(colors=[[0, 0, 0], [0, 0, 0]], controls=[0, 1]), + "other": SamplePydantic(sample_int=1, sample_str="text", sample_dataclass=SampleDataclass(1, "text")), + } + with (tmp_path / "test.json").open("w") as f_p: + json.dump(data, f_p, cls=PartSegEncoder) + with (tmp_path / "test.json").open("r") as f_p: + data2 = json.load(f_p, object_hook=partseg_object_hook) + assert data2["color1"] == Colormap(colors=[[0, 0, 0], [0, 0, 0]], controls=[0, 1]) + assert isinstance(data2["other"], SamplePydantic) + assert isinstance(data2["other"].sample_dataclass, SampleDataclass) + + def test_numpy_serialize(self, tmp_path): + data = {"arr": np.arange(10), "f": np.float32(0.1), "i": np.int16(1000)} + with (tmp_path / "test.json").open("w") as f_p: + json.dump(data, f_p, cls=PartSegEncoder) + with (tmp_path / "test.json").open("r") as f_p: + data2 = json.load(f_p, object_hook=partseg_object_hook) + assert data2["arr"] == list(range(10)) + assert np.isclose(data["f"], 0.1) + assert data2["i"] == 1000 + + def test_class_with_as_dict(self, tmp_path): + data = {"d": SampleAsDict(1, 10)} + with (tmp_path / "test.json").open("w") as f_p: + json.dump(data, f_p, cls=PartSegEncoder) + with (tmp_path / "test.json").open("r") as f_p: + data2 = json.load(f_p, object_hook=partseg_object_hook) + assert isinstance(data2["d"], SampleAsDict) + assert data2["d"].value1 == data["d"].value1 + assert data2["d"].value2 == data["d"].value2 + + def test_sub_class_serialization(self, tmp_path): + ob = DummyClassForTest.DummySubClassForTest(1, 2) + with (tmp_path / "test.json").open("w") as f_p: + json.dump(ob, f_p, cls=PartSegEncoder) + with (tmp_path / "test.json").open("r") as f_p: + ob2 = json.load(f_p, object_hook=partseg_object_hook) + assert ob2.data1 == 1 + assert ob2.data2 == 2 + + +def test_add_class_info_pydantic(clean_register): + @register_class + class SampleClass(BaseModel): + field: int = 1 + + dkt = {} + add_class_info(SampleClass(), dkt) + assert "__class__" in dkt + assert class_to_str(SampleClass) == dkt["__class__"] + assert len(dkt["__class_version_dkt__"]) == 1 + assert dkt["__class_version_dkt__"][class_to_str(SampleClass)] == "0.0.0" + + +def test_add_class_info_enum(clean_register): + @register_class + class SampleEnum(Enum): + field = 1 + + dkt = {} + add_class_info(SampleEnum.field, dkt) + assert "__class__" in dkt + assert class_to_str(SampleEnum) == dkt["__class__"] + assert len(dkt["__class_version_dkt__"]) == 1 + assert dkt["__class_version_dkt__"][class_to_str(SampleEnum)] == "0.0.0" + + +class TestPartSegObjectHook: + def test_no_inheritance_read(self, clean_register, tmp_path): + @register_class(version="0.0.1", migrations=[("0.0.1", rename_key("field", "field1"))]) + class BaseClass(BaseModel): + field1: int = 1 + + @register_class( + version="0.0.1", migrations=[("0.0.1", rename_key("field", "field1"))], use_parent_migrations=False + ) + class MainClass(BaseClass): + field2: int = 5 + + data_str = """ + {"field": 1, "field2": 5, + "__class__": + "test_PartSegCore.test_json_hooks.TestPartSegObjectHook.test_no_inheritance_read..MainClass", + "__class_version_dkt__": { + "test_PartSegCore.test_json_hooks.TestPartSegObjectHook.test_no_inheritance_read..MainClass": "0.0.0", + "test_PartSegCore.test_json_hooks.TestPartSegObjectHook.test_no_inheritance_read..BaseClass": "0.0.0" + }} + """ + + ob = json.loads(data_str, object_hook=partseg_object_hook) + assert isinstance(ob, MainClass) + + +class DummyClassForTest: + class DummySubClassForTest: + def __init__(self, data1, data2): + self.data1, self.data2 = data1, data2 + + def as_dict(self): + return {"data1": self.data1, "data2": self.data2} diff --git a/package/tests/test_PartSegCore/test_mask_create.py b/package/tests/test_PartSegCore/test_mask_create.py index 75909f45f..4ac2dd7af 100644 --- a/package/tests/test_PartSegCore/test_mask_create.py +++ b/package/tests/test_PartSegCore/test_mask_create.py @@ -291,7 +291,7 @@ def test_save_component_fill_holes(self): save_components=True, clip_to_mask=True, ) - mp2 = mp.replace_(fill_holes=RadiusType.R3D) + mp2 = mp.copy(update=dict(fill_holes=RadiusType.R3D)) mask1 = calculate_mask(mp, mask2, None, (1, 1, 1)) assert np.all(mask == mask1) mask1 = calculate_mask(mp2, mask2, None, (1, 1, 1)) @@ -376,7 +376,7 @@ def test_clip(self): mask2[:] = 0 mask2[4:6, 4:6, 4:6] = 1 mask3 = calculate_mask( - mp.replace_(**{"dilate": RadiusType.NO, "dilate_radius": 0, "reversed_mask": True}), mask2, mask, (1, 1, 1) + mp.copy(update={"dilate": RadiusType.NO, "dilate_radius": 0, "reversed_mask": True}), mask2, mask, (1, 1, 1) ) mask[mask2 == 1] = 0 assert np.all(mask3 == mask) diff --git a/package/tests/test_PartSegCore/test_measurements.py b/package/tests/test_PartSegCore/test_measurements.py index d3f7adae9..341aa645f 100644 --- a/package/tests/test_PartSegCore/test_measurements.py +++ b/package/tests/test_PartSegCore/test_measurements.py @@ -12,7 +12,7 @@ import pytest from sympy import symbols -from PartSegCore.algorithm_describe_base import AlgorithmProperty, ROIExtractionProfile +from PartSegCore.algorithm_describe_base import ROIExtractionProfile from PartSegCore.analysis import load_metadata from PartSegCore.analysis.measurement_base import AreaType, Leaf, MeasurementEntry, Node, PerComponent from PartSegCore.analysis.measurement_calculation import ( @@ -50,11 +50,11 @@ Voxels, ) from PartSegCore.autofit import density_mass_center -from PartSegCore.channel_class import Channel from PartSegCore.roi_info import ROIInfo from PartSegCore.segmentation.restartable_segmentation_algorithms import LowerThresholdAlgorithm from PartSegCore.universal_const import UNIT_SCALE, Units -from PartSegImage import Image +from PartSegCore.utils import BaseModel +from PartSegImage import Channel, Image def get_cube_array(): @@ -111,45 +111,46 @@ def get_two_component_mask(): class TestLeaf: def test_channel_calc(self, monkeypatch): + class SampleModel(BaseModel): + value: int = 1 + ch: Channel = 1 + mock = MagicMock() + mock.__new_style__ = True + mock.__argument_class__ = BaseModel mock.get_fields = MagicMock(return_value=[]) - leaf = Leaf(name="aa", dict={}) + leaf = Leaf(name="aa") assert leaf.get_channel_num({"aa": mock}) == set() - leaf = Leaf(name="aa", dict={}, channel=Channel(1)) - assert leaf.get_channel_num({"aa": mock}) == {1} - mock.get_fields = MagicMock( - return_value=[ - "eee", - AlgorithmProperty("value", "Value", 1), - AlgorithmProperty("ch", "Ch", 1, value_type=Channel), - ] - ) - leaf = Leaf(name="aa", dict={"value": 15, "ch": 3}) - assert leaf.get_channel_num({"aa": mock}) == {3} - leaf = Leaf(name="aa", dict={"value": 15, "ch": 3}, channel=Channel(1)) - assert leaf.get_channel_num({"aa": mock}) == {1, 3} + leaf = Leaf(name="aa", channel=Channel(1)) + assert leaf.get_channel_num({"aa": mock}) == {Channel(1)} + + mock.__argument_class__ = SampleModel + leaf = Leaf(name="aa", parameters={"value": 15, "ch": 3}) + assert leaf.get_channel_num({"aa": mock}) == {Channel(3)} + leaf = Leaf(name="aa", parameters={"value": 15, "ch": 3}, channel=Channel(1)) + assert leaf.get_channel_num({"aa": mock}) == {Channel(1), Channel(3)} def test_pretty_print(self, monkeypatch): mock = MagicMock() mock.get_fields = MagicMock(return_value=[]) - leaf = Leaf(name="aa", dict={}) + leaf = Leaf(name="aa") text = leaf.pretty_print({"aa": mock}) assert "ROI" not in text assert "Mask" not in text assert "per component" not in text assert "mean component" not in text assert "to the power" not in text - assert "per component" in Leaf(name="aa", dict={}, per_component=PerComponent.Yes).pretty_print({"aa": mock}) - assert "mean component" in Leaf(name="aa", dict={}, per_component=PerComponent.Mean).pretty_print({"aa": mock}) - assert "to the power" not in Leaf(name="aa", dict={}, power=1).pretty_print({"aa": mock}) - assert "to the power 2" in Leaf(name="aa", dict={}, power=2).pretty_print({"aa": mock}) + assert "per component" in Leaf(name="aa", per_component=PerComponent.Yes).pretty_print({"aa": mock}) + assert "mean component" in Leaf(name="aa", per_component=PerComponent.Mean).pretty_print({"aa": mock}) + assert "to the power" not in Leaf(name="aa", power=1).pretty_print({"aa": mock}) + assert "to the power 2" in Leaf(name="aa", power=2).pretty_print({"aa": mock}) monkeypatch.setattr(mock, "__module__", "PartSegCore.test") - assert Leaf(name="aa", dict={}).pretty_print({"aa": mock})[0] != "[" + assert Leaf(name="aa").pretty_print({"aa": mock})[0] != "[" monkeypatch.setattr(mock, "__module__", "PartSegPlugin.submodule") - assert Leaf(name="aa", dict={}).pretty_print({"aa": mock}).startswith("[PartSegPlugin]") + assert Leaf(name="aa").pretty_print({"aa": mock}).startswith("[PartSegPlugin]") monkeypatch.setattr(sys, "frozen", True, raising=False) monkeypatch.setattr(mock, "__module__", "plugins.PartSegPlugin.submodule") - assert Leaf(name="aa", dict={}).pretty_print({"aa": mock}).startswith("[PartSegPlugin]") + assert Leaf(name="aa").pretty_print({"aa": mock}).startswith("[PartSegPlugin]") class TestDiameter: @@ -864,7 +865,7 @@ def test_cube_zero(self, cube_image, d_mask, d_seg): voxel_size=cube_image.voxel_size, result_scalar=1, distance_from_mask=d_mask, - distance_to_segmentation=d_seg, + distance_to_roi=d_seg, ) == 0 ) @@ -889,7 +890,7 @@ def test_cube(self, cube_image, d_mask, d_seg, dist): voxel_size=cube_image.voxel_size, result_scalar=1, distance_from_mask=d_mask, - distance_to_segmentation=d_seg, + distance_to_roi=d_seg, ) == dist ) @@ -921,7 +922,7 @@ def test_two_components_center(self, comp1, comp2, two_comp_img, area_gen): voxel_size=two_comp_img.voxel_size, result_scalar=1, distance_from_mask=comp1, - distance_to_segmentation=comp2, + distance_to_roi=comp2, ), np.sqrt(np.sum(((mask_mid - area_mid) * (100, 50, 50)) ** 2)), ) @@ -1533,7 +1534,7 @@ def test_cube_volume_area_type(self): ), ), ] - profile = MeasurementProfile("statistic", statistics) + profile = MeasurementProfile(name="statistic", chosen_fields=statistics) result = profile.calculate( image, 0, @@ -1566,7 +1567,7 @@ def test_square_volume_area_type(self): ), ), ] - profile = MeasurementProfile("statistic", statistics) + profile = MeasurementProfile(name="statistic", chosen_fields=statistics) result = profile.calculate( image, 0, @@ -1603,7 +1604,7 @@ def test_cube_pixel_sum_area_type(self): ), ), ] - profile = MeasurementProfile("statistic", statistics) + profile = MeasurementProfile(name="statistic", chosen_fields=statistics) result = profile.calculate( image, 0, @@ -1637,7 +1638,7 @@ def test_cube_surface_area_type(self): ), ), ] - profile = MeasurementProfile("statistic", statistics) + profile = MeasurementProfile(name="statistic", chosen_fields=statistics) result = profile.calculate( image, 0, @@ -1719,7 +1720,7 @@ def test_cube_density(self): ), ), ] - profile = MeasurementProfile("statistic", statistics) + profile = MeasurementProfile(name="statistic", chosen_fields=statistics) result = profile.calculate( image, 0, @@ -1765,7 +1766,7 @@ def test_cube_volume_power(self): ), ), ] - profile = MeasurementProfile("statistic", statistics) + profile = MeasurementProfile(name="statistic", chosen_fields=statistics) result = profile.calculate( image, 0, @@ -1842,7 +1843,7 @@ def test_per_component_cache_collision(self): ), ] - profile = MeasurementProfile("statistic", statistics) + profile = MeasurementProfile(name="statistic", chosen_fields=statistics) result = profile.calculate( image, 0, @@ -1917,7 +1918,7 @@ def test_proportion(self): ), MeasurementEntry(name="Density per component", calculation_tree=Node(left=leaf4, op="/", right=leaf1)), ] - profile = MeasurementProfile("statistic", statistics) + profile = MeasurementProfile(name="statistic", chosen_fields=statistics) result = profile.calculate( image, 0, @@ -2139,9 +2140,9 @@ def test_variants(self, feature, distance): @pytest.fixture def roi_to_roi_extract(): parameters = LowerThresholdAlgorithm.get_default_values() - parameters["threshold"]["values"]["threshold"] = 1 - parameters["minimum_size"] = 1 - parameters["channel"] = 1 + parameters.threshold.values.threshold = 1 + parameters.minimum_size = 1 + parameters.channel = 1 return ROIExtractionProfile(name="default", algorithm=LowerThresholdAlgorithm.get_name(), values=parameters) @@ -2283,7 +2284,7 @@ def test_all_methods(method, dtype): roi_annotation={}, bounds_info=roi_info.bound_info, _component_num=1, - **method.get_default_values(), + **dict(method.get_default_values()), ) if method.get_units(3) != "str": float(res) @@ -2307,17 +2308,17 @@ def test_per_component(method, area): MeasurementEntry( name="Measurement", calculation_tree=method.get_starting_leaf().replace_( - per_component=PerComponent.No, area=area, dict=method.get_default_values() + per_component=PerComponent.No, area=area, parameters=method.get_default_values() ), ), MeasurementEntry( name="Measurement per component", calculation_tree=method.get_starting_leaf().replace_( - per_component=PerComponent.Yes, area=area, dict=method.get_default_values() + per_component=PerComponent.Yes, area=area, parameters=method.get_default_values() ), ), ] - profile = MeasurementProfile("statistic", statistics) + profile = MeasurementProfile(name="statistic", chosen_fields=statistics) result = profile.calculate( image, 0, diff --git a/package/tests/test_PartSegCore/test_pipeline.py b/package/tests/test_PartSegCore/test_pipeline.py index 899f890ea..47c4a5940 100644 --- a/package/tests/test_PartSegCore/test_pipeline.py +++ b/package/tests/test_PartSegCore/test_pipeline.py @@ -8,10 +8,10 @@ from PartSegCore.algorithm_describe_base import ROIExtractionProfile from PartSegCore.analysis import ( + AnalysisAlgorithmSelection, ProjectTuple, SegmentationPipeline, SegmentationPipelineElement, - analysis_algorithm_dict, ) from PartSegCore.analysis.calculate_pipeline import calculate_pipeline from PartSegCore.analysis.load_functions import LoadProject @@ -22,7 +22,7 @@ @pytest.mark.parametrize("channel", [0, 1]) def test_simple(image, algorithm_parameters, channel): algorithm_parameters["values"]["channel"] = channel - algorithm = analysis_algorithm_dict[algorithm_parameters["algorithm_name"]]() + algorithm = AnalysisAlgorithmSelection[algorithm_parameters["algorithm_name"]]() algorithm.set_image(image) algorithm.set_parameters(**algorithm_parameters["values"]) result = algorithm.calculation_run(lambda x, y: None) @@ -30,7 +30,7 @@ def test_simple(image, algorithm_parameters, channel): def test_pipeline_manual(image, algorithm_parameters, mask_property): - algorithm = analysis_algorithm_dict[algorithm_parameters["algorithm_name"]]() + algorithm = AnalysisAlgorithmSelection[algorithm_parameters["algorithm_name"]]() algorithm.set_image(image) algorithm.set_parameters(**algorithm_parameters["values"]) result = algorithm.calculation_run(lambda x, y: None) diff --git a/package/tests/test_PartSegCore/test_segmentation.py b/package/tests/test_PartSegCore/test_segmentation.py index a117421d7..0158a1311 100644 --- a/package/tests/test_PartSegCore/test_segmentation.py +++ b/package/tests/test_PartSegCore/test_segmentation.py @@ -7,9 +7,10 @@ import numpy as np import pytest +from pydantic import BaseModel from PartSegCore.algorithm_describe_base import ROIExtractionProfile -from PartSegCore.analysis.algorithm_description import analysis_algorithm_dict +from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection from PartSegCore.analysis.analysis_utils import SegmentationPipeline, SegmentationPipelineElement from PartSegCore.analysis.calculate_pipeline import calculate_pipeline from PartSegCore.convex_fill import _convex_fill, convex_fill @@ -18,8 +19,8 @@ from PartSegCore.roi_info import BoundInfo, ROIInfo from PartSegCore.segmentation import ROIExtractionAlgorithm, algorithm_base from PartSegCore.segmentation import restartable_segmentation_algorithms as sa -from PartSegCore.segmentation.noise_filtering import noise_filtering_dict -from PartSegCore.segmentation.watershed import flow_dict +from PartSegCore.segmentation.noise_filtering import NoiseFilterSelection +from PartSegCore.segmentation.watershed import FlowMethodSelection from PartSegImage import Image @@ -80,14 +81,14 @@ def empty(_s: str, _i: int): """mock function for callback""" -@pytest.mark.parametrize("algorithm_name", analysis_algorithm_dict.keys()) +@pytest.mark.parametrize("algorithm_name", AnalysisAlgorithmSelection.__register__.keys()) def test_base_parameters(algorithm_name): - algorithm_class = analysis_algorithm_dict[algorithm_name] + algorithm_class = AnalysisAlgorithmSelection[algorithm_name] assert algorithm_class.get_name() == algorithm_name algorithm_class: Type[ROIExtractionAlgorithm] obj = algorithm_class() values = algorithm_class.get_default_values() - obj.set_parameters(**values) + obj.set_parameters(values) parameters = obj.get_segmentation_profile() assert parameters.algorithm == algorithm_name assert parameters.values == values @@ -100,8 +101,8 @@ def check_result(self, result, sizes, op, parameters): assert result.parameters.values == parameters assert result.parameters.algorithm == self.get_algorithm_class().get_name() - def get_parameters(self) -> dict: - if hasattr(self, "parameters") and isinstance(self.parameters, dict): + def get_parameters(self) -> BaseModel: + if hasattr(self, "parameters") and isinstance(self.parameters, (dict, BaseModel)): return deepcopy(self.parameters) raise NotImplementedError @@ -128,12 +129,12 @@ def test_simple(self): alg: ROIExtractionAlgorithm = self.get_algorithm_class()() parameters = self.get_parameters() alg.set_image(image) - alg.set_parameters(**parameters) + alg.set_parameters(parameters) result = alg.calculation_run(empty) self.check_result(result, [96000, 72000], operator.eq, parameters) - parameters["threshold"]["values"]["threshold"] += self.get_shift() - alg.set_parameters(**parameters) + parameters.threshold.values.threshold += self.get_shift() + alg.set_parameters(parameters) result = alg.calculation_run(empty) self.check_result(result, [192000], operator.eq, parameters) @@ -141,26 +142,26 @@ def test_side_connection(self): image = self.get_side_object() alg: ROIExtractionAlgorithm = self.get_algorithm_class()() parameters = self.get_parameters() - parameters["side_connection"] = True + parameters.side_connection = True alg.set_image(image) - alg.set_parameters(**parameters) + alg.set_parameters(parameters) result = alg.calculation_run(empty) self.check_result(result, [96000 + 5, 72000 + 5], operator.eq, parameters) - parameters["side_connection"] = False - alg.set_parameters(**parameters) + parameters.side_connection = False + alg.set_parameters(parameters) result = alg.calculation_run(empty) self.check_result(result, [96000 + 5 + 72000 + 5], operator.eq, parameters) class TestLowerThreshold(BaseOneThreshold): - parameters = { - "channel": 0, - "minimum_size": 30000, - "threshold": {"name": "Manual", "values": {"threshold": 45}}, - "noise_filtering": {"name": "None", "values": {}}, - "side_connection": False, - } + parameters = sa.LowerThresholdAlgorithm.__argument_class__( + channel=0, + minimum_size=30000, + threshold={"name": "Manual", "values": {"threshold": 45}}, + noise_filtering={"name": "None", "values": {}}, + side_connection=False, + ) shift = -6 @staticmethod @@ -176,13 +177,13 @@ def get_algorithm_class(self) -> Type[ROIExtractionAlgorithm]: class TestUpperThreshold(BaseOneThreshold): - parameters = { - "channel": 0, - "minimum_size": 30000, - "threshold": {"name": "Manual", "values": {"threshold": 55}}, - "noise_filtering": {"name": "None", "values": {}}, - "side_connection": False, - } + parameters = sa.UpperThresholdAlgorithm.__argument_class__( + channel=0, + minimum_size=30000, + threshold={"name": "Manual", "values": {"threshold": 55}}, + noise_filtering={"name": "None", "values": {}}, + side_connection=False, + ) shift = 6 @staticmethod @@ -201,15 +202,17 @@ class TestRangeThresholdAlgorithm: def test_simple(self): image = get_two_parts() alg = sa.RangeThresholdAlgorithm() - parameters = { - "lower_threshold": 45, - "upper_threshold": 60, - "channel": 0, - "minimum_size": 8000, - "noise_filtering": {"name": "None", "values": {}}, - "side_connection": False, - } - alg.set_parameters(**parameters) + parameters = sa.RangeThresholdAlgorithm.__argument_class__( + threshold={ + "lower_threshold": 45, + "upper_threshold": 60, + }, + channel=0, + minimum_size=8000, + noise_filtering={"name": "None", "values": {}}, + side_connection=False, + ) + alg.set_parameters(parameters) alg.set_image(image) result = alg.calculation_run(empty) assert np.max(result.roi) == 2 @@ -219,8 +222,8 @@ def test_simple(self): assert result.parameters.values == parameters assert result.parameters.algorithm == alg.get_name() - parameters["lower_threshold"] -= 6 - alg.set_parameters(**parameters) + parameters.threshold.lower_threshold -= 6 + alg.set_parameters(parameters) result = alg.calculation_run(empty) assert np.max(result.roi) == 1 assert np.bincount(result.roi.flat)[1] == 30 * 80 * 80 - 20 * 50 * 70 @@ -230,15 +233,17 @@ def test_simple(self): def test_side_connection(self): image = get_two_parts_side() alg = sa.RangeThresholdAlgorithm() - parameters = { - "lower_threshold": 45, - "upper_threshold": 60, - "channel": 0, - "minimum_size": 8000, - "noise_filtering": {"name": "None", "values": {}}, - "side_connection": True, - } - alg.set_parameters(**parameters) + parameters = sa.RangeThresholdAlgorithm.__argument_class__( + threshold={ + "lower_threshold": 45, + "upper_threshold": 60, + }, + channel=0, + minimum_size=8000, + noise_filtering={"name": "None", "values": {}}, + side_connection=True, + ) + alg.set_parameters(parameters) alg.set_image(image) result = alg.calculation_run(empty) assert np.max(result.roi) == 2 @@ -249,8 +254,8 @@ def test_side_connection(self): assert result.parameters.values == parameters assert result.parameters.algorithm == alg.get_name() - parameters["side_connection"] = False - alg.set_parameters(**parameters) + parameters.side_connection = False + alg.set_parameters(parameters) result = alg.calculation_run(empty) assert np.max(result.roi) == 1 assert np.bincount(result.roi.flat)[1] == 30 * 70 * 80 - 20 * 50 * 70 + 10 @@ -259,7 +264,7 @@ def test_side_connection(self): class BaseFlowThreshold(BaseThreshold, ABC): # pylint: disable=W0223 - @pytest.mark.parametrize("sprawl_algorithm_name", flow_dict.keys()) + @pytest.mark.parametrize("sprawl_algorithm_name", FlowMethodSelection.__register__.keys()) @pytest.mark.parametrize("compare_op", [operator.eq, operator.ge]) @pytest.mark.parametrize("components", [2] + list(range(3, 15, 2))) def test_multiple(self, sprawl_algorithm_name, compare_op, components): @@ -267,24 +272,26 @@ def test_multiple(self, sprawl_algorithm_name, compare_op, components): parameters = self.get_parameters() image = self.get_multiple_part(components) alg.set_image(image) - sprawl_algorithm = flow_dict[sprawl_algorithm_name] - parameters["sprawl_type"] = {"name": sprawl_algorithm_name, "values": sprawl_algorithm.get_default_values()} + sprawl_algorithm = FlowMethodSelection[sprawl_algorithm_name] + parameters.flow_type = FlowMethodSelection( + name=sprawl_algorithm_name, values=sprawl_algorithm.get_default_values() + ) if compare_op(1, 0): - parameters["threshold"]["values"]["base_threshold"]["values"]["threshold"] += self.get_shift() - alg.set_parameters(**parameters) + parameters.threshold.values.base_threshold.values.threshold += self.get_shift() + alg.set_parameters(parameters) result = alg.calculation_run(empty) self.check_result(result, [4000] * components, compare_op, parameters) - @pytest.mark.parametrize("algorithm_name", flow_dict.keys()) + @pytest.mark.parametrize("algorithm_name", FlowMethodSelection.__register__.keys()) def test_side_connection(self, algorithm_name): image = self.get_side_object() alg = self.get_algorithm_class()() parameters = self.get_parameters() - parameters["side_connection"] = True + parameters.side_connection = True alg.set_image(image) - val = flow_dict[algorithm_name] - parameters["sprawl_type"] = {"name": algorithm_name, "values": val.get_default_values()} - alg.set_parameters(**parameters) + val = FlowMethodSelection[algorithm_name] + parameters.flow_type = FlowMethodSelection(name=algorithm_name, values=val.get_default_values()) + alg.set_parameters(parameters) result = alg.calculation_run(empty) self.check_result(result, [96000 + 5, 72000 + 5], operator.eq, parameters) @@ -293,20 +300,22 @@ def get_multiple_part(self, parts_num): class TestLowerThresholdFlow(BaseFlowThreshold): - parameters = { - "channel": 0, - "minimum_size": 30, - "threshold": { - "name": "Base/Core", - "values": { - "core_threshold": {"name": "Manual", "values": {"threshold": 55}}, - "base_threshold": {"name": "Manual", "values": {"threshold": 45}}, + parameters = sa.LowerThresholdFlowAlgorithm.__argument_class__( + **{ + "channel": 0, + "minimum_size": 30, + "threshold": { + "name": "Base/Core", + "values": { + "core_threshold": {"name": "Manual", "values": {"threshold": 55}}, + "base_threshold": {"name": "Manual", "values": {"threshold": 45}}, + }, }, - }, - "noise_filtering": {"name": "None", "values": {}}, - "side_connection": False, - "sprawl_type": {"name": "Euclidean sprawl", "values": {}}, - } + "noise_filtering": {"name": "None", "values": {}}, + "side_connection": False, + "flow_type": {"name": "Euclidean", "values": {}}, + } + ) shift = -6 get_base_object = staticmethod(get_two_parts) get_side_object = staticmethod(get_two_parts_side) @@ -317,20 +326,22 @@ def get_algorithm_class(self) -> Type[ROIExtractionAlgorithm]: class TestUpperThresholdFlow(BaseFlowThreshold): - parameters = { - "channel": 0, - "minimum_size": 30, - "threshold": { - "name": "Base/Core", - "values": { - "core_threshold": {"name": "Manual", "values": {"threshold": 45}}, - "base_threshold": {"name": "Manual", "values": {"threshold": 55}}, + parameters = sa.UpperThresholdFlowAlgorithm.__argument_class__( + **{ + "channel": 0, + "minimum_size": 30, + "threshold": { + "name": "Base/Core", + "values": { + "core_threshold": {"name": "Manual", "values": {"threshold": 45}}, + "base_threshold": {"name": "Manual", "values": {"threshold": 55}}, + }, }, - }, - "noise_filtering": {"name": "None", "values": {}}, - "side_connection": False, - "sprawl_type": {"name": "Euclidean sprawl", "values": {}}, - } + "noise_filtering": {"name": "None", "values": {}}, + "side_connection": False, + "flow_type": {"name": "Euclidean", "values": {}}, + } + ) shift = 6 get_base_object = staticmethod(get_two_parts_reversed) get_side_object = staticmethod(get_two_parts_side_reversed) @@ -663,9 +674,9 @@ def test_pipeline_simple(self, use_mask): class TestNoiseFiltering: - @pytest.mark.parametrize("algorithm_name", noise_filtering_dict.keys()) + @pytest.mark.parametrize("algorithm_name", NoiseFilterSelection.__register__.keys()) def test_base(self, algorithm_name): - noise_remove_algorithm = noise_filtering_dict[algorithm_name] + noise_remove_algorithm = NoiseFilterSelection[algorithm_name] data = get_two_parts_array()[0, ..., 0] noise_remove_algorithm.noise_filter(data, (1, 1, 1), noise_remove_algorithm.get_default_values()) diff --git a/package/tests/test_PartSegCore/test_segmentation_proceses.py b/package/tests/test_PartSegCore/test_segmentation_proceses.py index b235ebf89..992b0697a 100644 --- a/package/tests/test_PartSegCore/test_segmentation_proceses.py +++ b/package/tests/test_PartSegCore/test_segmentation_proceses.py @@ -6,10 +6,10 @@ import pytest from PartSegCore.algorithm_describe_base import ROIExtractionProfile -from PartSegCore.analysis.algorithm_description import analysis_algorithm_dict -from PartSegCore.analysis.load_functions import UpdateLoadedMetadataAnalysis -from PartSegCore.json_hooks import check_loaded_dict +from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection +from PartSegCore.io_utils import load_metadata_base from PartSegCore.segmentation.algorithm_base import ROIExtractionAlgorithm +from PartSegCore.utils import check_loaded_dict from PartSegImage import TiffImageReader @@ -22,7 +22,7 @@ def test_profile_execute(self, data_test_dir): profile_path = os.path.join(data_test_dir, "segment_profile_test.json") # noinspection PyBroadException try: - data = UpdateLoadedMetadataAnalysis.load_json_data(profile_path) + data = load_metadata_base(profile_path) assert check_loaded_dict(data) except Exception: # pylint: disable=W0703 pytest.fail("Fail in loading profile") @@ -34,9 +34,9 @@ def test_profile_execute(self, data_test_dir): val: ROIExtractionProfile for val in data.values(): - algorithm: ROIExtractionAlgorithm = analysis_algorithm_dict[val.algorithm]() + algorithm: ROIExtractionAlgorithm = AnalysisAlgorithmSelection[val.algorithm]() algorithm.set_image(image) algorithm.set_mask(image.mask.squeeze()) - algorithm.set_parameters(**val.values) + algorithm.set_parameters(val.values) result = algorithm.calculation_run(empty) assert np.max(result.roi) == 2 diff --git a/package/tests/test_PartSegCore/test_utils.py b/package/tests/test_PartSegCore/test_utils.py index b0c453ac9..36a685912 100644 --- a/package/tests/test_PartSegCore/test_utils.py +++ b/package/tests/test_PartSegCore/test_utils.py @@ -1,4 +1,11 @@ -from PartSegCore.utils import CallbackFun, CallbackMethod, get_callback +# pylint: disable=R0201 +import json +from unittest.mock import MagicMock + +import pytest + +from PartSegCore.json_hooks import PartSegEncoder, partseg_object_hook +from PartSegCore.utils import CallbackFun, CallbackMethod, EventedDict, ProfileDict, get_callback, recursive_update_dict def test_callback_fun(): @@ -45,3 +52,202 @@ def fun(self): # pylint: disable=R0201 assert isinstance(get_callback(call_fun), CallbackFun) assert isinstance(get_callback(a.fun), CallbackMethod) + + +def test_recursive_update_dict_basic(): + dict1 = {"a": 1, "b": 2} + dict2 = {"b": 3, "c": 4} + dict1_copy = dict1.copy() + dict1.update(dict2) + recursive_update_dict(dict1_copy, dict2) + assert dict1 == dict1_copy + + +def test_recursive_update_dict(): + dict1 = {"a": {"k": 1, "l": 2}, "b": {"k": 1, "l": 2}} + dict2 = {"a": {"m": 3, "l": 4}, "b": 3, "c": 4} + recursive_update_dict(dict1, dict2) + assert dict1 == {"a": {"k": 1, "l": 4, "m": 3}, "b": 3, "c": 4} + + +class TestEventedDict: + def test_simple_add(self): + receiver = MagicMock() + + dkt = EventedDict() + dkt.setted.connect(receiver.set) + dkt.deleted.connect(receiver.delete) + dkt["a"] = 1 + assert dkt["a"] == 1 + assert receiver.set.call_count == 1 + assert "'a': 1" in str(dkt) + assert "'a': 1" in repr(dkt) + dkt["a"] = 2 + assert dkt["a"] == 2 + assert receiver.set.call_count == 2 + assert len(dkt) == 1 + del dkt["a"] + assert receiver.set.call_count == 2 + assert receiver.delete.call_count == 1 + assert len(dkt) == 0 + + def test_simple_add_remove(self): + callback_list = [] + + def callback_add(): + callback_list.append(1) + + def callback_delete(): + callback_list.append(2) + + dkt = EventedDict() + dkt.setted.connect(callback_add) + dkt.deleted.connect(callback_delete) + + dkt[1] = 1 + dkt[2] = 1 + assert len(dkt) == 2 + assert callback_list == [1, 1] + del dkt[1] + assert len(dkt) == 1 + assert callback_list == [1, 1, 2] + + def test_nested_evented(self): + dkt = EventedDict(bar={"foo": {"baz": 1}}) + assert isinstance(dkt["bar"], EventedDict) + assert isinstance(dkt["bar"]["foo"], EventedDict) + assert dkt["bar"]["foo"]["baz"] == 1 + + dkt["baz"] = {"bar": {"foo": 1}} + assert isinstance(dkt["baz"], EventedDict) + assert isinstance(dkt["baz"]["bar"], EventedDict) + assert dkt["baz"]["bar"]["foo"] == 1 + + def test_serialize(self, tmp_path): + dkt = EventedDict( + **{"a": {"b": {"c": 1, "d": 2, "e": 3}, "f": 1}, "g": {"h": {"i": 1, "j": 2}, "k": [6, 7, 8]}} + ) + with (tmp_path / "test_dict.json").open("w") as f_p: + json.dump(dkt, f_p, cls=PartSegEncoder) + + with (tmp_path / "test_dict.json").open("r") as f_p: + dkt2 = json.load(f_p, object_hook=partseg_object_hook) + + assert isinstance(dkt2, EventedDict) + assert isinstance(dkt2["a"], EventedDict) + assert dkt["g"]["k"] == [6, 7, 8] + + def test_signal_names(self): + receiver = MagicMock() + dkt = EventedDict(baz={"foo": 1}) + dkt.setted.connect(receiver.set) + dkt.deleted.connect(receiver.deleted) + dkt["foo"] = 1 + assert receiver.set.call_count == 1 + receiver.set.assert_called_with("foo") + dkt["bar"] = EventedDict() + assert receiver.set.call_count == 2 + receiver.set.assert_called_with("bar") + dkt["bar"]["baz"] = 1 + assert receiver.set.call_count == 3 + receiver.set.assert_called_with("bar.baz") + dkt["baz"]["foo"] = 2 + assert receiver.set.call_count == 4 + receiver.set.assert_called_with("baz.foo") + + del dkt["bar"]["baz"] + assert receiver.deleted.call_count == 1 + receiver.deleted.assert_called_with("bar.baz") + del dkt["bar"] + assert receiver.deleted.call_count == 2 + receiver.deleted.assert_called_with("bar") + + def test_propagate_signal(self): + receiver = MagicMock() + dkt = EventedDict(baz={"foo": 1}) + dkt.setted.connect(receiver.set) + dkt.deleted.connect(receiver.deleted) + dkt["baz"].base_key = "" + dkt["baz"]["foo"] = 2 + receiver.set.assert_called_with("foo") + receiver.set.assert_called_once() + del dkt["baz"]["foo"] + receiver.deleted.assert_called_with("foo") + receiver.deleted.assert_called_once() + + +class TestProfileDict: + def test_simple(self): + dkt = ProfileDict() + dkt.set("a.b.c", 1) + dkt.set("a.b.a", 2) + assert dkt.get("a.b.c") == 1 + with pytest.raises(KeyError): + dkt.get("a.b.d") + dkt.get("a.b.d", 3) + assert dkt.get("a.b.d") == 3 + assert dkt.get("a.b") == {"a": 2, "c": 1, "d": 3} + with pytest.raises(TypeError): + dkt.set("a.b.c.d", 3) + + def test_update(self): + dkt = ProfileDict() + dkt.update(a=1, b=2, c=3) + assert dkt.my_dict == {"a": 1, "b": 2, "c": 3} + dkt2 = ProfileDict() + dkt2.update(c=4, d={"a": 2, "e": 7}) + assert dkt2.get("d.e") == 7 + dkt.update(dkt2) + assert dkt.get("d.e") == 7 + assert dkt.get("c") == 4 + dkt.update({"g": 1, "h": 4}) + assert dkt.get("g") == 1 + dkt.update({"w": 1, "z": 4}, w=3) + assert dkt.get("w") == 3 + assert dkt.verify_data() + assert dkt.filter_data() == [] + dkt.set("e.h.l", {"aaa": 1, "__error__": True}) + assert not dkt.verify_data() + assert dkt.filter_data() == ["e.h"] + + def test_serialize(self, tmp_path): + dkt = ProfileDict() + dkt.set("a.b.c", 1) + dkt.set("a.b.a", 2) + with open(tmp_path / "test.json", "w") as f_p: + json.dump(dkt, f_p, cls=PartSegEncoder) + with open(tmp_path / "test.json") as f_p: + dkt2 = json.load(f_p, object_hook=partseg_object_hook) + + assert dkt.my_dict == dkt2.my_dict + + def test_callback(self): + def dummy_call(): + receiver.dummy() + + receiver = MagicMock() + + dkt = ProfileDict() + dkt.connect("", receiver.empty) + dkt.connect("", dummy_call) + dkt.connect("b", receiver.b) + dkt.connect(["d", "c"], receiver.dc) + + dkt.set("test.a", 1) + assert receiver.empty.call_count == 1 + assert receiver.dummy.call_count == 1 + receiver.empty.assert_called_with("a") + receiver.dummy.assert_called_with() + dkt.set("test.a", 1) + assert receiver.empty.call_count == 1 + receiver.b.assert_not_called() + dkt.set("test2.a", 1) + assert receiver.empty.call_count == 2 + receiver.b.assert_not_called() + dkt.set(["test", "b"], 1) + assert receiver.empty.call_count == 3 + assert receiver.b.call_count == 1 + dkt.set("test.d.c", 1) + receiver.dc.assert_called_once() + dkt.set("test.a", 2) + assert receiver.empty.call_count == 5 diff --git a/package/tests/test_data/notebook/neuron_types_measurment.json b/package/tests/test_data/notebook/neuron_types_measurment.json new file mode 100644 index 000000000..035f65643 --- /dev/null +++ b/package/tests/test_data/notebook/neuron_types_measurment.json @@ -0,0 +1,75 @@ +{ + "neuron_types": { + "__StatisticProfile__": true, + "name": "neuron_types", + "chosen_fields": [ + { + "__ReadOnly__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.StatisticEntry", + "name": "Mask Volume", + "calculation_tree": { + "__ReadOnly__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.Leaf", + "name": "Volume", + "dict": {}, + "power": 1.0, + "area": { + "__Enum__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.AreaType", + "value": 2 + }, + "per_component": { + "__Enum__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.PerComponent", + "value": 1 + } + } + }, + { + "__ReadOnly__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.StatisticEntry", + "name": "Segmentation Volume/Mask Volume", + "calculation_tree": { + "__ReadOnly__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.Node", + "left": { + "__ReadOnly__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.Leaf", + "name": "Volume", + "dict": {}, + "power": 1.0, + "area": { + "__Enum__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.AreaType", + "value": 1 + }, + "per_component": { + "__Enum__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.PerComponent", + "value": 1 + } + }, + "op": "/", + "right": { + "__ReadOnly__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.Leaf", + "name": "Volume", + "dict": {}, + "power": 1.0, + "area": { + "__Enum__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.AreaType", + "value": 2 + }, + "per_component": { + "__Enum__": true, + "__subtype__": "segmentation_analysis.statistics_calculation.PerComponent", + "value": 1 + } + } + } + } + ], + "name_prefix": "" + } +} diff --git a/package/tests/test_data/notebook/neuron_types_segmentation.json b/package/tests/test_data/notebook/neuron_types_segmentation.json new file mode 100644 index 000000000..d9d3c6b70 --- /dev/null +++ b/package/tests/test_data/notebook/neuron_types_segmentation.json @@ -0,0 +1,22 @@ +{ + "neuron_types": { + "__SegmentationProperty__": true, + "name": "neuron_types", + "algorithm": "Lower threshold", + "values": { + "threshold": { + "name": "Manual", + "values": { + "threshold": 30000 + } + }, + "channel": 2, + "minimum_size": 255, + "noise_removal": { + "name": "None", + "values": {} + }, + "side_connection": true + } + } +} diff --git a/package/tests/test_data/notebook/segment_data.json b/package/tests/test_data/notebook/segment_data.json new file mode 100644 index 000000000..fa89ae9a5 --- /dev/null +++ b/package/tests/test_data/notebook/segment_data.json @@ -0,0 +1 @@ +{"__SegmentationProfile__": true, "name": "", "algorithm": "Threshold", "values": {"channel": 3, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Manual", "values": {"threshold": 19000}}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "None", "values": {}}, "side_connection": false, "minimum_size": 8000, "use_convex": true}} diff --git a/package/tests/test_data/old_saves/0.10.4/analysis/batch_plans_save.json b/package/tests/test_data/old_saves/0.10.4/analysis/batch_plans_save.json new file mode 100644 index 000000000..36e58a5e8 --- /dev/null +++ b/package/tests/test_data/old_saves/0.10.4/analysis/batch_plans_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {}} diff --git a/package/tests/test_data/old_saves/0.10.4/analysis/segmentation_pipeline_save.json b/package/tests/test_data/old_saves/0.10.4/analysis/segmentation_pipeline_save.json new file mode 100644 index 000000000..36e58a5e8 --- /dev/null +++ b/package/tests/test_data/old_saves/0.10.4/analysis/segmentation_pipeline_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {}} diff --git a/package/tests/test_data/old_saves/0.10.4/analysis/segmentation_profiles_save.json b/package/tests/test_data/old_saves/0.10.4/analysis/segmentation_profiles_save.json new file mode 100644 index 000000000..36e58a5e8 --- /dev/null +++ b/package/tests/test_data/old_saves/0.10.4/analysis/segmentation_profiles_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {}} diff --git a/package/tests/test_data/old_saves/0.10.4/analysis/segmentation_settings.json b/package/tests/test_data/old_saves/0.10.4/analysis/segmentation_settings.json new file mode 100644 index 000000000..4ffb4b265 --- /dev/null +++ b/package/tests/test_data/old_saves/0.10.4/analysis/segmentation_settings.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"units_value": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "algorithm_widget_state": {"Lower threshold": {"channel": 1, "noise_filtering": {"None": {}}, "threshold": {"Manual": {"threshold": 8000}, "Huang": {"masked": true, "bins": 128}, "Triangle": {"masked": true, "bins": 256}}, "minimum_size": 8000, "side_connection": false}, "Upper threshold": {"channel": 0, "noise_filtering": {"None": {}}, "threshold": {"Manual": {"threshold": 8000}}, "minimum_size": 8000, "side_connection": false}, "Range threshold": {"channel": 0, "noise_filtering": {"None": {}}, "lower_threshold": 10000, "upper_threshold": 10000, "minimum_size": 8000, "side_connection": false}, "Lower threshold flow": {"channel": 0, "noise_filtering": {"None": {}}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000}}, "base_threshold": {"Manual": {"threshold": 8000}}}}, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false}}, "minimum_size": 8000, "side_connection": false}, "Upper threshold flow": {"channel": 0, "noise_filtering": {"None": {}}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000}}, "base_threshold": {"Manual": {"threshold": 8000}}}}, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false}}, "minimum_size": 8000, "side_connection": false}, "Multiple Otsu": {"channel": 0, "noise_filtering": {"None": {}}, "components": 2, "valley": true, "hist_num": 128}, "Border Rim": {"distance": 0.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}, "Split Mask o Part": {"num_of_parts": 2, "equal_volume": false}}, "algorithms": {"Lower threshold": {"channel": 1, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Triangle", "values": {"masked": true, "bins": 256}}, "minimum_size": 8000, "side_connection": false}, "Upper threshold": {}, "Range threshold": {}, "Lower threshold flow": {}, "Upper threshold flow": {}, "Multiple Otsu": {}, "Border Rim": {}, "Split Mask o Part": {}}, "current_algorithm": "Lower threshold", "multiple_files": 2, "io": {"history": ["/home/czaki/Projekty/partseg/test_data/stack1_components"], "open_directory": "/home/czaki/Projekty/partseg/test_data/stack1_components", "save_directory": "/home/czaki", "export_directory": "/home/czaki", "batch_plan_directory": "/home/czaki", "multiple_open_directory": "/home/czaki/Projekty/partseg/test_data/stack1_components", "open_filter": "Image with mask (*.tif *.tiff *.lsm)", "multiple_open_filter": "Image with mask (*.tif *.tiff *.lsm)"}, "mask_manager": {"mask_property": {"__Serializable__": true, "__subtype__": "PartSegCore.mask_create.MaskProperty", "dilate": {"__RadiusType__": true, "value": 0}, "dilate_radius": 0, "fill_holes": {"__RadiusType__": true, "value": 1}, "max_holes_size": -1, "save_components": false, "clip_to_mask": false, "reversed_mask": false}}}} diff --git a/package/tests/test_data/old_saves/0.10.4/analysis/statistic_profiles_save.json b/package/tests/test_data/old_saves/0.10.4/analysis/statistic_profiles_save.json new file mode 100644 index 000000000..36e58a5e8 --- /dev/null +++ b/package/tests/test_data/old_saves/0.10.4/analysis/statistic_profiles_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {}} diff --git a/package/tests/test_data/old_saves/0.10.4/analysis/view_settings.json b/package/tests/test_data/old_saves/0.10.4/analysis/view_settings.json new file mode 100644 index 000000000..b6f0d3ea9 --- /dev/null +++ b/package/tests/test_data/old_saves/0.10.4/analysis/view_settings.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"custom_colormap": {}, "custom_label_colors": {}, "raw_image": {"image_state": {"opacity": 1.0, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.stack_image_view.LabelEnum", "value": 1}, "only_border": true, "border_thick": 1}, "range_0": [0, 65000], "use_gauss_0": false, "gauss_radius_0": 1.0, "lock_0": false, "channels_count": 4, "cmap0": "BlackRed", "cmap1": "BlackGreen", "lock_1": false, "use_gauss_1": false, "cmap2": "BlackBlue", "lock_2": false, "use_gauss_2": false, "cmap3": "BlackMagenta", "lock_3": false, "use_gauss_3": false, "gauss_radius_1": 1, "gauss_radius_2": 1, "gauss_radius_3": 1, "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_0": 1.0, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_1": 1, "filter_radius_2": 1, "filter_radius_3": 1}, "mask_presentation": ["white", 1], "result_image": {"image_state": {"opacity": 1.0, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.stack_image_view.LabelEnum", "value": 1}, "only_border": true, "border_thick": 1}, "range_0": [0, 65000], "use_gauss_0": false, "gauss_radius_0": 1, "lock_0": false, "channels_count": 4, "cmap0": "BlackRed", "cmap1": "BlackGreen", "lock_1": false, "use_gauss_1": false, "cmap2": "BlackBlue", "lock_2": false, "use_gauss_2": false, "cmap3": "BlackMagenta", "lock_3": false, "use_gauss_3": false, "gauss_radius_1": 1, "gauss_radius_2": 1, "gauss_radius_3": 1, "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_0": 1, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_1": 1, "filter_radius_2": 1, "filter_radius_3": 1}, "custom_colors": [], "colormaps": ["BlackRed", "BlackGreen", "BlackBlue", "BlackMagenta", "Grayscale", "inferno", "magma", "plasma", "viridis"], "hide_left_panel": 2, "batch_window_geometry": "01d9d0cb00030000000000cb000000a00000059e00000394000000cb000000c50000059e0000039400000000000000000780000000cb000000c50000059e00000394", "main_window_geometry": "01d9d0cb00030000000008f70000009000000df60000035d000008f7000000b500000df60000035d00000001000000000780000008f7000000b500000df60000035d", "advanced_window_geometry": "01d9d0cb0003000000000a870000010200000d06000002e100000a870000010200000d06000002e10000000100000000078000000a870000010200000d06000002e1", "mask_presentation_color": [255, 255, 255], "mask_presentation_opacity": 1, "labels_used": "default"}} diff --git a/package/tests/test_data/old_saves/0.13.1/analysis/batch_plans_save.json b/package/tests/test_data/old_saves/0.13.1/analysis/batch_plans_save.json new file mode 100644 index 000000000..ebdf7bd99 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.1/analysis/batch_plans_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"test": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "aaa", "algorithm": "Border Rim", "values": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}, {"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.Save", "suffix": "_proj", "directory": "", "algorithm": "Project (*.tgz *.tbz2 *.gz *.bz2)", "short_name": "project", "values": {}}, "children": []}]}]}, "name": "test"}, "test2": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "test1", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Iso Data", "values": {"masked": true, "bins": 256}}, "minimum_size": 500, "side_connection": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}, {"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.Save", "suffix": "_proj2", "directory": "", "algorithm": "Project (*.tgz *.tbz2 *.gz *.bz2)", "short_name": "project", "values": {}}, "children": []}]}]}, "name": "test2"}, "test3": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "thr_10000", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 10000}}, "minimum_size": 500, "side_connection": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}, "name": "test3"}, "Chr1_ch1_G1_MSO_ME16_Triangle128_150_136_COS": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MaskSuffix", "name": "", "suffix": "_mask"}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "Chr1_Ch1_G1_MSO_ME16_Triangle128_150_136_COS", "algorithm": "Lower threshold with watershed", "values": {"channel": 0, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Maximum Entropy", "values": {"masked": true, "bins": 16}}, "base_threshold": {"name": "Triangle", "values": {"masked": true, "bins": 128}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 150, "reflective": true}}, "minimum_size": 136, "side_connection": true}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 1}, "measurement_profile": {"__MeasurementProfile__": true, "name": "Chr1_2020", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Components number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Pixel brightness sum per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Pixel brightness sum", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Mean pixel brightness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Mean pixel brightness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Maximum pixel brightness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Maximum pixel brightness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Minimum pixel brightness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Minimum pixel brightness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI First principal axis length per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "First principal axis length", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Second principal axis length per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Second principal axis length", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Third principal axis length per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Third principal axis length", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Compactness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Compactness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Sphericity per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Sphericity", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Surface per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Surface", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume/Mask Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Node", "left": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}, "op": "/", "right": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=1]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 1}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=2]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 2}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=3]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 3}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=4]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 4}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=5]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 5}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume mean component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 3}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter mean component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 3}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}]}, "name": "Chr1_ch1_G1_MSO_ME16_Triangle128_150_136_COS"}}} diff --git a/package/tests/test_data/old_saves/0.13.1/analysis/segmentation_pipeline_save.json b/package/tests/test_data/old_saves/0.13.1/analysis/segmentation_pipeline_save.json new file mode 100644 index 000000000..36e58a5e8 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.1/analysis/segmentation_pipeline_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {}} diff --git a/package/tests/test_data/old_saves/0.13.1/analysis/segmentation_profiles_save.json b/package/tests/test_data/old_saves/0.13.1/analysis/segmentation_profiles_save.json new file mode 100644 index 000000000..22476a045 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.1/analysis/segmentation_profiles_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"aaa": {"__SegmentationProfile__": true, "name": "aaa", "algorithm": "Border Rim", "values": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}}, "test1": {"__SegmentationProfile__": true, "name": "test1", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Iso Data", "values": {"masked": true, "bins": 256}}, "minimum_size": 500, "side_connection": false}}, "thr_10000": {"__SegmentationProfile__": true, "name": "thr_10000", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 10000}}, "minimum_size": 500, "side_connection": false}}, "test": {"__SegmentationProfile__": true, "name": "test", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Manual", "values": {"threshold": 15500}}, "minimum_size": 500, "side_connection": false}}}} diff --git a/package/tests/test_data/old_saves/0.13.1/analysis/segmentation_settings.json b/package/tests/test_data/old_saves/0.13.1/analysis/segmentation_settings.json new file mode 100644 index 000000000..2a5bea418 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.1/analysis/segmentation_settings.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"units_value": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "algorithm_widget_state": {"Lower threshold": {"channel": 2, "noise_filtering": {"None": {}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"Manual": {"threshold": 730.0}, "Huang": {"masked": true, "bins": 128}, "Triangle": {"masked": true, "bins": 256}, "Shanbhag": {"masked": true, "bins": 256}, "Li": {"masked": true, "bins": 256}, "Yen": {"masked": true, "bins": 256}, "Kittler Illingworth": {"masked": true, "bins": 256}, "Iso Data": {"masked": true, "bins": 256}, "Otsu": {"masked": true, "bins": 128}, "Moments": {"masked": true, "bins": 256}, "Maximum Entropy": {"masked": true, "bins": 256}, "Renyi Entropy": {"masked": true, "bins": 256}, "Intermodes": {"masked": true, "bins": 256}}, "minimum_size": 100, "side_connection": false}, "Upper threshold": {"channel": 0, "noise_filtering": {"None": {}}, "threshold": {"Manual": {"threshold": 8000.0}}, "minimum_size": 8000, "side_connection": false}, "Range threshold": {"channel": 0, "noise_filtering": {"None": {}}, "lower_threshold": 10000, "upper_threshold": 16000, "minimum_size": 8000, "side_connection": false}, "Lower threshold flow": {"channel": 1, "noise_filtering": {"None": {}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1}}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 15000}, "Otsu": {"masked": true, "bins": 128}, "Li": {"masked": true, "bins": 256}, "Renyi Entropy": {"masked": true, "bins": 256}, "Shanbhag": {"masked": true, "bins": 256}, "Triangle": {"masked": true, "bins": 256}, "Huang": {"masked": true, "bins": 128}, "Yen": {"masked": true, "bins": 256}, "Iso Data": {"masked": true, "bins": 256}, "Kittler Illingworth": {"masked": true, "bins": 256}, "Moments": {"masked": true, "bins": 256}}, "base_threshold": {"Manual": {"threshold": 11000}, "Otsu": {"masked": true, "bins": 128}, "Li": {"masked": true, "bins": 256}, "Renyi Entropy": {"masked": true, "bins": 256}, "Shanbhag": {"masked": true, "bins": 256}}}}, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false}, "MultiScale Opening": {"step_limits": 100, "reflective": false}, "Euclidean": {}, "Path": {}}, "minimum_size": 80, "side_connection": false}, "Upper threshold flow": {"channel": 0, "noise_filtering": {"None": {}}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000}}, "base_threshold": {"Manual": {"threshold": 8000}}}}, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false}, "MultiScale Opening": {"step_limits": 100, "reflective": false}}, "minimum_size": 8000, "side_connection": false}, "Multiple Otsu": {"channel": 0, "noise_filtering": {"None": {}}, "components": 4, "valley": true, "hist_num": 128}, "Border Rim": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}, "Split Mask o Part": {"num_of_parts": 5, "equal_volume": false}, "Split Mask on Part": {"num_of_parts": 2, "equal_volume": false}, "Lower threshold with watershed": {"channel": 1, "noise_filtering": {"None": {}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1}}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 17000.0}}, "base_threshold": {"Manual": {"threshold": 11000.0}}}}, "sprawl_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false}, "Euclidean": {}}, "minimum_size": 80, "side_connection": false}, "Upper threshold with watershed": {"channel": 0, "noise_filtering": {"None": {}}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000.0}}, "base_threshold": {"Manual": {"threshold": 8000.0}}}}, "sprawl_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false}}, "minimum_size": 8000, "side_connection": false}, "Mask Distance Splitting": {"num_of_parts": 2, "equal_volume": false}, "Segment Neutrofile": {"dna_marker": 1, "threshold": {"Manual": {"threshold": 8000}}, "dead_dna_marker": 0, "dead_threshold": {"Manual": {"threshold": 8000}}, "minimum_size": 20, "net_size": 500, "separate_nets": true}, "sm-fish segmentation": {"channel_nuc": 0, "noise_filtering_nucleus": {"None": {}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "nucleus_threshold": {"Manual": {"threshold": 370.0}}, "minimum_nucleus_size": 500, "channel_molecule": 1, "background_estimate": {"None": {}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 5.0}}, "molecule_threshold": {"Manual": {"threshold": 4.0}}, "minimum_molecule_size": 15, "foreground_estimate": {"None": {}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2.5}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1}}}}, "algorithms": {"Lower threshold": {"channel": 2, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 730.0}}, "minimum_size": 100, "side_connection": false}, "Upper threshold": {}, "Range threshold": {"channel": 0, "noise_filtering": {"name": "None", "values": {}}, "lower_threshold": 10000, "upper_threshold": 16000, "minimum_size": 8000, "side_connection": false}, "Lower threshold flow": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 15000}}, "base_threshold": {"name": "Manual", "values": {"threshold": 11000}}}}, "sprawl_type": {"name": "Euclidean", "values": {}}, "minimum_size": 80, "side_connection": false}, "Upper threshold flow": {}, "Multiple Otsu": {"channel": 0, "noise_filtering": {"name": "None", "values": {}}, "components": 4, "valley": true, "hist_num": 128}, "Border Rim": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}, "Split Mask o Part": {"num_of_parts": 5, "equal_volume": false}, "Split Mask on Part": {"num_of_parts": 2, "equal_volume": false}, "Lower threshold with watershed": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 17000}}, "base_threshold": {"name": "Manual", "values": {"threshold": 11000}}}}, "sprawl_type": {"name": "Euclidean", "values": {}}, "minimum_size": 80, "side_connection": false}, "Upper threshold with watershed": {}, "Mask Distance Splitting": {}, "Segment Neutrofile": {}, "sm-fish segmentation": {"channel_nuc": 0, "noise_filtering_nucleus": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "nucleus_threshold": {"name": "Manual", "values": {"threshold": 370.0}}, "minimum_nucleus_size": 500, "channel_molecule": 1, "background_estimate": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 5.0}}, "foreground_estimate": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2.5}}, "molecule_threshold": {"name": "Manual", "values": {"threshold": 4.0}}, "minimum_molecule_size": 15}}, "current_algorithm": "sm-fish segmentation", "multiple_files": false, "io": {"history": ["/home/czaki/Projekty/partseg/test_data/stack1_components", "/home/czaki/Projekty/partseg/test_data", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui/dane partseg", "/home/czaki/Pobrane/tmp/test", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui"], "open_directory": "/home/czaki/Projekty/PartSeg/test_data/stack1_components", "save_directory": "/home/czaki/Pobrane/tmp/test", "export_directory": "/home/czaki", "batch_plan_directory": "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui/dane partseg", "multiple_open_directory": "/home/czaki/Dokumenty/smFish/segmentation/20.07.02 smFISH Col-0/5_007/components", "open_filter": "Image with mask (*.tif *.tiff *.lsm)", "multiple_open_filter": "Image with mask (*.tif *.tiff *.lsm)", "save_filter": "Segmentation (*.tiff *.tif)", "load_image_directory": "/home/czaki", "batch_directory": "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui/dane partseg", "files_open_history": [[["/home/czaki/Obrazy/2019_04_24--ecoli_neu_tiff_converted/Ecoli neu_120 min 2 1E-1N 001.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components/test_component2.tif", "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components/test_component2_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components/test_component2.csv"], "Points (*.csv)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component8.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component8_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component7.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component7_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components/Cy5_component2.tif", "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components/Cy5_component2_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components/Cy5_component2.csv"], "Points (*.csv)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components/test_component2.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components/Cy5_component2.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"]], "dir_location_history": ["/home/czaki/Obrazy/2019_04_24--ecoli_neu_tiff_converted", "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components", "/home/czaki/Projekty/PartSeg/test_data/stack1_components", "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components2", "/home/czaki/Obrazy", "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points", "/", "/home/czaki/Dokumenty/smFish/segmentation/20.07.02 smFISH Col-0/5_007/components", "/home/czaki/Projekty/PartSeg/test_data", "/home/czaki/Projekty/partseg/test_data/stack1_components"], "open_file": "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4.tif"}, "mask_manager": {"mask_property": {"__Serializable__": true, "__subtype__": "PartSegCore.mask_create.MaskProperty", "dilate": {"__RadiusType__": true, "value": 0}, "dilate_radius": 0, "fill_holes": {"__RadiusType__": true, "value": 1}, "max_holes_size": -1, "save_components": false, "clip_to_mask": false, "reversed_mask": false}}, "save_parameters": {"Project (*.tgz *.tbz2 *.gz *.bz2)": {}, "Segmentation (*.npy)": {}, "Segmentation (*.tiff *.tif)": {}}, "multiple_files_widget": false}} diff --git a/package/tests/test_data/old_saves/0.13.1/analysis/statistic_profiles_save.json b/package/tests/test_data/old_saves/0.13.1/analysis/statistic_profiles_save.json new file mode 100644 index 000000000..891c36979 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.1/analysis/statistic_profiles_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"test": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "problematic set": {"__MeasurementProfile__": true, "name": "problematic set", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Pixel Brightness Sum", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Pixel brightness sum", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}}} diff --git a/package/tests/test_data/old_saves/0.13.1/analysis/view_settings.json b/package/tests/test_data/old_saves/0.13.1/analysis/view_settings.json new file mode 100644 index 000000000..8ae295a45 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.1/analysis/view_settings.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"custom_colormap": {"custom_rzy4sE18lw": {"__Colormap__": true, "name": "custom_rzy4sE18lw", "colors": [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0], [1.0, 1.0, 0.0, 1.0], [0.6666666865348816, 0.3333333432674408, 0.0, 1.0]], "interpolation": "linear", "controls": [0.0, 0.3333333333333333, 0.6666666666666666, 1.0]}, "custom_ZM0naoF24j": {"__Colormap__": true, "name": "custom_ZM0naoF24j", "colors": [[0.0, 0.0, 0.0, 1.0], [0.3333333432674408, 1.0, 0.0, 1.0], [1.0, 1.0, 0.0, 1.0], [1.0, 0.3333333432674408, 0.0, 1.0], [1.0, 0.3333333432674408, 1.0, 1.0]], "interpolation": "linear", "controls": [0.0, 0.25, 0.5, 0.75, 1.0]}, "custom_b0u0qiWLPr": {"__Colormap__": true, "name": "custom_b0u0qiWLPr", "colors": [[0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 0.0, 1.0], [1.0, 0.6666666865348816, 0.0, 1.0], [1.0, 0.3333333432674408, 1.0, 1.0], [0.3333333432674408, 0.0, 1.0, 1.0]], "interpolation": "linear", "controls": [0.0, 0.25, 0.5, 0.75, 1.0]}}, "custom_label_colors": {"custom_C6IN7g50aN": [[255, 255, 0], [0, 170, 0]], "custom_eRJelZcZRS": [[170, 85, 255], [170, 85, 0], [255, 255, 0]], "custom_NjzKiUpV0l": [[170, 170, 0], [0, 85, 0], [255, 255, 0], [255, 0, 0], [85, 255, 0], [0, 85, 255]]}, "raw_image": {"image_state": {"opacity": 1.0, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.napari_image_view.LabelEnum", "value": 1}, "only_border": 2, "border_thick": 1}, "range_0": [0, 40000], "use_gauss_0": false, "gauss_radius_0": 1.0, "lock_0": 0, "channels_count": 4, "cmap0": "red", "cmap1": "green", "lock_1": 0, "use_gauss_1": false, "cmap2": "blue", "lock_2": 0, "use_gauss_2": false, "cmap3": "magenta", "lock_3": false, "use_gauss_3": false, "gauss_radius_1": 1, "gauss_radius_2": 1, "gauss_radius_3": 1, "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 1}, "filter_radius_0": 1.0, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 1}, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_1": 1.0, "filter_radius_2": 1.0, "filter_radius_3": 1.0, "range_1": [0, 40000], "range_3": [0, 65000], "gamma_value_0": 1.0, "gamma_value_1": 1.0, "gamma_value_2": 1, "gamma_value_3": 1, "range_2": [0, 65000], "cmap4": "inferno", "lock_4": false, "use_filter_4": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_4": 1, "filter_radius_4": 1}, "mask_presentation": ["white", 1], "result_image": {"image_state": {"opacity": 0.9, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.napari_image_view.LabelEnum", "value": 1}, "only_border": 2, "border_thick": 1}, "range_0": [0, 6000], "use_gauss_0": false, "gauss_radius_0": 1, "lock_0": 0, "channels_count": 4, "cmap0": "magma", "cmap1": "red", "lock_1": 0, "use_gauss_1": false, "cmap2": "magma", "lock_2": 2, "use_gauss_2": false, "cmap3": "custom_rzy4sE18lw", "lock_3": 0, "use_gauss_3": false, "gauss_radius_1": 1, "gauss_radius_2": 1, "gauss_radius_3": 1, "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_0": 5.0, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 1}, "filter_radius_1": 1.0, "filter_radius_2": 1.4, "filter_radius_3": 1.0, "range_1": [10000, 65000], "range_3": [7900, 39000], "range_2": [0, 20000], "gamma_value_0": 1.0, "gamma_value_1": 1.0, "gamma_value_2": 1, "gamma_value_3": 1.0, "cmap4": "inferno", "lock_4": false, "use_filter_4": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_4": 1, "filter_radius_4": 1}, "custom_colors": [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [0.0392156862745098, 0.1568627450980392, 0.047058823529411764, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [0.39215686274509803, 0.01568627450980392, 0.8627450980392157, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], "colormaps": ["red", "green", "blue", "magenta", "inferno", "magma", "plasma", "viridis", "custom_b0u0qiWLPr", "custom_ZM0naoF24j", "custom_rzy4sE18lw"], "hide_left_panel": 0, "batch_window_geometry": "01d9d0cb00030000000012820000015800001755000003d8000012820000017d00001755000003d800000002000000000780000012820000017d00001755000003d8", "main_window_geometry": "01d9d0cb0003000000000aac0000003e0000115e0000046800000aac000000630000115e0000046800000000000000000a0000000aac000000630000115e00000468", "advanced_window_geometry": "01d9d0cb0003000000000aa5000001e500000f320000051a00000aa50000020a00000f320000051a00000000000000000a0000000aa50000020a00000f320000051a", "mask_presentation_color": [255, 255, 0], "mask_presentation_opacity": 0.8, "labels_used": "default", "theme": "light", "mask_presentation _opacity": 1}} diff --git a/package/tests/test_data/old_saves/0.13.1/mask/segmentation_settings.json b/package/tests/test_data/old_saves/0.13.1/mask/segmentation_settings.json new file mode 100644 index 000000000..5cf654176 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.1/mask/segmentation_settings.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"algorithm_widget_state": {"Threshold": {"channel": 0, "noise_filtering": {"None": {}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"Manual": {"threshold": 8000.0}, "Otsu": {"masked": true, "bins": 128}, "Li": {"masked": true, "bins": 256}, "Renyi Entropy": {"masked": true, "bins": 256}, "Shanbhag": {"masked": true, "bins": 256}, "Triangle": {"masked": true, "bins": 256}}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {}, "Opening": {"smooth_border_radius": 2}, "Vote": {"neighbourhood_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.watershed.NeighType", "value": 18}, "support_level": 1}}, "side_connection": false, "minimum_size": 500, "use_convex": true}, "Threshold Flow": {"channel": 0, "noise_filtering": {"None": {}}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000.0}}, "base_threshold": {"Manual": {"threshold": 8000.0}}}}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {}}, "side_connection": false, "minimum_size": 8000, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false}, "MultiScale Opening": {"step_limits": 100, "reflective": false}}, "use_convex": false}, "Only Threshold": {"channel": 0, "noise_filtering": {"None": {}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": 5000}, "Auto Threshold": {"channel": 0, "noise_filtering": {"None": {}}, "threshold": {"Manual": {"threshold": 8000.0}}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {}}, "side_connection": false, "minimum_size": 8000, "suggested_size": 200000, "use_convex": false}, "Cellpose cyto": {"cell_channel": 0, "nucleus_channel": 0, "diameter": 30}}, "algorithms": {"Threshold": {"channel": 0, "threshold": {"name": "Manual", "values": {"threshold": 8000}}, "minimum_size": 500, "close_holes": true, "smooth_border": {"name": "None", "values": {}}, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "close_holes_size": 200, "side_connection": false, "use_convex": true}, "Threshold Flow": {"channel": 0, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 8000}}, "base_threshold": {"name": "Manual", "values": {"threshold": 8000}}}}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "None", "values": {}}, "side_connection": false, "minimum_size": 8000, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 100, "reflective": false}}, "use_convex": false}, "Only Threshold": {"channel": 0, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": 5000}, "Auto Threshold": {"channel": 0, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Manual", "values": {"threshold": 8000}}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "None", "values": {}}, "side_connection": false, "minimum_size": 8000, "suggested_size": 200000, "use_convex": false}, "Cellpose cyto": {}}, "current_algorithm": "Threshold", "multiple_files_widget": false, "units_value": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "io": {"load_image_directory": "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points", "load_data_filter": "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)", "history": ["/home/czaki/Dropbox/segmentation/stage_9_003", "/home/czaki/Projekty", "/home/czaki/Dropbox/segmentation/stage_9_002", "/home/czaki/Dropbox/stage_9_002", "/home/czaki/Dokumenty/smFish/first_data", "/home/czaki/Projekty/partseg/test_data", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui/dane partseg", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui", "/home/czaki/Pobrane/tmp", "/home/czaki/Obrazy"], "save_batch": "/home/czaki", "save_components_directory": "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points", "save_segmentation_directory": "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui", "open_segmentation_directory": "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points", "batch_directory": "/home/czaki", "multiple_open_directory": "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui", "multiple_open_filter": "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif)", "files_open_history": [[["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/test.obsep"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Projekty/PartSeg/test_data/obsep/test.obsep"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/dots2.csv"], "Points (*.csv)"], [["/home/czaki/Dokumenty/smFish/stage_7_004_with_points/dots.csv/data.csv"], "Points (*.csv)"], [["/home/czaki/Dokumenty/smFish/stage_7_004_with_points/dots.csv/channnel 1.tif"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Dokumenty/smFish/stage_7_004_with_points/test.obsep"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/dots1.csv"], "Points (*.csv)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/Cy5.TIF"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/Cy5_deconv.tif"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/PartSeg/test_data/test_nucleus.tif"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif)"]], "load_image_file": "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/test.obsep", "dir_location_history": ["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points", "/home/czaki/Projekty/PartSeg/test_data/obsep", "/home/czaki/Dokumenty/smFish/stage_7_004_with_points/dots.csv", "/home/czaki/Dokumenty/smFish/stage_7_004_with_points", "/home/czaki/Projekty/PartSeg/test_data", "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/components", "/home/czaki/Dokumenty/smFish/segmentation/20.07.02 smFISH Col-0/5_007", "/home/czaki/Dropbox/segmentation/stage_9_003", "/home/czaki/Dokumenty/smFish/segmentation/20.07.02 smFISH Col-0/5_009", "/home/czaki/Dokumenty/smFish/smFISH data 2/stage_4_006"]}, "simple_measurements": {"units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 1}}, "mask_manager": {"mask_property": {"__Serializable__": true, "__subtype__": "PartSegCore.mask_create.MaskProperty", "dilate": {"__RadiusType__": true, "value": 0}, "dilate_radius": 0, "fill_holes": {"__RadiusType__": true, "value": 0}, "max_holes_size": 0, "save_components": false, "clip_to_mask": false, "reversed_mask": false}}}} diff --git a/package/tests/test_data/old_saves/0.13.1/mask/view_settings.json b/package/tests/test_data/old_saves/0.13.1/mask/view_settings.json new file mode 100644 index 000000000..8b5881afc --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.1/mask/view_settings.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"custom_colormap": {"custom_7h4Y7cyokY": {"__Colormap__": true, "name": "custom_7h4Y7cyokY", "colors": [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0], [0.0, 1.0, 1.0, 1.0], [1.0, 1.0, 0.0, 1.0], [1.0, 0.0, 0.0, 1.0]], "interpolation": "linear", "controls": [0.0, 0.25, 0.5, 0.75, 1.0]}, "custom_VBodz4weU1": {"__Colormap__": true, "name": "custom_VBodz4weU1", "colors": [[0.0, 0.0, 0.0, 1.0], [1.0, 0.3333333432674408, 0.0, 1.0], [1.0, 1.0, 0.0, 1.0], [0.0, 0.3333333432674408, 1.0, 1.0], [0.3333333432674408, 1.0, 0.0, 1.0]], "interpolation": "linear", "controls": [0.0, 0.25, 0.5, 0.75, 1.0]}}, "custom_label_colors": {"custom_te9A1IUwAK": [[255, 255, 0], [0, 85, 255]], "custom_Yt60W8B0MX": [[255, 255, 0], [0, 170, 255], [255, 0, 127], [170, 255, 0]]}, "channelcontrol": {"image_state": {"opacity": 0.9999999999999999, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.napari_image_view.LabelEnum", "value": 1}, "only_border": 2, "border_thick": 3}, "range_0": [0, 3000], "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_0": 6.1, "lock_0": 0, "channels_count": 3, "cmap0": "red", "cmap1": "green", "lock_1": 0, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "cmap2": "blue", "lock_2": 2, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "cmap3": "magenta", "lock_3": 0, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_1": 1.0, "filter_radius_2": 1.0, "filter_radius_3": 1.0, "cmap4": "Grayscale", "lock_4": false, "use_filter_4": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_4": 1, "range_1": [0, 42000], "gamma_value_0": 1.0, "gamma_value_1": 1.0, "gamma_value_2": 1, "gamma_value_3": 1.0, "range_2": [0, 10000], "range_3": [0, 65000], "gamma_value_4": 1}, "mask_presentation_color": [255, 255, 255], "mask_presentation_opacity": 1, "colormaps": ["red", "green", "blue", "magenta", "inferno", "magma", "plasma", "viridis", "custom_VBodz4weU1"], "labels_used": "default", "main_window_geometry": "01d9d0cb000300000000094f0000009a00000feb0000050f0000094f000000bf00000feb0000050f00000000000000000a000000094f000000bf00000feb0000050f", "custom_colors": [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [0.0392156862745098, 0.1568627450980392, 0.047058823529411764, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [0.39215686274509803, 0.01568627450980392, 0.8627450980392157, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], "advanced_window_geometry": "01d9d0cb00030000000008690000014d00000d53000003d6000008690000017200000d53000003d600000000000000000a00000008690000017200000d53000003d6", "theme": "light", "simple_measurement_window_geometry": "01d9d0cb00030000000011c8000000ca0000156d000002ef000011c8000000ef0000156d000002ef00000002000000000780000011c8000000ef0000156d000002ef"}} diff --git a/package/tests/test_data/old_saves/0.13.14/analysis/batch_plans_save.json b/package/tests/test_data/old_saves/0.13.14/analysis/batch_plans_save.json new file mode 100644 index 000000000..fd9e010c8 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.14/analysis/batch_plans_save.json @@ -0,0 +1 @@ +{"default": {"test": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "aaa", "algorithm": "Border Rim", "values": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}, {"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.Save", "suffix": "_proj", "directory": "", "algorithm": "Project (*.tgz *.tbz2 *.gz *.bz2)", "short_name": "project", "values": {}}, "children": []}]}]}, "name": "test"}, "test2": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "test1", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Iso Data", "values": {"masked": true, "bins": 256}}, "minimum_size": 500, "side_connection": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}, {"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.Save", "suffix": "_proj2", "directory": "", "algorithm": "Project (*.tgz *.tbz2 *.gz *.bz2)", "short_name": "project", "values": {}}, "children": []}]}]}, "name": "test2"}, "test3": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "thr_10000", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 10000}}, "minimum_size": 500, "side_connection": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}, "name": "test3"}, "Chr1_ch1_G1_MSO_ME16_Triangle128_150_136_COS": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MaskSuffix", "name": "", "suffix": "_mask"}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "Chr1_Ch1_G1_MSO_ME16_Triangle128_150_136_COS", "algorithm": "Lower threshold with watershed", "values": {"channel": 0, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Maximum Entropy", "values": {"masked": true, "bins": 16}}, "base_threshold": {"name": "Triangle", "values": {"masked": true, "bins": 128}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 150, "reflective": true}}, "minimum_size": 136, "side_connection": true}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 1}, "measurement_profile": {"__MeasurementProfile__": true, "name": "Chr1_2020", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Components number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Pixel brightness sum per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Pixel brightness sum", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Mean pixel brightness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Mean pixel brightness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Maximum pixel brightness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Maximum pixel brightness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Minimum pixel brightness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Minimum pixel brightness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI First principal axis length per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "First principal axis length", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Second principal axis length per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Second principal axis length", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Third principal axis length per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Third principal axis length", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Compactness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Compactness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Sphericity per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Sphericity", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Surface per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Surface", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume/Mask Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Node", "left": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}, "op": "/", "right": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=1]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 1}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=2]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 2}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=3]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 3}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=4]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 4}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=5]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 5}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume mean component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 3}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter mean component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 3}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}]}, "name": "Chr1_ch1_G1_MSO_ME16_Triangle128_150_136_COS"}, "test_batch": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "base_seg", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 15000.0}}, "base_threshold": {"name": "Manual", "values": {"threshold": 11000.0}}}}, "sprawl_type": {"name": "Euclidean", "values": {}}, "minimum_size": 80, "side_connection": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test_batch", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Component Bounding Box per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Component Bounding Box", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}, "name": "test_batch"}, "Fos to Speckle ROI": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MaskSuffix", "name": "", "suffix": "_mask"}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "Speckle segmentation", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 3}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Li", "values": {"masked": true, "bins": 254}}, "base_threshold": {"name": "Otsu", "values": {"masked": true, "bins": 512}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 500, "reflective": false}}, "minimum_size": 15, "side_connection": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test Fos", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance per component [ROI extraction profile=Segmentation profile name: fos segmentation\nAlgorithm: Lower threshold\nchannel: 3\nnoise filtering: \n name: Median\n values: \n dimension type: Layer\n radius: 2\nthreshold: \n name: Yen\n values: \n masked: True\n bins: 512\nminimum size: 100\nside connection: True, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "fos segmentation", "algorithm": "Lower threshold", "values": {"channel": 2, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Yen", "values": {"masked": true, "bins": 512}}, "minimum_size": 100, "side_connection": true}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}]}, "name": "Fos to Speckle ROI"}, "gen_mask": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "generate_mask", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Manual", "values": {"threshold": 1.0}}, "minimum_size": 100, "side_connection": true}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MaskCreate", "name": "", "mask_property": {"__Serializable__": true, "__subtype__": "PartSegCore.mask_create.MaskProperty", "dilate": {"__RadiusType__": true, "value": 0}, "dilate_radius": 0, "fill_holes": {"__RadiusType__": true, "value": 0}, "max_holes_size": 0, "save_components": false, "clip_to_mask": false, "reversed_mask": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.Save", "suffix": "_mask", "directory": "", "algorithm": "Mask (*.tiff *.tif)", "short_name": "mask_tiff", "values": {}}, "children": []}]}]}]}, "name": "gen_mask"}, "test_aaa": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "aaa", "algorithm": "Border Rim", "values": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test_roi", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance[ROI extraction profile=Segmentation profile name: test2\nAlgorithm: Lower threshold\nchannel: 1\nnoise filtering: \n name: Median\n values: \n dimension type: Stack\n radius: 1\nthreshold: \n name: Manual\n values: \n threshold: 30000.0\nminimum size: 100\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "test2", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 30000.0}}, "minimum_size": 100, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance per component [ROI extraction profile=Segmentation profile name: test2\nAlgorithm: Lower threshold\nchannel: 1\nnoise filtering: \n name: Median\n values: \n dimension type: Stack\n radius: 1\nthreshold: \n name: Manual\n values: \n threshold: 30000.0\nminimum size: 100\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "test2", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 30000.0}}, "minimum_size": 100, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}, "name": "test_aaa"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.14/analysis/segmentation_pipeline_save.json b/package/tests/test_data/old_saves/0.13.14/analysis/segmentation_pipeline_save.json new file mode 100644 index 000000000..f66a313ff --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.14/analysis/segmentation_pipeline_save.json @@ -0,0 +1 @@ +{"default": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.14/analysis/segmentation_profiles_save.json b/package/tests/test_data/old_saves/0.13.14/analysis/segmentation_profiles_save.json new file mode 100644 index 000000000..be84be7fb --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.14/analysis/segmentation_profiles_save.json @@ -0,0 +1 @@ +{"default": {"aaa": {"__SegmentationProfile__": true, "name": "aaa", "algorithm": "Border Rim", "values": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}}, "test1": {"__SegmentationProfile__": true, "name": "test1", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Iso Data", "values": {"masked": true, "bins": 256}}, "minimum_size": 500, "side_connection": false}}, "thr_10000": {"__SegmentationProfile__": true, "name": "thr_10000", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 10000}}, "minimum_size": 500, "side_connection": false}}, "test": {"__SegmentationProfile__": true, "name": "test", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Manual", "values": {"threshold": 15000.0}}, "minimum_size": 100, "side_connection": true}}, "test2": {"__SegmentationProfile__": true, "name": "test2", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 30000.0}}, "minimum_size": 100, "side_connection": false}}, "base_seg": {"__SegmentationProfile__": true, "name": "base_seg", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 15000.0}}, "base_threshold": {"name": "Manual", "values": {"threshold": 11000.0}}}}, "sprawl_type": {"name": "Euclidean", "values": {}}, "minimum_size": 80, "side_connection": false}}, "fos segmentation": {"__SegmentationProfile__": true, "name": "fos segmentation", "algorithm": "Lower threshold", "values": {"channel": 2, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Yen", "values": {"masked": true, "bins": 512}}, "minimum_size": 100, "side_connection": true}}, "Speckle segmentation": {"__SegmentationProfile__": true, "name": "Speckle segmentation", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 3}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Li", "values": {"masked": true, "bins": 254}}, "base_threshold": {"name": "Otsu", "values": {"masked": true, "bins": 512}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 500, "reflective": false}}, "minimum_size": 15, "side_connection": false}}, "generate_mask": {"__SegmentationProfile__": true, "name": "generate_mask", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Manual", "values": {"threshold": 1.0}}, "minimum_size": 100, "side_connection": true}}, "test_profile": {"__SegmentationProfile__": true, "name": "test_profile", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Manual", "values": {"threshold": 7900.0}}, "minimum_size": 100, "side_connection": true}}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.14/analysis/segmentation_settings.json b/package/tests/test_data/old_saves/0.13.14/analysis/segmentation_settings.json new file mode 100644 index 000000000..b3d83c3c6 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.14/analysis/segmentation_settings.json @@ -0,0 +1 @@ +{"default": {"units_value": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "algorithm_widget_state": {"Lower threshold": {"channel": 1, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Manual": {"threshold": 70.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Huang": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Triangle": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Shanbhag": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Li": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Yen": {"masked": true, "bins": 512, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Kittler Illingworth": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Iso Data": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Otsu": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Moments": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Maximum Entropy": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Renyi Entropy": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Intermodes": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 99, "side_connection": true, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 8000, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Range threshold": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "lower_threshold": 10000, "upper_threshold": 16000, "minimum_size": 8000, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Lower threshold flow": {"channel": 1, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 15000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Otsu": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Li": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Renyi Entropy": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Shanbhag": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Triangle": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Huang": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Yen": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Iso Data": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Kittler Illingworth": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Moments": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"Manual": {"threshold": 11000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Otsu": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Li": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Renyi Entropy": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Shanbhag": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Euclidean": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Path": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 80, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold flow": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"Manual": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 8000, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Multiple Otsu": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "components": 4, "valley": true, "hist_num": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Border Rim": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Split Mask o Part": {"num_of_parts": 5, "equal_volume": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Split Mask on Part": {"num_of_parts": 2, "equal_volume": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Lower threshold with watershed": {"channel": 1, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 3, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 17000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Li": {"masked": true, "bins": 254, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"Manual": {"threshold": 9300.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Otsu": {"masked": true, "bins": 512, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Euclidean": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 80, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold with watershed": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 8000, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Mask Distance Splitting": {"num_of_parts": 2, "equal_volume": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Segment Neutrofile": {"dna_marker": 1, "threshold": {"Manual": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "dead_dna_marker": 0, "dead_threshold": {"Manual": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 20, "net_size": 500, "separate_nets": true, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish segmentation": {"channel_nuc": 0, "noise_filtering_nucleus": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 370.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_nucleus_size": 500, "channel_molecule": 1, "background_estimate": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 5.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "molecule_threshold": {"Manual": {"threshold": 4.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_molecule_size": 15, "foreground_estimate": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2.5, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "background_estimate_radius": 5.0, "foreground_estimate_radius": 2.5, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish laplacian segmentation": {"channel_nuc": 0, "noise_filtering_nucleus": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_nucleus_size": 500, "channel_molecule": 1, "laplacian_radius": 1.3, "molecule_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_molecule_size": 5, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish spot segmentation": {"channel_nuc": 0, "noise_filtering_nucleus": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_nucleus_size": 500, "spot_method": {"Gaussian spot estimate": {"background_estimate_radius": 5.0, "foreground_estimate_radius": 2.5, "estimate_mask": true, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "channel_molecule": 1, "molecule_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_molecule_size": 5, "leave_the_biggest": true, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Trapalyzer": {"inner_dna": 1, "inner_dna_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "inner_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "outer_dna": 1, "outer_dna_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "outer_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "net_size": {"lower_bound": 850.0, "upper_bound": 999999.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "net_ext_brightness": {"lower_bound": 21.0, "upper_bound": 100.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "net_ext_brightness_std": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "unknown_net": true, "pmn_neu_pixel count": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmn_neu_brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmn_neu_ext. brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmn_neu_brightness gradient": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "rnd_neu_pixel count": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "rnd_neu_brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "rnd_neu_ext. brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "rnd_neu_brightness gradient": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "ner_neu_pixel count": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "ner_neu_brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "ner_neu_ext. brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "ner_neu_brightness gradient": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmp_neu_pixel count": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmp_neu_brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmp_neu_ext. brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmp_neu_brightness gradient": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "bacteria_pixel count": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "bacteria_brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "bacteria_ext. brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "bacteria_brightness gradient": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_score": 0.8, "maximum_other": 0.4, "minimum_size": 40, "softness": 0.1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "algorithms": {"Lower threshold": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Manual", "values": {"threshold": 70.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 99, "side_connection": true, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Range threshold": {"channel": 0, "noise_filtering": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "lower_threshold": 10000, "upper_threshold": 16000, "minimum_size": 8000, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Lower threshold flow": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 15000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"name": "Manual", "values": {"threshold": 11000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"name": "Euclidean", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 80, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold flow": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Multiple Otsu": {"channel": 0, "noise_filtering": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "components": 4, "valley": true, "hist_num": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Border Rim": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Split Mask o Part": {"num_of_parts": 5, "equal_volume": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Split Mask on Part": {"num_of_parts": 2, "equal_volume": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Lower threshold with watershed": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 17000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"name": "Manual", "values": {"threshold": 9300.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"name": "Euclidean", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 80, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold with watershed": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Mask Distance Splitting": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Segment Neutrofile": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish segmentation": {"channel_nuc": 0, "noise_filtering_nucleus": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"name": "Manual", "values": {"threshold": 370.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_nucleus_size": 500, "channel_molecule": 1, "background_estimate": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 5.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "foreground_estimate": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2.5, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "molecule_threshold": {"name": "Manual", "values": {"threshold": 4.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_molecule_size": 15, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish laplacian segmentation": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish spot segmentation": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Trapalyzer": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "current_algorithm": "Lower threshold with watershed", "multiple_files": false, "io": {"history": ["/home/czaki/Projekty/partseg/test_data/stack1_components", "/home/czaki/Projekty/partseg/test_data", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui/dane partseg", "/home/czaki/Pobrane/tmp/test", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui"], "open_directory": "/home/czaki/Projekty/PartSeg/test_data/stack1_components", "save_directory": "/home/czaki/Pobrane/tmp", "export_directory": "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/measurements_profile_speckle segmentation.json", "batch_plan_directory": "/home/czaki/Pobrane/tmp", "multiple_open_directory": "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi", "open_filter": "Image with mask (*.tif *.tiff *.lsm)", "multiple_open_filter": "Image with mask (*.tif *.tiff *.lsm)", "save_filter": "Chimera CMAP (*.cmap)", "load_image_directory": "/home/czaki", "batch_directory": "/home/czaki/Projekty/PartSeg/test_data/stack1_components", "files_open_history": [[["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Obrazy/2019_04_24--ecoli_neu_tiff_converted/Ecoli neu_60min 1 4E-1N001.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Obrazy/2019_04_24--ecoli_neu_tiff_converted/Ecoli neu_60min 1 4E-1N001.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_3_Channel Alignment_component5.tif", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_3_Channel Alignment_component5_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/A/cut2/A_component2.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component1.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component1_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"]], "dir_location_history": ["/home/czaki/Projekty/PartSeg/test_data/stack1_components", "/home/czaki/Obrazy/2019_04_24--ecoli_neu_tiff_converted", "/home/czaki/Pobrane/tmp", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi", "/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_no_mask", "/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_mask", "/home/czaki/Pobrane/tmp/test_data", "/home/czaki/Projekty/PartSeg/test_data", "/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/A/cut2", "/home/czaki/Obrazy"], "open_file": "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif", "save_screenshot": "/home/czaki/Obrazy", "multiple_files_open_history": [[["/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_1_Channel Alignment_component6.tif", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_1_Channel Alignment_component6_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_3_Channel Alignment_component5.tif", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_3_Channel Alignment_component5_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_1_Channel Alignment_component11.tif", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_1_Channel Alignment_component11_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_2_Channel Alignment_component5.tif", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_2_Channel Alignment_component5_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_no_mask/test_component2.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_mask/test_component2.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_no_mask/test_component2.tif", "/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_no_mask/test_component2_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component1.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Pobrane/tmp/test_data/20210930-YoYo1-Hoechst-120min_DMSO-11_Channel Alignment_component7.tif", "/home/czaki/Pobrane/tmp/test_data/20210930-YoYo1-Hoechst-120min_DMSO-11_Channel Alignment_component7_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_component1.tgz"], "Project (*.tgz *.tbz2 *.gz *.bz2)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component1.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component1_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/A/cut2/A_component2.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"]], "__class__": "PartSegCore.json_hooks.EventedDict"}, "mask_manager": {"mask_property": {"__Serializable__": true, "__subtype__": "PartSegCore.mask_create.MaskProperty", "dilate": {"__RadiusType__": true, "value": 1}, "dilate_radius": 5, "fill_holes": {"__RadiusType__": true, "value": 1}, "max_holes_size": -1, "save_components": false, "clip_to_mask": false, "reversed_mask": false}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "save_parameters": {"Project (*.tgz *.tbz2 *.gz *.bz2)": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Segmentation (*.npy)": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Segmentation (*.tiff *.tif)": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Image (*.tiff *.tif)": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Chimera CMAP (*.cmap)": {"channel": 0, "separated_objects": false, "clip": false, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "reverse": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "multiple_files_widget": 0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.14/analysis/statistic_profiles_save.json b/package/tests/test_data/old_saves/0.13.14/analysis/statistic_profiles_save.json new file mode 100644 index 000000000..7b5984924 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.14/analysis/statistic_profiles_save.json @@ -0,0 +1 @@ +{"default": {"test": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "problematic set": {"__MeasurementProfile__": true, "name": "problematic set", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Pixel Brightness Sum", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Pixel brightness sum", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "test_load": {"__MeasurementProfile__": true, "name": "test_load", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask segmentation distance[Distance from mask=Border, Distance to segmentation=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "ROI distance", "dict": {"distance_from_mask": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_segmentation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "test2": {"__MeasurementProfile__": true, "name": "test2", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance[ROI extraction profile=Segmentation profile name: test1\nAlgorithm: Lower threshold\nchannel: 2\nnoise filtering: \n name: None\n values: \n\nthreshold: \n name: Iso Data\n values: \n masked: True\n bins: 256\nminimum size: 500\nside connection: False, Distance new ROI=Mass center, Distance to ROI=Geometrical center]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "test1", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Iso Data", "values": {"masked": true, "bins": 256}}, "minimum_size": 500, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 2}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 3}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "test_roi": {"__MeasurementProfile__": true, "name": "test_roi", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance[ROI extraction profile=Segmentation profile name: test2\nAlgorithm: Lower threshold\nchannel: 1\nnoise filtering: \n name: Median\n values: \n dimension type: Stack\n radius: 1\nthreshold: \n name: Manual\n values: \n threshold: 30000.0\nminimum size: 100\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "test2", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 30000.0}}, "minimum_size": 100, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance per component [ROI extraction profile=Segmentation profile name: test2\nAlgorithm: Lower threshold\nchannel: 1\nnoise filtering: \n name: Median\n values: \n dimension type: Stack\n radius: 1\nthreshold: \n name: Manual\n values: \n threshold: 30000.0\nminimum size: 100\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "test2", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 30000.0}}, "minimum_size": 100, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "test_batch": {"__MeasurementProfile__": true, "name": "test_batch", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Component Bounding Box per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Component Bounding Box", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "hanna_test": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance per component [ROI extraction profile=Segmentation profile name: Speckle segmentation\nAlgorithm: Lower threshold with watershed\nchannel: 2\nnoise filtering: \n name: Median\n values: \n dimension type: Layer\n radius: 3\nthreshold: \n name: Base/Core\n values: \n core threshold: \n name: Li\n values: \n masked: True\n bins: 254\n base threshold: \n name: Otsu\n values: \n masked: True\n bins: 512\nsprawl type: \n name: MultiScale Opening\n values: \n step limits: 500\n reflective: False\nminimum size: 15\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "Speckle segmentation", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 3}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Li", "values": {"masked": true, "bins": 254}}, "base_threshold": {"name": "Otsu", "values": {"masked": true, "bins": 512}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 500, "reflective": false}}, "minimum_size": 15, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "Speckle segmentation": {"__MeasurementProfile__": true, "name": "Speckle segmentation", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance per component [ROI extraction profile=Segmentation profile name: Speckle segmentation\nAlgorithm: Lower threshold with watershed\nchannel: 2\nnoise filtering: \n name: Median\n values: \n dimension type: Layer\n radius: 3\nthreshold: \n name: Base/Core\n values: \n core threshold: \n name: Li\n values: \n masked: True\n bins: 254\n base threshold: \n name: Otsu\n values: \n masked: True\n bins: 512\nsprawl type: \n name: MultiScale Opening\n values: \n step limits: 500\n reflective: False\nminimum size: 15\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "Speckle segmentation", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 3}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Li", "values": {"masked": true, "bins": 254}}, "base_threshold": {"name": "Otsu", "values": {"masked": true, "bins": 512}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 500, "reflective": false}}, "minimum_size": 15, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "test_coloc": {"__MeasurementProfile__": true, "name": "test_coloc", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Pearson correlation coefficient, Randomize channel=False, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Pearson correlation coefficient", "randomize": false, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Mander's overlap coefficient, Randomize channel=False, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Mander's overlap coefficient", "randomize": false, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Intensity correlation quotient, Randomize channel=False, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Intensity correlation quotient", "randomize": false, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Spearman rank correlation, Randomize channel=False, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Spearman rank correlation", "randomize": false, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Pearson correlation coefficient, Randomize channel=True, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Pearson correlation coefficient", "randomize": true, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Mander's overlap coefficient, Randomize channel=True, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Mander's overlap coefficient", "randomize": true, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Intensity correlation quotient, Randomize channel=True, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Intensity correlation quotient", "randomize": true, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Spearman rank correlation, Randomize channel=True, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Spearman rank correlation", "randomize": true, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.14/analysis/view_settings.json b/package/tests/test_data/old_saves/0.13.14/analysis/view_settings.json new file mode 100644 index 000000000..3cb496cba --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.14/analysis/view_settings.json @@ -0,0 +1 @@ +{"default": {"custom_colormap": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "custom_label_colors": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "first_start": false, "raw_image": {"range_0": [0, 65000], "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_0": 1, "lock_0": false, "gamma_value_0": 1, "image_state": {"only_border": false, "opacity": 1, "border_thick": 1, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.napari_image_view.LabelEnum", "value": 1}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "channels_count": 4, "cmap": {"num0": "red", "num1": "green", "num2": "blue", "num3": "magenta", "__class__": "PartSegCore.json_hooks.EventedDict"}, "lock_1": false, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_1": 1, "lock_2": false, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_2": 1, "lock_3": false, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_3": 1, "filter_radius_1": 1, "filter_radius_2": 1, "filter_radius_3": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "result_image": {"range_0": [0, 65000], "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_0": 1, "lock_0": false, "gamma_value_0": 1, "image_state": {"only_border": false, "opacity": 1, "border_thick": 1, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.napari_image_view.LabelEnum", "value": 1}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "channels_count": 4, "cmap": {"num0": "red", "num1": "green", "num2": "blue", "num3": "magenta", "__class__": "PartSegCore.json_hooks.EventedDict"}, "lock_1": false, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_1": 1, "lock_2": false, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_2": 1, "lock_3": false, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_3": 1, "filter_radius_1": 1, "filter_radius_2": 1, "filter_radius_3": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "rendering_mode": "iso_categorical", "custom_colors": [], "mask_presentation_color": [255, 255, 255], "mask_presentation_opacity": 1, "colormaps": ["red", "green", "blue", "magenta", "inferno", "magma"], "hide_left_panel": false, "labels_used": "default", "main_window_geometry": "01d9d0cb00030000000008d80000012900000f81000004c3000008d80000014e00000f81000004c300000000000000000a00000008d80000014e00000f81000004c3", "advanced_window_geometry": "01d9d0cb0003000000000000000000860000027f0000026500000000000000860000027f000002650000000100000000078000000000000000860000027f00000265", "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.14/mask/segmentation_settings.json b/package/tests/test_data/old_saves/0.13.14/mask/segmentation_settings.json new file mode 100644 index 000000000..2be449a61 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.14/mask/segmentation_settings.json @@ -0,0 +1 @@ +{"default": {"algorithm_widget_state": {"Threshold": {"channel": 3, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.3, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Manual": {"threshold": 11000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Otsu": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Li": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Renyi Entropy": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Shanbhag": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Triangle": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Opening": {"smooth_border_radius": 2, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Vote": {"neighbourhood_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.watershed.NeighType", "value": 18}, "support_level": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 500, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Threshold Flow": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Only Threshold": {"channel": 3, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": 11000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Auto Threshold": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "suggested_size": 200000, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Cellpose cyto": {"cell_channel": 0, "nucleus_channel": 0, "diameter": 30, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Cell from nucleus": {"nucleus_channel": 0, "nucleus_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_channel": 0, "cell_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "flow_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Cell from nucleus flow": {"nucleus_channel": 0, "nucleus_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_channel": 0, "cell_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "flow_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Maximum projection Threshold Flow": {"lower_layer": 0, "upper_layer": -1, "nucleus_channel": 0, "nucleus_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_channel": 0, "cell_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "flow_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Morphological Watersheed": {"channel": 3, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 20, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "algorithms": {"Threshold": {"channel": 3, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.3, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Manual", "values": {"threshold": 11000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "Opening", "values": {"smooth_border_radius": 2, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 500, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Threshold Flow": {"channel": 0, "noise_filtering": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"name": "Manual", "values": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Only Threshold": {"channel": 3, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": 11000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Auto Threshold": {"channel": 0, "noise_filtering": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Manual", "values": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "suggested_size": 200000, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Cellpose cyto": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Cell from nucleus": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Cell from nucleus flow": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Maximum projection Threshold Flow": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Morphological Watersheed": {"channel": 3, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Manual", "values": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 20, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "current_algorithm": "Morphological Watersheed", "multiple_files_widget": false, "units_value": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "io": {"load_image_directory": "/home/czaki/Projekty/PartSeg/test_data", "load_data_filter": "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)", "history": ["/home/czaki/Dropbox/segmentation/stage_9_003", "/home/czaki/Projekty", "/home/czaki/Dropbox/segmentation/stage_9_002", "/home/czaki/Dropbox/stage_9_002", "/home/czaki/Dokumenty/smFish/first_data", "/home/czaki/Projekty/partseg/test_data", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui/dane partseg", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui", "/home/czaki/Pobrane/tmp", "/home/czaki/Obrazy"], "save_batch": "/home/czaki", "save_components_directory": "/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004", "save_segmentation_directory": "/home/czaki/Projekty/PartSeg/examples", "open_segmentation_directory": "/home/czaki/Projekty/smfish-PartSeg/data/27.10.2021/Col-0_Salt_emb2_001", "batch_directory": "/home/czaki", "multiple_open_directory": "/media/czaki/Grzesiek/Ada_chromatyna/150615_timecourse/cLTP30min_2_H3P", "multiple_open_filter": "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)", "files_open_history": [[["/home/czaki/Projekty/PartSeg/test_data/test_nucleus.tif"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Projekty/smfish-PartSeg/data/27.10.2021/Col-0_Salt_emb2_001/Col-0_Salt_emb2_001_points.csv"], "Points (*.csv)"], [["/home/czaki/Projekty/smfish-PartSeg/data/27.10.2021/Col-0_Salt_emb2_001/test.obsep"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/test.obsep"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/test.obsep"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Dokumenty/arabidopsis/data/2018-04-19/3h1_DAPI_FP9_Series007_2_deconvolved.tif"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/points.csv"], "Points (*.csv)"], [["/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/A/A.lsm"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/C/C.lsm"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/B/B.lsm"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"]], "load_image_file": "/home/czaki/Projekty/PartSeg/test_data/test_nucleus.tif", "dir_location_history": ["/home/czaki/Projekty/PartSeg/test_data", "/home/czaki/Projekty/smfish-PartSeg/data/27.10.2021/Col-0_Salt_emb2_001", "/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004", "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points", "/home/czaki/Dokumenty/arabidopsis/data/2018-04-19", "/home/czaki/Projekty/PartSeg/examples", "/media/czaki/Grzesiek/Ada_chromatyna/150615_timecourse/cLTP30min_2_H3P", "/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/E", "/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/D", "/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/C"], "open_directory": "/home/czaki/Projekty/PartSeg/test_data", "open_filter": "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)", "open_file": "/home/czaki/Projekty/PartSeg/test_data/test_nucleus.tif", "__class__": "PartSegCore.json_hooks.EventedDict"}, "simple_measurements": {"units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 1}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "mask_manager": {"mask_property": {"__Serializable__": true, "__subtype__": "PartSegCore.mask_create.MaskProperty", "dilate": {"__RadiusType__": true, "value": 0}, "dilate_radius": 0, "fill_holes": {"__RadiusType__": true, "value": 0}, "max_holes_size": 0, "save_components": false, "clip_to_mask": false, "reversed_mask": false}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.14/mask/view_settings.json b/package/tests/test_data/old_saves/0.13.14/mask/view_settings.json new file mode 100644 index 000000000..15ccb1b5d --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.14/mask/view_settings.json @@ -0,0 +1 @@ +{"default": {"custom_colormap": {"custom_7h4Y7cyokY": {"__Colormap__": true, "name": "custom_7h4Y7cyokY", "colors": [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0], [0.0, 1.0, 1.0, 1.0], [1.0, 1.0, 0.0, 1.0], [1.0, 0.0, 0.0, 1.0]], "interpolation": "linear", "controls": [0.0, 0.25, 0.5, 0.75, 1.0]}, "custom_VBodz4weU1": {"__Colormap__": true, "name": "custom_VBodz4weU1", "colors": [[0.0, 0.0, 0.0, 1.0], [1.0, 0.3333333432674408, 0.0, 1.0], [1.0, 1.0, 0.0, 1.0], [0.0, 0.3333333432674408, 1.0, 1.0], [0.3333333432674408, 1.0, 0.0, 1.0]], "interpolation": "linear", "controls": [0.0, 0.25, 0.5, 0.75, 1.0]}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "custom_label_colors": {"custom_te9A1IUwAK": [[255, 255, 0], [0, 85, 255]], "custom_Yt60W8B0MX": [[255, 255, 0], [0, 170, 255], [255, 0, 127], [170, 255, 0]], "__class__": "PartSegCore.json_hooks.EventedDict"}, "channelcontrol": {"image_state": {"opacity": 0.8999999999999999, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.napari_image_view.LabelEnum", "value": 1}, "only_border": 0, "border_thick": 7, "__class__": "PartSegCore.json_hooks.EventedDict"}, "range_0": [0, 3000], "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_0": 6.1, "lock_0": 0, "channels_count": 4, "cmap0": "red", "cmap1": "green", "lock_1": 0, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "cmap2": "blue", "lock_2": 0, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "cmap3": "magenta", "lock_3": 0, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 2}, "filter_radius_1": 1.0, "filter_radius_2": 1.0, "filter_radius_3": 3.0, "cmap4": "Grayscale", "lock_4": false, "use_filter_4": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_4": 1, "range_1": [0, 42000], "gamma_value_0": 1.0, "gamma_value_1": 1.0, "gamma_value_2": 1, "gamma_value_3": 1.0, "range_2": [0, 10000], "range_3": [0, 65000], "gamma_value_4": 1, "cmap": {"num0": "red", "num1": "green", "num2": "blue", "num3": "magenta", "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "mask_presentation_color": [255, 255, 255], "mask_presentation_opacity": 1, "colormaps": ["red", "green", "blue", "magenta", "inferno", "magma", "plasma", "viridis"], "labels_used": "default", "main_window_geometry": "01d9d0cb00030000000007be0000001b0000117f0000059f00000a1a0000004000001042000003fe00000000020000000a00000007be000000400000117f0000059f", "custom_colors": [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [0.0392156862745098, 0.1568627450980392, 0.047058823529411764, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [0.39215686274509803, 0.01568627450980392, 0.8627450980392157, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], "advanced_window_geometry": "01d9d0cb0003000000001257000001da000017410000046300001257000001ff00001741000004630000000200000000078000001257000001ff0000174100000463", "theme": "light", "simple_measurement_window_geometry": "01d9d0cb00030000000011c8000000df0000156c00000304000011c8000001040000156c0000030400000002000000000780000011c8000001040000156c00000304", "first_start": false, "rendering_mode": "iso_categorical", "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.15/analysis/batch_plans_save.json b/package/tests/test_data/old_saves/0.13.15/analysis/batch_plans_save.json new file mode 100644 index 000000000..fd9e010c8 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.15/analysis/batch_plans_save.json @@ -0,0 +1 @@ +{"default": {"test": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "aaa", "algorithm": "Border Rim", "values": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}, {"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.Save", "suffix": "_proj", "directory": "", "algorithm": "Project (*.tgz *.tbz2 *.gz *.bz2)", "short_name": "project", "values": {}}, "children": []}]}]}, "name": "test"}, "test2": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "test1", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Iso Data", "values": {"masked": true, "bins": 256}}, "minimum_size": 500, "side_connection": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}, {"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.Save", "suffix": "_proj2", "directory": "", "algorithm": "Project (*.tgz *.tbz2 *.gz *.bz2)", "short_name": "project", "values": {}}, "children": []}]}]}, "name": "test2"}, "test3": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "thr_10000", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 10000}}, "minimum_size": 500, "side_connection": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}, "name": "test3"}, "Chr1_ch1_G1_MSO_ME16_Triangle128_150_136_COS": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MaskSuffix", "name": "", "suffix": "_mask"}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "Chr1_Ch1_G1_MSO_ME16_Triangle128_150_136_COS", "algorithm": "Lower threshold with watershed", "values": {"channel": 0, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Maximum Entropy", "values": {"masked": true, "bins": 16}}, "base_threshold": {"name": "Triangle", "values": {"masked": true, "bins": 128}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 150, "reflective": true}}, "minimum_size": 136, "side_connection": true}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 1}, "measurement_profile": {"__MeasurementProfile__": true, "name": "Chr1_2020", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Components number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Pixel brightness sum per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Pixel brightness sum", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Mean pixel brightness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Mean pixel brightness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Maximum pixel brightness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Maximum pixel brightness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Minimum pixel brightness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Minimum pixel brightness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI First principal axis length per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "First principal axis length", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Second principal axis length per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Second principal axis length", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Third principal axis length per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Third principal axis length", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Compactness per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Compactness", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Sphericity per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Sphericity", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Surface per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Surface", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume/Mask Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Node", "left": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}, "op": "/", "right": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=1]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 1}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=2]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 2}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=3]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 3}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=4]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 4}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask distance splitting volume per component [Number of Parts=5, Equal Volume=True, Which part (from border)=5]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "distance splitting volume", "dict": {"num_of_parts": 5, "equal_volume": true, "part_selection": 5}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume mean component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 3}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter mean component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 3}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}]}, "name": "Chr1_ch1_G1_MSO_ME16_Triangle128_150_136_COS"}, "test_batch": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "base_seg", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 15000.0}}, "base_threshold": {"name": "Manual", "values": {"threshold": 11000.0}}}}, "sprawl_type": {"name": "Euclidean", "values": {}}, "minimum_size": 80, "side_connection": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test_batch", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Component Bounding Box per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Component Bounding Box", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}, "name": "test_batch"}, "Fos to Speckle ROI": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MaskSuffix", "name": "", "suffix": "_mask"}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "Speckle segmentation", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 3}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Li", "values": {"masked": true, "bins": 254}}, "base_threshold": {"name": "Otsu", "values": {"masked": true, "bins": 512}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 500, "reflective": false}}, "minimum_size": 15, "side_connection": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test Fos", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance per component [ROI extraction profile=Segmentation profile name: fos segmentation\nAlgorithm: Lower threshold\nchannel: 3\nnoise filtering: \n name: Median\n values: \n dimension type: Layer\n radius: 2\nthreshold: \n name: Yen\n values: \n masked: True\n bins: 512\nminimum size: 100\nside connection: True, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "fos segmentation", "algorithm": "Lower threshold", "values": {"channel": 2, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Yen", "values": {"masked": true, "bins": 512}}, "minimum_size": 100, "side_connection": true}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}]}, "name": "Fos to Speckle ROI"}, "gen_mask": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "generate_mask", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Manual", "values": {"threshold": 1.0}}, "minimum_size": 100, "side_connection": true}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MaskCreate", "name": "", "mask_property": {"__Serializable__": true, "__subtype__": "PartSegCore.mask_create.MaskProperty", "dilate": {"__RadiusType__": true, "value": 0}, "dilate_radius": 0, "fill_holes": {"__RadiusType__": true, "value": 0}, "max_holes_size": 0, "save_components": false, "clip_to_mask": false, "reversed_mask": false}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.Save", "suffix": "_mask", "directory": "", "algorithm": "Mask (*.tiff *.tif)", "short_name": "mask_tiff", "values": {}}, "children": []}]}]}]}, "name": "gen_mask"}, "test_aaa": {"__CalculationPlan__": true, "tree": {"__CalculationTree__": true, "operation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.RootType", "value": 0}, "children": [{"__CalculationTree__": true, "operation": {"__SegmentationProfile__": true, "name": "aaa", "algorithm": "Border Rim", "values": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}}, "children": [{"__CalculationTree__": true, "operation": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.calculation_plan.MeasurementCalculate", "channel": -1, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "measurement_profile": {"__MeasurementProfile__": true, "name": "test_roi", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance[ROI extraction profile=Segmentation profile name: test2\nAlgorithm: Lower threshold\nchannel: 1\nnoise filtering: \n name: Median\n values: \n dimension type: Stack\n radius: 1\nthreshold: \n name: Manual\n values: \n threshold: 30000.0\nminimum size: 100\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "test2", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 30000.0}}, "minimum_size": 100, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance per component [ROI extraction profile=Segmentation profile name: test2\nAlgorithm: Lower threshold\nchannel: 1\nnoise filtering: \n name: Median\n values: \n dimension type: Stack\n radius: 1\nthreshold: \n name: Manual\n values: \n threshold: 30000.0\nminimum size: 100\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "test2", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 30000.0}}, "minimum_size": 100, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "name_prefix": ""}, "children": []}]}]}, "name": "test_aaa"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.15/analysis/segmentation_pipeline_save.json b/package/tests/test_data/old_saves/0.13.15/analysis/segmentation_pipeline_save.json new file mode 100644 index 000000000..f66a313ff --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.15/analysis/segmentation_pipeline_save.json @@ -0,0 +1 @@ +{"default": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.15/analysis/segmentation_profiles_save.json b/package/tests/test_data/old_saves/0.13.15/analysis/segmentation_profiles_save.json new file mode 100644 index 000000000..be84be7fb --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.15/analysis/segmentation_profiles_save.json @@ -0,0 +1 @@ +{"default": {"aaa": {"__SegmentationProfile__": true, "name": "aaa", "algorithm": "Border Rim", "values": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}}}, "test1": {"__SegmentationProfile__": true, "name": "test1", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Iso Data", "values": {"masked": true, "bins": 256}}, "minimum_size": 500, "side_connection": false}}, "thr_10000": {"__SegmentationProfile__": true, "name": "thr_10000", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 10000}}, "minimum_size": 500, "side_connection": false}}, "test": {"__SegmentationProfile__": true, "name": "test", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Manual", "values": {"threshold": 15000.0}}, "minimum_size": 100, "side_connection": true}}, "test2": {"__SegmentationProfile__": true, "name": "test2", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 30000.0}}, "minimum_size": 100, "side_connection": false}}, "base_seg": {"__SegmentationProfile__": true, "name": "base_seg", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 15000.0}}, "base_threshold": {"name": "Manual", "values": {"threshold": 11000.0}}}}, "sprawl_type": {"name": "Euclidean", "values": {}}, "minimum_size": 80, "side_connection": false}}, "fos segmentation": {"__SegmentationProfile__": true, "name": "fos segmentation", "algorithm": "Lower threshold", "values": {"channel": 2, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Yen", "values": {"masked": true, "bins": 512}}, "minimum_size": 100, "side_connection": true}}, "Speckle segmentation": {"__SegmentationProfile__": true, "name": "Speckle segmentation", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 3}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Li", "values": {"masked": true, "bins": 254}}, "base_threshold": {"name": "Otsu", "values": {"masked": true, "bins": 512}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 500, "reflective": false}}, "minimum_size": 15, "side_connection": false}}, "generate_mask": {"__SegmentationProfile__": true, "name": "generate_mask", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Manual", "values": {"threshold": 1.0}}, "minimum_size": 100, "side_connection": true}}, "test_profile": {"__SegmentationProfile__": true, "name": "test_profile", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2}}, "threshold": {"name": "Manual", "values": {"threshold": 7900.0}}, "minimum_size": 100, "side_connection": true}}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.15/analysis/segmentation_settings.json b/package/tests/test_data/old_saves/0.13.15/analysis/segmentation_settings.json new file mode 100644 index 000000000..30f3048d6 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.15/analysis/segmentation_settings.json @@ -0,0 +1 @@ +{"default": {"units_value": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "algorithm_widget_state": {"Lower threshold": {"channel": 1, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Manual": {"threshold": 70.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Huang": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Triangle": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Shanbhag": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Li": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Yen": {"masked": true, "bins": 512, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Kittler Illingworth": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Iso Data": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Otsu": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Moments": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Maximum Entropy": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Renyi Entropy": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Intermodes": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 99, "side_connection": true, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 8000, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Range threshold": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "lower_threshold": 10000, "upper_threshold": 16000, "minimum_size": 8000, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Lower threshold flow": {"channel": 1, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 15000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Otsu": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Li": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Renyi Entropy": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Shanbhag": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Triangle": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Huang": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Yen": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Iso Data": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Kittler Illingworth": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Moments": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"Manual": {"threshold": 11000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Otsu": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Li": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Renyi Entropy": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Shanbhag": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Euclidean": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Path": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 80, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold flow": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"Manual": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 8000, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Multiple Otsu": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "components": 4, "valley": true, "hist_num": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Border Rim": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Split Mask o Part": {"num_of_parts": 5, "equal_volume": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Split Mask on Part": {"num_of_parts": 2, "equal_volume": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Lower threshold with watershed": {"channel": 1, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 3, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 17000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Li": {"masked": true, "bins": 254, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"Manual": {"threshold": 9300.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Otsu": {"masked": true, "bins": 512, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Euclidean": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 80, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold with watershed": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 8000, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Mask Distance Splitting": {"num_of_parts": 2, "equal_volume": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Segment Neutrofile": {"dna_marker": 1, "threshold": {"Manual": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "dead_dna_marker": 0, "dead_threshold": {"Manual": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 20, "net_size": 500, "separate_nets": true, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish segmentation": {"channel_nuc": 0, "noise_filtering_nucleus": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 370.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_nucleus_size": 500, "channel_molecule": 1, "background_estimate": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 5.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "molecule_threshold": {"Manual": {"threshold": 4.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_molecule_size": 15, "foreground_estimate": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2.5, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "background_estimate_radius": 5.0, "foreground_estimate_radius": 2.5, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish laplacian segmentation": {"channel_nuc": 0, "noise_filtering_nucleus": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_nucleus_size": 500, "channel_molecule": 1, "laplacian_radius": 1.3, "molecule_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_molecule_size": 5, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish spot segmentation": {"channel_nuc": 0, "noise_filtering_nucleus": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_nucleus_size": 500, "spot_method": {"Gaussian spot estimate": {"background_estimate_radius": 5.0, "foreground_estimate_radius": 2.5, "estimate_mask": true, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "channel_molecule": 1, "molecule_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_molecule_size": 5, "leave_the_biggest": true, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Trapalyzer": {"inner_dna": 1, "inner_dna_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "inner_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "outer_dna": 1, "outer_dna_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "outer_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "net_size": {"lower_bound": 850.0, "upper_bound": 999999.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "net_ext_brightness": {"lower_bound": 21.0, "upper_bound": 100.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "net_ext_brightness_std": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "unknown_net": true, "pmn_neu_pixel count": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmn_neu_brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmn_neu_ext. brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmn_neu_brightness gradient": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "rnd_neu_pixel count": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "rnd_neu_brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "rnd_neu_ext. brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "rnd_neu_brightness gradient": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "ner_neu_pixel count": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "ner_neu_brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "ner_neu_ext. brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "ner_neu_brightness gradient": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmp_neu_pixel count": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmp_neu_brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmp_neu_ext. brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "pmp_neu_brightness gradient": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "bacteria_pixel count": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "bacteria_brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "bacteria_ext. brightness": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "bacteria_brightness gradient": {"lower_bound": 0.0, "upper_bound": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_score": 0.8, "maximum_other": 0.4, "minimum_size": 40, "softness": 0.1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "algorithms": {"Lower threshold": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Manual", "values": {"threshold": 70.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 99, "side_connection": true, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Range threshold": {"channel": 0, "noise_filtering": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "lower_threshold": 10000, "upper_threshold": 16000, "minimum_size": 8000, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Lower threshold flow": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 15000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"name": "Manual", "values": {"threshold": 11000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"name": "Euclidean", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 80, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold flow": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Multiple Otsu": {"channel": 0, "noise_filtering": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "components": 4, "valley": true, "hist_num": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Border Rim": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Split Mask o Part": {"num_of_parts": 5, "equal_volume": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Split Mask on Part": {"num_of_parts": 2, "equal_volume": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Lower threshold with watershed": {"channel": 1, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 17000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"name": "Manual", "values": {"threshold": 9300.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sprawl_type": {"name": "Euclidean", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_size": 80, "side_connection": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Upper threshold with watershed": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Mask Distance Splitting": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Segment Neutrofile": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish segmentation": {"channel_nuc": 0, "noise_filtering_nucleus": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"name": "Manual", "values": {"threshold": 370.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_nucleus_size": 500, "channel_molecule": 1, "background_estimate": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 5.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "foreground_estimate": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 2.5, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "molecule_threshold": {"name": "Manual", "values": {"threshold": 4.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "minimum_molecule_size": 15, "__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish laplacian segmentation": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "sm-fish spot segmentation": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Trapalyzer": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "current_algorithm": "Lower threshold with watershed", "multiple_files": false, "io": {"history": ["/home/czaki/Projekty/partseg/test_data/stack1_components", "/home/czaki/Projekty/partseg/test_data", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui/dane partseg", "/home/czaki/Pobrane/tmp/test", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui"], "open_directory": "/home/czaki/Projekty/PartSeg/test_data/stack1_components", "save_directory": "/home/czaki/Pobrane/tmp", "export_directory": "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/measurements_profile_speckle segmentation.json", "batch_plan_directory": "/home/czaki/Pobrane/tmp", "multiple_open_directory": "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi", "open_filter": "Image with mask (*.tif *.tiff *.lsm)", "multiple_open_filter": "Image with mask (*.tif *.tiff *.lsm)", "save_filter": "Chimera CMAP (*.cmap)", "load_image_directory": "/home/czaki", "batch_directory": "/home/czaki/Projekty/PartSeg/test_data/stack1_components", "files_open_history": [[["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Obrazy/2019_04_24--ecoli_neu_tiff_converted/Ecoli neu_60min 1 4E-1N001.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Obrazy/2019_04_24--ecoli_neu_tiff_converted/Ecoli neu_60min 1 4E-1N001.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_3_Channel Alignment_component5.tif", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_3_Channel Alignment_component5_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/A/cut2/A_component2.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"]], "dir_location_history": ["/home/czaki/Projekty/PartSeg/test_data/stack1_components", "/home/czaki/Pobrane/tmp", "/home/czaki/Obrazy/2019_04_24--ecoli_neu_tiff_converted", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi", "/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_no_mask", "/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_mask", "/home/czaki/Pobrane/tmp/test_data", "/home/czaki/Projekty/PartSeg/test_data", "/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/A/cut2", "/home/czaki/Obrazy"], "open_file": "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif", "save_screenshot": "/home/czaki/Obrazy", "multiple_files_open_history": [[["/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_1_Channel Alignment_component6.tif", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_1_Channel Alignment_component6_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_3_Channel Alignment_component5.tif", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_3_Channel Alignment_component5_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_1_Channel Alignment_component11.tif", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_1_Channel Alignment_component11_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_2_Channel Alignment_component5.tif", "/home/czaki/Projekty/PartSeg/test_data/roi_to_roi/20210521_Fos488_Erg647_SF3a66A555_crtl_cLTP_2_Channel Alignment_component5_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_no_mask/test_component2.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_mask/test_component2.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_no_mask/test_component2.tif", "/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/components_no_mask/test_component2_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component1.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Pobrane/tmp/test_data/20210930-YoYo1-Hoechst-120min_DMSO-11_Channel Alignment_component7.tif", "/home/czaki/Pobrane/tmp/test_data/20210930-YoYo1-Hoechst-120min_DMSO-11_Channel Alignment_component7_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_component1.tgz"], "Project (*.tgz *.tbz2 *.gz *.bz2)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component1.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component1_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/A/cut2/A_component2.tif"], "Image (*.tif *.tiff *.lsm *.czi *.oib *.oif)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component4_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"], [["/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6.tif", "/home/czaki/Projekty/PartSeg/test_data/stack1_components/stack1_component6_mask.tif"], "Image with mask (*.tif *.tiff *.lsm)"]], "__class__": "PartSegCore.json_hooks.EventedDict"}, "mask_manager": {"mask_property": {"__Serializable__": true, "__subtype__": "PartSegCore.mask_create.MaskProperty", "dilate": {"__RadiusType__": true, "value": 1}, "dilate_radius": 5, "fill_holes": {"__RadiusType__": true, "value": 1}, "max_holes_size": -1, "save_components": false, "clip_to_mask": false, "reversed_mask": false}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "save_parameters": {"Project (*.tgz *.tbz2 *.gz *.bz2)": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Segmentation (*.npy)": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Segmentation (*.tiff *.tif)": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Image (*.tiff *.tif)": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Chimera CMAP (*.cmap)": {"channel": 0, "separated_objects": false, "clip": false, "units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "reverse": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "multiple_files_widget": 0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.15/analysis/statistic_profiles_save.json b/package/tests/test_data/old_saves/0.13.15/analysis/statistic_profiles_save.json new file mode 100644 index 000000000..ce11ae873 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.15/analysis/statistic_profiles_save.json @@ -0,0 +1 @@ +{"default": {"test": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Components number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "problematic set": {"__MeasurementProfile__": true, "name": "problematic set", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Pixel Brightness Sum", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Pixel brightness sum", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Components Number", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Components number", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Segmentation Diameter", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "test_load": {"__MeasurementProfile__": true, "name": "test_load", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask segmentation distance[Distance from mask=Border, Distance to segmentation=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "ROI distance", "dict": {"distance_from_mask": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_segmentation": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "test2": {"__MeasurementProfile__": true, "name": "test2", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance[ROI extraction profile=Segmentation profile name: test1\nAlgorithm: Lower threshold\nchannel: 2\nnoise filtering: \n name: None\n values: \n\nthreshold: \n name: Iso Data\n values: \n masked: True\n bins: 256\nminimum size: 500\nside connection: False, Distance new ROI=Mass center, Distance to ROI=Geometrical center]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "test1", "algorithm": "Lower threshold", "values": {"channel": 1, "noise_filtering": {"name": "None", "values": {}}, "threshold": {"name": "Iso Data", "values": {"masked": true, "bins": 256}}, "minimum_size": 500, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 2}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 3}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "test_roi": {"__MeasurementProfile__": true, "name": "test_roi", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance[ROI extraction profile=Segmentation profile name: test2\nAlgorithm: Lower threshold\nchannel: 1\nnoise filtering: \n name: Median\n values: \n dimension type: Stack\n radius: 1\nthreshold: \n name: Manual\n values: \n threshold: 30000.0\nminimum size: 100\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "test2", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 30000.0}}, "minimum_size": 100, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance per component [ROI extraction profile=Segmentation profile name: test2\nAlgorithm: Lower threshold\nchannel: 1\nnoise filtering: \n name: Median\n values: \n dimension type: Stack\n radius: 1\nthreshold: \n name: Manual\n values: \n threshold: 30000.0\nminimum size: 100\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "test2", "algorithm": "Lower threshold", "values": {"channel": 0, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 2}, "radius": 1}}, "threshold": {"name": "Manual", "values": {"threshold": 30000.0}}, "minimum_size": 100, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "test_batch": {"__MeasurementProfile__": true, "name": "test_batch", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Volume per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Volume", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Diameter per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Diameter", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI Component Bounding Box per component ", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Component Bounding Box", "dict": {}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "hanna_test": {"__MeasurementProfile__": true, "name": "test", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance per component [ROI extraction profile=Segmentation profile name: Speckle segmentation\nAlgorithm: Lower threshold with watershed\nchannel: 2\nnoise filtering: \n name: Median\n values: \n dimension type: Layer\n radius: 3\nthreshold: \n name: Base/Core\n values: \n core threshold: \n name: Li\n values: \n masked: True\n bins: 254\n base threshold: \n name: Otsu\n values: \n masked: True\n bins: 512\nsprawl type: \n name: MultiScale Opening\n values: \n step limits: 500\n reflective: False\nminimum size: 15\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "Speckle segmentation", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 3}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Li", "values": {"masked": true, "bins": 254}}, "base_threshold": {"name": "Otsu", "values": {"masked": true, "bins": 512}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 500, "reflective": false}}, "minimum_size": 15, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "Speckle segmentation": {"__MeasurementProfile__": true, "name": "Speckle segmentation", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "ROI to ROI distance per component [ROI extraction profile=Segmentation profile name: Speckle segmentation\nAlgorithm: Lower threshold with watershed\nchannel: 2\nnoise filtering: \n name: Median\n values: \n dimension type: Layer\n radius: 3\nthreshold: \n name: Base/Core\n values: \n core threshold: \n name: Li\n values: \n masked: True\n bins: 254\n base threshold: \n name: Otsu\n values: \n masked: True\n bins: 512\nsprawl type: \n name: MultiScale Opening\n values: \n step limits: 500\n reflective: False\nminimum size: 15\nside connection: False, Distance new ROI=Border, Distance to ROI=Border]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "to ROI distance", "dict": {"profile": {"__SegmentationProfile__": true, "name": "Speckle segmentation", "algorithm": "Lower threshold with watershed", "values": {"channel": 1, "noise_filtering": {"name": "Median", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 3}}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Li", "values": {"masked": true, "bins": 254}}, "base_threshold": {"name": "Otsu", "values": {"masked": true, "bins": 512}}}}, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 500, "reflective": false}}, "minimum_size": 15, "side_connection": false}}, "distance_from_new_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}, "distance_to_roi": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_calculation.DistancePoint", "value": 1}}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 1}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 2}, "channel": null}}], "name_prefix": ""}, "test_coloc": {"__MeasurementProfile__": true, "name": "test_coloc", "chosen_fields": [{"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Pearson correlation coefficient, Randomize channel=False, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Pearson correlation coefficient", "randomize": false, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Mander's overlap coefficient, Randomize channel=False, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Mander's overlap coefficient", "randomize": false, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Intensity correlation quotient, Randomize channel=False, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Intensity correlation quotient", "randomize": false, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Spearman rank correlation, Randomize channel=False, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Spearman rank correlation", "randomize": false, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Pearson correlation coefficient, Randomize channel=True, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Pearson correlation coefficient", "randomize": true, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Mander's overlap coefficient, Randomize channel=True, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Mander's overlap coefficient", "randomize": true, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Intensity correlation quotient, Randomize channel=True, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Intensity correlation quotient", "randomize": true, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}, {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.MeasurementEntry", "name": "Mask Colocalization[Channel 1=0, Channel 2=1, Colocalization=Spearman rank correlation, Randomize channel=True, Randomize num=10]", "calculation_tree": {"__Serializable__": true, "__subtype__": "PartSegCore.analysis.measurement_base.Leaf", "name": "Colocalization", "dict": {"channel_fst": 0, "channel_scd": 1, "colocalization": "Spearman rank correlation", "randomize": true, "randomize_repeat": 10}, "power": 1.0, "area": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.AreaType", "value": 2}, "per_component": {"__Enum__": true, "__subtype__": "PartSegCore.analysis.measurement_base.PerComponent", "value": 1}, "channel": null}}], "name_prefix": ""}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.15/analysis/view_settings.json b/package/tests/test_data/old_saves/0.13.15/analysis/view_settings.json new file mode 100644 index 000000000..ac92b1196 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.15/analysis/view_settings.json @@ -0,0 +1 @@ +{"default": {"custom_colormap": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "custom_label_colors": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "first_start": false, "raw_image": {"range_0": [0, 65000], "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_0": 1, "lock_0": false, "gamma_value_0": 1, "image_state": {"only_border": false, "opacity": 1, "border_thick": 1, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.napari_image_view.LabelEnum", "value": 1}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "channels_count": 2, "cmap": {"num0": "red", "num1": "green", "num2": "blue", "num3": "magenta", "__class__": "PartSegCore.json_hooks.EventedDict"}, "lock_1": false, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_1": 1, "lock_2": false, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_2": 1, "lock_3": false, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_3": 1, "filter_radius_1": 1, "filter_radius_2": 1, "filter_radius_3": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "result_image": {"range_0": [0, 65000], "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_0": 1, "lock_0": false, "gamma_value_0": 1, "image_state": {"only_border": false, "opacity": 1, "border_thick": 1, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.napari_image_view.LabelEnum", "value": 1}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "channels_count": 2, "cmap": {"num0": "red", "num1": "green", "num2": "blue", "num3": "magenta", "__class__": "PartSegCore.json_hooks.EventedDict"}, "lock_1": false, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_1": 1, "lock_2": false, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_2": 1, "lock_3": false, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "gamma_value_3": 1, "filter_radius_1": 1, "filter_radius_2": 1, "filter_radius_3": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "rendering_mode": "iso_categorical", "custom_colors": [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [0.0392156862745098, 0.1568627450980392, 0.047058823529411764, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [0.39215686274509803, 0.01568627450980392, 0.8627450980392157, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], "mask_presentation_color": [255, 255, 255], "mask_presentation_opacity": 1, "colormaps": ["red", "green", "blue", "magenta", "inferno", "magma"], "hide_left_panel": false, "labels_used": "default", "main_window_geometry": "01d9d0cb0003000000000a840000004b0000112d000003e400000a84000000700000112d000003e400000000000000000a0000000a84000000700000112d000003e4", "advanced_window_geometry": "01d9d0cb0003000000000aa40000020600000edf000004ef00000aa40000022b00000edf000004ef00000000000000000a0000000aa40000022b00000edf000004ef", "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.15/mask/segmentation_settings.json b/package/tests/test_data/old_saves/0.13.15/mask/segmentation_settings.json new file mode 100644 index 000000000..2be449a61 --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.15/mask/segmentation_settings.json @@ -0,0 +1 @@ +{"default": {"algorithm_widget_state": {"Threshold": {"channel": 3, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.3, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Manual": {"threshold": 11000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Otsu": {"masked": true, "bins": 128, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Li": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Renyi Entropy": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Shanbhag": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Triangle": {"masked": true, "bins": 256, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Opening": {"smooth_border_radius": 2, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Vote": {"neighbourhood_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.watershed.NeighType", "value": 18}, "support_level": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 500, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Threshold Flow": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Base/Core": {"core_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "sprawl_type": {"MultiScale Opening sprawl": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Only Threshold": {"channel": 3, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": 11000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Auto Threshold": {"channel": 0, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "suggested_size": 200000, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Cellpose cyto": {"cell_channel": 0, "nucleus_channel": 0, "diameter": 30, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Cell from nucleus": {"nucleus_channel": 0, "nucleus_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_channel": 0, "cell_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "flow_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Cell from nucleus flow": {"nucleus_channel": 0, "nucleus_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_channel": 0, "cell_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "flow_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Maximum projection Threshold Flow": {"lower_layer": 0, "upper_layer": -1, "nucleus_channel": 0, "nucleus_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "nucleus_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_channel": 0, "cell_noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "cell_threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "flow_type": {"MultiScale Opening": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Morphological Watersheed": {"channel": 3, "noise_filtering": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Median": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Gauss": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"Manual": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"None": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 20, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "algorithms": {"Threshold": {"channel": 3, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.3, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Manual", "values": {"threshold": 11000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "Opening", "values": {"smooth_border_radius": 2, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 500, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Threshold Flow": {"channel": 0, "noise_filtering": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Base/Core", "values": {"core_threshold": {"name": "Manual", "values": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "base_threshold": {"name": "Manual", "values": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "sprawl_type": {"name": "MultiScale Opening", "values": {"step_limits": 100, "reflective": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Only Threshold": {"channel": 3, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": 11000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Auto Threshold": {"channel": 0, "noise_filtering": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Manual", "values": {"threshold": 8000, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 8000, "suggested_size": 200000, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "Cellpose cyto": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Cell from nucleus": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Cell from nucleus flow": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Maximum projection Threshold Flow": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "Morphological Watersheed": {"channel": 3, "noise_filtering": {"name": "Gauss", "values": {"dimension_type": {"__Enum__": true, "__subtype__": "PartSegCore.segmentation.noise_filtering.DimensionType", "value": 1}, "radius": 1.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "threshold": {"name": "Manual", "values": {"threshold": 8000.0, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "close_holes": true, "close_holes_size": 200, "smooth_border": {"name": "None", "values": {"__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "side_connection": false, "minimum_size": 20, "use_convex": false, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "current_algorithm": "Morphological Watersheed", "multiple_files_widget": false, "units_value": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 2}, "io": {"load_image_directory": "/home/czaki/Projekty/PartSeg/test_data", "load_data_filter": "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)", "history": ["/home/czaki/Dropbox/segmentation/stage_9_003", "/home/czaki/Projekty", "/home/czaki/Dropbox/segmentation/stage_9_002", "/home/czaki/Dropbox/stage_9_002", "/home/czaki/Dokumenty/smFish/first_data", "/home/czaki/Projekty/partseg/test_data", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui/dane partseg", "/home/czaki/Projekty/partseg/publications/article_text/figures/fig_gui", "/home/czaki/Pobrane/tmp", "/home/czaki/Obrazy"], "save_batch": "/home/czaki", "save_components_directory": "/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004", "save_segmentation_directory": "/home/czaki/Projekty/PartSeg/examples", "open_segmentation_directory": "/home/czaki/Projekty/smfish-PartSeg/data/27.10.2021/Col-0_Salt_emb2_001", "batch_directory": "/home/czaki", "multiple_open_directory": "/media/czaki/Grzesiek/Ada_chromatyna/150615_timecourse/cLTP30min_2_H3P", "multiple_open_filter": "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)", "files_open_history": [[["/home/czaki/Projekty/PartSeg/test_data/test_nucleus.tif"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Projekty/smfish-PartSeg/data/27.10.2021/Col-0_Salt_emb2_001/Col-0_Salt_emb2_001_points.csv"], "Points (*.csv)"], [["/home/czaki/Projekty/smfish-PartSeg/data/27.10.2021/Col-0_Salt_emb2_001/test.obsep"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/test.obsep"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points/test.obsep"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Dokumenty/arabidopsis/data/2018-04-19/3h1_DAPI_FP9_Series007_2_deconvolved.tif"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004/points.csv"], "Points (*.csv)"], [["/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/A/A.lsm"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/C/C.lsm"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"], [["/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/B/B.lsm"], "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)"]], "load_image_file": "/home/czaki/Projekty/PartSeg/test_data/test_nucleus.tif", "dir_location_history": ["/home/czaki/Projekty/PartSeg/test_data", "/home/czaki/Projekty/smfish-PartSeg/data/27.10.2021/Col-0_Salt_emb2_001", "/home/czaki/Projekty/smfish-PartSeg/data/deconvolved_7_004/stage_7_004", "/home/czaki/Dokumenty/smFish/smFISH_7_001_with_points", "/home/czaki/Dokumenty/arabidopsis/data/2018-04-19", "/home/czaki/Projekty/PartSeg/examples", "/media/czaki/Grzesiek/Ada_chromatyna/150615_timecourse/cLTP30min_2_H3P", "/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/E", "/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/D", "/media/czaki/Grzesiek/160112_Chr1aqua_q488_pred_TOPRO/C"], "open_directory": "/home/czaki/Projekty/PartSeg/test_data", "open_filter": "Image(*.tif *.tiff *.lsm *.czi *.oib *.oif *.obsep)", "open_file": "/home/czaki/Projekty/PartSeg/test_data/test_nucleus.tif", "__class__": "PartSegCore.json_hooks.EventedDict"}, "simple_measurements": {"units": {"__Enum__": true, "__subtype__": "PartSegCore.universal_const.Units", "value": 1}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "mask_manager": {"mask_property": {"__Serializable__": true, "__subtype__": "PartSegCore.mask_create.MaskProperty", "dilate": {"__RadiusType__": true, "value": 0}, "dilate_radius": 0, "fill_holes": {"__RadiusType__": true, "value": 0}, "max_holes_size": 0, "save_components": false, "clip_to_mask": false, "reversed_mask": false}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.13.15/mask/view_settings.json b/package/tests/test_data/old_saves/0.13.15/mask/view_settings.json new file mode 100644 index 000000000..15ccb1b5d --- /dev/null +++ b/package/tests/test_data/old_saves/0.13.15/mask/view_settings.json @@ -0,0 +1 @@ +{"default": {"custom_colormap": {"custom_7h4Y7cyokY": {"__Colormap__": true, "name": "custom_7h4Y7cyokY", "colors": [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 1.0], [0.0, 1.0, 1.0, 1.0], [1.0, 1.0, 0.0, 1.0], [1.0, 0.0, 0.0, 1.0]], "interpolation": "linear", "controls": [0.0, 0.25, 0.5, 0.75, 1.0]}, "custom_VBodz4weU1": {"__Colormap__": true, "name": "custom_VBodz4weU1", "colors": [[0.0, 0.0, 0.0, 1.0], [1.0, 0.3333333432674408, 0.0, 1.0], [1.0, 1.0, 0.0, 1.0], [0.0, 0.3333333432674408, 1.0, 1.0], [0.3333333432674408, 1.0, 0.0, 1.0]], "interpolation": "linear", "controls": [0.0, 0.25, 0.5, 0.75, 1.0]}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "custom_label_colors": {"custom_te9A1IUwAK": [[255, 255, 0], [0, 85, 255]], "custom_Yt60W8B0MX": [[255, 255, 0], [0, 170, 255], [255, 0, 127], [170, 255, 0]], "__class__": "PartSegCore.json_hooks.EventedDict"}, "channelcontrol": {"image_state": {"opacity": 0.8999999999999999, "show_label": {"__Enum__": true, "__subtype__": "PartSeg.common_gui.napari_image_view.LabelEnum", "value": 1}, "only_border": 0, "border_thick": 7, "__class__": "PartSegCore.json_hooks.EventedDict"}, "range_0": [0, 3000], "use_filter_0": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_0": 6.1, "lock_0": 0, "channels_count": 4, "cmap0": "red", "cmap1": "green", "lock_1": 0, "use_filter_1": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "cmap2": "blue", "lock_2": 0, "use_filter_2": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "cmap3": "magenta", "lock_3": 0, "use_filter_3": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 2}, "filter_radius_1": 1.0, "filter_radius_2": 1.0, "filter_radius_3": 3.0, "cmap4": "Grayscale", "lock_4": false, "use_filter_4": {"__Enum__": true, "__subtype__": "PartSegCore.image_operations.NoiseFilterType", "value": 0}, "filter_radius_4": 1, "range_1": [0, 42000], "gamma_value_0": 1.0, "gamma_value_1": 1.0, "gamma_value_2": 1, "gamma_value_3": 1.0, "range_2": [0, 10000], "range_3": [0, 65000], "gamma_value_4": 1, "cmap": {"num0": "red", "num1": "green", "num2": "blue", "num3": "magenta", "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.EventedDict"}, "mask_presentation_color": [255, 255, 255], "mask_presentation_opacity": 1, "colormaps": ["red", "green", "blue", "magenta", "inferno", "magma", "plasma", "viridis"], "labels_used": "default", "main_window_geometry": "01d9d0cb00030000000007be0000001b0000117f0000059f00000a1a0000004000001042000003fe00000000020000000a00000007be000000400000117f0000059f", "custom_colors": [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [0.0392156862745098, 0.1568627450980392, 0.047058823529411764, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [0.39215686274509803, 0.01568627450980392, 0.8627450980392157, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], "advanced_window_geometry": "01d9d0cb0003000000001257000001da000017410000046300001257000001ff00001741000004630000000200000000078000001257000001ff0000174100000463", "theme": "light", "simple_measurement_window_geometry": "01d9d0cb00030000000011c8000000df0000156c00000304000011c8000001040000156c0000030400000002000000000780000011c8000001040000156c00000304", "first_start": false, "rendering_mode": "iso_categorical", "__class__": "PartSegCore.json_hooks.EventedDict"}, "__class__": "PartSegCore.json_hooks.ProfileDict"} diff --git a/package/tests/test_data/old_saves/0.8.6/analysis/batch_plans_save.json b/package/tests/test_data/old_saves/0.8.6/analysis/batch_plans_save.json new file mode 100644 index 000000000..84c3ada99 --- /dev/null +++ b/package/tests/test_data/old_saves/0.8.6/analysis/batch_plans_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true} diff --git a/package/tests/test_data/old_saves/0.8.6/analysis/segmentation_pipeline_save.json b/package/tests/test_data/old_saves/0.8.6/analysis/segmentation_pipeline_save.json new file mode 100644 index 000000000..36e58a5e8 --- /dev/null +++ b/package/tests/test_data/old_saves/0.8.6/analysis/segmentation_pipeline_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {}} diff --git a/package/tests/test_data/old_saves/0.8.6/analysis/segmentation_profiles_save.json b/package/tests/test_data/old_saves/0.8.6/analysis/segmentation_profiles_save.json new file mode 100644 index 000000000..25f011dc1 --- /dev/null +++ b/package/tests/test_data/old_saves/0.8.6/analysis/segmentation_profiles_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"aa": {"__SegmentationProperty__": true, "name": "aa", "algorithm": "Lower threshold", "values": {"threshold": {"name": "Manual", "values": {"threshold": 8000}}, "channel": 0, "minimum_size": 80, "noise_removal": {"name": "None", "values": {}}, "side_connection": false}}, "tt": {"__SegmentationProperty__": true, "name": "tt", "algorithm": "Lower threshold", "values": {"threshold": {"name": "Manual", "values": {"threshold": 8100}}, "channel": 0, "minimum_size": 80, "noise_removal": {"name": "None", "values": {}}, "side_connection": false}}, "tt2": {"__SegmentationProperty__": true, "name": "tt2", "algorithm": "Lower threshold", "values": {"threshold": {"name": "Manual", "values": {"threshold": 8100}}, "channel": 0, "minimum_size": 80, "noise_removal": {"name": "None", "values": {}}, "side_connection": false}}, "dfgg": {"__SegmentationProperty__": true, "name": "dfgg", "algorithm": "Lower threshold", "values": {"threshold": {"name": "Manual", "values": {"threshold": 8100}}, "channel": 2, "minimum_size": 80, "noise_removal": {"name": "None", "values": {}}, "side_connection": false}}}} diff --git a/package/tests/test_data/old_saves/0.8.6/analysis/segmentation_settings.json b/package/tests/test_data/old_saves/0.8.6/analysis/segmentation_settings.json new file mode 100644 index 000000000..8385411c6 --- /dev/null +++ b/package/tests/test_data/old_saves/0.8.6/analysis/segmentation_settings.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"units_value": {"__Enum__": true, "__subtype__": "PartSeg.utils.universal_const.Units", "value": 2}, "algorithm_widget_state": {"Lower threshold": {"threshold": {"Manual": {"threshold": 8100}}, "channel": 2, "minimum_size": 80, "noise_removal": {"None": {}}, "side_connection": false}, "Upper threshold": {"threshold": {"Manual": {"threshold": 8000}}, "channel": 0, "minimum_size": 8000, "noise_removal": {"None": {}}, "side_connection": false}, "Range threshold": {"lower_threshold": 10000, "upper_threshold": 10000, "channel": 0, "minimum_size": 8000, "noise_removal": {"None": {}}, "side_connection": false}, "Lower threshold flow": {"threshold": {"Double Choose": {"core_threshold": {"Manual": {"threshold": 8000}}, "base_threshold": {"Manual": {"threshold": 8000}}}}, "sprawl_type": {"Path sprawl": {}}, "channel": 0, "minimum_size": 8000, "noise_removal": {"None": {}}, "side_connection": false}, "Upper threshold flow": {"threshold": {"Double Choose": {"core_threshold": {"Manual": {"threshold": 8000}}, "base_threshold": {"Manual": {"threshold": 8000}}}}, "sprawl_type": {"Path sprawl": {}}, "channel": 0, "minimum_size": 8000, "noise_removal": {"None": {}}, "side_connection": false}, "Multiple Otsu": {"channel": 0, "noise_removal": {"None": {}}, "components": 2, "valley": true, "hist_num": 128}, "Border Rim": {"distance": 700.0, "units": {"__Enum__": true, "__subtype__": "PartSeg.utils.universal_const.Units", "value": 2}}}, "algorithms": {"Lower threshold": {"threshold": {"name": "Manual", "values": {"threshold": 8100}}, "channel": 2, "minimum_size": 80, "noise_removal": {"name": "None", "values": {}}, "side_connection": false}, "Upper threshold": {}, "Range threshold": {}, "Lower threshold flow": {}, "Upper threshold flow": {}, "Multiple Otsu": {}, "Border Rim": {}}, "current_algorithm": "", "io": {"open_directory": "/home/czaki", "open_filter": "Image (*.tif *.tiff *.lsm)"}}} diff --git a/package/tests/test_data/old_saves/0.8.6/analysis/statistic_profiles_save.json b/package/tests/test_data/old_saves/0.8.6/analysis/statistic_profiles_save.json new file mode 100644 index 000000000..36e58a5e8 --- /dev/null +++ b/package/tests/test_data/old_saves/0.8.6/analysis/statistic_profiles_save.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {}} diff --git a/package/tests/test_data/old_saves/0.8.6/analysis/view_settings.json b/package/tests/test_data/old_saves/0.8.6/analysis/view_settings.json new file mode 100644 index 000000000..267f44970 --- /dev/null +++ b/package/tests/test_data/old_saves/0.8.6/analysis/view_settings.json @@ -0,0 +1 @@ +{"__ProfileDict__": true, "default": {"colormaps": ["BlackBlue", "BlackGreen", "BlackMagenta", "BlackRed", "gray"], "image_state": {"opacity": 1, "show_label": 1, "only_border": true, "border_thick": 1}, "mask_presentation": ["white", 1], "result_control": {"cmap0": "BlackRed", "cmap1": "BlackGreen", "cmap2": "BlackBlue", "cmap3": "BlackMagenta", "range_0": [0, 65000], "use_gauss_0": false, "gauss_radius_0": 1.0, "lock_0": false, "lock_1": false, "lock_2": false, "lock_3": false, "use_gauss_1": false, "gauss_radius_1": 1, "use_gauss_2": false, "gauss_radius_2": 1, "use_gauss_3": false, "gauss_radius_3": 1}, "hide_left_panel": false, "advanced_window_geometry": "01d9d0cb0002000000000883000000ec00000bea00000355000008830000011100000bea0000035500000001000000000780", "main_window_geometry": "01d9d0cb00020000000008c0000000b300000dbf00000383000008c0000000d800000dbf0000038300000001000000000780"}} diff --git a/setup.cfg b/setup.cfg index c0ef62124..75850783d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,8 +65,6 @@ install_requires = python_requires = >=3.7 package_dir = =package -setup_requires = - setuptools-scm[toml]>=3.4 tests_require = pytest pytest-cov @@ -147,6 +145,7 @@ parallel = True exclude_lines = pragma: no cover raise NotImplementedError + if typing.TYPE_CHECKING [flake8] max-line-length = 120 diff --git a/tutorials/tutorial_neuron_types/Neuron_types_example.ipynb b/tutorials/tutorial_neuron_types/Neuron_types_example.ipynb index 16b96dc36..ad52a8aa2 100644 --- a/tutorials/tutorial_neuron_types/Neuron_types_example.ipynb +++ b/tutorials/tutorial_neuron_types/Neuron_types_example.ipynb @@ -94,11 +94,11 @@ "from PartSegCore.segmentation.segmentation_algorithm import ThresholdAlgorithm\n", "from PartSegCore.mask.io_functions import load_stack_segmentation, save_components\n", "from PartSegCore.analysis.measurement_calculation import Diameter, Volume\n", - "from PartSegCore.mask.algorithm_description import mask_algorithm_dict\n", + "from PartSegCore.mask.algorithm_description import MaskAlgorithmSelection\n", "from PartSegCore.convex_fill import convex_fill\n", "\n", "from PartSegCore.analysis import load_metadata as load_analysis_metadata\n", - "from PartSegCore.analysis.algorithm_description import analysis_algorithm_dict\n", + "from PartSegCore.analysis.algorithm_description import AnalysisAlgorithmSelection\n", "from PartSegCore.universal_const import Units\n", "\n", "from matplotlib import pyplot as plt\n", @@ -207,8 +207,8 @@ "segment = ThresholdAlgorithm()\n", "segment.set_image(image)\n", "# Choose on of this line\n", - "segment.set_parameters(**parameters1)\n", - "segment.set_parameters(**parameters2)" + "segment.set_parameters(parameters1)\n", + "segment.set_parameters(parameters2)" ] }, { @@ -218,12 +218,12 @@ "outputs": [], "source": [ "# or \n", - "Algorithm = mask_algorithm_dict[segmentation_description.algorithm]\n", + "Algorithm = MaskAlgorithmSelection[segmentation_description.algorithm]\n", "segment = Algorithm()\n", "segment.set_image(image)\n", "# Choose on of this line\n", - "segment.set_parameters(**parameters1)\n", - "segment.set_parameters(**parameters2)" + "segment.set_parameters(parameters1)\n", + "segment.set_parameters(parameters2)" ] }, { @@ -250,7 +250,7 @@ "outputs": [], "source": [ "project_tuple = load_stack_segmentation(os.path.join(data_path, \"DMSO_120min_2_4.seg\"))\n", - "segmentation = project_tuple.roi" + "segmentation = project_tuple.roi_info.roi" ] }, { @@ -490,10 +490,10 @@ "parameters = segmentation_description.values\n", "# this change is done to better filter components which contains few nucleus\n", "# then I calculate convex hull inside segmentation_function\n", - "parameters[\"use_convex\"] = False\n", - "Algorithm = mask_algorithm_dict[segmentation_description.algorithm]\n", + "parameters.use_convex = False\n", + "Algorithm = MaskAlgorithmSelection[segmentation_description.algorithm]\n", "segment = Algorithm()\n", - "segment.set_parameters(**parameters)\n", + "segment.set_parameters(parameters)\n", "\n", "cutted_neurons_dict = dict()\n", "\n", @@ -520,9 +520,9 @@ "neuron_segmentation_description = load_analysis_metadata(os.path.join(data_path, \"neuron_types_segmentation.json\"))\n", "\n", "neuron_segmentation_profile = neuron_segmentation_description[\"neuron_types\"]\n", - "NeuronAlgoritm = analysis_algorithm_dict[neuron_segmentation_profile.algorithm]\n", + "NeuronAlgoritm = AnalysisAlgorithmSelection[neuron_segmentation_profile.algorithm]\n", "neuron_segment = NeuronAlgoritm()\n", - "neuron_segment.set_parameters(**neuron_segmentation_profile.values)" + "neuron_segment.set_parameters(neuron_segmentation_profile.values)" ] }, { @@ -559,7 +559,7 @@ " for _, nucleus in nucleus_list:\n", " neuron_segment.set_image(nucleus)\n", " neuron_segment.set_mask(nucleus.mask[0]) # do not touch time \n", - " neuron_segment.set_parameters(**neuron_segmentation_profile.values)\n", + " neuron_segment.set_parameters(neuron_segmentation_profile.values)\n", " result = neuron_segment.calculation_run(empty)\n", " measurment_dict[nucleus_type].append(\n", " measurment_object.calculate(nucleus, 2 , result.roi, Units.nm))\n", @@ -631,7 +631,7 @@ " if cmap_base is None: \n", " continue\n", " cmap = ListedColormap(cmap_base.map(np.linspace(0, 1, 255)))\n", - " res.append(cmap(Normalize(min_max[i][0], min_max[i][1])(data[..., i])))\n", + " res.append(cmap(Normalize(min_max[i][0], min_max[i][1])(data[i])))\n", " return (np.amax(res, axis=0) * 255).astype(np.uint8)" ] }, @@ -654,7 +654,7 @@ "source": [ "image_app = TiffImageReader.read_image(os.path.join(data_path, \"DMSO_120min_2_1.lsm\"))\n", "project_tuple_app = load_stack_segmentation(os.path.join(data_path, \"DMSO_120min_2_1.seg\"))\n", - "segmentation_app = project_tuple_app.roi" + "segmentation_app = project_tuple_app.roi_info.roi" ] }, { @@ -665,7 +665,7 @@ "source": [ "from PartSegCore.color_image import default_colormap_dict\n", "layer_num = 28\n", - "layer = image_app.get_layer(0, layer_num)\n", + "layer = image_app.get_data_by_axis(t=0, z=layer_num)\n", "components_to_show = np.ones(segmentation_app.max()+1, dtype=np.uint8)\n", "colormaps_list = [default_colormap_dict.get(x, None) for x in [\"red\", \"green\", \"blue\", None]]\n", "\n", @@ -695,7 +695,7 @@ "colormaps_list = [default_colormap_dict.get(x, None) for x in [\"red\", \"green\", \"blue\", None]]\n", "colored_stack = []\n", "for i in range(image_app.layers):\n", - " layer = image_app.get_layer(0, i)\n", + " layer = image_app.get_data_by_axis(t=0, z=i)\n", " colored_image = color_image_fun(layer, colors=colormaps_list, min_max=image_app.get_ranges())\n", " add_labels(colored_image, segmentation_app[i], 1, True, 2, components_to_show, default_labels)\n", " colored_stack.append(colored_image)\n", @@ -736,7 +736,7 @@ "for file_path in glob(os.path.join(data_path, \"*.lsm\")):\n", " print(\"proces\", os.path.basename(file_path))\n", " segment = ThresholdAlgorithm()\n", - " segment.set_parameters(**parameters)\n", + " segment.set_parameters(parameters)\n", " image = TiffImageReader.read_image(file_path)\n", " segment.set_image(image)\n", " result = segment.calculation_run(empty)\n",