Preprocessing

Use PreprocessingConfig to choose slide readers, tile geometry, tissue segmentation, annotation sampling, and previews. Preprocessing requires hs2p 6.1.0 or newer.

PreprocessingConfig holds hs2p’s config dataclasses as given:

  • tiling — hs2p.TilingConfig: spacing, tile size, tolerance, overlap, backend, mask_backend.

  • segmentation — hs2p.SegmentationConfig.

  • filter — hs2p.FilterConfig.

  • preview — hs2p.PreviewConfig.

  • masks — hs2p.MaskSamplingConfig.

slide2vec resolves two of them when they are None:

  • tiling=None (the default) takes requested_spacing_um and requested_tile_size_px from the model preset, with tolerance=0.05, overlap=0.0, and both backends "auto". Models with several supported spacings, such as Virchow2, have no preset spacing: pass a TilingConfig.

  • filter=None (the default) uses hs2p’s filter defaults with ref_tile_size set to the resolved tile size.

from hs2p import TilingConfig
from slide2vec import PreprocessingConfig

preprocessing = PreprocessingConfig(
    tiling=TilingConfig(
        requested_spacing_um=0.5,
        requested_tile_size_px=224,
        tolerance=0.05,
        overlap=0.0,
        backend="openslide",
    ),
)

A YAML config sets the same values under tiling (see CLI Guide); hs2p’s resolvers build the dataclasses and reject unknown keys.

Backends

tiling.backend controls which slide-reading library is used:

  • "auto" — tries cucim → vips → openslide → asap for WSI inputs and picks the first backend that can open and decode the path

  • "cucim" — NVIDIA cuCIM for supported slide formats, including SVS and TIFF

  • "openslide" — broad format support, CPU-only

  • "vips" — libvips, good for large TIFF files

  • "asap" — ASAP reader (requires separate installation)

Slides without resolution metadata require spacing_at_level_0. This includes untagged TIFFs opened with VIPS: hs2p 5 raises an error instead of assuming a spacing of 1000 µm/px.

Importing cuCIM opens NVIDIA’s cuFile (GPUDirect Storage) driver. Without the nvidia-fs kernel module that driver falls back to a compatibility mode which holds an idle CUDA context of about 520 MiB on every visible GPU, although reads then go through ordinary file I/O. import slide2vec therefore points CUFILE_ENV_PATH_JSON at a bundled config that disallows compatibility mode: cuCIM reads the same pixels and no context is created. Machines with nvidia-fs loaded keep GPUDirect Storage. To use your own cuFile config, set CUFILE_ENV_PATH_JSON before importing slide2vec; an existing value is kept.

tiling.mask_backend controls the reader used for source masks — precomputed tissue masks and annotation masks — and accepts the same values, plus "pil" for PNG/JPEG masks (selected directly by "auto") and "tifffile" for lossless label reads. "auto" selects "tifffile" for TIFF masks that store samples other than 8-bit unsigned (for example 16-bit labels), which the other readers would convert. It is resolved independently from the mask path, so a mask can use a different decoder than its slide. "auto" skips a backend that can open a mask but not decode it, such as cuCIM with a deflate-compressed label TIFF. An explicit mask_backend is used as given. It defaults to "auto" and is ignored for slides with no source mask.

The auto priority can change when hs2p or the installed backend set changes. Set tiling.backend and tiling.mask_backend explicitly when decoder selection must stay stable across upgrades.

Source masks

Continue passing source masks through mask_path and selecting their reader with mask_backend. hs2p 5 opens and aligns the mask internally; slide2vec callers do not need to construct hs2p.Mask objects.

Masks may have a different resolution from their slide, but must cover the same field of view at one scale, within one mask pixel per axis. Flat PNG/JPEG and untagged TIFF masks need no spacing metadata: hs2p derives their spacing from the slide dimensions. An incompatible shape or a spacing tag more than 5% from that derived spacing fails preprocessing. A tag differing by 1–5% emits a warning and the derived spacing is used.

