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,71 @@
|
|
|
1
|
+
"""Hugging Face model download utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from huggingface_hub import hf_hub_download # type: ignore[import-not-found]
|
|
10
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
11
|
+
hf_hub_download = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _resolve_repo_id(default_repo: str) -> str:
|
|
15
|
+
"""Resolve the model repository ID from environment or default."""
|
|
16
|
+
env_repo = os.environ.get("SENOQUANT_MODEL_REPO")
|
|
17
|
+
if env_repo:
|
|
18
|
+
return env_repo.strip()
|
|
19
|
+
return default_repo
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
DEFAULT_REPO_ID = "HaamsRee/senoquant-models"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ensure_hf_model(
|
|
26
|
+
filename: str,
|
|
27
|
+
target_dir: Path,
|
|
28
|
+
*,
|
|
29
|
+
repo_id: str,
|
|
30
|
+
revision: str | None = None,
|
|
31
|
+
) -> Path:
|
|
32
|
+
"""Ensure a model file exists, downloading from HF if needed.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
filename : str
|
|
37
|
+
File name to download from the HF repo.
|
|
38
|
+
target_dir : pathlib.Path
|
|
39
|
+
Local directory for the model file.
|
|
40
|
+
repo_id : str
|
|
41
|
+
Hugging Face repo id, e.g. "HaamsRee/senoquant-models".
|
|
42
|
+
revision : str or None, optional
|
|
43
|
+
Optional revision/tag/commit to pin.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
pathlib.Path
|
|
48
|
+
Local path to the downloaded model file.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
target_dir = Path(target_dir)
|
|
52
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
candidate = target_dir / filename
|
|
54
|
+
if candidate.exists():
|
|
55
|
+
return candidate
|
|
56
|
+
|
|
57
|
+
if hf_hub_download is None:
|
|
58
|
+
message = (
|
|
59
|
+
"huggingface_hub is required to download models. "
|
|
60
|
+
"Install it with `pip install huggingface_hub`."
|
|
61
|
+
)
|
|
62
|
+
raise RuntimeError(message)
|
|
63
|
+
|
|
64
|
+
resolved_repo = _resolve_repo_id(repo_id)
|
|
65
|
+
path = hf_hub_download(
|
|
66
|
+
repo_id=resolved_repo,
|
|
67
|
+
filename=filename,
|
|
68
|
+
revision=revision,
|
|
69
|
+
local_dir=str(target_dir),
|
|
70
|
+
)
|
|
71
|
+
return Path(path)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Nuclear dilation segmentation model."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nuclear_dilation",
|
|
3
|
+
"description": "Dilates nuclear masks to approximate cytoplasm",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"order": 4,
|
|
6
|
+
"tasks": {
|
|
7
|
+
"nuclear": {
|
|
8
|
+
"supported": false
|
|
9
|
+
},
|
|
10
|
+
"cytoplasmic": {
|
|
11
|
+
"supported": true,
|
|
12
|
+
"input_modes": ["nuclear"],
|
|
13
|
+
"nuclear_channel_optional": false
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"settings": [
|
|
17
|
+
{
|
|
18
|
+
"key": "dilation_iterations",
|
|
19
|
+
"label": "Dilation iterations",
|
|
20
|
+
"type": "int",
|
|
21
|
+
"min": 1,
|
|
22
|
+
"max": 100,
|
|
23
|
+
"default": 5
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Nuclear dilation cytoplasmic segmentation model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy import ndimage as ndi
|
|
9
|
+
|
|
10
|
+
from senoquant.tabs.segmentation.models.base import SenoQuantSegmentationModel
|
|
11
|
+
from senoquant.utils import layer_data_asarray
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NuclearDilationModel(SenoQuantSegmentationModel):
|
|
18
|
+
"""Dilates nuclear masks to approximate cytoplasm.
|
|
19
|
+
|
|
20
|
+
This model only requires a nuclear segmentation mask and dilates it
|
|
21
|
+
to approximate cytoplasmic boundaries. It is useful when cytoplasmic
|
|
22
|
+
staining is weak or unavailable.
|
|
23
|
+
|
|
24
|
+
Notes
|
|
25
|
+
-----
|
|
26
|
+
- Only supports cytoplasmic segmentation task.
|
|
27
|
+
- Requires nuclear_layer, ignores cytoplasmic_layer.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, models_root: Path | None = None) -> None:
|
|
32
|
+
"""Initialize the nuclear dilation model wrapper.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
models_root : pathlib.Path or None
|
|
37
|
+
Optional root directory for model storage.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
super().__init__("nuclear_dilation", models_root=models_root)
|
|
41
|
+
|
|
42
|
+
def run(self, **kwargs: object) -> dict:
|
|
43
|
+
"""Run nuclear dilation for cytoplasmic segmentation.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
**kwargs
|
|
48
|
+
task : str
|
|
49
|
+
Must be "cytoplasmic" for this model.
|
|
50
|
+
nuclear_layer : napari.layers.Labels
|
|
51
|
+
Nuclear segmentation mask layer.
|
|
52
|
+
cytoplasmic_layer : napari.layers.Image or None
|
|
53
|
+
Ignored by this model.
|
|
54
|
+
settings : dict
|
|
55
|
+
Model settings keyed by ``details.json``.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
dict
|
|
60
|
+
Dictionary with:
|
|
61
|
+
- ``masks``: dilated nuclear label image
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
task = kwargs.get("task")
|
|
65
|
+
if task != "cytoplasmic":
|
|
66
|
+
msg = "Nuclear dilation only supports cytoplasmic segmentation."
|
|
67
|
+
raise ValueError(msg)
|
|
68
|
+
|
|
69
|
+
nuclear_layer = kwargs.get("nuclear_layer")
|
|
70
|
+
settings = kwargs.get("settings", {})
|
|
71
|
+
|
|
72
|
+
if nuclear_layer is None:
|
|
73
|
+
msg = "Nuclear layer is required for nuclear dilation."
|
|
74
|
+
raise ValueError(msg)
|
|
75
|
+
|
|
76
|
+
nuclear_data = layer_data_asarray(nuclear_layer)
|
|
77
|
+
if nuclear_data is None:
|
|
78
|
+
msg = "Failed to read nuclear layer data."
|
|
79
|
+
raise ValueError(msg)
|
|
80
|
+
|
|
81
|
+
nuclear_data = nuclear_data.astype(np.uint32, copy=False)
|
|
82
|
+
settings_dict = {} if not isinstance(settings, dict) else settings
|
|
83
|
+
dilation_iterations = max(int(settings_dict.get("dilation_iterations", 5)), 1)
|
|
84
|
+
|
|
85
|
+
dilated_labels = np.zeros_like(nuclear_data)
|
|
86
|
+
for label_id in np.unique(nuclear_data):
|
|
87
|
+
if label_id == 0:
|
|
88
|
+
continue
|
|
89
|
+
mask = nuclear_data == label_id
|
|
90
|
+
dilated_mask = ndi.binary_dilation(
|
|
91
|
+
mask,
|
|
92
|
+
iterations=dilation_iterations,
|
|
93
|
+
)
|
|
94
|
+
dilated_labels[dilated_mask] = label_id
|
|
95
|
+
|
|
96
|
+
return {"masks": dilated_labels}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Perinuclear rings cytoplasmic segmentation model."""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "perinuclear_rings",
|
|
3
|
+
"description": "Creates perinuclear rings from nuclear masks",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"order": 5,
|
|
6
|
+
"tasks": {
|
|
7
|
+
"nuclear": {
|
|
8
|
+
"supported": false
|
|
9
|
+
},
|
|
10
|
+
"cytoplasmic": {
|
|
11
|
+
"supported": true,
|
|
12
|
+
"input_modes": ["nuclear"],
|
|
13
|
+
"nuclear_channel_optional": false
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"settings": [
|
|
17
|
+
{
|
|
18
|
+
"key": "erosion_px",
|
|
19
|
+
"label": "Inner erosion (px)",
|
|
20
|
+
"type": "int",
|
|
21
|
+
"min": 1,
|
|
22
|
+
"max": 50,
|
|
23
|
+
"default": 2
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"key": "dilation_px",
|
|
27
|
+
"label": "Outer dilation (px)",
|
|
28
|
+
"type": "int",
|
|
29
|
+
"min": 0,
|
|
30
|
+
"max": 50,
|
|
31
|
+
"default": 5
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Perinuclear rings cytoplasmic segmentation model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy import ndimage as ndi
|
|
9
|
+
|
|
10
|
+
from senoquant.tabs.segmentation.models.base import SenoQuantSegmentationModel
|
|
11
|
+
from senoquant.utils import layer_data_asarray
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PerinuclearRingsModel(SenoQuantSegmentationModel):
|
|
18
|
+
"""Creates perinuclear rings from nuclear masks.
|
|
19
|
+
|
|
20
|
+
This model generates ring-shaped labels around nuclei by eroding the
|
|
21
|
+
nuclear mask inward and dilating it outward, then subtracting the
|
|
22
|
+
eroded mask from the dilated mask. This is useful for detecting
|
|
23
|
+
perinuclear markers that localize to the region immediately
|
|
24
|
+
surrounding the nucleus.
|
|
25
|
+
|
|
26
|
+
The erosion parameter has a minimum of 1 pixel to ensure that the
|
|
27
|
+
resulting rings maintain at least 1 pixel overlap with the original
|
|
28
|
+
nuclear labels, which is required for label relationship logic in
|
|
29
|
+
quantification and batch processing.
|
|
30
|
+
|
|
31
|
+
Notes
|
|
32
|
+
-----
|
|
33
|
+
- Only supports cytoplasmic segmentation task.
|
|
34
|
+
- Requires nuclear_layer, ignores cytoplasmic_layer.
|
|
35
|
+
- Maintains label IDs from the original nuclear segmentation.
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, models_root: Path | None = None) -> None:
|
|
40
|
+
"""Initialize the perinuclear rings model wrapper.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
models_root : pathlib.Path or None
|
|
45
|
+
Optional root directory for model storage.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
super().__init__("perinuclear_rings", models_root=models_root)
|
|
49
|
+
|
|
50
|
+
def run(self, **kwargs: object) -> dict:
|
|
51
|
+
"""Generate perinuclear rings from nuclear segmentation.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
**kwargs
|
|
56
|
+
task : str
|
|
57
|
+
Must be "cytoplasmic" for this model.
|
|
58
|
+
nuclear_layer : napari.layers.Labels
|
|
59
|
+
Nuclear segmentation mask layer.
|
|
60
|
+
cytoplasmic_layer : napari.layers.Image or None
|
|
61
|
+
Ignored by this model.
|
|
62
|
+
settings : dict
|
|
63
|
+
Model settings keyed by ``details.json``:
|
|
64
|
+
- ``erosion_px``: pixels to erode inward (min 1)
|
|
65
|
+
- ``dilation_px``: pixels to dilate outward
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
dict
|
|
70
|
+
Dictionary with:
|
|
71
|
+
- ``masks``: perinuclear ring label image
|
|
72
|
+
|
|
73
|
+
Raises
|
|
74
|
+
------
|
|
75
|
+
ValueError
|
|
76
|
+
If task is not "cytoplasmic" or nuclear_layer is missing.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
task = kwargs.get("task")
|
|
80
|
+
if task != "cytoplasmic":
|
|
81
|
+
msg = "Perinuclear rings only supports cytoplasmic segmentation."
|
|
82
|
+
raise ValueError(msg)
|
|
83
|
+
|
|
84
|
+
nuclear_layer = kwargs.get("nuclear_layer")
|
|
85
|
+
settings = kwargs.get("settings", {})
|
|
86
|
+
|
|
87
|
+
if nuclear_layer is None:
|
|
88
|
+
msg = "Nuclear layer is required for perinuclear rings."
|
|
89
|
+
raise ValueError(msg)
|
|
90
|
+
|
|
91
|
+
nuclear_data = layer_data_asarray(nuclear_layer)
|
|
92
|
+
if nuclear_data is None:
|
|
93
|
+
msg = "Failed to read nuclear layer data."
|
|
94
|
+
raise ValueError(msg)
|
|
95
|
+
|
|
96
|
+
nuclear_data = nuclear_data.astype(np.uint32, copy=False)
|
|
97
|
+
settings_dict = {} if not isinstance(settings, dict) else settings
|
|
98
|
+
|
|
99
|
+
# Ensure erosion is at least 1 pixel for label relationship logic
|
|
100
|
+
erosion_px = max(int(settings_dict.get("erosion_px", 2)), 1)
|
|
101
|
+
dilation_px = max(int(settings_dict.get("dilation_px", 5)), 0)
|
|
102
|
+
|
|
103
|
+
ring_labels = np.zeros_like(nuclear_data)
|
|
104
|
+
|
|
105
|
+
# Process each nucleus individually to maintain label relationships
|
|
106
|
+
for label_id in np.unique(nuclear_data):
|
|
107
|
+
if label_id == 0:
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Create binary mask for this nucleus
|
|
111
|
+
nucleus_mask = nuclear_data == label_id
|
|
112
|
+
|
|
113
|
+
# Erode inward (minimum 1 px to maintain overlap)
|
|
114
|
+
eroded_mask = ndi.binary_erosion(
|
|
115
|
+
nucleus_mask,
|
|
116
|
+
iterations=erosion_px,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Dilate outward
|
|
120
|
+
dilated_mask = ndi.binary_dilation(
|
|
121
|
+
nucleus_mask,
|
|
122
|
+
iterations=dilation_px,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Ring is the difference: dilated - eroded
|
|
126
|
+
# This creates a ring that includes the original boundary
|
|
127
|
+
ring_mask = dilated_mask & ~eroded_mask
|
|
128
|
+
|
|
129
|
+
# Assign the original label ID to the ring
|
|
130
|
+
ring_labels[ring_mask] = label_id
|
|
131
|
+
|
|
132
|
+
return {"masks": ring_labels}
|