nnInteractive 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. {nninteractive-2.2.0 → nninteractive-2.3.0}/PKG-INFO +3 -1
  2. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/inference_session.py +61 -8
  3. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/remote/remote_session.py +10 -2
  4. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/remote/serialization.py +65 -3
  5. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/server/app.py +55 -3
  6. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive.egg-info/PKG-INFO +3 -1
  7. {nninteractive-2.2.0 → nninteractive-2.3.0}/pyproject.toml +1 -1
  8. {nninteractive-2.2.0 → nninteractive-2.3.0}/readme.md +2 -0
  9. {nninteractive-2.2.0 → nninteractive-2.3.0}/LICENSE +0 -0
  10. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/__init__.py +0 -0
  11. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/__init__.py +0 -0
  12. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/cvpr2025_challenge_baseline/__init__.py +0 -0
  13. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/cvpr2025_challenge_baseline/predict.py +0 -0
  14. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/remote/__init__.py +0 -0
  15. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/remote/_protocol.py +0 -0
  16. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/server/__init__.py +0 -0
  17. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/inference/server/main.py +0 -0
  18. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/interaction/__init__.py +0 -0
  19. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/interaction/point.py +0 -0
  20. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/setup.py +0 -0
  21. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/metadata.py +0 -0
  22. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/reader.py +0 -0
  23. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/run.py +0 -0
  24. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/__init__.py +0 -0
  25. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/__init__.py +0 -0
  26. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/automatic_mask_generator.py +0 -0
  27. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/benchmark.py +0 -0
  28. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/build_sam.py +0 -0
  29. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/__init__.py +0 -0
  30. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/__init__.py +0 -0
  31. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/hieradet.py +0 -0
  32. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/image_encoder.py +0 -0
  33. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/backbones/utils.py +0 -0
  34. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/memory_attention.py +0 -0
  35. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/memory_encoder.py +0 -0
  36. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/position_encoding.py +0 -0
  37. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/__init__.py +0 -0
  38. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/mask_decoder.py +0 -0
  39. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/prompt_encoder.py +0 -0
  40. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam/transformer.py +0 -0
  41. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam2_base.py +0 -0
  42. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/modeling/sam2_utils.py +0 -0
  43. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/sam2_image_predictor.py +0 -0
  44. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/sam2_video_predictor.py +0 -0
  45. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/sam2_video_predictor_legacy.py +0 -0
  46. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/utils/__init__.py +0 -0
  47. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/utils/amg.py +0 -0
  48. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/utils/misc.py +0 -0
  49. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/sam2/utils/transforms.py +0 -0
  50. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/setup.py +0 -0
  51. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/__init__.py +0 -0
  52. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/dataset/__init__.py +0 -0
  53. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/dataset/sam2_datasets.py +0 -0
  54. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/dataset/transforms.py +0 -0
  55. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/dataset/utils.py +0 -0
  56. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_dataset.py +0 -0
  57. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_raw_dataset.py +0 -0
  58. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_sampler.py +0 -0
  59. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/dataset/vos_segment_loader.py +0 -0
  60. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/loss_fns.py +0 -0
  61. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/model/__init__.py +0 -0
  62. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/model/sam2.py +0 -0
  63. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/optimizer.py +0 -0
  64. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/scripts/sav_frame_extraction_submitit.py +0 -0
  65. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/train.py +0 -0
  66. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/trainer.py +0 -0
  67. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/utils/__init__.py +0 -0
  68. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/utils/checkpoint_utils.py +0 -0
  69. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/utils/data_utils.py +0 -0
  70. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/utils/distributed.py +0 -0
  71. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/utils/logger.py +0 -0
  72. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/sam2/training/utils/train_utils.py +0 -0
  73. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/supervoxel/src/supervoxel.py +0 -0
  74. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/trainer/__init__.py +0 -0
  75. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/trainer/nnInteractiveTrainer.py +0 -0
  76. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/utils/__init__.py +0 -0
  77. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/utils/bboxes.py +0 -0
  78. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/utils/checkpoint_cleansing.py +0 -0
  79. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/utils/crop.py +0 -0
  80. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/utils/erosion_dilation.py +0 -0
  81. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/utils/inference_helpers.py +0 -0
  82. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/utils/os_shennanigans.py +0 -0
  83. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive/utils/rounding.py +0 -0
  84. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive.egg-info/SOURCES.txt +0 -0
  85. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive.egg-info/dependency_links.txt +0 -0
  86. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive.egg-info/entry_points.txt +0 -0
  87. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive.egg-info/requires.txt +0 -0
  88. {nninteractive-2.2.0 → nninteractive-2.3.0}/nnInteractive.egg-info/top_level.txt +0 -0
  89. {nninteractive-2.2.0 → nninteractive-2.3.0}/setup.cfg +0 -0
  90. {nninteractive-2.2.0 → nninteractive-2.3.0}/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.0
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
@@ -191,6 +192,11 @@ class nnInteractiveRemoteInferenceSession:
191
192
  self.preferred_scribble_thickness = caps["preferred_scribble_thickness"]
