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,157 @@
|
|
|
1
|
+
"""Post-processing helpers using StarDist geometry and NMS."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def instances_from_prediction_2d(
|
|
9
|
+
prob: np.ndarray,
|
|
10
|
+
dist: np.ndarray,
|
|
11
|
+
*,
|
|
12
|
+
grid: tuple[int, int],
|
|
13
|
+
prob_thresh: float,
|
|
14
|
+
nms_thresh: float,
|
|
15
|
+
scale: dict[str, float] | None = None,
|
|
16
|
+
img_shape: tuple[int, int] | None = None,
|
|
17
|
+
) -> tuple[np.ndarray, dict]:
|
|
18
|
+
"""Create 2D instance labels from StarDist outputs.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
prob : numpy.ndarray
|
|
23
|
+
Probability map with shape (Y, X).
|
|
24
|
+
dist : numpy.ndarray
|
|
25
|
+
Distance/ray map with shape (Y, X, R), where R is the number of rays.
|
|
26
|
+
grid : tuple[int, int]
|
|
27
|
+
Subsampling grid of the model (e.g., (1, 1) or (2, 2)).
|
|
28
|
+
prob_thresh : float
|
|
29
|
+
Probability threshold used to filter candidate points before NMS.
|
|
30
|
+
nms_thresh : float
|
|
31
|
+
NMS IoU/overlap threshold for suppressing nearby detections.
|
|
32
|
+
scale : dict[str, float], optional
|
|
33
|
+
Scale factors applied to the input image before inference. If
|
|
34
|
+
provided, must include ``"X"`` and ``"Y"`` so that points and
|
|
35
|
+
distances can be rescaled back to the original image space.
|
|
36
|
+
img_shape : tuple[int, int], optional
|
|
37
|
+
Original image shape (Y, X). If provided, the output label image
|
|
38
|
+
is generated in this shape instead of the scaled prediction shape.
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
tuple[numpy.ndarray, dict]
|
|
42
|
+
Label image of shape (Y, X) and a metadata dict with:
|
|
43
|
+
- ``points``: center points used for each instance.
|
|
44
|
+
- ``prob``: per-instance probabilities.
|
|
45
|
+
- ``dist``: per-instance ray distances.
|
|
46
|
+
|
|
47
|
+
Notes
|
|
48
|
+
-----
|
|
49
|
+
This function performs non-maximum suppression on the probability map
|
|
50
|
+
and then rasterizes polygons using the selected points and distances.
|
|
51
|
+
"""
|
|
52
|
+
from ..._stardist.nms import non_maximum_suppression
|
|
53
|
+
points, scores, distances = non_maximum_suppression(
|
|
54
|
+
dist,
|
|
55
|
+
prob,
|
|
56
|
+
grid=grid,
|
|
57
|
+
prob_thresh=prob_thresh,
|
|
58
|
+
nms_thresh=nms_thresh,
|
|
59
|
+
)
|
|
60
|
+
if scale is not None:
|
|
61
|
+
if not (isinstance(scale, dict) and "X" in scale and "Y" in scale):
|
|
62
|
+
raise ValueError("scale must be a dictionary with entries for 'X' and 'Y'")
|
|
63
|
+
rescale = (1 / scale["Y"], 1 / scale["X"])
|
|
64
|
+
points = points * np.array(rescale).reshape(1, 2)
|
|
65
|
+
else:
|
|
66
|
+
rescale = (1, 1)
|
|
67
|
+
from ..._stardist.geometry.geom2d import polygons_to_label
|
|
68
|
+
shape = img_shape if img_shape is not None else tuple(
|
|
69
|
+
s * g for s, g in zip(prob.shape, grid)
|
|
70
|
+
)
|
|
71
|
+
labels = polygons_to_label(
|
|
72
|
+
distances, points, shape=shape, prob=scores, scale_dist=rescale
|
|
73
|
+
)
|
|
74
|
+
return labels, {"points": points, "prob": scores, "dist": distances}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def instances_from_prediction_3d(
|
|
78
|
+
prob: np.ndarray,
|
|
79
|
+
dist: np.ndarray,
|
|
80
|
+
*,
|
|
81
|
+
grid: tuple[int, int, int],
|
|
82
|
+
prob_thresh: float,
|
|
83
|
+
nms_thresh: float,
|
|
84
|
+
rays,
|
|
85
|
+
scale: dict[str, float] | None = None,
|
|
86
|
+
img_shape: tuple[int, int, int] | None = None,
|
|
87
|
+
) -> tuple[np.ndarray, dict]:
|
|
88
|
+
"""Create 3D instance labels from StarDist outputs.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
prob : numpy.ndarray
|
|
93
|
+
Probability map with shape (Z, Y, X).
|
|
94
|
+
dist : numpy.ndarray
|
|
95
|
+
Distance/ray map with shape (Z, Y, X, R), where R is the number of rays.
|
|
96
|
+
grid : tuple[int, int, int]
|
|
97
|
+
Subsampling grid of the model (e.g., (1, 1, 1) or (2, 2, 2)).
|
|
98
|
+
prob_thresh : float
|
|
99
|
+
Probability threshold used to filter candidate points before NMS.
|
|
100
|
+
nms_thresh : float
|
|
101
|
+
NMS IoU/overlap threshold for suppressing nearby detections.
|
|
102
|
+
rays : object
|
|
103
|
+
StarDist 3D rays object describing ray directions and sampling.
|
|
104
|
+
scale : dict[str, float], optional
|
|
105
|
+
Scale factors applied to the input image before inference. If
|
|
106
|
+
provided, must include ``"X"``, ``"Y"``, and ``"Z"`` so that points
|
|
107
|
+
and rays can be rescaled back to the original image space.
|
|
108
|
+
img_shape : tuple[int, int, int], optional
|
|
109
|
+
Original image shape (Z, Y, X). If provided, the output label
|
|
110
|
+
volume is generated in this shape instead of the scaled prediction
|
|
111
|
+
shape.
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
tuple[numpy.ndarray, dict]
|
|
115
|
+
Label volume of shape (Z, Y, X) and a metadata dict with:
|
|
116
|
+
- ``points``: center points used for each instance.
|
|
117
|
+
- ``prob``: per-instance probabilities.
|
|
118
|
+
- ``dist``: per-instance ray distances.
|
|
119
|
+
|
|
120
|
+
Notes
|
|
121
|
+
-----
|
|
122
|
+
This function performs non-maximum suppression in 3D and then
|
|
123
|
+
rasterizes polyhedra using the selected points and distances. The
|
|
124
|
+
Python backend uses an axis-aligned bounding-box approximation.
|
|
125
|
+
"""
|
|
126
|
+
from ..._stardist.nms import non_maximum_suppression_3d
|
|
127
|
+
points, scores, distances = non_maximum_suppression_3d(
|
|
128
|
+
dist,
|
|
129
|
+
prob,
|
|
130
|
+
rays,
|
|
131
|
+
grid=grid,
|
|
132
|
+
prob_thresh=prob_thresh,
|
|
133
|
+
nms_thresh=nms_thresh,
|
|
134
|
+
)
|
|
135
|
+
if scale is not None:
|
|
136
|
+
if not (
|
|
137
|
+
isinstance(scale, dict)
|
|
138
|
+
and "X" in scale
|
|
139
|
+
and "Y" in scale
|
|
140
|
+
and "Z" in scale
|
|
141
|
+
):
|
|
142
|
+
raise ValueError(
|
|
143
|
+
"scale must be a dictionary with entries for 'X', 'Y', and 'Z'"
|
|
144
|
+
)
|
|
145
|
+
rescale = (1 / scale["Z"], 1 / scale["Y"], 1 / scale["X"])
|
|
146
|
+
points = points * np.array(rescale).reshape(1, 3)
|
|
147
|
+
rays = rays.copy(scale=rescale)
|
|
148
|
+
else:
|
|
149
|
+
rescale = (1, 1, 1)
|
|
150
|
+
from ..._stardist.geometry.geom3d import polyhedron_to_label
|
|
151
|
+
shape = img_shape if img_shape is not None else tuple(
|
|
152
|
+
s * g for s, g in zip(prob.shape, grid)
|
|
153
|
+
)
|
|
154
|
+
labels = polyhedron_to_label(
|
|
155
|
+
distances, points, rays=rays, shape=shape, prob=scores, verbose=False
|
|
156
|
+
)
|
|
157
|
+
return labels, {"points": points, "prob": scores, "dist": distances}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Preprocessing utilities for ONNX inference."""
|
|
2
|
+
|
|
3
|
+
from .core import (
|
|
4
|
+
normalize,
|
|
5
|
+
pad_for_tiling,
|
|
6
|
+
pad_to_multiple,
|
|
7
|
+
unpad_to_shape,
|
|
8
|
+
validate_image,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"normalize",
|
|
13
|
+
"pad_for_tiling",
|
|
14
|
+
"pad_to_multiple",
|
|
15
|
+
"unpad_to_shape",
|
|
16
|
+
"validate_image",
|
|
17
|
+
]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Preprocessing helpers for ONNX StarDist inference.
|
|
2
|
+
|
|
3
|
+
These helpers mirror the high-level behavior of StarDist's preprocessing:
|
|
4
|
+
validate input dimensionality, optionally normalize, and pad to the grid
|
|
5
|
+
required by the network. The helpers assume single-channel images with
|
|
6
|
+
spatial axes ordered as YX (2D) or ZYX (3D).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_image(image: np.ndarray) -> None:
|
|
17
|
+
"""Validate input image dimensionality.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
image : numpy.ndarray
|
|
22
|
+
Input image array expected to be 2D (YX) or 3D (ZYX) and single-channel.
|
|
23
|
+
|
|
24
|
+
Raises
|
|
25
|
+
------
|
|
26
|
+
ValueError
|
|
27
|
+
If the input is not 2D or 3D.
|
|
28
|
+
"""
|
|
29
|
+
if image.ndim not in (2, 3):
|
|
30
|
+
raise ValueError("Input image must be 2D (YX) or 3D (ZYX).")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def normalize(
|
|
34
|
+
image: np.ndarray,
|
|
35
|
+
pmin: float = 1.0,
|
|
36
|
+
pmax: float = 99.8,
|
|
37
|
+
eps: float = 1e-6,
|
|
38
|
+
) -> np.ndarray:
|
|
39
|
+
"""Percentile normalize a single-channel image.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
image : numpy.ndarray
|
|
44
|
+
Input image to normalize.
|
|
45
|
+
pmin : float, optional
|
|
46
|
+
Lower percentile for normalization. Default is 1.0.
|
|
47
|
+
pmax : float, optional
|
|
48
|
+
Upper percentile for normalization. Default is 99.8.
|
|
49
|
+
eps : float, optional
|
|
50
|
+
Small constant to avoid division by zero. Default is 1e-6.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
numpy.ndarray
|
|
55
|
+
Normalized float32 image with values clipped to [0, 1].
|
|
56
|
+
|
|
57
|
+
Raises
|
|
58
|
+
------
|
|
59
|
+
ValueError
|
|
60
|
+
If ``pmax`` is not greater than ``pmin``.
|
|
61
|
+
"""
|
|
62
|
+
if pmax <= pmin:
|
|
63
|
+
raise ValueError("pmax must be greater than pmin.")
|
|
64
|
+
lo = np.percentile(image, pmin, keepdims=True)
|
|
65
|
+
hi = np.percentile(image, pmax, keepdims=True)
|
|
66
|
+
scale = hi - lo
|
|
67
|
+
scale = np.where(scale < eps, 1.0, scale)
|
|
68
|
+
normalized = (image - lo) / scale
|
|
69
|
+
return np.clip(normalized, 0.0, 1.0).astype(np.float32)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def pad_to_multiple(
|
|
73
|
+
image: np.ndarray,
|
|
74
|
+
multiples: tuple[int, ...],
|
|
75
|
+
mode: str = "reflect",
|
|
76
|
+
) -> tuple[np.ndarray, tuple[tuple[int, int], ...]]:
|
|
77
|
+
"""Pad each axis so its length is divisible by the corresponding multiple.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
image : numpy.ndarray
|
|
82
|
+
Input image (2D or 3D).
|
|
83
|
+
multiples : tuple[int, ...]
|
|
84
|
+
Per-axis divisibility constraints (e.g., the StarDist grid).
|
|
85
|
+
mode : str, optional
|
|
86
|
+
Padding mode passed to ``numpy.pad``. Default is "reflect".
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
numpy.ndarray
|
|
91
|
+
Padded image as float32.
|
|
92
|
+
tuple[tuple[int, int], ...]
|
|
93
|
+
Padding applied per axis as (before, after) pairs. This implementation
|
|
94
|
+
only pads at the end of each axis (before = 0).
|
|
95
|
+
|
|
96
|
+
Raises
|
|
97
|
+
------
|
|
98
|
+
ValueError
|
|
99
|
+
If the multiples do not match the image dimensionality or contain
|
|
100
|
+
non-positive values.
|
|
101
|
+
"""
|
|
102
|
+
validate_image(image)
|
|
103
|
+
if len(multiples) != image.ndim:
|
|
104
|
+
raise ValueError("Multiples must match image dimensionality.")
|
|
105
|
+
pads = []
|
|
106
|
+
for dim, mult in zip(image.shape, multiples):
|
|
107
|
+
if mult <= 0:
|
|
108
|
+
raise ValueError("Multiples must be positive.")
|
|
109
|
+
pad_after = (mult - (dim % mult)) % mult
|
|
110
|
+
pads.append((0, pad_after))
|
|
111
|
+
padded = np.pad(image, pads, mode=mode)
|
|
112
|
+
return padded.astype(np.float32, copy=False), tuple(pads)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def unpad_to_shape(
|
|
116
|
+
data: np.ndarray,
|
|
117
|
+
pads: tuple[tuple[int, int], ...],
|
|
118
|
+
scale: tuple[int, ...] | None = None,
|
|
119
|
+
) -> np.ndarray:
|
|
120
|
+
"""Crop padded output back to the unpadded shape (accounting for scale).
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
data : numpy.ndarray
|
|
125
|
+
Output array to crop (e.g., probability or distance map).
|
|
126
|
+
pads : tuple[tuple[int, int], ...]
|
|
127
|
+
Padding applied to the input image as returned by ``pad_to_multiple``.
|
|
128
|
+
scale : tuple[int, ...] or None, optional
|
|
129
|
+
Per-axis scale factor between input and output spatial grids.
|
|
130
|
+
For StarDist, this is typically the grid (e.g., (1, 1) or (2, 2)).
|
|
131
|
+
If None, assumes scale 1 for all axes.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
numpy.ndarray
|
|
136
|
+
Cropped array with padding removed.
|
|
137
|
+
|
|
138
|
+
Raises
|
|
139
|
+
------
|
|
140
|
+
ValueError
|
|
141
|
+
If scale dimensionality does not match pads, or if non-zero
|
|
142
|
+
pre-padding is provided (this function assumes end-padding only).
|
|
143
|
+
"""
|
|
144
|
+
if scale is None:
|
|
145
|
+
scale = (1,) * len(pads)
|
|
146
|
+
if len(scale) != len(pads):
|
|
147
|
+
raise ValueError("Scale must match pad dimensionality.")
|
|
148
|
+
slices = []
|
|
149
|
+
for (before, after), mult in zip(pads, scale):
|
|
150
|
+
if before != 0:
|
|
151
|
+
raise ValueError("Only end-padding is supported.")
|
|
152
|
+
if after >= mult:
|
|
153
|
+
slices.append(slice(0, -(after // mult)))
|
|
154
|
+
else:
|
|
155
|
+
slices.append(slice(None))
|
|
156
|
+
slices.extend([slice(None)] * (data.ndim - len(pads)))
|
|
157
|
+
return data[tuple(slices)]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def pad_for_tiling(
|
|
161
|
+
image: np.ndarray,
|
|
162
|
+
grid: tuple[int, ...],
|
|
163
|
+
tile_shape: tuple[int, ...],
|
|
164
|
+
overlap: tuple[int, ...],
|
|
165
|
+
div_by: tuple[int, ...] | None = None,
|
|
166
|
+
mode: str = "reflect",
|
|
167
|
+
) -> tuple[np.ndarray, tuple[tuple[int, int], ...]]:
|
|
168
|
+
"""Pad input so tiled prediction aligns with overlap and divisibility.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
image : numpy.ndarray
|
|
173
|
+
Input image (2D or 3D).
|
|
174
|
+
grid : tuple[int, ...]
|
|
175
|
+
Model output grid/stride per axis.
|
|
176
|
+
tile_shape : tuple[int, ...]
|
|
177
|
+
Spatial size of each tile in input pixels.
|
|
178
|
+
overlap : tuple[int, ...]
|
|
179
|
+
Overlap per axis in input pixels.
|
|
180
|
+
div_by : tuple[int, ...] or None, optional
|
|
181
|
+
Additional per-axis divisibility constraints. If None, uses ``grid``.
|
|
182
|
+
mode : str, optional
|
|
183
|
+
Padding mode passed to ``numpy.pad``. Default is "reflect".
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
numpy.ndarray
|
|
188
|
+
Padded image as float32.
|
|
189
|
+
tuple[tuple[int, int], ...]
|
|
190
|
+
Padding applied per axis as (before, after) pairs. This implementation
|
|
191
|
+
only pads at the end of each axis (before = 0).
|
|
192
|
+
|
|
193
|
+
Raises
|
|
194
|
+
------
|
|
195
|
+
ValueError
|
|
196
|
+
If shapes are inconsistent or overlap is invalid.
|
|
197
|
+
"""
|
|
198
|
+
if len(grid) != image.ndim:
|
|
199
|
+
raise ValueError("Grid must match image dimensionality.")
|
|
200
|
+
if len(tile_shape) != image.ndim:
|
|
201
|
+
raise ValueError("tile_shape must match image dimensionality.")
|
|
202
|
+
if len(overlap) != image.ndim:
|
|
203
|
+
raise ValueError("overlap must match image dimensionality.")
|
|
204
|
+
|
|
205
|
+
if div_by is None:
|
|
206
|
+
div_by = grid
|
|
207
|
+
if len(div_by) != image.ndim:
|
|
208
|
+
raise ValueError("div_by must match image dimensionality.")
|
|
209
|
+
|
|
210
|
+
pads = []
|
|
211
|
+
for dim, g, ts, ov, d in zip(image.shape, grid, tile_shape, overlap, div_by):
|
|
212
|
+
step = ts - ov
|
|
213
|
+
if step <= 0:
|
|
214
|
+
raise ValueError("overlap must be smaller than tile size.")
|
|
215
|
+
if dim <= ts:
|
|
216
|
+
target = ts
|
|
217
|
+
else:
|
|
218
|
+
target = ts + math.ceil((dim - ts) / step) * step
|
|
219
|
+
div_req = math.lcm(int(g), int(d))
|
|
220
|
+
if div_req > 1:
|
|
221
|
+
while target % div_req != 0:
|
|
222
|
+
target += step
|
|
223
|
+
pads.append((0, int(max(0, target - dim))))
|
|
224
|
+
|
|
225
|
+
padded = np.pad(image, pads, mode=mode)
|
|
226
|
+
return padded.astype(np.float32, copy=False), tuple(pads)
|