nnInteractive 2.2.0__tar.gz → 2.3.1__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.
- {nninteractive-2.2.0 → nninteractive-2.3.1}/PKG-INFO +3 -1
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/inference_session.py +61 -8
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/remote/remote_session.py +27 -3
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/remote/serialization.py +74 -3
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/server/app.py +57 -3
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/PKG-INFO +3 -1
- {nninteractive-2.2.0 → nninteractive-2.3.1}/pyproject.toml +1 -1
- {nninteractive-2.2.0 → nninteractive-2.3.1}/readme.md +2 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/LICENSE +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/cvpr2025_challenge_baseline/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/cvpr2025_challenge_baseline/predict.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/remote/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/remote/_protocol.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/server/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/server/main.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/interaction/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/interaction/point.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/setup.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/metadata.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/reader.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/run.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/automatic_mask_generator.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/benchmark.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/build_sam.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/hieradet.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/image_encoder.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/utils.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/memory_attention.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/memory_encoder.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/position_encoding.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/mask_decoder.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/prompt_encoder.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/transformer.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam2_base.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam2_utils.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/sam2_image_predictor.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/sam2_video_predictor.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/sam2_video_predictor_legacy.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/amg.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/misc.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/transforms.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/setup.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/sam2_datasets.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/transforms.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/utils.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_dataset.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_raw_dataset.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_sampler.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_segment_loader.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/loss_fns.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/model/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/model/sam2.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/optimizer.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/scripts/sav_frame_extraction_submitit.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/train.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/trainer.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/checkpoint_utils.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/data_utils.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/distributed.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/logger.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/train_utils.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/supervoxel.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/trainer/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/trainer/nnInteractiveTrainer.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/__init__.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/bboxes.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/checkpoint_cleansing.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/crop.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/erosion_dilation.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/inference_helpers.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/os_shennanigans.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/rounding.py +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/SOURCES.txt +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/dependency_links.txt +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/entry_points.txt +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/requires.txt +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/top_level.txt +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/setup.cfg +0 -0
- {nninteractive-2.2.0 → nninteractive-2.3.1}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nnInteractive
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: Inference code for nnInteractive
|
|
5
5
|
Author: Helmholtz Imaging Applied Computer Vision Lab
|
|
6
6
|
Author-email: Fabian Isensee <f.isensee@dkfz-heidelberg.de>
|
|
@@ -549,6 +549,8 @@ Link: [](https
|
|
|
549
549
|
# License
|
|
550
550
|
Note that while this repository is available under Apache-2.0 license (see [LICENSE](./LICENSE)), the [model checkpoint](https://huggingface.co/nnInteractive/nnInteractive) is `Creative Commons Attribution Non Commercial Share Alike 4.0`!
|
|
551
551
|
|
|
552
|
+
Release model folders ship their own `LICENSE` file whose **first line is the license identifier** (e.g. `CC BY-NC-SA 4.0`); any following lines (such as a link to the full license) are ignored by the tool. At load time this first line is read and exposed as `session.license` so applications can display the model's license prominently. If a checkpoint folder has no `LICENSE` file, the official v1 checkpoint is assumed to be `CC BY-NC-SA 4.0` and any other checkpoint reports `!!MISSING!!`.
|
|
553
|
+
|
|
552
554
|
# Changelog
|
|
553
555
|
|
|
554
556
|
### 1.1.2 - 2025-08-02
|
|
@@ -79,6 +79,11 @@ class nnInteractiveInferenceSession:
|
|
|
79
79
|
self.channel_mapping: dict = {}
|
|
80
80
|
self.supports_initial_label: bool = True
|
|
81
81
|
self.supports_zero_shot_label_refinement: bool = True
|
|
82
|
+
# License of the loaded model checkpoint. Set when the model is loaded
|
|
83
|
+
# (read from the LICENSE file in the checkpoint folder, or derived for
|
|
84
|
+
# legacy checkpoints without one). Exposed so GUIs can display it once
|
|
85
|
+
# the session is initialized. "!!MISSING!!" means the license is unknown.
|
|
86
|
+
self.license: Optional[str] = None
|
|
82
87
|
|
|
83
88
|
# image specific
|
|
84
89
|
self.interactions = None # blosc2.NDArray once initialized
|
|
@@ -118,6 +123,31 @@ class nnInteractiveInferenceSession:
|
|
|
118
123
|
and checkpoint.get("init_args", {}).get("configuration") == "3d_fullres_ps192_bs24"
|
|
119
124
|
)
|
|
120
125
|
|
|
126
|
+
@classmethod
|
|
127
|
+
def _load_license(cls, model_training_output_dir: str, plans: dict, checkpoint: dict) -> str:
|
|
128
|
+
"""Determine the license of the model being loaded.
|
|
129
|
+
|
|
130
|
+
Reads the ``LICENSE`` file from the checkpoint folder if present.
|
|
131
|
+
Expected format: the FIRST line is a short license identifier (e.g.
|
|
132
|
+
``CC BY-NC-SA 4.0``); any following lines (URL, full text, …) are for
|
|
133
|
+
human readers and are ignored. Only the first non-empty line is
|
|
134
|
+
returned, so ``self.license`` stays a short, displayable string.
|
|
135
|
+
|
|
136
|
+
If the folder has no ``LICENSE`` file it is most likely a legacy model:
|
|
137
|
+
the official v1 checkpoint is CC BY-NC-SA 4.0, anything else is reported
|
|
138
|
+
as ``"!!MISSING!!"`` so callers (e.g. GUIs) can flag the unknown license.
|
|
139
|
+
"""
|
|
140
|
+
license_file = join(model_training_output_dir, "LICENSE")
|
|
141
|
+
if isfile(license_file):
|
|
142
|
+
with open(license_file, "r", encoding="utf-8") as f:
|
|
143
|
+
for line in f:
|
|
144
|
+
line = line.strip()
|
|
145
|
+
if line:
|
|
146
|
+
return line
|
|
147
|
+
if cls._is_official_checkpoint(plans, checkpoint):
|
|
148
|
+
return "CC BY-NC-SA 4.0"
|
|
149
|
+
return "!!MISSING!!"
|
|
150
|
+
|
|
121
151
|
def _legacy_default_capability(self) -> dict:
|
|
122
152
|
return {
|
|
123
153
|
"supported_interactions": {
|
|
@@ -535,7 +565,13 @@ class nnInteractiveInferenceSession:
|
|
|
535
565
|
dtype=np.float16,
|
|
536
566
|
chunks=(1, *[min(64, s) for s in shape[1:]]),
|
|
537
567
|
blocks=(1, *[min(32, s) for s in shape[1:]]),
|
|
538
|
-
|
|
568
|
+
# Interactions compress better with NOFILTER, which is also faster than SHUFFLE.
|
|
569
|
+
cparams={
|
|
570
|
+
"codec": blosc2.Codec.LZ4,
|
|
571
|
+
"clevel": 5,
|
|
572
|
+
"filters": [blosc2.Filter.NOFILTER],
|
|
573
|
+
"nthreads": min(self.torch_n_threads, os.cpu_count()),
|
|
574
|
+
},
|
|
539
575
|
dparams={"nthreads": 4},
|
|
540
576
|
)
|
|
541
577
|
self._interactions_shape = shape
|
|
@@ -604,7 +640,13 @@ class nnInteractiveInferenceSession:
|
|
|
604
640
|
dtype=np.float16,
|
|
605
641
|
chunks=(1, *[min(64, s) for s in self._interactions_shape[1:]]),
|
|
606
642
|
blocks=(1, *[min(32, s) for s in self._interactions_shape[1:]]),
|
|
607
|
-
|
|
643
|
+
# Interactions compress better with NOFILTER, which is also faster than SHUFFLE.
|
|
644
|
+
cparams={
|
|
645
|
+
"codec": blosc2.Codec.LZ4,
|
|
646
|
+
"clevel": 5,
|
|
647
|
+
"filters": [blosc2.Filter.NOFILTER],
|
|
648
|
+
"nthreads": os.cpu_count(),
|
|
649
|
+
},
|
|
608
650
|
dparams={"nthreads": 4},
|
|
609
651
|
)
|
|
610
652
|
self.current_interaction_intensity = 1.0
|
|
@@ -1334,6 +1376,16 @@ class nnInteractiveInferenceSession:
|
|
|
1334
1376
|
"""
|
|
1335
1377
|
artifacts = self._load_model_artifacts_from_disk(model_training_output_dir, use_fold, checkpoint_name)
|
|
1336
1378
|
self.initialize_from_loaded_artifacts(artifacts)
|
|
1379
|
+
# With torch.compile the network is compiled lazily on the first forward pass. For a
|
|
1380
|
+
# locally hosted model that lag would otherwise surface on the user's first real
|
|
1381
|
+
# prediction, where it is far more noticeable than during initialization. Trigger the
|
|
1382
|
+
# compilation now with a dummy forward pass so the cost is paid here instead. warmup()
|
|
1383
|
+
# is a no-op when the network is not compiled. The server takes care of its own warmup
|
|
1384
|
+
# explicitly (it shares one compiled network across sessions via
|
|
1385
|
+
# initialize_from_loaded_artifacts), so we only do this on the direct, local entry point.
|
|
1386
|
+
if self.use_torch_compile:
|
|
1387
|
+
print("torch.compile enabled; warming up (compiling) the network now (this is slow once)...")
|
|
1388
|
+
self.warmup()
|
|
1337
1389
|
|
|
1338
1390
|
def _load_model_artifacts_from_disk(
|
|
1339
1391
|
self,
|
|
@@ -1389,12 +1441,11 @@ class nnInteractiveInferenceSession:
|
|
|
1389
1441
|
checkpoint = torch.load(
|
|
1390
1442
|
join(model_training_output_dir, fold_folder, checkpoint_name), map_location=self.device, weights_only=False
|
|
1391
1443
|
)
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
)
|
|
1444
|
+
self.license = self._load_license(model_training_output_dir, plans, checkpoint)
|
|
1445
|
+
print("=" * 80)
|
|
1446
|
+
print("Model license:")
|
|
1447
|
+
print(self.license)
|
|
1448
|
+
print("=" * 80)
|
|
1398
1449
|
trainer_name = checkpoint["trainer_name"]
|
|
1399
1450
|
configuration_name = checkpoint["init_args"]["configuration"]
|
|
1400
1451
|
|
|
@@ -1440,6 +1491,7 @@ class nnInteractiveInferenceSession:
|
|
|
1440
1491
|
"dataset_json": dataset_json,
|
|
1441
1492
|
"trainer_name": trainer_name,
|
|
1442
1493
|
"label_manager": plans_manager.get_label_manager(dataset_json),
|
|
1494
|
+
"license": self.license,
|
|
1443
1495
|
}
|
|
1444
1496
|
|
|
1445
1497
|
def initialize_from_loaded_artifacts(self, artifacts: dict):
|
|
@@ -1463,6 +1515,7 @@ class nnInteractiveInferenceSession:
|
|
|
1463
1515
|
self.dataset_json = artifacts["dataset_json"]
|
|
1464
1516
|
self.trainer_name = artifacts["trainer_name"]
|
|
1465
1517
|
self.label_manager = artifacts["label_manager"]
|
|
1518
|
+
self.license = artifacts["license"]
|
|
1466
1519
|
if self.use_torch_compile and not isinstance(self.network, OptimizedModule):
|
|
1467
1520
|
print("Using torch.compile")
|
|
1468
1521
|
self.network = torch.compile(self.network)
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/remote/remote_session.py
RENAMED
|
@@ -14,6 +14,7 @@ import threading
|
|
|
14
14
|
import warnings
|
|
15
15
|
from typing import List, Optional, Tuple, Union
|
|
16
16
|
|
|
17
|
+
import blosc2
|
|
17
18
|
import httpx
|
|
18
19
|
import numpy as np
|
|
19
20
|
import torch
|
|
@@ -41,6 +42,16 @@ from nnInteractive.inference.remote._protocol import (
|
|
|
41
42
|
from nnInteractive.inference.remote.serialization import pack_array, unpack_array
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
def _compression_threads() -> int:
|
|
46
|
+
"""blosc2 thread count for client-side upload compression.
|
|
47
|
+
|
|
48
|
+
Full logical CPU count: blosc2 scales measurably onto SMT siblings, so use them all to
|
|
49
|
+
minimize upload latency. Per-call only (passed to pack_array → compress2), so it never
|
|
50
|
+
mutates blosc2's global nthreads.
|
|
51
|
+
"""
|
|
52
|
+
return max(1, os.cpu_count() or 1)
|
|
53
|
+
|
|
54
|
+
|
|
44
55
|
class SessionExpiredError(RuntimeError):
|
|
45
56
|
"""Raised when the server reports the client's lease no longer exists.
|
|
46
57
|
|
|
@@ -191,6 +202,11 @@ class nnInteractiveRemoteInferenceSession:
|
|
|
191
202
|
self.preferred_scribble_thickness = caps["preferred_scribble_thickness"]
|
|
192
203
|
self.interaction_decay = caps["interaction_decay"]
|
|
193
204
|
self.INFERENCE_SESSION_VERSION = caps["inference_session_version"]
|
|
205
|
+
# License of the model loaded on the server. Mirrors
|
|
206
|
+
# nnInteractiveInferenceSession.license so a GUI can display it
|
|
207
|
+
# regardless of whether it holds a local or remote session.
|
|
208
|
+
# "!!MISSING!!" means the server could not determine the license.
|
|
209
|
+
self.license: Optional[str] = caps.get("license")
|
|
194
210
|
|
|
195
211
|
self.original_image_shape: Optional[Tuple[int, ...]] = None
|
|
196
212
|
self.target_buffer: Union[np.ndarray, torch.Tensor, None] = None
|
|
@@ -284,7 +300,7 @@ class nnInteractiveRemoteInferenceSession:
|
|
|
284
300
|
def set_image(self, image: np.ndarray, image_properties: Optional[dict] = None) -> None:
|
|
285
301
|
assert image.ndim == 4, f"expected a 4d image as input, got {image.ndim}d. Shape {image.shape}"
|
|
286
302
|
meta = {"image_properties": image_properties or {}}
|
|
287
|
-
resp = self._post_binary(PATH_SET_IMAGE, meta, pack_array(image))
|
|
303
|
+
resp = self._post_binary(PATH_SET_IMAGE, meta, pack_array(image, nthreads=_compression_threads()))
|
|
288
304
|
info = resp.json()
|
|
289
305
|
self.original_image_shape = tuple(info["original_image_shape"])
|
|
290
306
|
|
|
@@ -395,7 +411,10 @@ class nnInteractiveRemoteInferenceSession:
|
|
|
395
411
|
"override_capability_checks": bool(override_capability_checks),
|
|
396
412
|
"interaction_bbox": ([list(b) for b in interaction_bbox] if interaction_bbox is not None else None),
|
|
397
413
|
}
|
|
398
|
-
|
|
414
|
+
# Interactions (scribble/lasso masks) compress best with NOFILTER; skip auto-selection.
|
|
415
|
+
resp = self._post_binary(
|
|
416
|
+
path, meta, pack_array(mask_image, filters=[blosc2.Filter.NOFILTER], nthreads=_compression_threads())
|
|
417
|
+
)
|
|
399
418
|
self._apply_prediction_response(resp)
|
|
400
419
|
|
|
401
420
|
def add_initial_seg_interaction(
|
|
@@ -420,7 +439,12 @@ class nnInteractiveRemoteInferenceSession:
|
|
|
420
439
|
"run_prediction": bool(run_prediction),
|
|
421
440
|
"override_capability_checks": bool(override_capability_checks),
|
|
422
441
|
}
|
|
423
|
-
|
|
442
|
+
# Segmentations compress best with NOFILTER; skip auto-selection.
|
|
443
|
+
resp = self._post_binary(
|
|
444
|
+
PATH_ADD_INITIAL_SEG,
|
|
445
|
+
meta,
|
|
446
|
+
pack_array(initial_seg, filters=[blosc2.Filter.NOFILTER], nthreads=_compression_threads()),
|
|
447
|
+
)
|
|
424
448
|
self._apply_prediction_response(resp)
|
|
425
449
|
|
|
426
450
|
# ------------------------------------------------------------------ #
|
|
@@ -47,8 +47,72 @@ _CODEC_ID = {
|
|
|
47
47
|
_ID_CODEC = {v: k for k, v in _CODEC_ID.items()}
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
# Fraction of each axis used for the center crop that the filter heuristic compresses.
|
|
51
|
+
_SELECT_FILTER_CROP_FRACTION = 0.25
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _compress_all(
|
|
55
|
+
raw: memoryview, total: int, codec: blosc2.Codec, clevel: int, filters: list, nthreads: Optional[int]
|
|
56
|
+
) -> int:
|
|
57
|
+
"""Compressed byte length of ``raw`` under ``filters``, chunked exactly as pack_array does."""
|
|
58
|
+
extra = {} if nthreads is None else {"nthreads": nthreads}
|
|
59
|
+
size = 0
|
|
60
|
+
nchunks = (total + _CHUNK_SIZE - 1) // _CHUNK_SIZE
|
|
61
|
+
for i in range(nchunks):
|
|
62
|
+
start = i * _CHUNK_SIZE
|
|
63
|
+
end = min(start + _CHUNK_SIZE, total)
|
|
64
|
+
size += len(blosc2.compress2(raw[start:end], codec=codec, clevel=clevel, filters=filters, **extra))
|
|
65
|
+
return size
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _select_filter(arr: np.ndarray, codec: blosc2.Codec, clevel: int, nthreads: Optional[int]) -> "blosc2.Filter":
|
|
69
|
+
"""Pick NOFILTER vs SHUFFLE for ``arr`` by trial-compressing a small centered crop.
|
|
70
|
+
|
|
71
|
+
Uses ``compress2`` on the raw bytes — exactly the path pack_array takes — so the decision
|
|
72
|
+
is consistent with how the whole array is actually compressed. The crop is
|
|
73
|
+
``_SELECT_FILTER_CROP_FRACTION`` of each axis (centered), keeping the trial cheap and
|
|
74
|
+
representative (lands on foreground). Ties go to NOFILTER; any failure falls back to it.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
crop_shape = [max(1, int(s * _SELECT_FILTER_CROP_FRACTION)) for s in arr.shape]
|
|
78
|
+
slices = tuple(slice((s - cs) // 2, (s - cs) // 2 + cs) for s, cs in zip(arr.shape, crop_shape))
|
|
79
|
+
crop = np.ascontiguousarray(arr[slices])
|
|
80
|
+
raw = memoryview(crop).cast("B")
|
|
81
|
+
total = raw.nbytes
|
|
82
|
+
|
|
83
|
+
best_filter, best_bytes = blosc2.Filter.NOFILTER, None
|
|
84
|
+
for f in (blosc2.Filter.NOFILTER, blosc2.Filter.SHUFFLE):
|
|
85
|
+
cb = _compress_all(raw, total, codec, clevel, [f], nthreads)
|
|
86
|
+
if best_bytes is None or cb < best_bytes:
|
|
87
|
+
best_bytes, best_filter = cb, f
|
|
88
|
+
return best_filter
|
|
89
|
+
except Exception as e:
|
|
90
|
+
from warnings import warn
|
|
91
|
+
|
|
92
|
+
warn(f"_select_filter failed ({e!r}); falling back to NOFILTER.")
|
|
93
|
+
return blosc2.Filter.NOFILTER
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def pack_array(
|
|
97
|
+
arr: np.ndarray,
|
|
98
|
+
codec: blosc2.Codec = blosc2.Codec.ZSTD,
|
|
99
|
+
clevel: int = 3,
|
|
100
|
+
filters: Optional[list] = None,
|
|
101
|
+
nthreads: Optional[int] = None,
|
|
102
|
+
) -> bytes:
|
|
103
|
+
"""Serialize a numpy array to a self-describing compressed byte string.
|
|
104
|
+
|
|
105
|
+
``filters`` is the blosc2 filter pipeline to apply. If ``None`` (the default), the
|
|
106
|
+
better of NOFILTER/SHUFFLE is auto-selected by trial-compressing a cheap, representative
|
|
107
|
+
slab — appropriate for images, whose optimum depends on the data. Callers that already
|
|
108
|
+
know the optimum (interactions and segmentations compress best with NOFILTER) should pass
|
|
109
|
+
``[blosc2.Filter.NOFILTER]`` to skip the selection. The chosen filter is self-describing
|
|
110
|
+
inside the blosc2 frame, so unpack_array (decompress2) needs no changes.
|
|
111
|
+
|
|
112
|
+
``nthreads`` is the per-call blosc2 thread count for compression. ``None`` (the default)
|
|
113
|
+
inherits blosc2's global ``nthreads`` (= core count). Passing an explicit value overrides
|
|
114
|
+
it for this call only, without mutating global state.
|
|
115
|
+
"""
|
|
52
116
|
arr = np.ascontiguousarray(arr)
|
|
53
117
|
dtype_str = arr.dtype.str.lstrip("<>|=").encode("ascii")
|
|
54
118
|
if arr.dtype.byteorder not in ("=", "|", "<"):
|
|
@@ -77,11 +141,18 @@ def pack_array(arr: np.ndarray, codec: blosc2.Codec = blosc2.Codec.ZSTD, clevel:
|
|
|
77
141
|
raw = memoryview(arr).cast("B")
|
|
78
142
|
total = raw.nbytes
|
|
79
143
|
nchunks = (total + _CHUNK_SIZE - 1) // _CHUNK_SIZE
|
|
144
|
+
|
|
145
|
+
if filters is None:
|
|
146
|
+
# Auto-select the better filter from a small centered crop, using the same
|
|
147
|
+
# compress2 path as below for consistency.
|
|
148
|
+
filters = [_select_filter(arr, codec, clevel, nthreads)]
|
|
149
|
+
|
|
150
|
+
extra = {} if nthreads is None else {"nthreads": nthreads}
|
|
80
151
|
parts = [header, shape_bytes, struct.pack("<I", nchunks)]
|
|
81
152
|
for i in range(nchunks):
|
|
82
153
|
start = i * _CHUNK_SIZE
|
|
83
154
|
end = min(start + _CHUNK_SIZE, total)
|
|
84
|
-
chunk = blosc2.compress2(raw[start:end], codec=codec, clevel=clevel)
|
|
155
|
+
chunk = blosc2.compress2(raw[start:end], codec=codec, clevel=clevel, filters=filters, **extra)
|
|
85
156
|
parts.append(struct.pack("<QQ", end - start, len(chunk)))
|
|
86
157
|
parts.append(chunk)
|
|
87
158
|
return b"".join(parts)
|
|
@@ -38,12 +38,15 @@ from __future__ import annotations
|
|
|
38
38
|
import asyncio
|
|
39
39
|
import json
|
|
40
40
|
import logging
|
|
41
|
+
import numbers
|
|
42
|
+
import os
|
|
41
43
|
import threading
|
|
42
44
|
import time
|
|
43
45
|
import uuid
|
|
44
46
|
from contextlib import asynccontextmanager
|
|
45
47
|
from typing import Optional
|
|
46
48
|
|
|
49
|
+
import blosc2
|
|
47
50
|
import numpy as np
|
|
48
51
|
import torch
|
|
49
52
|
from fastapi import Depends, FastAPI, HTTPException, Header, Request, Response, status
|
|
@@ -73,6 +76,14 @@ from nnInteractive.inference.remote.serialization import pack_array, unpack_arra
|
|
|
73
76
|
|
|
74
77
|
logger = logging.getLogger("nninteractive.server")
|
|
75
78
|
|
|
79
|
+
# Cap a single client's target buffer at 25% of total system RAM. Falls back to 32 GiB
|
|
80
|
+
# of headroom if the system RAM can't be determined.
|
|
81
|
+
try:
|
|
82
|
+
total_ram = os.sysconf("SC_PHYS_PAGES") * os.sysconf("SC_PAGE_SIZE")
|
|
83
|
+
except (ValueError, OSError, AttributeError):
|
|
84
|
+
total_ram = 32 * 1024**3
|
|
85
|
+
MAX_TARGET_BUFFER_BYTES = int(total_ram * 0.25)
|
|
86
|
+
|
|
76
87
|
|
|
77
88
|
class SessionEntry:
|
|
78
89
|
"""One client's session plus its bookkeeping."""
|
|
@@ -431,11 +442,54 @@ def make_app(
|
|
|
431
442
|
# Reset so a subsequent call without a prediction can't accidentally re-send a stale region.
|
|
432
443
|
session._last_paste_bbox = None
|
|
433
444
|
return Response(
|
|
434
|
-
|
|
445
|
+
# Segmentations compress best with NOFILTER; skip auto-selection.
|
|
446
|
+
content=pack_array(
|
|
447
|
+
sub, filters=[blosc2.Filter.NOFILTER], nthreads=min(session.torch_n_threads, os.cpu_count())
|
|
448
|
+
),
|
|
435
449
|
media_type=CONTENT_TYPE_OCTET_STREAM,
|
|
436
450
|
headers={META_HEADER: json.dumps(meta, separators=(",", ":"))},
|
|
437
451
|
)
|
|
438
452
|
|
|
453
|
+
def _parse_target_buffer_request(payload: dict) -> tuple[tuple[int, ...], np.dtype]:
|
|
454
|
+
if "shape" not in payload:
|
|
455
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="missing required field: shape")
|
|
456
|
+
if "dtype" not in payload:
|
|
457
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="missing required field: dtype")
|
|
458
|
+
|
|
459
|
+
raw_shape = payload["shape"]
|
|
460
|
+
if not isinstance(raw_shape, list):
|
|
461
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="shape must be a list of positive integers")
|
|
462
|
+
if len(raw_shape) != 3:
|
|
463
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="shape must be 3D")
|
|
464
|
+
|
|
465
|
+
shape = []
|
|
466
|
+
for dim in raw_shape:
|
|
467
|
+
if not isinstance(dim, numbers.Integral) or isinstance(dim, bool):
|
|
468
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="shape must contain only integers")
|
|
469
|
+
if dim <= 0:
|
|
470
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="shape dimensions must be positive")
|
|
471
|
+
shape.append(dim)
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
dtype = np.dtype(payload["dtype"])
|
|
475
|
+
except (TypeError, ValueError) as e:
|
|
476
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=f"invalid dtype: {payload['dtype']!r}") from e
|
|
477
|
+
# 'b' = bool, 'i' = signed int, 'u' = unsigned int.
|
|
478
|
+
if dtype.kind not in ("b", "i", "u"):
|
|
479
|
+
raise HTTPException(
|
|
480
|
+
status.HTTP_400_BAD_REQUEST,
|
|
481
|
+
detail=f"unsupported dtype {dtype}: target buffer must be bool or an integer type",
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
nbytes = int(np.prod(shape, dtype=np.uint64)) * dtype.itemsize
|
|
485
|
+
if nbytes > MAX_TARGET_BUFFER_BYTES:
|
|
486
|
+
raise HTTPException(
|
|
487
|
+
status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
|
488
|
+
detail=(f"target buffer would require {nbytes} bytes, " f"limit is {MAX_TARGET_BUFFER_BYTES} bytes"),
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
return tuple(shape), dtype
|
|
492
|
+
|
|
439
493
|
def _under_session_lock(entry: SessionEntry, fn):
|
|
440
494
|
"""Run ``fn(session)`` under the session's lock, converting known errors to HTTP 400.
|
|
441
495
|
|
|
@@ -530,8 +584,7 @@ def make_app(
|
|
|
530
584
|
|
|
531
585
|
@app.post(PATH_SET_TARGET_BUFFER, dependencies=[auth])
|
|
532
586
|
def set_target_buffer(payload: dict, entry: SessionEntry = lease) -> dict:
|
|
533
|
-
shape =
|
|
534
|
-
dtype = np.dtype(payload["dtype"])
|
|
587
|
+
shape, dtype = _parse_target_buffer_request(payload)
|
|
535
588
|
buf = np.zeros(shape, dtype=dtype)
|
|
536
589
|
|
|
537
590
|
def _do(session):
|
|
@@ -659,4 +712,5 @@ def _build_capability_snapshot(session: nnInteractiveInferenceSession) -> dict:
|
|
|
659
712
|
"patch_size": list(cfg.patch_size) if cfg is not None else None,
|
|
660
713
|
"do_autozoom": bool(session.do_autozoom),
|
|
661
714
|
"inference_session_version": session.INFERENCE_SESSION_VERSION,
|
|
715
|
+
"license": session.license,
|
|
662
716
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nnInteractive
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: Inference code for nnInteractive
|
|
5
5
|
Author: Helmholtz Imaging Applied Computer Vision Lab
|
|
6
6
|
Author-email: Fabian Isensee <f.isensee@dkfz-heidelberg.de>
|
|
@@ -549,6 +549,8 @@ Link: [](https
|
|
|
549
549
|
# License
|
|
550
550
|
Note that while this repository is available under Apache-2.0 license (see [LICENSE](./LICENSE)), the [model checkpoint](https://huggingface.co/nnInteractive/nnInteractive) is `Creative Commons Attribution Non Commercial Share Alike 4.0`!
|
|
551
551
|
|
|
552
|
+
Release model folders ship their own `LICENSE` file whose **first line is the license identifier** (e.g. `CC BY-NC-SA 4.0`); any following lines (such as a link to the full license) are ignored by the tool. At load time this first line is read and exposed as `session.license` so applications can display the model's license prominently. If a checkpoint folder has no `LICENSE` file, the official v1 checkpoint is assumed to be `CC BY-NC-SA 4.0` and any other checkpoint reports `!!MISSING!!`.
|
|
553
|
+
|
|
552
554
|
# Changelog
|
|
553
555
|
|
|
554
556
|
### 1.1.2 - 2025-08-02
|
|
@@ -311,6 +311,8 @@ Link: [](https
|
|
|
311
311
|
# License
|
|
312
312
|
Note that while this repository is available under Apache-2.0 license (see [LICENSE](./LICENSE)), the [model checkpoint](https://huggingface.co/nnInteractive/nnInteractive) is `Creative Commons Attribution Non Commercial Share Alike 4.0`!
|
|
313
313
|
|
|
314
|
+
Release model folders ship their own `LICENSE` file whose **first line is the license identifier** (e.g. `CC BY-NC-SA 4.0`); any following lines (such as a link to the full license) are ignored by the tool. At load time this first line is read and exposed as `session.license` so applications can display the model's license prominently. If a checkpoint folder has no `LICENSE` file, the official v1 checkpoint is assumed to be `CC BY-NC-SA 4.0` and any other checkpoint reports `!!MISSING!!`.
|
|
315
|
+
|
|
314
316
|
# Changelog
|
|
315
317
|
|
|
316
318
|
### 1.1.2 - 2025-08-02
|
|
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
|
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/benchmark.py
RENAMED
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/build_sam.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/__init__.py
RENAMED
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/amg.py
RENAMED
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/misc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/__init__.py
RENAMED
|
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
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/loss_fns.py
RENAMED
|
File without changes
|
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/model/sam2.py
RENAMED
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/optimizer.py
RENAMED
|
File without changes
|
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/train.py
RENAMED
|
File without changes
|
{nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/trainer.py
RENAMED
|
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
|
|
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
|