slide2vec 5.7.0__tar.gz → 5.8.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {slide2vec-5.7.0 → slide2vec-5.8.1}/PKG-INFO +10 -10
- {slide2vec-5.7.0 → slide2vec-5.8.1}/README.md +7 -7
- {slide2vec-5.7.0 → slide2vec-5.8.1}/pyproject.toml +4 -4
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/__init__.py +7 -1
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/api.py +24 -6
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/__init__.py +8 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/registry.py +338 -15
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/registry.py +8 -1
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec.egg-info/PKG-INFO +10 -10
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec.egg-info/SOURCES.txt +3 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec.egg-info/requires.txt +2 -2
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_image_stage.py +18 -3
- slide2vec-5.8.1/tests/test_encoder_capabilities.py +481 -0
- slide2vec-5.8.1/tests/test_encoder_plugins.py +298 -0
- slide2vec-5.8.1/tests/test_encoder_provider_failures.py +391 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_hs2p_package_cutover.py +3 -3
- {slide2vec-5.7.0 → slide2vec-5.8.1}/LICENSE +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/setup.cfg +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/__main__.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/artifacts.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/cli.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/configs/__init__.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/configs/default.yaml +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/configs/resources.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/data/__init__.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/data/dataset.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/data/tile_reader.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/data/tile_store.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/distributed/__init__.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/distributed/dense_image_worker.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/distributed/dense_worker.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/distributed/direct_embed_worker.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/distributed/image_worker.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/distributed/pipeline_worker.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/distributed/worker_entry.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/base.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/__init__.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/conch.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/dinov2.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/genbio.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/gigapath.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/gpfm.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/hibou.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/hoptimus.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/isight.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/lunit.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/midnight.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/moozy/__init__.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/moozy/blocks.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/moozy/case.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/moozy/loading.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/moozy/slide.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/moozy/types.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/mstar.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/musk.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/phikon.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/prism.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/prism2.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/prost40m.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/rudolfv2.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/titan.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/uni.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/virchow.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/models/waiv.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/encoders/validation.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/inference.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/progress.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/__init__.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/artifacts_collect.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/batching.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/cpu_budget.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/dense_encode.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/dense_encoder_input.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/dense_image_reading.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/dense_image_recipe.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/dense_image_shard.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/dense_image_stage.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/dense_regions.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/dense_shard.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/dense_sliding.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/dense_stage.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/distributed.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/distributed_stage.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/effective_encoder_input.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/embedding.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/embedding_persist.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/embedding_pipeline.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/encoder_input_contract.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/hierarchical.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/image_shard.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/image_specs.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/image_stage.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/manifest.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/model_settings.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/patient_pipeline.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/persist_callbacks.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/persistence.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/pooled_encoder_input.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/preprocessing.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/process_list.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/progress_bridge.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/serialization.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/sharding.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/slide_encode.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/tiling.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/tiling_pipeline.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/types.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/runtime/worker_io.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/utils/__init__.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/utils/config.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/utils/coordinates.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/utils/log_utils.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/utils/tiling_io.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec/utils/utils.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec.egg-info/dependency_links.txt +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec.egg-info/entry_points.txt +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec.egg-info/not-zip-safe +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/slide2vec.egg-info/top_level.txt +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_architecture_runtime_split.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_attention_extraction.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_encode_kit.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_encoder_input.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_extraction.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_image_reading.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_image_resume.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_image_shard.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_regions.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_shard.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_sliding.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_source_spacing.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_stage.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dense_worker.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_dinov2_natimage.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_encoder_input_contract.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_encoder_registry.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_gpfm_genbio_heavy.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_image_shard.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_image_stage.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_isight.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_mascaret.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_output_consistency.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_patch_size_metadata.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_patient_manifest.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_phaet.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_pooled_encoder_input.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_pooled_geometry.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_prism2.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_progress.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_regression_core.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_regression_inference.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_regression_models.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_rudolfv2.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_runtime_batching.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_sharding.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_slide_coordinate_preparation.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_soma_migration.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_tile_store.py +0 -0
- {slide2vec-5.7.0 → slide2vec-5.8.1}/tests/test_tiling_pipeline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slide2vec
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.8.1
|
|
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
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
18
|
-
Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.
|
|
18
|
+
Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2
|
|
19
19
|
Requires-Dist: omegaconf
|
|
20
20
|
Requires-Dist: matplotlib
|
|
21
21
|
Requires-Dist: numpy<2
|
|
@@ -75,7 +75,7 @@ Requires-Dist: numpy<2; extra == "fm"
|
|
|
75
75
|
Requires-Dist: pandas; extra == "fm"
|
|
76
76
|
Requires-Dist: pillow; extra == "fm"
|
|
77
77
|
Requires-Dist: rich; extra == "fm"
|
|
78
|
-
Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.
|
|
78
|
+
Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2; extra == "fm"
|
|
79
79
|
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"
|
|
@@ -207,7 +207,7 @@ embedded = model.embed_slide("/path/to/slide.svs", preprocessing=preprocessing)
|
|
|
207
207
|
|
|
208
208
|
Hierarchical outputs have shape `(num_regions, tiles_per_region, feature_dim)` and are written to `hierarchical_embeddings/` when persisted.
|
|
209
209
|
|
|
210
|
-
See [
|
|
210
|
+
See the [hierarchical features guide](https://clemsgrs.github.io/slide2vec/hierarchical.html) for details.
|
|
211
211
|
|
|
212
212
|
### Input Manifest
|
|
213
213
|
|
|
@@ -241,7 +241,7 @@ The package writes explicit artifact directories:
|
|
|
241
241
|
|
|
242
242
|
`slide2vec` currently ships presets for 28 tile-level models, 4 slide-level models,
|
|
243
243
|
and 1 patient-level model.
|
|
244
|
-
For the full catalog and preset names, see [
|
|
244
|
+
For the full catalog and preset names, see the [model zoo](https://clemsgrs.github.io/slide2vec/models.html).
|
|
245
245
|
|
|
246
246
|
## CLI
|
|
247
247
|
|
|
@@ -254,7 +254,7 @@ slide2vec /path/to/config.yaml
|
|
|
254
254
|
|
|
255
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
256
|
|
|
257
|
-
New to the CLI or doing batch runs to disk? Start with [
|
|
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.
|
|
258
258
|
|
|
259
259
|
## Docker
|
|
260
260
|
|
|
@@ -272,8 +272,8 @@ docker run --rm -it \
|
|
|
272
272
|
|
|
273
273
|
## Documentation
|
|
274
274
|
|
|
275
|
-
- [Documentation website](https://clemsgrs.github.io/slide2vec/)
|
|
276
|
-
- [
|
|
277
|
-
- [
|
|
278
|
-
- [
|
|
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
|
+
- [Model zoo](https://clemsgrs.github.io/slide2vec/models.html)
|
|
279
279
|
- [`tutorials/api_walkthrough.ipynb`](tutorials/api_walkthrough.ipynb) for a notebook walkthrough of the API
|
|
@@ -91,7 +91,7 @@ embedded = model.embed_slide("/path/to/slide.svs", preprocessing=preprocessing)
|
|
|
91
91
|
|
|
92
92
|
Hierarchical outputs have shape `(num_regions, tiles_per_region, feature_dim)` and are written to `hierarchical_embeddings/` when persisted.
|
|
93
93
|
|
|
94
|
-
See [
|
|
94
|
+
See the [hierarchical features guide](https://clemsgrs.github.io/slide2vec/hierarchical.html) for details.
|
|
95
95
|
|
|
96
96
|
### Input Manifest
|
|
97
97
|
|
|
@@ -125,7 +125,7 @@ The package writes explicit artifact directories:
|
|
|
125
125
|
|
|
126
126
|
`slide2vec` currently ships presets for 28 tile-level models, 4 slide-level models,
|
|
127
127
|
and 1 patient-level model.
|
|
128
|
-
For the full catalog and preset names, see [
|
|
128
|
+
For the full catalog and preset names, see the [model zoo](https://clemsgrs.github.io/slide2vec/models.html).
|
|
129
129
|
|
|
130
130
|
## CLI
|
|
131
131
|
|
|
@@ -138,7 +138,7 @@ slide2vec /path/to/config.yaml
|
|
|
138
138
|
|
|
139
139
|
By default, manifest-driven CLI runs use all available GPUs. Set `speed.num_gpus=4` when you want to cap the sharding explicitly.
|
|
140
140
|
|
|
141
|
-
New to the CLI or doing batch runs to disk? Start with [
|
|
141
|
+
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.
|
|
142
142
|
|
|
143
143
|
## Docker
|
|
144
144
|
|
|
@@ -156,8 +156,8 @@ docker run --rm -it \
|
|
|
156
156
|
|
|
157
157
|
## Documentation
|
|
158
158
|
|
|
159
|
-
- [Documentation website](https://clemsgrs.github.io/slide2vec/)
|
|
160
|
-
- [
|
|
161
|
-
- [
|
|
162
|
-
- [
|
|
159
|
+
- [Documentation website](https://clemsgrs.github.io/slide2vec/)
|
|
160
|
+
- [API guide](https://clemsgrs.github.io/slide2vec/api.html)
|
|
161
|
+
- [CLI guide](https://clemsgrs.github.io/slide2vec/cli.html)
|
|
162
|
+
- [Model zoo](https://clemsgrs.github.io/slide2vec/models.html)
|
|
163
163
|
- [`tutorials/api_walkthrough.ipynb`](tutorials/api_walkthrough.ipynb) for a notebook walkthrough of the API
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slide2vec"
|
|
7
|
-
version = "5.
|
|
7
|
+
version = "5.8.1"
|
|
8
8
|
description = "Embedding of whole slide images with Foundation Models"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -21,7 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.13",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"hs2p[asap,cucim,openslide,sam2,vips]>=4.4.
|
|
24
|
+
"hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2",
|
|
25
25
|
"omegaconf",
|
|
26
26
|
"matplotlib",
|
|
27
27
|
"numpy<2",
|
|
@@ -100,7 +100,7 @@ fm = [
|
|
|
100
100
|
"pandas",
|
|
101
101
|
"pillow",
|
|
102
102
|
"rich",
|
|
103
|
-
"hs2p[asap,cucim,openslide,sam2,vips]>=4.4.
|
|
103
|
+
"hs2p[asap,cucim,openslide,sam2,vips]>=4.4.2",
|
|
104
104
|
"wandb",
|
|
105
105
|
"torch>=2.3,<2.8",
|
|
106
106
|
"torchvision>=0.18.0",
|
|
@@ -180,7 +180,7 @@ no_implicit_reexport = true
|
|
|
180
180
|
max-line-length = 160
|
|
181
181
|
|
|
182
182
|
[tool.bumpver]
|
|
183
|
-
current_version = "5.
|
|
183
|
+
current_version = "5.8.1"
|
|
184
184
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
185
185
|
commit = false # We do version bumping in CI, not as a commit
|
|
186
186
|
tag = false # Git tag already exists — we don't auto-tag
|
|
@@ -21,13 +21,19 @@ from slide2vec.artifacts import (
|
|
|
21
21
|
TileEmbeddingArtifact,
|
|
22
22
|
)
|
|
23
23
|
from slide2vec.runtime.dense_encode import DenseEncodeGeometry, DenseEncodeKit
|
|
24
|
+
from slide2vec.encoders import (
|
|
25
|
+
EncoderProviderDiagnostic,
|
|
26
|
+
list_encoder_provider_diagnostics,
|
|
27
|
+
)
|
|
24
28
|
|
|
25
29
|
|
|
26
|
-
__version__ = "5.
|
|
30
|
+
__version__ = "5.8.1"
|
|
27
31
|
|
|
28
32
|
__all__ = [
|
|
29
33
|
"Model",
|
|
30
34
|
"list_models",
|
|
35
|
+
"EncoderProviderDiagnostic",
|
|
36
|
+
"list_encoder_provider_diagnostics",
|
|
31
37
|
"Pipeline",
|
|
32
38
|
"PreprocessingConfig",
|
|
33
39
|
"DenseOptions",
|
|
@@ -3,6 +3,7 @@ import copy
|
|
|
3
3
|
import logging
|
|
4
4
|
import math
|
|
5
5
|
import os
|
|
6
|
+
import warnings
|
|
6
7
|
from dataclasses import dataclass, field, replace
|
|
7
8
|
from contextlib import contextmanager
|
|
8
9
|
from pathlib import Path
|
|
@@ -23,6 +24,7 @@ from slide2vec.artifacts import (
|
|
|
23
24
|
from slide2vec.configs.resources import load_config
|
|
24
25
|
from slide2vec.encoders.registry import (
|
|
25
26
|
encoder_registry,
|
|
27
|
+
list_encoder_provider_diagnostics,
|
|
26
28
|
resolve_preprocessing_fields,
|
|
27
29
|
)
|
|
28
30
|
from slide2vec.encoders.validation import validate_encoder_config
|
|
@@ -1146,16 +1148,32 @@ def list_models(level: str | None = None) -> list[str]:
|
|
|
1146
1148
|
level: Optional model level filter. Supported values are ``"tile"``,
|
|
1147
1149
|
``"slide"``, and ``"patient"``.
|
|
1148
1150
|
"""
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
+
normalized_level = None
|
|
1152
|
+
if level is not None:
|
|
1153
|
+
normalized_level = str(level).strip().lower()
|
|
1154
|
+
if normalized_level not in {"tile", "slide", "patient"}:
|
|
1155
|
+
raise ValueError(
|
|
1156
|
+
"list_models(level=...) must be one of: tile, slide, patient"
|
|
1157
|
+
)
|
|
1151
1158
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1159
|
+
names = encoder_registry.names()
|
|
1160
|
+
diagnostics = list_encoder_provider_diagnostics()
|
|
1161
|
+
if diagnostics:
|
|
1162
|
+
detail = "; ".join(
|
|
1163
|
+
f"'{item.provider_key}': {item.concise()}" for item in diagnostics
|
|
1164
|
+
)
|
|
1165
|
+
warnings.warn(
|
|
1166
|
+
f"Skipped Encoder provider{'' if len(diagnostics) == 1 else 's'} {detail}",
|
|
1167
|
+
RuntimeWarning,
|
|
1168
|
+
stacklevel=2,
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
if level is None:
|
|
1172
|
+
return sorted(names)
|
|
1155
1173
|
|
|
1156
1174
|
return sorted(
|
|
1157
1175
|
name
|
|
1158
|
-
for name in
|
|
1176
|
+
for name in names
|
|
1159
1177
|
if encoder_registry.info(name)["level"] == normalized_level
|
|
1160
1178
|
)
|
|
1161
1179
|
|
|
@@ -15,9 +15,13 @@ from slide2vec.encoders.base import (
|
|
|
15
15
|
resolve_requested_output_variant,
|
|
16
16
|
)
|
|
17
17
|
from slide2vec.encoders.registry import (
|
|
18
|
+
EncoderCapabilities,
|
|
19
|
+
EncoderProviderDiagnostic,
|
|
18
20
|
encoder_registry,
|
|
21
|
+
list_encoder_provider_diagnostics,
|
|
19
22
|
normalize_patch_size,
|
|
20
23
|
register_encoder,
|
|
24
|
+
resolve_encoder_capabilities,
|
|
21
25
|
resolve_encoder_output,
|
|
22
26
|
resolve_patch_size,
|
|
23
27
|
resolve_preprocessing_requirements,
|
|
@@ -29,6 +33,8 @@ from slide2vec.encoders import models # noqa: F401
|
|
|
29
33
|
|
|
30
34
|
__all__ = [
|
|
31
35
|
"Encoder",
|
|
36
|
+
"EncoderCapabilities",
|
|
37
|
+
"EncoderProviderDiagnostic",
|
|
32
38
|
"PatientEncoder",
|
|
33
39
|
"TileEncoder",
|
|
34
40
|
"SlideEncoder",
|
|
@@ -37,8 +43,10 @@ __all__ = [
|
|
|
37
43
|
"resolve_recommended_dynamic_img_size",
|
|
38
44
|
"resolve_requested_output_variant",
|
|
39
45
|
"encoder_registry",
|
|
46
|
+
"list_encoder_provider_diagnostics",
|
|
40
47
|
"normalize_patch_size",
|
|
41
48
|
"register_encoder",
|
|
49
|
+
"resolve_encoder_capabilities",
|
|
42
50
|
"resolve_patch_size",
|
|
43
51
|
"resolve_preprocessing_requirements",
|
|
44
52
|
"resolve_encoder_output",
|
|
@@ -1,10 +1,201 @@
|
|
|
1
1
|
"""Encoder registry with enforced metadata schema."""
|
|
2
2
|
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum, auto
|
|
5
|
+
from importlib import metadata as importlib_metadata
|
|
6
|
+
import inspect
|
|
7
|
+
from threading import Condition, RLock, get_ident
|
|
3
8
|
from typing import Any
|
|
4
9
|
|
|
10
|
+
from slide2vec.encoders.base import PatientEncoder, SlideEncoder, TileEncoder
|
|
5
11
|
from slide2vec.runtime.registry import Registry
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
|
|
14
|
+
_ENCODER_ENTRY_POINT_GROUP = "slide2vec.encoders"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _DiscoveryState(Enum):
|
|
18
|
+
PENDING = auto()
|
|
19
|
+
RUNNING = auto()
|
|
20
|
+
COMPLETE = auto()
|
|
21
|
+
FAILED = auto()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class EncoderProviderDiagnostic:
|
|
26
|
+
"""Public, immutable description of one skipped installed provider."""
|
|
27
|
+
|
|
28
|
+
provider_key: str
|
|
29
|
+
provider: str
|
|
30
|
+
exception_type: str
|
|
31
|
+
message: str
|
|
32
|
+
|
|
33
|
+
def concise(self) -> str:
|
|
34
|
+
"""Format the diagnostic without traceback details."""
|
|
35
|
+
return f"{self.exception_type}: {self.message}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _installed_encoder_providers() -> list[importlib_metadata.EntryPoint]:
|
|
39
|
+
entry_points = importlib_metadata.entry_points()
|
|
40
|
+
providers = entry_points.select(group=_ENCODER_ENTRY_POINT_GROUP)
|
|
41
|
+
return sorted(providers, key=lambda provider: (provider.name, provider.value))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EncoderRegistry(Registry):
|
|
45
|
+
"""Encoder registry with one lazy, process-global plugin discovery pass."""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
super().__init__("encoders")
|
|
49
|
+
self._discovery_condition = Condition(RLock())
|
|
50
|
+
self._discovery_state = _DiscoveryState.PENDING
|
|
51
|
+
self._discovery_owner: int | None = None
|
|
52
|
+
self._discovery_error: BaseException | None = None
|
|
53
|
+
self._provider_diagnostics: tuple[EncoderProviderDiagnostic, ...] = ()
|
|
54
|
+
|
|
55
|
+
def _ensure_plugins_discovered(self) -> None:
|
|
56
|
+
current_thread = get_ident()
|
|
57
|
+
with self._discovery_condition:
|
|
58
|
+
while self._discovery_state is _DiscoveryState.RUNNING:
|
|
59
|
+
if self._discovery_owner == current_thread:
|
|
60
|
+
return
|
|
61
|
+
self._discovery_condition.wait()
|
|
62
|
+
if self._discovery_state is _DiscoveryState.COMPLETE:
|
|
63
|
+
return
|
|
64
|
+
if self._discovery_state is _DiscoveryState.FAILED:
|
|
65
|
+
assert self._discovery_error is not None
|
|
66
|
+
raise self._discovery_error
|
|
67
|
+
self._discovery_state = _DiscoveryState.RUNNING
|
|
68
|
+
self._discovery_owner = current_thread
|
|
69
|
+
|
|
70
|
+
diagnostics: list[EncoderProviderDiagnostic] = []
|
|
71
|
+
try:
|
|
72
|
+
for entry_point in _installed_encoder_providers():
|
|
73
|
+
entries_before_provider = dict(self._entries)
|
|
74
|
+
try:
|
|
75
|
+
entry_point.load()()
|
|
76
|
+
except BaseException as error:
|
|
77
|
+
self._entries = entries_before_provider
|
|
78
|
+
message = " ".join(str(error).split()) or "No error message"
|
|
79
|
+
diagnostics.append(
|
|
80
|
+
EncoderProviderDiagnostic(
|
|
81
|
+
provider_key=entry_point.name,
|
|
82
|
+
provider=entry_point.value,
|
|
83
|
+
exception_type=type(error).__name__,
|
|
84
|
+
message=message,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
except BaseException as error:
|
|
88
|
+
with self._discovery_condition:
|
|
89
|
+
self._discovery_state = _DiscoveryState.FAILED
|
|
90
|
+
self._discovery_owner = None
|
|
91
|
+
self._discovery_error = error
|
|
92
|
+
self._discovery_condition.notify_all()
|
|
93
|
+
raise
|
|
94
|
+
else:
|
|
95
|
+
with self._discovery_condition:
|
|
96
|
+
self._provider_diagnostics = tuple(diagnostics)
|
|
97
|
+
self._discovery_state = _DiscoveryState.COMPLETE
|
|
98
|
+
self._discovery_owner = None
|
|
99
|
+
self._discovery_condition.notify_all()
|
|
100
|
+
|
|
101
|
+
def _before_read(self) -> None:
|
|
102
|
+
self._ensure_plugins_discovered()
|
|
103
|
+
|
|
104
|
+
def provider_diagnostics(self) -> tuple[EncoderProviderDiagnostic, ...]:
|
|
105
|
+
"""Return an immutable snapshot of skipped provider diagnostics."""
|
|
106
|
+
self._ensure_plugins_discovered()
|
|
107
|
+
return self._provider_diagnostics
|
|
108
|
+
|
|
109
|
+
def _missing_preset_error(self, name: str) -> KeyError:
|
|
110
|
+
available = ", ".join(sorted(self._entries)) or "(none)"
|
|
111
|
+
message = f"'{name}' not found in encoders registry. Available: {available}"
|
|
112
|
+
if self._provider_diagnostics:
|
|
113
|
+
failures = "; ".join(
|
|
114
|
+
f"'{item.provider_key}' ({item.concise()})"
|
|
115
|
+
for item in self._provider_diagnostics
|
|
116
|
+
)
|
|
117
|
+
message += f". Skipped Encoder providers: {failures}"
|
|
118
|
+
return KeyError(message)
|
|
119
|
+
|
|
120
|
+
def require(self, name: str) -> type:
|
|
121
|
+
"""Retrieve an Encoder class, including provider context when it is missing."""
|
|
122
|
+
self._ensure_plugins_discovered()
|
|
123
|
+
if name not in self._entries:
|
|
124
|
+
raise self._missing_preset_error(name)
|
|
125
|
+
return self._entries[name].cls
|
|
126
|
+
|
|
127
|
+
def info(self, name: str) -> dict[str, Any]:
|
|
128
|
+
"""Return Encoder metadata, including provider context when it is missing."""
|
|
129
|
+
self._ensure_plugins_discovered()
|
|
130
|
+
if name not in self._entries:
|
|
131
|
+
raise self._missing_preset_error(name)
|
|
132
|
+
entry = self._entries[name]
|
|
133
|
+
return {"name": name, **entry.metadata}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
encoder_registry = EncoderRegistry()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def list_encoder_provider_diagnostics() -> tuple[EncoderProviderDiagnostic, ...]:
|
|
140
|
+
"""List deterministic diagnostics for installed providers skipped at discovery."""
|
|
141
|
+
return encoder_registry.provider_diagnostics()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class EncoderCapabilities:
|
|
146
|
+
"""Resolved, metadata-only preflight view of one registered Encoder preset."""
|
|
147
|
+
|
|
148
|
+
name: str
|
|
149
|
+
level: str
|
|
150
|
+
pooled: bool
|
|
151
|
+
dense: bool
|
|
152
|
+
attention: bool
|
|
153
|
+
slide: bool
|
|
154
|
+
patient: bool
|
|
155
|
+
patch_size: tuple[int, int] | None
|
|
156
|
+
tile_encoder: str | None
|
|
157
|
+
tile_encoder_output_variant: str | None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def resolve_encoder_capabilities(encoder_name: str) -> EncoderCapabilities:
|
|
161
|
+
"""Resolve supported extraction contracts without constructing the encoder."""
|
|
162
|
+
encoder_cls = encoder_registry.require(encoder_name)
|
|
163
|
+
metadata = encoder_registry.info(encoder_name)
|
|
164
|
+
_validate_encoder_capability_contract(encoder_name, encoder_cls, metadata)
|
|
165
|
+
level = resolve_encoder_level(encoder_name, metadata)
|
|
166
|
+
dense = all(_dense_class_contract(encoder_cls))
|
|
167
|
+
attention = _supports_attention(encoder_cls)
|
|
168
|
+
tile_encoder = None
|
|
169
|
+
tile_encoder_output_variant = None
|
|
170
|
+
if level in {"slide", "patient"}:
|
|
171
|
+
tile_encoder, tile_encoder_output_variant = (
|
|
172
|
+
_resolve_hierarchical_tile_dependency(encoder_name, metadata)
|
|
173
|
+
)
|
|
174
|
+
dependency_metadata = encoder_registry.info(tile_encoder)
|
|
175
|
+
dependency_level = resolve_encoder_level(tile_encoder, dependency_metadata)
|
|
176
|
+
if dependency_level != "tile":
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"Encoder '{encoder_name}' tile_encoder dependency '{tile_encoder}' "
|
|
179
|
+
f"must have level='tile', got level='{dependency_level}'."
|
|
180
|
+
)
|
|
181
|
+
dependency_output = resolve_tile_dependency_output(
|
|
182
|
+
encoder_name,
|
|
183
|
+
metadata=metadata,
|
|
184
|
+
)
|
|
185
|
+
tile_encoder_output_variant = str(dependency_output["output_variant"])
|
|
186
|
+
resolve_encoder_capabilities(tile_encoder)
|
|
187
|
+
return EncoderCapabilities(
|
|
188
|
+
name=encoder_name,
|
|
189
|
+
level=level,
|
|
190
|
+
pooled=issubclass(encoder_cls, TileEncoder),
|
|
191
|
+
dense=dense,
|
|
192
|
+
attention=attention,
|
|
193
|
+
slide=issubclass(encoder_cls, (SlideEncoder, PatientEncoder)),
|
|
194
|
+
patient=issubclass(encoder_cls, PatientEncoder),
|
|
195
|
+
patch_size=resolve_patch_size(encoder_name, metadata) if dense else None,
|
|
196
|
+
tile_encoder=tile_encoder,
|
|
197
|
+
tile_encoder_output_variant=tile_encoder_output_variant,
|
|
198
|
+
)
|
|
8
199
|
|
|
9
200
|
|
|
10
201
|
def require_encoder_metadata_field(
|
|
@@ -31,6 +222,122 @@ def resolve_encoder_level(
|
|
|
31
222
|
return level
|
|
32
223
|
|
|
33
224
|
|
|
225
|
+
_DENSE_CLASS_MEMBERS = (
|
|
226
|
+
"encode_tiles_dense",
|
|
227
|
+
"patch_size",
|
|
228
|
+
"get_normalization_transform",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _dense_class_contract(encoder_cls: type) -> tuple[bool, ...]:
|
|
233
|
+
if not issubclass(encoder_cls, TileEncoder):
|
|
234
|
+
return (False,) * len(_DENSE_CLASS_MEMBERS)
|
|
235
|
+
return tuple(
|
|
236
|
+
getattr(encoder_cls, member) is not getattr(TileEncoder, member)
|
|
237
|
+
for member in _DENSE_CLASS_MEMBERS
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _supports_attention(encoder_cls: type) -> bool:
|
|
242
|
+
return (
|
|
243
|
+
issubclass(encoder_cls, TileEncoder)
|
|
244
|
+
and getattr(encoder_cls, "encode_tiles_attention")
|
|
245
|
+
is not TileEncoder.encode_tiles_attention
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _format_names(names: list[str]) -> str:
|
|
250
|
+
if len(names) == 1:
|
|
251
|
+
return names[0]
|
|
252
|
+
if len(names) == 2:
|
|
253
|
+
return " and ".join(names)
|
|
254
|
+
return ", ".join(names[:-1]) + f", and {names[-1]}"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _validate_encoder_capability_contract(
|
|
258
|
+
encoder_name: str,
|
|
259
|
+
encoder_cls: type,
|
|
260
|
+
metadata: dict[str, Any],
|
|
261
|
+
) -> None:
|
|
262
|
+
level = resolve_encoder_level(encoder_name, metadata)
|
|
263
|
+
required_base = {
|
|
264
|
+
"tile": TileEncoder,
|
|
265
|
+
"slide": SlideEncoder,
|
|
266
|
+
"patient": PatientEncoder,
|
|
267
|
+
}[level]
|
|
268
|
+
if not issubclass(encoder_cls, required_base):
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"Encoder '{encoder_name}' declares level='{level}', but class "
|
|
271
|
+
f"{encoder_cls.__name__} must subclass {required_base.__name__}."
|
|
272
|
+
)
|
|
273
|
+
if inspect.isabstract(encoder_cls):
|
|
274
|
+
missing = sorted(getattr(encoder_cls, "__abstractmethods__"))
|
|
275
|
+
raise ValueError(
|
|
276
|
+
f"Encoder '{encoder_name}' class {encoder_cls.__name__} is abstract; "
|
|
277
|
+
f"implement {_format_names(missing)} before registration."
|
|
278
|
+
)
|
|
279
|
+
if level in {"slide", "patient"}:
|
|
280
|
+
require_encoder_metadata_field(encoder_name, metadata, "tile_encoder")
|
|
281
|
+
require_encoder_metadata_field(
|
|
282
|
+
encoder_name,
|
|
283
|
+
metadata,
|
|
284
|
+
"tile_encoder_output_variant",
|
|
285
|
+
)
|
|
286
|
+
dense_members = _dense_class_contract(encoder_cls)
|
|
287
|
+
has_static_patch_size = metadata.get("patch_size") is not None
|
|
288
|
+
if any(dense_members) and not all(dense_members):
|
|
289
|
+
implemented = [
|
|
290
|
+
member
|
|
291
|
+
for member, is_implemented in zip(_DENSE_CLASS_MEMBERS, dense_members)
|
|
292
|
+
if is_implemented
|
|
293
|
+
]
|
|
294
|
+
missing = [
|
|
295
|
+
member
|
|
296
|
+
for member, is_implemented in zip(_DENSE_CLASS_MEMBERS, dense_members)
|
|
297
|
+
if not is_implemented
|
|
298
|
+
]
|
|
299
|
+
raise ValueError(
|
|
300
|
+
f"Encoder '{encoder_name}' has an incomplete dense class contract: "
|
|
301
|
+
f"{_format_names(implemented)} "
|
|
302
|
+
f"{'is' if len(implemented) == 1 else 'are'} overridden, but "
|
|
303
|
+
f"{_format_names(missing)} "
|
|
304
|
+
f"{'is' if len(missing) == 1 else 'are'} inherited as unsupported. "
|
|
305
|
+
"Override all three dense members together and declare patch_size metadata."
|
|
306
|
+
)
|
|
307
|
+
if has_static_patch_size and not all(dense_members):
|
|
308
|
+
raise ValueError(
|
|
309
|
+
f"Encoder '{encoder_name}' has an inconsistent dense contract: "
|
|
310
|
+
"patch_size metadata is declared, but the class must also override "
|
|
311
|
+
"encode_tiles_dense, patch_size, and get_normalization_transform."
|
|
312
|
+
)
|
|
313
|
+
if all(dense_members) and not has_static_patch_size:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"Encoder '{encoder_name}' implements the dense class contract but does "
|
|
316
|
+
"not declare patch_size metadata. Add the encoder's static patch size "
|
|
317
|
+
"to @register_encoder."
|
|
318
|
+
)
|
|
319
|
+
if has_static_patch_size:
|
|
320
|
+
patch = metadata["patch_size"]
|
|
321
|
+
patch_values = patch if isinstance(patch, tuple) else (patch, patch)
|
|
322
|
+
valid_patch = (
|
|
323
|
+
len(patch_values) == 2
|
|
324
|
+
and all(type(value) is int and value > 0 for value in patch_values)
|
|
325
|
+
)
|
|
326
|
+
if not valid_patch:
|
|
327
|
+
raise ValueError(
|
|
328
|
+
f"Encoder '{encoder_name}' must declare patch_size as a positive int "
|
|
329
|
+
f"or pair of positive ints; got {patch!r}."
|
|
330
|
+
)
|
|
331
|
+
if _supports_attention(encoder_cls) and not (
|
|
332
|
+
all(dense_members) and has_static_patch_size
|
|
333
|
+
):
|
|
334
|
+
raise ValueError(
|
|
335
|
+
f"Encoder '{encoder_name}' overrides encode_tiles_attention without a "
|
|
336
|
+
"complete dense contract. Attention maps require encode_tiles_dense, "
|
|
337
|
+
"patch_size, get_normalization_transform, and static patch_size metadata."
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
34
341
|
def register_encoder(
|
|
35
342
|
name: str,
|
|
36
343
|
*,
|
|
@@ -66,9 +373,9 @@ def register_encoder(
|
|
|
66
373
|
cache key can be resolved via :func:`resolve_patch_size` WITHOUT
|
|
67
374
|
instantiating the (multi-GB) encoder; the model-load path asserts this
|
|
68
375
|
static value still equals the loaded model's runtime ``patch_size``.
|
|
69
|
-
level: Encoder output level ("tile" or "
|
|
70
|
-
tile_encoder: Registered tile encoder dependency for slide
|
|
71
|
-
tile_encoder_output_variant: Fixed tile
|
|
376
|
+
level: Encoder output level ("tile", "slide", or "patient").
|
|
377
|
+
tile_encoder: Registered tile encoder dependency for slide/patient models.
|
|
378
|
+
tile_encoder_output_variant: Fixed tile output variant for slide/patient models.
|
|
72
379
|
supported_spacing_um: The spacing(s) in µm/px the model was trained/validated
|
|
73
380
|
for; :func:`validate_encoder_config` rejects requests outside this set
|
|
74
381
|
unless ``allow_non_recommended_settings=True``. ``None`` marks a
|
|
@@ -111,7 +418,13 @@ def register_encoder(
|
|
|
111
418
|
"precision": precision,
|
|
112
419
|
"source": source,
|
|
113
420
|
}
|
|
114
|
-
|
|
421
|
+
|
|
422
|
+
def decorator(encoder_cls: type) -> type:
|
|
423
|
+
_validate_encoder_capability_contract(name, encoder_cls, metadata)
|
|
424
|
+
encoder_registry.register(name, encoder_cls, metadata=metadata)
|
|
425
|
+
return encoder_cls
|
|
426
|
+
|
|
427
|
+
return decorator
|
|
115
428
|
|
|
116
429
|
|
|
117
430
|
def resolve_variable_input_capability(
|
|
@@ -372,16 +685,8 @@ def resolve_tile_dependency_output(
|
|
|
372
685
|
resolved["encoder_name"] = encoder_name
|
|
373
686
|
return resolved
|
|
374
687
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
require_encoder_metadata_field(encoder_name, info, "tile_encoder")
|
|
378
|
-
)
|
|
379
|
-
tile_encoder_output_variant = str(
|
|
380
|
-
require_encoder_metadata_field(
|
|
381
|
-
encoder_name,
|
|
382
|
-
info,
|
|
383
|
-
"tile_encoder_output_variant",
|
|
384
|
-
)
|
|
688
|
+
tile_encoder_name, tile_encoder_output_variant = (
|
|
689
|
+
_resolve_hierarchical_tile_dependency(encoder_name, info)
|
|
385
690
|
)
|
|
386
691
|
tile_info = encoder_registry.info(tile_encoder_name)
|
|
387
692
|
resolved = resolve_encoder_output(
|
|
@@ -391,3 +696,21 @@ def resolve_tile_dependency_output(
|
|
|
391
696
|
)
|
|
392
697
|
resolved["encoder_name"] = tile_encoder_name
|
|
393
698
|
return resolved
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _resolve_hierarchical_tile_dependency(
|
|
702
|
+
encoder_name: str,
|
|
703
|
+
metadata: dict[str, Any],
|
|
704
|
+
) -> tuple[str, str]:
|
|
705
|
+
"""Read the fixed tile preset and output variant for a hierarchical encoder."""
|
|
706
|
+
tile_encoder_name = str(
|
|
707
|
+
require_encoder_metadata_field(encoder_name, metadata, "tile_encoder")
|
|
708
|
+
)
|
|
709
|
+
tile_encoder_output_variant = str(
|
|
710
|
+
require_encoder_metadata_field(
|
|
711
|
+
encoder_name,
|
|
712
|
+
metadata,
|
|
713
|
+
"tile_encoder_output_variant",
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
return tile_encoder_name, tile_encoder_output_variant
|