senoquant 1.0.0b3__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 +1 -1
- senoquant/_widget.py +9 -1
- senoquant/tabs/__init__.py +2 -0
- senoquant/tabs/batch/backend.py +58 -24
- senoquant/tabs/batch/frontend.py +119 -21
- senoquant/tabs/spots/frontend.py +54 -0
- senoquant/tabs/spots/models/rmp/details.json +55 -0
- senoquant/tabs/spots/models/rmp/model.py +574 -0
- senoquant/tabs/spots/models/ufish/details.json +16 -1
- senoquant/tabs/spots/models/ufish/model.py +211 -13
- senoquant/tabs/spots/ufish_utils/core.py +31 -1
- 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-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/METADATA +7 -6
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/RECORD +25 -15
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/WHEEL +0 -0
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/entry_points.txt +0 -0
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/licenses/LICENSE +0 -0
- {senoquant-1.0.0b3.dist-info → senoquant-1.0.0b4.dist-info}/top_level.txt +0 -0
senoquant/__init__.py
CHANGED
|
@@ -4,7 +4,7 @@ try:
|
|
|
4
4
|
from importlib.metadata import version
|
|
5
5
|
__version__ = version("senoquant")
|
|
6
6
|
except Exception:
|
|
7
|
-
__version__ = "1.0.
|
|
7
|
+
__version__ = "1.0.0b4" # Fallback for development
|
|
8
8
|
|
|
9
9
|
from ._widget import SenoQuantWidget
|
|
10
10
|
__all__ = ["SenoQuantWidget"]
|
senoquant/_widget.py
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from qtpy.QtWidgets import QTabWidget, QVBoxLayout, QWidget
|
|
4
4
|
|
|
5
|
-
from .tabs import
|
|
5
|
+
from .tabs import (
|
|
6
|
+
BatchTab,
|
|
7
|
+
QuantificationTab,
|
|
8
|
+
SegmentationTab,
|
|
9
|
+
SettingsTab,
|
|
10
|
+
SpotsTab,
|
|
11
|
+
VisualizationTab,
|
|
12
|
+
)
|
|
6
13
|
from .tabs.settings.backend import SettingsBackend
|
|
7
14
|
|
|
8
15
|
|
|
@@ -26,6 +33,7 @@ class SenoQuantWidget(QWidget):
|
|
|
26
33
|
)
|
|
27
34
|
tabs.addTab(SpotsTab(napari_viewer=napari_viewer), "Spots")
|
|
28
35
|
tabs.addTab(QuantificationTab(napari_viewer=napari_viewer), "Quantification")
|
|
36
|
+
tabs.addTab(VisualizationTab(napari_viewer=napari_viewer), "Visualization")
|
|
29
37
|
tabs.addTab(BatchTab(napari_viewer=napari_viewer), "Batch")
|
|
30
38
|
tabs.addTab(SettingsTab(backend=self._settings_backend), "Settings")
|
|
31
39
|
|
senoquant/tabs/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from .segmentation.frontend import SegmentationTab
|
|
4
4
|
from .spots.frontend import SpotsTab
|
|
5
5
|
from .quantification.frontend import QuantificationTab
|
|
6
|
+
from .visualization.frontend import VisualizationTab
|
|
6
7
|
from .settings.frontend import SettingsTab
|
|
7
8
|
from .batch.frontend import BatchTab
|
|
8
9
|
|
|
@@ -10,6 +11,7 @@ __all__ = [
|
|
|
10
11
|
"SegmentationTab",
|
|
11
12
|
"SpotsTab",
|
|
12
13
|
"QuantificationTab",
|
|
14
|
+
"VisualizationTab",
|
|
13
15
|
"SettingsTab",
|
|
14
16
|
"BatchTab",
|
|
15
17
|
]
|
senoquant/tabs/batch/backend.py
CHANGED
|
@@ -208,7 +208,8 @@ class BatchBackend:
|
|
|
208
208
|
cyto_channel : str or int or None, optional
|
|
209
209
|
Channel selection for cytoplasm.
|
|
210
210
|
cyto_nuclear_channel : str or int or None, optional
|
|
211
|
-
Optional nuclear
|
|
211
|
+
Optional nuclear input for cytoplasmic models. This may be a
|
|
212
|
+
channel selection or a generated nuclear label name.
|
|
212
213
|
cyto_settings : dict or None, optional
|
|
213
214
|
Model settings for cytoplasmic segmentation.
|
|
214
215
|
spot_detector : str or None, optional
|
|
@@ -260,6 +261,14 @@ class BatchBackend:
|
|
|
260
261
|
cyto_settings = cyto_settings or {}
|
|
261
262
|
spot_settings = spot_settings or {}
|
|
262
263
|
quant_backend = QuantificationBackend()
|
|
264
|
+
cyto_model_instance = None
|
|
265
|
+
cyto_nuclear_only = False
|
|
266
|
+
if cyto_model:
|
|
267
|
+
cyto_model_instance = self._segmentation_backend.get_model(cyto_model)
|
|
268
|
+
modes: list[str] = []
|
|
269
|
+
if hasattr(cyto_model_instance, "cytoplasmic_input_modes"):
|
|
270
|
+
modes = cyto_model_instance.cytoplasmic_input_modes()
|
|
271
|
+
cyto_nuclear_only = modes == ["nuclear"]
|
|
263
272
|
|
|
264
273
|
# Count total items to process
|
|
265
274
|
total_items = 0
|
|
@@ -349,36 +358,61 @@ class BatchBackend:
|
|
|
349
358
|
item_result.outputs[label_name] = out_path
|
|
350
359
|
|
|
351
360
|
if cyto_model:
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
)
|
|
358
|
-
if cyto_image is None:
|
|
359
|
-
raise RuntimeError(
|
|
360
|
-
"Failed to read cytoplasmic image data."
|
|
361
|
-
)
|
|
362
|
-
cyto_layer = Image(cyto_image, "cytoplasmic", cyto_meta)
|
|
363
|
-
cyto_nuclear_layer = None
|
|
364
|
-
if cyto_nuclear_channel is not None:
|
|
365
|
-
nuclear_idx = resolve_channel_index(
|
|
366
|
-
cyto_nuclear_channel, normalized_channels
|
|
361
|
+
cyto_layer = None
|
|
362
|
+
cyto_meta: dict = {}
|
|
363
|
+
if not cyto_nuclear_only:
|
|
364
|
+
channel_idx = resolve_channel_index(
|
|
365
|
+
cyto_channel, normalized_channels
|
|
367
366
|
)
|
|
368
|
-
|
|
369
|
-
path,
|
|
367
|
+
cyto_image, cyto_meta = load_channel_data(
|
|
368
|
+
path, channel_idx, scene_id
|
|
370
369
|
)
|
|
371
|
-
if
|
|
370
|
+
if cyto_image is None:
|
|
372
371
|
raise RuntimeError(
|
|
373
|
-
"Failed to read cytoplasmic
|
|
372
|
+
"Failed to read cytoplasmic image data."
|
|
373
|
+
)
|
|
374
|
+
cyto_layer = Image(cyto_image, "cytoplasmic", cyto_meta)
|
|
375
|
+
cyto_nuclear_layer = None
|
|
376
|
+
if cyto_nuclear_channel is not None:
|
|
377
|
+
cyto_nuclear_key = str(cyto_nuclear_channel)
|
|
378
|
+
if (
|
|
379
|
+
cyto_nuclear_only
|
|
380
|
+
and cyto_nuclear_key in labels_data
|
|
381
|
+
):
|
|
382
|
+
nuclear_meta = labels_meta.get(cyto_nuclear_key, {})
|
|
383
|
+
cyto_nuclear_layer = Labels(
|
|
384
|
+
labels_data[cyto_nuclear_key],
|
|
385
|
+
cyto_nuclear_key,
|
|
386
|
+
nuclear_meta,
|
|
387
|
+
)
|
|
388
|
+
if not cyto_meta:
|
|
389
|
+
cyto_meta = dict(nuclear_meta)
|
|
390
|
+
else:
|
|
391
|
+
nuclear_idx = resolve_channel_index(
|
|
392
|
+
cyto_nuclear_channel, normalized_channels
|
|
374
393
|
)
|
|
375
|
-
|
|
376
|
-
|
|
394
|
+
nuclear_image, nuclear_meta = load_channel_data(
|
|
395
|
+
path, nuclear_idx, scene_id
|
|
396
|
+
)
|
|
397
|
+
if nuclear_image is None:
|
|
398
|
+
raise RuntimeError(
|
|
399
|
+
"Failed to read cytoplasmic nuclear data."
|
|
400
|
+
)
|
|
401
|
+
cyto_nuclear_layer = Image(
|
|
402
|
+
nuclear_image, "nuclear", nuclear_meta
|
|
403
|
+
)
|
|
404
|
+
if not cyto_meta:
|
|
405
|
+
cyto_meta = dict(nuclear_meta)
|
|
406
|
+
if cyto_nuclear_only and cyto_nuclear_layer is None:
|
|
407
|
+
raise RuntimeError(
|
|
408
|
+
"Selected cytoplasmic model requires nuclear labels."
|
|
377
409
|
)
|
|
378
|
-
|
|
379
|
-
|
|
410
|
+
if cyto_model_instance is None:
|
|
411
|
+
raise RuntimeError("Failed to load cytoplasmic model.")
|
|
412
|
+
seg_result = cyto_model_instance.run(
|
|
380
413
|
task="cytoplasmic",
|
|
381
414
|
layer=cyto_layer,
|
|
415
|
+
cytoplasmic_layer=cyto_layer,
|
|
382
416
|
nuclear_layer=cyto_nuclear_layer,
|
|
383
417
|
settings=cyto_settings,
|
|
384
418
|
)
|
senoquant/tabs/batch/frontend.py
CHANGED
|
@@ -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."""
|
|
@@ -663,6 +701,8 @@ class BatchTab(QWidget):
|
|
|
663
701
|
self._nuclear_settings_widgets.clear()
|
|
664
702
|
self._nuclear_settings_meta.clear()
|
|
665
703
|
if not model_name or model_name.startswith("("):
|
|
704
|
+
self._refresh_channel_choices()
|
|
705
|
+
self._refresh_config_viewer()
|
|
666
706
|
return
|
|
667
707
|
model = self._segmentation_backend.get_model(model_name)
|
|
668
708
|
settings = model.list_settings()
|
|
@@ -671,17 +711,23 @@ class BatchTab(QWidget):
|
|
|
671
711
|
item.get("key", item.get("label", "")): item for item in settings
|
|
672
712
|
}
|
|
673
713
|
self._nuclear_settings_values = _defaults_from_settings(settings)
|
|
714
|
+
self._refresh_channel_choices()
|
|
715
|
+
self._refresh_config_viewer()
|
|
674
716
|
|
|
675
717
|
def _update_cyto_settings(self) -> None:
|
|
676
718
|
"""Refresh cytoplasmic model settings from the selected model."""
|
|
677
719
|
model_name = self._cyto_model_combo.currentText()
|
|
678
720
|
self._cyto_settings_widgets.clear()
|
|
679
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
|
|
680
726
|
if not model_name or model_name.startswith("("):
|
|
681
|
-
self._cyto_nuclear_combo.setEnabled(False)
|
|
682
727
|
if hasattr(self, "_cyto_nuclear_label"):
|
|
683
728
|
self._cyto_nuclear_label.setText("Nuclear channel")
|
|
684
|
-
self.
|
|
729
|
+
self._refresh_channel_choices()
|
|
730
|
+
self._update_processing_state()
|
|
685
731
|
return
|
|
686
732
|
model = self._segmentation_backend.get_model(model_name)
|
|
687
733
|
settings = model.list_settings()
|
|
@@ -691,20 +737,30 @@ class BatchTab(QWidget):
|
|
|
691
737
|
}
|
|
692
738
|
self._cyto_settings_values = _defaults_from_settings(settings)
|
|
693
739
|
modes = model.cytoplasmic_input_modes()
|
|
694
|
-
|
|
695
|
-
|
|
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:
|
|
696
747
|
optional = model.cytoplasmic_nuclear_optional()
|
|
697
748
|
suffix = "optional" if optional else "required"
|
|
698
749
|
if hasattr(self, "_cyto_nuclear_label"):
|
|
699
750
|
self._cyto_nuclear_label.setText(f"Nuclear channel ({suffix})")
|
|
700
|
-
self._cyto_nuclear_combo.setEnabled(True)
|
|
701
751
|
self._cyto_nuclear_optional = optional
|
|
752
|
+
self._cyto_supports_nuclear_input = True
|
|
702
753
|
else:
|
|
703
754
|
if hasattr(self, "_cyto_nuclear_label"):
|
|
704
755
|
self._cyto_nuclear_label.setText("Nuclear channel")
|
|
705
|
-
self._cyto_nuclear_combo.setEnabled(False)
|
|
706
|
-
self._cyto_nuclear_optional = False
|
|
707
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()
|
|
708
764
|
|
|
709
765
|
def _update_spot_settings(self) -> None:
|
|
710
766
|
"""Refresh spot detector settings from the selected detector."""
|
|
@@ -832,12 +888,18 @@ class BatchTab(QWidget):
|
|
|
832
888
|
nuclear_enabled = self._nuclear_enabled.isChecked()
|
|
833
889
|
cyto_enabled = self._cyto_enabled.isChecked()
|
|
834
890
|
spot_enabled = self._spots_enabled.isChecked()
|
|
891
|
+
if self._cyto_nuclear_from_labels:
|
|
892
|
+
self._refresh_channel_choices()
|
|
835
893
|
self._nuclear_model_combo.setEnabled(nuclear_enabled)
|
|
836
894
|
self._nuclear_channel_combo.setEnabled(nuclear_enabled)
|
|
837
895
|
self._nuclear_settings_button.setEnabled(nuclear_enabled)
|
|
838
896
|
self._cyto_model_combo.setEnabled(cyto_enabled)
|
|
839
|
-
self._cyto_channel_combo.setEnabled(
|
|
840
|
-
|
|
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
|
+
)
|
|
841
903
|
self._cyto_settings_button.setEnabled(cyto_enabled)
|
|
842
904
|
self._spot_detector_combo.setEnabled(spot_enabled)
|
|
843
905
|
self._spot_settings_button.setEnabled(spot_enabled)
|
|
@@ -886,10 +948,33 @@ class BatchTab(QWidget):
|
|
|
886
948
|
spot_detector = None
|
|
887
949
|
|
|
888
950
|
cyto_model = None
|
|
951
|
+
cyto_model_obj = None
|
|
889
952
|
if self._cyto_enabled.isChecked() and self._cyto_model_combo.isEnabled():
|
|
890
953
|
cyto_model = self._cyto_model_combo.currentText().strip()
|
|
891
954
|
if cyto_model.startswith("("):
|
|
892
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
|
|
893
978
|
|
|
894
979
|
quant_features = (
|
|
895
980
|
list(self._quant_tab._feature_configs)
|
|
@@ -1008,7 +1093,8 @@ class BatchTab(QWidget):
|
|
|
1008
1093
|
channel=self._cyto_channel_combo.currentText(),
|
|
1009
1094
|
nuclear_channel=(
|
|
1010
1095
|
""
|
|
1011
|
-
if self._cyto_nuclear_combo.currentText().strip()
|
|
1096
|
+
if self._cyto_nuclear_combo.currentText().strip()
|
|
1097
|
+
in {"(none)", "(no nuclear labels)"}
|
|
1012
1098
|
else self._cyto_nuclear_combo.currentText()
|
|
1013
1099
|
),
|
|
1014
1100
|
settings=cyto_settings,
|
|
@@ -1057,6 +1143,8 @@ class BatchTab(QWidget):
|
|
|
1057
1143
|
if not job.cytoplasmic.nuclear_channel:
|
|
1058
1144
|
if self._cyto_nuclear_combo.findText("(none)") != -1:
|
|
1059
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)")
|
|
1060
1148
|
else:
|
|
1061
1149
|
self._set_combo_value(
|
|
1062
1150
|
self._cyto_nuclear_combo, job.cytoplasmic.nuclear_channel
|
|
@@ -1082,6 +1170,16 @@ class BatchTab(QWidget):
|
|
|
1082
1170
|
self._refresh_spot_channel_choices()
|
|
1083
1171
|
self._refresh_config_viewer()
|
|
1084
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
|
+
|
|
1085
1183
|
def _save_profile(self) -> None:
|
|
1086
1184
|
"""Save the current configuration to a JSON profile."""
|
|
1087
1185
|
path, _ = QFileDialog.getSaveFileName(
|
senoquant/tabs/spots/frontend.py
CHANGED
|
@@ -651,6 +651,9 @@ class SpotsTab(QWidget):
|
|
|
651
651
|
if mask is not None:
|
|
652
652
|
filtered_mask = self._apply_size_filter(mask)
|
|
653
653
|
self._add_labels_layer(layer, filtered_mask, detector_name)
|
|
654
|
+
debug_images = result.get("debug_images")
|
|
655
|
+
if isinstance(debug_images, dict):
|
|
656
|
+
self._add_debug_image_layers(layer, detector_name, debug_images)
|
|
654
657
|
|
|
655
658
|
def _add_labels_layer(self, source_layer, mask, detector_name: str) -> None:
|
|
656
659
|
"""Add a labels layer for the detector mask."""
|
|
@@ -694,6 +697,57 @@ class SpotsTab(QWidget):
|
|
|
694
697
|
labels_layer.metadata = merged_metadata
|
|
695
698
|
labels_layer.contour = 1
|
|
696
699
|
|
|
700
|
+
def _add_debug_image_layers(
|
|
701
|
+
self,
|
|
702
|
+
source_layer,
|
|
703
|
+
detector_name: str,
|
|
704
|
+
debug_images: dict,
|
|
705
|
+
) -> None:
|
|
706
|
+
"""Add debug image layers emitted by a detector run."""
|
|
707
|
+
if self._viewer is None:
|
|
708
|
+
return
|
|
709
|
+
source_name = getattr(source_layer, "name", "") if source_layer is not None else ""
|
|
710
|
+
source_name = source_name.strip() if isinstance(source_name, str) else ""
|
|
711
|
+
|
|
712
|
+
for debug_key, debug_image in debug_images.items():
|
|
713
|
+
if debug_image is None:
|
|
714
|
+
continue
|
|
715
|
+
image_data = np.asarray(debug_image)
|
|
716
|
+
if image_data.size == 0:
|
|
717
|
+
continue
|
|
718
|
+
layer_name = f"{detector_name}_{debug_key}"
|
|
719
|
+
if source_name:
|
|
720
|
+
layer_name = f"{source_name}_{layer_name}"
|
|
721
|
+
if layer_name in self._viewer.layers:
|
|
722
|
+
self._viewer.layers.remove(layer_name)
|
|
723
|
+
|
|
724
|
+
metadata = {"task": "spots", "debug": True}
|
|
725
|
+
image_layer = None
|
|
726
|
+
if Image is not None and hasattr(self._viewer, "add_layer"):
|
|
727
|
+
image_layer = Image(
|
|
728
|
+
image_data,
|
|
729
|
+
name=layer_name,
|
|
730
|
+
metadata=metadata,
|
|
731
|
+
)
|
|
732
|
+
added_layer = self._viewer.add_layer(image_layer)
|
|
733
|
+
if added_layer is not None:
|
|
734
|
+
image_layer = added_layer
|
|
735
|
+
elif hasattr(self._viewer, "add_image"):
|
|
736
|
+
try:
|
|
737
|
+
image_layer = self._viewer.add_image(
|
|
738
|
+
image_data,
|
|
739
|
+
name=layer_name,
|
|
740
|
+
metadata=metadata,
|
|
741
|
+
)
|
|
742
|
+
except TypeError:
|
|
743
|
+
image_layer = self._viewer.add_image(
|
|
744
|
+
image_data,
|
|
745
|
+
name=layer_name,
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
if image_layer is not None:
|
|
749
|
+
image_layer.visible = True
|
|
750
|
+
|
|
697
751
|
def _apply_size_filter(self, mask: np.ndarray) -> np.ndarray:
|
|
698
752
|
"""Filter spots by size based on min/max settings.
|
|
699
753
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rmp",
|
|
3
|
+
"description": "RMP spot detector implementation",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"order": 0,
|
|
6
|
+
"settings": [
|
|
7
|
+
{
|
|
8
|
+
"key": "denoising_kernel_length",
|
|
9
|
+
"label": "Denoising kernel length",
|
|
10
|
+
"type": "int",
|
|
11
|
+
"min": 2,
|
|
12
|
+
"max": 9999,
|
|
13
|
+
"default": 2,
|
|
14
|
+
"enabled_by": "enable_denoising"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"key": "extraction_kernel_length",
|
|
18
|
+
"label": "Extraction kernel length",
|
|
19
|
+
"type": "int",
|
|
20
|
+
"min": 3,
|
|
21
|
+
"max": 9999,
|
|
22
|
+
"default": 10
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"key": "angle_spacing",
|
|
26
|
+
"label": "Angle spacing",
|
|
27
|
+
"type": "int",
|
|
28
|
+
"min": 1,
|
|
29
|
+
"max": 10,
|
|
30
|
+
"default": 5
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"key": "manual_threshold",
|
|
34
|
+
"label": "Manual threshold",
|
|
35
|
+
"type": "float",
|
|
36
|
+
"decimals": 2,
|
|
37
|
+
"min": 0.0,
|
|
38
|
+
"max": 1.0,
|
|
39
|
+
"default": 0.05,
|
|
40
|
+
"disabled_by": "auto_threshold"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"key": "auto_threshold",
|
|
44
|
+
"label": "Auto threshold",
|
|
45
|
+
"type": "bool",
|
|
46
|
+
"default": true
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"key": "enable_denoising",
|
|
50
|
+
"label": "Enable denoising",
|
|
51
|
+
"type": "bool",
|
|
52
|
+
"default": true
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|