Skip to content

Commit

Permalink
type the deepzoom generator
Browse files Browse the repository at this point in the history
Signed-off-by: Sam Maxwell <[email protected]>
  • Loading branch information
sammaxwellxyz committed Mar 6, 2024
1 parent 84627b1 commit c5b0cec
Showing 1 changed file with 63 additions and 31 deletions.
94 changes: 63 additions & 31 deletions openslide/deepzoom.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
This module provides functionality for generating Deep Zoom images from
OpenSlide objects.
"""
from __future__ import annotations

from io import BytesIO
import math
Expand All @@ -44,7 +45,13 @@ class DeepZoomGenerator:
openslide.PROPERTY_NAME_BOUNDS_HEIGHT,
)

def __init__(self, osr, tile_size=254, overlap=1, limit_bounds=False):
def __init__(
self,
osr: openslide.AbstractSlide,
tile_size: int = 254,
overlap: int = 1,
limit_bounds: bool = False,
):
"""Create a DeepZoomGenerator wrapping an OpenSlide object.
osr: a slide object.
Expand Down Expand Up @@ -99,7 +106,7 @@ def __init__(self, osr, tile_size=254, overlap=1, limit_bounds=False):
self._z_dimensions = tuple(reversed(z_dimensions))

# Tile
def tiles(z_lim):
def tiles(z_lim: int) -> int:
return int(math.ceil(z_lim / self._z_t_downsample))

self._t_dimensions = tuple(
Expand All @@ -110,7 +117,8 @@ def tiles(z_lim):
self._dz_levels = len(self._z_dimensions)

# Total downsamples for each Deep Zoom level
l0_z_downsamples = tuple(
# mypy infers this as a tuple[Any, ...] due to the ** operator
l0_z_downsamples: tuple[int, ...] = tuple(
2 ** (self._dz_levels - dz_level - 1) for dz_level in range(self._dz_levels)
)

Expand All @@ -132,7 +140,7 @@ def tiles(z_lim):
openslide.PROPERTY_NAME_BACKGROUND_COLOR, 'ffffff'
)

def __repr__(self):
def __repr__(self) -> str:
return '{}({!r}, tile_size={!r}, overlap={!r}, limit_bounds={!r})'.format(
self.__class__.__name__,
self._osr,
Expand All @@ -142,26 +150,26 @@ def __repr__(self):
)

@property
def level_count(self):
def level_count(self) -> int:
"""The number of Deep Zoom levels in the image."""
return self._dz_levels

@property
def level_tiles(self):
def level_tiles(self) -> tuple[tuple[int, int], ...]:
"""A list of (tiles_x, tiles_y) tuples for each Deep Zoom level."""
return self._t_dimensions

@property
def level_dimensions(self):
def level_dimensions(self) -> tuple[tuple[int, ...], ...]:
"""A list of (pixels_x, pixels_y) tuples for each Deep Zoom level."""
return self._z_dimensions

@property
def tile_count(self):
def tile_count(self) -> int:
"""The total number of Deep Zoom tiles in the image."""
return sum(t_cols * t_rows for t_cols, t_rows in self._t_dimensions)

def get_tile(self, level, address):
def get_tile(self, level: int, address: tuple[int, int]) -> Image.Image:
"""Return an RGB PIL.Image for a tile.
level: the Deep Zoom level.
Expand Down Expand Up @@ -189,7 +197,9 @@ def get_tile(self, level, address):

return tile

def _get_tile_info(self, dz_level, t_location):
def _get_tile_info(
self, dz_level: int, t_location: tuple[int, int]
) -> tuple[tuple[tuple[int, int], int, tuple[int, int]], tuple[int, int]]:
# Check parameters
if dz_level < 0 or dz_level >= self._dz_levels:
raise ValueError("Invalid level")
Expand All @@ -208,42 +218,62 @@ def _get_tile_info(self, dz_level, t_location):
)

# Get final size of the tile
z_size = tuple(
min(self._z_t_downsample, z_lim - self._z_t_downsample * t) + z_tl + z_br
for t, z_lim, z_tl, z_br in zip(
t_location, self._z_dimensions[dz_level], z_overlap_tl, z_overlap_br
z_size = (
min(
self._z_t_downsample,
self._z_dimensions[dz_level][0] - self._z_t_downsample * t_location[0],
)
+ z_overlap_tl[0]
+ z_overlap_br[0],
min(
self._z_t_downsample,
self._z_dimensions[dz_level][1] - self._z_t_downsample * t_location[1],
)
+ z_overlap_tl[1]
+ z_overlap_br[1],
)

# Obtain the region coordinates
z_location = [self._z_from_t(t) for t in t_location]
l_location = [
self._l_from_z(dz_level, z - z_tl)
for z, z_tl in zip(z_location, z_overlap_tl)
]
z_location = (self._z_from_t(t_location[0]), self._z_from_t(t_location[1]))
l_location = (
self._l_from_z(dz_level, z_location[0] - z_overlap_tl[0]),
self._l_from_z(dz_level, z_location[1] - z_overlap_tl[1]),
)
# Round location down and size up, and add offset of active area
l0_location = tuple(
int(self._l0_from_l(slide_level, l) + l0_off)
for l, l0_off in zip(l_location, self._l0_offset)
l0_location = (
int(self._l0_from_l(slide_level, l_location[0]) + self._l0_offset[0]),
int(self._l0_from_l(slide_level, l_location[1]) + self._l0_offset[1]),
)
l_size = tuple(
int(min(math.ceil(self._l_from_z(dz_level, dz)), l_lim - math.ceil(l)))
for l, dz, l_lim in zip(l_location, z_size, self._l_dimensions[slide_level])
l_size = (
int(
min(
math.ceil(self._l_from_z(dz_level, z_size[0])),
self._l_dimensions[slide_level][0] - math.ceil(l_location[0]),
)
),
int(
min(
math.ceil(self._l_from_z(dz_level, z_size[1])),
self._l_dimensions[slide_level][1] - math.ceil(l_location[1]),
)
),
)

# Return read_region() parameters plus tile size for final scaling
return ((l0_location, slide_level, l_size), z_size)

def _l0_from_l(self, slide_level, l):
def _l0_from_l(self, slide_level: int, l: float) -> float:
return self._l0_l_downsamples[slide_level] * l

def _l_from_z(self, dz_level, z):
def _l_from_z(self, dz_level: int, z: int) -> float:
return self._l_z_downsamples[dz_level] * z

def _z_from_t(self, t):
def _z_from_t(self, t: int) -> int:
return self._z_t_downsample * t

def get_tile_coordinates(self, level, address):
def get_tile_coordinates(
self, level: int, address: tuple[int, int]
) -> tuple[tuple[int, int], int, tuple[int, int]]:
"""Return the OpenSlide.read_region() arguments for the specified tile.
Most users should call get_tile() rather than calling
Expand All @@ -254,15 +284,17 @@ def get_tile_coordinates(self, level, address):
tuple."""
return self._get_tile_info(level, address)[0]

def get_tile_dimensions(self, level, address):
def get_tile_dimensions(
self, level: int, address: tuple[int, int]
) -> tuple[int, int]:
"""Return a (pixels_x, pixels_y) tuple for the specified tile.
level: the Deep Zoom level.
address: the address of the tile within the level as a (col, row)
tuple."""
return self._get_tile_info(level, address)[1]

def get_dzi(self, format):
def get_dzi(self, format: str) -> str:
"""Return a string containing the XML metadata for the .dzi file.
format: the format of the individual tiles ('png' or 'jpeg')"""
Expand Down

0 comments on commit c5b0cec

Please sign in to comment.