TwoPy 0.1.0__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.
- twopy/__init__.py +168 -0
- twopy/_segments.py +53 -0
- twopy/analysis/__init__.py +101 -0
- twopy/analysis/background_subtraction.py +672 -0
- twopy/analysis/dff.py +524 -0
- twopy/analysis/epoch_mapping.py +223 -0
- twopy/analysis/motion.py +66 -0
- twopy/analysis/persistence.py +975 -0
- twopy/analysis/response_processing/__init__.py +55 -0
- twopy/analysis/response_processing/apply.py +298 -0
- twopy/analysis/response_processing/options.py +192 -0
- twopy/analysis/response_processing/qc.py +270 -0
- twopy/analysis/response_processing/signals.py +200 -0
- twopy/analysis/responses.py +345 -0
- twopy/analysis/trials.py +341 -0
- twopy/analysis/workflow.py +457 -0
- twopy/api.py +78 -0
- twopy/config.py +200 -0
- twopy/conversion/__init__.py +40 -0
- twopy/conversion/alignment_crop.py +329 -0
- twopy/conversion/api.py +113 -0
- twopy/conversion/frame_ranges.py +35 -0
- twopy/conversion/hdf5_writing.py +362 -0
- twopy/conversion/matlab_values.py +123 -0
- twopy/conversion/source_loading.py +403 -0
- twopy/conversion/stimulus_code.py +248 -0
- twopy/conversion/types.py +295 -0
- twopy/converted.py +681 -0
- twopy/database/__init__.py +33 -0
- twopy/database/cache.py +166 -0
- twopy/database/catalog.py +90 -0
- twopy/database/connection.py +27 -0
- twopy/database/generic.py +254 -0
- twopy/database/modeled.py +419 -0
- twopy/database/sql.py +46 -0
- twopy/database/types.py +116 -0
- twopy/inspection.py +492 -0
- twopy/matlab.py +398 -0
- twopy/napari/__init__.py +27 -0
- twopy/napari/__main__.py +10 -0
- twopy/napari/constants.py +13 -0
- twopy/napari/controls.py +739 -0
- twopy/napari/display.py +181 -0
- twopy/napari/display_paths.py +197 -0
- twopy/napari/interactive.py +310 -0
- twopy/napari/launcher.py +175 -0
- twopy/napari/layout.py +147 -0
- twopy/napari/loading.py +207 -0
- twopy/napari/movie.py +71 -0
- twopy/napari/paths.py +181 -0
- twopy/napari/plotting/__init__.py +21 -0
- twopy/napari/plotting/data.py +266 -0
- twopy/napari/plotting/docks.py +1052 -0
- twopy/napari/plotting/export.py +803 -0
- twopy/napari/plotting/export_controls.py +209 -0
- twopy/napari/plotting/label_visibility.py +260 -0
- twopy/napari/plotting/options.py +269 -0
- twopy/napari/plotting/panels.py +119 -0
- twopy/napari/plotting/widgets.py +767 -0
- twopy/napari/protocols.py +90 -0
- twopy/napari/responses.py +91 -0
- twopy/napari/roi.py +238 -0
- twopy/napari/session.py +381 -0
- twopy/napari/state.py +106 -0
- twopy/napari/text.py +26 -0
- twopy/napari/types.py +39 -0
- twopy/napari/viewer.py +405 -0
- twopy/parity/__init__.py +30 -0
- twopy/parity/dff.py +399 -0
- twopy/parity/roi.py +145 -0
- twopy/parity/saved_analysis.py +174 -0
- twopy/photodiode.py +375 -0
- twopy/photodiode_classification.py +484 -0
- twopy/roi.py +462 -0
- twopy/session.py +167 -0
- twopy/spatial.py +154 -0
- twopy/stimulus.py +233 -0
- twopy/synchronization.py +235 -0
- twopy/typing_guards.py +255 -0
- twopy-0.1.0.dist-info/METADATA +311 -0
- twopy-0.1.0.dist-info/RECORD +84 -0
- twopy-0.1.0.dist-info/WHEEL +5 -0
- twopy-0.1.0.dist-info/entry_points.txt +2 -0
- twopy-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Qt controls that trigger response-plot exports.
|
|
2
|
+
|
|
3
|
+
Inputs: a callable that returns the current response export state.
|
|
4
|
+
Outputs: one Export tab with buttons for recording images, ROI overlays, and
|
|
5
|
+
response plots.
|
|
6
|
+
|
|
7
|
+
This module owns export button wiring only. It keeps the response dock focused
|
|
8
|
+
on live plot state while the pure export functions write the actual figures.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from qtpy.QtWidgets import QLabel, QPushButton, QVBoxLayout, QWidget
|
|
16
|
+
|
|
17
|
+
from twopy.converted import RecordingData
|
|
18
|
+
from twopy.napari.display_paths import format_output_folder
|
|
19
|
+
from twopy.napari.plotting.data import ResponsePlotData
|
|
20
|
+
from twopy.napari.plotting.export import (
|
|
21
|
+
export_epoch_plots,
|
|
22
|
+
export_epoch_roi_overlay_plots,
|
|
23
|
+
export_recording_roi_overlay,
|
|
24
|
+
export_recording_view,
|
|
25
|
+
export_roi_view,
|
|
26
|
+
)
|
|
27
|
+
from twopy.napari.text import counted_noun
|
|
28
|
+
|
|
29
|
+
__all__ = ["ResponseExportState", "create_response_export_tab"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ResponseExportState:
|
|
34
|
+
"""Current napari and response-plot state needed for export actions.
|
|
35
|
+
|
|
36
|
+
Inputs: viewer, loaded recording, ROI layer, plot data, visible selections,
|
|
37
|
+
axis bounds, and output folder.
|
|
38
|
+
Outputs: immutable state object consumed by export buttons.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
viewer: object | None
|
|
42
|
+
recording: RecordingData | None
|
|
43
|
+
roi_labels_layer: object | None
|
|
44
|
+
plot_data: ResponsePlotData | None
|
|
45
|
+
output_dir: Path
|
|
46
|
+
roi_label_values: tuple[int, ...]
|
|
47
|
+
roi_colors: tuple[str, ...]
|
|
48
|
+
epoch_indices: tuple[int, ...]
|
|
49
|
+
roi_indices: tuple[int, ...]
|
|
50
|
+
show_sem: bool
|
|
51
|
+
time_bounds: tuple[float, float]
|
|
52
|
+
value_bounds: tuple[float, float]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_response_export_tab(
|
|
56
|
+
get_state: Callable[[], ResponseExportState],
|
|
57
|
+
) -> QWidget:
|
|
58
|
+
"""Create the response export tab.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
get_state: Callback returning current response and napari state.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Qt widget with export buttons and status text.
|
|
65
|
+
"""
|
|
66
|
+
status_label = QLabel("Exports save beside the recording.")
|
|
67
|
+
status_label.setWordWrap(True)
|
|
68
|
+
tab = QWidget()
|
|
69
|
+
layout = QVBoxLayout()
|
|
70
|
+
for text, callback in (
|
|
71
|
+
("Save recording view", _save_recording_view),
|
|
72
|
+
("Save ROI view", _save_roi_view),
|
|
73
|
+
("Save recording ROI overlay", _save_recording_roi_overlay),
|
|
74
|
+
("Save plots", _save_plots),
|
|
75
|
+
("Save plots with ROIs", _save_plots_with_rois),
|
|
76
|
+
):
|
|
77
|
+
layout.addWidget(_button(text, get_state, status_label, callback))
|
|
78
|
+
layout.addWidget(status_label)
|
|
79
|
+
layout.addStretch(1)
|
|
80
|
+
tab.setLayout(layout)
|
|
81
|
+
return tab
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _save_recording_view(state: ResponseExportState) -> tuple[Path, ...] | str:
|
|
85
|
+
"""Export the current movie frame or mean-image fallback."""
|
|
86
|
+
if state.recording is None:
|
|
87
|
+
return "No recording loaded."
|
|
88
|
+
return export_recording_view(
|
|
89
|
+
viewer=state.viewer,
|
|
90
|
+
recording=state.recording,
|
|
91
|
+
output_dir=state.output_dir,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _save_roi_view(state: ResponseExportState) -> tuple[Path, ...] | str:
|
|
96
|
+
"""Export current ROI contours without a recording image."""
|
|
97
|
+
if state.recording is None:
|
|
98
|
+
return "No recording loaded."
|
|
99
|
+
return export_roi_view(
|
|
100
|
+
roi_labels_layer=state.roi_labels_layer,
|
|
101
|
+
recording=state.recording,
|
|
102
|
+
output_dir=state.output_dir,
|
|
103
|
+
roi_label_values=state.roi_label_values,
|
|
104
|
+
roi_colors=state.roi_colors,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _save_recording_roi_overlay(
|
|
109
|
+
state: ResponseExportState,
|
|
110
|
+
) -> tuple[Path, ...] | str:
|
|
111
|
+
"""Export current recording image with ROI contours overlaid."""
|
|
112
|
+
if state.recording is None:
|
|
113
|
+
return "No recording loaded."
|
|
114
|
+
return export_recording_roi_overlay(
|
|
115
|
+
viewer=state.viewer,
|
|
116
|
+
recording=state.recording,
|
|
117
|
+
roi_labels_layer=state.roi_labels_layer,
|
|
118
|
+
output_dir=state.output_dir,
|
|
119
|
+
roi_label_values=state.roi_label_values,
|
|
120
|
+
roi_colors=state.roi_colors,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _save_plots(state: ResponseExportState) -> tuple[Path, ...] | str:
|
|
125
|
+
"""Export each visible epoch response plot."""
|
|
126
|
+
if state.plot_data is None:
|
|
127
|
+
return "No responses available."
|
|
128
|
+
return export_epoch_plots(
|
|
129
|
+
plot_data=state.plot_data,
|
|
130
|
+
output_dir=state.output_dir,
|
|
131
|
+
epoch_indices=state.epoch_indices,
|
|
132
|
+
roi_indices=state.roi_indices,
|
|
133
|
+
roi_colors=state.roi_colors,
|
|
134
|
+
show_sem=state.show_sem,
|
|
135
|
+
time_bounds=state.time_bounds,
|
|
136
|
+
value_bounds=state.value_bounds,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _save_plots_with_rois(state: ResponseExportState) -> tuple[Path, ...] | str:
|
|
141
|
+
"""Export two-column ROI overlay plus response plot figures."""
|
|
142
|
+
if state.recording is None:
|
|
143
|
+
return "No recording loaded."
|
|
144
|
+
if state.plot_data is None:
|
|
145
|
+
return "No responses available."
|
|
146
|
+
return export_epoch_roi_overlay_plots(
|
|
147
|
+
viewer=state.viewer,
|
|
148
|
+
recording=state.recording,
|
|
149
|
+
roi_labels_layer=state.roi_labels_layer,
|
|
150
|
+
plot_data=state.plot_data,
|
|
151
|
+
output_dir=state.output_dir,
|
|
152
|
+
epoch_indices=state.epoch_indices,
|
|
153
|
+
roi_indices=state.roi_indices,
|
|
154
|
+
roi_label_values=state.roi_label_values,
|
|
155
|
+
roi_colors=state.roi_colors,
|
|
156
|
+
show_sem=state.show_sem,
|
|
157
|
+
time_bounds=state.time_bounds,
|
|
158
|
+
value_bounds=state.value_bounds,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _button(
|
|
163
|
+
text: str,
|
|
164
|
+
get_state: Callable[[], ResponseExportState],
|
|
165
|
+
status_label: QLabel,
|
|
166
|
+
callback: Callable[[ResponseExportState], tuple[Path, ...] | str],
|
|
167
|
+
) -> QPushButton:
|
|
168
|
+
"""Create one export button.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
text: Button label.
|
|
172
|
+
get_state: Callback returning current export state.
|
|
173
|
+
status_label: Label updated after the button runs.
|
|
174
|
+
callback: Export action for this button.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Configured Qt button.
|
|
178
|
+
"""
|
|
179
|
+
button = QPushButton(text)
|
|
180
|
+
|
|
181
|
+
def run_export() -> None:
|
|
182
|
+
"""Run the export action and show a short status."""
|
|
183
|
+
state = get_state()
|
|
184
|
+
result = callback(state)
|
|
185
|
+
status_label.setText(_export_status(result, state.recording))
|
|
186
|
+
|
|
187
|
+
button.clicked.connect(run_export)
|
|
188
|
+
return button
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _export_status(
|
|
192
|
+
result: tuple[Path, ...] | str,
|
|
193
|
+
recording: RecordingData | None,
|
|
194
|
+
) -> str:
|
|
195
|
+
"""Return a compact export status message.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
result: Either an error/status string or written files.
|
|
199
|
+
recording: Optional selected recording for compact folder display.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
User-facing status string.
|
|
203
|
+
"""
|
|
204
|
+
if isinstance(result, str):
|
|
205
|
+
return result
|
|
206
|
+
if len(result) == 0:
|
|
207
|
+
return "No files exported."
|
|
208
|
+
folder = format_output_folder(result[0].parent, recording)
|
|
209
|
+
return f"Saved {counted_noun(len(result), 'file')} to {folder}"
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Synchronize plot ROI visibility with the napari Labels overlay.
|
|
2
|
+
|
|
3
|
+
Inputs: the current napari Labels layer, ROI labels, ROI visibility state, and
|
|
4
|
+
ROI colors.
|
|
5
|
+
Outputs: a Labels-layer colormap where deselected ROIs are dimmed.
|
|
6
|
+
|
|
7
|
+
This module only changes how labels are displayed. It never edits the label
|
|
8
|
+
image, so hiding an ROI in the plot options cannot delete ROI data.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from typing import Any, Protocol, cast
|
|
13
|
+
|
|
14
|
+
from qtpy.QtGui import QColor
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"apply_roi_visibility_to_labels_layer",
|
|
18
|
+
"roi_label_value_from_label",
|
|
19
|
+
"roi_label_values_from_labels",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
_LABEL_COLOR_LOOKAHEAD = 4096
|
|
23
|
+
_BASE_LABEL_COLORMAP_METADATA_KEY = "twopy_base_label_colormap"
|
|
24
|
+
_DESELECTED_ROI_ALPHA = 0.2
|
|
25
|
+
type VisibilityKey = int | str
|
|
26
|
+
type VisibilityState = Mapping[int, bool] | Mapping[str, bool]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _LabelsLayerWithColormap(Protocol):
|
|
30
|
+
"""Small protocol for napari Labels layers with settable colormaps."""
|
|
31
|
+
|
|
32
|
+
colormap: object
|
|
33
|
+
metadata: dict[str, object]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _LabelColormapLike(Protocol):
|
|
37
|
+
"""Small protocol for napari label colormaps used for color sampling."""
|
|
38
|
+
|
|
39
|
+
colors: object
|
|
40
|
+
seed: object
|
|
41
|
+
background_value: object
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def apply_roi_visibility_to_labels_layer(
|
|
45
|
+
layer: object | None,
|
|
46
|
+
*,
|
|
47
|
+
roi_labels: tuple[str, ...],
|
|
48
|
+
visibility: VisibilityState,
|
|
49
|
+
colors: tuple[QColor, ...],
|
|
50
|
+
keys: tuple[VisibilityKey, ...] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Apply ROI visibility state to the napari Labels layer display.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
layer: Current napari Labels layer, if available.
|
|
56
|
+
roi_labels: ROI names in analysis/plot order.
|
|
57
|
+
visibility: Visibility by ROI name, or by ``keys`` when keys are
|
|
58
|
+
provided.
|
|
59
|
+
colors: ROI colors in the same order as ``roi_labels``.
|
|
60
|
+
keys: Optional stable visibility keys matching ``roi_labels``.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
None.
|
|
64
|
+
|
|
65
|
+
Deselected ROIs keep their label pixels in ``layer.data``; only their
|
|
66
|
+
display alpha is reduced. Future labels stay drawable because the direct
|
|
67
|
+
colormap contains napari-style colors for a broad range of label values
|
|
68
|
+
instead of a single fallback color.
|
|
69
|
+
"""
|
|
70
|
+
if layer is None or not hasattr(layer, "colormap"):
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
from napari.utils.colormaps import direct_colormap
|
|
75
|
+
except ImportError:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
labels_layer = cast(_LabelsLayerWithColormap, layer)
|
|
79
|
+
visibility_lookup = dict(visibility.items())
|
|
80
|
+
color_dict = _visible_label_color_dict(
|
|
81
|
+
labels_layer,
|
|
82
|
+
roi_labels=roi_labels,
|
|
83
|
+
)
|
|
84
|
+
for roi_index, roi_label in enumerate(roi_labels):
|
|
85
|
+
label_value = roi_label_value_from_label(
|
|
86
|
+
roi_label,
|
|
87
|
+
fallback=roi_index + 1,
|
|
88
|
+
)
|
|
89
|
+
key = (
|
|
90
|
+
keys[roi_index] if keys is not None and roi_index < len(keys) else roi_label
|
|
91
|
+
)
|
|
92
|
+
color = colors[roi_index] if roi_index < len(colors) else QColor("#4cc9f0")
|
|
93
|
+
color_dict[label_value] = _qcolor_rgba(color, visibility_lookup.get(key, True))
|
|
94
|
+
|
|
95
|
+
labels_layer.colormap = direct_colormap(color_dict)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def roi_label_value_from_label(label: str, *, fallback: int) -> int:
|
|
99
|
+
"""Return the napari integer label represented by one ROI label.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
label: ROI label from analysis output, usually ``roi_0001``.
|
|
103
|
+
fallback: One-based ROI position used when the label has no numeric
|
|
104
|
+
suffix.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Positive integer label value for the napari Labels layer.
|
|
108
|
+
|
|
109
|
+
napari lets users draw label ``10`` without drawing labels ``1`` through
|
|
110
|
+
``9``. ``make_roi_set_from_label_image`` preserves that value in labels like
|
|
111
|
+
``roi_0010``, so visibility needs to target label ``10`` instead of the
|
|
112
|
+
first plotted ROI position.
|
|
113
|
+
"""
|
|
114
|
+
suffix = label.rsplit("_", maxsplit=1)[-1]
|
|
115
|
+
if suffix.isdecimal():
|
|
116
|
+
return int(suffix)
|
|
117
|
+
return fallback
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def roi_label_values_from_labels(roi_labels: tuple[str, ...]) -> tuple[int, ...]:
|
|
121
|
+
"""Return napari integer Labels values for plot ROI labels.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
roi_labels: ROI labels in plot order.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Integer Labels-layer values in the same order.
|
|
128
|
+
"""
|
|
129
|
+
return tuple(
|
|
130
|
+
roi_label_value_from_label(roi_label, fallback=index + 1)
|
|
131
|
+
for index, roi_label in enumerate(roi_labels)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _qcolor_rgba(color: QColor, visible: bool) -> tuple[float, float, float, float]:
|
|
136
|
+
"""Return a normalized RGBA tuple for one ROI label.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
color: ROI color.
|
|
140
|
+
visible: Whether the ROI should be fully visible in the Labels overlay.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
RGBA tuple in napari's expected ``0`` to ``1`` float range.
|
|
144
|
+
"""
|
|
145
|
+
return (
|
|
146
|
+
color.redF(),
|
|
147
|
+
color.greenF(),
|
|
148
|
+
color.blueF(),
|
|
149
|
+
1.0 if visible else _DESELECTED_ROI_ALPHA,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _visible_label_color_dict(
|
|
154
|
+
layer: _LabelsLayerWithColormap,
|
|
155
|
+
*,
|
|
156
|
+
roi_labels: tuple[str, ...],
|
|
157
|
+
) -> dict[int | None, object]:
|
|
158
|
+
"""Return visible colors for known and future Labels values.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
layer: napari Labels layer.
|
|
162
|
+
roi_labels: ROI names in analysis/plot order.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Mapping from label values to RGBA colors for napari's direct colormap.
|
|
166
|
+
"""
|
|
167
|
+
color_count = max(
|
|
168
|
+
_LABEL_COLOR_LOOKAHEAD,
|
|
169
|
+
_max_roi_label_value(roi_labels) + 512,
|
|
170
|
+
)
|
|
171
|
+
color_dict: dict[int | None, object] = {0: "transparent"}
|
|
172
|
+
for label_value in range(1, color_count + 1):
|
|
173
|
+
color_dict[label_value] = _base_label_rgba(layer, label_value)
|
|
174
|
+
color_dict[None] = color_dict[color_count]
|
|
175
|
+
return color_dict
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _max_roi_label_value(roi_labels: tuple[str, ...]) -> int:
|
|
179
|
+
"""Return the largest integer label represented by ROI labels.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
roi_labels: ROI names in analysis/plot order.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Maximum positive Labels value, or ``0`` when there are no ROIs.
|
|
186
|
+
"""
|
|
187
|
+
if len(roi_labels) == 0:
|
|
188
|
+
return 0
|
|
189
|
+
return max(
|
|
190
|
+
roi_label_value_from_label(roi_label, fallback=index + 1)
|
|
191
|
+
for index, roi_label in enumerate(roi_labels)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _base_label_rgba(
|
|
196
|
+
layer: _LabelsLayerWithColormap,
|
|
197
|
+
label_value: int,
|
|
198
|
+
) -> tuple[float, float, float, float]:
|
|
199
|
+
"""Return napari's cyclic label color for one label value.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
layer: napari Labels layer.
|
|
203
|
+
label_value: Positive integer label value.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
RGBA tuple in ``0`` to ``1`` float range.
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
from napari.utils.colormaps import label_colormap
|
|
210
|
+
except ImportError:
|
|
211
|
+
return _qcolor_rgba(QColor("#4cc9f0"), True)
|
|
212
|
+
|
|
213
|
+
params = _base_label_colormap_params(layer)
|
|
214
|
+
colormap = label_colormap(
|
|
215
|
+
num_colors=params["num_colors"],
|
|
216
|
+
seed=params["seed"],
|
|
217
|
+
background_value=params["background_value"],
|
|
218
|
+
)
|
|
219
|
+
rgba = colormap.map(label_value)
|
|
220
|
+
return (
|
|
221
|
+
float(rgba[0]),
|
|
222
|
+
float(rgba[1]),
|
|
223
|
+
float(rgba[2]),
|
|
224
|
+
float(rgba[3]),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _base_label_colormap_params(
|
|
229
|
+
layer: _LabelsLayerWithColormap,
|
|
230
|
+
) -> dict[str, int | float]:
|
|
231
|
+
"""Return stable cyclic-colormap parameters for one Labels layer.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
layer: napari Labels layer.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Plain parameters for reconstructing napari's cyclic label colormap.
|
|
238
|
+
|
|
239
|
+
The first call records the original cyclic colormap. Later calls may see a
|
|
240
|
+
direct visibility colormap, so reading from metadata preserves the original
|
|
241
|
+
color cycle while users toggle ROI visibility.
|
|
242
|
+
"""
|
|
243
|
+
stored = layer.metadata.get(_BASE_LABEL_COLORMAP_METADATA_KEY)
|
|
244
|
+
if isinstance(stored, dict):
|
|
245
|
+
stored_params = cast(dict[str, object], stored)
|
|
246
|
+
return {
|
|
247
|
+
"num_colors": int(cast(Any, stored_params["num_colors"])),
|
|
248
|
+
"seed": float(cast(Any, stored_params["seed"])),
|
|
249
|
+
"background_value": int(cast(Any, stored_params["background_value"])),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
colormap = cast(_LabelColormapLike, layer.colormap)
|
|
253
|
+
colors = getattr(colormap, "colors", ())
|
|
254
|
+
params = {
|
|
255
|
+
"num_colors": max(len(colors) - 1, 1),
|
|
256
|
+
"seed": float(getattr(colormap, "seed", 0.5)),
|
|
257
|
+
"background_value": int(getattr(colormap, "background_value", 0)),
|
|
258
|
+
}
|
|
259
|
+
layer.metadata[_BASE_LABEL_COLORMAP_METADATA_KEY] = params
|
|
260
|
+
return params
|