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,326 @@
1
+ """I/O helpers for batch processing.
2
+
3
+ This module provides filesystem and image-loading utilities used by the
4
+ batch backend. Functions are intentionally stateless and easy to mock in
5
+ tests.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Iterable
12
+
13
+ import numpy as np
14
+
15
+ from senoquant.reader import core as reader_core
16
+ from .config import BatchChannelConfig
17
+
18
+
19
+ def normalize_extensions(extensions: Iterable[str] | None) -> set[str] | None:
20
+ """Normalize extension list to lowercase with leading dots.
21
+
22
+ Parameters
23
+ ----------
24
+ extensions : iterable of str or None
25
+ Raw extension strings (with or without dots).
26
+
27
+ Returns
28
+ -------
29
+ set of str or None
30
+ Normalized extensions or None when no filtering is requested.
31
+ """
32
+ if extensions is None:
33
+ return None
34
+ normalized = set()
35
+ for ext in extensions:
36
+ if not ext:
37
+ continue
38
+ cleaned = ext.strip().lower()
39
+ if not cleaned:
40
+ continue
41
+ if not cleaned.startswith("."):
42
+ cleaned = f".{cleaned}"
43
+ normalized.add(cleaned)
44
+ return normalized or None
45
+
46
+
47
+ def iter_input_files(
48
+ root: Path, extensions: set[str] | None, include_subfolders: bool
49
+ ) -> Iterable[Path]:
50
+ """Yield input files from a root folder.
51
+
52
+ Parameters
53
+ ----------
54
+ root : Path
55
+ Directory to scan.
56
+ extensions : set of str or None
57
+ Allowed file extensions. None disables filtering.
58
+ include_subfolders : bool
59
+ Whether to scan subfolders recursively.
60
+
61
+ Yields
62
+ ------
63
+ Path
64
+ File paths that match the extension criteria.
65
+ """
66
+ if not root.exists():
67
+ return
68
+ iterator = root.rglob("*") if include_subfolders else root.iterdir()
69
+ for path in iterator:
70
+ if not path.is_file():
71
+ continue
72
+ if extensions is None:
73
+ yield path
74
+ continue
75
+ name = path.name.lower()
76
+ if any(name.endswith(ext) for ext in extensions):
77
+ yield path
78
+
79
+
80
+ def basename_for_path(path: Path) -> str:
81
+ """Return a filesystem-friendly base name for a file path.
82
+
83
+ Parameters
84
+ ----------
85
+ path : Path
86
+ Input file path.
87
+
88
+ Returns
89
+ -------
90
+ str
91
+ Base name with common microscopy extensions removed.
92
+ """
93
+ name = path.name
94
+ lowered = name.lower()
95
+ for ext in (".ome.tiff", ".ome.tif", ".tiff", ".tif"):
96
+ if lowered.endswith(ext):
97
+ return name[: -len(ext)]
98
+ if "." in name:
99
+ return name.rsplit(".", 1)[0]
100
+ return name
101
+
102
+
103
+ def safe_scene_dir(scene_id: str) -> str:
104
+ """Return a sanitized scene identifier for folder naming.
105
+
106
+ Parameters
107
+ ----------
108
+ scene_id : str
109
+ Scene identifier from BioIO.
110
+
111
+ Returns
112
+ -------
113
+ str
114
+ Filesystem-safe scene folder name.
115
+ """
116
+ safe = scene_id.strip().replace("/", "_").replace("\\", "_")
117
+ return safe or "scene"
118
+
119
+
120
+ def write_array(
121
+ output_dir: Path, name: str, data: np.ndarray, output_format: str
122
+ ) -> Path:
123
+ """Write an array to disk in the requested format.
124
+
125
+ Parameters
126
+ ----------
127
+ output_dir : Path
128
+ Destination folder.
129
+ name : str
130
+ Base name for the output file.
131
+ data : numpy.ndarray
132
+ Array data to serialize.
133
+ output_format : str
134
+ Output format (``"tif"`` or ``"npy"``).
135
+
136
+ Returns
137
+ -------
138
+ Path
139
+ Path to the written file.
140
+ """
141
+ output_format = output_format.lower().strip()
142
+ if output_format == "npy":
143
+ path = output_dir / f"{name}.npy"
144
+ np.save(path, data)
145
+ return path
146
+
147
+ path = output_dir / f"{name}.tif"
148
+ try:
149
+ import tifffile
150
+
151
+ tifffile.imwrite(str(path), data)
152
+ return path
153
+ except Exception:
154
+ fallback = output_dir / f"{name}.npy"
155
+ np.save(fallback, data)
156
+ return fallback
157
+
158
+
159
+ def resolve_channel_index(
160
+ choice: str | int | None,
161
+ channel_map: list[BatchChannelConfig],
162
+ ) -> int:
163
+ """Resolve a channel selection into a numeric index.
164
+
165
+ Parameters
166
+ ----------
167
+ choice : str or int or None
168
+ Channel selection from the UI (name or index).
169
+ channel_map : list of BatchChannelConfig
170
+ Mapping from names to indices.
171
+
172
+ Returns
173
+ -------
174
+ int
175
+ Resolved channel index.
176
+
177
+ Raises
178
+ ------
179
+ ValueError
180
+ If the selection is missing or unknown.
181
+ """
182
+ if isinstance(choice, int):
183
+ return choice
184
+ if choice is None:
185
+ raise ValueError("Channel selection is required.")
186
+ text = str(choice).strip()
187
+ if not text:
188
+ raise ValueError("Channel selection is required.")
189
+ if text.isdigit():
190
+ return int(text)
191
+ for channel in channel_map:
192
+ if channel.name == text:
193
+ return channel.index
194
+ raise ValueError(f"Unknown channel selection: {text}.")
195
+
196
+
197
+ def spot_label_name(
198
+ choice: str | int,
199
+ channel_map: list[BatchChannelConfig],
200
+ ) -> str:
201
+ """Build the output label name for a spot channel.
202
+
203
+ Parameters
204
+ ----------
205
+ choice : str or int
206
+ Channel selection.
207
+ channel_map : list of BatchChannelConfig
208
+ Channel mapping list for name lookup.
209
+
210
+ Returns
211
+ -------
212
+ str
213
+ Standardized spot label name.
214
+ """
215
+ if isinstance(choice, int):
216
+ name = str(choice)
217
+ else:
218
+ name = str(choice).strip()
219
+ if name.isdigit():
220
+ return f"spot_labels_{name}"
221
+ for channel in channel_map:
222
+ if channel.name == name:
223
+ name = channel.name
224
+ break
225
+ return f"spot_labels_{sanitize_label(name)}"
226
+
227
+
228
+ def sanitize_label(name: str) -> str:
229
+ """Sanitize a label name for filesystem use."""
230
+ safe = []
231
+ for char in name.strip():
232
+ if char.isalnum():
233
+ safe.append(char)
234
+ else:
235
+ safe.append("_")
236
+ result = "".join(safe).strip("_")
237
+ return result or "spots"
238
+
239
+
240
+ def load_channel_data(
241
+ path: Path,
242
+ channel_index: int,
243
+ scene_id: str | None,
244
+ ) -> tuple[np.ndarray | None, dict]:
245
+ """Load a single-channel image array for the given path.
246
+
247
+ Parameters
248
+ ----------
249
+ path : Path
250
+ Input file path.
251
+ channel_index : int
252
+ Channel index to extract.
253
+ scene_id : str or None
254
+ Optional scene identifier.
255
+
256
+ Returns
257
+ -------
258
+ tuple of (numpy.ndarray or None, dict)
259
+ The extracted image data and metadata.
260
+ """
261
+ image = reader_core._open_bioimage(str(path))
262
+ try:
263
+ if scene_id:
264
+ image.set_scene(scene_id)
265
+ metadata = {"physical_pixel_sizes": reader_core._physical_pixel_sizes(image)}
266
+ axes_present = reader_core._axes_present(image)
267
+ dims = getattr(image, "dims", None)
268
+ c_size = getattr(dims, "C", 1) if "C" in axes_present else 1
269
+ z_size = getattr(dims, "Z", 1) if "Z" in axes_present else 1
270
+
271
+ if c_size > 1:
272
+ order = "CZYX" if z_size > 1 else "CYX"
273
+ else:
274
+ order = "ZYX" if z_size > 1 else "YX"
275
+
276
+ kwargs: dict[str, int] = {}
277
+ if "T" in axes_present and "T" not in order:
278
+ kwargs["T"] = 0
279
+ if "C" in axes_present and "C" not in order:
280
+ kwargs["C"] = 0
281
+ if "Z" in axes_present and "Z" not in order:
282
+ kwargs["Z"] = 0
283
+
284
+ data = image.get_image_data(order, **kwargs)
285
+ if c_size > 1:
286
+ if channel_index >= c_size or channel_index < 0:
287
+ raise ValueError(
288
+ f"Channel index {channel_index} out of range for {path.name}."
289
+ )
290
+ data = data[channel_index]
291
+ return np.asarray(data), metadata
292
+ finally:
293
+ if hasattr(image, "close"):
294
+ try:
295
+ image.close()
296
+ except Exception:
297
+ pass
298
+
299
+
300
+
301
+ def list_scenes(path: Path) -> list[str]:
302
+ """Return scene identifiers for a BioIO image path.
303
+
304
+ Parameters
305
+ ----------
306
+ path : Path
307
+ Input file path.
308
+
309
+ Returns
310
+ -------
311
+ list of str
312
+ Scene identifiers, or an empty list if unavailable.
313
+ """
314
+ try:
315
+ image = reader_core._open_bioimage(str(path))
316
+ except Exception:
317
+ return []
318
+ try:
319
+ scenes = list(getattr(image, "scenes", []) or [])
320
+ finally:
321
+ if hasattr(image, "close"):
322
+ try:
323
+ image.close()
324
+ except Exception:
325
+ pass
326
+ return scenes
@@ -0,0 +1,86 @@
1
+ """Lightweight layer shims used for batch processing.
2
+
3
+ These classes emulate the minimal attributes used by feature exporters
4
+ and quantification routines, without requiring a live napari viewer.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Iterable
10
+
11
+ import numpy as np
12
+
13
+ class Image:
14
+ """Lightweight image layer placeholder.
15
+
16
+ Parameters
17
+ ----------
18
+ data : numpy.ndarray or None
19
+ Image data array.
20
+ name : str
21
+ Layer name.
22
+ metadata : dict or None, optional
23
+ Metadata dictionary (e.g., pixel sizes).
24
+ rgb : bool, optional
25
+ Whether the layer should be treated as RGB.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ data: np.ndarray | None,
31
+ name: str,
32
+ metadata: dict | None = None,
33
+ rgb: bool = False,
34
+ ) -> None:
35
+ self.data = data
36
+ self.name = name
37
+ self.metadata = metadata or {}
38
+ self.rgb = rgb
39
+
40
+
41
+ class Labels:
42
+ """Lightweight labels layer placeholder.
43
+
44
+ Parameters
45
+ ----------
46
+ data : numpy.ndarray or None
47
+ Label image data.
48
+ name : str
49
+ Layer name.
50
+ metadata : dict or None, optional
51
+ Metadata dictionary (e.g., pixel sizes).
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ data: np.ndarray | None,
57
+ name: str,
58
+ metadata: dict | None = None,
59
+ ) -> None:
60
+ self.data = data
61
+ self.name = name
62
+ self.metadata = metadata or {}
63
+
64
+
65
+ class BatchViewer:
66
+ """Minimal viewer shim exposing layers for export routines.
67
+
68
+ Parameters
69
+ ----------
70
+ layers : iterable of object or None, optional
71
+ Initial layer collection.
72
+ """
73
+
74
+ def __init__(self, layers: Iterable[object] | None = None) -> None:
75
+ """Initialize the viewer shim."""
76
+ self.layers = list(layers) if layers is not None else []
77
+
78
+ def set_layers(self, layers: Iterable[object]) -> None:
79
+ """Replace the current layer collection.
80
+
81
+ Parameters
82
+ ----------
83
+ layers : iterable of object
84
+ New layer collection.
85
+ """
86
+ self.layers = list(layers)
@@ -0,0 +1 @@
1
+ """Quantification tab modules."""
@@ -0,0 +1,228 @@
1
+ """Backend logic for the Quantification tab."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Iterable
8
+ import shutil
9
+ import tempfile
10
+
11
+ from .features import FeatureConfig
12
+
13
+
14
+ @dataclass
15
+ class FeatureExportResult:
16
+ """Output metadata for a single feature export.
17
+
18
+ Attributes
19
+ ----------
20
+ feature_id : str
21
+ Stable identifier for the exported feature instance.
22
+ feature_type : str
23
+ Feature type name used for routing (e.g., ``"Markers"``).
24
+ feature_name : str
25
+ Display name provided by the user.
26
+ temp_dir : Path
27
+ Temporary directory where the feature wrote its outputs.
28
+ outputs : list of Path
29
+ Explicit file paths returned by the feature processor.
30
+ """
31
+
32
+ feature_id: str
33
+ feature_type: str
34
+ feature_name: str
35
+ temp_dir: Path
36
+ outputs: list[Path] = field(default_factory=list)
37
+
38
+
39
+ @dataclass
40
+ class QuantificationResult:
41
+ """Aggregated output information for a quantification run.
42
+
43
+ Attributes
44
+ ----------
45
+ output_root : Path
46
+ Root output directory for the run.
47
+ temp_root : Path
48
+ Temporary root directory used during processing.
49
+ feature_outputs : list of FeatureExportResult
50
+ Per-feature export metadata for the run.
51
+ """
52
+
53
+ output_root: Path
54
+ temp_root: Path
55
+ feature_outputs: list[FeatureExportResult]
56
+
57
+
58
+ class QuantificationBackend:
59
+ """Backend orchestrator for quantification exports.
60
+
61
+ Notes
62
+ -----
63
+ Feature export routines live with their feature implementations. The
64
+ backend iterates through configured feature contexts, asks each feature
65
+ handler to export into a temporary directory, and then routes those
66
+ outputs into a final output structure.
67
+ """
68
+
69
+ def __init__(self) -> None:
70
+ """Initialize the backend state.
71
+
72
+ Attributes
73
+ ----------
74
+ metrics : list
75
+ Placeholder container for computed metrics.
76
+ """
77
+ self.metrics: list[object] = []
78
+
79
+ def process(
80
+ self,
81
+ features: Iterable[object],
82
+ output_path: str,
83
+ output_name: str,
84
+ export_format: str,
85
+ cleanup: bool = True,
86
+ ) -> QuantificationResult:
87
+ """Run feature exports and route their outputs.
88
+
89
+ Parameters
90
+ ----------
91
+ features : iterable of object
92
+ Feature UI contexts with ``state`` and ``feature_handler``.
93
+ Each handler should implement ``export(temp_dir, export_format)``.
94
+ output_path : str
95
+ Base output folder path.
96
+ output_name : str
97
+ Folder name used to group exported outputs.
98
+ export_format : str
99
+ File format requested by the user (``"csv"`` or ``"xlsx"``).
100
+ cleanup : bool, optional
101
+ Whether to delete temporary export folders after routing.
102
+
103
+ Returns
104
+ -------
105
+ QuantificationResult
106
+ Output metadata for the completed run.
107
+
108
+ Notes
109
+ -----
110
+ If a feature export does not return explicit output paths, the backend
111
+ will move all files found in the feature's temp directory. This allows
112
+ feature implementations to either return specific files or simply write
113
+ into the provided temporary directory.
114
+ """
115
+ output_root = self._resolve_output_root(output_path, output_name)
116
+ output_root.mkdir(parents=True, exist_ok=True)
117
+ temp_root = Path(tempfile.mkdtemp(prefix="senoquant-quant-"))
118
+
119
+ feature_outputs: list[FeatureExportResult] = []
120
+ for context in features:
121
+ feature = getattr(context, "state", None)
122
+ handler = getattr(context, "feature_handler", None)
123
+ if not isinstance(feature, FeatureConfig):
124
+ continue
125
+ temp_dir = temp_root / feature.feature_id
126
+ temp_dir.mkdir(parents=True, exist_ok=True)
127
+ outputs: list[Path] = []
128
+ if handler is not None and hasattr(handler, "export"):
129
+ outputs = [
130
+ Path(path)
131
+ for path in handler.export(temp_dir, export_format)
132
+ ]
133
+ feature_outputs.append(
134
+ FeatureExportResult(
135
+ feature_id=feature.feature_id,
136
+ feature_type=feature.type_name,
137
+ feature_name=feature.name,
138
+ temp_dir=temp_dir,
139
+ outputs=outputs,
140
+ )
141
+ )
142
+
143
+ self._route_feature_outputs(output_root, feature_outputs)
144
+ if cleanup:
145
+ shutil.rmtree(temp_root, ignore_errors=True)
146
+ return QuantificationResult(
147
+ output_root=output_root,
148
+ temp_root=temp_root,
149
+ feature_outputs=feature_outputs,
150
+ )
151
+
152
+ def _resolve_output_root(self, output_path: str, output_name: str) -> Path:
153
+ """Resolve the final output root directory.
154
+
155
+ Parameters
156
+ ----------
157
+ output_path : str
158
+ Base output folder path.
159
+ output_name : str
160
+ Folder name used to group exported outputs.
161
+
162
+ Returns
163
+ -------
164
+ Path
165
+ Resolved output directory path.
166
+ """
167
+ base = Path(output_path) if output_path else Path.cwd()
168
+ if output_name:
169
+ return base / output_name
170
+ return base
171
+
172
+ def _route_feature_outputs(
173
+ self,
174
+ output_root: Path,
175
+ feature_outputs: Iterable[FeatureExportResult],
176
+ ) -> None:
177
+ """Move feature outputs from temp folders to the final location.
178
+
179
+ Parameters
180
+ ----------
181
+ output_root : Path
182
+ Destination root folder.
183
+ feature_outputs : iterable of FeatureExportResult
184
+ Export results to route.
185
+
186
+ Notes
187
+ -----
188
+ When a feature returns no explicit output list, all files present
189
+ in the temporary directory are routed instead. Subdirectories are
190
+ not traversed.
191
+ """
192
+ for feature_output in feature_outputs:
193
+ feature_dir = output_root / self._feature_dir_name(feature_output)
194
+ feature_dir.mkdir(parents=True, exist_ok=True)
195
+ outputs = feature_output.outputs
196
+ if outputs:
197
+ for path in outputs:
198
+ if path.exists():
199
+ shutil.move(str(path), feature_dir / path.name)
200
+ else:
201
+ for path in feature_output.temp_dir.glob("*"):
202
+ if path.is_file():
203
+ shutil.move(str(path), feature_dir / path.name)
204
+
205
+ def _feature_dir_name(self, feature_output: FeatureExportResult) -> str:
206
+ """Build a filesystem-friendly folder name for a feature.
207
+
208
+ Parameters
209
+ ----------
210
+ feature_output : FeatureExportResult
211
+ Export result metadata.
212
+
213
+ Returns
214
+ -------
215
+ str
216
+ Directory name for the feature outputs.
217
+
218
+ Notes
219
+ -----
220
+ Non-alphanumeric characters are replaced to avoid filesystem issues.
221
+ """
222
+ name = feature_output.feature_name.strip()
223
+ if not name:
224
+ name = feature_output.feature_type
225
+ safe = "".join(
226
+ char if char.isalnum() or char in "-_ " else "_" for char in name
227
+ )
228
+ return safe.replace(" ", "_").lower()