192
193
  self.interaction_decay = caps["interaction_decay"]
193
194
  self.INFERENCE_SESSION_VERSION = caps["inference_session_version"]
195
+ # License of the model loaded on the server. Mirrors
196
+ # nnInteractiveInferenceSession.license so a GUI can display it
197
+ # regardless of whether it holds a local or remote session.
198
+ # "!!MISSING!!" means the server could not determine the license.
199
+ self.license: Optional[str] = caps.get("license")
194
200
 
195
201
  self.original_image_shape: Optional[Tuple[int, ...]] = None
196
202
  self.target_buffer: Union[np.ndarray, torch.Tensor, None] = None
@@ -395,7 +401,8 @@ class nnInteractiveRemoteInferenceSession:
395
401
  "override_capability_checks": bool(override_capability_checks),
396
402
  "interaction_bbox": ([list(b) for b in interaction_bbox] if interaction_bbox is not None else None),
397
403
  }
398
- resp = self._post_binary(path, meta, pack_array(mask_image))
404
+ # Interactions (scribble/lasso masks) compress best with NOFILTER; skip auto-selection.
405
+ resp = self._post_binary(path, meta, pack_array(mask_image, filters=[blosc2.Filter.NOFILTER]))
399
406
  self._apply_prediction_response(resp)
400
407
 
401
408
  def add_initial_seg_interaction(
@@ -420,7 +427,8 @@ class nnInteractiveRemoteInferenceSession:
420
427
  "run_prediction": bool(run_prediction),
421
428
  "override_capability_checks": bool(override_capability_checks),
422
429
  }
423
- resp = self._post_binary(PATH_ADD_INITIAL_SEG, meta, pack_array(initial_seg))
430
+ # Segmentations compress best with NOFILTER; skip auto-selection.
431
+ resp = self._post_binary(PATH_ADD_INITIAL_SEG, meta, pack_array(initial_seg, filters=[blosc2.Filter.NOFILTER]))
424
432
  self._apply_prediction_response(resp)
425
433
 
426
434
  # ------------------------------------------------------------------ #
