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.
Files changed (90) hide show
  1. {nninteractive-2.2.0 → nninteractive-2.3.1}/PKG-INFO +3 -1
  2. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/inference_session.py +61 -8
  3. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/remote/remote_session.py +27 -3
  4. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/remote/serialization.py +74 -3
  5. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/server/app.py +57 -3
  6. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/PKG-INFO +3 -1
  7. {nninteractive-2.2.0 → nninteractive-2.3.1}/pyproject.toml +1 -1
  8. {nninteractive-2.2.0 → nninteractive-2.3.1}/readme.md +2 -0
  9. {nninteractive-2.2.0 → nninteractive-2.3.1}/LICENSE +0 -0
  10. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/__init__.py +0 -0
  11. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/__init__.py +0 -0
  12. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/cvpr2025_challenge_baseline/__init__.py +0 -0
  13. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/cvpr2025_challenge_baseline/predict.py +0 -0
  14. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/remote/__init__.py +0 -0
  15. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/remote/_protocol.py +0 -0
  16. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/server/__init__.py +0 -0
  17. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/inference/server/main.py +0 -0
  18. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/interaction/__init__.py +0 -0
  19. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/interaction/point.py +0 -0
  20. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/setup.py +0 -0
  21. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/metadata.py +0 -0
  22. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/reader.py +0 -0
  23. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/run.py +0 -0
  24. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/__init__.py +0 -0
  25. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/__init__.py +0 -0
  26. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/automatic_mask_generator.py +0 -0
  27. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/benchmark.py +0 -0
  28. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/build_sam.py +0 -0
  29. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/__init__.py +0 -0
  30. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/__init__.py +0 -0
  31. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/hieradet.py +0 -0
  32. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/image_encoder.py +0 -0
  33. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/utils.py +0 -0
  34. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/memory_attention.py +0 -0
  35. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/memory_encoder.py +0 -0
  36. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/position_encoding.py +0 -0
  37. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/__init__.py +0 -0
  38. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/mask_decoder.py +0 -0
  39. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/prompt_encoder.py +0 -0
  40. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/transformer.py +0 -0
  41. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam2_base.py +0 -0
  42. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam2_utils.py +0 -0
  43. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/sam2_image_predictor.py +0 -0
  44. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/sam2_video_predictor.py +0 -0
  45. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/sam2_video_predictor_legacy.py +0 -0
  46. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/__init__.py +0 -0
  47. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/amg.py +0 -0
  48. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/misc.py +0 -0
  49. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/sam2/utils/transforms.py +0 -0
  50. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/setup.py +0 -0
  51. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/__init__.py +0 -0
  52. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/__init__.py +0 -0
  53. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/sam2_datasets.py +0 -0
  54. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/transforms.py +0 -0
  55. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/utils.py +0 -0
  56. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_dataset.py +0 -0
  57. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_raw_dataset.py +0 -0
  58. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_sampler.py +0 -0
  59. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_segment_loader.py +0 -0
  60. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/loss_fns.py +0 -0
  61. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/model/__init__.py +0 -0
  62. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/model/sam2.py +0 -0
  63. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/optimizer.py +0 -0
  64. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/scripts/sav_frame_extraction_submitit.py +0 -0
  65. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/train.py +0 -0
  66. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/trainer.py +0 -0
  67. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/__init__.py +0 -0
  68. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/checkpoint_utils.py +0 -0
  69. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/data_utils.py +0 -0
  70. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/distributed.py +0 -0
  71. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/logger.py +0 -0
  72. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/sam2/training/utils/train_utils.py +0 -0
  73. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/supervoxel/src/supervoxel.py +0 -0
  74. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/trainer/__init__.py +0 -0
  75. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/trainer/nnInteractiveTrainer.py +0 -0
  76. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/__init__.py +0 -0
  77. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/bboxes.py +0 -0
  78. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/checkpoint_cleansing.py +0 -0
  79. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/crop.py +0 -0
  80. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/erosion_dilation.py +0 -0
  81. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/inference_helpers.py +0 -0
  82. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/os_shennanigans.py +0 -0
  83. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive/utils/rounding.py +0 -0
  84. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/SOURCES.txt +0 -0
  85. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/dependency_links.txt +0 -0
  86. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/entry_points.txt +0 -0
  87. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/requires.txt +0 -0
  88. {nninteractive-2.2.0 → nninteractive-2.3.1}/nnInteractive.egg-info/top_level.txt +0 -0
  89. {nninteractive-2.2.0 → nninteractive-2.3.1}/setup.cfg +0 -0
  90. {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.2.0
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: [![arXiv](https://img.shields.io/badge/arXiv-2503.08373-b31b1b.svg)](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
- cparams={"codec": blosc2.Codec.LZ4, "clevel": 5, "nthreads": min(self.torch_n_threads, os.cpu_count())},
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
- cparams={"codec": blosc2.Codec.LZ4, "clevel": 5, "nthreads": os.cpu_count()},
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
- if self._is_official_checkpoint(plans, checkpoint):
1393
- print(
1394
- "License reminder: The official nnInteractive checkpoint is licensed under "
1395
- "Creative Commons Attribution Non Commercial Share Alike 4.0 (CC BY-NC-SA 4.0). "
1396
- "See the license note in readme.md (# License)."
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)
@@ -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
- resp = self._post_binary(path, meta, pack_array(mask_image))
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
- resp = self._post_binary(PATH_ADD_INITIAL_SEG, meta, pack_array(initial_seg))
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
- def pack_array(arr: np.ndarray, codec: blosc2.Codec = blosc2.Codec.ZSTD, clevel: int = 3) -> bytes:
51
- """Serialize a numpy array to a self-describing compressed byte string."""
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
- content=pack_array(sub),
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 = tuple(int(x) for x in payload["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.2.0
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: [![arXiv](https://img.shields.io/badge/arXiv-2503.08373-b31b1b.svg)](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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nnInteractive"
3
- version = "2.2.0"
3
+ version = "2.3.1"
4
4
  requires-python = ">=3.10"
5
5
  description = "Inference code for nnInteractive"
6
6
  readme = "readme.md"
@@ -311,6 +311,8 @@ Link: [![arXiv](https://img.shields.io/badge/arXiv-2503.08373-b31b1b.svg)](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