slide2vec 5.9.2__tar.gz → 6.0.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 (169) hide show
  1. {slide2vec-5.9.2 → slide2vec-6.0.0}/PKG-INFO +38 -112
  2. slide2vec-6.0.0/README.md +90 -0
  3. {slide2vec-5.9.2 → slide2vec-6.0.0}/pyproject.toml +3 -3
  4. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/__init__.py +1 -1
  5. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/api.py +4 -4
  6. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/base.py +43 -21
  7. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/__init__.py +2 -0
  8. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/dinov2.py +8 -5
  9. slide2vec-6.0.0/slide2vec/encoders/models/dinov3.py +79 -0
  10. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/gigapath.py +9 -7
  11. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/gpfm.py +10 -1
  12. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/mstar.py +1 -1
  13. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/registry.py +17 -5
  14. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/inference.py +11 -1
  15. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/artifacts_collect.py +1 -0
  16. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/batching.py +11 -1
  17. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_encoder_input.py +0 -12
  18. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_regions.py +3 -2
  19. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/embedding.py +4 -1
  20. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/embedding_persist.py +3 -1
  21. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/encoder_input_contract.py +8 -2
  22. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/model_settings.py +2 -0
  23. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/patient_pipeline.py +3 -1
  24. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/persist_callbacks.py +99 -18
  25. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/pooled_encoder_input.py +9 -28
  26. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/types.py +3 -0
  27. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/PKG-INFO +38 -112
  28. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/SOURCES.txt +3 -0
  29. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/requires.txt +1 -1
  30. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_encoder_input.py +3 -3
  31. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_image_stage.py +3 -0
  32. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dinov2_natimage.py +3 -3
  33. slide2vec-6.0.0/tests/test_dinov3_natimage.py +188 -0
  34. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_encoder_capabilities.py +8 -9
  35. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_encoder_input_contract.py +50 -8
  36. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_encoder_plugins.py +4 -1
  37. slide2vec-6.0.0/tests/test_encoder_preprocessing.py +368 -0
  38. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_encoder_provider_failures.py +3 -0
  39. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_encoder_registry.py +73 -0
  40. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_patch_size_metadata.py +1 -0
  41. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_pooled_encoder_input.py +60 -44
  42. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_pooled_geometry.py +62 -24
  43. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_regression_core.py +1 -0
  44. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_regression_inference.py +240 -1
  45. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_regression_models.py +1 -1
  46. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_rudolfv2.py +2 -1
  47. slide2vec-5.9.2/README.md +0 -164
  48. {slide2vec-5.9.2 → slide2vec-6.0.0}/LICENSE +0 -0
  49. {slide2vec-5.9.2 → slide2vec-6.0.0}/setup.cfg +0 -0
  50. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/__main__.py +0 -0
  51. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/artifacts.py +0 -0
  52. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/cli.py +0 -0
  53. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/configs/__init__.py +0 -0
  54. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/configs/default.yaml +0 -0
  55. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/configs/resources.py +0 -0
  56. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/data/__init__.py +0 -0
  57. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/data/dataset.py +0 -0
  58. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/data/tile_reader.py +0 -0
  59. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/data/tile_store.py +0 -0
  60. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/__init__.py +0 -0
  61. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/dense_image_worker.py +0 -0
  62. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/dense_worker.py +0 -0
  63. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/direct_embed_worker.py +0 -0
  64. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/image_worker.py +0 -0
  65. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/pipeline_worker.py +0 -0
  66. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/worker_entry.py +0 -0
  67. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/__init__.py +0 -0
  68. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/conch.py +0 -0
  69. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/genbio.py +0 -0
  70. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/hibou.py +0 -0
  71. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/hoptimus.py +0 -0
  72. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/isight.py +0 -0
  73. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/lunit.py +0 -0
  74. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/midnight.py +0 -0
  75. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/__init__.py +0 -0
  76. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/blocks.py +0 -0
  77. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/case.py +0 -0
  78. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/loading.py +0 -0
  79. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/slide.py +0 -0
  80. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/types.py +0 -0
  81. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/musk.py +0 -0
  82. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/phikon.py +0 -0
  83. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/prism.py +0 -0
  84. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/prism2.py +0 -0
  85. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/prost40m.py +0 -0
  86. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/rudolfv2.py +0 -0
  87. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/titan.py +0 -0
  88. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/uni.py +0 -0
  89. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/virchow.py +0 -0
  90. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/waiv.py +0 -0
  91. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/validation.py +0 -0
  92. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/progress.py +0 -0
  93. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/__init__.py +0 -0
  94. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/cpu_budget.py +0 -0
  95. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_encode.py +0 -0
  96. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_image_reading.py +0 -0
  97. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_image_recipe.py +0 -0
  98. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_image_shard.py +0 -0
  99. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_image_stage.py +0 -0
  100. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_shard.py +0 -0
  101. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_sliding.py +0 -0
  102. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_stage.py +0 -0
  103. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/distributed.py +0 -0
  104. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/distributed_stage.py +0 -0
  105. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/effective_encoder_input.py +0 -0
  106. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/embedding_pipeline.py +0 -0
  107. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/hierarchical.py +0 -0
  108. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/image_shard.py +0 -0
  109. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/image_specs.py +0 -0
  110. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/image_stage.py +0 -0
  111. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/manifest.py +0 -0
  112. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/persistence.py +0 -0
  113. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/preprocessing.py +0 -0
  114. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/process_list.py +0 -0
  115. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/progress_bridge.py +0 -0
  116. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/registry.py +0 -0
  117. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/serialization.py +0 -0
  118. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/sharding.py +0 -0
  119. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/slide_encode.py +0 -0
  120. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/tiling.py +0 -0
  121. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/tiling_pipeline.py +0 -0
  122. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/worker_io.py +0 -0
  123. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/__init__.py +0 -0
  124. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/config.py +0 -0
  125. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/coordinates.py +0 -0
  126. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/log_utils.py +0 -0
  127. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/tiling_io.py +0 -0
  128. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/utils.py +0 -0
  129. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/dependency_links.txt +0 -0
  130. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/entry_points.txt +0 -0
  131. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/not-zip-safe +0 -0
  132. {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/top_level.txt +0 -0
  133. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_architecture_runtime_split.py +0 -0
  134. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_attention_extraction.py +0 -0
  135. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_benchmark_tooling.py +0 -0
  136. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_encode_kit.py +0 -0
  137. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_extraction.py +0 -0
  138. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_image_reading.py +0 -0
  139. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_image_resume.py +0 -0
  140. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_image_shard.py +0 -0
  141. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_regions.py +0 -0
  142. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_shard.py +0 -0
  143. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_sliding.py +0 -0
  144. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_source_spacing.py +0 -0
  145. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_stage.py +0 -0
  146. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_worker.py +0 -0
  147. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_gpfm_genbio_heavy.py +0 -0
  148. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_hs2p_package_cutover.py +0 -0
  149. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_image_shard.py +0 -0
  150. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_image_stage.py +0 -0
  151. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_inference_benchmark.py +0 -0
  152. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_isight.py +0 -0
  153. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_load_model_hf_auth.py +0 -0
  154. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_mascaret.py +0 -0
  155. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_on_slide_persisted.py +0 -0
  156. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_output_consistency.py +0 -0
  157. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_patient_manifest.py +0 -0
  158. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_phaet.py +0 -0
  159. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_prism.py +0 -0
  160. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_prism2.py +0 -0
  161. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_process_list_performance.py +0 -0
  162. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_progress.py +0 -0
  163. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_runtime_batching.py +0 -0
  164. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_sharding.py +0 -0
  165. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_slide_coordinate_preparation.py +0 -0
  166. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_soma_migration.py +0 -0
  167. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_tile_store.py +0 -0
  168. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_tiling_pipeline.py +0 -0
  169. {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_titan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slide2vec
3
- Version: 5.9.2
3
+ Version: 6.0.0
4
4
  Summary: Embedding of whole slide images with Foundation Models
5
5
  Author-email: Clément Grisi <clement.grisi@radboudumc.nl>
6
6
  License-Expression: Apache-2.0
@@ -80,7 +80,7 @@ Requires-Dist: wandb; extra == "fm"
80
80
  Requires-Dist: torch<2.8,>=2.3; extra == "fm"
81
81
  Requires-Dist: torchvision>=0.18.0; extra == "fm"
82
82
  Requires-Dist: einops>=0.8.0; extra == "fm"
83
- Requires-Dist: timm>=1.0.3; extra == "fm"
83
+ Requires-Dist: timm>=1.0.20; extra == "fm"
84
84
  Requires-Dist: huggingface_hub<1.0,>=0.30.0; extra == "fm"
85
85
  Requires-Dist: environs; extra == "fm"
86
86
  Requires-Dist: einops-exts>=0.0.4; extra == "fm"
@@ -119,162 +119,88 @@ Dynamic: license-file
119
119
  [![PyPI version](https://img.shields.io/pypi/v/slide2vec?label=pypi&logo=pypi&color=3776AB)](https://pypi.org/project/slide2vec/)
120
120
  [![Docs](https://img.shields.io/badge/docs-website-blue)](https://clemsgrs.github.io/slide2vec/)
121
121
 
122
- `slide2vec` is a Python package for efficient encoding of whole-slide images using publicly available foundation models. It builds on [`hs2p`](https://pypi.org/project/hs2p/) for fast preprocessing and exposes a focused surface around `Model`, `Pipeline`, and `ExecutionOptions`.
122
+ `slide2vec` encodes whole-slide images with publicly available pathology foundation models. It uses [`hs2p`](https://pypi.org/project/hs2p/) for tissue detection and tiling, and handles batching, multi-GPU execution, and embedding storage.
123
123
 
124
- Documentation site: [https://clemsgrs.github.io/slide2vec/](https://clemsgrs.github.io/slide2vec/)
124
+ ## Install
125
125
 
126
- ## Installation
126
+ Python 3.10 or newer is required:
127
127
 
128
128
  ```shell
129
129
  pip install slide2vec
130
- pip install "slide2vec[fm]"
131
130
  ```
132
131
 
133
- `slide2vec` keeps the base install focused on the core package surface. Use `slide2vec[fm]` when you want the PyPI-hosted FM dependencies.
132
+ Many models need additional dependencies available through `pip install "slide2vec[fm]"`. See the [model installation guide](https://clemsgrs.github.io/slide2vec/models.html#model-installation) for model-specific extras, separate environments, and upstream packages.
134
133
 
135
- Some model backends still rely on upstream Git repositories that PyPI will not accept as package metadata. Install those separately when needed:
134
+ For gated models such as Virchow2, request access on the model's Hugging Face page and authenticate with `hf auth login` or an `HF_TOKEN` environment variable.
136
135
 
137
- ```shell
138
- pip install git+https://github.com/lilab-stanford/MUSK.git
139
- pip install git+https://github.com/Mahmoodlab/CONCH.git
140
- pip install git+https://github.com/prov-gigapath/prov-gigapath.git
141
- ```
142
-
143
- AtlasPatch-backed tissue segmentation is available through hs2p's `sam2` path in the bundled install.
144
-
145
- Waiv encoders use a separately tested Transformers 5 runtime. Install them in
146
- their own environment with `pip install "slide2vec[waiv]"`; the `waiv` extra is
147
- incompatible with the existing `fm`, `prism`, and `titan` dependency pins.
148
-
149
- ## Python API
136
+ ## Embed a slide
150
137
 
151
138
  ```python
152
- from slide2vec import Model
153
- from slide2vec.utils.config import hf_login
154
-
155
- hf_login()
139
+ from slide2vec import Model, PreprocessingConfig
156
140
 
157
141
  model = Model.from_preset("virchow2")
158
- embedded = model.embed_slide("/path/to/slide.svs")
142
+ preprocessing = PreprocessingConfig(requested_spacing_um=0.5)
143
+ embedded = model.embed_slide("/path/to/slide.svs", preprocessing=preprocessing)
159
144
 
160
- tile_embeddings = embedded.tile_embeddings
161
- x = embedded.x
162
- y = embedded.y
145
+ tile_embeddings = embedded.tile_embeddings # (N, 2560)
146
+ x, y = embedded.x, embedded.y # level-0 tile coordinates
163
147
  ```
164
148
 
165
- Use `list_models()` when you want to inspect the shipped presets programmatically:
149
+ The preset supplies tile size and precision defaults. Declare spacing explicitly for models such as Virchow2 that support several scales. Use `list_models()` to list presets, or filter with `list_models("tile")`, `list_models("slide")`, or `list_models("patient")`.
166
150
 
167
- ```python
168
- from slide2vec import list_models
151
+ See [getting started](https://clemsgrs.github.io/slide2vec/getting-started.html) for preprocessing and execution settings, and the [API guide](https://clemsgrs.github.io/slide2vec/api.html) for patient embeddings, image inputs, and dense grids.
152
+
153
+ ## Save a batch
169
154
 
170
- all_models = list_models()
171
- tile_models = list_models("tile")
172
- slide_models = list_models("slide")
173
- patient_models = list_models("patient")
155
+ Create a CSV manifest:
156
+
157
+ ```csv
158
+ sample_id,image_path
159
+ slide-1,/data/slide-1.svs
160
+ slide-2,/data/slide-2.svs
174
161
  ```
175
162
 
176
- Use `Pipeline(...)` for manifest-driven batch processing when you want artifacts written to disk instead of only in-memory outputs:
163
+ Optional `mask_path` and `spacing_at_level_0` columns supply a mask or correct missing or incorrect level-0 spacing. Patient-level models also require `patient_id`; see the [manifest schema](https://clemsgrs.github.io/slide2vec/manifest.html).
177
164
 
178
165
  ```python
179
- from slide2vec import ExecutionOptions, Pipeline, PreprocessingConfig
166
+ from slide2vec import ExecutionOptions, Model, Pipeline, PreprocessingConfig
180
167
 
181
168
  pipeline = Pipeline(
182
- model=model,
183
- preprocessing=PreprocessingConfig(
184
- requested_spacing_um=0.5,
185
- requested_tile_size_px=224,
186
- masks={"min_coverage": {"tissue": 0.1}},
187
- ),
188
- execution=ExecutionOptions(output_dir="outputs/demo"),
169
+ model=Model.from_preset("virchow2"),
170
+ preprocessing=PreprocessingConfig(requested_spacing_um=0.5),
171
+ execution=ExecutionOptions(output_dir="outputs/run"),
189
172
  )
190
173
  result = pipeline.run(manifest_path="/path/to/slides.csv")
191
174
  ```
192
175
 
193
- By default, `ExecutionOptions()` uses all available GPUs. Set `ExecutionOptions(num_gpus=4)` when you want to cap the sharding explicitly.
194
-
195
- ### Hierarchical Feature Extraction
196
-
197
- Tile embeddings can be spatially grouped into regions for downstream models that consume region-level structure. Enable it by setting `region_tile_multiple` on `PreprocessingConfig`:
198
-
199
- ```python
200
- preprocessing = PreprocessingConfig(
201
- requested_spacing_um=0.5,
202
- requested_tile_size_px=224,
203
- region_tile_multiple=6, # 6x6 tiles per region
204
- )
205
- embedded = model.embed_slide("/path/to/slide.svs", preprocessing=preprocessing)
206
- ```
207
-
208
- Hierarchical outputs have shape `(num_regions, tiles_per_region, feature_dim)` and are written to `hierarchical_embeddings/` when persisted.
209
-
210
- See the [hierarchical features guide](https://clemsgrs.github.io/slide2vec/hierarchical.html) for details.
211
-
212
- ### Input Manifest
213
-
214
- Manifest-driven runs use the schema below. `mask_path` and `spacing_at_level_0` are optional.
215
-
216
- ```csv
217
- sample_id,image_path,mask_path,spacing_at_level_0
218
- slide-1,/path/to/slide-1.svs,/path/to/mask-1.png,0.25
219
- slide-2,/path/to/slide-2.svs,,
220
- ...
221
- ```
222
-
223
- Use `spacing_at_level_0` when the slide file reports a missing or incorrect level-0 spacing and you want to override it.
224
-
176
+ Runs use all available GPUs by default; set `ExecutionOptions(num_gpus=2)` to limit them. Embeddings are saved as `.pt` tensors with metadata sidecars. Use `ExecutionOptions(output_format="npz")` for NumPy archives. The [output guide](https://clemsgrs.github.io/slide2vec/output-layout.html) describes directories, shapes, coordinates, and progress records.
225
177
 
226
- ### Outputs
178
+ Add `region_tile_multiple=6` to the preprocessing config to group tiles into 6×6 regions. These produce `(num_regions, 36, feature_dim)` tensors in `hierarchical_embeddings/`; see [hierarchical features](https://clemsgrs.github.io/slide2vec/hierarchical.html).
227
179
 
228
- The package writes explicit artifact directories:
229
-
230
- - `tile_embeddings/<sample_id>.pt` or `.npz`
231
- - `tile_embeddings/<sample_id>.meta.json`
232
- - `hierarchical_embeddings/<sample_id>.pt` or `.npz` (when `region_tile_multiple` is set)
233
- - `hierarchical_embeddings/<sample_id>.meta.json`
234
- - `slide_embeddings/<sample_id>.pt` or `.npz`
235
- - `slide_embeddings/<sample_id>.meta.json`
236
- - optional `slide_latents/<sample_id>.pt` or `.npz`
237
-
238
- `.pt` remains the default format. `.npz` is available through `ExecutionOptions(output_format="npz")`.
239
-
240
- ### Supported Models
241
-
242
- `slide2vec` currently ships presets for 28 tile-level models, 4 slide-level models,
243
- and 1 patient-level model.
244
- For the full catalog and preset names, see the [model zoo](https://clemsgrs.github.io/slide2vec/models.html).
245
-
246
- ## CLI
247
-
248
- The CLI is a thin wrapper over the package API.
249
- Bundled configs live under `slide2vec/configs/preprocessing/` and `slide2vec/configs/models/`.
180
+ The same batch workflow is available from the terminal:
250
181
 
251
182
  ```shell
252
183
  slide2vec /path/to/config.yaml
253
184
  ```
254
185
 
255
- By default, manifest-driven CLI runs use all available GPUs. Set `speed.num_gpus=4` when you want to cap the sharding explicitly.
256
-
257
- New to the CLI or doing batch runs to disk? Start with the [CLI guide](https://clemsgrs.github.io/slide2vec/cli.html) for the config-driven workflow and common run patterns.
186
+ The [CLI guide](https://clemsgrs.github.io/slide2vec/cli.html) provides a complete config example, overrides, and resume instructions.
258
187
 
259
188
  ## Docker
260
189
 
261
190
  [![Docker Version](https://img.shields.io/docker/v/waticlems/slide2vec?sort=semver&label=docker&logo=docker&color=2496ED)](https://hub.docker.com/r/waticlems/slide2vec)
262
191
 
263
- Docker remains available when you prefer a containerized runtime:
264
-
265
192
  ```shell
266
193
  docker pull waticlems/slide2vec:latest
267
194
  docker run --rm -it \
268
195
  -v /path/to/your/data:/data \
269
- -e HF_TOKEN=<your-huggingface-api-token> \
196
+ -e HF_TOKEN \
270
197
  waticlems/slide2vec:latest
271
198
  ```
272
199
 
273
- ## Documentation
200
+ Set `HF_TOKEN` in your shell before starting the container.
201
+
202
+ ## More documentation
274
203
 
275
- - [Documentation website](https://clemsgrs.github.io/slide2vec/)
276
- - [API guide](https://clemsgrs.github.io/slide2vec/api.html)
277
- - [CLI guide](https://clemsgrs.github.io/slide2vec/cli.html)
278
204
  - [Model zoo](https://clemsgrs.github.io/slide2vec/models.html)
279
- - [Performance benchmarks and QA](docs/performance.md)
280
- - [`tutorials/api_walkthrough.ipynb`](tutorials/api_walkthrough.ipynb) for a notebook walkthrough of the API
205
+ - [Performance benchmarks and QA](https://clemsgrs.github.io/slide2vec/performance.html)
206
+ - [API walkthrough notebook](tutorials/api_walkthrough.ipynb)
@@ -0,0 +1,90 @@
1
+ # slide2vec
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/slide2vec?label=pypi&logo=pypi&color=3776AB)](https://pypi.org/project/slide2vec/)
4
+ [![Docs](https://img.shields.io/badge/docs-website-blue)](https://clemsgrs.github.io/slide2vec/)
5
+
6
+ `slide2vec` encodes whole-slide images with publicly available pathology foundation models. It uses [`hs2p`](https://pypi.org/project/hs2p/) for tissue detection and tiling, and handles batching, multi-GPU execution, and embedding storage.
7
+
8
+ ## Install
9
+
10
+ Python 3.10 or newer is required:
11
+
12
+ ```shell
13
+ pip install slide2vec
14
+ ```
15
+
16
+ Many models need additional dependencies available through `pip install "slide2vec[fm]"`. See the [model installation guide](https://clemsgrs.github.io/slide2vec/models.html#model-installation) for model-specific extras, separate environments, and upstream packages.
17
+
18
+ For gated models such as Virchow2, request access on the model's Hugging Face page and authenticate with `hf auth login` or an `HF_TOKEN` environment variable.
19
+
20
+ ## Embed a slide
21
+
22
+ ```python
23
+ from slide2vec import Model, PreprocessingConfig
24
+
25
+ model = Model.from_preset("virchow2")
26
+ preprocessing = PreprocessingConfig(requested_spacing_um=0.5)
27
+ embedded = model.embed_slide("/path/to/slide.svs", preprocessing=preprocessing)
28
+
29
+ tile_embeddings = embedded.tile_embeddings # (N, 2560)
30
+ x, y = embedded.x, embedded.y # level-0 tile coordinates
31
+ ```
32
+
33
+ The preset supplies tile size and precision defaults. Declare spacing explicitly for models such as Virchow2 that support several scales. Use `list_models()` to list presets, or filter with `list_models("tile")`, `list_models("slide")`, or `list_models("patient")`.
34
+
35
+ See [getting started](https://clemsgrs.github.io/slide2vec/getting-started.html) for preprocessing and execution settings, and the [API guide](https://clemsgrs.github.io/slide2vec/api.html) for patient embeddings, image inputs, and dense grids.
36
+
37
+ ## Save a batch
38
+
39
+ Create a CSV manifest:
40
+
41
+ ```csv
42
+ sample_id,image_path
43
+ slide-1,/data/slide-1.svs
44
+ slide-2,/data/slide-2.svs
45
+ ```
46
+
47
+ Optional `mask_path` and `spacing_at_level_0` columns supply a mask or correct missing or incorrect level-0 spacing. Patient-level models also require `patient_id`; see the [manifest schema](https://clemsgrs.github.io/slide2vec/manifest.html).
48
+
49
+ ```python
50
+ from slide2vec import ExecutionOptions, Model, Pipeline, PreprocessingConfig
51
+
52
+ pipeline = Pipeline(
53
+ model=Model.from_preset("virchow2"),
54
+ preprocessing=PreprocessingConfig(requested_spacing_um=0.5),
55
+ execution=ExecutionOptions(output_dir="outputs/run"),
56
+ )
57
+ result = pipeline.run(manifest_path="/path/to/slides.csv")
58
+ ```
59
+
60
+ Runs use all available GPUs by default; set `ExecutionOptions(num_gpus=2)` to limit them. Embeddings are saved as `.pt` tensors with metadata sidecars. Use `ExecutionOptions(output_format="npz")` for NumPy archives. The [output guide](https://clemsgrs.github.io/slide2vec/output-layout.html) describes directories, shapes, coordinates, and progress records.
61
+
62
+ Add `region_tile_multiple=6` to the preprocessing config to group tiles into 6×6 regions. These produce `(num_regions, 36, feature_dim)` tensors in `hierarchical_embeddings/`; see [hierarchical features](https://clemsgrs.github.io/slide2vec/hierarchical.html).
63
+
64
+ The same batch workflow is available from the terminal:
65
+
66
+ ```shell
67
+ slide2vec /path/to/config.yaml
68
+ ```
69
+
70
+ The [CLI guide](https://clemsgrs.github.io/slide2vec/cli.html) provides a complete config example, overrides, and resume instructions.
71
+
72
+ ## Docker
73
+
74
+ [![Docker Version](https://img.shields.io/docker/v/waticlems/slide2vec?sort=semver&label=docker&logo=docker&color=2496ED)](https://hub.docker.com/r/waticlems/slide2vec)
75
+
76
+ ```shell
77
+ docker pull waticlems/slide2vec:latest
78
+ docker run --rm -it \
79
+ -v /path/to/your/data:/data \
80
+ -e HF_TOKEN \
81
+ waticlems/slide2vec:latest
82
+ ```
83
+
84
+ Set `HF_TOKEN` in your shell before starting the container.
85
+
86
+ ## More documentation
87
+
88
+ - [Model zoo](https://clemsgrs.github.io/slide2vec/models.html)
89
+ - [Performance benchmarks and QA](https://clemsgrs.github.io/slide2vec/performance.html)
90
+ - [API walkthrough notebook](tutorials/api_walkthrough.ipynb)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "slide2vec"
7
- version = "5.9.2"
7
+ version = "6.0.0"
8
8
  description = "Embedding of whole slide images with Foundation Models"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -105,7 +105,7 @@ fm = [
105
105
  "torch>=2.3,<2.8",
106
106
  "torchvision>=0.18.0",
107
107
  "einops>=0.8.0",
108
- "timm>=1.0.3",
108
+ "timm>=1.0.20",
109
109
  "huggingface_hub>=0.30.0,<1.0",
110
110
  "environs",
111
111
  "einops-exts>=0.0.4",
@@ -180,7 +180,7 @@ no_implicit_reexport = true
180
180
  max-line-length = 160
181
181
 
182
182
  [tool.bumpver]
183
- current_version = "5.9.2"
183
+ current_version = "6.0.0"
184
184
  version_pattern = "MAJOR.MINOR.PATCH"
185
185
  commit = false
186
186
  tag = false
@@ -27,7 +27,7 @@ from slide2vec.encoders import (
27
27
  )
28
28
 
29
29
 
30
- __version__ = "5.9.2"
30
+ __version__ = "6.0.0"
31
31
 
32
32
  __all__ = [
33
33
  "Model",
@@ -1010,14 +1010,14 @@ class Model:
1010
1010
  )
1011
1011
  self._encoder_input = contract
1012
1012
  plan = contract.plan
1013
- if emit_run_info and plan.requires_variable_model_input:
1013
+ if emit_run_info:
1014
1014
  logging.getLogger("slide2vec").info(
1015
- "Pooled encoder input for '%s': preset %dpx, requested %dpx, "
1016
- "exact encoder input %dpx; using normalization-only preprocessing.",
1015
+ "Pooled encoder input for '%s': preset %dpx, requested %dpx; encoding "
1016
+ "exactly %dpx with geometry-preserving preprocessing.",
1017
1017
  self.name,
1018
1018
  plan.preset_input_size_px,
1019
1019
  plan.requested_tile_size_px,
1020
- plan.expected_encoder_input_size_px,
1020
+ plan.requested_tile_size_px,
1021
1021
  )
1022
1022
  return contract
1023
1023
 
@@ -7,6 +7,7 @@ from typing import Callable
7
7
  import timm
8
8
  import torch
9
9
  from timm.data import create_transform, resolve_data_config
10
+ from timm.layers import apply_rot_embed_cat
10
11
  from torch import Tensor
11
12
  from torchvision.transforms import v2
12
13
 
@@ -192,7 +193,7 @@ def prefix_attention_to_grid(
192
193
  return maps.reshape(batch_size, num_query * num_heads, grid_h, grid_w)
193
194
 
194
195
 
195
- def timm_self_attention_weights(attn_module, x: Tensor) -> Tensor:
196
+ def timm_self_attention_weights(attn_module, x: Tensor, *, rope: Tensor | None = None) -> Tensor:
196
197
  """Recompute a timm ``Attention`` block's softmax weights ``(B, nh, N, N)``.
197
198
 
198
199
  timm's attention runs a *fused* SDPA kernel by default, which never
@@ -202,12 +203,23 @@ def timm_self_attention_weights(attn_module, x: Tensor) -> Tensor:
202
203
  / ``num_heads`` / ``head_dim`` / ``scale`` — i.e. exactly the non-fused branch of
203
204
  ``Attention.forward``, so the result is bit-equivalent to the weights the fused
204
205
  kernel applies internally. Dropout is omitted (extraction runs under ``eval``).
206
+
207
+ ``rope`` is the rotary embedding an ``EvaAttention`` block (DINOv3) receives
208
+ as its ``rope`` forward argument; when given, it is applied to the q/k rows
209
+ from ``attn_module.num_prefix_tokens`` onward (spatial tokens only, CLS and
210
+ registers untouched), exactly as ``EvaAttention.forward`` does. ``None`` for
211
+ plain ``Attention`` blocks leaves the recompute unchanged.
205
212
  """
206
- if not hasattr(attn_module, "qkv"):
213
+ if getattr(attn_module, "qkv", None) is None:
207
214
  raise NotImplementedError(
208
215
  f"{type(attn_module).__name__} has no fused 'qkv' projection; attention "
209
216
  "extraction currently supports timm ViT Attention blocks only."
210
217
  )
218
+ if getattr(attn_module, "q_bias", None) is not None:
219
+ raise NotImplementedError(
220
+ f"{type(attn_module).__name__} uses separate q/k/v biases; attention "
221
+ "extraction supports fused-qkv blocks only."
222
+ )
211
223
  batch_size, num_tokens, _ = x.shape
212
224
  num_heads = int(attn_module.num_heads)
213
225
  head_dim = int(getattr(attn_module, "head_dim", x.shape[-1] // num_heads))
@@ -220,6 +232,11 @@ def timm_self_attention_weights(attn_module, x: Tensor) -> Tensor:
220
232
  # q_norm / k_norm are Identity unless the model uses QK-norm; apply them either way.
221
233
  q = attn_module.q_norm(q)
222
234
  k = attn_module.k_norm(k)
235
+ if rope is not None:
236
+ npt = int(attn_module.num_prefix_tokens)
237
+ half = getattr(attn_module, "rotate_half", False)
238
+ q = torch.cat([q[:, :, :npt], apply_rot_embed_cat(q[:, :, npt:], rope, half=half)], dim=2).type_as(x)
239
+ k = torch.cat([k[:, :, :npt], apply_rot_embed_cat(k[:, :, npt:], rope, half=half)], dim=2).type_as(x)
223
240
  q = q * attn_module.scale
224
241
  attn = q @ k.transpose(-2, -1)
225
242
  return attn.softmax(dim=-1)
@@ -291,16 +308,22 @@ def timm_trunk_attention(
291
308
  resolved = resolve_block_indices(blocks, len(block_list), encoder_name=encoder_name)
292
309
 
293
310
  captured: dict[int, Tensor] = {}
311
+ captured_rope: dict[int, Tensor | None] = {}
294
312
 
295
313
  def _make_hook(index: int):
296
- def _hook(_module, inputs):
297
- captured[index] = inputs[0]
314
+ def _hook(_module, args, kwargs):
315
+ captured[index] = args[0]
316
+ # EvaAttention (RoPE models) receives the rotary embedding as its
317
+ # second forward argument; plain Attention blocks receive none.
318
+ captured_rope[index] = kwargs.get("rope", args[1] if len(args) > 1 else None)
298
319
 
299
320
  return _hook
300
321
 
301
322
  handles = []
302
323
  for index in sorted(set(resolved)):
303
- handles.append(block_list[index].attn.register_forward_pre_hook(_make_hook(index)))
324
+ handles.append(
325
+ block_list[index].attn.register_forward_pre_hook(_make_hook(index), with_kwargs=True)
326
+ )
304
327
  try:
305
328
  trunk.forward_features(batch)
306
329
  finally:
@@ -311,7 +334,9 @@ def timm_trunk_attention(
311
334
  num_prefix = int(getattr(trunk, "num_prefix_tokens", 1))
312
335
  grids = []
313
336
  for index in resolved:
314
- attn_weights = timm_self_attention_weights(block_list[index].attn, captured[index])
337
+ attn_weights = timm_self_attention_weights(
338
+ block_list[index].attn, captured[index], rope=captured_rope[index]
339
+ )
315
340
  grids.append(
316
341
  prefix_attention_to_grid(
317
342
  attn_weights,
@@ -463,26 +488,23 @@ class TileEncoder(Encoder):
463
488
  )
464
489
 
465
490
  def get_normalization_transform(self) -> Callable:
466
- """Geometry-preserving photometric transform.
467
-
468
- Returns a transform that applies ONLY this encoder's normalization
469
- (per-channel mean/std) — **no Resize, no CenterCrop** so callers can
470
- preserve the full input geometry. This deliberately differs from
471
- ``get_transform`` (the shipped pooled recipe):
472
- some encoders resize-then-center-crop there (GigaPath ``Resize(256) ->
473
- CenterCrop(224)``; Lunit ``crop_pct=0.9 -> Resize(248) -> CenterCrop(224)``),
474
- which drops the tile margins. Geometry policy remains the caller's
475
- responsibility. Default: unsupported, mirroring ``encode_tiles_dense``.
491
+ """Geometry-preserving photometric transform. Required for every tile encoder.
492
+
493
+ Returns a transform that applies ONLY this encoder's photometrics — dtype
494
+ conversion, intensity scaling, channel handling and per-channel mean/std
495
+ normalization with **no Resize and no CenterCrop**. Every *declared* run
496
+ (pooled slide tiles and dense ROIs) encodes exactly the geometry it requested
497
+ through this transform; ``get_transform`` (the shipped recipe, which may
498
+ resize-then-center-crop) is applied only to *given* pre-cropped images.
499
+ ``register_encoder`` rejects a tile encoder that does not override it.
476
500
  """
477
501
  raise NotImplementedError(
478
502
  f"{type(self).__name__} does not provide a normalization transform. "
479
- "The encoder cannot preserve caller-requested image geometry."
503
+ "Override get_normalization_transform with this encoder's photometric-only "
504
+ "preprocessing (dtype, scaling, normalization; no Resize/CenterCrop) so "
505
+ "declared runs can encode exactly the requested tile geometry."
480
506
  )
481
507
 
482
- def get_dense_transform(self) -> Callable:
483
- """Backward-compatible alias for the shared normalization primitive."""
484
- return self.get_normalization_transform()
485
-
486
508
 
487
509
  class SlideEncoder(Encoder):
488
510
  """Base class for encoders that pool tile features into slide features."""
@@ -6,6 +6,7 @@ Importing this package registers all encoders in the encoder_registry.
6
6
  from . import (
7
7
  conch,
8
8
  dinov2,
9
+ dinov3,
9
10
  genbio,
10
11
  gigapath,
11
12
  gpfm,
@@ -31,6 +32,7 @@ from . import (
31
32
  __all__ = [
32
33
  "conch",
33
34
  "dinov2",
35
+ "dinov3",
34
36
  "genbio",
35
37
  "gigapath",
36
38
  "gpfm",
@@ -26,10 +26,13 @@ and :func:`validate_encoder_config` never rejects a requested spacing for it
26
26
  (unlike the pathology encoders, which are validated at a specific spacing). It
27
27
  still needs *a* spacing to tile a slide, so ``default_spacing_um=0.5`` sets the
28
28
  tiling default: 0.5 µm/px is the task-spacing the pathology tile encoders
29
- declare, so selecting this encoder by name lands on identical tile geometry and
30
- it drops in as a matched control. Because it is agnostic, sweeping other
31
- task-spacings (e.g. 0.25) needs no ``allow_non_recommended_settings`` escape
32
- hatch any requested spacing is accepted as-is.
29
+ declare. Declared pooled runs read and encode the requested tile size with
30
+ normalization only: 518px by default, or 224px for matched-resolution
31
+ experiments when requested explicitly with ``allow_non_recommended_settings=True``.
32
+ Given pre-cropped tiles still use the shipped Resize 518 -> CenterCrop 518
33
+ transform. Because it is agnostic, sweeping other task-spacings (e.g. 0.25)
34
+ needs no ``allow_non_recommended_settings`` escape hatch — any requested spacing
35
+ is accepted as-is.
33
36
  """
34
37
 
35
38
  from slide2vec.encoders.base import TimmTileEncoder
@@ -40,7 +43,7 @@ from slide2vec.encoders.registry import register_encoder
40
43
  "dinov2-vitb14",
41
44
  output_variants={"default": {"encode_dim": 768}},
42
45
  default_output_variant="default",
43
- input_size=224,
46
+ input_size=518,
44
47
  supports_variable_input_size=True,
45
48
  patch_size=14,
46
49
  supported_spacing_um=None, # spacing-agnostic: no intrinsic µm/px, so no validation constraint
@@ -0,0 +1,79 @@
1
+ """Natural-image DINOv3 ViT-B/16 encoder.
2
+
3
+ ``dinov3-vitb16`` is the DINOv3 ViT-B/16 (Siméoni et al., 2025) distilled on
4
+ LVD-1689M natural images, shipped by ``timm`` as
5
+ ``vit_base_patch16_dinov3.lvd1689m`` (weights on Hugging Face under
6
+ ``timm/vit_base_patch16_dinov3.lvd1689m``; DINOv3 license). It complements the
7
+ ``dinov2-vitb14`` natural-image baseline but is not architecture-identical:
8
+ 16px patches, four register tokens and rotary positional embeddings (RoPE)
9
+ instead of 14px patches and learned positional embeddings.
10
+
11
+ The timm backbone is an ``Eva`` model whose checkpoint defaults to
12
+ ``global_pool="avg"``: the pooled output is the mean of the **spatial patch
13
+ tokens** after the final LayerNorm, excluding the CLS token and the four
14
+ registers. That shipped output is the default ``patch_mean`` variant; ``cls``
15
+ exposes the CLS token after the same final norm. Both are 768-d.
16
+
17
+ Spacing-agnostic like ``dinov2-vitb14`` (``supported_spacing_um=None``,
18
+ ``default_spacing_um=0.5``). Declared pooled runs encode exactly the requested
19
+ tile size with normalization only: 256px by default, or 224px with
20
+ ``allow_non_recommended_settings=True``. Given pre-cropped tiles use the shipped
21
+ Resize 256 -> CenterCrop 256 recipe.
22
+ """
23
+
24
+ import timm
25
+ from packaging.version import Version
26
+ from torch import Tensor
27
+
28
+ from slide2vec.encoders.base import TimmTileEncoder, resolve_requested_output_variant
29
+ from slide2vec.encoders.registry import register_encoder
30
+
31
+ _POOL_TYPES = {"patch_mean": "avg", "cls": "token"}
32
+ _MIN_TIMM_VERSION = "1.0.20" # first release registering vit_base_patch16_dinov3
33
+
34
+
35
+ def require_timm_for_dinov3() -> None:
36
+ if Version(timm.__version__) < Version(_MIN_TIMM_VERSION):
37
+ raise ImportError(
38
+ f"dinov3-vitb16 requires timm>={_MIN_TIMM_VERSION} (the DINOv3 "
39
+ f"architectures were added in that release); found timm=={timm.__version__}. "
40
+ "Note that the 'titan' extra pins timm==1.0.3 and cannot be installed "
41
+ "alongside dinov3-vitb16."
42
+ )
43
+
44
+
45
+ @register_encoder(
46
+ "dinov3-vitb16",
47
+ output_variants={
48
+ "patch_mean": {"encode_dim": 768},
49
+ "cls": {"encode_dim": 768},
50
+ },
51
+ default_output_variant="patch_mean",
52
+ input_size=256,
53
+ supports_variable_input_size=True,
54
+ patch_size=16,
55
+ supported_spacing_um=None, # spacing-agnostic: no intrinsic µm/px
56
+ default_spacing_um=0.5, # tiling default: match the pathology encoders' task-spacing
57
+ precision="fp16",
58
+ source="timm/vit_base_patch16_dinov3.lvd1689m",
59
+ )
60
+ class DINOv3ViTB16(TimmTileEncoder):
61
+ def __init__(self, *, output_variant: str | None = None):
62
+ require_timm_for_dinov3()
63
+ self._output_variant = resolve_requested_output_variant(
64
+ output_variant, default="patch_mean", allowed=tuple(_POOL_TYPES)
65
+ )
66
+ super().__init__(
67
+ "vit_base_patch16_dinov3.lvd1689m",
68
+ output_variant=self._output_variant,
69
+ dynamic_img_size=True,
70
+ )
71
+
72
+ def encode_tiles(self, batch: Tensor) -> Tensor:
73
+ # forward_features applies the final LayerNorm to every token; Eva.pool
74
+ # then reduces with the backbone's own num_prefix_tokens (CLS + 4 reg):
75
+ # "avg" = mean over spatial tokens only, "token" = CLS. fc_norm is
76
+ # Identity for this checkpoint (kept for parity with forward_head).
77
+ tokens = self._model.forward_features(batch)
78
+ pooled = self._model.pool(tokens, pool_type=_POOL_TYPES[self._output_variant])
79
+ return self._model.fc_norm(pooled)