"""OpenSlide backend for slide reading (CPU-based)."""
from pathlib import Path
from typing import Dict, Tuple, Union
import PIL.Image
import openslide
from glasscut.exceptions import SlidePropertyError
from .base import SlideBackend
[docs]
class OpenSlideBackend(SlideBackend):
"""OpenSlide-based backend for reading whole slide images.
This is the CPU-based fallback backend. It uses the OpenSlide library
to read various slide formats (SVS, TIFF, etc.).
"""
[docs]
def __init__(self) -> None:
self._slide = None
self._path = None
[docs]
def open(self, path: Union[str, Path]) -> None:
"""Open a slide file using OpenSlide.
Parameters
----------
path : Union[str, Path]
Path to the slide file
Raises
------
FileNotFoundError
If the slide file does not exist
BackendError
If OpenSlide cannot open the file
"""
self._path = str(path) if isinstance(path, Path) else path
try:
self._slide = openslide.open_slide(self._path)
except FileNotFoundError:
raise FileNotFoundError(f"Slide file not found: {self._path}")
except Exception as e:
raise RuntimeError(f"Failed to open slide with OpenSlide: {e}")
[docs]
def close(self) -> None:
"""Close the slide."""
if self._slide is not None:
self._slide.close()
self._slide = None
@property
def dimensions(self) -> Tuple[int, int]:
"""Get slide dimensions at level 0.
Returns
-------
Tuple[int, int]
(width, height) at highest magnification
"""
if self._slide is None:
raise RuntimeError("Slide not opened")
return self._slide.dimensions
@property
def properties(self) -> Dict[str, str]:
"""Get slide metadata properties.
Returns
-------
Dict[str, str]
Dictionary of slide properties
"""
if self._slide is None:
raise RuntimeError("Slide not opened")
return dict(self._slide.properties)
@property
def num_levels(self) -> int:
"""Get number of pyramid levels.
Returns
-------
int
Number of pyramid levels
"""
if self._slide is None:
raise RuntimeError("Slide not opened")
return len(self._slide.level_dimensions)
[docs]
def read_region(
self, location: Tuple[int, int], level: int, size: Tuple[int, int]
) -> PIL.Image.Image:
"""Read a region/tile from the slide.
Parameters
----------
location : Tuple[int, int]
(x, y) coordinates at level 0
level : int
Pyramid level to read from
size : Tuple[int, int]
(width, height) of the tile in pixels
Returns
-------
PIL.Image.Image
The tile image in RGB format
"""
if self._slide is None:
raise RuntimeError("Slide not opened")
image = self._slide.read_region(location, level, size)
return image.convert("RGB")
[docs]
def get_thumbnail(self, size: Tuple[int, int]) -> PIL.Image.Image:
"""Get thumbnail of the slide.
Parameters
----------
size : Tuple[int, int]
Maximum size of the thumbnail (width, height)
Returns
-------
PIL.Image.Image
Thumbnail image in RGB format
"""
if self._slide is None:
raise RuntimeError("Slide not opened")
thumbnail = self._slide.get_thumbnail(size)
return thumbnail.convert("RGB")
@property
def mpp(self) -> float:
"""Get microns per pixel at base magnification.
Returns
-------
float
Microns per pixel (MPP)
Raises
------
SlidePropertyError
If MPP cannot be determined from metadata
"""
if self._slide is None:
raise RuntimeError("Slide not opened")
props = self.properties
# Try common MPP property names
if "openslide.mpp-x" in props:
return float(props["openslide.mpp-x"])
if "aperio.MPP" in props:
return float(props["aperio.MPP"])
if (
"tiff.XResolution" in props
and props.get("tiff.ResolutionUnit") == "centimeter"
):
return 1e4 / float(props["tiff.XResolution"])
raise SlidePropertyError(
f"Could not determine MPP from slide properties. "
f"Available properties: {list(props.keys())}"
)
@property
def base_magnification(self) -> int | float:
"""Get the base magnification (objective power) of the slide.
Returns
-------
int | float
Base magnification value
Raises
------
SlidePropertyError
If base magnification cannot be determined from metadata
"""
if self._slide is None:
raise RuntimeError("Slide not opened")
props = self.properties
# Try common objective power property names (OpenSlide)
if "openslide.objective-power" in props:
try:
return float(props["openslide.objective-power"])
except ValueError:
pass
# Try Aperio format
if "aperio.AppMag" in props:
try:
return float(props["aperio.AppMag"])
except ValueError:
pass
# Try generic magnification properties
if "magnification" in props:
try:
return float(props["magnification"])
except ValueError:
pass
# If all else fails, raise an error
raise SlidePropertyError(
f"Could not determine base magnification from slide properties. "
f"Available properties: {list(props.keys())}"
)