senoquant 1.0.0b1__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 -0
- senoquant/_reader.py +7 -0
- senoquant/_widget.py +33 -0
- senoquant/napari.yaml +83 -0
- senoquant/reader/__init__.py +5 -0
- senoquant/reader/core.py +369 -0
- senoquant/tabs/__init__.py +15 -0
- senoquant/tabs/batch/__init__.py +10 -0
- senoquant/tabs/batch/backend.py +641 -0
- senoquant/tabs/batch/config.py +270 -0
- senoquant/tabs/batch/frontend.py +1283 -0
- senoquant/tabs/batch/io.py +326 -0
- senoquant/tabs/batch/layers.py +86 -0
- senoquant/tabs/quantification/__init__.py +1 -0
- senoquant/tabs/quantification/backend.py +228 -0
- senoquant/tabs/quantification/features/__init__.py +80 -0
- senoquant/tabs/quantification/features/base.py +142 -0
- senoquant/tabs/quantification/features/marker/__init__.py +5 -0
- senoquant/tabs/quantification/features/marker/config.py +69 -0
- senoquant/tabs/quantification/features/marker/dialog.py +437 -0
- senoquant/tabs/quantification/features/marker/export.py +879 -0
- senoquant/tabs/quantification/features/marker/feature.py +119 -0
- senoquant/tabs/quantification/features/marker/morphology.py +285 -0
- senoquant/tabs/quantification/features/marker/rows.py +654 -0
- senoquant/tabs/quantification/features/marker/thresholding.py +46 -0
- senoquant/tabs/quantification/features/roi.py +346 -0
- senoquant/tabs/quantification/features/spots/__init__.py +5 -0
- senoquant/tabs/quantification/features/spots/config.py +62 -0
- senoquant/tabs/quantification/features/spots/dialog.py +477 -0
- senoquant/tabs/quantification/features/spots/export.py +1292 -0
- senoquant/tabs/quantification/features/spots/feature.py +112 -0
- senoquant/tabs/quantification/features/spots/morphology.py +279 -0
- senoquant/tabs/quantification/features/spots/rows.py +241 -0
- senoquant/tabs/quantification/frontend.py +815 -0
- senoquant/tabs/segmentation/__init__.py +1 -0
- senoquant/tabs/segmentation/backend.py +131 -0
- senoquant/tabs/segmentation/frontend.py +1009 -0
- senoquant/tabs/segmentation/models/__init__.py +5 -0
- senoquant/tabs/segmentation/models/base.py +146 -0
- senoquant/tabs/segmentation/models/cpsam/details.json +65 -0
- senoquant/tabs/segmentation/models/cpsam/model.py +150 -0
- senoquant/tabs/segmentation/models/default_2d/details.json +69 -0
- senoquant/tabs/segmentation/models/default_2d/model.py +664 -0
- senoquant/tabs/segmentation/models/default_3d/details.json +69 -0
- senoquant/tabs/segmentation/models/default_3d/model.py +682 -0
- senoquant/tabs/segmentation/models/hf.py +71 -0
- senoquant/tabs/segmentation/models/nuclear_dilation/__init__.py +1 -0
- senoquant/tabs/segmentation/models/nuclear_dilation/details.json +26 -0
- senoquant/tabs/segmentation/models/nuclear_dilation/model.py +96 -0
- senoquant/tabs/segmentation/models/perinuclear_rings/__init__.py +1 -0
- senoquant/tabs/segmentation/models/perinuclear_rings/details.json +34 -0
- senoquant/tabs/segmentation/models/perinuclear_rings/model.py +132 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/__init__.py +2 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/__init__.py +3 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/__init__.py +6 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/generate.py +470 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/prepare.py +273 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/rawdata.py +112 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/transform.py +384 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/__init__.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/blocks.py +184 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/losses.py +79 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/nets.py +165 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/predict.py +467 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/probability.py +67 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/train.py +148 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/io/__init__.py +163 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/__init__.py +52 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/base_model.py +329 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_isotropic.py +160 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_projection.py +178 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_standard.py +446 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_upsampling.py +54 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/config.py +254 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/pretrained.py +119 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/scripts/__init__.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/scripts/care_predict.py +180 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/__init__.py +5 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/plot_utils.py +159 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/six.py +18 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/tf.py +644 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/utils.py +272 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/version.py +1 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/docs/source/conf.py +368 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/setup.py +68 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_datagen.py +169 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_models.py +462 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_utils.py +166 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +34 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/__init__.py +30 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/big.py +624 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/bioimageio_utils.py +494 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/data/__init__.py +39 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/__init__.py +10 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/geom2d.py +215 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/geom3d.py +349 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/matching.py +483 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/__init__.py +28 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/base.py +1217 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/model2d.py +594 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/model3d.py +696 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/nms.py +384 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/__init__.py +2 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/plot.py +74 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/render.py +298 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/rays3d.py +373 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/sample_patches.py +65 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/__init__.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/predict2d.py +90 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/predict3d.py +93 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/utils.py +408 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/version.py +1 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/__init__.py +45 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/__init__.py +17 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/cli.py +55 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/core.py +285 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/__init__.py +15 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/cli.py +36 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/divisibility.py +193 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +100 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/receptive_field.py +182 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/rf_cli.py +48 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/valid_sizes.py +278 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/post/__init__.py +8 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/post/core.py +157 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/pre/__init__.py +17 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/pre/core.py +226 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/predict/__init__.py +5 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/predict/core.py +401 -0
- senoquant/tabs/settings/__init__.py +1 -0
- senoquant/tabs/settings/backend.py +29 -0
- senoquant/tabs/settings/frontend.py +19 -0
- senoquant/tabs/spots/__init__.py +1 -0
- senoquant/tabs/spots/backend.py +139 -0
- senoquant/tabs/spots/frontend.py +800 -0
- senoquant/tabs/spots/models/__init__.py +5 -0
- senoquant/tabs/spots/models/base.py +94 -0
- senoquant/tabs/spots/models/rmp/details.json +61 -0
- senoquant/tabs/spots/models/rmp/model.py +499 -0
- senoquant/tabs/spots/models/udwt/details.json +103 -0
- senoquant/tabs/spots/models/udwt/model.py +482 -0
- senoquant/utils.py +25 -0
- senoquant-1.0.0b1.dist-info/METADATA +193 -0
- senoquant-1.0.0b1.dist-info/RECORD +148 -0
- senoquant-1.0.0b1.dist-info/WHEEL +5 -0
- senoquant-1.0.0b1.dist-info/entry_points.txt +2 -0
- senoquant-1.0.0b1.dist-info/licenses/LICENSE +28 -0
- senoquant-1.0.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Spots feature UI."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from qtpy.QtWidgets import QCheckBox, QDialog, QPushButton
|
|
6
|
+
|
|
7
|
+
from ..base import SenoQuantFeature
|
|
8
|
+
from ..roi import ROISection
|
|
9
|
+
from .config import SpotsFeatureData
|
|
10
|
+
from .dialog import SpotsChannelsDialog
|
|
11
|
+
from .export import export_spots
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SpotsFeature(SenoQuantFeature):
|
|
15
|
+
"""Spots feature controls."""
|
|
16
|
+
|
|
17
|
+
feature_type = "Spots"
|
|
18
|
+
order = 20
|
|
19
|
+
|
|
20
|
+
def build(self) -> None:
|
|
21
|
+
"""Build the spots feature UI."""
|
|
22
|
+
self._build_channels_section()
|
|
23
|
+
data = self._state.data
|
|
24
|
+
if getattr(self._tab, "_enable_rois", True):
|
|
25
|
+
if isinstance(data, SpotsFeatureData):
|
|
26
|
+
roi_section = ROISection(self._tab, self._context, data.rois)
|
|
27
|
+
else:
|
|
28
|
+
roi_section = ROISection(self._tab, self._context, [])
|
|
29
|
+
roi_section.build()
|
|
30
|
+
self._ui["roi_section"] = roi_section
|
|
31
|
+
|
|
32
|
+
def on_features_changed(self, configs: list) -> None:
|
|
33
|
+
"""Update ROI titles when feature ordering changes.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
configs : list of FeatureUIContext
|
|
38
|
+
Current feature contexts.
|
|
39
|
+
"""
|
|
40
|
+
roi_section = self._ui.get("roi_section")
|
|
41
|
+
if roi_section is not None:
|
|
42
|
+
roi_section.update_titles()
|
|
43
|
+
|
|
44
|
+
def _build_channels_section(self) -> None:
|
|
45
|
+
"""Build the channels button that opens the popup dialog."""
|
|
46
|
+
left_dynamic_layout = self._context.left_dynamic_layout
|
|
47
|
+
button = QPushButton("Add channels")
|
|
48
|
+
button.clicked.connect(self._open_channels_dialog)
|
|
49
|
+
left_dynamic_layout.addWidget(button)
|
|
50
|
+
data = self._state.data
|
|
51
|
+
checkbox = QCheckBox("Export colocalization")
|
|
52
|
+
checkbox.setChecked(
|
|
53
|
+
isinstance(data, SpotsFeatureData) and data.export_colocalization
|
|
54
|
+
)
|
|
55
|
+
checkbox.toggled.connect(self._set_export_colocalization)
|
|
56
|
+
left_dynamic_layout.addWidget(checkbox)
|
|
57
|
+
self._ui["channels_button"] = button
|
|
58
|
+
self._ui["colocalization_checkbox"] = checkbox
|
|
59
|
+
self._update_channels_button_label()
|
|
60
|
+
|
|
61
|
+
def _set_export_colocalization(self, checked: bool) -> None:
|
|
62
|
+
"""Store colocalization export preference."""
|
|
63
|
+
data = self._state.data
|
|
64
|
+
if not isinstance(data, SpotsFeatureData):
|
|
65
|
+
return
|
|
66
|
+
data.export_colocalization = checked
|
|
67
|
+
|
|
68
|
+
def _open_channels_dialog(self) -> None:
|
|
69
|
+
"""Open the channels configuration dialog."""
|
|
70
|
+
dialog = self._ui.get("channels_dialog")
|
|
71
|
+
if dialog is None or not isinstance(dialog, QDialog):
|
|
72
|
+
dialog = SpotsChannelsDialog(self)
|
|
73
|
+
dialog.accepted.connect(self._update_channels_button_label)
|
|
74
|
+
self._ui["channels_dialog"] = dialog
|
|
75
|
+
dialog.show()
|
|
76
|
+
dialog.raise_()
|
|
77
|
+
dialog.activateWindow()
|
|
78
|
+
|
|
79
|
+
def _update_channels_button_label(self) -> None:
|
|
80
|
+
"""Update the channels button label based on saved data."""
|
|
81
|
+
button = self._ui.get("channels_button")
|
|
82
|
+
if button is None:
|
|
83
|
+
return
|
|
84
|
+
data = self._state.data
|
|
85
|
+
if isinstance(data, SpotsFeatureData) and (
|
|
86
|
+
data.channels or data.segmentations
|
|
87
|
+
):
|
|
88
|
+
button.setText("Edit channels")
|
|
89
|
+
else:
|
|
90
|
+
button.setText("Add channels")
|
|
91
|
+
|
|
92
|
+
def export(self, temp_dir: Path, export_format: str):
|
|
93
|
+
"""Export spots outputs into a temporary directory.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
temp_dir : Path
|
|
98
|
+
Temporary directory where outputs should be written.
|
|
99
|
+
export_format : str
|
|
100
|
+
File format requested by the user (``"csv"`` or ``"xlsx"``).
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
iterable of Path
|
|
105
|
+
Paths to files produced by the export routine.
|
|
106
|
+
"""
|
|
107
|
+
return export_spots(
|
|
108
|
+
self._state,
|
|
109
|
+
temp_dir,
|
|
110
|
+
viewer=self._tab._viewer,
|
|
111
|
+
export_format=export_format,
|
|
112
|
+
)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Morphological descriptor extraction for spots cells table.
|
|
2
|
+
|
|
3
|
+
This module provides utilities to extract morphological properties
|
|
4
|
+
from cell segmentations used in spots analysis. Morphology is only
|
|
5
|
+
added to the cells table, not the spots table.
|
|
6
|
+
|
|
7
|
+
Descriptors include area/volume, shape metrics, and perimeter.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from skimage.measure import regionprops_table
|
|
16
|
+
|
|
17
|
+
# Float-valued morphological properties to extract from regionprops.
|
|
18
|
+
MORPHOLOGY_PROPERTIES = (
|
|
19
|
+
"area", # Number of pixels in the region
|
|
20
|
+
"eccentricity", # Eccentricity of the ellipse fitted to the region
|
|
21
|
+
"extent", # Ratio of region area to bounding box area
|
|
22
|
+
"feret_diameter_max", # Maximum Feret diameter
|
|
23
|
+
"major_axis_length", # Major axis of the ellipse fitted to the region
|
|
24
|
+
"minor_axis_length", # Minor axis of the ellipse fitted to the region
|
|
25
|
+
"orientation", # Angle between the major axis and horizontal
|
|
26
|
+
"perimeter", # Perimeter estimated by the Freeman chain code
|
|
27
|
+
"perimeter_crofton", # Crofton perimeter (Euclidean-like estimate)
|
|
28
|
+
"solidity", # Ratio of region area to convex hull area
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
NDIM_2D = 2
|
|
32
|
+
NDIM_3D = 3
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _collect_simple_properties(
|
|
36
|
+
props: dict,
|
|
37
|
+
label_ids: np.ndarray,
|
|
38
|
+
) -> dict[str, np.ndarray]:
|
|
39
|
+
"""Extract simple (non-array) properties from regionprops.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
props : dict
|
|
44
|
+
Output from regionprops_table.
|
|
45
|
+
label_ids : numpy.ndarray
|
|
46
|
+
Label ids for indexing.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
dict of str to numpy.ndarray
|
|
51
|
+
Simple property arrays keyed by property name.
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
result: dict[str, np.ndarray] = {}
|
|
55
|
+
for prop_name, prop_values in props.items():
|
|
56
|
+
if prop_name == "label":
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
if isinstance(prop_values, list) and prop_values:
|
|
60
|
+
first_val = prop_values[0]
|
|
61
|
+
if isinstance(first_val, (np.ndarray, list, tuple)):
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
prop_array = np.asarray(prop_values, dtype=float)
|
|
66
|
+
if prop_array.size == len(label_ids):
|
|
67
|
+
result[f"morph_{prop_name}"] = prop_array
|
|
68
|
+
except (ValueError, TypeError):
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _compute_derived_metrics(
|
|
75
|
+
result: dict[str, np.ndarray],
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Compute derived morphological metrics in-place.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
result : dict of str to numpy.ndarray
|
|
82
|
+
Morphological properties to augment with derived metrics.
|
|
83
|
+
|
|
84
|
+
Notes
|
|
85
|
+
-----
|
|
86
|
+
Circularity is 2D-only: 4*pi*area / perimeter^2. It is only computed
|
|
87
|
+
when perimeter is available (which indicates 2D data).
|
|
88
|
+
Aspect ratio is 2D-only and only computed when major/minor axis lengths
|
|
89
|
+
are present in the result.
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
# Get area/volume (whichever exists after dimensionality-based renaming)
|
|
93
|
+
if "morph_area" in result:
|
|
94
|
+
area_or_volume = result["morph_area"]
|
|
95
|
+
elif "morph_volume" in result:
|
|
96
|
+
area_or_volume = result["morph_volume"]
|
|
97
|
+
else:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Circularity is 2D-only: 4*pi*area / perimeter^2
|
|
101
|
+
if "morph_perimeter" in result:
|
|
102
|
+
perim = result["morph_perimeter"]
|
|
103
|
+
circularity = np.divide(
|
|
104
|
+
4 * np.pi * area_or_volume,
|
|
105
|
+
perim ** 2,
|
|
106
|
+
out=np.full_like(area_or_volume, np.nan),
|
|
107
|
+
where=perim != 0,
|
|
108
|
+
)
|
|
109
|
+
result["morph_circularity"] = circularity
|
|
110
|
+
|
|
111
|
+
# Aspect ratio: only computed if major/minor axis lengths are present (2D only)
|
|
112
|
+
if (
|
|
113
|
+
"morph_major_axis_length" in result
|
|
114
|
+
and "morph_minor_axis_length" in result
|
|
115
|
+
):
|
|
116
|
+
major = result["morph_major_axis_length"]
|
|
117
|
+
minor = result["morph_minor_axis_length"]
|
|
118
|
+
aspect_ratio = np.divide(
|
|
119
|
+
major,
|
|
120
|
+
minor,
|
|
121
|
+
out=np.full_like(major, np.nan),
|
|
122
|
+
where=minor != 0,
|
|
123
|
+
)
|
|
124
|
+
result["morph_aspect_ratio"] = aspect_ratio
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _compute_physical_area(
|
|
128
|
+
result: dict[str, np.ndarray],
|
|
129
|
+
pixel_sizes: np.ndarray,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Add physical area/volume measurements to result in-place.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
result : dict of str to numpy.ndarray
|
|
136
|
+
Morphological properties to augment.
|
|
137
|
+
pixel_sizes : numpy.ndarray
|
|
138
|
+
Per-axis pixel sizes in micrometers.
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
# Determine if we have area (2D) or volume (3D)
|
|
142
|
+
if "morph_area" in result:
|
|
143
|
+
area_or_volume = result["morph_area"]
|
|
144
|
+
is_volume = False
|
|
145
|
+
elif "morph_volume" in result:
|
|
146
|
+
area_or_volume = result["morph_volume"]
|
|
147
|
+
is_volume = True
|
|
148
|
+
else:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
pixels = area_or_volume
|
|
152
|
+
ndim = len(pixel_sizes)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
if ndim == NDIM_2D and not is_volume:
|
|
156
|
+
area_phys = pixels * (pixel_sizes[0] * pixel_sizes[1])
|
|
157
|
+
result["morph_area_um2"] = area_phys
|
|
158
|
+
elif ndim == NDIM_3D and is_volume:
|
|
159
|
+
volume_phys = pixels * (
|
|
160
|
+
pixel_sizes[0] * pixel_sizes[1] * pixel_sizes[2]
|
|
161
|
+
)
|
|
162
|
+
result["morph_volume_um3"] = volume_phys
|
|
163
|
+
except (TypeError, ValueError):
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def extract_morphology(
|
|
168
|
+
labels: np.ndarray,
|
|
169
|
+
label_ids: np.ndarray,
|
|
170
|
+
pixel_sizes: np.ndarray | None = None,
|
|
171
|
+
) -> dict[str, np.ndarray]:
|
|
172
|
+
"""Extract morphological descriptors for each labeled region.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
labels : numpy.ndarray
|
|
177
|
+
Label image with integer ids.
|
|
178
|
+
label_ids : numpy.ndarray
|
|
179
|
+
Specific label ids to extract properties for.
|
|
180
|
+
pixel_sizes : numpy.ndarray or None, optional
|
|
181
|
+
Per-axis pixel sizes in micrometers. When provided, physical
|
|
182
|
+
measurements are computed.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
dict of str to numpy.ndarray
|
|
187
|
+
Morphological descriptors. Keys are property names and values are
|
|
188
|
+
arrays with one entry per label id.
|
|
189
|
+
|
|
190
|
+
Notes
|
|
191
|
+
-----
|
|
192
|
+
Properties that depend on regionprops (e.g., eccentricity, solidity)
|
|
193
|
+
are only available for 2D images. For 3D, only simple properties like
|
|
194
|
+
volume are available.
|
|
195
|
+
|
|
196
|
+
The "area" property is renamed to "volume" for 3D images.
|
|
197
|
+
|
|
198
|
+
Some properties may not be available depending on the scikit-image
|
|
199
|
+
version. Missing properties are silently skipped.
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
# For 3D, some properties are not available. Try with all properties first,
|
|
203
|
+
# and fall back to basic properties if it fails.
|
|
204
|
+
try:
|
|
205
|
+
props = regionprops_table(
|
|
206
|
+
labels,
|
|
207
|
+
properties=MORPHOLOGY_PROPERTIES,
|
|
208
|
+
)
|
|
209
|
+
except (ValueError, RuntimeError):
|
|
210
|
+
# Fall back to basic properties for 3D
|
|
211
|
+
try:
|
|
212
|
+
props = regionprops_table(
|
|
213
|
+
labels,
|
|
214
|
+
properties=("area",),
|
|
215
|
+
)
|
|
216
|
+
except (ValueError, RuntimeError) as exc:
|
|
217
|
+
warnings.warn(
|
|
218
|
+
f"Failed to extract morphological properties: {exc}",
|
|
219
|
+
RuntimeWarning,
|
|
220
|
+
stacklevel=2,
|
|
221
|
+
)
|
|
222
|
+
return {}
|
|
223
|
+
|
|
224
|
+
result = _collect_simple_properties(props, label_ids)
|
|
225
|
+
|
|
226
|
+
# For 3D images, rename "area" to "volume"
|
|
227
|
+
if labels.ndim == NDIM_3D and "morph_area" in result:
|
|
228
|
+
result["morph_volume"] = result.pop("morph_area")
|
|
229
|
+
|
|
230
|
+
_compute_derived_metrics(result)
|
|
231
|
+
|
|
232
|
+
if pixel_sizes is not None:
|
|
233
|
+
_compute_physical_area(result, pixel_sizes)
|
|
234
|
+
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def add_morphology_columns(
|
|
239
|
+
rows: list[dict],
|
|
240
|
+
labels: np.ndarray,
|
|
241
|
+
label_ids: np.ndarray,
|
|
242
|
+
pixel_sizes: np.ndarray | None = None,
|
|
243
|
+
) -> list[str]:
|
|
244
|
+
"""Add morphological descriptor columns to output rows.
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
rows : list of dict
|
|
249
|
+
Output row dictionaries to update in-place.
|
|
250
|
+
labels : numpy.ndarray
|
|
251
|
+
Label image with integer ids.
|
|
252
|
+
label_ids : numpy.ndarray
|
|
253
|
+
Label ids corresponding to the output rows.
|
|
254
|
+
pixel_sizes : numpy.ndarray or None, optional
|
|
255
|
+
Per-axis pixel sizes in micrometers.
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
list of str
|
|
260
|
+
List of column names added to the rows.
|
|
261
|
+
|
|
262
|
+
Notes
|
|
263
|
+
-----
|
|
264
|
+
This function modifies ``rows`` in-place and returns the list of new
|
|
265
|
+
column names that were added for header generation.
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
morphology_data = extract_morphology(labels, label_ids, pixel_sizes)
|
|
269
|
+
|
|
270
|
+
if not morphology_data or not rows:
|
|
271
|
+
return []
|
|
272
|
+
|
|
273
|
+
column_names: list[str] = []
|
|
274
|
+
for col_name, col_values in morphology_data.items():
|
|
275
|
+
column_names.append(col_name)
|
|
276
|
+
for row, value in zip(rows, col_values, strict=True):
|
|
277
|
+
row[col_name] = float(value) if not np.isnan(value) else value
|
|
278
|
+
|
|
279
|
+
return column_names
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Spots channels dialog rows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from qtpy.QtWidgets import (
|
|
8
|
+
QFormLayout,
|
|
9
|
+
QGroupBox,
|
|
10
|
+
QLineEdit,
|
|
11
|
+
QPushButton,
|
|
12
|
+
QSizePolicy,
|
|
13
|
+
QVBoxLayout,
|
|
14
|
+
QWidget,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from ..base import RefreshingComboBox
|
|
18
|
+
from .config import SpotsChannelConfig, SpotsSegmentationConfig
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from .dialog import SpotsChannelsDialog
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SpotsSegmentationRow(QGroupBox):
|
|
25
|
+
"""Segmentation row widget for spots segmentation filters."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self, dialog: SpotsChannelsDialog, data: SpotsSegmentationConfig
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Initialize a segmentation row widget.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
dialog : SpotsChannelsDialog
|
|
35
|
+
Parent dialog instance.
|
|
36
|
+
data : SpotsSegmentationConfig
|
|
37
|
+
Segmentation configuration data.
|
|
38
|
+
"""
|
|
39
|
+
super().__init__()
|
|
40
|
+
self._dialog = dialog
|
|
41
|
+
self._tab = dialog._tab
|
|
42
|
+
self.data = data
|
|
43
|
+
|
|
44
|
+
self.setFlat(True)
|
|
45
|
+
self.setStyleSheet(
|
|
46
|
+
"QGroupBox {"
|
|
47
|
+
" margin-top: 6px;"
|
|
48
|
+
"}"
|
|
49
|
+
"QGroupBox::title {"
|
|
50
|
+
" subcontrol-origin: margin;"
|
|
51
|
+
" subcontrol-position: top left;"
|
|
52
|
+
" padding: 0 6px;"
|
|
53
|
+
"}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
layout = QVBoxLayout()
|
|
57
|
+
layout.setContentsMargins(8, 8, 8, 8)
|
|
58
|
+
layout.setSpacing(6)
|
|
59
|
+
|
|
60
|
+
form_layout = QFormLayout()
|
|
61
|
+
form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
62
|
+
labels_combo = RefreshingComboBox(
|
|
63
|
+
refresh_callback=lambda combo_ref=None: self._dialog._refresh_labels_combo(
|
|
64
|
+
labels_combo, filter_type="cellular"
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
self._tab._configure_combo(labels_combo)
|
|
68
|
+
labels_combo.currentTextChanged.connect(
|
|
69
|
+
lambda text: self._set_data("label", text)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
form_layout.addRow("Segmentation", labels_combo)
|
|
73
|
+
layout.addLayout(form_layout)
|
|
74
|
+
|
|
75
|
+
delete_button = QPushButton("Delete")
|
|
76
|
+
delete_button.clicked.connect(
|
|
77
|
+
lambda: self._dialog._remove_segmentation(self)
|
|
78
|
+
)
|
|
79
|
+
layout.addWidget(delete_button)
|
|
80
|
+
|
|
81
|
+
self._labels_combo = labels_combo
|
|
82
|
+
self.setLayout(layout)
|
|
83
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
84
|
+
self._restore_state()
|
|
85
|
+
|
|
86
|
+
def update_title(self, index: int) -> None:
|
|
87
|
+
"""Update the title label for the segmentation row.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
index : int
|
|
92
|
+
0-based index used in the title.
|
|
93
|
+
"""
|
|
94
|
+
self.setTitle(f"Segmentation {index}")
|
|
95
|
+
|
|
96
|
+
def _set_data(self, key: str, value) -> None:
|
|
97
|
+
"""Update the segmentation data model.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
key : str
|
|
102
|
+
Data field name to update.
|
|
103
|
+
value : object
|
|
104
|
+
Value to assign to the field.
|
|
105
|
+
"""
|
|
106
|
+
setattr(self.data, key, value)
|
|
107
|
+
|
|
108
|
+
def _restore_state(self) -> None:
|
|
109
|
+
"""Restore UI state from stored segmentation data.
|
|
110
|
+
|
|
111
|
+
Notes
|
|
112
|
+
-----
|
|
113
|
+
This sets the labels combo to the stored label name when available.
|
|
114
|
+
"""
|
|
115
|
+
label_name = self.data.label
|
|
116
|
+
if label_name:
|
|
117
|
+
self._labels_combo.setCurrentText(label_name)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class SpotsChannelRow(QGroupBox):
|
|
122
|
+
"""Channel row widget for spots feature channels."""
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self, dialog: SpotsChannelsDialog, data: SpotsChannelConfig
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Initialize a channel row widget.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
dialog : SpotsChannelsDialog
|
|
132
|
+
Parent dialog instance.
|
|
133
|
+
data : SpotsChannelConfig
|
|
134
|
+
Channel configuration data.
|
|
135
|
+
"""
|
|
136
|
+
super().__init__()
|
|
137
|
+
self._dialog = dialog
|
|
138
|
+
self._tab = dialog._tab
|
|
139
|
+
self.data = data
|
|
140
|
+
|
|
141
|
+
self.setFlat(True)
|
|
142
|
+
self.setStyleSheet(
|
|
143
|
+
"QGroupBox {"
|
|
144
|
+
" margin-top: 6px;"
|
|
145
|
+
"}"
|
|
146
|
+
"QGroupBox::title {"
|
|
147
|
+
" subcontrol-origin: margin;"
|
|
148
|
+
" subcontrol-position: top left;"
|
|
149
|
+
" padding: 0 6px;"
|
|
150
|
+
"}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
layout = QVBoxLayout()
|
|
154
|
+
layout.setContentsMargins(8, 8, 8, 8)
|
|
155
|
+
layout.setSpacing(6)
|
|
156
|
+
|
|
157
|
+
channel_form = QFormLayout()
|
|
158
|
+
channel_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
159
|
+
name_input = QLineEdit()
|
|
160
|
+
name_input.setPlaceholderText("Channel name")
|
|
161
|
+
name_input.setMinimumWidth(160)
|
|
162
|
+
name_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
163
|
+
name_input.textChanged.connect(
|
|
164
|
+
lambda text: self._set_data("name", text)
|
|
165
|
+
)
|
|
166
|
+
channel_combo = RefreshingComboBox(
|
|
167
|
+
refresh_callback=lambda combo_ref=None: self._dialog._refresh_image_combo(
|
|
168
|
+
channel_combo
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
self._tab._configure_combo(channel_combo)
|
|
172
|
+
channel_combo.currentTextChanged.connect(
|
|
173
|
+
lambda text: self._set_data("channel", text)
|
|
174
|
+
)
|
|
175
|
+
segmentation_combo = RefreshingComboBox(
|
|
176
|
+
refresh_callback=lambda combo_ref=None: self._dialog._refresh_labels_combo(
|
|
177
|
+
segmentation_combo, filter_type="spots"
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
self._tab._configure_combo(segmentation_combo)
|
|
181
|
+
segmentation_combo.currentTextChanged.connect(
|
|
182
|
+
lambda text: self._set_data("spots_segmentation", text)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
channel_form.addRow("Name", name_input)
|
|
186
|
+
channel_form.addRow("Channel", channel_combo)
|
|
187
|
+
channel_form.addRow("Spots segmentation", segmentation_combo)
|
|
188
|
+
layout.addLayout(channel_form)
|
|
189
|
+
|
|
190
|
+
delete_button = QPushButton("Delete")
|
|
191
|
+
delete_button.clicked.connect(lambda: self._dialog._remove_channel(self))
|
|
192
|
+
layout.addWidget(delete_button)
|
|
193
|
+
|
|
194
|
+
self.setLayout(layout)
|
|
195
|
+
|
|
196
|
+
self._channel_combo = channel_combo
|
|
197
|
+
self._name_input = name_input
|
|
198
|
+
self._segmentation_combo = segmentation_combo
|
|
199
|
+
|
|
200
|
+
self._restore_state()
|
|
201
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
202
|
+
|
|
203
|
+
def update_title(self, index: int) -> None:
|
|
204
|
+
"""Update the title label for the channel row.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
index : int
|
|
209
|
+
0-based index used in the title.
|
|
210
|
+
"""
|
|
211
|
+
self.setTitle(f"Channel {index}")
|
|
212
|
+
|
|
213
|
+
def _set_data(self, key: str, value) -> None:
|
|
214
|
+
"""Update the channel data model.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
key : str
|
|
219
|
+
Data key to update.
|
|
220
|
+
value : object
|
|
221
|
+
New value to store.
|
|
222
|
+
"""
|
|
223
|
+
setattr(self.data, key, value)
|
|
224
|
+
|
|
225
|
+
def _restore_state(self) -> None:
|
|
226
|
+
"""Restore UI state from stored channel data.
|
|
227
|
+
|
|
228
|
+
Notes
|
|
229
|
+
-----
|
|
230
|
+
Populates name, channel, and segmentation combos when values are
|
|
231
|
+
present in the configuration.
|
|
232
|
+
"""
|
|
233
|
+
channel_label = self.data.name
|
|
234
|
+
if channel_label:
|
|
235
|
+
self._name_input.setText(channel_label)
|
|
236
|
+
channel_name = self.data.channel
|
|
237
|
+
if channel_name:
|
|
238
|
+
self._channel_combo.setCurrentText(channel_name)
|
|
239
|
+
segmentation_name = self.data.spots_segmentation
|
|
240
|
+
if segmentation_name:
|
|
241
|
+
self._segmentation_combo.setCurrentText(segmentation_name)
|