slide2vec 5.1.1__tar.gz → 5.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {slide2vec-5.1.1 → slide2vec-5.3.0}/PKG-INFO +2 -2
- {slide2vec-5.1.1 → slide2vec-5.3.0}/README.md +1 -1
- {slide2vec-5.1.1 → slide2vec-5.3.0}/pyproject.toml +2 -2
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/__init__.py +1 -1
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/api.py +11 -1
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/artifacts.py +22 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/configs/default.yaml +1 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/__init__.py +4 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/__init__.py +8 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/conch.py +2 -0
- slide2vec-5.3.0/slide2vec/encoders/models/dinov2.py +56 -0
- slide2vec-5.3.0/slide2vec/encoders/models/genbio.py +155 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/gigapath.py +1 -0
- slide2vec-5.3.0/slide2vec/encoders/models/gpfm.py +89 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/hibou.py +2 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/hoptimus.py +3 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/lunit.py +1 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/midnight.py +1 -0
- slide2vec-5.3.0/slide2vec/encoders/models/mstar.py +34 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/musk.py +1 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/phikon.py +2 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/prost40m.py +1 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/uni.py +2 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/virchow.py +2 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/registry.py +117 -26
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/inference.py +16 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/dense_regions.py +3 -3
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/embedding.py +12 -6
- slide2vec-5.3.0/slide2vec/runtime/model_settings.py +101 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/patient_pipeline.py +9 -2
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/serialization.py +3 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/PKG-INFO +2 -2
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/SOURCES.txt +7 -0
- slide2vec-5.3.0/tests/test_dinov2_natimage.py +116 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_encoder_registry.py +51 -0
- slide2vec-5.3.0/tests/test_gpfm_genbio_heavy.py +64 -0
- slide2vec-5.3.0/tests/test_patch_size_metadata.py +213 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_regression_core.py +168 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_regression_inference.py +5 -0
- slide2vec-5.1.1/slide2vec/runtime/model_settings.py +0 -48
- {slide2vec-5.1.1 → slide2vec-5.3.0}/LICENSE +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/setup.cfg +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/__main__.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/cli.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/configs/__init__.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/configs/resources.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/data/__init__.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/data/dataset.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/data/tile_reader.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/data/tile_store.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/distributed/__init__.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/distributed/direct_embed_worker.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/distributed/pipeline_worker.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/base.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/__init__.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/blocks.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/case.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/loading.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/slide.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/types.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/prism.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/titan.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/validation.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/progress.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/__init__.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/artifacts_collect.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/batching.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/cpu_budget.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/dense_sliding.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/distributed.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/distributed_stage.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/embedding_persist.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/embedding_pipeline.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/hierarchical.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/manifest.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/persist_callbacks.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/persistence.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/process_list.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/progress_bridge.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/registry.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/slide_encode.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/tiling.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/tiling_pipeline.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/types.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/worker_io.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/__init__.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/config.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/coordinates.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/log_utils.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/tiling_io.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/utils.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/dependency_links.txt +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/entry_points.txt +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/not-zip-safe +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/requires.txt +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/top_level.txt +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_architecture_runtime_split.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_attention_extraction.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_dense_extraction.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_dense_regions.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_dense_sliding.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_hs2p_package_cutover.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_output_consistency.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_progress.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_regression_models.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_runtime_batching.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_tile_store.py +0 -0
- {slide2vec-5.1.1 → slide2vec-5.3.0}/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.3.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
|
|
@@ -225,7 +225,7 @@ The package writes explicit artifact directories:
|
|
|
225
225
|
|
|
226
226
|
### Supported Models
|
|
227
227
|
|
|
228
|
-
`slide2vec` currently ships preset configs for
|
|
228
|
+
`slide2vec` currently ships preset configs for 22 tile-level models and 3 slide-level models.
|
|
229
229
|
For the full catalog and preset names, see [`docs/models.md`](docs/models.md).
|
|
230
230
|
|
|
231
231
|
## CLI
|
|
@@ -119,7 +119,7 @@ The package writes explicit artifact directories:
|
|
|
119
119
|
|
|
120
120
|
### Supported Models
|
|
121
121
|
|
|
122
|
-
`slide2vec` currently ships preset configs for
|
|
122
|
+
`slide2vec` currently ships preset configs for 22 tile-level models and 3 slide-level models.
|
|
123
123
|
For the full catalog and preset names, see [`docs/models.md`](docs/models.md).
|
|
124
124
|
|
|
125
125
|
## CLI
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slide2vec"
|
|
7
|
-
version = "5.
|
|
7
|
+
version = "5.3.0"
|
|
8
8
|
description = "Embedding of whole slide images with Foundation Models"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -167,7 +167,7 @@ no_implicit_reexport = true
|
|
|
167
167
|
max-line-length = 160
|
|
168
168
|
|
|
169
169
|
[tool.bumpver]
|
|
170
|
-
current_version = "5.
|
|
170
|
+
current_version = "5.3.0"
|
|
171
171
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
172
172
|
commit = false # We do version bumping in CI, not as a commit
|
|
173
173
|
tag = false # Git tag already exists — we don't auto-tag
|
|
@@ -21,7 +21,11 @@ from slide2vec.encoders.registry import (
|
|
|
21
21
|
resolve_preprocessing_defaults,
|
|
22
22
|
)
|
|
23
23
|
from slide2vec.encoders.validation import validate_encoder_config
|
|
24
|
-
from slide2vec.runtime.model_settings import
|
|
24
|
+
from slide2vec.runtime.model_settings import (
|
|
25
|
+
canonicalize_model_name,
|
|
26
|
+
normalize_output_dtype,
|
|
27
|
+
normalize_precision_name,
|
|
28
|
+
)
|
|
25
29
|
from slide2vec.progress import emit_progress
|
|
26
30
|
from slide2vec.runtime.types import LoadedModel
|
|
27
31
|
from slide2vec.utils.utils import cpu_worker_limit, slurm_cpu_limit
|
|
@@ -226,6 +230,10 @@ class ExecutionOptions:
|
|
|
226
230
|
#: Forward-pass dtype — ``"fp16"``, ``"bf16"``, ``"fp32"``,
|
|
227
231
|
#: or ``None`` (auto-determined from the model preset).
|
|
228
232
|
precision: str | None = None
|
|
233
|
+
#: On-disk feature dtype — ``"fp16"``, ``"fp32"``, or ``None`` to follow
|
|
234
|
+
#: :attr:`precision` (fp16 → fp16, else fp32). Applies to tile, slide, hierarchical,
|
|
235
|
+
#: and patient artifacts; ``"bf16"`` is rejected (numpy has no bfloat16).
|
|
236
|
+
output_dtype: str | None = None
|
|
229
237
|
#: DataLoader prefetch queue depth per worker (default ``4``).
|
|
230
238
|
prefetch_factor: int = 4
|
|
231
239
|
#: Persist tile embeddings to disk when running a slide-level model.
|
|
@@ -253,6 +261,7 @@ class ExecutionOptions:
|
|
|
253
261
|
),
|
|
254
262
|
num_gpus=1 if run_on_cpu else (int(configured_num_gpus) if configured_num_gpus is not None else None),
|
|
255
263
|
precision="fp32" if run_on_cpu else requested_precision,
|
|
264
|
+
output_dtype=getattr(cfg.speed, "output_dtype", None),
|
|
256
265
|
prefetch_factor=prefetch_factor,
|
|
257
266
|
save_tile_embeddings=bool(cfg.model.save_tile_embeddings),
|
|
258
267
|
save_slide_embeddings=bool(cfg.model.save_slide_embeddings),
|
|
@@ -263,6 +272,7 @@ class ExecutionOptions:
|
|
|
263
272
|
resolved_num_gpus = _default_num_gpus() if self.num_gpus is None else self.num_gpus
|
|
264
273
|
object.__setattr__(self, "num_gpus", resolved_num_gpus)
|
|
265
274
|
object.__setattr__(self, "precision", normalize_precision_name(self.precision))
|
|
275
|
+
object.__setattr__(self, "output_dtype", normalize_output_dtype(self.output_dtype))
|
|
266
276
|
if resolved_num_gpus < 1:
|
|
267
277
|
raise ValueError("ExecutionOptions.num_gpus must be at least 1")
|
|
268
278
|
if self.prefetch_factor < 1:
|
|
@@ -7,6 +7,8 @@ import numpy as np
|
|
|
7
7
|
import torch
|
|
8
8
|
from hs2p.fileops import is_flattened_annotation
|
|
9
9
|
|
|
10
|
+
from slide2vec.runtime.model_settings import output_torch_dtype
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
@dataclass(frozen=True, kw_only=True)
|
|
12
14
|
class TileEmbeddingArtifact:
|
|
@@ -74,6 +76,26 @@ def _validate_output_format(output_format: str) -> str:
|
|
|
74
76
|
return normalized
|
|
75
77
|
|
|
76
78
|
|
|
79
|
+
_OUTPUT_NUMPY_DTYPE = {"fp16": np.float16, "fp32": np.float32}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cast_feature_dtype(data: Any, precision: str) -> Any:
|
|
83
|
+
"""Cast features to the on-disk ``precision`` (``"fp16"`` / ``"fp32"``), keeping their kind.
|
|
84
|
+
|
|
85
|
+
Torch tensors are cast via ``.to`` and arrays via ``astype``; ``None`` (no features)
|
|
86
|
+
passes through. This is what makes the pooled tile/slide/hierarchical/patient artifacts
|
|
87
|
+
land in a deterministic dtype, mirroring the dense path's ``output_dtype``. The precision
|
|
88
|
+
is resolved upstream by :func:`slide2vec.runtime.model_settings.resolve_output_precision`,
|
|
89
|
+
so only ``"fp16"`` / ``"fp32"`` reach here.
|
|
90
|
+
"""
|
|
91
|
+
if data is None:
|
|
92
|
+
return data
|
|
93
|
+
torch_dtype = output_torch_dtype(precision) # validates precision (shared string→dtype map)
|
|
94
|
+
if torch.is_tensor(data):
|
|
95
|
+
return data.to(torch_dtype)
|
|
96
|
+
return np.asarray(data).astype(_OUTPUT_NUMPY_DTYPE[precision], copy=False)
|
|
97
|
+
|
|
98
|
+
|
|
77
99
|
def _ensure_array(data: Any) -> np.ndarray:
|
|
78
100
|
if isinstance(data, np.ndarray):
|
|
79
101
|
return data
|
|
@@ -88,6 +88,7 @@ tiling:
|
|
|
88
88
|
|
|
89
89
|
speed:
|
|
90
90
|
precision: # model inference precision ["fp32", "fp16", "bf16"]; if not set, determined automatically based on model recommendations
|
|
91
|
+
output_dtype: # on-disk feature dtype ["fp16", "fp32"]; if not set, follows speed.precision (fp16 -> fp16, otherwise fp32)
|
|
91
92
|
num_dataloader_workers: # number of DataLoader worker processes per GPU rank for reading tiles during embedding; defaults to auto (job CPU budget split across GPUs, except cuCIM on-the-fly uses per-GPU budget // speed.num_cucim_workers)
|
|
92
93
|
num_gpus: # number of GPUs to use for feature extraction; defaults to all available GPUs
|
|
93
94
|
num_preprocessing_workers: # number of workers for hs2p tiling (WSI reading, JPEG encoding, tar writing); defaults to the runtime CPU budget capped at 64
|
|
@@ -16,8 +16,10 @@ from slide2vec.encoders.base import (
|
|
|
16
16
|
)
|
|
17
17
|
from slide2vec.encoders.registry import (
|
|
18
18
|
encoder_registry,
|
|
19
|
+
normalize_patch_size,
|
|
19
20
|
register_encoder,
|
|
20
21
|
resolve_encoder_output,
|
|
22
|
+
resolve_patch_size,
|
|
21
23
|
resolve_preprocessing_requirements,
|
|
22
24
|
resolve_tile_dependency_output,
|
|
23
25
|
)
|
|
@@ -35,7 +37,9 @@ __all__ = [
|
|
|
35
37
|
"resolve_recommended_dynamic_img_size",
|
|
36
38
|
"resolve_requested_output_variant",
|
|
37
39
|
"encoder_registry",
|
|
40
|
+
"normalize_patch_size",
|
|
38
41
|
"register_encoder",
|
|
42
|
+
"resolve_patch_size",
|
|
39
43
|
"resolve_preprocessing_requirements",
|
|
40
44
|
"resolve_encoder_output",
|
|
41
45
|
"resolve_tile_dependency_output",
|
|
@@ -5,12 +5,16 @@ Importing this package registers all encoders in the encoder_registry.
|
|
|
5
5
|
|
|
6
6
|
from . import (
|
|
7
7
|
conch,
|
|
8
|
+
dinov2,
|
|
9
|
+
genbio,
|
|
8
10
|
gigapath,
|
|
11
|
+
gpfm,
|
|
9
12
|
hibou,
|
|
10
13
|
hoptimus,
|
|
11
14
|
lunit,
|
|
12
15
|
midnight,
|
|
13
16
|
moozy,
|
|
17
|
+
mstar,
|
|
14
18
|
musk,
|
|
15
19
|
phikon,
|
|
16
20
|
prost40m,
|
|
@@ -22,12 +26,16 @@ from . import (
|
|
|
22
26
|
|
|
23
27
|
__all__ = [
|
|
24
28
|
"conch",
|
|
29
|
+
"dinov2",
|
|
30
|
+
"genbio",
|
|
25
31
|
"gigapath",
|
|
32
|
+
"gpfm",
|
|
26
33
|
"hibou",
|
|
27
34
|
"hoptimus",
|
|
28
35
|
"lunit",
|
|
29
36
|
"midnight",
|
|
30
37
|
"moozy",
|
|
38
|
+
"mstar",
|
|
31
39
|
"musk",
|
|
32
40
|
"phikon",
|
|
33
41
|
"prost40m",
|
|
@@ -78,6 +78,7 @@ def _encode_trunk_dense(*, trunk, batch: Tensor, encoder_name: str) -> Tensor:
|
|
|
78
78
|
output_variants={"default": {"encode_dim": 512}},
|
|
79
79
|
default_output_variant="default",
|
|
80
80
|
input_size=448,
|
|
81
|
+
patch_size=16,
|
|
81
82
|
supported_spacing_um=0.5,
|
|
82
83
|
precision="fp32",
|
|
83
84
|
source="MahmoodLab/conch",
|
|
@@ -163,6 +164,7 @@ class CONCH(TileEncoder):
|
|
|
163
164
|
output_variants={"default": {"encode_dim": 768}},
|
|
164
165
|
default_output_variant="default",
|
|
165
166
|
input_size=448,
|
|
167
|
+
patch_size=16,
|
|
166
168
|
supported_spacing_um=0.5,
|
|
167
169
|
precision="fp16",
|
|
168
170
|
source="MahmoodLab/TITAN",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Natural-image DINOv2 ViT-B/14 control encoder.
|
|
2
|
+
|
|
3
|
+
``dinov2-vitb14`` is a **non-pathology** ViT: the original DINOv2 ViT-B/14
|
|
4
|
+
(Oquab et al., 2024) self-supervised on LVD-142M *natural* images, shipped by
|
|
5
|
+
``timm`` as ``vit_base_patch14_dinov2.lvd142m`` (weights hosted on Hugging Face
|
|
6
|
+
under ``timm/vit_base_patch14_dinov2.lvd142m`` — public, no gated access).
|
|
7
|
+
|
|
8
|
+
It exists as a **control**: nearly every pathology tile encoder here (UNI,
|
|
9
|
+
Virchow, GigaPath, H-optimus, Midnight, …) is a DINOv2-family ViT, so pairing
|
|
10
|
+
them with a DINOv2 ViT trained on natural images holds the architecture and the
|
|
11
|
+
self-supervised objective fixed and varies only the *pretraining domain*. That
|
|
12
|
+
isolates the question "does pathology-pretraining actually pay off?" for a
|
|
13
|
+
downstream task (e.g. cell detection).
|
|
14
|
+
|
|
15
|
+
Structurally it is a plain :class:`TimmTileEncoder` (mirroring ``lunit`` /
|
|
16
|
+
``prost40m`` / ``uni``): the dense (``encode_tiles_dense``) and attention
|
|
17
|
+
(``encode_tiles_attention``) paths are inherited unchanged from the timm ViT
|
|
18
|
+
base, so the control is dense-extraction- and attention-capable exactly like the
|
|
19
|
+
pathology encoders. ``dynamic_img_size=True`` lets the (natively 518px) backbone
|
|
20
|
+
run at the 224px detection tile geometry via positional-embedding interpolation,
|
|
21
|
+
a no-op at the native size (verified in the shared dense-extraction suite).
|
|
22
|
+
|
|
23
|
+
Spacing note: a natural-image model has **no** intrinsic micron-per-pixel
|
|
24
|
+
spacing, so it declares ``supported_spacing_um=None`` — it is *spacing-agnostic*
|
|
25
|
+
and :func:`validate_encoder_config` never rejects a requested spacing for it
|
|
26
|
+
(unlike the pathology encoders, which are validated at a specific spacing). It
|
|
27
|
+
still needs *a* spacing to tile a slide, so ``default_spacing_um=0.5`` sets the
|
|
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.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from slide2vec.encoders.base import TimmTileEncoder
|
|
36
|
+
from slide2vec.encoders.registry import register_encoder
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@register_encoder(
|
|
40
|
+
"dinov2-vitb14",
|
|
41
|
+
output_variants={"default": {"encode_dim": 768}},
|
|
42
|
+
default_output_variant="default",
|
|
43
|
+
input_size=224,
|
|
44
|
+
patch_size=14,
|
|
45
|
+
supported_spacing_um=None, # spacing-agnostic: no intrinsic µm/px, so no validation constraint
|
|
46
|
+
default_spacing_um=0.5, # tiling default: match the pathology encoders' task-spacing
|
|
47
|
+
precision="fp16",
|
|
48
|
+
source="timm/vit_base_patch14_dinov2.lvd142m",
|
|
49
|
+
)
|
|
50
|
+
class DINOv2ViTB14(TimmTileEncoder):
|
|
51
|
+
def __init__(self, *, output_variant: str | None = None):
|
|
52
|
+
super().__init__(
|
|
53
|
+
"vit_base_patch14_dinov2.lvd142m",
|
|
54
|
+
output_variant=output_variant,
|
|
55
|
+
dynamic_img_size=True, # enable dense extraction; no-op at native size
|
|
56
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""GenBio-PathFM tile encoder.
|
|
2
|
+
|
|
3
|
+
GenBio-PathFM (GenBio AI, 2024; ``genbio-ai/genbio-pathfm``) is a 1.1B-param ViT
|
|
4
|
+
histopathology tile encoder (JEDI = JEPA + DINO training on public data). It is
|
|
5
|
+
loaded via HF ``AutoModel(trust_remote_code=True)`` (auto_map ->
|
|
6
|
+
``GenBioPathFMModel``), so it is a custom :class:`TileEncoder`, not a
|
|
7
|
+
``TimmTileEncoder``. Weights are openly downloadable (safetensors); released
|
|
8
|
+
under the custom **GenBio AI Community License** (not OSI-approved — read it for
|
|
9
|
+
acceptable-use / commercial restrictions; slide2vec only wraps user-downloaded
|
|
10
|
+
weights and does not redistribute them).
|
|
11
|
+
|
|
12
|
+
Output dim (4608, verified on real weights): the backbone is a *single-channel*
|
|
13
|
+
ViT (``in_chans=1``, ``embed_dim=1536``). The model's canonical ``forward`` takes
|
|
14
|
+
an RGB ``[B, 3, H, W]`` tensor, treats each colour channel as a separate
|
|
15
|
+
single-channel image, encodes all three, and concatenates the three per-channel
|
|
16
|
+
CLS tokens -> ``[B, embed_dim * 3] = [B, 4608]``. This per-channel-CLS
|
|
17
|
+
concatenation is the model's intrinsic design (matching the HF card's advertised
|
|
18
|
+
feature dimension of 4608), not an ad-hoc CLS+patch pooling; ``encode_tiles``
|
|
19
|
+
therefore returns the model's default ``forward`` output directly. (Confirmed by
|
|
20
|
+
running a ``(1, 3, 224, 224)`` dummy through the real weights -> shape
|
|
21
|
+
``(1, 4608)``; see ``tests/test_gpfm_genbio_heavy.py``.)
|
|
22
|
+
|
|
23
|
+
Normalization is **non-ImageNet** (``config.json`` ``image_mean`` / ``image_std``)
|
|
24
|
+
and must be set explicitly.
|
|
25
|
+
|
|
26
|
+
Dense (spatial-grid) extraction is supported via the model's
|
|
27
|
+
``forward_with_patches`` (a DINOv2-style ``x_norm_patchtokens`` path): it returns
|
|
28
|
+
the fused per-channel patch tokens ``(B, T, 4608)`` — the three single-channel
|
|
29
|
+
patch-token grids concatenated along the feature dim, with the prefix tokens
|
|
30
|
+
(CLS + storage tokens) already stripped — which fold straight into a
|
|
31
|
+
``(B, 4608, h, w)`` grid. The patch size is 16 (a 224 tile -> a 14x14 = 196 grid).
|
|
32
|
+
|
|
33
|
+
Attention-map extraction is **not** supported. The backbone computes attention
|
|
34
|
+
with a fused ``F.scaled_dot_product_attention`` (no materialized weights, no
|
|
35
|
+
``output_attentions``), and — more fundamentally — it encodes the three colour
|
|
36
|
+
channels as three independent single-channel images, so there is no single
|
|
37
|
+
coherent CLS-over-patches attention to extract: any "attention grid" would be
|
|
38
|
+
three separate grayscale-channel attentions. Recovering it would need a bespoke
|
|
39
|
+
per-channel recompute path that diverges from the shared timm/HF attention
|
|
40
|
+
helpers, so GenBio deliberately opts out of ``encode_tiles_attention``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from typing import Callable
|
|
44
|
+
|
|
45
|
+
import torch
|
|
46
|
+
from torch import Tensor
|
|
47
|
+
from torchvision.transforms import v2
|
|
48
|
+
|
|
49
|
+
from slide2vec.encoders.base import (
|
|
50
|
+
TileEncoder,
|
|
51
|
+
preferred_default_device,
|
|
52
|
+
reshape_tokens_to_grid,
|
|
53
|
+
resolve_requested_output_variant,
|
|
54
|
+
)
|
|
55
|
+
from slide2vec.encoders.registry import register_encoder
|
|
56
|
+
|
|
57
|
+
_HF_REPO_ID = "genbio-ai/genbio-pathfm"
|
|
58
|
+
# Non-ImageNet normalization from the model's config.json (image_mean / image_std).
|
|
59
|
+
_GENBIO_MEAN = (0.697, 0.575, 0.728)
|
|
60
|
+
_GENBIO_STD = (0.188, 0.240, 0.187)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@register_encoder(
|
|
64
|
+
"genbio-pathfm",
|
|
65
|
+
# encode_dim 4608 = embed_dim (1536) x 3 colour channels; patch_size 16 (a 224
|
|
66
|
+
# tile -> a 14x14 = 196 patch-token grid) — see module docstring.
|
|
67
|
+
output_variants={"default": {"encode_dim": 4608}},
|
|
68
|
+
default_output_variant="default",
|
|
69
|
+
input_size=224,
|
|
70
|
+
patch_size=16,
|
|
71
|
+
supported_spacing_um=0.5, # 20x; card states no magnification, house default
|
|
72
|
+
precision="fp32", # upstream runs plain fp32, no autocast
|
|
73
|
+
source="genbio-ai/genbio-pathfm",
|
|
74
|
+
)
|
|
75
|
+
class GenBioPathFM(TileEncoder):
|
|
76
|
+
def __init__(self, *, output_variant: str | None = None):
|
|
77
|
+
from transformers import AutoModel
|
|
78
|
+
|
|
79
|
+
self._model = AutoModel.from_pretrained(_HF_REPO_ID, trust_remote_code=True).eval()
|
|
80
|
+
self._device = preferred_default_device()
|
|
81
|
+
self._output_variant = resolve_requested_output_variant(output_variant)
|
|
82
|
+
|
|
83
|
+
def get_transform(self) -> Callable:
|
|
84
|
+
# Mirrors the model card: Resize to an exact 224x224 (tuple form, as the
|
|
85
|
+
# card's ``Resize((224, 224))``) + custom Normalize. No CenterCrop: the card
|
|
86
|
+
# has none, and after a square resize it would be a no-op anyway.
|
|
87
|
+
return v2.Compose([
|
|
88
|
+
v2.ToImage(),
|
|
89
|
+
v2.Resize((224, 224), interpolation=v2.InterpolationMode.BICUBIC, antialias=True),
|
|
90
|
+
v2.ToDtype(torch.float32, scale=True),
|
|
91
|
+
v2.Normalize(mean=_GENBIO_MEAN, std=_GENBIO_STD),
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
def get_dense_transform(self) -> Callable:
|
|
95
|
+
# Normalization only — no Resize/CenterCrop (see TileEncoder.get_dense_transform),
|
|
96
|
+
# so the dense grid stays registered to the full source tile.
|
|
97
|
+
return v2.Compose([
|
|
98
|
+
v2.ToImage(),
|
|
99
|
+
v2.ToDtype(torch.float32, scale=True),
|
|
100
|
+
v2.Normalize(mean=_GENBIO_MEAN, std=_GENBIO_STD),
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
def encode_tiles(self, batch: Tensor) -> Tensor:
|
|
104
|
+
# Canonical forward: per-channel CLS tokens concatenated -> (B, 4608).
|
|
105
|
+
return self._model(batch)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def patch_size(self) -> tuple[int, int]:
|
|
109
|
+
return (16, 16)
|
|
110
|
+
|
|
111
|
+
def encode_tiles_dense(self, batch: Tensor) -> Tensor:
|
|
112
|
+
"""Encode tiles into a dense spatial grid. (B, C, H, W) -> (B, d, h, w).
|
|
113
|
+
|
|
114
|
+
Uses the model's ``forward_with_patches`` (DINOv2-style), which returns the
|
|
115
|
+
fused per-channel patch tokens ``(B, T, 4608)`` — the three single-channel
|
|
116
|
+
patch-token grids concatenated along the feature dim, with the prefix
|
|
117
|
+
tokens (CLS + storage tokens) already stripped — then folds that token
|
|
118
|
+
sequence back into its spatial grid (``num_prefix_tokens=0``). ``H, W`` must
|
|
119
|
+
be divisible by the patch size; non-224 inputs rely on the backbone's
|
|
120
|
+
DINOv2 positional-embedding interpolation.
|
|
121
|
+
"""
|
|
122
|
+
if batch.ndim != 4:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
"encode_tiles_dense expects a (B, C, H, W) batch, got shape "
|
|
125
|
+
f"{tuple(batch.shape)}."
|
|
126
|
+
)
|
|
127
|
+
_, _, height, width = batch.shape
|
|
128
|
+
patch_h, patch_w = self.patch_size
|
|
129
|
+
if height % patch_h != 0 or width % patch_w != 0:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Dense extraction for '{type(self).__name__}' requires input "
|
|
132
|
+
f"divisible by the patch size: got {height}x{width}, patch "
|
|
133
|
+
f"{patch_h}x{patch_w}. Pad the tile up to a patch multiple first."
|
|
134
|
+
)
|
|
135
|
+
_, patch_tokens = self._model.forward_with_patches(batch)
|
|
136
|
+
return reshape_tokens_to_grid(
|
|
137
|
+
patch_tokens,
|
|
138
|
+
grid_h=height // patch_h,
|
|
139
|
+
grid_w=width // patch_w,
|
|
140
|
+
num_prefix_tokens=0,
|
|
141
|
+
encoder_name=type(self).__name__,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def encode_dim(self) -> int:
|
|
146
|
+
return 4608
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def device(self) -> torch.device:
|
|
150
|
+
return self._device
|
|
151
|
+
|
|
152
|
+
def to(self, device: torch.device | str) -> "GenBioPathFM":
|
|
153
|
+
self._device = torch.device(device)
|
|
154
|
+
self._model = self._model.to(self._device)
|
|
155
|
+
return self
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""GPFM tile encoder (Generalizable Pathology Foundation Model).
|
|
2
|
+
|
|
3
|
+
GPFM (Ma et al., 2024; ``birkhoffkiki/GPFM``) is a ``ViT-L/14`` DINOv2 tile
|
|
4
|
+
encoder (embedding dim 1024, patch_size 14). Weights are openly available under
|
|
5
|
+
the MIT license at ``majiabo/GPFM`` but ship as a standalone ``GPFM.pth``
|
|
6
|
+
checkpoint, so — unlike ``hf-hub:`` timm presets — we build the timm arch
|
|
7
|
+
unpretrained and ``load_state_dict`` the downloaded checkpoint (the MOOZY
|
|
8
|
+
loading pattern).
|
|
9
|
+
|
|
10
|
+
The published ``GPFM.pth`` is a bare DINOv2 ``state_dict`` (``cls_token``,
|
|
11
|
+
``pos_embed``, ``blocks.*``, …) that loads into
|
|
12
|
+
``vit_large_patch14_dinov2.lvd142m`` with ``strict=True`` and zero missing /
|
|
13
|
+
unexpected keys (verified on the real weights; see
|
|
14
|
+
``tests/test_gpfm_genbio_heavy.py``). The loader below still defensively unwraps
|
|
15
|
+
common checkpoint wrappers (``{"model": ...}`` / ``{"teacher": ...}`` /
|
|
16
|
+
``{"student": ...}`` / ``{"state_dict": ...}``) and strips ``module.`` /
|
|
17
|
+
``backbone.`` prefixes so a re-exported checkpoint keeps loading strictly.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Mapping
|
|
21
|
+
|
|
22
|
+
import torch
|
|
23
|
+
from huggingface_hub import hf_hub_download
|
|
24
|
+
|
|
25
|
+
from slide2vec.encoders.base import TimmTileEncoder
|
|
26
|
+
from slide2vec.encoders.registry import register_encoder
|
|
27
|
+
|
|
28
|
+
_HF_REPO_ID = "majiabo/GPFM"
|
|
29
|
+
_HF_CHECKPOINT = "GPFM.pth"
|
|
30
|
+
_CHECKPOINT_WRAPPER_KEYS = ("model", "teacher", "student", "state_dict", "teacher_backbone")
|
|
31
|
+
_STATE_DICT_PREFIXES = ("module.", "backbone.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _unwrap_gpfm_state_dict(payload: Mapping[str, object]) -> dict[str, torch.Tensor]:
|
|
35
|
+
"""Reduce a GPFM checkpoint payload to a bare ``state_dict``.
|
|
36
|
+
|
|
37
|
+
Unwraps a single common wrapper key if present, then strips ``module.`` /
|
|
38
|
+
``backbone.`` prefixes off every key. A no-op on the published bare
|
|
39
|
+
``GPFM.pth`` (no wrapper, no prefixes), so it preserves strict loading there
|
|
40
|
+
while tolerating a re-wrapped/prefixed re-export.
|
|
41
|
+
"""
|
|
42
|
+
state: object = payload
|
|
43
|
+
for wrapper in _CHECKPOINT_WRAPPER_KEYS:
|
|
44
|
+
if isinstance(state, Mapping) and wrapper in state and isinstance(state[wrapper], Mapping):
|
|
45
|
+
state = state[wrapper]
|
|
46
|
+
break
|
|
47
|
+
if not isinstance(state, Mapping):
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Unexpected GPFM checkpoint payload: expected a state_dict mapping, got {type(state)}"
|
|
50
|
+
)
|
|
51
|
+
cleaned: dict[str, torch.Tensor] = {}
|
|
52
|
+
for key, value in state.items():
|
|
53
|
+
name = key
|
|
54
|
+
for prefix in _STATE_DICT_PREFIXES:
|
|
55
|
+
if name.startswith(prefix):
|
|
56
|
+
name = name[len(prefix):]
|
|
57
|
+
cleaned[name] = value
|
|
58
|
+
return cleaned
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@register_encoder(
|
|
62
|
+
"gpfm",
|
|
63
|
+
output_variants={"default": {"encode_dim": 1024}},
|
|
64
|
+
default_output_variant="default",
|
|
65
|
+
input_size=224,
|
|
66
|
+
patch_size=14,
|
|
67
|
+
supported_spacing_um=0.5, # 512px@0.25um (128um FOV) resized to 224 => ~0.5um/px effective (20x), as for UNI
|
|
68
|
+
precision="fp32", # upstream runs plain fp32, no autocast
|
|
69
|
+
source="majiabo/GPFM",
|
|
70
|
+
)
|
|
71
|
+
class GPFM(TimmTileEncoder):
|
|
72
|
+
def __init__(self, *, output_variant: str | None = None):
|
|
73
|
+
super().__init__(
|
|
74
|
+
"vit_large_patch14_dinov2.lvd142m",
|
|
75
|
+
output_variant=output_variant,
|
|
76
|
+
pretrained=False,
|
|
77
|
+
img_size=224,
|
|
78
|
+
init_values=1e-5,
|
|
79
|
+
dynamic_img_size=True,
|
|
80
|
+
)
|
|
81
|
+
checkpoint_path = hf_hub_download(repo_id=_HF_REPO_ID, filename=_HF_CHECKPOINT)
|
|
82
|
+
payload = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
|
|
83
|
+
if not isinstance(payload, Mapping):
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"Invalid GPFM checkpoint payload: expected a dict, got {type(payload)}"
|
|
86
|
+
)
|
|
87
|
+
state_dict = _unwrap_gpfm_state_dict(payload)
|
|
88
|
+
self._model.load_state_dict(state_dict, strict=True)
|
|
89
|
+
self._model.eval()
|
|
@@ -151,6 +151,7 @@ class _HibouBase(TileEncoder):
|
|
|
151
151
|
output_variants={"default": {"encode_dim": 768}},
|
|
152
152
|
default_output_variant="default",
|
|
153
153
|
input_size=224,
|
|
154
|
+
patch_size=14,
|
|
154
155
|
supported_spacing_um=0.5,
|
|
155
156
|
precision="fp16",
|
|
156
157
|
source="histai/hibou-b",
|
|
@@ -167,6 +168,7 @@ class HibouB(_HibouBase):
|
|
|
167
168
|
output_variants={"default": {"encode_dim": 1024}},
|
|
168
169
|
default_output_variant="default",
|
|
169
170
|
input_size=224,
|
|
171
|
+
patch_size=14,
|
|
170
172
|
supported_spacing_um=0.5,
|
|
171
173
|
precision="fp16",
|
|
172
174
|
source="histai/hibou-L",
|
|
@@ -72,6 +72,7 @@ class _HOptimusBase(TimmTileEncoder):
|
|
|
72
72
|
output_variants={"default": {"encode_dim": 1536}},
|
|
73
73
|
default_output_variant="default",
|
|
74
74
|
input_size=224,
|
|
75
|
+
patch_size=14,
|
|
75
76
|
supported_spacing_um=0.5,
|
|
76
77
|
precision="fp16",
|
|
77
78
|
source="bioptimus/H-optimus-0",
|
|
@@ -108,6 +109,7 @@ class HOptimus0(TimmTileEncoder):
|
|
|
108
109
|
output_variants={"default": {"encode_dim": 1536}},
|
|
109
110
|
default_output_variant="default",
|
|
110
111
|
input_size=224,
|
|
112
|
+
patch_size=14,
|
|
111
113
|
supported_spacing_um=0.5,
|
|
112
114
|
precision="fp16",
|
|
113
115
|
source="bioptimus/H-optimus-1",
|
|
@@ -145,6 +147,7 @@ class HOptimus1(TimmTileEncoder):
|
|
|
145
147
|
},
|
|
146
148
|
default_output_variant="cls_patch_mean",
|
|
147
149
|
input_size=224,
|
|
150
|
+
patch_size=14,
|
|
148
151
|
supported_spacing_um=0.5,
|
|
149
152
|
precision="fp16",
|
|
150
153
|
source="bioptimus/H0-mini",
|
|
@@ -9,6 +9,7 @@ from slide2vec.encoders.registry import register_encoder
|
|
|
9
9
|
output_variants={"default": {"encode_dim": 384}},
|
|
10
10
|
default_output_variant="default",
|
|
11
11
|
input_size=224,
|
|
12
|
+
patch_size=8,
|
|
12
13
|
supported_spacing_um=0.5,
|
|
13
14
|
precision="fp32",
|
|
14
15
|
source="1aurent/vit_small_patch8_224.lunit_dino",
|
|
@@ -27,6 +27,7 @@ from slide2vec.encoders.registry import register_encoder
|
|
|
27
27
|
output_variants={"default": {"encode_dim": 3072}},
|
|
28
28
|
default_output_variant="default",
|
|
29
29
|
input_size=224,
|
|
30
|
+
patch_size=14,
|
|
30
31
|
supported_spacing_um=[0.25, 0.5, 1.0, 2.0],
|
|
31
32
|
precision="fp16",
|
|
32
33
|
source="kaiko-ai/midnight",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""mSTAR tile encoder implementation.
|
|
2
|
+
|
|
3
|
+
mSTAR (Wang et al., 2024; ``Innse/mSTAR``) is released as a ``ViT-L/16`` patch
|
|
4
|
+
encoder, not a slide aggregator: the published checkpoint is a per-tile feature
|
|
5
|
+
extractor and slide2vec handles WSI -> coordinates -> per-tile features itself.
|
|
6
|
+
We therefore register it as a **tile** encoder.
|
|
7
|
+
|
|
8
|
+
The weights live in the **gated** Hugging Face repo `Wangyh/mSTAR`
|
|
9
|
+
(``hf-hub:Wangyh/mSTAR``). Loading them requires access approval on Hugging Face
|
|
10
|
+
and an ``HF_TOKEN`` in the environment.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from slide2vec.encoders.base import TimmTileEncoder
|
|
14
|
+
from slide2vec.encoders.registry import register_encoder
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@register_encoder(
|
|
18
|
+
"mstar",
|
|
19
|
+
output_variants={"default": {"encode_dim": 1024}},
|
|
20
|
+
default_output_variant="default",
|
|
21
|
+
input_size=224,
|
|
22
|
+
patch_size=16,
|
|
23
|
+
supported_spacing_um=0.5, # 256px @ 20x, resized to 224 (per paper)
|
|
24
|
+
precision="fp32", # upstream runs plain fp32, no autocast
|
|
25
|
+
source="Wangyh/mSTAR",
|
|
26
|
+
)
|
|
27
|
+
class mSTAR(TimmTileEncoder):
|
|
28
|
+
def __init__(self, *, output_variant: str | None = None):
|
|
29
|
+
super().__init__(
|
|
30
|
+
"hf-hub:Wangyh/mSTAR",
|
|
31
|
+
output_variant=output_variant,
|
|
32
|
+
init_values=1e-5,
|
|
33
|
+
dynamic_img_size=True,
|
|
34
|
+
)
|
|
@@ -148,6 +148,7 @@ class _PhikonBase(TileEncoder):
|
|
|
148
148
|
output_variants={"default": {"encode_dim": 768}},
|
|
149
149
|
default_output_variant="default",
|
|
150
150
|
input_size=224,
|
|
151
|
+
patch_size=16,
|
|
151
152
|
supported_spacing_um=0.5,
|
|
152
153
|
precision="fp32",
|
|
153
154
|
source="owkin/phikon",
|
|
@@ -165,6 +166,7 @@ class Phikon(_PhikonBase):
|
|
|
165
166
|
output_variants={"default": {"encode_dim": 1024}},
|
|
166
167
|
default_output_variant="default",
|
|
167
168
|
input_size=224,
|
|
169
|
+
patch_size=16,
|
|
168
170
|
supported_spacing_um=0.5,
|
|
169
171
|
precision="fp32",
|
|
170
172
|
source="owkin/phikon-v2",
|