From c0d19c39da92f8b6468d7375190199847bc921e8 Mon Sep 17 00:00:00 2001 From: Sam Maxwell Date: Thu, 22 Feb 2024 00:15:48 +0000 Subject: [PATCH] type the deepzoom generator Signed-off-by: Sam Maxwell --- openslide/deepzoom.py | 144 ++++++++++++++++++++++++++---------------- 1 file changed, 89 insertions(+), 55 deletions(-) diff --git a/openslide/deepzoom.py b/openslide/deepzoom.py index 28ec7a81..a9206a53 100644 --- a/openslide/deepzoom.py +++ b/openslide/deepzoom.py @@ -25,6 +25,7 @@ from io import BytesIO import math +from typing import List, Tuple from xml.etree.ElementTree import Element, ElementTree, SubElement from PIL import Image @@ -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. @@ -79,13 +86,13 @@ def __init__(self, osr, tile_size=254, overlap=1, limit_bounds=False): for prop, l0_lim in zip(self.BOUNDS_SIZE_PROPS, osr.dimensions) ) # Dimensions of active area - self._l_dimensions = tuple( - tuple( - int(math.ceil(l_lim * scale)) - for l_lim, scale in zip(l_size, size_scale) + self._l_dimensions = [ + ( + int(math.ceil(l_size[0] * size_scale[0])), + int(math.ceil(l_size[1] * size_scale[1])), ) for l_size in osr.level_dimensions - ) + ] else: self._l_dimensions = osr.level_dimensions self._l0_offset = (0, 0) @@ -94,25 +101,28 @@ def __init__(self, osr, tile_size=254, overlap=1, limit_bounds=False): z_size = self._l0_dimensions z_dimensions = [z_size] while z_size[0] > 1 or z_size[1] > 1: - z_size = tuple(max(1, int(math.ceil(z / 2))) for z in z_size) + z_size = ( + max(1, int(math.ceil(z_size[0] / 2))), + max(1, int(math.ceil(z_size[1] / 2))), + ) z_dimensions.append(z_size) - self._z_dimensions = tuple(reversed(z_dimensions)) + self._z_dimensions = list(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( + self._t_dimensions = [ (tiles(z_w), tiles(z_h)) for z_w, z_h in self._z_dimensions - ) + ] # Deep Zoom level count self._dz_levels = len(self._z_dimensions) # Total downsamples for each Deep Zoom level - l0_z_downsamples = tuple( + l0_z_downsamples: List[int] = [ 2 ** (self._dz_levels - dz_level - 1) for dz_level in range(self._dz_levels) - ) + ] # Preferred slide levels for each Deep Zoom level self._slide_from_dz_level = tuple( @@ -121,19 +131,19 @@ def tiles(z_lim): # Piecewise downsamples self._l0_l_downsamples = self._osr.level_downsamples - self._l_z_downsamples = tuple( + self._l_z_downsamples = [ l0_z_downsamples[dz_level] / self._l0_l_downsamples[self._slide_from_dz_level[dz_level]] for dz_level in range(self._dz_levels) - ) + ] # Slide background color - self._bg_color = '#' + self._osr.properties.get( - openslide.PROPERTY_NAME_BACKGROUND_COLOR, 'ffffff' + self._bg_color = "#" + self._osr.properties.get( + openslide.PROPERTY_NAME_BACKGROUND_COLOR, "ffffff" ) - def __repr__(self): - return '{}({!r}, tile_size={!r}, overlap={!r}, limit_bounds={!r})'.format( + def __repr__(self) -> str: + return "{}({!r}, tile_size={!r}, overlap={!r}, limit_bounds={!r})".format( self.__class__.__name__, self._osr, self._z_t_downsample, @@ -142,26 +152,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) -> List[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) -> List[Tuple[int, 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. @@ -171,25 +181,27 @@ def get_tile(self, level, address): # Read tile args, z_size = self._get_tile_info(level, address) tile = self._osr.read_region(*args) - profile = tile.info.get('icc_profile') + profile = tile.info.get("icc_profile") # Apply on solid background - bg = Image.new('RGB', tile.size, self._bg_color) + bg = Image.new("RGB", tile.size, self._bg_color) tile = Image.composite(tile, bg, tile) # Scale to the correct size if tile.size != z_size: # Image.Resampling added in Pillow 9.1.0 # Image.LANCZOS removed in Pillow 10 - tile.thumbnail(z_size, getattr(Image, 'Resampling', Image).LANCZOS) + tile.thumbnail(z_size, getattr(Image, "Resampling", Image).LANCZOS) # Reference ICC profile if profile is not None: - tile.info['icc_profile'] = profile + tile.info["icc_profile"] = profile 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") @@ -208,42 +220,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 @@ -254,7 +286,9 @@ 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. @@ -262,20 +296,20 @@ def get_tile_dimensions(self, level, address): 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')""" image = Element( - 'Image', + "Image", TileSize=str(self._z_t_downsample), Overlap=str(self._z_overlap), Format=format, - xmlns='http://schemas.microsoft.com/deepzoom/2008', + xmlns="http://schemas.microsoft.com/deepzoom/2008", ) w, h = self._l0_dimensions - SubElement(image, 'Size', Width=str(w), Height=str(h)) + SubElement(image, "Size", Width=str(w), Height=str(h)) tree = ElementTree(element=image) buf = BytesIO() - tree.write(buf, encoding='UTF-8') - return buf.getvalue().decode('UTF-8') + tree.write(buf, encoding="UTF-8") + return buf.getvalue().decode("UTF-8")