"""Main Slide class for WSI manipulation."""
import math
import ntpath
from pathlib import Path
from types import TracebackType
import numpy as np
import PIL.Image
from .backends import CuCimBackend, OpenSlideBackend, SlideBackend
from .utils import build_magnification_mapping, magnification_to_level
from glasscut.exceptions import TileSizeOrCoordinatesError
from glasscut.tile import Tile
from glasscut.utils import lazyproperty
[docs]
class Slide:
"""Represents a whole slide image with magnification-based access.
This class provides an interface to access whole slide images.
It abstracts away the backend (OpenSlide or cuCim).
Parameters
----------
path : Union[str, pathlib.Path]
Path to the slide file
use_cucim : bool, optional
Whether to try using cuCim GPU backend. If False or cuCim is not
available, falls back to OpenSlide. Default is True.
"""
[docs]
def __init__(self, path: str | Path, use_cucim: bool = True) -> None:
self._path = str(path) if isinstance(path, Path) else path
self._backend: SlideBackend | None = None
# Try to initialize backend
if use_cucim:
try:
self._backend = CuCimBackend()
self._backend.open(self._path)
except Exception:
# Fallback to OpenSlide
self._backend = OpenSlideBackend()
self._backend.open(self._path)
else:
self._backend = OpenSlideBackend()
self._backend.open(self._path)
def __repr__(self) -> str:
return (
f"Slide(path={self._path}, "
f"magnifications={self.magnifications}, "
f"dimensions={self.dimensions})"
)
[docs]
def __enter__(self):
"""Context manager entry."""
return self
[docs]
def __exit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
"""Context manager exit."""
self.close()
[docs]
def close(self) -> None:
"""Close the slide and free resources."""
if self._backend is not None:
self._backend.close()
# ===== Public Properties =====
@lazyproperty
def name(self) -> str:
"""Slide name without extension.
Returns
-------
str
Slide filename without extension
"""
bname = ntpath.basename(self._path)
return bname[: bname.rfind(".")]
@lazyproperty
def dimensions(self) -> tuple[int, int]:
"""Slide dimensions (width, height) at base magnification.
Returns
-------
tuple[int, int]
(width, height) in pixels at highest magnification (typically 40x)
"""
if self._backend is None:
raise RuntimeError("Slide not opened")
return self._backend.dimensions
@lazyproperty
def magnifications(self) -> list[float]:
"""Available magnifications for this slide.
These are calculated from the actual slide's base magnification
(objective power) and the number of pyramid levels.
Returns
-------
list[float]
List of magnifications in descending order (e.g., [40.0, 20.0, 10.0, 5.0])
"""
if self._backend is None:
raise RuntimeError("Slide not opened")
base_mag = self._backend.base_magnification
return build_magnification_mapping(base_mag, self._backend.num_levels)
@lazyproperty
def mpp(self) -> float:
"""Microns per pixel at base magnification.
Returns
-------
float
Microns per pixel
Raises
------
SlidelazypropertyError
If MPP cannot be determined from slide metadata
"""
if self._backend is None:
raise RuntimeError("Slide not opened")
return self._backend.mpp
@lazyproperty
def properties(self) -> dict[str, str]:
"""Slide metadata properties.
Returns
-------
dict
Dictionary of all slide properties
"""
if self._backend is None:
raise RuntimeError("Slide not opened")
return self._backend.properties
@lazyproperty
def thumbnail(self) -> PIL.Image.Image:
"""Get thumbnail of the slide.
The thumbnail size is automatically calculated based on slide dimensions.
Returns
-------
PIL.Image.Image
Thumbnail image in RGB format
"""
if self._backend is None:
raise RuntimeError("Slide not opened")
size = self._compute_thumbnail_size()
return self._backend.get_thumbnail(size)
# ===== Public Methods =====
# ===== Private Helper Methods =====
def _has_valid_coords(self, coords: tuple[int, int], tile_size: tuple[int, int]) -> bool:
"""Check if coordinates are valid for the slide.
Parameters
----------
coords : tuple[int, int]
(x, y) coordinates (upper-left corner of tile)
tile_size : tuple[int, int]
(width, height) of the tile
Returns
-------
bool
True if coordinates are valid, False otherwise
"""
x, y = coords
w, h = tile_size
slide_w, slide_h = self.dimensions
return (
0 <= x < slide_w
and 0 <= y < slide_h
and x + w <= slide_w
and y + h <= slide_h
)
def _compute_thumbnail_size(self) -> tuple[int, int]:
"""Compute thumbnail size proportionally to slide dimensions.
Returns
-------
tuple[int, int]
Thumbnail size (width, height)
"""
width, height = self.dimensions
return (
int(width / np.power(10, math.ceil(math.log10(width)) - 3)),
int(height / np.power(10, math.ceil(math.log10(height)) - 3)),
)