Tissue masks must contain only the declared background and tissue IDs (0 and 1 by default); undeclared values such as 255 now fail instead of being treated as background. Annotation masks must likewise match masks.pixel_mapping. Multi-channel masks are accepted only when all channels are identical. These checks also apply to previews, and failures are recorded in process_list.csv.

Coordinate artifacts retain their field names. mask_spacing_um now records the effective, dimension-derived spacing of the mask level read. Mask level selection uses a fixed 1% tolerance independently of tiling.tolerance; mask_level and mask_spacing_um can therefore differ from hs2p 4.x runs.

Pooled Tile Geometry

Pooled extraction uses three distinct pixel sizes, recorded in tile and hierarchical artifact sidecars:

  • read_tile_size_px — the raw square read from the selected WSI pyramid level.

  • requested_tile_size_px — the tile after geometry correction. This is also the encoder input: declared runs apply only the encoder’s photometric preprocessing (no resize or center crop).

  • encoder_input_size_px — the tensor side length the encoder received. Equals requested_tile_size_px for declared runs.

The default requested_tile_size_px is the registry input_size. A different size requires allow_non_recommended_settings=True, an encoder that supports variable input, and a size divisible by the model’s patch geometry. See Model Zoo for the given-input recipes and the sampling change from earlier releases.

Tissue Segmentation

segmentation is an hs2p.SegmentationConfig passed to hs2p‘s segmentation pipeline. The default uses method="hsv". method selects the algorithm:

  • hsv - heuristic based on the HSV colour space. Fast and robust for H&E slides.

  • otsu - thresholds the saturation channel using Otsu’s method.

  • threshold - applies a fixed saturation threshold.

  • sam2 - runs the AtlasPatch SAM2 tissue segmentation model on an internal 8.0 µm/px thumbnail. Requires the atlaspatch package. sam2_device selects "cpu" (the default), "cuda", or a CUDA device such as "cuda:0".

Example:

from hs2p import SegmentationConfig
from slide2vec import Model, PreprocessingConfig

model = Model.from_preset("conch")
preprocessing = PreprocessingConfig(
    segmentation=SegmentationConfig(method="sam2", sam2_device="cuda"),
)
embedded = model.embed_slide("/path/to/slide.svs", preprocessing=preprocessing)

Or in a YAML config:

tiling:
  seg_params:
    method: "sam2"
    sam2_device: "cuda"

Annotation-Aware Sampling

By default slide2vec tiles tissue: the masks vocabulary is the binary {background: 0, tissue: 1}, and embeddings cover tiles that pass the sampling filters. To restrict extraction to specific annotated classes (tumor-only, stroma-only, …), customize the masks block. Any divergence from the default vocabulary opts the run into annotation-aware sampling; the plain tissue path is otherwise unchanged.

In annotation mode the per-slide mask_path (argument or manifest column) is required and is read as a multi-label raster: each class occupies a distinct integer pixel value. masks is an hs2p.MaskSamplingConfig and is used as given. PreprocessingConfig().masks is the default; derive from it with dataclasses.replace(). Its fields:

  • pixel_mapping — {class_name: pixel value}. Values must be distinct integers in [0, 255]; merged is a reserved name. A class may list several raster values ({"tumor": (2, 3)}): they are sampled as one class whose coverage is their sum, and a value may appear under one class only.

  • min_coverage — {class_name: float}; the minimum fraction of a tile covered by that class to keep it. Only listed classes are sampled. min_coverage["tissue"] is the tissue threshold. The default is hs2p’s, 0.01, for both the Python API and YAML/CLI runs.

  • colors — {class_name: (r, g, b) | None} used when rendering previews; lists every class of pixel_mapping.

  • output_mode — "per_annotation" (one artifact set per sampled class) or "merged" (one set per slide over the union of tiles passing any class). Patient-level models accept only "merged"; "per_annotation" raises a ValueError before embedding.

  • strategy — "joint" (the default) samples once over the union, then keeps tiles per class by coverage; "independent" samples each class against its own mask.

Tumor-only bag features. Add a tumor class and sample only it:

from dataclasses import replace

