napari-nninteractive 2.2.0__tar.gz → 2.3.2__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_nninteractive-2.2.0/src/napari_nninteractive.egg-info → napari_nninteractive-2.3.2}/PKG-INFO +3 -3
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/pyproject.toml +3 -6
- napari_nninteractive-2.3.2/src/napari_nninteractive/__init__.py +12 -0
- napari_nninteractive-2.3.2/src/napari_nninteractive/_version_check.py +78 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/widget_controls.py +63 -14
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/widget_gui.py +117 -4
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/widget_main.py +167 -15
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2/src/napari_nninteractive.egg-info}/PKG-INFO +3 -3
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive.egg-info/SOURCES.txt +1 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive.egg-info/requires.txt +2 -2
- napari_nninteractive-2.2.0/src/napari_nninteractive/__init__.py +0 -4
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/LICENSE +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/MANIFEST.in +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/README.md +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/setup.cfg +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/controls/__init__.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/controls/bbox_controls.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/controls/lasso_controls.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/controls/point_controls.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/controls/scribble_controls.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/layers/__init__.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/layers/abstract_layer.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/layers/bbox_layer.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/layers/lasso_layer.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/layers/point_layer.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/layers/scribble_layer.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/napari.yaml +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/utils/__init__.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/utils/affine.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/utils/utils.py +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive.egg-info/dependency_links.txt +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive.egg-info/entry_points.txt +0 -0
- {napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive.egg-info/top_level.txt +0 -0
{napari_nninteractive-2.2.0/src/napari_nninteractive.egg-info → napari_nninteractive-2.3.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: napari-nninteractive
|
|
3
|
-
Version: 2.2
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: nnInteractive plugin for Napari
|
|
5
5
|
Author: Lars Krämer, Fabian Isensee, Maximilian Rokuss
|
|
6
6
|
Author-email: lars.kraemer@dkfz-heidelberg.de, f.isensee@dkfz-heidelberg.de, maximilian.rokuss@dkfz-heidelberg.de
|
|
@@ -231,7 +231,7 @@ Requires-Dist: qtpy
|
|
|
231
231
|
Requires-Dist: napari-nifti
|
|
232
232
|
Requires-Dist: huggingface_hub
|
|
233
233
|
Requires-Dist: hf_transfer
|
|
234
|
-
Requires-Dist: nnInteractive>=2.
|
|
234
|
+
Requires-Dist: nnInteractive>=2.4.0
|
|
235
235
|
Requires-Dist: napari_toolkit
|
|
236
236
|
Provides-Extra: testing
|
|
237
237
|
Requires-Dist: tox; extra == "testing"
|
|
@@ -241,7 +241,7 @@ Requires-Dist: pytest-qt; extra == "testing"
|
|
|
241
241
|
Requires-Dist: napari; extra == "testing"
|
|
242
242
|
Requires-Dist: pyqt5; extra == "testing"
|
|
243
243
|
Provides-Extra: remote
|
|
244
|
-
Requires-Dist: nnInteractive[client]>=2.
|
|
244
|
+
Requires-Dist: nnInteractive[client]>=2.4.0; extra == "remote"
|
|
245
245
|
Dynamic: license-file
|
|
246
246
|
|
|
247
247
|
<img src="https://github.com/MIC-DKFZ/napari-nninteractive/raw/main/imgs/nnInteractive_header.png" width="1200">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "napari-nninteractive"
|
|
3
|
-
|
|
3
|
+
version = "2.3.2"
|
|
4
4
|
description = "nnInteractive plugin for Napari"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = {file = "LICENSE"}
|
|
@@ -32,7 +32,7 @@ dependencies = [
|
|
|
32
32
|
"napari-nifti",
|
|
33
33
|
"huggingface_hub",
|
|
34
34
|
"hf_transfer",
|
|
35
|
-
"nnInteractive>=2.
|
|
35
|
+
"nnInteractive>=2.4.0",
|
|
36
36
|
"napari_toolkit",
|
|
37
37
|
]
|
|
38
38
|
|
|
@@ -46,7 +46,7 @@ testing = [
|
|
|
46
46
|
"pyqt5",
|
|
47
47
|
]
|
|
48
48
|
remote = [
|
|
49
|
-
"nnInteractive[client]>=2.
|
|
49
|
+
"nnInteractive[client]>=2.4.0",
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
[project.entry-points."napari.manifest"]
|
|
@@ -69,9 +69,6 @@ where = ["src"]
|
|
|
69
69
|
Homepage = "https://github.com/MIC-DKFZ/napari-nninteractive"
|
|
70
70
|
Code = "https://github.com/MIC-DKFZ/napari-nninteractive"
|
|
71
71
|
|
|
72
|
-
[tool.setuptools.dynamic]
|
|
73
|
-
version = {attr = "napari_nninteractive.__init__.__version__"}
|
|
74
|
-
|
|
75
72
|
[tool.black]
|
|
76
73
|
line-length = 100
|
|
77
74
|
target-version = ['py38', 'py39', 'py310']
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError
|
|
2
|
+
from importlib.metadata import version as _version
|
|
3
|
+
|
|
4
|
+
from .widget_main import nnInteractiveWidget
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = _version("napari-nninteractive")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
__version__ = "unknown"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ("nnInteractiveWidget",)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Non-blocking check for newer releases of the plugin and its backend on PyPI.
|
|
2
|
+
|
|
3
|
+
The network request runs in a background daemon thread so it never blocks GUI
|
|
4
|
+
startup, and the result is delivered back on the GUI thread via a Qt signal.
|
|
5
|
+
The check is best effort: when PyPI is unreachable (offline, behind a firewall,
|
|
6
|
+
etc.) it simply stays silent instead of nagging the user.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import threading
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
from urllib.request import urlopen
|
|
13
|
+
|
|
14
|
+
from qtpy.QtCore import QObject, Signal
|
|
15
|
+
|
|
16
|
+
# PyPI project names to check, in display order.
|
|
17
|
+
PACKAGES = ("napari-nninteractive", "nnInteractive")
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from packaging.version import InvalidVersion
|
|
21
|
+
from packaging.version import parse as _parse_version
|
|
22
|
+
except ImportError: # packaging is virtually always present, but degrade gracefully
|
|
23
|
+
_parse_version = None
|
|
24
|
+
InvalidVersion = Exception
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _installed_version(package: str):
|
|
28
|
+
"""Return the installed version string, or None if the package is missing."""
|
|
29
|
+
try:
|
|
30
|
+
return version(package)
|
|
31
|
+
except PackageNotFoundError:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _latest_version(package: str, timeout: float = 5.0) -> str:
|
|
36
|
+
"""Return the latest release version for `package` from the PyPI JSON API."""
|
|
37
|
+
url = f"https://pypi.org/pypi/{package}/json"
|
|
38
|
+
with urlopen(url, timeout=timeout) as response: # noqa: S310 - fixed https URL
|
|
39
|
+
data = json.load(response)
|
|
40
|
+
return data["info"]["version"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _is_outdated(installed: str, latest: str) -> bool:
|
|
44
|
+
"""True if `installed` is an older release than `latest`."""
|
|
45
|
+
if _parse_version is not None:
|
|
46
|
+
try:
|
|
47
|
+
return _parse_version(installed) < _parse_version(latest)
|
|
48
|
+
except InvalidVersion:
|
|
49
|
+
return False
|
|
50
|
+
# Fallback when `packaging` is unavailable: only flag an exact mismatch.
|
|
51
|
+
return installed != latest
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class VersionChecker(QObject):
|
|
55
|
+
"""Checks PyPI for newer releases in a background daemon thread.
|
|
56
|
+
|
|
57
|
+
Connect to `finished`, then call `start()`. The signal carries a dict mapping
|
|
58
|
+
each package name to an `(installed, latest)` tuple; either entry may be None
|
|
59
|
+
(package not installed, or PyPI could not be reached). It is emitted from the
|
|
60
|
+
worker thread, so Qt delivers it to GUI-thread slots via a queued connection.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# Emits {package_name: (installed_or_None, latest_or_None)}
|
|
64
|
+
finished = Signal(object)
|
|
65
|
+
|
|
66
|
+
def start(self) -> None:
|
|
67
|
+
threading.Thread(target=self._run, name="nni-version-check", daemon=True).start()
|
|
68
|
+
|
|
69
|
+
def _run(self) -> None:
|
|
70
|
+
results = {}
|
|
71
|
+
for package in PACKAGES:
|
|
72
|
+
installed = _installed_version(package)
|
|
73
|
+
try:
|
|
74
|
+
latest = _latest_version(package)
|
|
75
|
+
except Exception: # noqa: BLE001 - offline / PyPI down / bad payload: skip silently
|
|
76
|
+
latest = None
|
|
77
|
+
results[package] = (installed, latest)
|
|
78
|
+
self.finished.emit(results)
|
|
@@ -59,6 +59,12 @@ class LayerControls(BaseGUI):
|
|
|
59
59
|
self.colormap = ColorMapper(49, seed=0.5, background_value=0)
|
|
60
60
|
self._scribble_brush_size = 5
|
|
61
61
|
self.object_index = 0
|
|
62
|
+
# Names of the interaction layers that committed an interaction, newest last. Used by
|
|
63
|
+
# on_undo to remove the visual marker of the most recently undone interaction. The
|
|
64
|
+
# backend supports single-level undo, so only the top entry is ever undoable; older
|
|
65
|
+
# entries stay because their interactions are still applied. None means an interaction
|
|
66
|
+
# without a layer marker (e.g. Initialize with Mask).
|
|
67
|
+
self._interaction_history = []
|
|
62
68
|
|
|
63
69
|
self._viewer.layers.selection.events.active.connect(self.on_layer_selected)
|
|
64
70
|
|
|
@@ -69,6 +75,8 @@ class LayerControls(BaseGUI):
|
|
|
69
75
|
for layer_name in layer_names:
|
|
70
76
|
if layer_name in self._viewer.layers:
|
|
71
77
|
self._viewer.layers.remove(layer_name)
|
|
78
|
+
# The interaction markers are gone, so nothing is left to undo for them.
|
|
79
|
+
self._interaction_history = []
|
|
72
80
|
|
|
73
81
|
def add_point_layer(self) -> None:
|
|
74
82
|
"""Adds a single point layer to the viewer."""
|
|
@@ -335,11 +343,12 @@ class LayerControls(BaseGUI):
|
|
|
335
343
|
self.session_cfg["affine"].scale
|
|
336
344
|
)
|
|
337
345
|
|
|
338
|
-
# Decide whether to resume the previous segmentation. We only resume
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
# (identity, not merely the same
|
|
342
|
-
# shape check is a cheap guard against
|
|
346
|
+
# Decide whether to resume the previous segmentation. We only resume when
|
|
347
|
+
# the _resume_after_reconnect flag is armed -- set after a connection loss
|
|
348
|
+
# (_handle_session_expired) -- and only when the surviving label layer
|
|
349
|
+
# belongs to the *same* image layer object (identity, not merely the same
|
|
350
|
+
# shape, which would be brittle). The shape check is a cheap guard against
|
|
351
|
+
# a layer that no longer matches.
|
|
343
352
|
resume = (
|
|
344
353
|
getattr(self, "_resume_after_reconnect", False)
|
|
345
354
|
and self.label_layer_name in self._viewer.layers
|
|
@@ -356,10 +365,15 @@ class LayerControls(BaseGUI):
|
|
|
356
365
|
else:
|
|
357
366
|
# Create the target label array and layer
|
|
358
367
|
self._data_result = np.zeros(self.session_cfg["shape"], dtype=np.uint8)
|
|
359
|
-
|
|
368
|
+
# Continue numbering/colors from any objects already segmented for this
|
|
369
|
+
# image in a previous session instead of restarting at 1.
|
|
370
|
+
self.object_index = self._next_object_index()
|
|
360
371
|
if self.label_layer_name in self._viewer.layers:
|
|
361
372
|
self._viewer.layers.remove(self.label_layer_name)
|
|
362
373
|
self.add_label_layer(self._data_result, self.label_layer_name)
|
|
374
|
+
# Colour the working layer to match where the counter resumes, so the
|
|
375
|
+
# in-progress object already shows its eventual colour.
|
|
376
|
+
self._viewer.layers[self.label_layer_name].colormap = self.colormap[self.object_index]
|
|
363
377
|
|
|
364
378
|
# Pin the resume to this image object so a later reconnect can verify it
|
|
365
379
|
# is the same image before resuming. _resuming is read by the subclass'
|
|
@@ -367,6 +381,9 @@ class LayerControls(BaseGUI):
|
|
|
367
381
|
self._resume_image_layer = image_layer
|
|
368
382
|
self._resuming = resume
|
|
369
383
|
|
|
384
|
+
# Fresh image/model pair: nothing from a previous object is undoable.
|
|
385
|
+
self._interaction_history = []
|
|
386
|
+
|
|
370
387
|
# Lock the Session
|
|
371
388
|
self._lock_session()
|
|
372
389
|
|
|
@@ -375,15 +392,36 @@ class LayerControls(BaseGUI):
|
|
|
375
392
|
super().on_reset_interactions()
|
|
376
393
|
self.on_layer_selected()
|
|
377
394
|
|
|
378
|
-
def
|
|
395
|
+
def _next_object_index(self) -> int:
|
|
396
|
+
"""Continue object numbering from work already present in the viewer for
|
|
397
|
+
this image, so a new session does not restart at 1 and collide with the
|
|
398
|
+
names/colors of objects from a previous session. Considers both the
|
|
399
|
+
per-object ``object N - <name>`` layers and the values of the aggregated
|
|
400
|
+
``semantic map - <name>`` layer. Returns 0 when nothing is present.
|
|
379
401
|
"""
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
402
|
+
name = self.session_cfg["name"]
|
|
403
|
+
# determine_layer_index returns max(N) + 1 over the "object N - <name>"
|
|
404
|
+
# layers (or 0 when there are none); object_index is that max, one less.
|
|
405
|
+
highest = (
|
|
406
|
+
determine_layer_index(
|
|
407
|
+
[layer.name for layer in self._viewer.layers], "object ", f" - {name}"
|
|
408
|
+
)
|
|
409
|
+
- 1
|
|
410
|
+
)
|
|
411
|
+
sem_name = f"semantic map - {name}"
|
|
412
|
+
if sem_name in self._viewer.layers:
|
|
413
|
+
highest = max(highest, int(self._viewer.layers[sem_name].data.max()))
|
|
414
|
+
return max(highest, 0)
|
|
415
|
+
|
|
416
|
+
def _store_current_object(self) -> None:
|
|
417
|
+
"""Promote the in-progress label layer to a finished object.
|
|
418
|
+
|
|
419
|
+
Either as its own ``object N - <name>`` layer or, in instance-aggregation
|
|
420
|
+
mode, merged into the shared ``semantic map - <name>`` layer. Advances
|
|
421
|
+
object_index so subsequent objects keep consistent numbering and colours.
|
|
422
|
+
Shared by ``on_next`` and by the reset paths that offer to keep the
|
|
423
|
+
in-progress segmentation before tearing down the session.
|
|
385
424
|
"""
|
|
386
|
-
# Rename the current layer and add a new one
|
|
387
425
|
label_layer = self._viewer.layers[self.label_layer_name]
|
|
388
426
|
if not self.instance_aggregation_ckbx.isChecked():
|
|
389
427
|
|
|
@@ -402,7 +440,18 @@ class LayerControls(BaseGUI):
|
|
|
402
440
|
sem_layer.refresh()
|
|
403
441
|
|
|
404
442
|
self.object_index += 1
|
|
405
|
-
|
|
443
|
+
|
|
444
|
+
def on_next(self) -> None:
|
|
445
|
+
"""
|
|
446
|
+
Prepares the next label layer for interactions in the viewer.
|
|
447
|
+
|
|
448
|
+
Retrieves the index of the last labeled object, renames the current label layer with
|
|
449
|
+
this index, unbinds the original data by creating a deep copy, and clears all interaction
|
|
450
|
+
layers. A new label layer with an updated colormap is then added to the viewer.
|
|
451
|
+
"""
|
|
452
|
+
# Store the current object and recolour the working layer for the next one.
|
|
453
|
+
self._store_current_object()
|
|
454
|
+
self._viewer.layers[self.label_layer_name].colormap = self.colormap[self.object_index]
|
|
406
455
|
|
|
407
456
|
self._clear_layers()
|
|
408
457
|
self.prompt_button._uncheck()
|
{napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/widget_gui.py
RENAMED
|
@@ -30,6 +30,8 @@ from qtpy.QtWidgets import (
|
|
|
30
30
|
QWidget,
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
+
from napari_nninteractive._version_check import VersionChecker, _is_outdated
|
|
34
|
+
|
|
33
35
|
|
|
34
36
|
class BaseGUI(QWidget):
|
|
35
37
|
"""
|
|
@@ -66,15 +68,61 @@ class BaseGUI(QWidget):
|
|
|
66
68
|
|
|
67
69
|
_ = setup_acknowledgements(_scroll_layout, width=self._width) # Acknowledgements
|
|
68
70
|
|
|
71
|
+
# Update notice, below the logo (filled in asynchronously once PyPI has been queried).
|
|
72
|
+
self.version_status_label = QLabel("")
|
|
73
|
+
self.version_status_label.setWordWrap(True)
|
|
74
|
+
self.version_status_label.setAlignment(Qt.AlignLeft)
|
|
75
|
+
# Let the user select/copy the update command with the mouse or keyboard.
|
|
76
|
+
self.version_status_label.setTextInteractionFlags(
|
|
77
|
+
Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard
|
|
78
|
+
)
|
|
79
|
+
self.version_status_label.setVisible(False)
|
|
80
|
+
_scroll_layout.addWidget(self.version_status_label)
|
|
81
|
+
|
|
69
82
|
self._unlock_session()
|
|
70
83
|
self._viewer.bind_key("Ctrl+Q", self._close, overwrite=True)
|
|
71
84
|
|
|
85
|
+
# Non-blocking check for newer releases on PyPI. Kept as an attribute so
|
|
86
|
+
# it outlives __init__; the daemon thread it spawns never blocks startup.
|
|
87
|
+
self._version_checker = VersionChecker()
|
|
88
|
+
self._version_checker.finished.connect(self._on_version_check_finished)
|
|
89
|
+
self._version_checker.start()
|
|
90
|
+
|
|
72
91
|
# Base Behaviour
|
|
73
92
|
def _close(self):
|
|
74
93
|
"""Closes the viewer and quits the application."""
|
|
75
94
|
self._viewer.close()
|
|
76
95
|
quit()
|
|
77
96
|
|
|
97
|
+
def _on_version_check_finished(self, results: dict) -> None:
|
|
98
|
+
"""Show an up-to-date / update-available notice from the PyPI check.
|
|
99
|
+
|
|
100
|
+
`results` maps each package name to an `(installed, latest)` tuple; either
|
|
101
|
+
entry may be None (package not installed or PyPI unreachable). When nothing
|
|
102
|
+
could be compared the label stays hidden rather than showing a false notice.
|
|
103
|
+
"""
|
|
104
|
+
outdated = [
|
|
105
|
+
pkg
|
|
106
|
+
for pkg, (installed, latest) in results.items()
|
|
107
|
+
if installed and latest and _is_outdated(installed, latest)
|
|
108
|
+
]
|
|
109
|
+
checkable = any(installed and latest for installed, latest in results.values())
|
|
110
|
+
|
|
111
|
+
if not checkable:
|
|
112
|
+
self.version_status_label.setVisible(False)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
self.version_status_label.setVisible(True)
|
|
116
|
+
if outdated:
|
|
117
|
+
self.version_status_label.setText(
|
|
118
|
+
"Update available. Please run:\n"
|
|
119
|
+
"pip install -U nnInteractive napari-nninteractive"
|
|
120
|
+
)
|
|
121
|
+
self.version_status_label.setStyleSheet("color: #e8830c; font-weight: bold;") # orange
|
|
122
|
+
else:
|
|
123
|
+
self.version_status_label.setText("nnInteractive is up to date")
|
|
124
|
+
self.version_status_label.setStyleSheet("color: #2e9e2e;") # green
|
|
125
|
+
|
|
78
126
|
def _unlock_session(self):
|
|
79
127
|
"""Unlocks the session, enabling model and image selection, and initializing controls."""
|
|
80
128
|
self.init_button.setEnabled(True)
|
|
@@ -90,6 +138,7 @@ class BaseGUI(QWidget):
|
|
|
90
138
|
self.run_ckbx.setEnabled(False)
|
|
91
139
|
self.export_button.setEnabled(False)
|
|
92
140
|
self.reset_interaction_button.setEnabled(False)
|
|
141
|
+
self.undo_button.setEnabled(False)
|
|
93
142
|
self.propagate_ckbx.setEnabled(False)
|
|
94
143
|
self.label_for_init.setEnabled(False)
|
|
95
144
|
self.class_for_init.setEnabled(False)
|
|
@@ -127,6 +176,7 @@ class BaseGUI(QWidget):
|
|
|
127
176
|
self.run_ckbx.setEnabled(True)
|
|
128
177
|
self.export_button.setEnabled(True)
|
|
129
178
|
self.reset_interaction_button.setEnabled(True)
|
|
179
|
+
self.undo_button.setEnabled(True)
|
|
130
180
|
self.propagate_ckbx.setEnabled(True)
|
|
131
181
|
self.label_for_init.setEnabled(True)
|
|
132
182
|
self.class_for_init.setEnabled(True)
|
|
@@ -169,26 +219,51 @@ class BaseGUI(QWidget):
|
|
|
169
219
|
_boxlayout = QHBoxLayout()
|
|
170
220
|
_local_layout.addLayout(_boxlayout)
|
|
171
221
|
self.model_selection_local = setup_lineedit(
|
|
172
|
-
_boxlayout, placeholder="Use Local Checkpoint...", function=self.
|
|
222
|
+
_boxlayout, placeholder="Use Local Checkpoint...", function=self.on_checkpoint_changed
|
|
173
223
|
)
|
|
174
224
|
|
|
175
225
|
def _reset_local_ckpt_lineedit():
|
|
176
226
|
self.model_selection_local.setText("")
|
|
177
|
-
self.
|
|
227
|
+
self.on_checkpoint_changed()
|
|
178
228
|
|
|
179
229
|
btn = setup_iconbutton(
|
|
180
230
|
_boxlayout, "", "delete_shape", self._viewer.theme, function=_reset_local_ckpt_lineedit
|
|
181
231
|
)
|
|
182
232
|
btn.setFixedWidth(30)
|
|
183
233
|
|
|
234
|
+
# --- Advanced (local) options --- #
|
|
235
|
+
# These are niche settings, so they live in a collapsible section that is folded
|
|
236
|
+
# by default. The fold state and the chosen values are persisted via QSettings.
|
|
237
|
+
advanced_collapsed = self._settings.value("advanced_collapsed", True, type=bool)
|
|
238
|
+
self.advanced_box, _advanced_layout = setup_vcollapsiblegroupbox(
|
|
239
|
+
_local_layout, text="Advanced", collapsed=advanced_collapsed
|
|
240
|
+
)
|
|
241
|
+
|
|
184
242
|
self.use_torch_compile_ckbx = setup_checkbox(
|
|
185
|
-
|
|
243
|
+
_advanced_layout,
|
|
186
244
|
"use torch.compile",
|
|
187
|
-
False,
|
|
245
|
+
self._settings.value("use_torch_compile", False, type=bool),
|
|
188
246
|
tooltips="If checked: enable torch.compile for local inference. The model is compiled "
|
|
189
247
|
"during Initialize, so initialization takes longer, but every prediction afterwards is faster.",
|
|
190
248
|
)
|
|
191
249
|
|
|
250
|
+
_storage_layout = QHBoxLayout()
|
|
251
|
+
_advanced_layout.addLayout(_storage_layout)
|
|
252
|
+
setup_label(_storage_layout, "interaction storage")
|
|
253
|
+
self.interactions_storage_combo = setup_combobox(
|
|
254
|
+
_storage_layout,
|
|
255
|
+
options=["auto", "blosc2", "tensor"],
|
|
256
|
+
tooltips="Storage backend for the interaction tensor (local inference only):\n"
|
|
257
|
+
"• auto: dense tensor for smaller images, blosc2 above ~512x512x512 (default)\n"
|
|
258
|
+
"• blosc2: much less RAM, slightly slower\n"
|
|
259
|
+
"• tensor: much more RAM, slightly faster\n"
|
|
260
|
+
"Pick blosc2 manually if you are short on RAM.",
|
|
261
|
+
)
|
|
262
|
+
saved_storage = self._settings.value("interactions_storage", "auto", type=str)
|
|
263
|
+
_storage_idx = self.interactions_storage_combo.findText(saved_storage)
|
|
264
|
+
if _storage_idx >= 0:
|
|
265
|
+
self.interactions_storage_combo.setCurrentIndex(_storage_idx)
|
|
266
|
+
|
|
192
267
|
# --- Remote container --- #
|
|
193
268
|
self.remote_container = QWidget()
|
|
194
269
|
_remote_layout = QVBoxLayout()
|
|
@@ -251,6 +326,26 @@ class BaseGUI(QWidget):
|
|
|
251
326
|
lambda t: self._settings.setValue("server_url", t)
|
|
252
327
|
)
|
|
253
328
|
|
|
329
|
+
# Persist the advanced options (fold state + chosen values) between sessions.
|
|
330
|
+
self.advanced_box.toggled.connect(
|
|
331
|
+
lambda expanded: self._settings.setValue("advanced_collapsed", not expanded)
|
|
332
|
+
)
|
|
333
|
+
self.use_torch_compile_ckbx.toggled.connect(
|
|
334
|
+
lambda checked: self._settings.setValue("use_torch_compile", checked)
|
|
335
|
+
)
|
|
336
|
+
self.interactions_storage_combo.currentTextChanged.connect(
|
|
337
|
+
lambda t: self._settings.setValue("interactions_storage", t)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# torch.compile and interaction storage are baked into the session at Initialize.
|
|
341
|
+
# Changing one afterwards would leave the GUI out of sync with the live session, so
|
|
342
|
+
# uninitialize and force a re-Initialize -- but keep the in-progress segmentation.
|
|
343
|
+
# Wired after the construction-time restore above, so it never fires during build.
|
|
344
|
+
self.use_torch_compile_ckbx.toggled.connect(lambda *_: self.on_local_settings_changed())
|
|
345
|
+
self.interactions_storage_combo.currentTextChanged.connect(
|
|
346
|
+
lambda *_: self.on_local_settings_changed()
|
|
347
|
+
)
|
|
348
|
+
|
|
254
349
|
_group_box.setLayout(_layout)
|
|
255
350
|
return _group_box
|
|
256
351
|
|
|
@@ -284,6 +379,15 @@ class BaseGUI(QWidget):
|
|
|
284
379
|
self.model_license_label.setWordWrap(True)
|
|
285
380
|
_layout.addWidget(self.model_license_label)
|
|
286
381
|
|
|
382
|
+
self.undo_button = setup_iconbutton(
|
|
383
|
+
_layout,
|
|
384
|
+
"Undo",
|
|
385
|
+
"step_left",
|
|
386
|
+
self._viewer.theme,
|
|
387
|
+
self.on_undo,
|
|
388
|
+
tooltips="Undo the last interaction for the current object - press Ctrl+Z",
|
|
389
|
+
shortcut="Ctrl+Z",
|
|
390
|
+
)
|
|
287
391
|
self.reset_interaction_button = setup_iconbutton(
|
|
288
392
|
_layout,
|
|
289
393
|
"Reset Object",
|
|
@@ -495,10 +599,19 @@ class BaseGUI(QWidget):
|
|
|
495
599
|
def on_remote_settings_changed(self, *args, **kwargs) -> None:
|
|
496
600
|
"""Placeholder for handling changes to remote URL/API key fields."""
|
|
497
601
|
|
|
602
|
+
def on_local_settings_changed(self, *args, **kwargs) -> None:
|
|
603
|
+
"""Placeholder for changes to baked-in local options (torch.compile / storage)."""
|
|
604
|
+
|
|
605
|
+
def on_checkpoint_changed(self, *args, **kwargs) -> None:
|
|
606
|
+
"""Placeholder for edits to / clearing of the local checkpoint path."""
|
|
607
|
+
|
|
498
608
|
def on_reset_interactions(self):
|
|
499
609
|
"""Reset only the current interaction"""
|
|
500
610
|
self._clear_layers()
|
|
501
611
|
|
|
612
|
+
def on_undo(self, *args, **kwargs) -> None:
|
|
613
|
+
"""Placeholder method for undoing the last interaction."""
|
|
614
|
+
|
|
502
615
|
def on_next(self) -> None:
|
|
503
616
|
"""Resets the interactions."""
|
|
504
617
|
print("_reset_interactions")
|
{napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/widget_main.py
RENAMED
|
@@ -45,6 +45,32 @@ class nnInteractiveWidget(LayerControls):
|
|
|
45
45
|
"""
|
|
46
46
|
A widget for the nnInteractive plugin in Napari that manages model inference sessions
|
|
47
47
|
and allows interactive layer-based actions.
|
|
48
|
+
|
|
49
|
+
Handling the in-progress object when a session ends
|
|
50
|
+
---------------------------------------------------
|
|
51
|
+
Whenever a live session is torn down we have to decide what happens to the
|
|
52
|
+
object the user is currently working on (the un-committed "nnInteractive -
|
|
53
|
+
Label Layer"). The behaviour deliberately splits along *why* the session
|
|
54
|
+
ended:
|
|
55
|
+
|
|
56
|
+
* **User-triggered reinitialization** -- changing the model, the Local/Remote
|
|
57
|
+
mode, the server URL/key (``on_model_selected``), the local checkpoint
|
|
58
|
+
(``on_checkpoint_changed``) or a baked-in option such as torch.compile or
|
|
59
|
+
the interaction storage backend (``on_local_settings_changed``). The new
|
|
60
|
+
session cannot meaningfully continue the old object, so we *wrap it up*:
|
|
61
|
+
``_store_in_progress_segmentation`` commits it as a finished object (exactly
|
|
62
|
+
like "Next Object") and the user starts fresh on the next Initialize. If
|
|
63
|
+
they don't want the stored object they can simply delete the layer.
|
|
64
|
+
|
|
65
|
+
* **Unintentional loss** -- the remote lease expired or the connection dropped
|
|
66
|
+
(``_handle_session_expired``). The user did not ask to stop, so we instead
|
|
67
|
+
*resume*: the label layer is kept and the resume machinery
|
|
68
|
+
(``_resume_after_reconnect`` / ``_resume_image_layer`` / ``_resuming``,
|
|
69
|
+
consumed in ``LayerControls.on_init``) seeds the reconnected session with it
|
|
70
|
+
so refinement continues on the same object.
|
|
71
|
+
|
|
72
|
+
In short: deliberate resets bank the work and start over; accidental drops
|
|
73
|
+
preserve and resume it.
|
|
48
74
|
"""
|
|
49
75
|
|
|
50
76
|
def __init__(self, viewer: Viewer, parent: Optional[QWidget] = None):
|
|
@@ -64,6 +90,9 @@ class nnInteractiveWidget(LayerControls):
|
|
|
64
90
|
self._resume_after_reconnect = False
|
|
65
91
|
self._resume_image_layer = None
|
|
66
92
|
self._resuming = False
|
|
93
|
+
# Checkpoint-path text the current session was built from. Lets a
|
|
94
|
+
# re-submitted, unchanged path be a no-op instead of an uninitialize.
|
|
95
|
+
self._active_checkpoint_text = None
|
|
67
96
|
self._viewer.dims.events.order.connect(self.on_axis_change)
|
|
68
97
|
|
|
69
98
|
# Belt-and-suspenders lease release on shutdown. closeEvent on this
|
|
@@ -163,6 +192,9 @@ class nnInteractiveWidget(LayerControls):
|
|
|
163
192
|
# Init succeeded; clear the resume state so a normal re-init starts fresh.
|
|
164
193
|
self._resume_after_reconnect = False
|
|
165
194
|
self._resuming = False
|
|
195
|
+
# Remember the checkpoint text this session was built from, so re-pressing
|
|
196
|
+
# Enter on an unchanged path keeps the session instead of resetting it.
|
|
197
|
+
self._active_checkpoint_text = self.model_selection_local.text()
|
|
166
198
|
|
|
167
199
|
if self._viewer.dims.not_displayed != ():
|
|
168
200
|
self._scribble_brush_size = self.session.preferred_scribble_thickness[
|
|
@@ -210,6 +242,7 @@ class nnInteractiveWidget(LayerControls):
|
|
|
210
242
|
torch_n_threads=os.cpu_count(),
|
|
211
243
|
verbose=False,
|
|
212
244
|
do_autozoom=self.propagate_ckbx.isChecked(),
|
|
245
|
+
interactions_storage=self.interactions_storage_combo.currentText(),
|
|
213
246
|
)
|
|
214
247
|
|
|
215
248
|
self.session.initialize_from_trained_model_folder(
|
|
@@ -238,32 +271,40 @@ class nnInteractiveWidget(LayerControls):
|
|
|
238
271
|
server_url=server_url, api_key=api_key
|
|
239
272
|
)
|
|
240
273
|
except ServerAtCapacityError:
|
|
241
|
-
self.remote_status_label.setText("
|
|
242
|
-
return None
|
|
243
|
-
except _SESSION_LOST_ERRORS:
|
|
244
|
-
# Extremely unlikely at /claim time, but handle for completeness.
|
|
245
|
-
self.remote_status_label.setText("server rejected the claim; try again")
|
|
274
|
+
self.remote_status_label.setText("Server full; try again later.")
|
|
246
275
|
return None
|
|
276
|
+
# Connectivity problems must be handled BEFORE the session-lost case below:
|
|
277
|
+
# httpx.ConnectError/ConnectTimeout are subclasses of httpx.TransportError, so a
|
|
278
|
+
# broad TransportError catch would otherwise swallow them and report the wrong cause.
|
|
247
279
|
except httpx.ConnectError:
|
|
248
|
-
|
|
280
|
+
# DNS failure, connection refused, no route — nothing is listening/reachable.
|
|
281
|
+
self.remote_status_label.setText("Cannot reach server; check URL/port.")
|
|
249
282
|
return None
|
|
250
283
|
except httpx.ConnectTimeout:
|
|
251
|
-
self.remote_status_label.setText(
|
|
284
|
+
self.remote_status_label.setText("Connection timed out; check URL/network.")
|
|
285
|
+
return None
|
|
286
|
+
except httpx.TimeoutException:
|
|
287
|
+
# Connected, but the server did not answer the claim in time.
|
|
288
|
+
self.remote_status_label.setText("Server not responding; try again.")
|
|
289
|
+
return None
|
|
290
|
+
except SessionExpiredError:
|
|
291
|
+
# The connection worked but the server refused/expired the claim itself.
|
|
292
|
+
self.remote_status_label.setText("Claim rejected; try again.")
|
|
252
293
|
return None
|
|
253
294
|
except httpx.HTTPStatusError as e:
|
|
254
295
|
if e.response.status_code == 401:
|
|
255
|
-
self.remote_status_label.setText("
|
|
296
|
+
self.remote_status_label.setText("Invalid API key.")
|
|
256
297
|
elif "text/html" in e.response.headers.get("content-type", ""):
|
|
257
|
-
self.remote_status_label.setText(
|
|
258
|
-
"server returned HTML (HTTP proxy?); set NO_PROXY"
|
|
259
|
-
)
|
|
298
|
+
self.remote_status_label.setText("Not an nnInteractive server (proxy?).")
|
|
260
299
|
else:
|
|
261
|
-
self.remote_status_label.setText(
|
|
262
|
-
|
|
263
|
-
|
|
300
|
+
self.remote_status_label.setText(f"Server error {e.response.status_code}.")
|
|
301
|
+
return None
|
|
302
|
+
except httpx.TransportError:
|
|
303
|
+
# Any other network-level failure (proxy, protocol, broken connection).
|
|
304
|
+
self.remote_status_label.setText("Network error; check connection.")
|
|
264
305
|
return None
|
|
265
306
|
except Exception as e: # noqa: BLE001
|
|
266
|
-
self.remote_status_label.setText(f"
|
|
307
|
+
self.remote_status_label.setText(f"Error: {e}")
|
|
267
308
|
return None
|
|
268
309
|
|
|
269
310
|
# Honor the current auto-zoom checkbox on the remote session too.
|
|
@@ -354,8 +395,35 @@ class nnInteractiveWidget(LayerControls):
|
|
|
354
395
|
self.remote_container.setVisible(self._remote_mode)
|
|
355
396
|
self.on_model_selected()
|
|
356
397
|
|
|
398
|
+
def _store_in_progress_segmentation(self) -> None:
|
|
399
|
+
"""Before a genuine reset (model / mode / server change) drops the session,
|
|
400
|
+
store the object currently being worked on.
|
|
401
|
+
|
|
402
|
+
A genuine reset starts a fresh session, so the in-progress segmentation
|
|
403
|
+
cannot be resumed (unlike a reconnect or a baked-in option change). Rather
|
|
404
|
+
than silently discarding it, store it as a finished object - exactly like
|
|
405
|
+
'Next Object' does. The user can delete the stored object manually if they
|
|
406
|
+
do not want it. The working layer is then removed: its data has already
|
|
407
|
+
been copied into the stored object, and removing it prevents the same
|
|
408
|
+
object being stored twice if the user resets again before re-initializing.
|
|
409
|
+
|
|
410
|
+
Does nothing when there is no non-empty in-progress segmentation.
|
|
411
|
+
"""
|
|
412
|
+
if (
|
|
413
|
+
self.session_cfg is None
|
|
414
|
+
or self.label_layer_name not in self._viewer.layers
|
|
415
|
+
or not np.any(self._viewer.layers[self.label_layer_name].data)
|
|
416
|
+
):
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
self._store_current_object()
|
|
420
|
+
self._viewer.layers.remove(self.label_layer_name)
|
|
421
|
+
|
|
357
422
|
def on_model_selected(self):
|
|
358
423
|
"""Reset the current session completely"""
|
|
424
|
+
# A genuine reset cannot resume the in-progress object, so store it as a
|
|
425
|
+
# finished object before the session is gone instead of losing it.
|
|
426
|
+
self._store_in_progress_segmentation()
|
|
359
427
|
super().on_model_selected()
|
|
360
428
|
self.session = None
|
|
361
429
|
# Genuine reset: the previous model's license no longer applies.
|
|
@@ -366,6 +434,50 @@ class nnInteractiveWidget(LayerControls):
|
|
|
366
434
|
self._resume_after_reconnect = False
|
|
367
435
|
self._resume_image_layer = None
|
|
368
436
|
|
|
437
|
+
def _uninitialize_storing_segmentation(self) -> bool:
|
|
438
|
+
"""Drop the live session, first storing the in-progress object as a
|
|
439
|
+
finished object (like a model/mode change) instead of resuming it on the
|
|
440
|
+
next Initialize. Returns True if a session was actually torn down, False
|
|
441
|
+
when nothing was initialized.
|
|
442
|
+
"""
|
|
443
|
+
if self.session is None:
|
|
444
|
+
# Nothing initialized yet, so there is nothing to store or tear down;
|
|
445
|
+
# the new value is simply picked up at the next Initialize.
|
|
446
|
+
return False
|
|
447
|
+
self._store_in_progress_segmentation()
|
|
448
|
+
self.session = None
|
|
449
|
+
self._clear_layers()
|
|
450
|
+
self._unlock_session()
|
|
451
|
+
return True
|
|
452
|
+
|
|
453
|
+
def on_local_settings_changed(self, *args, **kwargs):
|
|
454
|
+
"""A baked-in local option (torch.compile / interaction storage) changed.
|
|
455
|
+
|
|
456
|
+
The live session was built with the old value, so drop it and force a
|
|
457
|
+
re-Initialize. The new session cannot resume the in-progress object, so
|
|
458
|
+
store it as a finished object first instead of discarding it. The model is
|
|
459
|
+
unchanged, so the displayed license still applies.
|
|
460
|
+
"""
|
|
461
|
+
self._uninitialize_storing_segmentation()
|
|
462
|
+
|
|
463
|
+
def on_checkpoint_changed(self, *args, **kwargs):
|
|
464
|
+
"""The local checkpoint path was edited or cleared (the 'x' button).
|
|
465
|
+
|
|
466
|
+
Like a settings change, drop the session and store the in-progress object
|
|
467
|
+
as a finished object before it is lost. The checkpoint may point at a
|
|
468
|
+
different model though, so drop the displayed license; on_init repopulates
|
|
469
|
+
it once the new session is up.
|
|
470
|
+
"""
|
|
471
|
+
# Re-submitting the same path the live session was built from changes
|
|
472
|
+
# nothing, so leave the session initialized.
|
|
473
|
+
if (
|
|
474
|
+
self.session is not None
|
|
475
|
+
and self.model_selection_local.text() == self._active_checkpoint_text
|
|
476
|
+
):
|
|
477
|
+
return
|
|
478
|
+
if self._uninitialize_storing_segmentation():
|
|
479
|
+
self._update_license_display(None)
|
|
480
|
+
|
|
369
481
|
def on_image_selected(self):
|
|
370
482
|
"""Reset the current sessions interaction but keep the session itself"""
|
|
371
483
|
super().on_image_selected()
|
|
@@ -499,8 +611,46 @@ class nnInteractiveWidget(LayerControls):
|
|
|
499
611
|
self._handle_session_expired()
|
|
500
612
|
return
|
|
501
613
|
|
|
614
|
+
# Record which layer holds this interaction's marker so on_undo can remove it.
|
|
615
|
+
self._interaction_history.append(_layer_name)
|
|
502
616
|
self._viewer.layers[self.label_layer_name].refresh()
|
|
503
617
|
|
|
618
|
+
def on_undo(self):
|
|
619
|
+
"""Undo the most recent interaction for the current object.
|
|
620
|
+
|
|
621
|
+
Reverts the segmentation via the backend's single-level undo and removes the visual
|
|
622
|
+
marker of the undone interaction. Only the most recent interaction can be undone; the
|
|
623
|
+
backend re-arms so the next new interaction becomes undoable again.
|
|
624
|
+
"""
|
|
625
|
+
if self.session is None:
|
|
626
|
+
return
|
|
627
|
+
if not getattr(self.session, "supports_undo", False):
|
|
628
|
+
show_warning("Undo is not supported by this server. Please update nninteractive-server.")
|
|
629
|
+
return
|
|
630
|
+
try:
|
|
631
|
+
undone = self.session.undo()
|
|
632
|
+
except _SESSION_LOST_ERRORS:
|
|
633
|
+
self._handle_session_expired()
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
if not undone:
|
|
637
|
+
show_warning("Nothing to undo.")
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
# Remove the visual marker of the undone interaction, if we tracked one.
|
|
641
|
+
if self._interaction_history:
|
|
642
|
+
layer_name = self._interaction_history.pop()
|
|
643
|
+
if layer_name is not None and layer_name in self._viewer.layers:
|
|
644
|
+
layer = self._viewer.layers[layer_name]
|
|
645
|
+
try:
|
|
646
|
+
layer.remove_last()
|
|
647
|
+
layer.refresh()
|
|
648
|
+
except Exception as e: # noqa: BLE001
|
|
649
|
+
print(f"[napari-nninteractive] could not remove last interaction marker: {e!r}")
|
|
650
|
+
|
|
651
|
+
if self.label_layer_name in self._viewer.layers:
|
|
652
|
+
self._viewer.layers[self.label_layer_name].refresh()
|
|
653
|
+
|
|
504
654
|
def on_load_mask(self):
|
|
505
655
|
|
|
506
656
|
_layer_data = self._viewer.layers[self.label_for_init.currentText()].data
|
|
@@ -520,6 +670,8 @@ class nnInteractiveWidget(LayerControls):
|
|
|
520
670
|
except _SESSION_LOST_ERRORS:
|
|
521
671
|
self._handle_session_expired()
|
|
522
672
|
return
|
|
673
|
+
# Undoable via the backend; there is no interaction-layer marker to remove.
|
|
674
|
+
self._interaction_history.append(None)
|
|
523
675
|
self._viewer.layers[self.label_layer_name].refresh()
|
|
524
676
|
else:
|
|
525
677
|
warnings.warn("Mask is not valid - probably its empty", UserWarning, stacklevel=1)
|
{napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2/src/napari_nninteractive.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: napari-nninteractive
|
|
3
|
-
Version: 2.2
|
|
3
|
+
Version: 2.3.2
|
|
4
4
|
Summary: nnInteractive plugin for Napari
|
|
5
5
|
Author: Lars Krämer, Fabian Isensee, Maximilian Rokuss
|
|
6
6
|
Author-email: lars.kraemer@dkfz-heidelberg.de, f.isensee@dkfz-heidelberg.de, maximilian.rokuss@dkfz-heidelberg.de
|
|
@@ -231,7 +231,7 @@ Requires-Dist: qtpy
|
|
|
231
231
|
Requires-Dist: napari-nifti
|
|
232
232
|
Requires-Dist: huggingface_hub
|
|
233
233
|
Requires-Dist: hf_transfer
|
|
234
|
-
Requires-Dist: nnInteractive>=2.
|
|
234
|
+
Requires-Dist: nnInteractive>=2.4.0
|
|
235
235
|
Requires-Dist: napari_toolkit
|
|
236
236
|
Provides-Extra: testing
|
|
237
237
|
Requires-Dist: tox; extra == "testing"
|
|
@@ -241,7 +241,7 @@ Requires-Dist: pytest-qt; extra == "testing"
|
|
|
241
241
|
Requires-Dist: napari; extra == "testing"
|
|
242
242
|
Requires-Dist: pyqt5; extra == "testing"
|
|
243
243
|
Provides-Extra: remote
|
|
244
|
-
Requires-Dist: nnInteractive[client]>=2.
|
|
244
|
+
Requires-Dist: nnInteractive[client]>=2.4.0; extra == "remote"
|
|
245
245
|
Dynamic: license-file
|
|
246
246
|
|
|
247
247
|
<img src="https://github.com/MIC-DKFZ/napari-nninteractive/raw/main/imgs/nnInteractive_header.png" width="1200">
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/napari.yaml
RENAMED
|
File without changes
|
{napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/utils/__init__.py
RENAMED
|
File without changes
|
{napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/utils/affine.py
RENAMED
|
File without changes
|
{napari_nninteractive-2.2.0 → napari_nninteractive-2.3.2}/src/napari_nninteractive/utils/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|