@@ -47,8 +47,64 @@ _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(raw: memoryview, total: int, codec: blosc2.Codec, clevel: int, filters: list) -> int:
55
+ """Compressed byte length of ``raw`` under ``filters``, chunked exactly as pack_array does."""
56
+ size = 0
57
+ nchunks = (total + _CHUNK_SIZE - 1) // _CHUNK_SIZE
58
+ for i in range(nchunks):
59
+ start = i * _CHUNK_SIZE
60
+ end = min(start + _CHUNK_SIZE, total)
61
+ size += len(blosc2.compress2(raw[start:end], codec=codec, clevel=clevel, filters=filters))
62
+ return size
63
+
64
+
65
+ def _select_filter(arr: np.ndarray, codec: blosc2.Codec, clevel: int) -> "blosc2.Filter":
66
+ """Pick NOFILTER vs SHUFFLE for ``arr`` by trial-compressing a small centered crop.
67
+
68
+ Uses ``compress2`` on the raw bytes — exactly the path pack_array takes — so the decision
69
+ is consistent with how the whole array is actually compressed. The crop is
70
+ ``_SELECT_FILTER_CROP_FRACTION`` of each axis (centered), keeping the trial cheap and
71
+ representative (lands on foreground). Ties go to NOFILTER; any failure falls back to it.
72
+ """
73
+ try:
74
+ crop_shape = [max(1, int(s * _SELECT_FILTER_CROP_FRACTION)) for s in arr.shape]
75
+ slices = tuple(slice((s - cs) // 2, (s - cs) // 2 + cs) for s, cs in zip(arr.shape, crop_shape))
76
+ crop = np.ascontiguousarray(arr[slices])
77
+ raw = memoryview(crop).cast("B")
78
+ total = raw.nbytes
79
+
80
+ best_filter, best_bytes = blosc2.Filter.NOFILTER, None
81
+ for f in (blosc2.Filter.NOFILTER, blosc2.Filter.SHUFFLE):
82
+ cb = _compress_all(raw, total, codec, clevel, [f])
83
+ if best_bytes is None or cb < best_bytes:
84
+ best_bytes, best_filter = cb, f
85
+ return best_filter
86
+ except Exception as e:
87
+ from warnings import warn
88
+
89
+ warn(f"_select_filter failed ({e!r}); falling back to NOFILTER.")
90
+ return blosc2.Filter.NOFILTER
91
+
92
+
93
+ def pack_array(
94
+ arr: np.ndarray,
95
+ codec: blosc2.Codec = blosc2.Codec.ZSTD,
96
+ clevel: int = 3,
97
+ filters: Optional[list] = None,
98
+ ) -> bytes:
99
+ """Serialize a numpy array to a self-describing compressed byte string.
100
+
101
+ ``filters`` is the blosc2 filter pipeline to apply. If ``None`` (the default), the
102
+ better of NOFILTER/SHUFFLE is auto-selected by trial-compressing a cheap, representative
103
+ slab — appropriate for images, whose optimum depends on the data. Callers that already
104
+ know the optimum (interactions and segmentations compress best with NOFILTER) should pass
105
+ ``[blosc2.Filter.NOFILTER]`` to skip the selection. The chosen filter is self-describing
106
+ inside the blosc2 frame, so unpack_array (decompress2) needs no changes.
107
+ """
52
108
  arr = np.ascontiguousarray(arr)
53
109
  dtype_str = arr.dtype.str.lstrip("<>|=").encode("ascii")
54
110
  if arr.dtype.byteorder not in ("=", "|", "<"):
@@ -77,11 +133,17 @@ def pack_array(arr: np.ndarray, codec: blosc2.Codec = blosc2.Codec.ZSTD, clevel:
77
133
  raw = memoryview(arr).cast("B")
78
134
  total = raw.nbytes
79
135
  nchunks = (total + _CHUNK_SIZE - 1) // _CHUNK_SIZE
136
+
137
+ if filters is None:
138
+ # Auto-select the better filter from a small centered crop, using the same
139
+ # compress2 path as below for consistency.
140
+ filters = [_select_filter(arr, codec, clevel)]
141
+
80
142
  parts = [header, shape_bytes, struct.pack("<I", nchunks)]
81
143
  for i in range(nchunks):
82
144
  start = i * _CHUNK_SIZE
83
145
  end = min(start + _CHUNK_SIZE, total)
84
- chunk = blosc2.compress2(raw[start:end], codec=codec, clevel=clevel)
146
+ chunk = blosc2.compress2(raw[start:end], codec=codec, clevel=clevel, filters=filters)
85
147
  parts.append(struct.pack("<QQ", end - start, len(chunk)))
86
148
  parts.append(chunk)
87
149
  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,52 @@ 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(sub, filters=[blosc2.Filter.NOFILTER]),
435
447
  media_type=CONTENT_TYPE_OCTET_STREAM,
436
448
  headers={META_HEADER: json.dumps(meta, separators=(",", ":"))},
437
449
  )
438
450
 
451
+ def _parse_target_buffer_request(payload: dict) -> tuple[tuple[int, ...], np.dtype]:
452
+ if "shape" not in payload:
453
+ raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="missing required field: shape")
454
+ if "dtype" not in payload:
455
+ raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="missing required field: dtype")
456
+
457
+ raw_shape = payload["shape"]
458
+ if not isinstance(raw_shape, list):
459
+ raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="shape must be a list of positive integers")
460
+ if len(raw_shape) != 3:
461
+ raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="shape must be 3D")
462
+
463
+ shape = []
464
+ for dim in raw_shape:
465
+ if not isinstance(dim, numbers.Integral) or isinstance(dim, bool):
466
+ raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="shape must contain only integers")
467
+ if dim <= 0:
468
+ raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="shape dimensions must be positive")
469
+ shape.append(dim)
470
+
471
+ try:
472
+ dtype = np.dtype(payload["dtype"])
473
+ except (TypeError, ValueError) as e:
474
+ raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=f"invalid dtype: {payload['dtype']!r}") from e
475
+ # 'b' = bool, 'i' = signed int, 'u' = unsigned int.
476
+ if dtype.kind not in ("b", "i", "u"):
477
+ raise HTTPException(
478
+ status.HTTP_400_BAD_REQUEST,
479
+ detail=f"unsupported dtype {dtype}: target buffer must be bool or an integer type",
480
+ )
481
+
482
+ nbytes = int(np.prod(shape, dtype=np.uint64)) * dtype.itemsize
483
+ if nbytes > MAX_TARGET_BUFFER_BYTES:
484
+ raise HTTPException(
485
+ status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
486
+ detail=(f"target buffer would require {nbytes} bytes, " f"limit is {MAX_TARGET_BUFFER_BYTES} bytes"),
487
+ )
488
+
489
+ return tuple(shape), dtype
490
+
439
491
  def _under_session_lock(entry: SessionEntry, fn):
440
492
  """Run ``fn(session)`` under the session's lock, converting known errors to HTTP 400.
441
493
 
@@ -530,8 +582,7 @@ def make_app(
530
582
 
531
583
  @app.post(PATH_SET_TARGET_BUFFER, dependencies=[auth])
532
584
  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"])
585
+ shape, dtype = _parse_target_buffer_request(payload)
535
586
  buf = np.zeros(shape, dtype=dtype)
536
587
 
537
588
  def _do(session):
@@ -659,4 +710,5 @@ def _build_capability_snapshot(session: nnInteractiveInferenceSession) -> dict:
659
710
  "patch_size": list(cfg.patch_size) if cfg is not None else None,
660
711
  "do_autozoom": bool(session.do_autozoom),
661
712
  "inference_session_version": session.INFERENCE_SESSION_VERSION,
713
+ "license": session.license,
662
714
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nnInteractive
3
- Version: 2.2.0
3
+ Version: 2.3.0
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.0"
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