senoquant 1.0.0b2__py3-none-any.whl → 1.0.0b3__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 -2
- senoquant/_reader.py +1 -1
- senoquant/reader/core.py +201 -18
- senoquant/tabs/batch/backend.py +18 -3
- senoquant/tabs/batch/frontend.py +8 -4
- senoquant/tabs/quantification/features/marker/dialog.py +26 -6
- senoquant/tabs/quantification/features/marker/export.py +97 -24
- senoquant/tabs/quantification/features/marker/rows.py +2 -2
- senoquant/tabs/quantification/features/spots/dialog.py +41 -11
- senoquant/tabs/quantification/features/spots/export.py +163 -10
- senoquant/tabs/quantification/frontend.py +2 -2
- senoquant/tabs/segmentation/frontend.py +46 -9
- senoquant/tabs/segmentation/models/cpsam/model.py +1 -1
- senoquant/tabs/segmentation/models/default_2d/model.py +22 -77
- senoquant/tabs/segmentation/models/default_3d/model.py +8 -74
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +13 -13
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/stardist_libs.py +171 -0
- senoquant/tabs/spots/frontend.py +42 -5
- senoquant/tabs/spots/models/ufish/details.json +17 -0
- senoquant/tabs/spots/models/ufish/model.py +129 -0
- senoquant/tabs/spots/ufish_utils/__init__.py +13 -0
- senoquant/tabs/spots/ufish_utils/core.py +357 -0
- senoquant/utils.py +1 -1
- senoquant-1.0.0b3.dist-info/METADATA +161 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b3.dist-info}/RECORD +41 -28
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b3.dist-info}/top_level.txt +1 -0
- ufish/__init__.py +1 -0
- ufish/api.py +778 -0
- ufish/model/__init__.py +0 -0
- ufish/model/loss.py +62 -0
- ufish/model/network/__init__.py +0 -0
- ufish/model/network/spot_learn.py +50 -0
- ufish/model/network/ufish_net.py +204 -0
- ufish/model/train.py +175 -0
- ufish/utils/__init__.py +0 -0
- ufish/utils/img.py +418 -0
- ufish/utils/log.py +8 -0
- ufish/utils/spot_calling.py +115 -0
- senoquant/tabs/spots/models/rmp/details.json +0 -61
- senoquant/tabs/spots/models/rmp/model.py +0 -499
- senoquant/tabs/spots/models/udwt/details.json +0 -103
- senoquant/tabs/spots/models/udwt/model.py +0 -482
- senoquant-1.0.0b2.dist-info/METADATA +0 -193
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b3.dist-info}/WHEEL +0 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b3.dist-info}/entry_points.txt +0 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b3.dist-info}/licenses/LICENSE +0 -0
senoquant/__init__.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"""SenoQuant napari plugin package."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
try:
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
__version__ = version("senoquant")
|
|
6
|
+
except Exception:
|
|
7
|
+
__version__ = "1.0.0b3" # Fallback for development
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
from ._widget import SenoQuantWidget
|
|
6
10
|
__all__ = ["SenoQuantWidget"]
|
senoquant/_reader.py
CHANGED
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
490
|
+
napari layer tuples for each channel.
|
|
308
491
|
"""
|
|
309
492
|
dims = getattr(image, "dims", None)
|
|
310
493
|
axes_present = _axes_present(image)
|
senoquant/tabs/batch/backend.py
CHANGED
|
@@ -343,7 +343,9 @@ class BatchBackend:
|
|
|
343
343
|
output_format,
|
|
344
344
|
)
|
|
345
345
|
labels_data[label_name] = masks
|
|
346
|
-
labels_meta[label_name] =
|
|
346
|
+
labels_meta[label_name] = _with_task_metadata(
|
|
347
|
+
metadata, "nuclear"
|
|
348
|
+
)
|
|
347
349
|
item_result.outputs[label_name] = out_path
|
|
348
350
|
|
|
349
351
|
if cyto_model:
|
|
@@ -393,7 +395,9 @@ class BatchBackend:
|
|
|
393
395
|
output_format,
|
|
394
396
|
)
|
|
395
397
|
labels_data[label_name] = masks
|
|
396
|
-
labels_meta[label_name] =
|
|
398
|
+
labels_meta[label_name] = _with_task_metadata(
|
|
399
|
+
cyto_meta, "cytoplasmic"
|
|
400
|
+
)
|
|
397
401
|
item_result.outputs[label_name] = out_path
|
|
398
402
|
|
|
399
403
|
if spot_detector:
|
|
@@ -434,7 +438,9 @@ class BatchBackend:
|
|
|
434
438
|
output_format,
|
|
435
439
|
)
|
|
436
440
|
labels_data[label_name] = mask
|
|
437
|
-
labels_meta[label_name] =
|
|
441
|
+
labels_meta[label_name] = _with_task_metadata(
|
|
442
|
+
spot_meta, "spots"
|
|
443
|
+
)
|
|
438
444
|
item_result.outputs[label_name] = out_path
|
|
439
445
|
|
|
440
446
|
if quantification_features:
|
|
@@ -575,6 +581,15 @@ def _resolve_output_dir(
|
|
|
575
581
|
return output_dir
|
|
576
582
|
|
|
577
583
|
|
|
584
|
+
def _with_task_metadata(metadata: dict | None, task: str) -> dict:
|
|
585
|
+
"""Return a metadata copy with task type attached."""
|
|
586
|
+
payload: dict[str, object] = {}
|
|
587
|
+
if isinstance(metadata, dict):
|
|
588
|
+
payload.update(metadata)
|
|
589
|
+
payload["task"] = task
|
|
590
|
+
return payload
|
|
591
|
+
|
|
592
|
+
|
|
578
593
|
def _build_viewer_for_quantification(
|
|
579
594
|
path: Path,
|
|
580
595
|
scene_id: str | None,
|
senoquant/tabs/batch/frontend.py
CHANGED
|
@@ -94,7 +94,7 @@ class BatchTab(QWidget):
|
|
|
94
94
|
backend : BatchBackend or None, optional
|
|
95
95
|
Backend instance used to execute batch runs.
|
|
96
96
|
napari_viewer : object or None, optional
|
|
97
|
-
|
|
97
|
+
napari viewer instance for populating layer choices.
|
|
98
98
|
"""
|
|
99
99
|
super().__init__()
|
|
100
100
|
self._viewer = napari_viewer
|
|
@@ -639,18 +639,22 @@ class BatchTab(QWidget):
|
|
|
639
639
|
nuclear_channel = self._nuclear_channel_combo.currentText()
|
|
640
640
|
if nuclear_model and nuclear_channel and not nuclear_model.startswith("("):
|
|
641
641
|
label_name = f"{nuclear_channel}_{nuclear_model}_nuc_labels"
|
|
642
|
-
layers.append(Labels(None, label_name))
|
|
642
|
+
layers.append(Labels(None, label_name, metadata={"task": "nuclear"}))
|
|
643
643
|
if getattr(self, "_cyto_enabled", None) is not None and self._cyto_enabled.isChecked():
|
|
644
644
|
cyto_model = self._cyto_model_combo.currentText()
|
|
645
645
|
cyto_channel = self._cyto_channel_combo.currentText()
|
|
646
646
|
if cyto_model and cyto_channel and not cyto_model.startswith("("):
|
|
647
647
|
label_name = f"{cyto_channel}_{cyto_model}_cyto_labels"
|
|
648
|
-
layers.append(
|
|
648
|
+
layers.append(
|
|
649
|
+
Labels(None, label_name, metadata={"task": "cytoplasmic"})
|
|
650
|
+
)
|
|
649
651
|
if getattr(self, "_spots_enabled", None) is not None and self._spots_enabled.isChecked():
|
|
650
652
|
spot_detector = self._spot_detector_combo.currentText()
|
|
651
653
|
if spot_detector and not spot_detector.startswith("("):
|
|
652
654
|
for label_name in _spot_label_names(self._spot_channel_rows, spot_detector):
|
|
653
|
-
layers.append(
|
|
655
|
+
layers.append(
|
|
656
|
+
Labels(None, label_name, metadata={"task": "spots"})
|
|
657
|
+
)
|
|
654
658
|
self._config_viewer.set_layers(layers)
|
|
655
659
|
|
|
656
660
|
def _update_nuclear_settings(self) -> None:
|
|
@@ -193,28 +193,48 @@ class MarkerChannelsDialog(QDialog):
|
|
|
193
193
|
return
|
|
194
194
|
for layer in viewer.layers:
|
|
195
195
|
if layer.__class__.__name__ == "Labels":
|
|
196
|
-
layer_name = layer.name
|
|
197
196
|
# Only show cellular labels (nuclear/cytoplasmic), exclude spot labels
|
|
198
|
-
if self._is_cellular_label(
|
|
199
|
-
combo.addItem(
|
|
197
|
+
if self._is_cellular_label(layer):
|
|
198
|
+
combo.addItem(layer.name)
|
|
200
199
|
if current:
|
|
201
200
|
index = combo.findText(current)
|
|
202
201
|
if index != -1:
|
|
203
202
|
combo.setCurrentIndex(index)
|
|
204
203
|
|
|
205
|
-
def
|
|
204
|
+
def _layer_task(self, layer: object) -> str | None:
|
|
205
|
+
"""Return normalized segmentation task from layer metadata."""
|
|
206
|
+
metadata = getattr(layer, "metadata", None)
|
|
207
|
+
if not isinstance(metadata, dict):
|
|
208
|
+
return None
|
|
209
|
+
task = metadata.get("task")
|
|
210
|
+
if not isinstance(task, str):
|
|
211
|
+
return None
|
|
212
|
+
normalized = task.strip().lower()
|
|
213
|
+
return normalized or None
|
|
214
|
+
|
|
215
|
+
def _is_cellular_label(self, layer: object | str) -> bool:
|
|
206
216
|
"""Check if a label layer is a cellular segmentation.
|
|
207
217
|
|
|
208
218
|
Parameters
|
|
209
219
|
----------
|
|
210
|
-
|
|
211
|
-
|
|
220
|
+
layer : object or str
|
|
221
|
+
Labels layer object or labels layer name.
|
|
212
222
|
|
|
213
223
|
Returns
|
|
214
224
|
-------
|
|
215
225
|
bool
|
|
216
226
|
True if the layer is a cellular label (nuclear or cytoplasmic).
|
|
217
227
|
"""
|
|
228
|
+
if isinstance(layer, str):
|
|
229
|
+
layer_name = layer
|
|
230
|
+
task = None
|
|
231
|
+
else:
|
|
232
|
+
layer_name = str(getattr(layer, "name", ""))
|
|
233
|
+
task = self._layer_task(layer)
|
|
234
|
+
if task in {"nuclear", "cytoplasmic"}:
|
|
235
|
+
return True
|
|
236
|
+
if task is not None:
|
|
237
|
+
return False
|
|
218
238
|
return layer_name.endswith("_nuc_labels") or layer_name.endswith("_cyto_labels")
|
|
219
239
|
|
|
220
240
|
def _refresh_image_combo(self, combo: QComboBox) -> None:
|
|
@@ -41,7 +41,7 @@ def export_marker(
|
|
|
41
41
|
temp_dir : Path
|
|
42
42
|
Temporary directory where outputs should be written.
|
|
43
43
|
viewer : object, optional
|
|
44
|
-
|
|
44
|
+
napari viewer instance used to resolve layers by name.
|
|
45
45
|
export_format : str, optional
|
|
46
46
|
File format for exports (``"csv"`` or ``"xlsx"``).
|
|
47
47
|
enable_thresholds : bool, optional
|
|
@@ -76,6 +76,23 @@ def export_marker(
|
|
|
76
76
|
if metadata_path is not None:
|
|
77
77
|
outputs.append(metadata_path)
|
|
78
78
|
|
|
79
|
+
all_segmentations: dict[str, tuple[np.ndarray, np.ndarray]] = {}
|
|
80
|
+
for segmentation in data.segmentations:
|
|
81
|
+
seg_name = segmentation.label.strip()
|
|
82
|
+
if not seg_name:
|
|
83
|
+
continue
|
|
84
|
+
seg_layer = _find_layer(viewer, seg_name, "Labels")
|
|
85
|
+
if seg_layer is None:
|
|
86
|
+
continue
|
|
87
|
+
seg_labels = layer_data_asarray(seg_layer)
|
|
88
|
+
if seg_labels.size == 0:
|
|
89
|
+
continue
|
|
90
|
+
seg_label_ids, _seg_centroids = _compute_centroids(seg_labels)
|
|
91
|
+
if seg_label_ids.size == 0:
|
|
92
|
+
continue
|
|
93
|
+
all_segmentations[seg_name] = (seg_labels, seg_label_ids)
|
|
94
|
+
cross_map = _build_cross_segmentation_map(all_segmentations)
|
|
95
|
+
|
|
79
96
|
for index, segmentation in enumerate(data.segmentations, start=0):
|
|
80
97
|
label_name = segmentation.label.strip()
|
|
81
98
|
if not label_name:
|
|
@@ -103,7 +120,7 @@ def export_marker(
|
|
|
103
120
|
break
|
|
104
121
|
rows = _initialize_rows(label_ids, centroids, pixel_sizes)
|
|
105
122
|
_add_roi_columns(rows, labels, label_ids, viewer, data.rois, label_name)
|
|
106
|
-
|
|
123
|
+
_morph_columns = add_morphology_columns(
|
|
107
124
|
rows, labels, label_ids, pixel_sizes
|
|
108
125
|
)
|
|
109
126
|
|
|
@@ -115,11 +132,12 @@ def export_marker(
|
|
|
115
132
|
metadata = getattr(first_channel_layer, "metadata", {})
|
|
116
133
|
file_path = metadata.get("path")
|
|
117
134
|
|
|
118
|
-
# Determine segmentation type from
|
|
119
|
-
seg_type =
|
|
120
|
-
|
|
135
|
+
# Determine segmentation type from labels metadata with suffix fallback.
|
|
136
|
+
seg_type = _segmentation_type_from_layer(labels_layer, label_name)
|
|
137
|
+
_ref_columns = _add_reference_columns(
|
|
121
138
|
rows, labels, label_ids, file_path, seg_type
|
|
122
139
|
)
|
|
140
|
+
_add_cross_reference_column(rows, label_name, label_ids, cross_map)
|
|
123
141
|
|
|
124
142
|
header = list(rows[0].keys()) if rows else []
|
|
125
143
|
|
|
@@ -207,7 +225,7 @@ def _find_layer(viewer, name: str, layer_type: str):
|
|
|
207
225
|
Parameters
|
|
208
226
|
----------
|
|
209
227
|
viewer : object
|
|
210
|
-
|
|
228
|
+
napari viewer instance containing layers.
|
|
211
229
|
name : str
|
|
212
230
|
Layer name to locate.
|
|
213
231
|
layer_type : str
|
|
@@ -224,6 +242,40 @@ def _find_layer(viewer, name: str, layer_type: str):
|
|
|
224
242
|
return None
|
|
225
243
|
|
|
226
244
|
|
|
245
|
+
def _segmentation_type_from_layer(layer: object, label_name: str) -> str:
|
|
246
|
+
"""Return segmentation type from layer metadata or legacy suffixes.
|
|
247
|
+
|
|
248
|
+
Parameters
|
|
249
|
+
----------
|
|
250
|
+
layer : object
|
|
251
|
+
Labels layer object.
|
|
252
|
+
label_name : str
|
|
253
|
+
Labels layer name used for fallback suffix parsing.
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
-------
|
|
257
|
+
str
|
|
258
|
+
Segmentation type ("nuclear" or "cytoplasmic").
|
|
259
|
+
|
|
260
|
+
Notes
|
|
261
|
+
-----
|
|
262
|
+
Metadata value ``metadata["task"]`` is authoritative when valid.
|
|
263
|
+
Legacy layer-name suffixes are used as fallback for older layers.
|
|
264
|
+
"""
|
|
265
|
+
metadata = getattr(layer, "metadata", None)
|
|
266
|
+
if isinstance(metadata, dict):
|
|
267
|
+
task = metadata.get("task")
|
|
268
|
+
if isinstance(task, str):
|
|
269
|
+
normalized = task.strip().lower()
|
|
270
|
+
if normalized in {"nuclear", "cytoplasmic"}:
|
|
271
|
+
return normalized
|
|
272
|
+
if label_name.endswith("_cyto_labels"):
|
|
273
|
+
return "cytoplasmic"
|
|
274
|
+
if label_name.endswith("_nuc_labels"):
|
|
275
|
+
return "nuclear"
|
|
276
|
+
return "nuclear"
|
|
277
|
+
|
|
278
|
+
|
|
227
279
|
def _compute_centroids(labels: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
|
228
280
|
"""Compute centroid coordinates for each non-zero label.
|
|
229
281
|
|
|
@@ -299,7 +351,7 @@ def _pixel_volume(layer, ndim: int) -> float:
|
|
|
299
351
|
Parameters
|
|
300
352
|
----------
|
|
301
353
|
layer : object
|
|
302
|
-
|
|
354
|
+
napari image layer providing metadata.
|
|
303
355
|
ndim : int
|
|
304
356
|
Dimensionality of the image data.
|
|
305
357
|
|
|
@@ -348,7 +400,7 @@ def _pixel_sizes(layer, ndim: int) -> np.ndarray | None:
|
|
|
348
400
|
Parameters
|
|
349
401
|
----------
|
|
350
402
|
layer : object
|
|
351
|
-
|
|
403
|
+
napari image layer providing metadata.
|
|
352
404
|
ndim : int
|
|
353
405
|
Dimensionality of the image data.
|
|
354
406
|
|
|
@@ -429,7 +481,7 @@ def _add_roi_columns(
|
|
|
429
481
|
label_ids : numpy.ndarray
|
|
430
482
|
Label ids corresponding to the output rows.
|
|
431
483
|
viewer : object or None
|
|
432
|
-
|
|
484
|
+
napari viewer used to resolve shapes layers.
|
|
433
485
|
rois : sequence of ROIConfig
|
|
434
486
|
ROI configuration entries to evaluate.
|
|
435
487
|
label_name : str
|
|
@@ -480,7 +532,7 @@ def _shapes_layer_mask(
|
|
|
480
532
|
Parameters
|
|
481
533
|
----------
|
|
482
534
|
layer : object
|
|
483
|
-
|
|
535
|
+
napari shapes layer instance.
|
|
484
536
|
shape : tuple of int
|
|
485
537
|
Target mask shape matching the labels array.
|
|
486
538
|
|
|
@@ -776,25 +828,46 @@ def _build_cross_segmentation_map(
|
|
|
776
828
|
-----
|
|
777
829
|
This function identifies which labels from different segmentations
|
|
778
830
|
overlap spatially, enabling cross-referencing between tables.
|
|
831
|
+
The resulting mapping is bidirectional: if ``A:1`` overlaps ``B:2``,
|
|
832
|
+
both keys receive a reference to the other.
|
|
779
833
|
"""
|
|
780
834
|
cross_map: dict[tuple[str, int], list[tuple[str, int]]] = {}
|
|
835
|
+
valid_ids: dict[str, set[int]] = {}
|
|
836
|
+
|
|
837
|
+
for seg_name, (_labels, label_ids) in all_segmentations.items():
|
|
838
|
+
ids = {int(label_id) for label_id in np.asarray(label_ids, dtype=int)}
|
|
839
|
+
valid_ids[seg_name] = ids
|
|
840
|
+
for label_id in ids:
|
|
841
|
+
cross_map[(seg_name, label_id)] = []
|
|
781
842
|
|
|
782
843
|
seg_names = list(all_segmentations.keys())
|
|
783
844
|
for i, seg1_name in enumerate(seg_names):
|
|
784
|
-
labels1,
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
845
|
+
labels1, _label_ids1 = all_segmentations[seg1_name]
|
|
846
|
+
# Check overlaps with all other segmentations.
|
|
847
|
+
for seg2_name in seg_names[i + 1 :]:
|
|
848
|
+
labels2, _label_ids2 = all_segmentations[seg2_name]
|
|
849
|
+
if labels1.shape != labels2.shape:
|
|
850
|
+
warnings.warn(
|
|
851
|
+
"Marker export: segmentation shape mismatch for "
|
|
852
|
+
f"'{seg1_name}' vs '{seg2_name}'. "
|
|
853
|
+
"Skipping cross-segmentation overlap mapping for this pair.",
|
|
854
|
+
RuntimeWarning,
|
|
855
|
+
)
|
|
856
|
+
continue
|
|
857
|
+
|
|
858
|
+
mask = (labels1 > 0) & (labels2 > 0)
|
|
859
|
+
if not np.any(mask):
|
|
860
|
+
continue
|
|
861
|
+
|
|
862
|
+
overlap_pairs = np.column_stack((labels1[mask], labels2[mask]))
|
|
863
|
+
unique_pairs = np.unique(overlap_pairs, axis=0)
|
|
864
|
+
for label_id1, label_id2 in unique_pairs:
|
|
865
|
+
id1 = int(label_id1)
|
|
866
|
+
id2 = int(label_id2)
|
|
867
|
+
if id1 not in valid_ids[seg1_name] or id2 not in valid_ids[seg2_name]:
|
|
868
|
+
continue
|
|
869
|
+
cross_map[(seg1_name, id1)].append((seg2_name, id2))
|
|
870
|
+
cross_map[(seg2_name, id2)].append((seg1_name, id1))
|
|
798
871
|
|
|
799
872
|
return cross_map
|
|
800
873
|
|
|
@@ -593,7 +593,7 @@ class MarkerChannelRow(QGroupBox):
|
|
|
593
593
|
slider : QWidget
|
|
594
594
|
Range slider widget.
|
|
595
595
|
layer : object
|
|
596
|
-
|
|
596
|
+
napari image layer providing intensity bounds.
|
|
597
597
|
min_spin : QDoubleSpinBox or None
|
|
598
598
|
Spin box that displays the minimum threshold value.
|
|
599
599
|
max_spin : QDoubleSpinBox or None
|
|
@@ -628,7 +628,7 @@ class MarkerChannelRow(QGroupBox):
|
|
|
628
628
|
Parameters
|
|
629
629
|
----------
|
|
630
630
|
layer : object
|
|
631
|
-
|
|
631
|
+
napari image layer providing contrast bounds and data.
|
|
632
632
|
|
|
633
633
|
Returns
|
|
634
634
|
-------
|