scikit-ops 0.1.0.dev0__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.
- scikit_ops-0.1.0.dev0.dist-info/METADATA +372 -0
- scikit_ops-0.1.0.dev0.dist-info/RECORD +66 -0
- scikit_ops-0.1.0.dev0.dist-info/WHEEL +4 -0
- scikit_ops-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- skop/__init__.py +14 -0
- skop/envs/cellpose3/pixi.toml +46 -0
- skop/envs/cupy/pixi.toml +35 -0
- skop/envs/minimal/pixi.toml +25 -0
- skop/envs/pytorch/pixi.toml +88 -0
- skop/envs/sdeconv/pixi.toml +85 -0
- skop/envs/segment-everything/pixi.toml +96 -0
- skop/envs/skimage/pixi.toml +28 -0
- skop/envs/stardist-tf/pixi.toml +91 -0
- skop/envs/unseg-cv/pixi.toml +27 -0
- skop/models.py +145 -0
- skop/ops/__init__.py +19 -0
- skop/ops/_util.py +115 -0
- skop/ops/deconvolve/__init__.py +18 -0
- skop/ops/deconvolve/_edges.py +92 -0
- skop/ops/deconvolve/_pad.py +117 -0
- skop/ops/deconvolve/richardson_lucy.py +100 -0
- skop/ops/deconvolve/richardson_lucy_cupy.py +116 -0
- skop/ops/detect/__init__.py +28 -0
- skop/ops/detect/_result.py +45 -0
- skop/ops/detect/fastsam.py +103 -0
- skop/ops/detect/jdll_yolo.py +115 -0
- skop/ops/detect/object_aware_yolo.py +112 -0
- skop/ops/detect/yolo.py +441 -0
- skop/ops/edges.py +382 -0
- skop/ops/generate.py +77 -0
- skop/ops/kernels/__init__.py +39 -0
- skop/ops/kernels/_fluorophore.py +42 -0
- skop/ops/kernels/_recenter.py +60 -0
- skop/ops/kernels/gibson_lanni.py +147 -0
- skop/ops/kernels/paraxial.py +128 -0
- skop/ops/kernels/psf.py +59 -0
- skop/ops/labels.py +144 -0
- skop/ops/mask/__init__.py +29 -0
- skop/ops/mask/_result.py +40 -0
- skop/ops/mask/microsam.py +114 -0
- skop/ops/mask/mobilesam.py +176 -0
- skop/ops/morphology.py +214 -0
- skop/ops/normalize.py +85 -0
- skop/ops/segment/__init__.py +24 -0
- skop/ops/segment/cellpose3.py +245 -0
- skop/ops/segment/cellpose4.py +127 -0
- skop/ops/segment/instanseg.py +162 -0
- skop/ops/segment/stardist2d.py +192 -0
- skop/ops/segment/starfun3d.py +104 -0
- skop/ops/segment/unseg/__init__.py +138 -0
- skop/ops/segment/unseg/_algorithm.py +1618 -0
- skop/ops/smooth.py +452 -0
- skop/ops/threshold.py +278 -0
- skop/ops/toy.py +114 -0
- skop/ops/train/__init__.py +81 -0
- skop/ops/train/_util.py +152 -0
- skop/ops/train/cellpose3.py +189 -0
- skop/ops/train/cellpose4.py +164 -0
- skop/ops/train/stardist2d.py +439 -0
- skop/ops/workflows/__init__.py +43 -0
- skop/ops/workflows/deconvolve/__init__.py +7 -0
- skop/ops/workflows/deconvolve/with_psf.py +103 -0
- skop/ops/workflows/mask/__init__.py +7 -0
- skop/ops/workflows/mask/detect_then_mask.py +97 -0
- skop/ops/workflows/segment/__init__.py +7 -0
- skop/ops/workflows/segment/connect_2d_in_3d.py +145 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""StarDist 2D inference, pretrained or from a model on disk.
|
|
2
|
+
|
|
3
|
+
Ported from src/imgops/implementations/stardist2d.py. Shares the
|
|
4
|
+
'stardist-tf' environment with starfun3d.py.
|
|
5
|
+
|
|
6
|
+
Two ops for the pretrained models rather than one with a ``model`` switch,
|
|
7
|
+
because they do not accept the same thing: the H&E model is trained on stain
|
|
8
|
+
colour and always wants a channel axis, while the fluorescence model wants a
|
|
9
|
+
single channel and collapses one if it is given it. That is a difference in
|
|
10
|
+
what the op *consumes*, so it belongs in ``Axes`` where a front end can see
|
|
11
|
+
it, not in an enum parameter where it is invisible until StarDist raises.
|
|
12
|
+
|
|
13
|
+
``stardist2d_custom`` is the third, and takes a directory. It cannot make the
|
|
14
|
+
same declaration -- a model on disk is single- or multi-channel depending on
|
|
15
|
+
how it was trained -- so it declares ``c?`` and reads the model's own
|
|
16
|
+
``config.json`` to decide. A caller with many trained models points this one
|
|
17
|
+
op at each of them in turn; see design 0011.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Annotated
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from opspec.op import Axes, op
|
|
27
|
+
from opspec.runtime import progress
|
|
28
|
+
from opspec.types import ImageOf, LabelsOf
|
|
29
|
+
|
|
30
|
+
from .._util import to_gray
|
|
31
|
+
|
|
32
|
+
_FLUO = "2D_versatile_fluo"
|
|
33
|
+
_HE = "2D_versatile_he"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@op(env="stardist-tf")
|
|
37
|
+
def stardist2d_fluo(
|
|
38
|
+
image: Annotated[ImageOf[np.ndarray], Axes("y", "x", "c?")],
|
|
39
|
+
prob_thresh: Annotated[
|
|
40
|
+
float,
|
|
41
|
+
{"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
|
|
42
|
+
] = 0.5,
|
|
43
|
+
nms_thresh: Annotated[
|
|
44
|
+
float,
|
|
45
|
+
{"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
|
|
46
|
+
] = 0.4,
|
|
47
|
+
normalize: bool = True,
|
|
48
|
+
) -> LabelsOf[np.ndarray]:
|
|
49
|
+
"""Detect objects in a fluorescence image with pretrained StarDist.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
image: Plane to segment. A trailing RGB(A) axis is collapsed, since
|
|
53
|
+
this model wants a single channel.
|
|
54
|
+
prob_thresh: Object probability threshold.
|
|
55
|
+
nms_thresh: Non-maximum suppression threshold.
|
|
56
|
+
normalize: Whether to percentile-normalize the input first.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
A label image, one integer per detected object.
|
|
60
|
+
|
|
61
|
+
Note: the model weights are downloaded on first use.
|
|
62
|
+
"""
|
|
63
|
+
return _predict(to_gray(image), _FLUO, prob_thresh, nms_thresh, normalize)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@op(env="stardist-tf")
|
|
67
|
+
def stardist2d_he(
|
|
68
|
+
image: Annotated[ImageOf[np.ndarray], Axes("y", "x", "c")],
|
|
69
|
+
prob_thresh: Annotated[
|
|
70
|
+
float,
|
|
71
|
+
{"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
|
|
72
|
+
] = 0.5,
|
|
73
|
+
nms_thresh: Annotated[
|
|
74
|
+
float,
|
|
75
|
+
{"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
|
|
76
|
+
] = 0.4,
|
|
77
|
+
normalize: bool = True,
|
|
78
|
+
) -> LabelsOf[np.ndarray]:
|
|
79
|
+
"""Detect objects in an H&E-stained image with pretrained StarDist.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
image: Plane to segment, with its colour channels intact -- this model
|
|
83
|
+
is trained on stain colour, so the channel axis is required.
|
|
84
|
+
prob_thresh: Object probability threshold.
|
|
85
|
+
nms_thresh: Non-maximum suppression threshold.
|
|
86
|
+
normalize: Whether to percentile-normalize the input first.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
A label image, one integer per detected object.
|
|
90
|
+
|
|
91
|
+
Note: the model weights are downloaded on first use.
|
|
92
|
+
"""
|
|
93
|
+
return _predict(image, _HE, prob_thresh, nms_thresh, normalize)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@op(env="stardist-tf")
|
|
97
|
+
def stardist2d_custom(
|
|
98
|
+
image: Annotated[ImageOf[np.ndarray], Axes("y", "x", "c?")],
|
|
99
|
+
model_dir: Annotated[str, {"widget_type": "FileEdit", "mode": "d"}] = "",
|
|
100
|
+
prob_thresh: Annotated[
|
|
101
|
+
float,
|
|
102
|
+
{"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
|
|
103
|
+
] = 0.5,
|
|
104
|
+
nms_thresh: Annotated[
|
|
105
|
+
float,
|
|
106
|
+
{"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
|
|
107
|
+
] = 0.4,
|
|
108
|
+
normalize: bool = True,
|
|
109
|
+
) -> LabelsOf[np.ndarray]:
|
|
110
|
+
"""Detect objects with a StarDist 2D model loaded from disk.
|
|
111
|
+
|
|
112
|
+
The counterpart to the two pretrained ops: same prediction, but the model
|
|
113
|
+
comes from a directory rather than StarDist's download cache. That is what
|
|
114
|
+
a model trained by ``skop.ops.train`` -- or by anything else that writes a
|
|
115
|
+
StarDist model -- is loaded with.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
image: Plane to segment. The trailing channel axis is collapsed when
|
|
119
|
+
the model expects a single channel, and kept when it does not;
|
|
120
|
+
the model's own config decides, not the caller.
|
|
121
|
+
model_dir: Directory holding ``config.json`` and the weights. The
|
|
122
|
+
directory name is the model name, as StarDist stores it.
|
|
123
|
+
prob_thresh: Object probability threshold.
|
|
124
|
+
nms_thresh: Non-maximum suppression threshold.
|
|
125
|
+
normalize: Whether to percentile-normalize the input first.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A label image, one integer per detected object.
|
|
129
|
+
"""
|
|
130
|
+
import json
|
|
131
|
+
import os
|
|
132
|
+
|
|
133
|
+
from stardist.models import StarDist2D
|
|
134
|
+
|
|
135
|
+
if not model_dir:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
"model_dir is required -- this op loads a model from disk. Use "
|
|
138
|
+
"stardist2d_fluo or stardist2d_he for the pretrained models."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
model_dir = os.path.normpath(model_dir)
|
|
142
|
+
basedir, name = os.path.split(model_dir)
|
|
143
|
+
config_path = os.path.join(model_dir, "config.json")
|
|
144
|
+
if not os.path.isfile(config_path):
|
|
145
|
+
raise ValueError(f"No StarDist model in {model_dir}: no config.json")
|
|
146
|
+
|
|
147
|
+
with open(config_path) as f:
|
|
148
|
+
n_channel_in = json.load(f).get("n_channel_in", 1)
|
|
149
|
+
|
|
150
|
+
x = image if n_channel_in > 1 else to_gray(image)
|
|
151
|
+
|
|
152
|
+
progress(f"Loading model {name}")
|
|
153
|
+
net = StarDist2D(None, name=name, basedir=basedir)
|
|
154
|
+
|
|
155
|
+
return _run(net, x, prob_thresh, nms_thresh, normalize)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _predict(
|
|
159
|
+
x: np.ndarray,
|
|
160
|
+
model: str,
|
|
161
|
+
prob_thresh: float,
|
|
162
|
+
nms_thresh: float,
|
|
163
|
+
normalize: bool,
|
|
164
|
+
) -> np.ndarray:
|
|
165
|
+
"""Run one pretrained StarDist 2D model over an already-shaped array."""
|
|
166
|
+
from stardist.models import StarDist2D
|
|
167
|
+
|
|
168
|
+
progress(f"Loading pretrained model {model}")
|
|
169
|
+
net = StarDist2D.from_pretrained(model)
|
|
170
|
+
return _run(net, x, prob_thresh, nms_thresh, normalize)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _run(
|
|
174
|
+
net,
|
|
175
|
+
x: np.ndarray,
|
|
176
|
+
prob_thresh: float,
|
|
177
|
+
nms_thresh: float,
|
|
178
|
+
normalize: bool,
|
|
179
|
+
) -> np.ndarray:
|
|
180
|
+
"""Normalize if asked, then predict instances with an already-loaded net."""
|
|
181
|
+
from csbdeep.utils import normalize as normalize_percentile
|
|
182
|
+
|
|
183
|
+
if normalize:
|
|
184
|
+
x = normalize_percentile(x, 1, 99.8)
|
|
185
|
+
|
|
186
|
+
progress("Predicting instances")
|
|
187
|
+
labels, _ = net.predict_instances(
|
|
188
|
+
x,
|
|
189
|
+
prob_thresh=prob_thresh,
|
|
190
|
+
nms_thresh=nms_thresh,
|
|
191
|
+
)
|
|
192
|
+
return np.asarray(labels).astype(np.uint16)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""3D nuclei segmentation with pretrained StarDist models.
|
|
2
|
+
|
|
3
|
+
Ported from https://github.com/ctrueden/starfun3d. Shares the 'stardist-tf'
|
|
4
|
+
environment with stardist2d.py.
|
|
5
|
+
|
|
6
|
+
The models come from Galindo et al. (2023), "3D Nuclei Segmentation By
|
|
7
|
+
Combining GAN Based Image Synthesis and Existing 3D Manual Annotations",
|
|
8
|
+
https://doi.org/10.1101/2023.12.06.570366, published at
|
|
9
|
+
https://zenodo.org/records/10518151. They total 36 MB, so they are fetched
|
|
10
|
+
into skop's asset cache on first use rather than carried in the repository.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Annotated, NamedTuple
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from opspec.op import op
|
|
21
|
+
from opspec.runtime import progress
|
|
22
|
+
from opspec.types import ImageOf, LabelsOf, PointsOf
|
|
23
|
+
|
|
24
|
+
_ZENODO = "https://zenodo.org/records/10518151/files/model_{name}.zip?download=1"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Model(Enum):
|
|
28
|
+
"""Pretrained StarDist3D models, one per acquisition modality.
|
|
29
|
+
|
|
30
|
+
Average nucleus size in the training data, in pixels:
|
|
31
|
+
confocal [x=39, y=39, z=7], sospim [x=27, y=28, z=10],
|
|
32
|
+
spinning [x=39, y=39, z=7].
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
confocal = "confocal"
|
|
36
|
+
sospim = "sospim"
|
|
37
|
+
spinning = "spinning"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Nuclei(NamedTuple):
|
|
41
|
+
labels: LabelsOf[np.ndarray]
|
|
42
|
+
points: PointsOf[np.ndarray]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@op(env="stardist-tf")
|
|
46
|
+
def segment_nuclei(
|
|
47
|
+
image: ImageOf[np.ndarray],
|
|
48
|
+
model: Model = Model.confocal,
|
|
49
|
+
prob_thresh: Annotated[
|
|
50
|
+
float | None,
|
|
51
|
+
{"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
|
|
52
|
+
] = None,
|
|
53
|
+
nms_thresh: Annotated[
|
|
54
|
+
float | None,
|
|
55
|
+
{"widget_type": "FloatSlider", "min": 0.0, "max": 1.0, "step": 0.05},
|
|
56
|
+
] = None,
|
|
57
|
+
normalize: bool = True,
|
|
58
|
+
) -> Nuclei:
|
|
59
|
+
"""Segment nuclei in a 3D volume.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
image: Volume to segment, in (Z, Y, X) order.
|
|
63
|
+
model: Which pretrained model to use; pick the one matching how the
|
|
64
|
+
data was acquired.
|
|
65
|
+
prob_thresh: Object probability threshold. None uses the model's own.
|
|
66
|
+
nms_thresh: Non-maximum suppression threshold. None uses the model's.
|
|
67
|
+
normalize: Whether to percentile-normalize the input first.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
A label volume, and the detected nucleus centers as (N, 3) points.
|
|
71
|
+
"""
|
|
72
|
+
from csbdeep.utils import normalize as normalize_percentile
|
|
73
|
+
from stardist.models import StarDist3D
|
|
74
|
+
|
|
75
|
+
from ophelper import assets
|
|
76
|
+
|
|
77
|
+
model_dir = assets.unzip_from_url(
|
|
78
|
+
_ZENODO.format(name=model.value),
|
|
79
|
+
f"starfun3d/{model.value}",
|
|
80
|
+
marker="config.json",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
progress(f"Loading model {model.value}")
|
|
84
|
+
# NB: StarDist3D locates a model as basedir/name, so the model's own
|
|
85
|
+
# directory name must be used -- the original starfun3d passed a constant
|
|
86
|
+
# here, and so loaded the confocal model whichever model was asked for.
|
|
87
|
+
net = StarDist3D(None, name=model_dir.name, basedir=str(model_dir.parent))
|
|
88
|
+
|
|
89
|
+
x = normalize_percentile(image, 1, 99.8) if normalize else image
|
|
90
|
+
|
|
91
|
+
predict_kwargs = {}
|
|
92
|
+
if prob_thresh is not None:
|
|
93
|
+
predict_kwargs["prob_thresh"] = prob_thresh
|
|
94
|
+
if nms_thresh is not None:
|
|
95
|
+
predict_kwargs["nms_thresh"] = nms_thresh
|
|
96
|
+
|
|
97
|
+
progress("Predicting instances")
|
|
98
|
+
labels, details = net.predict_instances(x, **predict_kwargs)
|
|
99
|
+
|
|
100
|
+
points = np.asarray(details.get("points", np.zeros((0, 3))))
|
|
101
|
+
return Nuclei(
|
|
102
|
+
labels=np.asarray(labels).astype(np.uint16),
|
|
103
|
+
points=points.astype(np.int32),
|
|
104
|
+
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""UNSEG: unsupervised segmentation of nuclei and cells.
|
|
2
|
+
|
|
3
|
+
Segments tissue images carrying both a nucleus marker and a cell membrane
|
|
4
|
+
marker, without training data.
|
|
5
|
+
|
|
6
|
+
Ported from https://github.com/ctrueden/unseg-fiji. The algorithm lives in
|
|
7
|
+
_algorithm.py, which is imported inside the op body because its own imports
|
|
8
|
+
(OpenCV, scikit-learn, scikit-image) are far too heavy for discovery.
|
|
9
|
+
|
|
10
|
+
Uttam Lab. UNSEG: unsupervised segmentation of cells and their nuclei in
|
|
11
|
+
complex tissue samples.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Annotated, NamedTuple
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from opspec.op import op
|
|
22
|
+
from opspec.runtime import progress
|
|
23
|
+
from opspec.types import ImageOf, LabelsOf
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DistanceTransform(Enum):
|
|
27
|
+
"""How to compute distances when splitting touching objects."""
|
|
28
|
+
|
|
29
|
+
geodesic = "GDT"
|
|
30
|
+
euclidean = "DT"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TernaryMethod(Enum):
|
|
34
|
+
"""How to resolve the three-way foreground/background/boundary decision."""
|
|
35
|
+
|
|
36
|
+
argmax = "Argmax"
|
|
37
|
+
posterior = "Posterior"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Segmentation(NamedTuple):
|
|
41
|
+
nuclei: LabelsOf[np.ndarray]
|
|
42
|
+
cells: LabelsOf[np.ndarray]
|
|
43
|
+
n_nuclei: int
|
|
44
|
+
n_cells: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@op(env="unseg-cv")
|
|
48
|
+
def unseg(
|
|
49
|
+
image: ImageOf[np.ndarray],
|
|
50
|
+
channel_axis: int = 0,
|
|
51
|
+
nuclei_channel: int = 2,
|
|
52
|
+
membrane_channel: int = 0,
|
|
53
|
+
area_threshold: Annotated[int, {"min": 1, "max": 10000}] = 20,
|
|
54
|
+
convexity_threshold: Annotated[int, {"min": 0, "max": 100}] = 4,
|
|
55
|
+
cell_marker_threshold: Annotated[int, {"min": 0, "max": 255}] = 25,
|
|
56
|
+
dist_tr: DistanceTransform = DistanceTransform.geodesic,
|
|
57
|
+
sigma0: Annotated[int, {"min": 0, "max": 20}] = 3,
|
|
58
|
+
k0: Annotated[int, {"min": 0, "max": 20}] = 1,
|
|
59
|
+
r0: Annotated[int, {"min": 1, "max": 50}] = 5,
|
|
60
|
+
pct: tuple[float, float] = (0.01, 0.01),
|
|
61
|
+
nk: tuple[int, ...] = (5, 10, 20, 40),
|
|
62
|
+
t0: Annotated[float, {"min": 0.0, "max": 1.0, "step": 0.05}] = 0.5,
|
|
63
|
+
ternary_met: TernaryMethod = TernaryMethod.argmax,
|
|
64
|
+
area_ratio_threshold: Annotated[
|
|
65
|
+
float, {"min": 0.0, "max": 1.0, "step": 0.05}
|
|
66
|
+
] = 0.65,
|
|
67
|
+
dilation_radius: Annotated[int, {"min": 1, "max": 50}] = 5,
|
|
68
|
+
) -> Segmentation:
|
|
69
|
+
"""Segment nuclei and cells in a two-marker tissue image.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
image: Multichannel 2D image carrying both markers.
|
|
73
|
+
channel_axis: Which axis of ``image`` holds the channels. Fiji and
|
|
74
|
+
napari usually hand over (C, Y, X), hence the default of 0.
|
|
75
|
+
nuclei_channel: Index of the nucleus marker channel, e.g. DAPI.
|
|
76
|
+
membrane_channel: Index of the cell membrane marker channel, e.g.
|
|
77
|
+
Na+/K+ ATPase.
|
|
78
|
+
area_threshold: Smallest object area to keep, in pixels.
|
|
79
|
+
convexity_threshold: How non-convex an object may be before UNSEG
|
|
80
|
+
tries to split it.
|
|
81
|
+
cell_marker_threshold: Membrane marker intensity below which a pixel
|
|
82
|
+
is not considered part of a cell.
|
|
83
|
+
dist_tr: Distance transform used when splitting touching objects.
|
|
84
|
+
sigma0: Smoothing width for the empirical probability estimate.
|
|
85
|
+
k0: Gradient-adaptive smoothing strength.
|
|
86
|
+
r0: Radius of the local binary mask neighborhood.
|
|
87
|
+
pct: Background removal percentiles, one per marker.
|
|
88
|
+
nk: Cluster counts to try when building the global binary mask.
|
|
89
|
+
t0: Threshold on the global binary mask.
|
|
90
|
+
ternary_met: How to resolve the three-way mask.
|
|
91
|
+
area_ratio_threshold: Nucleus-to-cell area ratio above which a cell is
|
|
92
|
+
rejected.
|
|
93
|
+
dilation_radius: How far to grow nuclei when seeding cells.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Nucleus and cell label images, and the count of each.
|
|
97
|
+
"""
|
|
98
|
+
from ._algorithm import nuclei_cell_segmentation
|
|
99
|
+
|
|
100
|
+
nuclei_marker = np.take(image, nuclei_channel, axis=channel_axis)
|
|
101
|
+
membrane_marker = np.take(image, membrane_channel, axis=channel_axis)
|
|
102
|
+
if nuclei_marker.ndim != 2:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"UNSEG segments 2D images, but selecting a channel of a "
|
|
105
|
+
f"{image.ndim}D image left {nuclei_marker.ndim} dimensions. "
|
|
106
|
+
f"Check channel_axis (currently {channel_axis})."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
intensity = np.zeros((*nuclei_marker.shape, 2), dtype="float64")
|
|
110
|
+
intensity[:, :, 0] = nuclei_marker
|
|
111
|
+
intensity[:, :, 1] = membrane_marker
|
|
112
|
+
|
|
113
|
+
progress(f"Segmenting {nuclei_marker.shape} image")
|
|
114
|
+
nuc_seg, cel_seg, n_nuclei, n_cells = nuclei_cell_segmentation(
|
|
115
|
+
intensity,
|
|
116
|
+
area_threshold=area_threshold,
|
|
117
|
+
convexity_threshold=convexity_threshold,
|
|
118
|
+
cell_marker_threshold=cell_marker_threshold,
|
|
119
|
+
dist_tr=dist_tr.value,
|
|
120
|
+
sigma0=sigma0,
|
|
121
|
+
k0=k0,
|
|
122
|
+
r0=r0,
|
|
123
|
+
pct=list(pct),
|
|
124
|
+
nk=list(nk),
|
|
125
|
+
t0=t0,
|
|
126
|
+
ternary_met=ternary_met.value,
|
|
127
|
+
# NB: no display in a worker process.
|
|
128
|
+
visualization=False,
|
|
129
|
+
area_ratio_threshold=area_ratio_threshold,
|
|
130
|
+
dilation_radius=dilation_radius,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return Segmentation(
|
|
134
|
+
nuclei=np.asarray(nuc_seg).astype(np.int32),
|
|
135
|
+
cells=np.asarray(cel_seg).astype(np.int32),
|
|
136
|
+
n_nuclei=int(n_nuclei),
|
|
137
|
+
n_cells=int(n_cells),
|
|
138
|
+
)
|