Converting images to OME-TIFF#

spatialrefinery.convert_to_ometiff converts a whole-slide or microscopy image into a pyramidal OME-TIFF: a tiled, multi-resolution TIFF that viewers (and SpatialData) can read efficiently at any zoom level without loading the full-resolution image into memory.

It dispatches on the input file’s suffix to a registered converter – OpenSlideImageConverter for .svs, .ndpi, .tif, .tiff, .mrxs, .scn, .bif, .vms, .svslide, BioioImageConverter for .czi (which needs the optional czi extra), and SlideioImageConverter for .vsi, .qptiff, .zvi, .afi (no extra needed). What you need: one whole-slide image file. Runtime: proportional to image size; pyramid generation is the dominant cost.

from pathlib import Path

from spatialrefinery import convert_to_ometiff
from spatialrefinery.core.registry import RegistryError, get_converter_for, list_converters

Registered converters#

list_converters() shows every suffix with a registered converter, and which class handles it:

list_converters()
{'.afi': 'SlideioImageConverter',
 '.bif': 'OpenSlideImageConverter',
 '.czi': 'BioioImageConverter',
 '.mrxs': 'OpenSlideImageConverter',
 '.ndpi': 'OpenSlideImageConverter',
 '.qptiff': 'SlideioImageConverter',
 '.scn': 'OpenSlideImageConverter',
 '.svs': 'OpenSlideImageConverter',
 '.svslide': 'OpenSlideImageConverter',
 '.tif': 'OpenSlideImageConverter',
 '.tiff': 'OpenSlideImageConverter',
 '.vms': 'OpenSlideImageConverter',
 '.vsi': 'SlideioImageConverter',
 '.zvi': 'SlideioImageConverter'}

Converting a single file#

subresolutions controls how many pyramid levels are generated below full resolution (each level halves the resolution); tile_size is the TIFF tile edge length in pixels. The defaults (4 subresolutions, 1024px tiles) work well for typical whole-slide images.

SOURCE = Path("/Users/rushin.gindra/Documents/Research/SpatialProjects/Phoenix/datasets/svs_files/example.svs")
OUTPUT_DIR = Path("/Users/rushin.gindra/Documents/Research/SpatialProjects/Phoenix/datasets/ometiff_output/")

ometiff_paths = convert_to_ometiff(
    source=SOURCE,
    output_dir=OUTPUT_DIR,
    subresolutions=4,
    tile_size=1024,
    overwrite=False,
)
ometiff_paths
INFO     Writing pyramid level 0 (shape: (44928, 99840, 3))
INFO     Writing pyramid level 1 (shape: (22464, 49920, 3))
INFO     Writing pyramid level 2 (shape: (11232, 24960, 3))
INFO     Writing pyramid level 3 (shape: (5616, 12480, 3))
INFO     Writing pyramid level 4 (shape: (2808, 6240, 3))
[PosixPath('/Users/rushin.gindra/Documents/Research/SpatialProjects/Phoenix/datasets/ometiff_output/example.ome.tif')]

A source file can contain more than one scene/plane (common for .czi), in which case convert_to_ometiff writes one .ome.tif per scene and returns every path it wrote.

Batch converting a directory#

Discover every convertible file under a directory by matching the registered suffixes, then convert each one – skipping (rather than aborting on) files with no registered converter or that fail to convert:

def find_convertible_files(input_path: Path) -> list[Path]:
    """Return every file under `input_path` with a registered converter suffix."""
    suffixes = list_converters().keys()
    files = []
    for suffix in suffixes:
        files.extend(input_path.glob(f"*{suffix}"))
        files.extend(input_path.glob(f"*{suffix.upper()}"))
    return sorted(set(files))


WSI_DIR = Path("wsi_dir")
all_outputs: list[Path] = []

for file in find_convertible_files(WSI_DIR):
    try:
        get_converter_for(file)  # fail fast with a clear message before doing any work
    except RegistryError as e:
        print(f"Skipping {file}: {e}")
        continue

    try:
        all_outputs.extend(convert_to_ometiff(file, OUTPUT_DIR, overwrite=False))
    except Exception as e:  # noqa: BLE001 - one file's failure must not abort the batch
        print(f"Failed to convert {file}: {e}")

print(f"Created {len(all_outputs)} OME-TIFF file(s)")
Created 0 OME-TIFF file(s)

Handling unsupported files#

get_converter_for (and therefore convert_to_ometiff) raises RegistryError for two cases: a suffix with no registered converter, and a source that is itself already an .ome.tif/.ome.tiff – re-running a conversion over a directory that already contains outputs will not re-ingest them as inputs.

try:
    convert_to_ometiff(Path("unsupported.xyz"), OUTPUT_DIR)
except RegistryError as e:
    print(e)
"No converter registered for suffix '.xyz'. Known suffixes: .afi, .bif, .czi, .mrxs, .ndpi, .qptiff, .scn, .svs, .svslide, .tif, .tiff, .vms, .vsi, .zvi"

CZI support#

.czi files require the optional czi extra:

pip install "spatialrefinery[czi]"

Without it, .czi sources raise RegistryError just like any other unregistered suffix.

.vsi, .qptiff, .zvi and .afi files need no extra – their reader, slideio, is a required dependency.

Running this as a script#

python scripts/convert_to_ometiff.py --input_path wsi_dir/ --output_dir ometiff_output/ -p 4

What’s next#

Continue to Nucleus segmentation to SpatialData zarr to segment nuclei in the OME-TIFF you just wrote and package them as a SpatialData zarr store.