senoquant 1.0.0b2__py3-none-any.whl → 1.0.0b4__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 (57) hide show
  1. senoquant/__init__.py +6 -2
  2. senoquant/_reader.py +1 -1
  3. senoquant/_widget.py +9 -1
  4. senoquant/reader/core.py +201 -18
  5. senoquant/tabs/__init__.py +2 -0
  6. senoquant/tabs/batch/backend.py +76 -27
  7. senoquant/tabs/batch/frontend.py +127 -25
  8. senoquant/tabs/quantification/features/marker/dialog.py +26 -6
  9. senoquant/tabs/quantification/features/marker/export.py +97 -24
  10. senoquant/tabs/quantification/features/marker/rows.py +2 -2
  11. senoquant/tabs/quantification/features/spots/dialog.py +41 -11
  12. senoquant/tabs/quantification/features/spots/export.py +163 -10
  13. senoquant/tabs/quantification/frontend.py +2 -2
  14. senoquant/tabs/segmentation/frontend.py +46 -9
  15. senoquant/tabs/segmentation/models/cpsam/model.py +1 -1
  16. senoquant/tabs/segmentation/models/default_2d/model.py +22 -77
  17. senoquant/tabs/segmentation/models/default_3d/model.py +8 -74
  18. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +0 -0
  19. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +13 -13
  20. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/stardist_libs.py +171 -0
  21. senoquant/tabs/spots/frontend.py +96 -5
  22. senoquant/tabs/spots/models/rmp/details.json +3 -9
  23. senoquant/tabs/spots/models/rmp/model.py +341 -266
  24. senoquant/tabs/spots/models/ufish/details.json +32 -0
  25. senoquant/tabs/spots/models/ufish/model.py +327 -0
  26. senoquant/tabs/spots/ufish_utils/__init__.py +13 -0
  27. senoquant/tabs/spots/ufish_utils/core.py +387 -0
  28. senoquant/tabs/visualization/__init__.py +1 -0
  29. senoquant/tabs/visualization/backend.py +306 -0
  30. senoquant/tabs/visualization/frontend.py +1113 -0
  31. senoquant/tabs/visualization/plots/__init__.py +80 -0
  32. senoquant/tabs/visualization/plots/base.py +152 -0
  33. senoquant/tabs/visualization/plots/double_expression.py +187 -0
  34. senoquant/tabs/visualization/plots/spatialplot.py +156 -0
  35. senoquant/tabs/visualization/plots/umap.py +140 -0
  36. senoquant/utils.py +1 -1
  37. senoquant-1.0.0b4.dist-info/METADATA +162 -0
  38. {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/RECORD +53 -30
  39. {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/top_level.txt +1 -0
  40. ufish/__init__.py +1 -0
  41. ufish/api.py +778 -0
  42. ufish/model/__init__.py +0 -0
  43. ufish/model/loss.py +62 -0
  44. ufish/model/network/__init__.py +0 -0
  45. ufish/model/network/spot_learn.py +50 -0
  46. ufish/model/network/ufish_net.py +204 -0
  47. ufish/model/train.py +175 -0
  48. ufish/utils/__init__.py +0 -0
  49. ufish/utils/img.py +418 -0
  50. ufish/utils/log.py +8 -0
  51. ufish/utils/spot_calling.py +115 -0
  52. senoquant/tabs/spots/models/udwt/details.json +0 -103
  53. senoquant/tabs/spots/models/udwt/model.py +0 -482
  54. senoquant-1.0.0b2.dist-info/METADATA +0 -193
  55. {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/WHEEL +0 -0
  56. {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/entry_points.txt +0 -0
  57. {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/licenses/LICENSE +0 -0
senoquant/__init__.py CHANGED
@@ -1,6 +1,10 @@
1
1
  """SenoQuant napari plugin package."""
2
2
 
3
- from ._widget import SenoQuantWidget
3
+ try:
4
+ from importlib.metadata import version
5
+ __version__ = version("senoquant")
6
+ except Exception:
7
+ __version__ = "1.0.0b4" # Fallback for development
4
8
 
5
- __version__ = "1.0.0b2"
9
+ from ._widget import SenoQuantWidget
6
10
  __all__ = ["SenoQuantWidget"]
senoquant/_reader.py CHANGED
@@ -1,4 +1,4 @@
1
- """Napari reader entrypoint for SenoQuant."""
1
+ """napari reader entrypoint for SenoQuant."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
senoquant/_widget.py CHANGED
@@ -2,7 +2,14 @@
2
2
 
3
3
  from qtpy.QtWidgets import QTabWidget, QVBoxLayout, QWidget
4
4
 
5
- from .tabs import BatchTab, QuantificationTab, SegmentationTab, SettingsTab, SpotsTab
5
+ from .tabs import (
6
+ BatchTab,
7
+ QuantificationTab,
8
+ SegmentationTab,
9
+ SettingsTab,
10
+ SpotsTab,
11
+ VisualizationTab,
12
+ )
6
13
  from .tabs.settings.backend import SettingsBackend
7
14
 
8
15
 
@@ -26,6 +33,7 @@ class SenoQuantWidget(QWidget):
26
33
  )
27
34
  tabs.addTab(SpotsTab(napari_viewer=napari_viewer), "Spots")
28
35
  tabs.addTab(QuantificationTab(napari_viewer=napari_viewer), "Quantification")
36
+ tabs.addTab(VisualizationTab(napari_viewer=napari_viewer), "Visualization")
29
37
  tabs.addTab(BatchTab(napari_viewer=napari_viewer), "Batch")
30
38
  tabs.addTab(SettingsTab(backend=self._settings_backend), "Settings")
31
39
 
senoquant/reader/core.py CHANGED
@@ -75,7 +75,7 @@ def _read_senoquant(path: str) -> Iterable[tuple]:
75
75
  Returns
76
76
  -------
77
77
  iterable of tuple
78
- Napari layer tuples of the form ``(data, metadata, layer_type)``.
78
+ napari layer tuples of the form ``(data, metadata, layer_type)``.
79
79
 
80
80
  Notes
81
81
  -----
@@ -91,25 +91,208 @@ def _read_senoquant(path: str) -> Iterable[tuple]:
91
91
 
92
92
  base_name = Path(path).name
93
93
  image = _open_bioimage(path)
94
- layers: list[tuple] = []
95
- colormap_cycle = _colormap_cycle()
96
- scenes = image.scenes
97
-
98
- for scene_idx, scene_id in enumerate(scenes):
99
- image.set_scene(scene_id)
100
- layers.extend(
101
- _iter_channel_layers(
102
- image,
103
- base_name=base_name,
104
- scene_id=scene_id,
105
- scene_idx=scene_idx,
106
- total_scenes=len(scenes),
107
- path=path,
108
- colormap_cycle=colormap_cycle,
94
+ try:
95
+ layers: list[tuple] = []
96
+ colormap_cycle = _colormap_cycle()
97
+ scenes = list(getattr(image, "scenes", []) or [])
98
+ selected_scene_indices = _select_scene_indices(path, scenes)
99
+ if not selected_scene_indices:
100
+ return layers
101
+
102
+ for scene_idx in selected_scene_indices:
103
+ scene_id = scenes[scene_idx]
104
+ image.set_scene(scene_id)
105
+ layers.extend(
106
+ _iter_channel_layers(
107
+ image,
108
+ base_name=base_name,
109
+ scene_id=scene_id,
110
+ scene_idx=scene_idx,
111
+ total_scenes=len(scenes),
112
+ path=path,
113
+ colormap_cycle=colormap_cycle,
114
+ )
109
115
  )
116
+
117
+ return layers
118
+ finally:
119
+ if hasattr(image, "close"):
120
+ try:
121
+ image.close()
122
+ except Exception:
123
+ pass
124
+
125
+
126
+ def _select_scene_indices(path: str, scenes: list[str]) -> list[int]:
127
+ """Return scene indices selected by the user for loading."""
128
+ if not scenes:
129
+ return []
130
+ if len(scenes) == 1:
131
+ return [0]
132
+
133
+ selected = _prompt_scene_selection(path, scenes)
134
+ if selected is None:
135
+ return []
136
+ return selected
137
+
138
+
139
+ def _prompt_scene_selection(path: str, scenes: list[str]) -> list[int] | None:
140
+ """Show a scene-selection dialog and return selected indices.
141
+
142
+ Returns
143
+ -------
144
+ list[int] or None
145
+ Selected scene indices. Returns ``None`` when the dialog is cancelled.
146
+ If Qt is not available, all scenes are selected.
147
+ """
148
+ try:
149
+ from qtpy.QtCore import Qt
150
+ from qtpy.QtWidgets import (
151
+ QApplication,
152
+ QDialog,
153
+ QDialogButtonBox,
154
+ QHBoxLayout,
155
+ QLabel,
156
+ QListWidget,
157
+ QListWidgetItem,
158
+ QMessageBox,
159
+ QPushButton,
160
+ QVBoxLayout,
161
+ )
162
+ except Exception:
163
+ return list(range(len(scenes)))
164
+
165
+ app = QApplication.instance()
166
+ if app is None:
167
+ return list(range(len(scenes)))
168
+
169
+ dialog = QDialog(_napari_dialog_parent(app))
170
+ dialog.setWindowTitle("Select scenes to load")
171
+ dialog.setMinimumWidth(520)
172
+ _apply_napari_dialog_theme(dialog, app)
173
+
174
+ layout = QVBoxLayout(dialog)
175
+ layout.addWidget(QLabel(f"File: {Path(path).name}"))
176
+ layout.addWidget(QLabel(f"Select scenes to load ({len(scenes)} total):"))
177
+
178
+ scene_list = QListWidget(dialog)
179
+ for index, scene_id in enumerate(scenes):
180
+ scene_name = str(scene_id).strip() or f"Scene {index}"
181
+ item = QListWidgetItem(f"{index}: {scene_name}")
182
+ item.setData(Qt.UserRole, index)
183
+ item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
184
+ item.setCheckState(Qt.Checked)
185
+ scene_list.addItem(item)
186
+ scene_list.setMinimumHeight(300)
187
+ layout.addWidget(scene_list)
188
+
189
+ controls = QHBoxLayout()
190
+ select_all_button = QPushButton("Select all")
191
+ clear_all_button = QPushButton("Clear all")
192
+ controls.addWidget(select_all_button)
193
+ controls.addWidget(clear_all_button)
194
+ controls.addStretch(1)
195
+ layout.addLayout(controls)
196
+
197
+ select_all_button.clicked.connect(
198
+ lambda: _set_scene_checks(scene_list, Qt.Checked)
199
+ )
200
+ clear_all_button.clicked.connect(lambda: _set_scene_checks(scene_list, Qt.Unchecked))
201
+
202
+ buttons = QDialogButtonBox(
203
+ QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=dialog
204
+ )
205
+ layout.addWidget(buttons)
206
+
207
+ def _accept_if_valid() -> None:
208
+ checked = _checked_scene_indices(scene_list)
209
+ if checked:
210
+ dialog.accept()
211
+ return
212
+ QMessageBox.warning(
213
+ dialog,
214
+ "No scenes selected",
215
+ "Select at least one scene to load.",
110
216
  )
111
217
 
112
- return layers
218
+ buttons.accepted.connect(_accept_if_valid)
219
+ buttons.rejected.connect(dialog.reject)
220
+
221
+ if dialog.exec() != QDialog.Accepted:
222
+ return None
223
+ return _checked_scene_indices(scene_list)
224
+
225
+
226
+ def _napari_dialog_parent(app):
227
+ """Return a good parent window for napari-linked dialogs."""
228
+ try:
229
+ import napari
230
+
231
+ viewer = napari.current_viewer()
232
+ except Exception:
233
+ viewer = None
234
+ if viewer is not None:
235
+ window = getattr(viewer, "window", None)
236
+ qt_window = getattr(window, "_qt_window", None)
237
+ if qt_window is not None:
238
+ return qt_window
239
+ return app.activeWindow()
240
+
241
+
242
+ def _apply_napari_dialog_theme(dialog, app) -> None:
243
+ """Apply napari/app stylesheet so popup theming is consistent."""
244
+ stylesheet = ""
245
+ try:
246
+ import napari
247
+
248
+ viewer = napari.current_viewer()
249
+ theme_id = getattr(viewer, "theme", None) if viewer is not None else None
250
+ if theme_id:
251
+ try:
252
+ from napari.qt import get_stylesheet
253
+
254
+ stylesheet = str(get_stylesheet(theme_id) or "")
255
+ except Exception:
256
+ stylesheet = ""
257
+ except Exception:
258
+ stylesheet = ""
259
+
260
+ if not stylesheet:
261
+ try:
262
+ stylesheet = str(app.styleSheet() or "")
263
+ except Exception:
264
+ stylesheet = ""
265
+ if not stylesheet:
266
+ try:
267
+ parent = dialog.parentWidget()
268
+ stylesheet = str(parent.styleSheet() if parent is not None else "")
269
+ except Exception:
270
+ stylesheet = ""
271
+
272
+ if stylesheet:
273
+ dialog.setStyleSheet(stylesheet)
274
+
275
+
276
+ def _set_scene_checks(scene_list, state) -> None:
277
+ """Set check state for all scene list items."""
278
+ for row in range(scene_list.count()):
279
+ item = scene_list.item(row)
280
+ if item is not None:
281
+ item.setCheckState(state)
282
+
283
+
284
+ def _checked_scene_indices(scene_list) -> list[int]:
285
+ """Return checked scene indices from a QListWidget."""
286
+ from qtpy.QtCore import Qt
287
+
288
+ selected: list[int] = []
289
+ for row in range(scene_list.count()):
290
+ item = scene_list.item(row)
291
+ if item is None:
292
+ continue
293
+ if item.checkState() == Qt.Checked:
294
+ selected.append(int(item.data(Qt.UserRole)))
295
+ return selected
113
296
 
114
297
 
115
298
  def _open_bioimage(path: str):
@@ -304,7 +487,7 @@ def _iter_channel_layers(
304
487
  Returns
305
488
  -------
306
489
  list of tuple
307
- Napari layer tuples for each channel.
490
+ napari layer tuples for each channel.
308
491
  """
309
492
  dims = getattr(image, "dims", None)
310
493
  axes_present = _axes_present(image)
@@ -3,6 +3,7 @@
3
3
  from .segmentation.frontend import SegmentationTab
4
4
  from .spots.frontend import SpotsTab
5
5
  from .quantification.frontend import QuantificationTab
6
+ from .visualization.frontend import VisualizationTab
6
7
  from .settings.frontend import SettingsTab
7
8
  from .batch.frontend import BatchTab
8
9
 
@@ -10,6 +11,7 @@ __all__ = [
10
11
  "SegmentationTab",
11
12
  "SpotsTab",
12
13
  "QuantificationTab",
14
+ "VisualizationTab",
13
15
  "SettingsTab",
14
16
  "BatchTab",
15
17
  ]
@@ -208,7 +208,8 @@ class BatchBackend:
208
208
  cyto_channel : str or int or None, optional
209
209
  Channel selection for cytoplasm.
210
210
  cyto_nuclear_channel : str or int or None, optional
211
- Optional nuclear channel used by cytoplasmic models.
211
+ Optional nuclear input for cytoplasmic models. This may be a
212
+ channel selection or a generated nuclear label name.
212
213
  cyto_settings : dict or None, optional
213
214
  Model settings for cytoplasmic segmentation.
214
215
  spot_detector : str or None, optional
@@ -260,6 +261,14 @@ class BatchBackend:
260
261
  cyto_settings = cyto_settings or {}
261
262
  spot_settings = spot_settings or {}
262
263
  quant_backend = QuantificationBackend()
264
+ cyto_model_instance = None
265
+ cyto_nuclear_only = False
266
+ if cyto_model:
267
+ cyto_model_instance = self._segmentation_backend.get_model(cyto_model)
268
+ modes: list[str] = []
269
+ if hasattr(cyto_model_instance, "cytoplasmic_input_modes"):
270
+ modes = cyto_model_instance.cytoplasmic_input_modes()
271
+ cyto_nuclear_only = modes == ["nuclear"]
263
272
 
264
273
  # Count total items to process
265
274
  total_items = 0
@@ -343,40 +352,67 @@ class BatchBackend:
343
352
  output_format,
344
353
  )
345
354
  labels_data[label_name] = masks
346
- labels_meta[label_name] = metadata
355
+ labels_meta[label_name] = _with_task_metadata(
356
+ metadata, "nuclear"
357
+ )
347
358
  item_result.outputs[label_name] = out_path
348
359
 
349
360
  if cyto_model:
350
- channel_idx = resolve_channel_index(
351
- cyto_channel, normalized_channels
352
- )
353
- cyto_image, cyto_meta = load_channel_data(
354
- path, channel_idx, scene_id
355
- )
356
- if cyto_image is None:
357
- raise RuntimeError(
358
- "Failed to read cytoplasmic image data."
359
- )
360
- cyto_layer = Image(cyto_image, "cytoplasmic", cyto_meta)
361
- cyto_nuclear_layer = None
362
- if cyto_nuclear_channel is not None:
363
- nuclear_idx = resolve_channel_index(
364
- cyto_nuclear_channel, normalized_channels
361
+ cyto_layer = None
362
+ cyto_meta: dict = {}
363
+ if not cyto_nuclear_only:
364
+ channel_idx = resolve_channel_index(
365
+ cyto_channel, normalized_channels
365
366
  )
366
- nuclear_image, nuclear_meta = load_channel_data(
367
- path, nuclear_idx, scene_id
367
+ cyto_image, cyto_meta = load_channel_data(
368
+ path, channel_idx, scene_id
368
369
  )
369
- if nuclear_image is None:
370
+ if cyto_image is None:
370
371
  raise RuntimeError(
371
- "Failed to read cytoplasmic nuclear data."
372
+ "Failed to read cytoplasmic image data."
372
373
  )
373
- cyto_nuclear_layer = Image(
374
- nuclear_image, "nuclear", nuclear_meta
374
+ cyto_layer = Image(cyto_image, "cytoplasmic", cyto_meta)
375
+ cyto_nuclear_layer = None
376
+ if cyto_nuclear_channel is not None:
377
+ cyto_nuclear_key = str(cyto_nuclear_channel)
378
+ if (
379
+ cyto_nuclear_only
380
+ and cyto_nuclear_key in labels_data
381
+ ):
382
+ nuclear_meta = labels_meta.get(cyto_nuclear_key, {})
383
+ cyto_nuclear_layer = Labels(
384
+ labels_data[cyto_nuclear_key],
385
+ cyto_nuclear_key,
386
+ nuclear_meta,
387
+ )
388
+ if not cyto_meta:
389
+ cyto_meta = dict(nuclear_meta)
390
+ else:
391
+ nuclear_idx = resolve_channel_index(
392
+ cyto_nuclear_channel, normalized_channels
393
+ )
394
+ nuclear_image, nuclear_meta = load_channel_data(
395
+ path, nuclear_idx, scene_id
396
+ )
397
+ if nuclear_image is None:
398
+ raise RuntimeError(
399
+ "Failed to read cytoplasmic nuclear data."
400
+ )
401
+ cyto_nuclear_layer = Image(
402
+ nuclear_image, "nuclear", nuclear_meta
403
+ )
404
+ if not cyto_meta:
405
+ cyto_meta = dict(nuclear_meta)
406
+ if cyto_nuclear_only and cyto_nuclear_layer is None:
407
+ raise RuntimeError(
408
+ "Selected cytoplasmic model requires nuclear labels."
375
409
  )
376
- model = self._segmentation_backend.get_model(cyto_model)
377
- seg_result = model.run(
410
+ if cyto_model_instance is None:
411
+ raise RuntimeError("Failed to load cytoplasmic model.")
412
+ seg_result = cyto_model_instance.run(
378
413
  task="cytoplasmic",
379
414
  layer=cyto_layer,
415
+ cytoplasmic_layer=cyto_layer,
380
416
  nuclear_layer=cyto_nuclear_layer,
381
417
  settings=cyto_settings,
382
418
  )
@@ -393,7 +429,9 @@ class BatchBackend:
393
429
  output_format,
394
430
  )
395
431
  labels_data[label_name] = masks
396
- labels_meta[label_name] = cyto_meta
432
+ labels_meta[label_name] = _with_task_metadata(
433
+ cyto_meta, "cytoplasmic"
434
+ )
397
435
  item_result.outputs[label_name] = out_path
398
436
 
399
437
  if spot_detector:
@@ -434,7 +472,9 @@ class BatchBackend:
434
472
  output_format,
435
473
  )
436
474
  labels_data[label_name] = mask
437
- labels_meta[label_name] = spot_meta
475
+ labels_meta[label_name] = _with_task_metadata(
476
+ spot_meta, "spots"
477
+ )
438
478
  item_result.outputs[label_name] = out_path
439
479
 
440
480
  if quantification_features:
@@ -575,6 +615,15 @@ def _resolve_output_dir(
575
615
  return output_dir
576
616
 
577
617
 
618
+ def _with_task_metadata(metadata: dict | None, task: str) -> dict:
619
+ """Return a metadata copy with task type attached."""
620
+ payload: dict[str, object] = {}
621
+ if isinstance(metadata, dict):
622
+ payload.update(metadata)
623
+ payload["task"] = task
624
+ return payload
625
+
626
+
578
627
  def _build_viewer_for_quantification(
579
628
  path: Path,
580
629
  scene_id: str | None,