from slide2vec import Model, PreprocessingConfig

model = Model.from_preset("conch")
default_masks = PreprocessingConfig().masks
preprocessing = PreprocessingConfig(
    masks=replace(
        default_masks,
        pixel_mapping={**default_masks.pixel_mapping, "tumor": 2},  # tumor pixels == 2
        colors={**default_masks.colors, "tumor": (255, 0, 0)},
        min_coverage={"tumor": 0.5},
    ),
)

slide = {
    "sample_id": "slide-1",
    "image_path": "/path/to/slide.svs",
    "mask_path": "/path/to/annotation_mask.tif",  # multi-label raster
}
embedded = model.embed_slide(slide, preprocessing=preprocessing)

assert embedded.annotation == "tumor"  # every bag is stamped with its label
tumor_bag = embedded.tile_embeddings   # shape (N_tumor, D) — tumor tiles only

Because this run produces exactly one bag, bare embed_slide(...) returns it directly. Requesting a class the run did not produce raises a ValueError listing the available bags.

Multiple classes per slide. Give each class its own min_coverage and use a Pipeline so per-class artifacts are persisted under a <class>/ subdirectory (e.g. tile_embeddings/tumor/<sample_id>.pt):

from slide2vec import ExecutionOptions, Pipeline

preprocessing = PreprocessingConfig(
    masks=replace(
        default_masks,
        pixel_mapping={**default_masks.pixel_mapping, "tumor": 2, "stroma": 3},
        colors={**default_masks.colors, "tumor": (255, 0, 0), "stroma": (0, 0, 255)},
        min_coverage={"tumor": 0.5, "stroma": 0.5},
    ),
)
pipeline = Pipeline(
    model=model,
    preprocessing=preprocessing,
    execution=ExecutionOptions(output_dir="outputs/run"),
)

# The manifest's mask_path column points at each slide's multi-label raster.
result = pipeline.run(manifest_path="/path/to/slides.csv")

This fans out per (sample_id, annotation) across tile, slide, and hierarchical embeddings — and across GPUs — recording each class’s own feature_path in process_list.csv. See Output Layout for the namespaced directory structure.

The merged output mode. With masks.output_mode="merged" the run does not fan out per class: it samples the union of tiles passing any class and produces one bag per slide, labelled "merged". On disk it lands at the flat output root (tile_embeddings/<sample_id>.pt) with no <class>/ subdirectory, exactly like the default tissue case:

merged_preprocessing = replace(
    preprocessing,
    masks=replace(preprocessing.masks, output_mode="merged"),
)
results = model.embed_slides([slide], preprocessing=merged_preprocessing)
merged = results["slide-1"]["merged"]

# A single bag is also available directly:
merged = model.embed_slide(slide, preprocessing=merged_preprocessing)
assert merged.annotation == "merged"

Working with several classes in memory. embed_slides returns a nested mapping {sample_id: {label: EmbeddedSlide}} where the inner key is each bag’s annotation label (a class name, "tissue", or "merged"; never None). Every EmbeddedSlide is also stamped with its annotation:

results = model.embed_slides(
    [slide],
    preprocessing=preprocessing,   # tumor + stroma configuration above
)

tumor_bag = results["slide-1"]["tumor"].tile_embeddings    # shape (N_tumor, D)
stroma_bag = results["slide-1"]["stroma"].tile_embeddings  # shape (N_stroma, D)

Pass annotations=[...] to restrict the inner keys to the classes you care about; omit it to receive every bag the run produced:

results = model.embed_slides(
    [slide], preprocessing=preprocessing, annotations=["tumor"],
)
results["slide-1"].keys()  # dict_keys(['tumor']) — stroma is dropped

Selecting bags with embed_slide. embed_slide returns one bag (or a list of bags) for a single slide via the annotation selector:

# One class → one EmbeddedSlide.
tumor = model.embed_slide(slide, preprocessing=preprocessing, annotation="tumor")

# A list of classes → a list of EmbeddedSlide in the requested order.
tumor, stroma = model.embed_slide(
    slide, preprocessing=preprocessing, annotation=["tumor", "stroma"],
)

