Skip to content

Commit 501574e

Browse files
authored
fix: Fix checking if channel requested by MeasurementProfile exists (#1165)
fix PARTSEG-VA <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Introduced a method to check the existence of channels, enhancing validation during measurements. - Updated handling of channel identifiers to accept both `Channel` objects and strings for improved flexibility. - Improved dialog functionality by allowing it to have a parent widget for better user experience. - Enhanced the measurement profile structure for clarity and functionality, focusing on ROI measurements. - **Bug Fixes** - Enhanced error messaging for channel validity to align with user input, improving usability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent d87bd21 commit 501574e

17 files changed

Lines changed: 467 additions & 192 deletions

File tree

package/PartSeg/_roi_analysis/advanced_window.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,7 @@ def import_measurement_profiles(self):
805805
if err:
806806
QMessageBox.warning(self, "Import error", "error during importing, part of data were filtered.")
807807
measurement_dict = self.settings.measurement_profiles
808-
imp = ImportDialog(stat, measurement_dict, StringViewer, MeasurementProfile)
808+
imp = ImportDialog(stat, measurement_dict, StringViewer, MeasurementProfile, parent=self)
809809
if not imp.exec_():
810810
return
811811
for original_name, final_name in imp.get_import_list():

package/PartSeg/_roi_analysis/measurement_widget.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,11 +367,11 @@ def append_measurement_result(self):
367367
units = self.units_choose.currentEnum()
368368

369369
for num in compute_class.get_channels_num():
370-
if num >= self.settings.image.channels:
370+
if not self.settings.image.has_channel(num):
371371
QMessageBox.warning(
372372
self,
373373
"Measurement error",
374-
f"Cannot calculate this measurement because image do not have channel {num+1}",
374+
f"Cannot calculate this measurement because image do not have channel {num}",
375375
)
376376
return
377377

package/PartSeg/common_gui/napari_image_view.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ def mask_opacity(self) -> float:
413413
def mask_color(self) -> ColorInfo:
414414
"""Get mask marking color"""
415415
color = Color(np.divide(self.settings.get_from_profile("mask_presentation_color", [255, 255, 255]), 255))
416-
return {0: (0, 0, 0, 0), 1: color.rgba}
416+
return {0: (0, 0, 0, 0), 1: color.rgba, None: (0, 0, 0, 0)}
417417

418418
def get_image(self, image: Optional[Image]) -> Image:
419419
if image is not None:

package/PartSeg/plugins/napari_widgets/measurement_widget.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from PartSeg.plugins.napari_widgets._settings import get_settings
1515
from PartSeg.plugins.napari_widgets.utils import NapariFormDialog, generate_image
1616
from PartSegCore.roi_info import ROIInfo
17+
from PartSegImage import Channel
1718

1819
if TYPE_CHECKING:
1920
from PartSegCore.analysis.measurement_calculation import MeasurementProfile, MeasurementResult
@@ -58,11 +59,13 @@ def append_measurement_result(self):
5859
if self.channels_chose.value is None:
5960
return
6061
for name in compute_class.get_channels_num():
61-
if name not in self.napari_viewer.layers:
62+
if name.value not in self.napari_viewer.layers:
6263
show_info(f"Cannot calculate this measurement because image do not have layer {name}")
6364
return
6465
units = self.units_choose.currentEnum()
65-
image = generate_image(self.napari_viewer, self.channels_chose.value.name, *compute_class.get_channels_num())
66+
image = generate_image(
67+
self.napari_viewer, Channel(self.channels_chose.value.name), *compute_class.get_channels_num()
68+
)
6669
if self.mask_chose.value is not None:
6770
image.set_mask(self.mask_chose.value.data)
6871
roi_info = ROIInfo(self.roi_chose.value.data).fit_to_image(image)

package/PartSeg/plugins/napari_widgets/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ def __init__(self, *args, **kwargs):
6161
def generate_image(viewer: Viewer, *layer_names):
6262
axis_order = Image.axis_order.replace("C", "")
6363
image_list = []
64+
if isinstance(layer_names[0], str):
65+
layer_names = [Channel(el) for el in layer_names]
6466
for name in dict.fromkeys(layer_names):
65-
image_layer = viewer.layers[name]
67+
image_layer = viewer.layers[name.value]
6668
data_scale = image_layer.scale[-3:] / UNIT_SCALE[Units.nm.value]
6769
image_list.append(
6870
Image(

package/PartSegCore/analysis/measurement_base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,13 @@ def calculate_property(
395395

396396
@classmethod
397397
def get_starting_leaf(cls) -> Leaf:
398-
"""This leaf is put on default list"""
398+
"""This leaf is put on a default list"""
399+
if (
400+
hasattr(cls, "__argument_class__")
401+
and cls.__argument_class__ is not None
402+
and cls.__argument_class__ is not BaseModel
403+
):
404+
return Leaf(name=cls._display_name(), parameters=cls.__argument_class__())
399405
return Leaf(name=cls._display_name())
400406

401407
@classmethod

package/PartSegImage/image.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,11 @@ def get_data_by_axis(self, **kwargs) -> np.ndarray:
526526
axis_pos = self.get_array_axis_positions()
527527
if "c" in kwargs:
528528
kwargs["C"] = kwargs.pop("c")
529-
if "C" in kwargs and isinstance(kwargs["C"], str):
530-
kwargs["C"] = self.channel_names.index(kwargs["C"])
529+
if "C" in kwargs:
530+
if isinstance(kwargs["C"], Channel):
531+
kwargs["C"] = kwargs["C"].value
532+
if isinstance(kwargs["C"], str):
533+
kwargs["C"] = self.channel_names.index(kwargs["C"])
531534

532535
channel = kwargs.pop("C", slice(None) if "C" in self.axis_order else 0)
533536
if isinstance(channel, Channel):
@@ -571,6 +574,14 @@ def get_channel(self, num: int | str | Channel) -> np.ndarray:
571574
"""
572575
return self.get_data_by_axis(c=num)
573576

577+
def has_channel(self, num: int | str | Channel) -> bool:
578+
if isinstance(num, Channel):
579+
num = num.value
580+
581+
if isinstance(num, str):
582+
return num in self.channel_names
583+
return 0 <= num < self.channels
584+
574585
def get_layer(self, time: int, stack: int) -> np.ndarray:
575586
"""
576587
return single layer contains data for all channel

package/tests/conftest.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from PartSegCore.algorithm_describe_base import ROIExtractionProfile
1313
from PartSegCore.analysis import ProjectTuple, SegmentationPipeline, SegmentationPipelineElement
1414
from PartSegCore.analysis.measurement_base import AreaType, MeasurementEntry, PerComponent
15-
from PartSegCore.analysis.measurement_calculation import ComponentsNumber, MeasurementProfile, Volume
15+
from PartSegCore.analysis.measurement_calculation import (
16+
ColocalizationMeasurement,
17+
ComponentsNumber,
18+
MeasurementProfile,
19+
Volume,
20+
)
1621
from PartSegCore.image_operations import RadiusType
1722
from PartSegCore.mask.io_functions import MaskProjectTuple
1823
from PartSegCore.mask_create import MaskProperty
@@ -69,15 +74,15 @@ def image2d(tmp_path):
6974

7075
@pytest.fixture()
7176
def stack_image():
72-
data = np.zeros([20, 40, 40], dtype=np.uint8)
77+
data = np.zeros([20, 40, 40, 2], dtype=np.uint8)
7378
for x, y in itertools.product([0, 20], repeat=2):
7479
data[1:-1, x + 2 : x + 18, y + 2 : y + 18] = 100
7580
for x, y in itertools.product([0, 20], repeat=2):
7681
data[3:-3, x + 4 : x + 16, y + 4 : y + 16] = 120
7782
for x, y in itertools.product([0, 20], repeat=2):
7883
data[5:-5, x + 6 : x + 14, y + 6 : y + 14] = 140
7984

80-
return MaskProjectTuple("test_path", Image(data, (2, 1, 1), axes_order="ZYX", file_path="test_path"))
85+
return MaskProjectTuple("test_path", Image(data, (2, 1, 1), axes_order="ZYXC", file_path="test_path"))
8186

8287

8388
@pytest.fixture()
@@ -201,8 +206,18 @@ def measurement_profiles():
201206
calculation_tree=Volume.get_starting_leaf().replace_(area=AreaType.Mask, per_component=PerComponent.No),
202207
),
203208
]
204-
return MeasurementProfile(name="statistic1", chosen_fields=statistics), MeasurementProfile(
205-
name="statistic2", chosen_fields=statistics + statistics2
209+
statistics3 = [
210+
MeasurementEntry(
211+
name="Colocalisation",
212+
calculation_tree=ColocalizationMeasurement.get_starting_leaf().replace_(
213+
per_component=PerComponent.No, area=AreaType.ROI
214+
),
215+
),
216+
]
217+
return (
218+
MeasurementProfile(name="statistic1", chosen_fields=statistics),
219+
MeasurementProfile(name="statistic2", chosen_fields=statistics + statistics2),
220+
MeasurementProfile(name="statistic3", chosen_fields=statistics + statistics2 + statistics3),
206221
)
207222

208223

package/tests/test_PartSeg/conftest.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@
1212

1313

1414
@pytest.fixture()
15-
def base_settings(image, tmp_path, measurement_profiles, qapp):
15+
def base_settings(image, tmp_path, measurement_profiles):
1616
settings = BaseSettings(tmp_path)
1717
settings.image = image
1818
return settings
1919

2020

2121
@pytest.fixture()
22-
def part_settings(image, tmp_path, measurement_profiles, qapp):
22+
def part_settings(image, tmp_path, measurement_profiles):
2323
settings = PartSettings(tmp_path)
2424
settings.image = image
2525
for el in measurement_profiles:
@@ -28,7 +28,7 @@ def part_settings(image, tmp_path, measurement_profiles, qapp):
2828

2929

3030
@pytest.fixture()
31-
def stack_settings(tmp_path, image, qapp):
31+
def stack_settings(tmp_path, image):
3232
settings = StackSettings(tmp_path)
3333
settings.image = image
3434
chose = ChosenComponents()
@@ -38,7 +38,7 @@ def stack_settings(tmp_path, image, qapp):
3838

3939

4040
@pytest.fixture()
41-
def part_settings_with_project(image, analysis_segmentation2, tmp_path, qapp):
41+
def part_settings_with_project(image, analysis_segmentation2, tmp_path):
4242
settings = PartSettings(tmp_path)
4343
settings.image = image
4444
settings.set_project_info(analysis_segmentation2)

package/tests/test_PartSeg/roi_analysis/test_advanced_window.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,10 @@ def test_base_steep(self, qtbot, part_settings):
148148
widget.profile_name.setText("test")
149149
assert widget.save_butt.isEnabled()
150150

151-
assert len(part_settings.measurement_profiles) == 2
151+
assert len(part_settings.measurement_profiles) == 3
152152
with qtbot.waitSignal(widget.save_butt.clicked):
153153
widget.save_butt.click()
154-
assert len(part_settings.measurement_profiles) == 3
154+
assert len(part_settings.measurement_profiles) == 4
155155

156156
with qtbot.waitSignal(widget.profile_name.textChanged):
157157
widget.profile_name.setText("")

0 commit comments

Comments
 (0)