Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add option to calculate Measurement per Mask component #590

Merged
merged 20 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test_napari_widgets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
fail-fast: false
matrix:
platform: [ ubuntu-20.04 ]
python: ['3.8', '3.9']
napari_version: [napari411, napari412, napari413, napari414, napari415]
python: ['3.9']
napari_version: [napari412, napari413, napari414, napari415]
steps:
- uses: actions/checkout@v2

Expand Down
15 changes: 4 additions & 11 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
trigger:
branches:
include:
- '*'
exclude:
- dependabot/*
- documentation
- release/*
- hotfix/*
- bugfix/*
- sourcery/*
- update_requirements
- pre-commit-ci-update-config
- deepsource*
- master
- develop
- feature/**
- main
tags:
include:
- v*
Expand Down
22 changes: 13 additions & 9 deletions package/PartSeg/_roi_analysis/advanced_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ def profile_chosen(self, text):
self.delete_btn.setText("Delete pipeline")
self.rename_btn.setText("Rename pipeline")
else:
return
except KeyError:
return # pragma: no cover
except KeyError: # pragma: no cover
return

# TODO update with knowledge from profile dict
Expand Down Expand Up @@ -388,7 +388,7 @@ def __init__(self, settings: PartSettings, parent=None):
self.soft_reset_butt = QPushButton("Remove user parameters")
self.profile_name = QLineEdit(self)

self.delete_profile_butt = QPushButton("Delete ")
self.delete_profile_butt = QPushButton("Delete")
self.export_profiles_butt = QPushButton("Export")
self.import_profiles_butt = QPushButton("Import")
self.edit_profile_butt = QPushButton("Edit")
Expand Down Expand Up @@ -638,9 +638,12 @@ def get_parameters(
if node.area is None:
node = node.replace_(area=area)
if node.per_component is None:
node = node.replace_(per_component=component)
try:
node = node.replace_(per_component=component)
except ValueError as e: # pragma: no cover
QMessageBox().warning(self, "Problem in add measurement", str(e))
with suppress(KeyError):
arguments = MEASUREMENT_DICT[str(node.name)].get_fields()
arguments = MEASUREMENT_DICT[str(node.name)]._get_fields() # pylint: disable=protected-access
if len(arguments) > 0 and not dict(node.parameters):
dial = self.form_dialog(arguments)
if dial.exec_():
Expand All @@ -651,7 +654,7 @@ def get_parameters(

def choose_option(self):
selected_item = self.profile_options.currentItem()
if not isinstance(selected_item, MeasurementListWidgetItem):
if not isinstance(selected_item, MeasurementListWidgetItem): # pragma: no cover
raise ValueError(f"Current item (type: {type(selected_item)} is not instance of MeasurementListWidgetItem")
node = deepcopy(selected_item.stat)
# noinspection PyTypeChecker
Expand Down Expand Up @@ -704,6 +707,7 @@ def save_action(self):
self.settings.measurement_profiles[stat_prof.name] = stat_prof
self.settings.dump()
self.export_profiles_butt.setEnabled(True)
self.save_butt.setDisabled(True)

def named_save_action(self):
if self.profile_name.text() in self.settings.measurement_profiles:
Expand Down Expand Up @@ -894,11 +898,11 @@ def accept_response(self):
for name, (type_of, item) in self.object_dict.items():
if type_of == str:
val = str(item.text())
if val.strip() != "":
res[name] = val
else:
if not val.strip():
QMessageBox.warning(self, "Not all fields filled", "")
return
else:
res[name] = val
else:
val = type_of(item.value())
res[name] = val
Expand Down
10 changes: 6 additions & 4 deletions package/PartSeg/_roi_analysis/prepare_plan_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,9 @@ def enable_protect(self):
self.protect = previous

@classmethod
def refresh_profiles(cls, list_widget: QListWidget, new_values: typing.List[str]):
def refresh_profiles(
cls, list_widget: typing.Union[QListWidget, SearchableListWidget], new_values: typing.List[str]
):
index = cls.get_index(list_widget.currentItem(), new_values)
list_widget.clear()
list_widget.addItems(new_values)
Expand Down Expand Up @@ -840,7 +842,7 @@ def add_roi_extraction(self, roi_extraction: ROIExtractionOp):

def add_roi_extraction_pipeline(self, roi_extraction_pipeline: SegmentationPipeline):
if self.update_element_chk.isChecked():
QMessageBox.warning("Cannot update pipeline", "Cannot update pipeline")
QMessageBox.warning(self, "Cannot update pipeline", "Cannot update pipeline")
return
pos = self.calculation_plan.current_pos[:]
old_pos = pos[:]
Expand Down Expand Up @@ -1124,7 +1126,7 @@ def _save_roi_profile(self, data: ROIExtractionProfile):
)
if not ok:
return
return self._save_roi_profile(data.copy(update={"name": text}))
return self._save_roi_profile(typing.cast(ROIExtractionProfile, data.copy(update={"name": text})))
self.settings.roi_profiles[data.name] = data

def _save_measurement_profile(self, data: MeasurementProfile):
Expand All @@ -1134,7 +1136,7 @@ def _save_measurement_profile(self, data: MeasurementProfile):
)
if not ok:
return
return self._save_measurement_profile(data.copy(update={"name": text}))
return self._save_measurement_profile(typing.cast(MeasurementProfile, data.copy(update={"name": text})))
self.settings.measurement_profiles[data.name] = data

def update_plan_list(self):
Expand Down
20 changes: 12 additions & 8 deletions package/PartSeg/plugins/napari_widgets/measurement_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from napari import Viewer
from napari.layers import Image as NapariImage
from napari.layers import Labels
from napari.utils.notifications import show_info
from napari.utils.notifications import notification_manager, show_info
from qtpy.QtWidgets import QCheckBox, QLabel, QTabWidget

from PartSeg._roi_analysis.advanced_window import MeasurementSettings
Expand Down Expand Up @@ -76,14 +76,14 @@ def append_measurement_result(self):
if "Mask component" in df and self.mask_chose.value is not None:
df2 = df.groupby("Mask component").mean()
df2["index"] = df2.index
update_properties(df, self.mask_chose.value, self.overwrite.isChecked())
update_properties(df2, self.mask_chose.value, self.overwrite.isChecked())
df["index"] = df.index
update_properties(df, self.roi_chose.value, self.overwrite.isChecked())
if stat is None:
return
self.measurements_storage.add_measurements(stat)
self.previous_profile = compute_class.name
self.refresh_view()
update_properties(df, self.roi_chose.value, self.overwrite.isChecked())

def check_if_measurement_can_be_calculated(self, name):
if name in (NO_MEASUREMENT_STRING, ""):
Expand Down Expand Up @@ -124,8 +124,12 @@ def reset_choices(self, event=None):


def update_properties(new_properties, layer: Labels, overwrite):
if not overwrite:
for key, value in layer.properties.items():
if key not in new_properties:
new_properties[key] = value
layer.properties = new_properties
try:
if not overwrite:
new_properties = new_properties.copy()
for key, value in layer.properties.items():
if key not in new_properties:
new_properties[key] = value
layer.properties = new_properties
except Exception as e: # pylint: disable=broad-except # pragma: no cover
notification_manager.recive_error(e)
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def reset_choices(self, event=None):
self.form_widget.reset_choices(event)

def get_layer_list(self) -> typing.List[str]:
return [x.name for x in self.get_layers().values()]
return [x.name for x in self.get_layers().values() if x.name != "mask"]

def get_values(self):
values = self.form_widget.get_values()
Expand Down Expand Up @@ -167,7 +167,7 @@ def profile_dict(self) -> typing.Dict[str, ROIExtractionProfile]:
return self.settings.get_from_profile(f"{self.prefix()}.profiles", {})

def save_action(self):
widget: NapariInteractiveAlgorithmSettingsWidget = self.algorithm_chose.current_widget()
widget = typing.cast(NapariInteractiveAlgorithmSettingsWidget, self.algorithm_chose.current_widget())
profiles = self.profile_dict
while True:
text, ok = QInputDialog.getText(self, "Profile Name", "Input profile name here")
Expand Down Expand Up @@ -202,14 +202,14 @@ def algorithm_changed(self):
self.mask_name = ""

def update_mask(self):
widget: NapariInteractiveAlgorithmSettingsWidget = self.algorithm_chose.current_widget()
widget = typing.cast(NapariInteractiveAlgorithmSettingsWidget, self.algorithm_chose.current_widget())
mask = widget.get_layers().get("mask", None)
if getattr(mask, "name", "") != self.mask_name or (widget.mask() is None and mask is not None):
widget.set_mask(getattr(mask, "data", None))
self.mask_name = getattr(mask, "name", "")

def update_image(self):
widget: NapariInteractiveAlgorithmSettingsWidget = self.algorithm_chose.current_widget()
widget = typing.cast(NapariInteractiveAlgorithmSettingsWidget, self.algorithm_chose.current_widget())
self.settings.last_executed_algorithm = widget.name
layer_names: typing.List[str] = widget.get_layer_list()
if layer_names == self.channel_names:
Expand All @@ -222,7 +222,7 @@ def update_image(self):
self.mask_name = ""

def _run_calculation(self):
widget: NapariInteractiveAlgorithmSettingsWidget = self.algorithm_chose.current_widget()
widget = typing.cast(NapariInteractiveAlgorithmSettingsWidget, self.algorithm_chose.current_widget())
self.settings.last_executed_algorithm = widget.name
self.update_image()
self.update_mask()
Expand Down
6 changes: 1 addition & 5 deletions package/PartSeg/plugins/napari_widgets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,7 @@ def _element_list(self, fields) -> typing.Iterable[QtAlgorithmProperty]:
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)
}
return {name: el.get_value() for name, el in self.widgets_dict.items() if 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"}
Expand Down
18 changes: 16 additions & 2 deletions package/PartSegCore/analysis/measurement_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class PerComponent(Enum):
No = 1
Yes = 2
Mean = 3
Per_Mask_component = 4

def __str__(self):
return self.name.replace("_", " ")
Expand Down Expand Up @@ -99,6 +100,14 @@ def _validate_parameters(cls, v, values): # pylint: disable=R0201
v = REGISTER.migrate_data(class_to_str(method.__argument_class__), {}, v)
return method.__argument_class__(**v)

@validator("per_component")
def _validate_per_component(cls, v, values): # pylint: disable=R0201
if not isinstance(v, PerComponent) or "area" not in values or values["area"] is None:
return v
if v == PerComponent.Per_Mask_component and values["area"] != AreaType.ROI:
raise ValueError("Per_Mask_component can be used only with ROI area")
return v

def get_channel_num(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) -> Set[Channel]:
"""
Get set with number of channels needed for calculate this measurement
Expand Down Expand Up @@ -171,6 +180,8 @@ def pretty_print(self, measurement_dict: Dict[str, "MeasurementMethodBase"]) ->
if self.per_component is not None:
if self.per_component == PerComponent.Yes:
resp += " per component "
elif self.per_component == PerComponent.Per_Mask_component:
resp += " per mask component "
elif self.per_component == PerComponent.Mean:
resp += " mean component "
resp += self._parameters_string(measurement_dict)
Expand All @@ -196,11 +207,14 @@ def get_unit(self, ndim: int) -> Symbol:

def is_per_component(self) -> bool:
"""If measurement return list of result or single value."""
return self.per_component == PerComponent.Yes
return self.per_component in {PerComponent.Yes, PerComponent.Per_Mask_component}

def need_mask(self) -> bool:
"""If this measurement need mast for proper calculation."""
return self.area in [AreaType.Mask, AreaType.Mask_without_ROI]
return (
self.area in [AreaType.Mask, AreaType.Mask_without_ROI]
or self.per_component is PerComponent.Per_Mask_component
)


def replace(self, **kwargs) -> Leaf:
Expand Down
Loading