napari-sprout 1.1.0__tar.gz
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.
- napari_sprout-1.1.0/LICENSE +21 -0
- napari_sprout-1.1.0/PKG-INFO +58 -0
- napari_sprout-1.1.0/README.md +28 -0
- napari_sprout-1.1.0/napari_sprout/__init__.py +14 -0
- napari_sprout-1.1.0/napari_sprout/_widget.py +262 -0
- napari_sprout-1.1.0/napari_sprout/layers/__init__.py +1 -0
- napari_sprout-1.1.0/napari_sprout/napari.yaml +33 -0
- napari_sprout-1.1.0/napari_sprout/utils/__init__.py +1 -0
- napari_sprout-1.1.0/napari_sprout/utils/boundary_check.py +53 -0
- napari_sprout-1.1.0/napari_sprout/utils/fit_table.py +49 -0
- napari_sprout-1.1.0/napari_sprout/utils/histogram_threshold_widget.py +504 -0
- napari_sprout-1.1.0/napari_sprout/utils/histogram_widget.py +244 -0
- napari_sprout-1.1.0/napari_sprout/utils/jobs.py +270 -0
- napari_sprout-1.1.0/napari_sprout/utils/layer_files.py +39 -0
- napari_sprout-1.1.0/napari_sprout/utils/layer_lock.py +61 -0
- napari_sprout-1.1.0/napari_sprout/utils/roi_layers.py +637 -0
- napari_sprout-1.1.0/napari_sprout/utils/roi_widget.py +819 -0
- napari_sprout-1.1.0/napari_sprout/utils/slice_preview.py +112 -0
- napari_sprout-1.1.0/napari_sprout/utils/status.py +193 -0
- napari_sprout-1.1.0/napari_sprout/utils/util_widget.py +1370 -0
- napari_sprout-1.1.0/napari_sprout/widgets/__init__.py +1 -0
- napari_sprout-1.1.0/napari_sprout/widgets/boundary_widget.py +744 -0
- napari_sprout-1.1.0/napari_sprout/widgets/click_owner.py +101 -0
- napari_sprout-1.1.0/napari_sprout/widgets/edit_widget.py +1696 -0
- napari_sprout-1.1.0/napari_sprout/widgets/grow_widget.py +785 -0
- napari_sprout-1.1.0/napari_sprout/widgets/info_widget.py +333 -0
- napari_sprout-1.1.0/napari_sprout/widgets/map_widget.py +528 -0
- napari_sprout-1.1.0/napari_sprout/widgets/prompt_tab.py +1683 -0
- napari_sprout-1.1.0/napari_sprout/widgets/prompt_widget.py +189 -0
- napari_sprout-1.1.0/napari_sprout/widgets/seed_widget.py +1042 -0
- napari_sprout-1.1.0/napari_sprout/widgets/tools_widget.py +989 -0
- napari_sprout-1.1.0/napari_sprout.egg-info/PKG-INFO +58 -0
- napari_sprout-1.1.0/napari_sprout.egg-info/SOURCES.txt +37 -0
- napari_sprout-1.1.0/napari_sprout.egg-info/dependency_links.txt +1 -0
- napari_sprout-1.1.0/napari_sprout.egg-info/entry_points.txt +2 -0
- napari_sprout-1.1.0/napari_sprout.egg-info/requires.txt +11 -0
- napari_sprout-1.1.0/napari_sprout.egg-info/top_level.txt +1 -0
- napari_sprout-1.1.0/pyproject.toml +50 -0
- napari_sprout-1.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Yichen He
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: napari-sprout
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: napari plugin for SPROUT: seeds, growing, editing and AI prompts for 2D and 3D segmentation
|
|
5
|
+
Author: Yichen He
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/EchanHe/SPROUT
|
|
8
|
+
Project-URL: Documentation, https://github.com/EchanHe/SPROUT#readme
|
|
9
|
+
Project-URL: Issues, https://github.com/EchanHe/SPROUT/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/EchanHe/SPROUT/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: napari,segmentation,image analysis,micro-CT,CT,MRI,3D,multi-class
|
|
12
|
+
Classifier: Framework :: napari
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: sprout-seg~=1.1.0
|
|
21
|
+
Requires-Dist: napari>=0.6.1
|
|
22
|
+
Requires-Dist: qtpy
|
|
23
|
+
Requires-Dist: magicgui
|
|
24
|
+
Requires-Dist: superqt
|
|
25
|
+
Provides-Extra: all
|
|
26
|
+
Requires-Dist: napari[all]; extra == "all"
|
|
27
|
+
Provides-Extra: ai
|
|
28
|
+
Requires-Dist: sprout-seg[ai]~=1.1.0; extra == "ai"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# napari-sprout
|
|
32
|
+
|
|
33
|
+
The napari plugin of [SPROUT](https://github.com/EchanHe/SPROUT): multi-part segmentation of 2D and 3D greyscale images (e.g. CT, MRI) without training data.
|
|
34
|
+
|
|
35
|
+
- **Seeds**: thresholds and erosion split an image into parts; adaptive and sweep modes combine several settings.
|
|
36
|
+
- **Grow**: seeds grow back to the full shape of each part.
|
|
37
|
+
- **Edit**: split, merge, relabel and clean regions.
|
|
38
|
+
- **Prompt (AI)**: seeds as prompts for nnInteractive.
|
|
39
|
+
- **ROI & Boundary**: work inside a region; make boundary masks.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install "napari-sprout[all]" # with napari and Qt
|
|
45
|
+
pip install napari-sprout # napari already installed
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For the Prompt (AI) tab also run `pip install "sprout-seg[ai]"` (with an NVIDIA GPU, install PyTorch for your CUDA version first).
|
|
49
|
+
|
|
50
|
+
## Use
|
|
51
|
+
|
|
52
|
+
Start napari and open **Plugins → SPROUT**. Videos: [SPROUT playlist](https://www.youtube.com/watch?v=4DHI1gs1lzc&list=PL8tsvVKJ9n6FjYYplo-IAwKBLxDh578aj). Docs and the command line (`sprout`): [github.com/EchanHe/SPROUT](https://github.com/EchanHe/SPROUT).
|
|
53
|
+
|
|
54
|
+
## Cite
|
|
55
|
+
|
|
56
|
+
Y. He, M. Camaiti, L.E. Roberts, J.M. Mulqueeney, E. Ioannou, M. Didziokas, A. Goswami. *SPROUT: A User-friendly, Scalable Toolkit for Multi-class Segmentation of Volumetric Images.* [bioRxiv 2024](https://www.biorxiv.org/content/10.1101/2024.11.22.624847v2.abstract).
|
|
57
|
+
|
|
58
|
+
MIT License.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# napari-sprout
|
|
2
|
+
|
|
3
|
+
The napari plugin of [SPROUT](https://github.com/EchanHe/SPROUT): multi-part segmentation of 2D and 3D greyscale images (e.g. CT, MRI) without training data.
|
|
4
|
+
|
|
5
|
+
- **Seeds**: thresholds and erosion split an image into parts; adaptive and sweep modes combine several settings.
|
|
6
|
+
- **Grow**: seeds grow back to the full shape of each part.
|
|
7
|
+
- **Edit**: split, merge, relabel and clean regions.
|
|
8
|
+
- **Prompt (AI)**: seeds as prompts for nnInteractive.
|
|
9
|
+
- **ROI & Boundary**: work inside a region; make boundary masks.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install "napari-sprout[all]" # with napari and Qt
|
|
15
|
+
pip install napari-sprout # napari already installed
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For the Prompt (AI) tab also run `pip install "sprout-seg[ai]"` (with an NVIDIA GPU, install PyTorch for your CUDA version first).
|
|
19
|
+
|
|
20
|
+
## Use
|
|
21
|
+
|
|
22
|
+
Start napari and open **Plugins → SPROUT**. Videos: [SPROUT playlist](https://www.youtube.com/watch?v=4DHI1gs1lzc&list=PL8tsvVKJ9n6FjYYplo-IAwKBLxDh578aj). Docs and the command line (`sprout`): [github.com/EchanHe/SPROUT](https://github.com/EchanHe/SPROUT).
|
|
23
|
+
|
|
24
|
+
## Cite
|
|
25
|
+
|
|
26
|
+
Y. He, M. Camaiti, L.E. Roberts, J.M. Mulqueeney, E. Ioannou, M. Didziokas, A. Goswami. *SPROUT: A User-friendly, Scalable Toolkit for Multi-class Segmentation of Volumetric Images.* [bioRxiv 2024](https://www.biorxiv.org/content/10.1101/2024.11.22.624847v2.abstract).
|
|
27
|
+
|
|
28
|
+
MIT License.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""napari-sprout: A napari plugin for SPROUT segmentation."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.1.0"
|
|
4
|
+
|
|
5
|
+
from ._widget import make_sprout_widget
|
|
6
|
+
from ._widget import make_sprout_widget_edit
|
|
7
|
+
from ._widget import make_sprout_widget_info
|
|
8
|
+
from ._widget import make_sprout_widget_map
|
|
9
|
+
|
|
10
|
+
__all__ = ["make_sprout_widget",
|
|
11
|
+
"make_sprout_widget_edit",
|
|
12
|
+
"make_sprout_widget_info",
|
|
13
|
+
"make_sprout_widget_map"
|
|
14
|
+
]
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Main widget for napari-SPROUT plugin."""
|
|
2
|
+
|
|
3
|
+
from qtpy.QtWidgets import (
|
|
4
|
+
QWidget, QVBoxLayout, QTabWidget, QPushButton,
|
|
5
|
+
QLabel, QHBoxLayout, QFileDialog
|
|
6
|
+
)
|
|
7
|
+
# from qtpy.QtCore import Qt
|
|
8
|
+
import napari
|
|
9
|
+
from napari.utils.notifications import show_info, show_error
|
|
10
|
+
import numpy as np
|
|
11
|
+
import tifffile
|
|
12
|
+
|
|
13
|
+
from .widgets.seed_widget import SeedGenerationWidget
|
|
14
|
+
from .widgets.grow_widget import SeedGrowthWidget
|
|
15
|
+
from .widgets.boundary_widget import BoundaryWidget
|
|
16
|
+
from .widgets.prompt_tab import PromptTab
|
|
17
|
+
from .utils.roi_widget import RoiTools
|
|
18
|
+
|
|
19
|
+
from .widgets.info_widget import LayerInfoWidget
|
|
20
|
+
from .widgets.edit_widget import QtLabelSelector
|
|
21
|
+
from .widgets.map_widget import LabelMapperWidget
|
|
22
|
+
from .widgets.tools_widget import SPROUTToolWidget
|
|
23
|
+
|
|
24
|
+
class SPROUTWidget(QWidget):
|
|
25
|
+
"""Main SPROUT widget containing all workflow steps."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, napari_viewer):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.viewer = napari_viewer
|
|
30
|
+
|
|
31
|
+
self._init_ui()
|
|
32
|
+
self._connect_signals()
|
|
33
|
+
|
|
34
|
+
def _init_ui(self):
|
|
35
|
+
"""Initialize the user interface."""
|
|
36
|
+
layout = QVBoxLayout()
|
|
37
|
+
|
|
38
|
+
# Set minimum size to ensure visibility
|
|
39
|
+
self.setMinimumSize(500, 500)
|
|
40
|
+
|
|
41
|
+
# Header
|
|
42
|
+
header_layout = QHBoxLayout()
|
|
43
|
+
title = QLabel("<h4>SPROUT Segmentation</h2>")
|
|
44
|
+
header_layout.addWidget(title)
|
|
45
|
+
header_layout.addStretch()
|
|
46
|
+
|
|
47
|
+
# Load image button
|
|
48
|
+
# self.load_btn = QPushButton("Load Image")
|
|
49
|
+
# header_layout.addWidget(self.load_btn)
|
|
50
|
+
|
|
51
|
+
layout.addLayout(header_layout)
|
|
52
|
+
|
|
53
|
+
# Info label
|
|
54
|
+
# info_text = (
|
|
55
|
+
# "Semi-automated Parcellation of Region Outputs Using "
|
|
56
|
+
# "Thresholding and Transformation"
|
|
57
|
+
# )
|
|
58
|
+
# info_label = QLabel(info_text)
|
|
59
|
+
# info_label.setWordWrap(True)
|
|
60
|
+
# info_label.setStyleSheet("QLabel { color: #666; }")
|
|
61
|
+
# layout.addWidget(info_label)
|
|
62
|
+
|
|
63
|
+
# Tab widget for workflow steps
|
|
64
|
+
self.tabs = QTabWidget()
|
|
65
|
+
|
|
66
|
+
# Create widgets for each step
|
|
67
|
+
self.seed_widget = SeedGenerationWidget(self.viewer)
|
|
68
|
+
self.grow_widget = SeedGrowthWidget(self.viewer)
|
|
69
|
+
self.prompt_tab = PromptTab(self.viewer)
|
|
70
|
+
self.boundary_widget = BoundaryWidget(self.viewer)
|
|
71
|
+
|
|
72
|
+
# ROI tools and the Boundary tab share the last tab (two sub-tabs)
|
|
73
|
+
self.roi_tools = RoiTools(self.viewer)
|
|
74
|
+
self.roi_boundary_tabs = QTabWidget()
|
|
75
|
+
self.roi_boundary_tabs.addTab(self.roi_tools, "ROI")
|
|
76
|
+
self.roi_boundary_tabs.addTab(self.boundary_widget, "Boundary")
|
|
77
|
+
|
|
78
|
+
# Add tabs
|
|
79
|
+
self.tabs.addTab(self.seed_widget, "1. Seeds")
|
|
80
|
+
self.tabs.addTab(self.grow_widget, "2. Grow")
|
|
81
|
+
self.tabs.addTab(self.prompt_tab, "3. Prompt (AI)")
|
|
82
|
+
self.tabs.addTab(self.roi_boundary_tabs, "ROI && Boundary") # "&&": a single "&" is a Qt mnemonic
|
|
83
|
+
|
|
84
|
+
layout.addWidget(self.tabs)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
self.setLayout(layout)
|
|
88
|
+
|
|
89
|
+
def _connect_signals(self):
|
|
90
|
+
"""Connect widget signals."""
|
|
91
|
+
# self.load_btn.clicked.connect(self.load_image)
|
|
92
|
+
self.seed_widget.seeds_generated.connect(self._on_seeds_generated)
|
|
93
|
+
self.grow_widget.growth_completed.connect(self._on_growth_completed)
|
|
94
|
+
self.boundary_widget.boundary_created.connect(self._on_boundary_created)
|
|
95
|
+
|
|
96
|
+
def _on_boundary_created(self, name):
|
|
97
|
+
"""Select a newly created boundary layer in the Seeds and Grow tabs."""
|
|
98
|
+
for w in (self.seed_widget, self.grow_widget):
|
|
99
|
+
w.boundary_combo.setCurrentText(name)
|
|
100
|
+
|
|
101
|
+
def load_image(self):
|
|
102
|
+
"""Load an image file."""
|
|
103
|
+
filename, _ = QFileDialog.getOpenFileName(
|
|
104
|
+
self,
|
|
105
|
+
"Load Image",
|
|
106
|
+
"",
|
|
107
|
+
"Image Files (*.tif *.tiff);;All Files (*)"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if filename:
|
|
111
|
+
try:
|
|
112
|
+
# Load image
|
|
113
|
+
image = tifffile.imread(filename)
|
|
114
|
+
|
|
115
|
+
# Add to viewer
|
|
116
|
+
self.viewer.add_image(
|
|
117
|
+
image,
|
|
118
|
+
name=filename.split('/')[-1]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Refresh layer lists
|
|
122
|
+
self.seed_widget.refresh_layers()
|
|
123
|
+
self.grow_widget.refresh_layers()
|
|
124
|
+
|
|
125
|
+
show_info(f"Loaded image: {filename}")
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
show_error(f"Error loading image: {str(e)}")
|
|
129
|
+
|
|
130
|
+
def _on_seeds_generated(self, seeds, sizes):
|
|
131
|
+
"""Handle seed generation completion."""
|
|
132
|
+
# Switch to grow tab
|
|
133
|
+
self.tabs.setCurrentIndex(1)
|
|
134
|
+
|
|
135
|
+
# Refresh grow widget layers
|
|
136
|
+
self.grow_widget.refresh_layers()
|
|
137
|
+
|
|
138
|
+
show_info("Seeds generated! You can now edit them or proceed to growth.")
|
|
139
|
+
|
|
140
|
+
def _on_growth_completed(self, result):
|
|
141
|
+
"""Handle growth completion."""
|
|
142
|
+
show_info("Growth completed! You can save the result or continue editing.")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def make_sprout_widget(viewer: "napari.viewer.Viewer" = None):
|
|
146
|
+
"""Create the SPROUT widget.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
viewer : napari.viewer.Viewer, optional
|
|
151
|
+
The napari viewer instance. If not provided, will get current viewer.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
widget : SPROUTWidget
|
|
156
|
+
The SPROUT widget instance.
|
|
157
|
+
"""
|
|
158
|
+
if viewer is None:
|
|
159
|
+
# Get the current viewer from napari
|
|
160
|
+
import napari
|
|
161
|
+
viewer = napari.current_viewer()
|
|
162
|
+
if viewer is None:
|
|
163
|
+
raise RuntimeError("No napari viewer found")
|
|
164
|
+
|
|
165
|
+
widget = SPROUTWidget(viewer)
|
|
166
|
+
# no widget.show() in these factories: napari shows the widget when it docks it
|
|
167
|
+
# (a show() before that flashes it as a separate window)
|
|
168
|
+
return widget
|
|
169
|
+
|
|
170
|
+
def make_sprout_widget_edit(viewer: "napari.viewer.Viewer" = None):
|
|
171
|
+
"""Create the SPROUT widget for editing.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
viewer : napari.viewer.Viewer, optional
|
|
176
|
+
The napari viewer instance. If not provided, will get current viewer.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
widget : SPROUTWidget
|
|
181
|
+
The SPROUT widget instance.
|
|
182
|
+
"""
|
|
183
|
+
if viewer is None:
|
|
184
|
+
# Get the current viewer from napari
|
|
185
|
+
import napari
|
|
186
|
+
viewer = napari.current_viewer()
|
|
187
|
+
if viewer is None:
|
|
188
|
+
raise RuntimeError("No napari viewer found")
|
|
189
|
+
|
|
190
|
+
widget = QtLabelSelector(viewer)
|
|
191
|
+
return widget
|
|
192
|
+
|
|
193
|
+
def make_sprout_widget_info(viewer: "napari.viewer.Viewer" = None):
|
|
194
|
+
"""Create the SPROUT widget for information.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
viewer : napari.viewer.Viewer, optional
|
|
199
|
+
The napari viewer instance. If not provided, will get current viewer.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
widget : SPROUTWidget
|
|
204
|
+
The SPROUT widget instance.
|
|
205
|
+
"""
|
|
206
|
+
if viewer is None:
|
|
207
|
+
# Get the current viewer from napari
|
|
208
|
+
import napari
|
|
209
|
+
viewer = napari.current_viewer()
|
|
210
|
+
if viewer is None:
|
|
211
|
+
raise RuntimeError("No napari viewer found")
|
|
212
|
+
|
|
213
|
+
widget = LayerInfoWidget(viewer)
|
|
214
|
+
return widget
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def make_sprout_widget_map(viewer: "napari.viewer.Viewer" = None):
|
|
218
|
+
"""Create the SPROUT widget for mapping.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
viewer : napari.viewer.Viewer, optional
|
|
223
|
+
The napari viewer instance. If not provided, will get current viewer.
|
|
224
|
+
|
|
225
|
+
Returns
|
|
226
|
+
-------
|
|
227
|
+
widget : SPROUTWidget
|
|
228
|
+
The SPROUT widget instance.
|
|
229
|
+
"""
|
|
230
|
+
if viewer is None:
|
|
231
|
+
# Get the current viewer from napari
|
|
232
|
+
import napari
|
|
233
|
+
viewer = napari.current_viewer()
|
|
234
|
+
if viewer is None:
|
|
235
|
+
raise RuntimeError("No napari viewer found")
|
|
236
|
+
|
|
237
|
+
widget = LabelMapperWidget(viewer)
|
|
238
|
+
return widget
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def make_sprout_widget_tools(viewer: "napari.viewer.Viewer" = None):
|
|
242
|
+
"""Create the SPROUT widget for tools.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
viewer : napari.viewer.Viewer, optional
|
|
247
|
+
The napari viewer instance. If not provided, will get current viewer.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
widget : SPROUTWidget
|
|
252
|
+
The SPROUT widget instance.
|
|
253
|
+
"""
|
|
254
|
+
if viewer is None:
|
|
255
|
+
# Get the current viewer from napari
|
|
256
|
+
import napari
|
|
257
|
+
viewer = napari.current_viewer()
|
|
258
|
+
if viewer is None:
|
|
259
|
+
raise RuntimeError("No napari viewer found")
|
|
260
|
+
|
|
261
|
+
widget = SPROUTToolWidget(viewer)
|
|
262
|
+
return widget
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Layer handling for napari-sprout."""
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: napari-sprout
|
|
2
|
+
display_name: SPROUT
|
|
3
|
+
contributions:
|
|
4
|
+
commands:
|
|
5
|
+
- id: napari-sprout.make_widget
|
|
6
|
+
python_name: napari_sprout._widget:make_sprout_widget
|
|
7
|
+
title: SPROUT Main Widget
|
|
8
|
+
|
|
9
|
+
- id: napari-sprout.edit_widget
|
|
10
|
+
python_name: napari_sprout._widget:make_sprout_widget_edit
|
|
11
|
+
title: SPROUT Edit Labels
|
|
12
|
+
|
|
13
|
+
- id: napari-sprout.info_widget
|
|
14
|
+
python_name: napari_sprout._widget:make_sprout_widget_info
|
|
15
|
+
title: SPROUT Data Info
|
|
16
|
+
|
|
17
|
+
- id: napari-sprout.map_sprout
|
|
18
|
+
python_name: napari_sprout._widget:make_sprout_widget_map
|
|
19
|
+
title: SPROUT Label Mapping
|
|
20
|
+
- id: napari-sprout.tools_widget
|
|
21
|
+
python_name: napari_sprout._widget:make_sprout_widget_tools
|
|
22
|
+
title: SPROUT Batch Builder
|
|
23
|
+
widgets:
|
|
24
|
+
- command: napari-sprout.make_widget
|
|
25
|
+
display_name: SPROUT
|
|
26
|
+
- command: napari-sprout.edit_widget
|
|
27
|
+
display_name: Edit
|
|
28
|
+
- command: napari-sprout.map_sprout
|
|
29
|
+
display_name: Label Mapping
|
|
30
|
+
- command: napari-sprout.tools_widget
|
|
31
|
+
display_name: Batch Builder
|
|
32
|
+
- command: napari-sprout.info_widget
|
|
33
|
+
display_name: Data Info
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility functions for napari-sprout."""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Check the boundary mask when Generate Seeds / Start Growth is clicked.
|
|
2
|
+
|
|
3
|
+
A mask that cannot be used gives a red line under the Boundary combo (no dialog) and the
|
|
4
|
+
run does not start.
|
|
5
|
+
|
|
6
|
+
SPROUT needs a boundary with exactly two values (0 = keep, non-zero = boundary).
|
|
7
|
+
Without this check a wrong layer only fails inside the background worker, with a
|
|
8
|
+
less specific message.
|
|
9
|
+
"""
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def boundary_problem(data):
|
|
14
|
+
"""None if `data` works as a SPROUT boundary, else a short description.
|
|
15
|
+
|
|
16
|
+
Mirrors config_core.check_and_cast_boundary: bool, or exactly two values one of
|
|
17
|
+
which is 0. ~1 s on 1e9 voxels (np.unique would take ~20 s).
|
|
18
|
+
"""
|
|
19
|
+
data = np.asarray(data)
|
|
20
|
+
if data.dtype == bool:
|
|
21
|
+
return None if data.any() else "it is empty (no boundary voxels)"
|
|
22
|
+
mx, mn = data.max(), data.min()
|
|
23
|
+
if mx == 0 and mn == 0:
|
|
24
|
+
return "it is empty (no boundary voxels)"
|
|
25
|
+
if mn != 0 and mx != 0 and not np.any(data == 0):
|
|
26
|
+
return "it has no 0 voxels (everything would be boundary)"
|
|
27
|
+
if np.any((data != 0) & (data != mx)):
|
|
28
|
+
step = max(1, int(round((data.size / 2e6) ** (1 / data.ndim))))
|
|
29
|
+
values = np.unique(data[(slice(None, None, step),) * data.ndim])
|
|
30
|
+
shown = ", ".join(str(v) for v in values[:6]) + (", ..." if values.size > 6 else "")
|
|
31
|
+
return f"it has more than two values ({shown})"
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def boundary_message(name, problem):
|
|
36
|
+
return (f"The boundary mask '{name}' cannot be used: {problem}.\n"
|
|
37
|
+
"A boundary mask needs exactly two values: 0 = keep, non-zero = boundary.\n"
|
|
38
|
+
"Choose another layer, or set Boundary Mask to None.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def boundary_ok(parent, name, data, note=None):
|
|
42
|
+
"""True if the boundary can be used; otherwise say why in `note` (the red line under
|
|
43
|
+
the Boundary combo, a utils.status.StatusLine) and return False. No dialog."""
|
|
44
|
+
if note is not None:
|
|
45
|
+
note.clear()
|
|
46
|
+
if data is None:
|
|
47
|
+
return True
|
|
48
|
+
problem = boundary_problem(data)
|
|
49
|
+
if problem is None:
|
|
50
|
+
return True
|
|
51
|
+
if note is not None:
|
|
52
|
+
note.set(boundary_message(name, problem), "error")
|
|
53
|
+
return False
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""A table as high as its rows (no empty filler), capped; it scrolls inside beyond that."""
|
|
2
|
+
from qtpy.QtCore import QTimer
|
|
3
|
+
from qtpy.QtWidgets import QSizePolicy, QTableWidget
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FitTable(QTableWidget):
|
|
7
|
+
"""Header + rows high, up to MAX_ROWS rows; an inner scrollbar beyond that.
|
|
8
|
+
|
|
9
|
+
The height follows row inserts / removals by itself (call fit() after anything else
|
|
10
|
+
that changes row heights). Full width: the columns stretch over it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
MAX_ROWS = 10
|
|
14
|
+
|
|
15
|
+
def __init__(self, *args, max_rows=None, auto_fit=True, **kwargs):
|
|
16
|
+
super().__init__(*args, **kwargs)
|
|
17
|
+
if max_rows is not None:
|
|
18
|
+
self.MAX_ROWS = max_rows
|
|
19
|
+
if auto_fit:
|
|
20
|
+
m = self.model()
|
|
21
|
+
for sig in (m.rowsInserted, m.rowsRemoved, m.modelReset, m.layoutChanged):
|
|
22
|
+
sig.connect(self._fit_later)
|
|
23
|
+
self.horizontalScrollBar().rangeChanged.connect(self._fit_if_alive)
|
|
24
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
25
|
+
self.fit()
|
|
26
|
+
|
|
27
|
+
def _fit_later(self, *_):
|
|
28
|
+
self.fit()
|
|
29
|
+
QTimer.singleShot(0, self._fit_if_alive) # row heights settle after the insert
|
|
30
|
+
|
|
31
|
+
def _fit_if_alive(self):
|
|
32
|
+
try:
|
|
33
|
+
self.fit()
|
|
34
|
+
except RuntimeError: # deleted meanwhile
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def fit(self):
|
|
38
|
+
n = min(self.rowCount(), self.MAX_ROWS)
|
|
39
|
+
h = 2 * self.frameWidth() + self.horizontalHeader().sizeHint().height()
|
|
40
|
+
h += sum(self.rowHeight(i) for i in range(n))
|
|
41
|
+
if self.horizontalScrollBar().maximum() > 0: # columns wider than the table (not the header length:
|
|
42
|
+
# right after the vertical scrollbar appears it is briefly wider than the viewport)
|
|
43
|
+
h += self.horizontalScrollBar().sizeHint().height()
|
|
44
|
+
if self.minimumHeight() != h or self.maximumHeight() != h:
|
|
45
|
+
self.setFixedHeight(h)
|
|
46
|
+
|
|
47
|
+
def resizeEvent(self, event):
|
|
48
|
+
super().resizeEvent(event)
|
|
49
|
+
self.fit() # a narrower table may need its horizontal scrollbar
|