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,401 @@
|
|
|
1
|
+
"""Tiled ONNX prediction helpers for StarDist.
|
|
2
|
+
|
|
3
|
+
This module provides ONNX-based prediction with optional tiling. It mirrors
|
|
4
|
+
the structure of StarDist's Keras/CSBDeep prediction flow but is specialized
|
|
5
|
+
for single-channel 2D (YX) and 3D (ZYX) inputs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from itertools import product
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from ..pre import pad_for_tiling, unpad_to_shape, validate_image
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class TilingSpec:
|
|
20
|
+
"""Tiling configuration for prediction.
|
|
21
|
+
|
|
22
|
+
Attributes
|
|
23
|
+
----------
|
|
24
|
+
tile_shape : tuple[int, ...]
|
|
25
|
+
Tile size per spatial axis in input pixels.
|
|
26
|
+
overlap : tuple[int, ...]
|
|
27
|
+
Overlap per spatial axis in input pixels.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
tile_shape: tuple[int, ...]
|
|
31
|
+
overlap: tuple[int, ...]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def default_tiling_spec(
|
|
35
|
+
shape: tuple[int, ...],
|
|
36
|
+
tile_shape: tuple[int, ...] | None = None,
|
|
37
|
+
overlap: tuple[int, ...] | None = None,
|
|
38
|
+
) -> TilingSpec:
|
|
39
|
+
"""Create a default tiling configuration for a given shape.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
shape : tuple[int, ...]
|
|
44
|
+
Spatial shape of the input image.
|
|
45
|
+
tile_shape : tuple[int, ...] or None, optional
|
|
46
|
+
Tile size per axis. Defaults to the full ``shape``.
|
|
47
|
+
overlap : tuple[int, ...] or None, optional
|
|
48
|
+
Overlap per axis in input pixels. Defaults to zero overlap.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
TilingSpec
|
|
53
|
+
Tiling specification with validated defaults.
|
|
54
|
+
|
|
55
|
+
Raises
|
|
56
|
+
------
|
|
57
|
+
ValueError
|
|
58
|
+
If provided shapes do not match dimensionality.
|
|
59
|
+
"""
|
|
60
|
+
if tile_shape is None:
|
|
61
|
+
tile_shape = shape
|
|
62
|
+
if overlap is None:
|
|
63
|
+
overlap = (0,) * len(shape)
|
|
64
|
+
if len(tile_shape) != len(shape):
|
|
65
|
+
raise ValueError("tile_shape must match input dimensionality.")
|
|
66
|
+
if len(overlap) != len(shape):
|
|
67
|
+
raise ValueError("overlap must match input dimensionality.")
|
|
68
|
+
return TilingSpec(tile_shape=tile_shape, overlap=overlap)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def predict_tiled(
|
|
72
|
+
image: np.ndarray,
|
|
73
|
+
session,
|
|
74
|
+
*,
|
|
75
|
+
input_name: str,
|
|
76
|
+
output_names: list[str],
|
|
77
|
+
grid: tuple[int, ...],
|
|
78
|
+
input_layout: str,
|
|
79
|
+
prob_layout: str,
|
|
80
|
+
dist_layout: str,
|
|
81
|
+
tile_shape: tuple[int, ...] | None = None,
|
|
82
|
+
overlap: tuple[int, ...] | None = None,
|
|
83
|
+
div_by: tuple[int, ...] | None = None,
|
|
84
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
85
|
+
"""Run ONNX prediction with optional tiling.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
image : numpy.ndarray
|
|
90
|
+
Input image array. Must be 2D (YX) or 3D (ZYX) and single-channel.
|
|
91
|
+
session : object
|
|
92
|
+
ONNX Runtime session instance.
|
|
93
|
+
input_name : str
|
|
94
|
+
Input tensor name for the ONNX model.
|
|
95
|
+
output_names : list[str]
|
|
96
|
+
Output tensor names for the ONNX model. The first is interpreted as
|
|
97
|
+
probability, the second as distances.
|
|
98
|
+
grid : tuple[int, ...]
|
|
99
|
+
Subsampling grid of the model (e.g., (1, 1) or (2, 2, 2)).
|
|
100
|
+
input_layout : str
|
|
101
|
+
Input tensor layout. Supported values:
|
|
102
|
+
- 2D: "NCHW" or "NHWC"
|
|
103
|
+
- 3D: "NCDHW" or "NDHWC"
|
|
104
|
+
prob_layout : str
|
|
105
|
+
Probability output layout. Supported values:
|
|
106
|
+
- 2D: "NCHW" or "NHWC"
|
|
107
|
+
- 3D: "NCDHW" or "NDHWC"
|
|
108
|
+
dist_layout : str
|
|
109
|
+
Distance output layout. Supported values:
|
|
110
|
+
- 2D: "NRYX" or "NYXR"
|
|
111
|
+
- 3D: "NRZYX" or "NZYXR"
|
|
112
|
+
tile_shape : tuple[int, ...] or None, optional
|
|
113
|
+
Tile size per spatial axis in input pixels. If None, the full padded
|
|
114
|
+
image is used.
|
|
115
|
+
overlap : tuple[int, ...] or None, optional
|
|
116
|
+
Overlap per spatial axis in input pixels. Defaults to zero.
|
|
117
|
+
Padding is computed so each axis aligns with the tiling grid, i.e.,
|
|
118
|
+
the padded size is ``tile_shape + k * (tile_shape - overlap)`` and
|
|
119
|
+
divisible by the model grid/divisibility constraints.
|
|
120
|
+
div_by : tuple[int, ...] or None, optional
|
|
121
|
+
Additional per-axis divisibility constraint (e.g., from ONNX graph
|
|
122
|
+
inspection). If provided, padding is also aligned to these multiples.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
tuple[numpy.ndarray, numpy.ndarray]
|
|
127
|
+
Probability map and distance map with padding removed and grid
|
|
128
|
+
accounted for. The probability output has shape (Y, X) or (Z, Y, X),
|
|
129
|
+
and the distance output has shape (Y, X, R) or (Z, Y, X, R).
|
|
130
|
+
|
|
131
|
+
Raises
|
|
132
|
+
------
|
|
133
|
+
ValueError
|
|
134
|
+
If input dimensionality or layout parameters are invalid.
|
|
135
|
+
RuntimeError
|
|
136
|
+
If the ONNX model outputs do not include prob and dist outputs.
|
|
137
|
+
"""
|
|
138
|
+
validate_image(image)
|
|
139
|
+
if len(grid) != image.ndim:
|
|
140
|
+
raise ValueError("Grid must match image dimensionality.")
|
|
141
|
+
|
|
142
|
+
tiling = default_tiling_spec(
|
|
143
|
+
image.shape, tile_shape=tile_shape, overlap=overlap
|
|
144
|
+
)
|
|
145
|
+
tile_shape = tiling.tile_shape
|
|
146
|
+
overlap = tiling.overlap
|
|
147
|
+
|
|
148
|
+
if div_by is None:
|
|
149
|
+
div_by = grid
|
|
150
|
+
if len(div_by) != image.ndim:
|
|
151
|
+
raise ValueError("div_by must match image dimensionality.")
|
|
152
|
+
|
|
153
|
+
padded, pads = pad_for_tiling(
|
|
154
|
+
image, grid, tile_shape, overlap, div_by=div_by, mode="reflect"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
tiles = _iter_tiles(padded.shape, tile_shape, overlap)
|
|
158
|
+
prob_out = None
|
|
159
|
+
dist_out = None
|
|
160
|
+
|
|
161
|
+
for read_slice, crop_slice, write_slice in tiles:
|
|
162
|
+
tile = padded[read_slice]
|
|
163
|
+
prob_tile, dist_tile = _run_onnx(
|
|
164
|
+
session,
|
|
165
|
+
input_name,
|
|
166
|
+
output_names,
|
|
167
|
+
_prepare_input(tile, input_layout),
|
|
168
|
+
prob_layout,
|
|
169
|
+
dist_layout,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if prob_out is None:
|
|
173
|
+
out_shape = tuple(s // g for s, g in zip(padded.shape, grid))
|
|
174
|
+
prob_out = np.zeros(out_shape, dtype=np.float32)
|
|
175
|
+
dist_out = np.zeros(out_shape + (dist_tile.shape[-1],), dtype=np.float32)
|
|
176
|
+
|
|
177
|
+
prob_write, crop_write = _tile_write_slices(
|
|
178
|
+
crop_slice, write_slice, grid
|
|
179
|
+
)
|
|
180
|
+
prob_out[prob_write] = prob_tile[crop_write]
|
|
181
|
+
dist_out[prob_write + (slice(None),)] = dist_tile[crop_write + (slice(None),)]
|
|
182
|
+
|
|
183
|
+
prob_out = unpad_to_shape(prob_out, pads, scale=grid)
|
|
184
|
+
dist_out = unpad_to_shape(dist_out, pads, scale=grid)
|
|
185
|
+
return prob_out, dist_out
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _run_onnx(
|
|
189
|
+
session,
|
|
190
|
+
input_name: str,
|
|
191
|
+
output_names: list[str],
|
|
192
|
+
input_tensor: np.ndarray,
|
|
193
|
+
prob_layout: str,
|
|
194
|
+
dist_layout: str,
|
|
195
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
196
|
+
"""Run the ONNX session and parse prob/dist outputs.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
session : object
|
|
201
|
+
ONNX Runtime session instance.
|
|
202
|
+
input_name : str
|
|
203
|
+
Input tensor name.
|
|
204
|
+
output_names : list[str]
|
|
205
|
+
Output tensor names (prob, dist).
|
|
206
|
+
input_tensor : numpy.ndarray
|
|
207
|
+
Input tensor ready for ONNX execution.
|
|
208
|
+
prob_layout : str
|
|
209
|
+
Layout of the prob output.
|
|
210
|
+
dist_layout : str
|
|
211
|
+
Layout of the dist output.
|
|
212
|
+
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
tuple[numpy.ndarray, numpy.ndarray]
|
|
216
|
+
Probability map and distance map in image layout.
|
|
217
|
+
"""
|
|
218
|
+
outputs = session.run(output_names, {input_name: input_tensor})
|
|
219
|
+
if len(outputs) < 2:
|
|
220
|
+
raise RuntimeError("ONNX model must return prob and dist outputs.")
|
|
221
|
+
prob = _parse_prob(outputs[0], prob_layout, input_tensor.ndim - 2)
|
|
222
|
+
dist = _parse_dist(outputs[1], dist_layout, input_tensor.ndim - 2)
|
|
223
|
+
return prob, dist
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _prepare_input(image: np.ndarray, layout: str) -> np.ndarray:
|
|
227
|
+
"""Prepare a single-channel image for ONNX input.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
image : numpy.ndarray
|
|
232
|
+
Input image array (2D or 3D).
|
|
233
|
+
layout : str
|
|
234
|
+
Desired input layout (NCHW/NHWC or NCDHW/NDHWC).
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
numpy.ndarray
|
|
239
|
+
Batched input tensor with explicit channel axis.
|
|
240
|
+
"""
|
|
241
|
+
if image.ndim == 2:
|
|
242
|
+
if layout == "NCHW":
|
|
243
|
+
return image[np.newaxis, np.newaxis, ...]
|
|
244
|
+
if layout == "NHWC":
|
|
245
|
+
return image[np.newaxis, ..., np.newaxis]
|
|
246
|
+
if image.ndim == 3:
|
|
247
|
+
if layout == "NCDHW":
|
|
248
|
+
return image[np.newaxis, np.newaxis, ...]
|
|
249
|
+
if layout == "NDHWC":
|
|
250
|
+
return image[np.newaxis, ..., np.newaxis]
|
|
251
|
+
raise ValueError(f"Unsupported input layout {layout} for ndim={image.ndim}.")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _parse_prob(prob: np.ndarray, layout: str, ndim: int) -> np.ndarray:
|
|
255
|
+
"""Parse probability output into image layout.
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
prob : numpy.ndarray
|
|
260
|
+
Raw probability output from ONNX.
|
|
261
|
+
layout : str
|
|
262
|
+
Layout of the prob output tensor.
|
|
263
|
+
ndim : int
|
|
264
|
+
Spatial dimensionality (2 or 3).
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
numpy.ndarray
|
|
269
|
+
Probability map in spatial layout.
|
|
270
|
+
"""
|
|
271
|
+
if ndim == 2:
|
|
272
|
+
if layout == "NCHW":
|
|
273
|
+
return prob[0, 0]
|
|
274
|
+
if layout == "NHWC":
|
|
275
|
+
return prob[0, ..., 0]
|
|
276
|
+
if ndim == 3:
|
|
277
|
+
if layout == "NCDHW":
|
|
278
|
+
return prob[0, 0]
|
|
279
|
+
if layout == "NDHWC":
|
|
280
|
+
return prob[0, ..., 0]
|
|
281
|
+
raise ValueError(f"Unsupported prob layout {layout} for ndim={ndim}.")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _parse_dist(dist: np.ndarray, layout: str, ndim: int) -> np.ndarray:
|
|
285
|
+
"""Parse distance output into image layout.
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
dist : numpy.ndarray
|
|
290
|
+
Raw distance output from ONNX.
|
|
291
|
+
layout : str
|
|
292
|
+
Layout of the dist output tensor.
|
|
293
|
+
ndim : int
|
|
294
|
+
Spatial dimensionality (2 or 3).
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
numpy.ndarray
|
|
299
|
+
Distance map with rays as the last axis.
|
|
300
|
+
"""
|
|
301
|
+
if ndim == 2:
|
|
302
|
+
if layout == "NRYX":
|
|
303
|
+
return dist[0].transpose(1, 2, 0)
|
|
304
|
+
if layout == "NYXR":
|
|
305
|
+
return dist[0]
|
|
306
|
+
if ndim == 3:
|
|
307
|
+
if layout == "NRZYX":
|
|
308
|
+
return dist[0].transpose(1, 2, 3, 0)
|
|
309
|
+
if layout == "NZYXR":
|
|
310
|
+
return dist[0]
|
|
311
|
+
raise ValueError(f"Unsupported dist layout {layout} for ndim={ndim}.")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _iter_tiles(shape: tuple[int, ...], tile_shape: tuple[int, ...], overlap: tuple[int, ...]):
|
|
315
|
+
"""Yield read/crop/write slices for tiled prediction.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
shape : tuple[int, ...]
|
|
320
|
+
Shape of the padded input image.
|
|
321
|
+
tile_shape : tuple[int, ...]
|
|
322
|
+
Spatial size of each tile.
|
|
323
|
+
overlap : tuple[int, ...]
|
|
324
|
+
Overlap per axis in input pixels.
|
|
325
|
+
|
|
326
|
+
Yields
|
|
327
|
+
------
|
|
328
|
+
tuple[tuple[slice, ...], tuple[slice, ...], tuple[slice, ...]]
|
|
329
|
+
Read slices, crop slices, and write slices per tile.
|
|
330
|
+
"""
|
|
331
|
+
tile_ranges = []
|
|
332
|
+
# Build per-axis start positions and overlap metadata.
|
|
333
|
+
for dim, size, ov in zip(shape, tile_shape, overlap):
|
|
334
|
+
if size <= 0:
|
|
335
|
+
raise ValueError("tile_shape entries must be positive.")
|
|
336
|
+
if ov >= size:
|
|
337
|
+
raise ValueError("overlap must be smaller than tile size.")
|
|
338
|
+
# Step is the non-overlapping stride between consecutive tiles.
|
|
339
|
+
step = size - ov
|
|
340
|
+
max_start = max(0, dim - size)
|
|
341
|
+
starts = list(range(0, max_start + 1, step))
|
|
342
|
+
if not starts:
|
|
343
|
+
starts = [0]
|
|
344
|
+
# Ensure the last tile reaches the end even if step doesn't align.
|
|
345
|
+
if starts[-1] != max_start:
|
|
346
|
+
starts.append(max_start)
|
|
347
|
+
tile_ranges.append((starts, size, ov))
|
|
348
|
+
|
|
349
|
+
# Iterate all coordinate combinations across axes.
|
|
350
|
+
for starts in product(*[r[0] for r in tile_ranges]):
|
|
351
|
+
read_slices = []
|
|
352
|
+
crop_slices = []
|
|
353
|
+
write_slices = []
|
|
354
|
+
# Compute read/crop/write slices for each axis.
|
|
355
|
+
for axis, (start, (_, size, ov)) in enumerate(zip(starts, tile_ranges)):
|
|
356
|
+
end = min(start + size, shape[axis])
|
|
357
|
+
# Read the full tile region from the padded input.
|
|
358
|
+
read_slices.append(slice(start, end))
|
|
359
|
+
|
|
360
|
+
ov_before = ov // 2
|
|
361
|
+
ov_after = ov - ov_before
|
|
362
|
+
# Crop overlap from interior tiles, keep full extent at borders.
|
|
363
|
+
crop_start = 0 if start == 0 else ov_before
|
|
364
|
+
crop_end = (end - start) if end == shape[axis] else (end - start - ov_after)
|
|
365
|
+
crop_slices.append(slice(crop_start, crop_end))
|
|
366
|
+
# Write the cropped region back into the global output frame.
|
|
367
|
+
write_slices.append(slice(start + crop_start, start + crop_end))
|
|
368
|
+
|
|
369
|
+
yield tuple(read_slices), tuple(crop_slices), tuple(write_slices)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _tile_write_slices(
|
|
373
|
+
crop_slice: tuple[slice, ...],
|
|
374
|
+
write_slice: tuple[slice, ...],
|
|
375
|
+
grid: tuple[int, ...],
|
|
376
|
+
) -> tuple[tuple[slice, ...], tuple[slice, ...]]:
|
|
377
|
+
"""Compute output-write and crop slices for prob/dist outputs.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
381
|
+
crop_slice : tuple[slice, ...]
|
|
382
|
+
Crop slices applied to the tile predictions.
|
|
383
|
+
write_slice : tuple[slice, ...]
|
|
384
|
+
Write slices in input pixel coordinates.
|
|
385
|
+
grid : tuple[int, ...]
|
|
386
|
+
Subsampling grid for the model outputs.
|
|
387
|
+
|
|
388
|
+
Returns
|
|
389
|
+
-------
|
|
390
|
+
tuple[tuple[slice, ...], tuple[slice, ...]]
|
|
391
|
+
Output write slices (in output coordinates) and crop slices
|
|
392
|
+
(in tile output coordinates).
|
|
393
|
+
"""
|
|
394
|
+
prob_write = []
|
|
395
|
+
crop_write = []
|
|
396
|
+
for crop, write, g in zip(crop_slice, write_slice, grid):
|
|
397
|
+
prob_write.append(slice(write.start // g, write.stop // g))
|
|
398
|
+
crop_write.append(slice(crop.start // g, crop.stop // g))
|
|
399
|
+
prob_write = tuple(prob_write)
|
|
400
|
+
crop_write = tuple(crop_write)
|
|
401
|
+
return prob_write, crop_write
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Settings tab modules."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Backend logic for the Settings tab."""
|
|
2
|
+
|
|
3
|
+
from qtpy.QtCore import QObject, Signal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SettingsBackend(QObject):
|
|
7
|
+
preload_models_changed = Signal(bool)
|
|
8
|
+
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
"""Initialize settings storage with defaults."""
|
|
11
|
+
super().__init__()
|
|
12
|
+
self._preferences = {"preload_models": True}
|
|
13
|
+
|
|
14
|
+
def preload_models_enabled(self) -> bool:
|
|
15
|
+
"""Return whether model preload is enabled."""
|
|
16
|
+
return bool(self._preferences.get("preload_models", True))
|
|
17
|
+
|
|
18
|
+
def set_preload_models(self, enabled: bool) -> None:
|
|
19
|
+
"""Update the preload setting and emit changes.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
enabled : bool
|
|
24
|
+
Whether to preload models on startup.
|
|
25
|
+
"""
|
|
26
|
+
if self.preload_models_enabled() == enabled:
|
|
27
|
+
return
|
|
28
|
+
self._preferences["preload_models"] = enabled
|
|
29
|
+
self.preload_models_changed.emit(enabled)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Frontend widget for the Settings tab."""
|
|
2
|
+
|
|
3
|
+
from qtpy.QtWidgets import QCheckBox, QLabel, QVBoxLayout, QWidget
|
|
4
|
+
|
|
5
|
+
from .backend import SettingsBackend
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SettingsTab(QWidget):
|
|
9
|
+
def __init__(self, backend: SettingsBackend | None = None) -> None:
|
|
10
|
+
super().__init__()
|
|
11
|
+
self._backend = backend or SettingsBackend()
|
|
12
|
+
|
|
13
|
+
layout = QVBoxLayout()
|
|
14
|
+
self._preload_checkbox = QCheckBox("Preload segmentation models on startup")
|
|
15
|
+
self._preload_checkbox.setChecked(self._backend.preload_models_enabled())
|
|
16
|
+
self._preload_checkbox.toggled.connect(self._backend.set_preload_models)
|
|
17
|
+
layout.addWidget(self._preload_checkbox)
|
|
18
|
+
layout.addStretch(1)
|
|
19
|
+
self.setLayout(layout)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Spots tab modules."""
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Backend logic for the Spots tab."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from skimage.measure import label, regionprops
|
|
11
|
+
|
|
12
|
+
from .models import SenoQuantSpotDetector
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SpotsBackend:
|
|
16
|
+
"""Manage spot detectors and their storage locations.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
models_root : pathlib.Path or None
|
|
21
|
+
Optional root folder for detector storage. Defaults to the local models
|
|
22
|
+
directory for this tab.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, models_root: Path | None = None) -> None:
|
|
26
|
+
self._models_root = models_root or (Path(__file__).parent / "models")
|
|
27
|
+
self._detectors: dict[str, SenoQuantSpotDetector] = {}
|
|
28
|
+
|
|
29
|
+
def get_detector(self, name: str) -> SenoQuantSpotDetector:
|
|
30
|
+
"""Return a detector wrapper for the given name.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
name : str
|
|
35
|
+
Detector name used to locate or create the detector folder.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
SenoQuantSpotDetector
|
|
40
|
+
Detector instance.
|
|
41
|
+
"""
|
|
42
|
+
detector = self._detectors.get(name)
|
|
43
|
+
if detector is None:
|
|
44
|
+
detector_cls = self._load_detector_class(name)
|
|
45
|
+
if detector_cls is None:
|
|
46
|
+
detector = SenoQuantSpotDetector(name, self._models_root)
|
|
47
|
+
else:
|
|
48
|
+
detector = detector_cls(models_root=self._models_root)
|
|
49
|
+
self._detectors[name] = detector
|
|
50
|
+
return detector
|
|
51
|
+
|
|
52
|
+
def list_detector_names(self) -> list[str]:
|
|
53
|
+
"""List available detector folders under the models root.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
list[str]
|
|
58
|
+
Sorted detector folder names ordered by display_order, then by name.
|
|
59
|
+
"""
|
|
60
|
+
if not self._models_root.exists():
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
entries: list[tuple[float, str]] = []
|
|
64
|
+
for path in self._models_root.iterdir():
|
|
65
|
+
if path.is_dir() and not path.name.startswith("__"):
|
|
66
|
+
detector = self.get_detector(path.name)
|
|
67
|
+
order = detector.display_order()
|
|
68
|
+
order_key = order if order is not None else float("inf")
|
|
69
|
+
entries.append((order_key, path.name))
|
|
70
|
+
entries.sort(key=lambda item: (item[0], item[1]))
|
|
71
|
+
return [name for _, name in entries]
|
|
72
|
+
|
|
73
|
+
def _load_detector_class(self, name: str) -> type[SenoQuantSpotDetector] | None:
|
|
74
|
+
"""Load the detector class from a detector folder's model.py.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
name : str
|
|
79
|
+
Detector folder name under the models root.
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
type[SenoQuantSpotDetector] or None
|
|
84
|
+
Concrete detector class to instantiate.
|
|
85
|
+
"""
|
|
86
|
+
model_path = self._models_root / name / "model.py"
|
|
87
|
+
if not model_path.exists():
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
module_name = f"senoquant.tabs.spots.models.{name}.model"
|
|
91
|
+
package_name = f"senoquant.tabs.spots.models.{name}"
|
|
92
|
+
spec = importlib.util.spec_from_file_location(module_name, model_path)
|
|
93
|
+
if spec is None or spec.loader is None:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
module = importlib.util.module_from_spec(spec)
|
|
97
|
+
module.__package__ = package_name
|
|
98
|
+
sys.modules[module_name] = module
|
|
99
|
+
spec.loader.exec_module(module)
|
|
100
|
+
|
|
101
|
+
candidates = [
|
|
102
|
+
obj
|
|
103
|
+
for obj in module.__dict__.values()
|
|
104
|
+
if isinstance(obj, type)
|
|
105
|
+
and issubclass(obj, SenoQuantSpotDetector)
|
|
106
|
+
and obj is not SenoQuantSpotDetector
|
|
107
|
+
]
|
|
108
|
+
if not candidates:
|
|
109
|
+
return None
|
|
110
|
+
return candidates[0]
|
|
111
|
+
|
|
112
|
+
def compute_colocalization(
|
|
113
|
+
self, data_a: np.ndarray, data_b: np.ndarray
|
|
114
|
+
) -> dict:
|
|
115
|
+
"""Compute colocalization centroids from two label arrays.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
data_a : numpy.ndarray
|
|
120
|
+
First label layer data.
|
|
121
|
+
data_b : numpy.ndarray
|
|
122
|
+
Second label layer data.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
dict
|
|
127
|
+
Dictionary containing the ``points`` array.
|
|
128
|
+
"""
|
|
129
|
+
intersection = (data_a > 0) & (data_b > 0)
|
|
130
|
+
if not np.any(intersection):
|
|
131
|
+
return {"points": np.empty((0, intersection.ndim), dtype=np.float32)}
|
|
132
|
+
|
|
133
|
+
labeled = label(intersection)
|
|
134
|
+
if labeled.max() == 0:
|
|
135
|
+
return {"points": np.empty((0, intersection.ndim), dtype=np.float32)}
|
|
136
|
+
|
|
137
|
+
points = [region.centroid for region in regionprops(labeled)]
|
|
138
|
+
coords = np.asarray(points, dtype=np.float32)
|
|
139
|
+
return {"points": coords}
|