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.
- {slide2vec-5.9.2 → slide2vec-6.0.0}/PKG-INFO +38 -112
- slide2vec-6.0.0/README.md +90 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/pyproject.toml +3 -3
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/__init__.py +1 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/api.py +4 -4
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/base.py +43 -21
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/__init__.py +2 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/dinov2.py +8 -5
- slide2vec-6.0.0/slide2vec/encoders/models/dinov3.py +79 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/gigapath.py +9 -7
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/gpfm.py +10 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/mstar.py +1 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/registry.py +17 -5
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/inference.py +11 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/artifacts_collect.py +1 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/batching.py +11 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_encoder_input.py +0 -12
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_regions.py +3 -2
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/embedding.py +4 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/embedding_persist.py +3 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/encoder_input_contract.py +8 -2
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/model_settings.py +2 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/patient_pipeline.py +3 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/persist_callbacks.py +99 -18
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/pooled_encoder_input.py +9 -28
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/types.py +3 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/PKG-INFO +38 -112
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/SOURCES.txt +3 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/requires.txt +1 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_encoder_input.py +3 -3
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_image_stage.py +3 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dinov2_natimage.py +3 -3
- slide2vec-6.0.0/tests/test_dinov3_natimage.py +188 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_encoder_capabilities.py +8 -9
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_encoder_input_contract.py +50 -8
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_encoder_plugins.py +4 -1
- slide2vec-6.0.0/tests/test_encoder_preprocessing.py +368 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_encoder_provider_failures.py +3 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_encoder_registry.py +73 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_patch_size_metadata.py +1 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_pooled_encoder_input.py +60 -44
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_pooled_geometry.py +62 -24
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_regression_core.py +1 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_regression_inference.py +240 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_regression_models.py +1 -1
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_rudolfv2.py +2 -1
- slide2vec-5.9.2/README.md +0 -164
- {slide2vec-5.9.2 → slide2vec-6.0.0}/LICENSE +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/setup.cfg +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/__main__.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/artifacts.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/cli.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/configs/__init__.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/configs/default.yaml +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/configs/resources.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/data/__init__.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/data/dataset.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/data/tile_reader.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/data/tile_store.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/__init__.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/dense_image_worker.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/dense_worker.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/direct_embed_worker.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/image_worker.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/pipeline_worker.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/distributed/worker_entry.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/__init__.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/conch.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/genbio.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/hibou.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/hoptimus.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/isight.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/lunit.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/midnight.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/__init__.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/blocks.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/case.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/loading.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/slide.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/moozy/types.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/musk.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/phikon.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/prism.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/prism2.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/prost40m.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/rudolfv2.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/titan.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/uni.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/virchow.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/models/waiv.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/encoders/validation.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/progress.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/__init__.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/cpu_budget.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_encode.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_image_reading.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_image_recipe.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_image_shard.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_image_stage.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_shard.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_sliding.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/dense_stage.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/distributed.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/distributed_stage.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/effective_encoder_input.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/embedding_pipeline.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/hierarchical.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/image_shard.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/image_specs.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/image_stage.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/manifest.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/persistence.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/preprocessing.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/process_list.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/progress_bridge.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/registry.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/serialization.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/sharding.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/slide_encode.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/tiling.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/tiling_pipeline.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/runtime/worker_io.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/__init__.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/config.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/coordinates.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/log_utils.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/tiling_io.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec/utils/utils.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/dependency_links.txt +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/entry_points.txt +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/not-zip-safe +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/slide2vec.egg-info/top_level.txt +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_architecture_runtime_split.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_attention_extraction.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_benchmark_tooling.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_encode_kit.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_extraction.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_image_reading.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_image_resume.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_image_shard.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_regions.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_shard.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_sliding.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_source_spacing.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_stage.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_dense_worker.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_gpfm_genbio_heavy.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_hs2p_package_cutover.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_image_shard.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_image_stage.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_inference_benchmark.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_isight.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_load_model_hf_auth.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_mascaret.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_on_slide_persisted.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_output_consistency.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_patient_manifest.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_phaet.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_prism.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_prism2.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_process_list_performance.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_progress.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_runtime_batching.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_sharding.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_slide_coordinate_preparation.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_soma_migration.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_tile_store.py +0 -0
- {slide2vec-5.9.2 → slide2vec-6.0.0}/tests/test_tiling_pipeline.py +0 -0
- {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:
|
|
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.
|
|
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
|
[](https://pypi.org/project/slide2vec/)
|
|
120
120
|
[](https://clemsgrs.github.io/slide2vec/)
|
|
121
121
|
|
|
122
|
-
`slide2vec`
|
|
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
|
-
|
|
124
|
+
## Install
|
|
125
125
|
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()`
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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=
|
|
183
|
-
preprocessing=PreprocessingConfig(
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
[](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
|
|
196
|
+
-e HF_TOKEN \
|
|
270
197
|
waticlems/slide2vec:latest
|
|
271
198
|
```
|
|
272
199
|
|
|
273
|
-
|
|
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](
|
|
280
|
-
- [
|
|
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
|
+
[](https://pypi.org/project/slide2vec/)
|
|
4
|
+
[](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
|
+
[](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 = "
|
|
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.
|
|
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 = "
|
|
183
|
+
current_version = "6.0.0"
|
|
184
184
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
185
185
|
commit = false
|
|
186
186
|
tag = false
|
|
@@ -1010,14 +1010,14 @@ class Model:
|
|
|
1010
1010
|
)
|
|
1011
1011
|
self._encoder_input = contract
|
|
1012
1012
|
plan = contract.plan
|
|
1013
|
-
if emit_run_info
|
|
1013
|
+
if emit_run_info:
|
|
1014
1014
|
logging.getLogger("slide2vec").info(
|
|
1015
|
-
"Pooled encoder input for '%s': preset %dpx, requested %dpx
|
|
1016
|
-
"
|
|
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.
|
|
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
|
|
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,
|
|
297
|
-
captured[index] =
|
|
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(
|
|
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(
|
|
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
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
"
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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=
|
|
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)
|