Bare embed_slide(slide) returns the single bag when the run produced exactly one; if the run fanned out into several bags it raises a ValueError naming the available bags and directing you to embed_slides, so per-class results are never silently dropped.

Preview Images

slide2vec can write a tissue mask preview and a tiling preview for each slide — useful for quality control. slide2vec’s default preview enables both. This disables only the tiling preview:

from dataclasses import replace

default_preview = PreprocessingConfig().preview
preprocessing = PreprocessingConfig(
    preview=replace(default_preview, save_tiling_preview=False),
)

Preview images are written to <output_dir>/preview/mask/<sample_id>.jpg and <output_dir>/preview/tiling/<sample_id>.jpg for tissue or merged output. Per-class tiling previews add a <class>/ subdirectory below preview/tiling/; the mask preview is shared across classes. Paths are also recorded in process_list.csv and on the returned EmbeddedSlide (mask_preview_path, tiling_preview_path).

When resuming a run, existing preview paths are preserved in process_list.csv if the preview files still exist on disk.

Field reference

class slide2vec.PreprocessingConfig(*, tiling=None, segmentation=<factory>, filter=None, preview=<factory>, masks=<factory>, requested_region_size_px=None, region_tile_multiple=None, read_coordinates_from=None, read_tiles_from=None, on_the_fly=True, save_tiles=False, gpu_decode=False, adaptive_batching=False, use_supertiles=True, jpeg_backend='pil', num_cucim_workers=4, resume=False)

Bases: object

Configuration for slide tiling and preprocessing.

tiling, segmentation, filter, preview and masks hold hs2p’s config dataclasses as given. The other fields are slide2vec’s.

tiling: TilingConfig | None = None

hs2p tiling geometry and slide/mask backends. None resolves when the model is known: requested_spacing_um and requested_tile_size_px from the model preset, tolerance=0.05, overlap=0.0, backend and mask_backend "auto".

segmentation: SegmentationConfig

hs2p tissue segmentation. The default uses method="hsv".

filter: FilterConfig | None = None

hs2p contour and tile filtering. None resolves to hs2p’s filter defaults with ref_tile_size equal to the resolved tiling.requested_tile_size_px.

preview: PreviewConfig

hs2p previews. The default saves the mask and tiling previews.

masks: MaskSamplingConfig

hs2p annotation-mask vocabulary and sampling. The default is hs2p’s DEFAULT_MASK_SAMPLING: binary tissue tiling ({background: 0, tissue: 1}) with min_coverage={"tissue": 0.01}. See Preprocessing.

requested_region_size_px: int | None = None

Parent region side length in pixels (hierarchical mode). Auto-derived as requested_tile_size_px × region_tile_multiple when None.

region_tile_multiple: int | None = None

Region grid width/height in tiles (e.g. 6 → 6×6 = 36 tiles per region). Enables hierarchical extraction when set; must be ≥ 2.

read_coordinates_from: Path | None = None

Directory containing pre-extracted tile coordinates to reuse, skipping tiling.

read_tiles_from: Path | None = None

Directory containing pre-extracted tile images to skip the tiling step entirely.

on_the_fly: bool = True

Read and decode tiles on demand rather than pre-loading into memory.

save_tiles: bool = False

Write the extracted tiles to tiles/<sample_id>.tiles.tar next to the coordinate artifacts. Tiles are also written when on_the_fly is False and read_tiles_from is unset, because embedding then reads them from the archive.

gpu_decode: bool = False

Decode tiles on the GPU via CuCIM / nvImageCodec when True.

adaptive_batching: bool = False

Dynamically adjust batch size based on tile count.

use_supertiles: bool = True

Group adjacent tiles into supertile batches for faster I/O.

jpeg_backend: str = 'pil'

JPEG encoder for extracted tile archives — portable "pil" (default) or explicitly requested "turbojpeg".

num_cucim_workers: int = 4

Number of CuCIM reader threads.

resume: bool = False

Skip slides already present in the output directory when True.