slide2vec 5.3.0__tar.gz → 5.4.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.3.0 → slide2vec-5.4.0}/PKG-INFO +3 -3
- {slide2vec-5.3.0 → slide2vec-5.4.0}/pyproject.toml +4 -4
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/__init__.py +12 -2
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/api.py +129 -2
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/artifacts.py +119 -2
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/configs/default.yaml +1 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/data/tile_reader.py +88 -12
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/distributed/__init__.py +34 -66
- slide2vec-5.4.0/slide2vec/distributed/dense_worker.py +98 -0
- slide2vec-5.4.0/slide2vec/distributed/direct_embed_worker.py +194 -0
- slide2vec-5.4.0/slide2vec/distributed/pipeline_worker.py +111 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/base.py +15 -13
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/conch.py +4 -2
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/dinov2.py +1 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/genbio.py +3 -2
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/gigapath.py +7 -1
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/gpfm.py +1 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/hibou.py +3 -1
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/hoptimus.py +19 -1
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/lunit.py +1 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/midnight.py +2 -1
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/mstar.py +1 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/musk.py +2 -1
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/phikon.py +4 -2
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/prost40m.py +1 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/uni.py +2 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/virchow.py +34 -5
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/registry.py +34 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/inference.py +45 -6
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/progress.py +41 -4
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/batching.py +26 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/cpu_budget.py +1 -1
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/dense_regions.py +88 -51
- slide2vec-5.4.0/slide2vec/runtime/dense_shard.py +314 -0
- slide2vec-5.4.0/slide2vec/runtime/dense_stage.py +299 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/distributed.py +15 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/distributed_stage.py +5 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/embedding.py +21 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/embedding_persist.py +8 -3
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/embedding_pipeline.py +12 -6
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/patient_pipeline.py +2 -1
- slide2vec-5.4.0/slide2vec/runtime/pooled_encoder_input.py +102 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/process_list.py +2 -2
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/progress_bridge.py +1 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/serialization.py +50 -1
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/tiling.py +7 -4
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/types.py +1 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/utils/config.py +9 -9
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/utils/tiling_io.py +11 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec.egg-info/PKG-INFO +3 -3
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec.egg-info/SOURCES.txt +9 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec.egg-info/requires.txt +2 -2
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_dense_extraction.py +6 -6
- slide2vec-5.4.0/tests/test_dense_regions.py +450 -0
- slide2vec-5.4.0/tests/test_dense_shard.py +453 -0
- slide2vec-5.4.0/tests/test_dense_stage.py +274 -0
- slide2vec-5.4.0/tests/test_dense_worker.py +94 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_hs2p_package_cutover.py +32 -3
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_output_consistency.py +12 -65
- slide2vec-5.4.0/tests/test_pooled_encoder_input.py +513 -0
- slide2vec-5.4.0/tests/test_pooled_geometry.py +279 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_progress.py +168 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_regression_core.py +61 -3
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_regression_inference.py +87 -57
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_regression_models.py +5 -5
- slide2vec-5.3.0/slide2vec/distributed/direct_embed_worker.py +0 -192
- slide2vec-5.3.0/slide2vec/distributed/pipeline_worker.py +0 -113
- slide2vec-5.3.0/tests/test_dense_regions.py +0 -264
- {slide2vec-5.3.0 → slide2vec-5.4.0}/LICENSE +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/README.md +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/setup.cfg +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/__main__.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/cli.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/configs/__init__.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/configs/resources.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/data/__init__.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/data/dataset.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/data/tile_store.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/__init__.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/__init__.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/moozy/__init__.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/moozy/blocks.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/moozy/case.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/moozy/loading.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/moozy/slide.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/moozy/types.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/prism.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/models/titan.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/encoders/validation.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/__init__.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/artifacts_collect.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/dense_sliding.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/hierarchical.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/manifest.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/model_settings.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/persist_callbacks.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/persistence.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/registry.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/slide_encode.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/tiling_pipeline.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/runtime/worker_io.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/utils/__init__.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/utils/coordinates.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/utils/log_utils.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec/utils/utils.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec.egg-info/dependency_links.txt +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec.egg-info/entry_points.txt +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec.egg-info/not-zip-safe +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/slide2vec.egg-info/top_level.txt +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_architecture_runtime_split.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_attention_extraction.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_dense_sliding.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_dinov2_natimage.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_encoder_registry.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_gpfm_genbio_heavy.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_patch_size_metadata.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_runtime_batching.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.0}/tests/test_tile_store.py +0 -0
- {slide2vec-5.3.0 → slide2vec-5.4.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.4.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
|
|
@@ -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.
|
|
18
|
+
Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.3.0
|
|
19
19
|
Requires-Dist: omegaconf
|
|
20
20
|
Requires-Dist: matplotlib
|
|
21
21
|
Requires-Dist: numpy<2
|
|
@@ -65,7 +65,7 @@ Requires-Dist: numpy<2; extra == "fm"
|
|
|
65
65
|
Requires-Dist: pandas; extra == "fm"
|
|
66
66
|
Requires-Dist: pillow; extra == "fm"
|
|
67
67
|
Requires-Dist: rich; extra == "fm"
|
|
68
|
-
Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.
|
|
68
|
+
Requires-Dist: hs2p[asap,cucim,openslide,sam2,vips]>=4.3.0; extra == "fm"
|
|
69
69
|
Requires-Dist: wandb; extra == "fm"
|
|
70
70
|
Requires-Dist: torch<2.8,>=2.3; extra == "fm"
|
|
71
71
|
Requires-Dist: torchvision>=0.18.0; extra == "fm"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slide2vec"
|
|
7
|
-
version = "5.
|
|
7
|
+
version = "5.4.0"
|
|
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.
|
|
24
|
+
"hs2p[asap,cucim,openslide,sam2,vips]>=4.3.0",
|
|
25
25
|
"omegaconf",
|
|
26
26
|
"matplotlib",
|
|
27
27
|
"numpy<2",
|
|
@@ -88,7 +88,7 @@ fm = [
|
|
|
88
88
|
"pandas",
|
|
89
89
|
"pillow",
|
|
90
90
|
"rich",
|
|
91
|
-
"hs2p[asap,cucim,openslide,sam2,vips]>=4.
|
|
91
|
+
"hs2p[asap,cucim,openslide,sam2,vips]>=4.3.0",
|
|
92
92
|
"wandb",
|
|
93
93
|
"torch>=2.3,<2.8",
|
|
94
94
|
"torchvision>=0.18.0",
|
|
@@ -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.4.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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from slide2vec.api import (
|
|
2
|
+
DenseOptions,
|
|
2
3
|
EmbeddedPatient,
|
|
3
4
|
EmbeddedSlide,
|
|
4
5
|
ExecutionOptions,
|
|
@@ -6,18 +7,26 @@ from slide2vec.api import (
|
|
|
6
7
|
Pipeline,
|
|
7
8
|
PreprocessingConfig,
|
|
8
9
|
RunResult,
|
|
10
|
+
SlideRegions,
|
|
9
11
|
list_models,
|
|
10
12
|
)
|
|
11
|
-
from slide2vec.artifacts import
|
|
13
|
+
from slide2vec.artifacts import (
|
|
14
|
+
DenseRegionArtifact,
|
|
15
|
+
HierarchicalEmbeddingArtifact,
|
|
16
|
+
SlideEmbeddingArtifact,
|
|
17
|
+
TileEmbeddingArtifact,
|
|
18
|
+
)
|
|
12
19
|
|
|
13
20
|
|
|
14
|
-
__version__ = "5.
|
|
21
|
+
__version__ = "5.4.0"
|
|
15
22
|
|
|
16
23
|
__all__ = [
|
|
17
24
|
"Model",
|
|
18
25
|
"list_models",
|
|
19
26
|
"Pipeline",
|
|
20
27
|
"PreprocessingConfig",
|
|
28
|
+
"DenseOptions",
|
|
29
|
+
"SlideRegions",
|
|
21
30
|
"ExecutionOptions",
|
|
22
31
|
"RunResult",
|
|
23
32
|
"EmbeddedPatient",
|
|
@@ -25,5 +34,6 @@ __all__ = [
|
|
|
25
34
|
"SlideEmbeddingArtifact",
|
|
26
35
|
"HierarchicalEmbeddingArtifact",
|
|
27
36
|
"TileEmbeddingArtifact",
|
|
37
|
+
"DenseRegionArtifact",
|
|
28
38
|
"__version__",
|
|
29
39
|
]
|
|
@@ -11,6 +11,7 @@ import torch
|
|
|
11
11
|
from hs2p import SlideSpec
|
|
12
12
|
|
|
13
13
|
from slide2vec.artifacts import (
|
|
14
|
+
DenseRegionArtifact,
|
|
14
15
|
HierarchicalEmbeddingArtifact,
|
|
15
16
|
PatientEmbeddingArtifact,
|
|
16
17
|
SlideEmbeddingArtifact,
|
|
@@ -28,6 +29,7 @@ from slide2vec.runtime.model_settings import (
|
|
|
28
29
|
)
|
|
29
30
|
from slide2vec.progress import emit_progress
|
|
30
31
|
from slide2vec.runtime.types import LoadedModel
|
|
32
|
+
from slide2vec.runtime.pooled_encoder_input import PooledEncoderInputPlan
|
|
31
33
|
from slide2vec.utils.utils import cpu_worker_limit, slurm_cpu_limit
|
|
32
34
|
|
|
33
35
|
PathLike = str | Path
|
|
@@ -101,6 +103,13 @@ class PreprocessingConfig:
|
|
|
101
103
|
#: Slide reading backend. ``"auto"`` tries cucim → openslide → vips in order.
|
|
102
104
|
#: Explicit choices: ``"cucim"``, ``"openslide"``, ``"vips"``, ``"asap"``.
|
|
103
105
|
backend: str = "auto"
|
|
106
|
+
#: Source-mask reading backend, resolved independently from the *mask* path
|
|
107
|
+
#: (hs2p ≥ 4.3.0). ``"auto"`` probes openability just like :attr:`backend`. Set this
|
|
108
|
+
#: explicitly (e.g. ``"openslide"``) when a precomputed tissue or annotation mask needs
|
|
109
|
+
#: a different decoder than its slide — hs2p no longer silently falls back to another
|
|
110
|
+
#: reader, so a mask the slide backend cannot decode fails unless overridden here.
|
|
111
|
+
#: Accepts the same values as :attr:`backend`; ignored for slides with no source mask.
|
|
112
|
+
mask_backend: str = "auto"
|
|
104
113
|
#: Target spacing in µm/px. Resolved from the model preset when ``None``.
|
|
105
114
|
requested_spacing_um: float | None = None
|
|
106
115
|
#: Tile side length in pixels at *requested_spacing_um*.
|
|
@@ -181,6 +190,7 @@ class PreprocessingConfig:
|
|
|
181
190
|
region_tile_multiple = getattr(tiling.params, "region_tile_multiple", None)
|
|
182
191
|
return cls(
|
|
183
192
|
backend=tiling.backend,
|
|
193
|
+
mask_backend=getattr(tiling, "mask_backend", "auto"),
|
|
184
194
|
requested_spacing_um=float(tiling.params.requested_spacing_um),
|
|
185
195
|
requested_tile_size_px=int(tiling.params.requested_tile_size_px),
|
|
186
196
|
requested_region_size_px=int(region_size_px) if region_size_px is not None else None,
|
|
@@ -208,6 +218,9 @@ class PreprocessingConfig:
|
|
|
208
218
|
def with_backend(self, backend: str) -> "PreprocessingConfig":
|
|
209
219
|
return replace(self, backend=backend)
|
|
210
220
|
|
|
221
|
+
def with_mask_backend(self, mask_backend: str) -> "PreprocessingConfig":
|
|
222
|
+
return replace(self, mask_backend=mask_backend)
|
|
223
|
+
|
|
211
224
|
|
|
212
225
|
|
|
213
226
|
@dataclass(frozen=True, kw_only=True)
|
|
@@ -312,6 +325,63 @@ class ExecutionOptions:
|
|
|
312
325
|
return replace(self, output_dir=Path(output_dir))
|
|
313
326
|
|
|
314
327
|
|
|
328
|
+
@dataclass(frozen=True, kw_only=True)
|
|
329
|
+
class DenseOptions:
|
|
330
|
+
"""Dense ``(d, gh, gw)`` grid extraction settings (issue #217).
|
|
331
|
+
|
|
332
|
+
The dense counterpart of the pooled :class:`PreprocessingConfig`: it names the
|
|
333
|
+
extraction geometry (spacing → level, supervision ``target_size``, padding) and the
|
|
334
|
+
dense encode knobs (whole-tile vs sliding-window, patch grid vs CLS-attention). Unlike
|
|
335
|
+
the pooled path there is no tiling — the caller supplies ROI coordinates directly (see
|
|
336
|
+
:class:`SlideRegions`) — so a ``DenseOptions`` carries only what slide2vec needs to read
|
|
337
|
+
and encode each ROI. ``ExecutionOptions`` is reused unchanged for output/precision/GPUs.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
#: Target spacing in µm/px the ROI is read at (resolved to a pyramid level per slide).
|
|
341
|
+
spacing_um: float
|
|
342
|
+
#: Supervision tile side length in pixels at *spacing_um* (the dense grid registers to it).
|
|
343
|
+
target_size: int
|
|
344
|
+
#: Relative spacing tolerance for pyramid level selection.
|
|
345
|
+
tolerance: float = 0.05
|
|
346
|
+
#: Slide reading backend. ``"auto"`` resolves per slide (cucim → openslide → vips).
|
|
347
|
+
backend: str = "auto"
|
|
348
|
+
#: Padding mode used to pad the tile up to the encoder's patch multiple.
|
|
349
|
+
#: One of ``"reflect"`` / ``"replicate"`` / ``"constant"`` / ``"zero"``.
|
|
350
|
+
pad_mode: str = "reflect"
|
|
351
|
+
#: Constant fill value for ``pad_mode in {"constant", "zero"}`` (ignored otherwise).
|
|
352
|
+
image_pad_value: float | None = None
|
|
353
|
+
#: Encoder field-of-view chunk fed through the backbone per forward. ``None`` (default)
|
|
354
|
+
#: is one whole-tile forward; a smaller value slides the encoder and blends token grids.
|
|
355
|
+
window_size: int | None = None
|
|
356
|
+
#: Fractional window overlap in ``[0, 1)`` for the sliding path (ignored when
|
|
357
|
+
#: ``window_size is None``).
|
|
358
|
+
overlap: float = 0.0
|
|
359
|
+
#: ``"patch_features"`` (the patch-token grid) or ``"cls_attention"`` (CLS/register
|
|
360
|
+
#: self-attention grid).
|
|
361
|
+
feature_kind: str = "patch_features"
|
|
362
|
+
#: Transformer blocks whose CLS attention is read (``cls_attention`` only).
|
|
363
|
+
attention_blocks: tuple[int, ...] = (-1,)
|
|
364
|
+
#: Include register-token query rows as extra attention channels (``cls_attention`` only).
|
|
365
|
+
attention_include_registers: bool = False
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@dataclass(frozen=True, kw_only=True)
|
|
369
|
+
class SlideRegions:
|
|
370
|
+
"""One slide's ROIs for dense extraction: ``(sample_id, image_path, coordinates, annotation)``.
|
|
371
|
+
|
|
372
|
+
The dense input unit soma's slide-manifest path hands to
|
|
373
|
+
:meth:`Model.embed_regions_dense`. ``coordinates`` is an ``(N, 2)`` array of level-0
|
|
374
|
+
top-left ``(x, y)`` pixel coordinates; each ROI is read + encoded into one persisted
|
|
375
|
+
``(d, gh, gw)`` grid named ``<x>_<y>.pt``. ``annotation`` namespaces the output under a
|
|
376
|
+
per-class subdirectory (reusing the pooled convention); ``None`` is the flat layout.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
sample_id: str
|
|
380
|
+
image_path: PathLike
|
|
381
|
+
coordinates: Any
|
|
382
|
+
annotation: str | None = None
|
|
383
|
+
|
|
384
|
+
|
|
315
385
|
@dataclass(frozen=True, kw_only=True)
|
|
316
386
|
class RunResult:
|
|
317
387
|
"""Return value of :meth:`Pipeline.run`."""
|
|
@@ -374,6 +444,8 @@ class EmbeddedSlide:
|
|
|
374
444
|
tiling_preview_path: Path | None = None
|
|
375
445
|
#: Encoder latent representations when available; ``None`` otherwise.
|
|
376
446
|
latents: Any | None = None
|
|
447
|
+
#: Factual square tensor side length immediately before tile encoding.
|
|
448
|
+
encoder_input_size_px: int | None = None
|
|
377
449
|
|
|
378
450
|
|
|
379
451
|
class Model:
|
|
@@ -391,6 +463,8 @@ class Model:
|
|
|
391
463
|
self.allow_non_recommended_settings = bool(allow_non_recommended_settings)
|
|
392
464
|
self._output_variant = output_variant
|
|
393
465
|
self._backend: LoadedModel | None = None
|
|
466
|
+
self._pooled_input_plan: PooledEncoderInputPlan | None = None
|
|
467
|
+
self._backend_pooled_input_plan: PooledEncoderInputPlan | None = None
|
|
394
468
|
|
|
395
469
|
@classmethod
|
|
396
470
|
def from_preset(
|
|
@@ -588,8 +662,58 @@ class Model:
|
|
|
588
662
|
execution=resolved,
|
|
589
663
|
)
|
|
590
664
|
|
|
665
|
+
def embed_regions_dense(
|
|
666
|
+
self,
|
|
667
|
+
regions: "Sequence[SlideRegions]",
|
|
668
|
+
*,
|
|
669
|
+
dense: "DenseOptions",
|
|
670
|
+
execution: ExecutionOptions | None = None,
|
|
671
|
+
) -> list[DenseRegionArtifact]:
|
|
672
|
+
"""Extract + persist a dense ``(d, gh, gw)`` grid per caller-supplied ROI.
|
|
673
|
+
|
|
674
|
+
The dense counterpart of the pooled coordinate path: each ``SlideRegions`` names a
|
|
675
|
+
slide + a set of level-0 ROI coordinates, and every ROI is read, encoded through the
|
|
676
|
+
dense transform, and written to ``dense_embeddings/[<class>/]<sample_id>/<x>_<y>.pt``
|
|
677
|
+
plus a geometry sidecar. The run splits its ROIs across all visible GPUs
|
|
678
|
+
(``execution.num_gpus``); ``num_gpus=1`` encodes fully in-process. Resume is
|
|
679
|
+
automatic — ROIs whose sidecar already exists are skipped. Returns one
|
|
680
|
+
:class:`~slide2vec.artifacts.DenseRegionArtifact` per input ROI.
|
|
681
|
+
"""
|
|
682
|
+
from slide2vec.runtime.dense_stage import embed_regions_dense
|
|
683
|
+
|
|
684
|
+
resolved = _coerce_execution_options(execution, model=self)
|
|
685
|
+
_require_output_dir_for_persistence(resolved, method_name="Model.embed_regions_dense(...)")
|
|
686
|
+
with _auto_progress_reporting(output_dir=resolved.output_dir):
|
|
687
|
+
return embed_regions_dense(self, regions, dense=dense, execution=resolved)
|
|
688
|
+
|
|
689
|
+
def _prepare_pooled_input(
|
|
690
|
+
self,
|
|
691
|
+
preprocessing: PreprocessingConfig,
|
|
692
|
+
*,
|
|
693
|
+
emit_run_info: bool,
|
|
694
|
+
) -> PooledEncoderInputPlan:
|
|
695
|
+
plan = PooledEncoderInputPlan.resolve(
|
|
696
|
+
self.name,
|
|
697
|
+
requested_tile_size_px=int(preprocessing.requested_tile_size_px),
|
|
698
|
+
allow_non_recommended_settings=self.allow_non_recommended_settings,
|
|
699
|
+
)
|
|
700
|
+
self._pooled_input_plan = plan
|
|
701
|
+
if emit_run_info and plan.requires_variable_model_input:
|
|
702
|
+
logging.getLogger("slide2vec").info(
|
|
703
|
+
"Pooled encoder input for '%s': preset %dpx, requested %dpx, "
|
|
704
|
+
"exact encoder input %dpx; using normalization-only preprocessing.",
|
|
705
|
+
self.name,
|
|
706
|
+
plan.preset_input_size_px,
|
|
707
|
+
plan.requested_tile_size_px,
|
|
708
|
+
plan.expected_encoder_input_size_px,
|
|
709
|
+
)
|
|
710
|
+
return plan
|
|
711
|
+
|
|
591
712
|
def _load_backend(self) -> LoadedModel:
|
|
592
|
-
if
|
|
713
|
+
if (
|
|
714
|
+
self._backend is None
|
|
715
|
+
or self._backend_pooled_input_plan != self._pooled_input_plan
|
|
716
|
+
):
|
|
593
717
|
from slide2vec.inference import load_model
|
|
594
718
|
|
|
595
719
|
emit_progress("model.loading", model_name=self.name)
|
|
@@ -597,7 +721,10 @@ class Model:
|
|
|
597
721
|
name=self.name,
|
|
598
722
|
device=self._requested_device,
|
|
599
723
|
output_variant=self._output_variant,
|
|
724
|
+
allow_non_recommended_settings=self.allow_non_recommended_settings,
|
|
725
|
+
pooled_input_plan=self._pooled_input_plan,
|
|
600
726
|
)
|
|
727
|
+
self._backend_pooled_input_plan = self._pooled_input_plan
|
|
601
728
|
emit_progress("model.ready", model_name=self.name, device=str(self._backend.device))
|
|
602
729
|
return self._backend
|
|
603
730
|
|
|
@@ -838,12 +965,12 @@ def _validate_model_config(
|
|
|
838
965
|
info = encoder_registry.info(name)
|
|
839
966
|
if info["level"] != "tile":
|
|
840
967
|
raise ValueError("Hierarchical preprocessing is only supported for tile encoders")
|
|
968
|
+
model._prepare_pooled_input(preprocessing, emit_run_info=True)
|
|
841
969
|
# Skip precision validation for CPU execution (fp32 is always valid on CPU).
|
|
842
970
|
on_cpu = model._requested_device == "cpu"
|
|
843
971
|
precision = None if on_cpu or execution is None else execution.precision
|
|
844
972
|
validate_encoder_config(
|
|
845
973
|
name,
|
|
846
|
-
requested_tile_size_px=preprocessing.requested_tile_size_px,
|
|
847
974
|
requested_spacing_um=preprocessing.requested_spacing_um,
|
|
848
975
|
precision=precision,
|
|
849
976
|
output_variant=model._output_variant,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
import json
|
|
3
|
-
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path, PureWindowsPath
|
|
4
5
|
from typing import Any
|
|
6
|
+
from uuid import uuid4
|
|
5
7
|
|
|
6
8
|
import numpy as np
|
|
7
9
|
import torch
|
|
@@ -69,6 +71,30 @@ class HierarchicalEmbeddingArtifact:
|
|
|
69
71
|
return load_metadata(self.metadata_path)
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
@dataclass(frozen=True, kw_only=True)
|
|
75
|
+
class DenseRegionArtifact:
|
|
76
|
+
"""One persisted dense ROI grid: the ``(d, gh, gw)`` payload + its geometry sidecar.
|
|
77
|
+
|
|
78
|
+
Dense emits one *directory* per slide (``dense_embeddings/[<class>/]<sample_id>/``) and
|
|
79
|
+
one ``<x>_<y>.pt`` / ``<x>_<y>.meta.json`` pair per ROI — the counterpart of the pooled
|
|
80
|
+
one-file-per-slide artifacts. Named from what slide2vec knows (slide + level-0 top-left
|
|
81
|
+
coordinate); soma maps its ROI ``sample_id`` back onto ``(x, y)``.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
sample_id: str
|
|
85
|
+
x: int
|
|
86
|
+
y: int
|
|
87
|
+
path: Path
|
|
88
|
+
metadata_path: Path
|
|
89
|
+
feature_dim: int
|
|
90
|
+
grid_shape: tuple[int, int]
|
|
91
|
+
annotation: str | None = None
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def metadata(self) -> dict[str, Any]:
|
|
95
|
+
return load_metadata(self.metadata_path)
|
|
96
|
+
|
|
97
|
+
|
|
72
98
|
def _validate_output_format(output_format: str) -> str:
|
|
73
99
|
normalized = output_format.lower()
|
|
74
100
|
if normalized not in {"pt", "npz"}:
|
|
@@ -112,7 +138,15 @@ def _ensure_tensor(data: Any):
|
|
|
112
138
|
|
|
113
139
|
def _write_metadata(path: Path, metadata: dict[str, Any]) -> None:
|
|
114
140
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
-
path.
|
|
141
|
+
tmp_path = path.with_name(f".{path.name}.tmp-{os.getpid()}-{uuid4().hex}")
|
|
142
|
+
try:
|
|
143
|
+
tmp_path.write_text(
|
|
144
|
+
json.dumps(metadata, indent=2, sort_keys=True),
|
|
145
|
+
encoding="utf-8",
|
|
146
|
+
)
|
|
147
|
+
os.replace(tmp_path, path)
|
|
148
|
+
finally:
|
|
149
|
+
tmp_path.unlink(missing_ok=True)
|
|
116
150
|
|
|
117
151
|
|
|
118
152
|
def tile_embeddings_subdir(annotation: str | None) -> str:
|
|
@@ -162,6 +196,89 @@ def hierarchical_embeddings_subdir(annotation: str | None) -> str:
|
|
|
162
196
|
return f"hierarchical_embeddings/{annotation}"
|
|
163
197
|
|
|
164
198
|
|
|
199
|
+
def _validate_path_component(value: str, *, field: str) -> str:
|
|
200
|
+
component = str(value)
|
|
201
|
+
if (
|
|
202
|
+
not component
|
|
203
|
+
or component in {".", ".."}
|
|
204
|
+
or "/" in component
|
|
205
|
+
or "\\" in component
|
|
206
|
+
or "\x00" in component
|
|
207
|
+
or PureWindowsPath(component).drive
|
|
208
|
+
):
|
|
209
|
+
raise ValueError(f"{field} must be a non-empty filesystem path component")
|
|
210
|
+
return component
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def dense_embeddings_subdir(annotation: str | None) -> str:
|
|
214
|
+
"""Namespace the ``dense_embeddings`` output dir per annotation class.
|
|
215
|
+
|
|
216
|
+
Reuses hs2p's flatten rule (the single source of truth, shared with
|
|
217
|
+
:func:`tile_embeddings_subdir` and the other pooled subdir helpers): ``None`` and the
|
|
218
|
+
sentinel ``"tissue"`` collapse to the flat ``dense_embeddings`` root; any real class
|
|
219
|
+
label gets its own ``dense_embeddings/<class>`` subdirectory.
|
|
220
|
+
"""
|
|
221
|
+
if is_flattened_annotation(annotation):
|
|
222
|
+
return "dense_embeddings"
|
|
223
|
+
annotation_component = _validate_path_component(annotation, field="annotation")
|
|
224
|
+
return f"dense_embeddings/{annotation_component}"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def region_dense_paths(
|
|
228
|
+
output_dir: str | Path, *, sample_id: str, annotation: str | None, x: int, y: int
|
|
229
|
+
) -> tuple[Path, Path]:
|
|
230
|
+
"""``(payload_path, sidecar_path)`` for one ROI: ``.../<sample_id>/<x>_<y>.{pt,meta.json}``.
|
|
231
|
+
|
|
232
|
+
Geometry-independent (named only from slide + level-0 ``(x, y)`` + class), so the resume
|
|
233
|
+
check can test sidecar existence before any slide is opened.
|
|
234
|
+
"""
|
|
235
|
+
output_root = Path(output_dir).expanduser().resolve()
|
|
236
|
+
sample_component = _validate_path_component(sample_id, field="sample_id")
|
|
237
|
+
slide_dir = (output_root / dense_embeddings_subdir(annotation) / sample_component).resolve()
|
|
238
|
+
if not slide_dir.is_relative_to(output_root):
|
|
239
|
+
raise ValueError("Dense artifact path must stay within output_dir")
|
|
240
|
+
stem = f"{int(x)}_{int(y)}"
|
|
241
|
+
return slide_dir / f"{stem}.pt", slide_dir / f"{stem}.meta.json"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def write_dense_region(
|
|
245
|
+
grid,
|
|
246
|
+
*,
|
|
247
|
+
output_dir: str | Path,
|
|
248
|
+
sample_id: str,
|
|
249
|
+
annotation: str | None,
|
|
250
|
+
x: int,
|
|
251
|
+
y: int,
|
|
252
|
+
metadata: dict[str, Any],
|
|
253
|
+
) -> DenseRegionArtifact:
|
|
254
|
+
"""Persist one ``(d, gh, gw)`` grid + its geometry sidecar, atomically and sidecar-last.
|
|
255
|
+
|
|
256
|
+
Write order (D6): payload to a temp file in the destination directory → ``os.replace``
|
|
257
|
+
into ``<x>_<y>.pt`` (atomic on the same filesystem) → then the ``<x>_<y>.meta.json``
|
|
258
|
+
sidecar. A payload present without its sidecar therefore unambiguously means an
|
|
259
|
+
incomplete ROI, and resume treats the sidecar as the done-marker.
|
|
260
|
+
"""
|
|
261
|
+
payload_path, metadata_path = region_dense_paths(
|
|
262
|
+
output_dir, sample_id=sample_id, annotation=annotation, x=x, y=y
|
|
263
|
+
)
|
|
264
|
+
payload_path.parent.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
grid_array = _ensure_array(grid)
|
|
266
|
+
tmp_path = payload_path.with_name(f"{payload_path.name}.tmp-{os.getpid()}")
|
|
267
|
+
torch.save(_ensure_tensor(grid), tmp_path)
|
|
268
|
+
os.replace(tmp_path, payload_path)
|
|
269
|
+
_write_metadata(metadata_path, metadata)
|
|
270
|
+
return DenseRegionArtifact(
|
|
271
|
+
sample_id=sample_id,
|
|
272
|
+
x=int(x),
|
|
273
|
+
y=int(y),
|
|
274
|
+
path=payload_path,
|
|
275
|
+
metadata_path=metadata_path,
|
|
276
|
+
feature_dim=int(grid_array.shape[0]),
|
|
277
|
+
grid_shape=(int(grid_array.shape[1]), int(grid_array.shape[2])),
|
|
278
|
+
annotation=annotation,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
165
282
|
def _setup_artifact_paths(
|
|
166
283
|
output_dir: str | Path, subdir: str, sample_id: str, output_format: str
|
|
167
284
|
) -> tuple[Path, Path]:
|
|
@@ -26,6 +26,7 @@ tiling:
|
|
|
26
26
|
read_coordinates_from: # path to an existing directory containing pre-extracted `.coordinates.npz` / `.coordinates.meta.json` artifacts to reuse instead of starting tiling from scratch
|
|
27
27
|
read_tiles_from: # path to an existing directory containing pre-extracted `.tiles.tar` tile stores to reuse instead of starting tiling from scratch
|
|
28
28
|
backend: "auto" # backend to use for slide reading; "auto" lets hs2p resolve the best backend per slide, preferring cuCIM when available
|
|
29
|
+
mask_backend: "auto" # backend for reading source masks (precomputed tissue / annotation), resolved independently from the mask path; "auto" probes openability like `backend`. Set explicitly (e.g. "openslide") when a mask needs a different decoder than its slide — hs2p (>=4.3.0) no longer silently falls back
|
|
29
30
|
independent_sampling: true # selection strategy when annotation sampling is active. true: sample each class independently against its own binary mask (independent selection); false: sample once over the union of active classes, then post-filter per class by coverage (joint selection). Ignored when the masks vocabulary is left at the tissue-only default.
|
|
30
31
|
masks:
|
|
31
32
|
# Annotation-mask vocabulary forwarded to hs2p's sampling resolver. The shipped default
|