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.
Files changed (148) hide show
  1. senoquant/__init__.py +6 -0
  2. senoquant/_reader.py +7 -0
  3. senoquant/_widget.py +33 -0
  4. senoquant/napari.yaml +83 -0
  5. senoquant/reader/__init__.py +5 -0
  6. senoquant/reader/core.py +369 -0
  7. senoquant/tabs/__init__.py +15 -0
  8. senoquant/tabs/batch/__init__.py +10 -0
  9. senoquant/tabs/batch/backend.py +641 -0
  10. senoquant/tabs/batch/config.py +270 -0
  11. senoquant/tabs/batch/frontend.py +1283 -0
  12. senoquant/tabs/batch/io.py +326 -0
  13. senoquant/tabs/batch/layers.py +86 -0
  14. senoquant/tabs/quantification/__init__.py +1 -0
  15. senoquant/tabs/quantification/backend.py +228 -0
  16. senoquant/tabs/quantification/features/__init__.py +80 -0
  17. senoquant/tabs/quantification/features/base.py +142 -0
  18. senoquant/tabs/quantification/features/marker/__init__.py +5 -0
  19. senoquant/tabs/quantification/features/marker/config.py +69 -0
  20. senoquant/tabs/quantification/features/marker/dialog.py +437 -0
  21. senoquant/tabs/quantification/features/marker/export.py +879 -0
  22. senoquant/tabs/quantification/features/marker/feature.py +119 -0
  23. senoquant/tabs/quantification/features/marker/morphology.py +285 -0
  24. senoquant/tabs/quantification/features/marker/rows.py +654 -0
  25. senoquant/tabs/quantification/features/marker/thresholding.py +46 -0
  26. senoquant/tabs/quantification/features/roi.py +346 -0
  27. senoquant/tabs/quantification/features/spots/__init__.py +5 -0
  28. senoquant/tabs/quantification/features/spots/config.py +62 -0
  29. senoquant/tabs/quantification/features/spots/dialog.py +477 -0
  30. senoquant/tabs/quantification/features/spots/export.py +1292 -0
  31. senoquant/tabs/quantification/features/spots/feature.py +112 -0
  32. senoquant/tabs/quantification/features/spots/morphology.py +279 -0
  33. senoquant/tabs/quantification/features/spots/rows.py +241 -0
  34. senoquant/tabs/quantification/frontend.py +815 -0
  35. senoquant/tabs/segmentation/__init__.py +1 -0
  36. senoquant/tabs/segmentation/backend.py +131 -0
  37. senoquant/tabs/segmentation/frontend.py +1009 -0
  38. senoquant/tabs/segmentation/models/__init__.py +5 -0
  39. senoquant/tabs/segmentation/models/base.py +146 -0
  40. senoquant/tabs/segmentation/models/cpsam/details.json +65 -0
  41. senoquant/tabs/segmentation/models/cpsam/model.py +150 -0
  42. senoquant/tabs/segmentation/models/default_2d/details.json +69 -0
  43. senoquant/tabs/segmentation/models/default_2d/model.py +664 -0
  44. senoquant/tabs/segmentation/models/default_3d/details.json +69 -0
  45. senoquant/tabs/segmentation/models/default_3d/model.py +682 -0
  46. senoquant/tabs/segmentation/models/hf.py +71 -0
  47. senoquant/tabs/segmentation/models/nuclear_dilation/__init__.py +1 -0
  48. senoquant/tabs/segmentation/models/nuclear_dilation/details.json +26 -0
  49. senoquant/tabs/segmentation/models/nuclear_dilation/model.py +96 -0
  50. senoquant/tabs/segmentation/models/perinuclear_rings/__init__.py +1 -0
  51. senoquant/tabs/segmentation/models/perinuclear_rings/details.json +34 -0
  52. senoquant/tabs/segmentation/models/perinuclear_rings/model.py +132 -0
  53. senoquant/tabs/segmentation/stardist_onnx_utils/__init__.py +2 -0
  54. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/__init__.py +3 -0
  55. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/__init__.py +6 -0
  56. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/generate.py +470 -0
  57. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/prepare.py +273 -0
  58. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/rawdata.py +112 -0
  59. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/transform.py +384 -0
  60. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/__init__.py +0 -0
  61. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/blocks.py +184 -0
  62. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/losses.py +79 -0
  63. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/nets.py +165 -0
  64. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/predict.py +467 -0
  65. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/probability.py +67 -0
  66. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/train.py +148 -0
  67. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/io/__init__.py +163 -0
  68. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/__init__.py +52 -0
  69. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/base_model.py +329 -0
  70. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_isotropic.py +160 -0
  71. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_projection.py +178 -0
  72. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_standard.py +446 -0
  73. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_upsampling.py +54 -0
  74. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/config.py +254 -0
  75. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/pretrained.py +119 -0
  76. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/scripts/__init__.py +0 -0
  77. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/scripts/care_predict.py +180 -0
  78. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/__init__.py +5 -0
  79. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/plot_utils.py +159 -0
  80. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/six.py +18 -0
  81. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/tf.py +644 -0
  82. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/utils.py +272 -0
  83. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/version.py +1 -0
  84. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/docs/source/conf.py +368 -0
  85. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/setup.py +68 -0
  86. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_datagen.py +169 -0
  87. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_models.py +462 -0
  88. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_utils.py +166 -0
  89. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +34 -0
  90. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/__init__.py +30 -0
  91. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/big.py +624 -0
  92. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/bioimageio_utils.py +494 -0
  93. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/data/__init__.py +39 -0
  94. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/__init__.py +10 -0
  95. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/geom2d.py +215 -0
  96. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/geom3d.py +349 -0
  97. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/matching.py +483 -0
  98. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/__init__.py +28 -0
  99. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/base.py +1217 -0
  100. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/model2d.py +594 -0
  101. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/model3d.py +696 -0
  102. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/nms.py +384 -0
  103. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/__init__.py +2 -0
  104. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/plot.py +74 -0
  105. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/render.py +298 -0
  106. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/rays3d.py +373 -0
  107. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/sample_patches.py +65 -0
  108. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/__init__.py +0 -0
  109. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/predict2d.py +90 -0
  110. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/predict3d.py +93 -0
  111. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/utils.py +408 -0
  112. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/version.py +1 -0
  113. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/__init__.py +45 -0
  114. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/__init__.py +17 -0
  115. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/cli.py +55 -0
  116. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/core.py +285 -0
  117. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/__init__.py +15 -0
  118. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/cli.py +36 -0
  119. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/divisibility.py +193 -0
  120. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +100 -0
  121. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/receptive_field.py +182 -0
  122. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/rf_cli.py +48 -0
  123. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/valid_sizes.py +278 -0
  124. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/post/__init__.py +8 -0
  125. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/post/core.py +157 -0
  126. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/pre/__init__.py +17 -0
  127. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/pre/core.py +226 -0
  128. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/predict/__init__.py +5 -0
  129. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/predict/core.py +401 -0
  130. senoquant/tabs/settings/__init__.py +1 -0
  131. senoquant/tabs/settings/backend.py +29 -0
  132. senoquant/tabs/settings/frontend.py +19 -0
  133. senoquant/tabs/spots/__init__.py +1 -0
  134. senoquant/tabs/spots/backend.py +139 -0
  135. senoquant/tabs/spots/frontend.py +800 -0
  136. senoquant/tabs/spots/models/__init__.py +5 -0
  137. senoquant/tabs/spots/models/base.py +94 -0
  138. senoquant/tabs/spots/models/rmp/details.json +61 -0
  139. senoquant/tabs/spots/models/rmp/model.py +499 -0
  140. senoquant/tabs/spots/models/udwt/details.json +103 -0
  141. senoquant/tabs/spots/models/udwt/model.py +482 -0
  142. senoquant/utils.py +25 -0
  143. senoquant-1.0.0b1.dist-info/METADATA +193 -0
  144. senoquant-1.0.0b1.dist-info/RECORD +148 -0
  145. senoquant-1.0.0b1.dist-info/WHEEL +5 -0
  146. senoquant-1.0.0b1.dist-info/entry_points.txt +2 -0
  147. senoquant-1.0.0b1.dist-info/licenses/LICENSE +28 -0
  148. 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}