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.
Files changed (108) hide show
  1. {slide2vec-5.1.1 → slide2vec-5.3.0}/PKG-INFO +2 -2
  2. {slide2vec-5.1.1 → slide2vec-5.3.0}/README.md +1 -1
  3. {slide2vec-5.1.1 → slide2vec-5.3.0}/pyproject.toml +2 -2
  4. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/__init__.py +1 -1
  5. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/api.py +11 -1
  6. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/artifacts.py +22 -0
  7. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/configs/default.yaml +1 -0
  8. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/__init__.py +4 -0
  9. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/__init__.py +8 -0
  10. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/conch.py +2 -0
  11. slide2vec-5.3.0/slide2vec/encoders/models/dinov2.py +56 -0
  12. slide2vec-5.3.0/slide2vec/encoders/models/genbio.py +155 -0
  13. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/gigapath.py +1 -0
  14. slide2vec-5.3.0/slide2vec/encoders/models/gpfm.py +89 -0
  15. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/hibou.py +2 -0
  16. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/hoptimus.py +3 -0
  17. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/lunit.py +1 -0
  18. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/midnight.py +1 -0
  19. slide2vec-5.3.0/slide2vec/encoders/models/mstar.py +34 -0
  20. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/musk.py +1 -0
  21. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/phikon.py +2 -0
  22. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/prost40m.py +1 -0
  23. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/uni.py +2 -0
  24. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/virchow.py +2 -0
  25. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/registry.py +117 -26
  26. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/inference.py +16 -0
  27. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/dense_regions.py +3 -3
  28. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/embedding.py +12 -6
  29. slide2vec-5.3.0/slide2vec/runtime/model_settings.py +101 -0
  30. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/patient_pipeline.py +9 -2
  31. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/serialization.py +3 -0
  32. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/PKG-INFO +2 -2
  33. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/SOURCES.txt +7 -0
  34. slide2vec-5.3.0/tests/test_dinov2_natimage.py +116 -0
  35. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_encoder_registry.py +51 -0
  36. slide2vec-5.3.0/tests/test_gpfm_genbio_heavy.py +64 -0
  37. slide2vec-5.3.0/tests/test_patch_size_metadata.py +213 -0
  38. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_regression_core.py +168 -0
  39. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_regression_inference.py +5 -0
  40. slide2vec-5.1.1/slide2vec/runtime/model_settings.py +0 -48
  41. {slide2vec-5.1.1 → slide2vec-5.3.0}/LICENSE +0 -0
  42. {slide2vec-5.1.1 → slide2vec-5.3.0}/setup.cfg +0 -0
  43. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/__main__.py +0 -0
  44. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/cli.py +0 -0
  45. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/configs/__init__.py +0 -0
  46. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/configs/resources.py +0 -0
  47. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/data/__init__.py +0 -0
  48. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/data/dataset.py +0 -0
  49. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/data/tile_reader.py +0 -0
  50. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/data/tile_store.py +0 -0
  51. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/distributed/__init__.py +0 -0
  52. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/distributed/direct_embed_worker.py +0 -0
  53. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/distributed/pipeline_worker.py +0 -0
  54. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/base.py +0 -0
  55. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/__init__.py +0 -0
  56. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/blocks.py +0 -0
  57. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/case.py +0 -0
  58. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/loading.py +0 -0
  59. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/slide.py +0 -0
  60. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/moozy/types.py +0 -0
  61. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/prism.py +0 -0
  62. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/models/titan.py +0 -0
  63. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/encoders/validation.py +0 -0
  64. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/progress.py +0 -0
  65. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/__init__.py +0 -0
  66. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/artifacts_collect.py +0 -0
  67. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/batching.py +0 -0
  68. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/cpu_budget.py +0 -0
  69. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/dense_sliding.py +0 -0
  70. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/distributed.py +0 -0
  71. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/distributed_stage.py +0 -0
  72. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/embedding_persist.py +0 -0
  73. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/embedding_pipeline.py +0 -0
  74. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/hierarchical.py +0 -0
  75. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/manifest.py +0 -0
  76. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/persist_callbacks.py +0 -0
  77. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/persistence.py +0 -0
  78. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/process_list.py +0 -0
  79. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/progress_bridge.py +0 -0
  80. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/registry.py +0 -0
  81. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/slide_encode.py +0 -0
  82. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/tiling.py +0 -0
  83. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/tiling_pipeline.py +0 -0
  84. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/types.py +0 -0
  85. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/runtime/worker_io.py +0 -0
  86. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/__init__.py +0 -0
  87. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/config.py +0 -0
  88. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/coordinates.py +0 -0
  89. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/log_utils.py +0 -0
  90. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/tiling_io.py +0 -0
  91. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec/utils/utils.py +0 -0
  92. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/dependency_links.txt +0 -0
  93. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/entry_points.txt +0 -0
  94. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/not-zip-safe +0 -0
  95. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/requires.txt +0 -0
  96. {slide2vec-5.1.1 → slide2vec-5.3.0}/slide2vec.egg-info/top_level.txt +0 -0
  97. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_architecture_runtime_split.py +0 -0
  98. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_attention_extraction.py +0 -0
  99. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_dense_extraction.py +0 -0
  100. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_dense_regions.py +0 -0
  101. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_dense_sliding.py +0 -0
  102. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_hs2p_package_cutover.py +0 -0
  103. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_output_consistency.py +0 -0
  104. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_progress.py +0 -0
  105. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_regression_models.py +0 -0
  106. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_runtime_batching.py +0 -0
  107. {slide2vec-5.1.1 → slide2vec-5.3.0}/tests/test_tile_store.py +0 -0
  108. {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.1.1
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 17 tile-level models and 3 slide-level models.
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 17 tile-level models and 3 slide-level models.
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.1.1"
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.1.1"
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
@@ -11,7 +11,7 @@ from slide2vec.api import (
11
11
  from slide2vec.artifacts import HierarchicalEmbeddingArtifact, SlideEmbeddingArtifact, TileEmbeddingArtifact
12
12
 
13
13
 
14
- __version__ = "5.1.1"
14
+ __version__ = "5.3.0"
15
15
 
16
16
  __all__ = [
17
17
  "Model",
@@ -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 canonicalize_model_name, normalize_precision_name
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
@@ -27,6 +27,7 @@ _GIGAPATH_STD = (0.229, 0.224, 0.225)
27
27
  output_variants={"default": {"encode_dim": 1536}},
28
28
  default_output_variant="default",
29
29
  input_size=256,
30
+ patch_size=14,
30
31
  supported_spacing_um=0.5,
31
32
  precision="fp16",
32
33
  source="prov-gigapath/prov-gigapath",
@@ -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
+ )
@@ -35,6 +35,7 @@ def _as_hw(value: int | tuple[int, int] | list[int]) -> tuple[int, int]:
35
35
  },
36
36
  default_output_variant="ms_aug",
37
37
  input_size=384,
38
+ patch_size=16,
38
39
  supported_spacing_um=[0.25, 0.5, 1.0],
39
40
  precision="fp16",
40
41
  source="xiangjx/musk",
@@ -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",
@@ -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=16,
12
13
  supported_spacing_um=0.5,
13
14
  precision="fp32",
14
15
  source="waticlems/Prost40M",