senoquant 1.0.0b2__py3-none-any.whl → 1.0.0b4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- senoquant/__init__.py +6 -2
- senoquant/_reader.py +1 -1
- senoquant/_widget.py +9 -1
- senoquant/reader/core.py +201 -18
- senoquant/tabs/__init__.py +2 -0
- senoquant/tabs/batch/backend.py +76 -27
- senoquant/tabs/batch/frontend.py +127 -25
- senoquant/tabs/quantification/features/marker/dialog.py +26 -6
- senoquant/tabs/quantification/features/marker/export.py +97 -24
- senoquant/tabs/quantification/features/marker/rows.py +2 -2
- senoquant/tabs/quantification/features/spots/dialog.py +41 -11
- senoquant/tabs/quantification/features/spots/export.py +163 -10
- senoquant/tabs/quantification/frontend.py +2 -2
- senoquant/tabs/segmentation/frontend.py +46 -9
- senoquant/tabs/segmentation/models/cpsam/model.py +1 -1
- senoquant/tabs/segmentation/models/default_2d/model.py +22 -77
- senoquant/tabs/segmentation/models/default_3d/model.py +8 -74
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +13 -13
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/stardist_libs.py +171 -0
- senoquant/tabs/spots/frontend.py +96 -5
- senoquant/tabs/spots/models/rmp/details.json +3 -9
- senoquant/tabs/spots/models/rmp/model.py +341 -266
- senoquant/tabs/spots/models/ufish/details.json +32 -0
- senoquant/tabs/spots/models/ufish/model.py +327 -0
- senoquant/tabs/spots/ufish_utils/__init__.py +13 -0
- senoquant/tabs/spots/ufish_utils/core.py +387 -0
- senoquant/tabs/visualization/__init__.py +1 -0
- senoquant/tabs/visualization/backend.py +306 -0
- senoquant/tabs/visualization/frontend.py +1113 -0
- senoquant/tabs/visualization/plots/__init__.py +80 -0
- senoquant/tabs/visualization/plots/base.py +152 -0
- senoquant/tabs/visualization/plots/double_expression.py +187 -0
- senoquant/tabs/visualization/plots/spatialplot.py +156 -0
- senoquant/tabs/visualization/plots/umap.py +140 -0
- senoquant/utils.py +1 -1
- senoquant-1.0.0b4.dist-info/METADATA +162 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/RECORD +53 -30
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/top_level.txt +1 -0
- ufish/__init__.py +1 -0
- ufish/api.py +778 -0
- ufish/model/__init__.py +0 -0
- ufish/model/loss.py +62 -0
- ufish/model/network/__init__.py +0 -0
- ufish/model/network/spot_learn.py +50 -0
- ufish/model/network/ufish_net.py +204 -0
- ufish/model/train.py +175 -0
- ufish/utils/__init__.py +0 -0
- ufish/utils/img.py +418 -0
- ufish/utils/log.py +8 -0
- ufish/utils/spot_calling.py +115 -0
- senoquant/tabs/spots/models/udwt/details.json +0 -103
- senoquant/tabs/spots/models/udwt/model.py +0 -482
- senoquant-1.0.0b2.dist-info/METADATA +0 -193
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/WHEEL +0 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/entry_points.txt +0 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/licenses/LICENSE +0 -0
senoquant/tabs/batch/frontend.py
CHANGED
|
@@ -94,7 +94,7 @@ class BatchTab(QWidget):
|
|
|
94
94
|
backend : BatchBackend or None, optional
|
|
95
95
|
Backend instance used to execute batch runs.
|
|
96
96
|
napari_viewer : object or None, optional
|
|
97
|
-
|
|
97
|
+
napari viewer instance for populating layer choices.
|
|
98
98
|
"""
|
|
99
99
|
super().__init__()
|
|
100
100
|
self._viewer = napari_viewer
|
|
@@ -123,6 +123,11 @@ class BatchTab(QWidget):
|
|
|
123
123
|
self._spot_min_size_spin: QSpinBox | None = None
|
|
124
124
|
self._spot_max_size_spin: QSpinBox | None = None
|
|
125
125
|
self._add_spot_button: QPushButton | None = None
|
|
126
|
+
self._cyto_nuclear_optional = False
|
|
127
|
+
self._cyto_nuclear_from_labels = False
|
|
128
|
+
self._cyto_requires_cyto_channel = True
|
|
129
|
+
self._cyto_supports_nuclear_input = False
|
|
130
|
+
self._refreshing_channel_choices = False
|
|
126
131
|
self._config_viewer = BatchViewer()
|
|
127
132
|
|
|
128
133
|
layout = QVBoxLayout()
|
|
@@ -281,6 +286,9 @@ class BatchTab(QWidget):
|
|
|
281
286
|
self._nuclear_model_combo.currentTextChanged.connect(
|
|
282
287
|
lambda _text: self._update_nuclear_settings()
|
|
283
288
|
)
|
|
289
|
+
self._nuclear_channel_combo.currentTextChanged.connect(
|
|
290
|
+
self._on_nuclear_channel_changed
|
|
291
|
+
)
|
|
284
292
|
|
|
285
293
|
cyto_layout = QFormLayout()
|
|
286
294
|
cyto_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
@@ -293,7 +301,6 @@ class BatchTab(QWidget):
|
|
|
293
301
|
self._cyto_channel_combo = QComboBox()
|
|
294
302
|
self._cyto_nuclear_combo = QComboBox()
|
|
295
303
|
self._cyto_nuclear_label = QLabel("Nuclear channel")
|
|
296
|
-
self._cyto_nuclear_optional = False
|
|
297
304
|
cyto_layout.addRow(self._cyto_enabled)
|
|
298
305
|
cyto_layout.addRow("Cytoplasmic model", self._cyto_model_combo)
|
|
299
306
|
cyto_layout.addRow("Cytoplasmic channel", self._cyto_channel_combo)
|
|
@@ -593,6 +600,9 @@ class BatchTab(QWidget):
|
|
|
593
600
|
|
|
594
601
|
def _refresh_channel_choices(self) -> None:
|
|
595
602
|
"""Refresh combo boxes that depend on channel mapping."""
|
|
603
|
+
if self._refreshing_channel_choices:
|
|
604
|
+
return
|
|
605
|
+
self._refreshing_channel_choices = True
|
|
596
606
|
names = [config.name for config in self._channel_configs]
|
|
597
607
|
|
|
598
608
|
def populate_combo(
|
|
@@ -600,13 +610,16 @@ class BatchTab(QWidget):
|
|
|
600
610
|
*,
|
|
601
611
|
include_none: bool = False,
|
|
602
612
|
none_label: str = "(none)",
|
|
613
|
+
explicit_items: list[str] | None = None,
|
|
603
614
|
) -> None:
|
|
604
615
|
current = combo.currentText()
|
|
605
616
|
combo.clear()
|
|
606
617
|
items: list[str] = []
|
|
607
618
|
if include_none:
|
|
608
619
|
items.append(none_label)
|
|
609
|
-
if
|
|
620
|
+
if explicit_items is not None:
|
|
621
|
+
items.extend(explicit_items)
|
|
622
|
+
elif names:
|
|
610
623
|
items.extend(names)
|
|
611
624
|
elif not include_none:
|
|
612
625
|
items.append("0")
|
|
@@ -618,15 +631,40 @@ class BatchTab(QWidget):
|
|
|
618
631
|
if index != -1:
|
|
619
632
|
combo.setCurrentIndex(index)
|
|
620
633
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
self.
|
|
628
|
-
|
|
629
|
-
|
|
634
|
+
try:
|
|
635
|
+
if getattr(self, "_nuclear_channel_combo", None) is not None:
|
|
636
|
+
populate_combo(self._nuclear_channel_combo)
|
|
637
|
+
if getattr(self, "_cyto_channel_combo", None) is not None:
|
|
638
|
+
populate_combo(self._cyto_channel_combo)
|
|
639
|
+
if getattr(self, "_cyto_nuclear_combo", None) is not None:
|
|
640
|
+
if self._cyto_nuclear_from_labels:
|
|
641
|
+
label_options = self._cyto_nuclear_label_options()
|
|
642
|
+
populate_combo(
|
|
643
|
+
self._cyto_nuclear_combo,
|
|
644
|
+
explicit_items=label_options,
|
|
645
|
+
none_label="(no nuclear labels)",
|
|
646
|
+
)
|
|
647
|
+
else:
|
|
648
|
+
populate_combo(
|
|
649
|
+
self._cyto_nuclear_combo,
|
|
650
|
+
include_none=self._cyto_nuclear_optional,
|
|
651
|
+
)
|
|
652
|
+
finally:
|
|
653
|
+
self._refreshing_channel_choices = False
|
|
654
|
+
|
|
655
|
+
def _cyto_nuclear_label_options(self) -> list[str]:
|
|
656
|
+
"""Return available nuclear label names for nuclear-only cyto models."""
|
|
657
|
+
if getattr(self, "_nuclear_enabled", None) is None:
|
|
658
|
+
return []
|
|
659
|
+
if not self._nuclear_enabled.isChecked():
|
|
660
|
+
return []
|
|
661
|
+
nuclear_model = self._nuclear_model_combo.currentText().strip()
|
|
662
|
+
nuclear_channel = self._nuclear_channel_combo.currentText().strip()
|
|
663
|
+
if not nuclear_model or nuclear_model.startswith("("):
|
|
664
|
+
return []
|
|
665
|
+
if not nuclear_channel or nuclear_channel.startswith("("):
|
|
666
|
+
return []
|
|
667
|
+
return [f"{nuclear_channel}_{nuclear_model}_nuc_labels"]
|
|
630
668
|
|
|
631
669
|
def _refresh_config_viewer(self) -> None:
|
|
632
670
|
"""Refresh the quantification preview viewer shim."""
|
|
@@ -639,18 +677,22 @@ class BatchTab(QWidget):
|
|
|
639
677
|
nuclear_channel = self._nuclear_channel_combo.currentText()
|
|
640
678
|
if nuclear_model and nuclear_channel and not nuclear_model.startswith("("):
|
|
641
679
|
label_name = f"{nuclear_channel}_{nuclear_model}_nuc_labels"
|
|
642
|
-
layers.append(Labels(None, label_name))
|
|
680
|
+
layers.append(Labels(None, label_name, metadata={"task": "nuclear"}))
|
|
643
681
|
if getattr(self, "_cyto_enabled", None) is not None and self._cyto_enabled.isChecked():
|
|
644
682
|
cyto_model = self._cyto_model_combo.currentText()
|
|
645
683
|
cyto_channel = self._cyto_channel_combo.currentText()
|
|
646
684
|
if cyto_model and cyto_channel and not cyto_model.startswith("("):
|
|
647
685
|
label_name = f"{cyto_channel}_{cyto_model}_cyto_labels"
|
|
648
|
-
layers.append(
|
|
686
|
+
layers.append(
|
|
687
|
+
Labels(None, label_name, metadata={"task": "cytoplasmic"})
|
|
688
|
+
)
|
|
649
689
|
if getattr(self, "_spots_enabled", None) is not None and self._spots_enabled.isChecked():
|
|
650
690
|
spot_detector = self._spot_detector_combo.currentText()
|
|
651
691
|
if spot_detector and not spot_detector.startswith("("):
|
|
652
692
|
for label_name in _spot_label_names(self._spot_channel_rows, spot_detector):
|
|
653
|
-
layers.append(
|
|
693
|
+
layers.append(
|
|
694
|
+
Labels(None, label_name, metadata={"task": "spots"})
|
|
695
|
+
)
|
|
654
696
|
self._config_viewer.set_layers(layers)
|
|
655
697
|
|
|
656
698
|
def _update_nuclear_settings(self) -> None:
|
|
@@ -659,6 +701,8 @@ class BatchTab(QWidget):
|
|
|
659
701
|
self._nuclear_settings_widgets.clear()
|
|
660
702
|
self._nuclear_settings_meta.clear()
|
|
661
703
|
if not model_name or model_name.startswith("("):
|
|
704
|
+
self._refresh_channel_choices()
|
|
705
|
+
self._refresh_config_viewer()
|
|
662
706
|
return
|
|
663
707
|
model = self._segmentation_backend.get_model(model_name)
|
|
664
708
|
settings = model.list_settings()
|
|
@@ -667,17 +711,23 @@ class BatchTab(QWidget):
|
|
|
667
711
|
item.get("key", item.get("label", "")): item for item in settings
|
|
668
712
|
}
|
|
669
713
|
self._nuclear_settings_values = _defaults_from_settings(settings)
|
|
714
|
+
self._refresh_channel_choices()
|
|
715
|
+
self._refresh_config_viewer()
|
|
670
716
|
|
|
671
717
|
def _update_cyto_settings(self) -> None:
|
|
672
718
|
"""Refresh cytoplasmic model settings from the selected model."""
|
|
673
719
|
model_name = self._cyto_model_combo.currentText()
|
|
674
720
|
self._cyto_settings_widgets.clear()
|
|
675
721
|
self._cyto_settings_meta.clear()
|
|
722
|
+
self._cyto_nuclear_optional = False
|
|
723
|
+
self._cyto_nuclear_from_labels = False
|
|
724
|
+
self._cyto_requires_cyto_channel = True
|
|
725
|
+
self._cyto_supports_nuclear_input = False
|
|
676
726
|
if not model_name or model_name.startswith("("):
|
|
677
|
-
self._cyto_nuclear_combo.setEnabled(False)
|
|
678
727
|
if hasattr(self, "_cyto_nuclear_label"):
|
|
679
728
|
self._cyto_nuclear_label.setText("Nuclear channel")
|
|
680
|
-
self.
|
|
729
|
+
self._refresh_channel_choices()
|
|
730
|
+
self._update_processing_state()
|
|
681
731
|
return
|
|
682
732
|
model = self._segmentation_backend.get_model(model_name)
|
|
683
733
|
settings = model.list_settings()
|
|
@@ -687,20 +737,30 @@ class BatchTab(QWidget):
|
|
|
687
737
|
}
|
|
688
738
|
self._cyto_settings_values = _defaults_from_settings(settings)
|
|
689
739
|
modes = model.cytoplasmic_input_modes()
|
|
690
|
-
|
|
691
|
-
|
|
740
|
+
if modes == ["nuclear"]:
|
|
741
|
+
if hasattr(self, "_cyto_nuclear_label"):
|
|
742
|
+
self._cyto_nuclear_label.setText("Nuclear labels layer")
|
|
743
|
+
self._cyto_nuclear_from_labels = True
|
|
744
|
+
self._cyto_requires_cyto_channel = False
|
|
745
|
+
self._cyto_supports_nuclear_input = True
|
|
746
|
+
elif "nuclear+cytoplasmic" in modes:
|
|
692
747
|
optional = model.cytoplasmic_nuclear_optional()
|
|
693
748
|
suffix = "optional" if optional else "required"
|
|
694
749
|
if hasattr(self, "_cyto_nuclear_label"):
|
|
695
750
|
self._cyto_nuclear_label.setText(f"Nuclear channel ({suffix})")
|
|
696
|
-
self._cyto_nuclear_combo.setEnabled(True)
|
|
697
751
|
self._cyto_nuclear_optional = optional
|
|
752
|
+
self._cyto_supports_nuclear_input = True
|
|
698
753
|
else:
|
|
699
754
|
if hasattr(self, "_cyto_nuclear_label"):
|
|
700
755
|
self._cyto_nuclear_label.setText("Nuclear channel")
|
|
701
|
-
self._cyto_nuclear_combo.setEnabled(False)
|
|
702
|
-
self._cyto_nuclear_optional = False
|
|
703
756
|
self._refresh_channel_choices()
|
|
757
|
+
self._update_processing_state()
|
|
758
|
+
|
|
759
|
+
def _on_nuclear_channel_changed(self, _text: str) -> None:
|
|
760
|
+
"""Refresh dependent combos when the nuclear channel changes."""
|
|
761
|
+
if self._cyto_nuclear_from_labels:
|
|
762
|
+
self._refresh_channel_choices()
|
|
763
|
+
self._refresh_config_viewer()
|
|
704
764
|
|
|
705
765
|
def _update_spot_settings(self) -> None:
|
|
706
766
|
"""Refresh spot detector settings from the selected detector."""
|
|
@@ -828,12 +888,18 @@ class BatchTab(QWidget):
|
|
|
828
888
|
nuclear_enabled = self._nuclear_enabled.isChecked()
|
|
829
889
|
cyto_enabled = self._cyto_enabled.isChecked()
|
|
830
890
|
spot_enabled = self._spots_enabled.isChecked()
|
|
891
|
+
if self._cyto_nuclear_from_labels:
|
|
892
|
+
self._refresh_channel_choices()
|
|
831
893
|
self._nuclear_model_combo.setEnabled(nuclear_enabled)
|
|
832
894
|
self._nuclear_channel_combo.setEnabled(nuclear_enabled)
|
|
833
895
|
self._nuclear_settings_button.setEnabled(nuclear_enabled)
|
|
834
896
|
self._cyto_model_combo.setEnabled(cyto_enabled)
|
|
835
|
-
self._cyto_channel_combo.setEnabled(
|
|
836
|
-
|
|
897
|
+
self._cyto_channel_combo.setEnabled(
|
|
898
|
+
cyto_enabled and self._cyto_requires_cyto_channel
|
|
899
|
+
)
|
|
900
|
+
self._cyto_nuclear_combo.setEnabled(
|
|
901
|
+
cyto_enabled and self._cyto_supports_nuclear_input
|
|
902
|
+
)
|
|
837
903
|
self._cyto_settings_button.setEnabled(cyto_enabled)
|
|
838
904
|
self._spot_detector_combo.setEnabled(spot_enabled)
|
|
839
905
|
self._spot_settings_button.setEnabled(spot_enabled)
|
|
@@ -882,10 +948,33 @@ class BatchTab(QWidget):
|
|
|
882
948
|
spot_detector = None
|
|
883
949
|
|
|
884
950
|
cyto_model = None
|
|
951
|
+
cyto_model_obj = None
|
|
885
952
|
if self._cyto_enabled.isChecked() and self._cyto_model_combo.isEnabled():
|
|
886
953
|
cyto_model = self._cyto_model_combo.currentText().strip()
|
|
887
954
|
if cyto_model.startswith("("):
|
|
888
955
|
cyto_model = None
|
|
956
|
+
elif cyto_model:
|
|
957
|
+
cyto_model_obj = self._segmentation_backend.get_model(cyto_model)
|
|
958
|
+
|
|
959
|
+
if cyto_model_obj is not None and self._cyto_requires_nuclear(cyto_model_obj):
|
|
960
|
+
cyto_nuclear_choice = self._cyto_nuclear_combo.currentText().strip()
|
|
961
|
+
if (
|
|
962
|
+
not cyto_nuclear_choice
|
|
963
|
+
or cyto_nuclear_choice == "(none)"
|
|
964
|
+
or cyto_nuclear_choice == "(no nuclear labels)"
|
|
965
|
+
):
|
|
966
|
+
self._notify("Select the required cytoplasmic nuclear input.")
|
|
967
|
+
return
|
|
968
|
+
|
|
969
|
+
if (
|
|
970
|
+
cyto_model_obj.cytoplasmic_input_modes() == ["nuclear"]
|
|
971
|
+
and not nuclear_model
|
|
972
|
+
):
|
|
973
|
+
self._notify(
|
|
974
|
+
"Selected cytoplasmic model requires nuclear labels. "
|
|
975
|
+
"Enable nuclear segmentation first."
|
|
976
|
+
)
|
|
977
|
+
return
|
|
889
978
|
|
|
890
979
|
quant_features = (
|
|
891
980
|
list(self._quant_tab._feature_configs)
|
|
@@ -1004,7 +1093,8 @@ class BatchTab(QWidget):
|
|
|
1004
1093
|
channel=self._cyto_channel_combo.currentText(),
|
|
1005
1094
|
nuclear_channel=(
|
|
1006
1095
|
""
|
|
1007
|
-
if self._cyto_nuclear_combo.currentText().strip()
|
|
1096
|
+
if self._cyto_nuclear_combo.currentText().strip()
|
|
1097
|
+
in {"(none)", "(no nuclear labels)"}
|
|
1008
1098
|
else self._cyto_nuclear_combo.currentText()
|
|
1009
1099
|
),
|
|
1010
1100
|
settings=cyto_settings,
|
|
@@ -1053,6 +1143,8 @@ class BatchTab(QWidget):
|
|
|
1053
1143
|
if not job.cytoplasmic.nuclear_channel:
|
|
1054
1144
|
if self._cyto_nuclear_combo.findText("(none)") != -1:
|
|
1055
1145
|
self._set_combo_value(self._cyto_nuclear_combo, "(none)")
|
|
1146
|
+
elif self._cyto_nuclear_combo.findText("(no nuclear labels)") != -1:
|
|
1147
|
+
self._set_combo_value(self._cyto_nuclear_combo, "(no nuclear labels)")
|
|
1056
1148
|
else:
|
|
1057
1149
|
self._set_combo_value(
|
|
1058
1150
|
self._cyto_nuclear_combo, job.cytoplasmic.nuclear_channel
|
|
@@ -1078,6 +1170,16 @@ class BatchTab(QWidget):
|
|
|
1078
1170
|
self._refresh_spot_channel_choices()
|
|
1079
1171
|
self._refresh_config_viewer()
|
|
1080
1172
|
|
|
1173
|
+
@staticmethod
|
|
1174
|
+
def _cyto_requires_nuclear(model) -> bool:
|
|
1175
|
+
"""Return whether the selected cytoplasmic model requires nuclear input."""
|
|
1176
|
+
modes = model.cytoplasmic_input_modes()
|
|
1177
|
+
if modes == ["nuclear"]:
|
|
1178
|
+
return True
|
|
1179
|
+
if "nuclear+cytoplasmic" not in modes:
|
|
1180
|
+
return False
|
|
1181
|
+
return not model.cytoplasmic_nuclear_optional()
|
|
1182
|
+
|
|
1081
1183
|
def _save_profile(self) -> None:
|
|
1082
1184
|
"""Save the current configuration to a JSON profile."""
|
|
1083
1185
|
path, _ = QFileDialog.getSaveFileName(
|
|
@@ -193,28 +193,48 @@ class MarkerChannelsDialog(QDialog):
|
|
|
193
193
|
return
|
|
194
194
|
for layer in viewer.layers:
|
|
195
195
|
if layer.__class__.__name__ == "Labels":
|
|
196
|
-
layer_name = layer.name
|
|
197
196
|
# Only show cellular labels (nuclear/cytoplasmic), exclude spot labels
|
|
198
|
-
if self._is_cellular_label(
|
|
199
|
-
combo.addItem(
|
|
197
|
+
if self._is_cellular_label(layer):
|
|
198
|
+
combo.addItem(layer.name)
|
|
200
199
|
if current:
|
|
201
200
|
index = combo.findText(current)
|
|
202
201
|
if index != -1:
|
|
203
202
|
combo.setCurrentIndex(index)
|
|
204
203
|
|
|
205
|
-
def
|
|
204
|
+
def _layer_task(self, layer: object) -> str | None:
|
|
205
|
+
"""Return normalized segmentation task from layer metadata."""
|
|
206
|
+
metadata = getattr(layer, "metadata", None)
|
|
207
|
+
if not isinstance(metadata, dict):
|
|
208
|
+
return None
|
|
209
|
+
task = metadata.get("task")
|
|
210
|
+
if not isinstance(task, str):
|
|
211
|
+
return None
|
|
212
|
+
normalized = task.strip().lower()
|
|
213
|
+
return normalized or None
|
|
214
|
+
|
|
215
|
+
def _is_cellular_label(self, layer: object | str) -> bool:
|
|
206
216
|
"""Check if a label layer is a cellular segmentation.
|
|
207
217
|
|
|
208
218
|
Parameters
|
|
209
219
|
----------
|
|
210
|
-
|
|
211
|
-
|
|
220
|
+
layer : object or str
|
|
221
|
+
Labels layer object or labels layer name.
|
|
212
222
|
|
|
213
223
|
Returns
|
|
214
224
|
-------
|
|
215
225
|
bool
|
|
216
226
|
True if the layer is a cellular label (nuclear or cytoplasmic).
|
|
217
227
|
"""
|
|
228
|
+
if isinstance(layer, str):
|
|
229
|
+
layer_name = layer
|
|
230
|
+
task = None
|
|
231
|
+
else:
|
|
232
|
+
layer_name = str(getattr(layer, "name", ""))
|
|
233
|
+
task = self._layer_task(layer)
|
|
234
|
+
if task in {"nuclear", "cytoplasmic"}:
|
|
235
|
+
return True
|
|
236
|
+
if task is not None:
|
|
237
|
+
return False
|
|
218
238
|
return layer_name.endswith("_nuc_labels") or layer_name.endswith("_cyto_labels")
|
|
219
239
|
|
|
220
240
|
def _refresh_image_combo(self, combo: QComboBox) -> None:
|
|
@@ -41,7 +41,7 @@ def export_marker(
|
|
|
41
41
|
temp_dir : Path
|
|
42
42
|
Temporary directory where outputs should be written.
|
|
43
43
|
viewer : object, optional
|
|
44
|
-
|
|
44
|
+
napari viewer instance used to resolve layers by name.
|
|
45
45
|
export_format : str, optional
|
|
46
46
|
File format for exports (``"csv"`` or ``"xlsx"``).
|
|
47
47
|
enable_thresholds : bool, optional
|
|
@@ -76,6 +76,23 @@ def export_marker(
|
|
|
76
76
|
if metadata_path is not None:
|
|
77
77
|
outputs.append(metadata_path)
|
|
78
78
|
|
|
79
|
+
all_segmentations: dict[str, tuple[np.ndarray, np.ndarray]] = {}
|
|
80
|
+
for segmentation in data.segmentations:
|
|
81
|
+
seg_name = segmentation.label.strip()
|
|
82
|
+
if not seg_name:
|
|
83
|
+
continue
|
|
84
|
+
seg_layer = _find_layer(viewer, seg_name, "Labels")
|
|
85
|
+
if seg_layer is None:
|
|
86
|
+
continue
|
|
87
|
+
seg_labels = layer_data_asarray(seg_layer)
|
|
88
|
+
if seg_labels.size == 0:
|
|
89
|
+
continue
|
|
90
|
+
seg_label_ids, _seg_centroids = _compute_centroids(seg_labels)
|
|
91
|
+
if seg_label_ids.size == 0:
|
|
92
|
+
continue
|
|
93
|
+
all_segmentations[seg_name] = (seg_labels, seg_label_ids)
|
|
94
|
+
cross_map = _build_cross_segmentation_map(all_segmentations)
|
|
95
|
+
|
|
79
96
|
for index, segmentation in enumerate(data.segmentations, start=0):
|
|
80
97
|
label_name = segmentation.label.strip()
|
|
81
98
|
if not label_name:
|
|
@@ -103,7 +120,7 @@ def export_marker(
|
|
|
103
120
|
break
|
|
104
121
|
rows = _initialize_rows(label_ids, centroids, pixel_sizes)
|
|
105
122
|
_add_roi_columns(rows, labels, label_ids, viewer, data.rois, label_name)
|
|
106
|
-
|
|
123
|
+
_morph_columns = add_morphology_columns(
|
|
107
124
|
rows, labels, label_ids, pixel_sizes
|
|
108
125
|
)
|
|
109
126
|
|
|
@@ -115,11 +132,12 @@ def export_marker(
|
|
|
115
132
|
metadata = getattr(first_channel_layer, "metadata", {})
|
|
116
133
|
file_path = metadata.get("path")
|
|
117
134
|
|
|
118
|
-
# Determine segmentation type from
|
|
119
|
-
seg_type =
|
|
120
|
-
|
|
135
|
+
# Determine segmentation type from labels metadata with suffix fallback.
|
|
136
|
+
seg_type = _segmentation_type_from_layer(labels_layer, label_name)
|
|
137
|
+
_ref_columns = _add_reference_columns(
|
|
121
138
|
rows, labels, label_ids, file_path, seg_type
|
|
122
139
|
)
|
|
140
|
+
_add_cross_reference_column(rows, label_name, label_ids, cross_map)
|
|
123
141
|
|
|
124
142
|
header = list(rows[0].keys()) if rows else []
|
|
125
143
|
|
|
@@ -207,7 +225,7 @@ def _find_layer(viewer, name: str, layer_type: str):
|
|
|
207
225
|
Parameters
|
|
208
226
|
----------
|
|
209
227
|
viewer : object
|
|
210
|
-
|
|
228
|
+
napari viewer instance containing layers.
|
|
211
229
|
name : str
|
|
212
230
|
Layer name to locate.
|
|
213
231
|
layer_type : str
|
|
@@ -224,6 +242,40 @@ def _find_layer(viewer, name: str, layer_type: str):
|
|
|
224
242
|
return None
|
|
225
243
|
|
|
226
244
|
|
|
245
|
+
def _segmentation_type_from_layer(layer: object, label_name: str) -> str:
|
|
246
|
+
"""Return segmentation type from layer metadata or legacy suffixes.
|
|
247
|
+
|
|
248
|
+
Parameters
|
|
249
|
+
----------
|
|
250
|
+
layer : object
|
|
251
|
+
Labels layer object.
|
|
252
|
+
label_name : str
|
|
253
|
+
Labels layer name used for fallback suffix parsing.
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
-------
|
|
257
|
+
str
|
|
258
|
+
Segmentation type ("nuclear" or "cytoplasmic").
|
|
259
|
+
|
|
260
|
+
Notes
|
|
261
|
+
-----
|
|
262
|
+
Metadata value ``metadata["task"]`` is authoritative when valid.
|
|
263
|
+
Legacy layer-name suffixes are used as fallback for older layers.
|
|
264
|
+
"""
|
|
265
|
+
metadata = getattr(layer, "metadata", None)
|
|
266
|
+
if isinstance(metadata, dict):
|
|
267
|
+
task = metadata.get("task")
|
|
268
|
+
if isinstance(task, str):
|
|
269
|
+
normalized = task.strip().lower()
|
|
270
|
+
if normalized in {"nuclear", "cytoplasmic"}:
|
|
271
|
+
return normalized
|
|
272
|
+
if label_name.endswith("_cyto_labels"):
|
|
273
|
+
return "cytoplasmic"
|
|
274
|
+
if label_name.endswith("_nuc_labels"):
|
|
275
|
+
return "nuclear"
|
|
276
|
+
return "nuclear"
|
|
277
|
+
|
|
278
|
+
|
|
227
279
|
def _compute_centroids(labels: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
228
280
|
"""Compute centroid coordinates for each non-zero label.
|
|
229
281
|
|
|
@@ -299,7 +351,7 @@ def _pixel_volume(layer, ndim: int) -> float:
|
|
|
299
351
|
Parameters
|
|
300
352
|
----------
|
|
301
353
|
layer : object
|
|
302
|
-
|
|
354
|
+
napari image layer providing metadata.
|
|
303
355
|
ndim : int
|
|
304
356
|
Dimensionality of the image data.
|
|
305
357
|
|
|
@@ -348,7 +400,7 @@ def _pixel_sizes(layer, ndim: int) -> np.ndarray | None:
|
|
|
348
400
|
Parameters
|
|
349
401
|
----------
|
|
350
402
|
layer : object
|
|
351
|
-
|
|
403
|
+
napari image layer providing metadata.
|
|
352
404
|
ndim : int
|
|
353
405
|
Dimensionality of the image data.
|
|
354
406
|
|
|
@@ -429,7 +481,7 @@ def _add_roi_columns(
|
|
|
429
481
|
label_ids : numpy.ndarray
|
|
430
482
|
Label ids corresponding to the output rows.
|
|
431
483
|
viewer : object or None
|
|
432
|
-
|
|
484
|
+
napari viewer used to resolve shapes layers.
|
|
433
485
|
rois : sequence of ROIConfig
|
|
434
486
|
ROI configuration entries to evaluate.
|
|
435
487
|
label_name : str
|
|
@@ -480,7 +532,7 @@ def _shapes_layer_mask(
|
|
|
480
532
|
Parameters
|
|
481
533
|
----------
|
|
482
534
|
layer : object
|
|
483
|
-
|
|
535
|
+
napari shapes layer instance.
|
|
484
536
|
shape : tuple of int
|
|
485
537
|
Target mask shape matching the labels array.
|
|
486
538
|
|
|
@@ -776,25 +828,46 @@ def _build_cross_segmentation_map(
|
|
|
776
828
|
-----
|
|
777
829
|
This function identifies which labels from different segmentations
|
|
778
830
|
overlap spatially, enabling cross-referencing between tables.
|
|
831
|
+
The resulting mapping is bidirectional: if ``A:1`` overlaps ``B:2``,
|
|
832
|
+
both keys receive a reference to the other.
|
|
779
833
|
"""
|
|
780
834
|
cross_map: dict[tuple[str, int], list[tuple[str, int]]] = {}
|
|
835
|
+
valid_ids: dict[str, set[int]] = {}
|
|
836
|
+
|
|
837
|
+
for seg_name, (_labels, label_ids) in all_segmentations.items():
|
|
838
|
+
ids = {int(label_id) for label_id in np.asarray(label_ids, dtype=int)}
|
|
839
|
+
valid_ids[seg_name] = ids
|
|
840
|
+
for label_id in ids:
|
|
841
|
+
cross_map[(seg_name, label_id)] = []
|
|
781
842
|
|
|
782
843
|
seg_names = list(all_segmentations.keys())
|
|
783
844
|
for i, seg1_name in enumerate(seg_names):
|
|
784
|
-
labels1,
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
845
|
+
labels1, _label_ids1 = all_segmentations[seg1_name]
|
|
846
|
+
# Check overlaps with all other segmentations.
|
|
847
|
+
for seg2_name in seg_names[i + 1 :]:
|
|
848
|
+
labels2, _label_ids2 = all_segmentations[seg2_name]
|
|
849
|
+
if labels1.shape != labels2.shape:
|
|
850
|
+
warnings.warn(
|
|
851
|
+
"Marker export: segmentation shape mismatch for "
|
|
852
|
+
f"'{seg1_name}' vs '{seg2_name}'. "
|
|
853
|
+
"Skipping cross-segmentation overlap mapping for this pair.",
|
|
854
|
+
RuntimeWarning,
|
|
855
|
+
)
|
|
856
|
+
continue
|
|
857
|
+
|
|
858
|
+
mask = (labels1 > 0) & (labels2 > 0)
|
|
859
|
+
if not np.any(mask):
|
|
860
|
+
continue
|
|
861
|
+
|
|
862
|
+
overlap_pairs = np.column_stack((labels1[mask], labels2[mask]))
|
|
863
|
+
unique_pairs = np.unique(overlap_pairs, axis=0)
|
|
864
|
+
for label_id1, label_id2 in unique_pairs:
|
|
865
|
+
id1 = int(label_id1)
|
|
866
|
+
id2 = int(label_id2)
|
|
867
|
+
if id1 not in valid_ids[seg1_name] or id2 not in valid_ids[seg2_name]:
|
|
868
|
+
continue
|
|
869
|
+
cross_map[(seg1_name, id1)].append((seg2_name, id2))
|
|
870
|
+
cross_map[(seg2_name, id2)].append((seg1_name, id1))
|
|
798
871
|
|
|
799
872
|
return cross_map
|
|
800
873
|
|
|
@@ -593,7 +593,7 @@ class MarkerChannelRow(QGroupBox):
|
|
|
593
593
|
slider : QWidget
|
|
594
594
|
Range slider widget.
|
|
595
595
|
layer : object
|
|
596
|
-
|
|
596
|
+
napari image layer providing intensity bounds.
|
|
597
597
|
min_spin : QDoubleSpinBox or None
|
|
598
598
|
Spin box that displays the minimum threshold value.
|
|
599
599
|
max_spin : QDoubleSpinBox or None
|
|
@@ -628,7 +628,7 @@ class MarkerChannelRow(QGroupBox):
|
|
|
628
628
|
Parameters
|
|
629
629
|
----------
|
|
630
630
|
layer : object
|
|
631
|
-
|
|
631
|
+
napari image layer providing contrast bounds and data.
|
|
632
632
|
|
|
633
633
|
Returns
|
|
634
634
|
-------
|
|
@@ -216,45 +216,75 @@ class SpotsChannelsDialog(QDialog):
|
|
|
216
216
|
return
|
|
217
217
|
for layer in viewer.layers:
|
|
218
218
|
if layer.__class__.__name__ == "Labels":
|
|
219
|
-
layer_name = layer.name
|
|
220
219
|
# Filter based on label type
|
|
221
|
-
if filter_type == "cellular" and self._is_cellular_label(
|
|
222
|
-
combo.addItem(
|
|
223
|
-
elif filter_type == "spots" and self._is_spot_label(
|
|
224
|
-
combo.addItem(
|
|
220
|
+
if filter_type == "cellular" and self._is_cellular_label(layer):
|
|
221
|
+
combo.addItem(layer.name)
|
|
222
|
+
elif filter_type == "spots" and self._is_spot_label(layer):
|
|
223
|
+
combo.addItem(layer.name)
|
|
225
224
|
if current:
|
|
226
225
|
index = combo.findText(current)
|
|
227
226
|
if index != -1:
|
|
228
227
|
combo.setCurrentIndex(index)
|
|
229
228
|
|
|
230
|
-
def
|
|
229
|
+
def _layer_task(self, layer: object) -> str | None:
|
|
230
|
+
"""Return normalized segmentation task from layer metadata."""
|
|
231
|
+
metadata = getattr(layer, "metadata", None)
|
|
232
|
+
if not isinstance(metadata, dict):
|
|
233
|
+
return None
|
|
234
|
+
task = metadata.get("task")
|
|
235
|
+
if not isinstance(task, str):
|
|
236
|
+
return None
|
|
237
|
+
normalized = task.strip().lower()
|
|
238
|
+
return normalized or None
|
|
239
|
+
|
|
240
|
+
def _is_cellular_label(self, layer: object | str) -> bool:
|
|
231
241
|
"""Check if a label layer is a cellular segmentation.
|
|
232
242
|
|
|
233
243
|
Parameters
|
|
234
244
|
----------
|
|
235
|
-
|
|
236
|
-
|
|
245
|
+
layer : object or str
|
|
246
|
+
Labels layer object or labels layer name.
|
|
237
247
|
|
|
238
248
|
Returns
|
|
239
249
|
-------
|
|
240
250
|
bool
|
|
241
251
|
True if the layer is a cellular label (nuclear or cytoplasmic).
|
|
242
252
|
"""
|
|
253
|
+
if isinstance(layer, str):
|
|
254
|
+
layer_name = layer
|
|
255
|
+
task = None
|
|
256
|
+
else:
|
|
257
|
+
layer_name = str(getattr(layer, "name", ""))
|
|
258
|
+
task = self._layer_task(layer)
|
|
259
|
+
if task in {"nuclear", "cytoplasmic"}:
|
|
260
|
+
return True
|
|
261
|
+
if task is not None:
|
|
262
|
+
return False
|
|
243
263
|
return layer_name.endswith("_nuc_labels") or layer_name.endswith("_cyto_labels")
|
|
244
264
|
|
|
245
|
-
def _is_spot_label(self,
|
|
265
|
+
def _is_spot_label(self, layer: object | str) -> bool:
|
|
246
266
|
"""Check if a label layer is a spot segmentation.
|
|
247
267
|
|
|
248
268
|
Parameters
|
|
249
269
|
----------
|
|
250
|
-
|
|
251
|
-
|
|
270
|
+
layer : object or str
|
|
271
|
+
Labels layer object or labels layer name.
|
|
252
272
|
|
|
253
273
|
Returns
|
|
254
274
|
-------
|
|
255
275
|
bool
|
|
256
276
|
True if the layer is a spot label.
|
|
257
277
|
"""
|
|
278
|
+
if isinstance(layer, str):
|
|
279
|
+
layer_name = layer
|
|
280
|
+
task = None
|
|
281
|
+
else:
|
|
282
|
+
layer_name = str(getattr(layer, "name", ""))
|
|
283
|
+
task = self._layer_task(layer)
|
|
284
|
+
if task == "spots":
|
|
285
|
+
return True
|
|
286
|
+
if task is not None:
|
|
287
|
+
return False
|
|
258
288
|
return layer_name.endswith("_spot_labels")
|
|
259
289
|
|
|
260
290
|
def _refresh_image_combo(self, combo: QComboBox) -> None:
|