Skip to content

Commit c0d19c3

Browse files
committed
type the deepzoom generator
Signed-off-by: Sam Maxwell <[email protected]>
1 parent 149f470 commit c0d19c3

File tree

1 file changed

+89
-55
lines changed

1 file changed

+89
-55
lines changed

openslide/deepzoom.py

Lines changed: 89 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from io import BytesIO
2727
import math
28+
from typing import List, Tuple
2829
from xml.etree.ElementTree import Element, ElementTree, SubElement
2930

3031
from PIL import Image
@@ -44,7 +45,13 @@ class DeepZoomGenerator:
4445
openslide.PROPERTY_NAME_BOUNDS_HEIGHT,
4546
)
4647

47-
def __init__(self, osr, tile_size=254, overlap=1, limit_bounds=False):
48+
def __init__(
49+
self,
50+
osr: openslide.AbstractSlide,
51+
tile_size: int = 254,
52+
overlap: int = 1,
53+
limit_bounds: bool = False,
54+
):
4855
"""Create a DeepZoomGenerator wrapping an OpenSlide object.
4956
5057
osr: a slide object.
@@ -79,13 +86,13 @@ def __init__(self, osr, tile_size=254, overlap=1, limit_bounds=False):
7986
for prop, l0_lim in zip(self.BOUNDS_SIZE_PROPS, osr.dimensions)
8087
)
8188
# Dimensions of active area
82-
self._l_dimensions = tuple(
83-
tuple(
84-
int(math.ceil(l_lim * scale))
85-
for l_lim, scale in zip(l_size, size_scale)
89+
self._l_dimensions = [
90+
(
91+
int(math.ceil(l_size[0] * size_scale[0])),
92+
int(math.ceil(l_size[1] * size_scale[1])),
8693
)
8794
for l_size in osr.level_dimensions
88-
)
95+
]
8996
else:
9097
self._l_dimensions = osr.level_dimensions
9198
self._l0_offset = (0, 0)
@@ -94,25 +101,28 @@ def __init__(self, osr, tile_size=254, overlap=1, limit_bounds=False):
94101
z_size = self._l0_dimensions
95102
z_dimensions = [z_size]
96103
while z_size[0] > 1 or z_size[1] > 1:
97-
z_size = tuple(max(1, int(math.ceil(z / 2))) for z in z_size)
104+
z_size = (
105+
max(1, int(math.ceil(z_size[0] / 2))),
106+
max(1, int(math.ceil(z_size[1] / 2))),
107+
)
98108
z_dimensions.append(z_size)
99-
self._z_dimensions = tuple(reversed(z_dimensions))
109+
self._z_dimensions = list(reversed(z_dimensions))
100110

101111
# Tile
102-
def tiles(z_lim):
112+
def tiles(z_lim: int) -> int:
103113
return int(math.ceil(z_lim / self._z_t_downsample))
104114

105-
self._t_dimensions = tuple(
115+
self._t_dimensions = [
106116
(tiles(z_w), tiles(z_h)) for z_w, z_h in self._z_dimensions
107-
)
117+
]
108118

109119
# Deep Zoom level count
110120
self._dz_levels = len(self._z_dimensions)
111121

112122
# Total downsamples for each Deep Zoom level
113-
l0_z_downsamples = tuple(
123+
l0_z_downsamples: List[int] = [
114124
2 ** (self._dz_levels - dz_level - 1) for dz_level in range(self._dz_levels)
115-
)
125+
]
116126

117127
# Preferred slide levels for each Deep Zoom level
118128
self._slide_from_dz_level = tuple(
@@ -121,19 +131,19 @@ def tiles(z_lim):
121131

122132
# Piecewise downsamples
123133
self._l0_l_downsamples = self._osr.level_downsamples
124-
self._l_z_downsamples = tuple(
134+
self._l_z_downsamples = [
125135
l0_z_downsamples[dz_level]
126136
/ self._l0_l_downsamples[self._slide_from_dz_level[dz_level]]
127137
for dz_level in range(self._dz_levels)
128-
)
138+
]
129139

130140
# Slide background color
131-
self._bg_color = '#' + self._osr.properties.get(
132-
openslide.PROPERTY_NAME_BACKGROUND_COLOR, 'ffffff'
141+
self._bg_color = "#" + self._osr.properties.get(
142+
openslide.PROPERTY_NAME_BACKGROUND_COLOR, "ffffff"
133143
)
134144

135-
def __repr__(self):
136-
return '{}({!r}, tile_size={!r}, overlap={!r}, limit_bounds={!r})'.format(
145+
def __repr__(self) -> str:
146+
return "{}({!r}, tile_size={!r}, overlap={!r}, limit_bounds={!r})".format(
137147
self.__class__.__name__,
138148
self._osr,
139149
self._z_t_downsample,
@@ -142,26 +152,26 @@ def __repr__(self):
142152
)
143153

144154
@property
145-
def level_count(self):
155+
def level_count(self) -> int:
146156
"""The number of Deep Zoom levels in the image."""
147157
return self._dz_levels
148158

149159
@property
150-
def level_tiles(self):
160+
def level_tiles(self) -> List[Tuple[int, int]]:
151161
"""A list of (tiles_x, tiles_y) tuples for each Deep Zoom level."""
152162
return self._t_dimensions
153163

154164
@property
155-
def level_dimensions(self):
165+
def level_dimensions(self) -> List[Tuple[int, int]]:
156166
"""A list of (pixels_x, pixels_y) tuples for each Deep Zoom level."""
157167
return self._z_dimensions
158168

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

164-
def get_tile(self, level, address):
174+
def get_tile(self, level: int, address: Tuple[int, int]) -> Image.Image:
165175
"""Return an RGB PIL.Image for a tile.
166176
167177
level: the Deep Zoom level.
@@ -171,25 +181,27 @@ def get_tile(self, level, address):
171181
# Read tile
172182
args, z_size = self._get_tile_info(level, address)
173183
tile = self._osr.read_region(*args)
174-
profile = tile.info.get('icc_profile')
184+
profile = tile.info.get("icc_profile")
175185

176186
# Apply on solid background
177-
bg = Image.new('RGB', tile.size, self._bg_color)
187+
bg = Image.new("RGB", tile.size, self._bg_color)
178188
tile = Image.composite(tile, bg, tile)
179189

180190
# Scale to the correct size
181191
if tile.size != z_size:
182192
# Image.Resampling added in Pillow 9.1.0
183193
# Image.LANCZOS removed in Pillow 10
184-
tile.thumbnail(z_size, getattr(Image, 'Resampling', Image).LANCZOS)
194+
tile.thumbnail(z_size, getattr(Image, "Resampling", Image).LANCZOS)
185195

186196
# Reference ICC profile
187197
if profile is not None:
188-
tile.info['icc_profile'] = profile
198+
tile.info["icc_profile"] = profile
189199

190200
return tile
191201

192-
def _get_tile_info(self, dz_level, t_location):
202+
def _get_tile_info(
203+
self, dz_level: int, t_location: Tuple[int, int]
204+
) -> Tuple[Tuple[Tuple[int, int], int, Tuple[int, int]], Tuple[int, int]]:
193205
# Check parameters
194206
if dz_level < 0 or dz_level >= self._dz_levels:
195207
raise ValueError("Invalid level")
@@ -208,42 +220,62 @@ def _get_tile_info(self, dz_level, t_location):
208220
)
209221

210222
# Get final size of the tile
211-
z_size = tuple(
212-
min(self._z_t_downsample, z_lim - self._z_t_downsample * t) + z_tl + z_br
213-
for t, z_lim, z_tl, z_br in zip(
214-
t_location, self._z_dimensions[dz_level], z_overlap_tl, z_overlap_br
223+
z_size = (
224+
min(
225+
self._z_t_downsample,
226+
self._z_dimensions[dz_level][0] - self._z_t_downsample * t_location[0],
227+
)
228+
+ z_overlap_tl[0]
229+
+ z_overlap_br[0],
230+
min(
231+
self._z_t_downsample,
232+
self._z_dimensions[dz_level][1] - self._z_t_downsample * t_location[1],
215233
)
234+
+ z_overlap_tl[1]
235+
+ z_overlap_br[1],
216236
)
217237

218238
# Obtain the region coordinates
219-
z_location = [self._z_from_t(t) for t in t_location]
220-
l_location = [
221-
self._l_from_z(dz_level, z - z_tl)
222-
for z, z_tl in zip(z_location, z_overlap_tl)
223-
]
239+
z_location = (self._z_from_t(t_location[0]), self._z_from_t(t_location[1]))
240+
l_location = (
241+
self._l_from_z(dz_level, z_location[0] - z_overlap_tl[0]),
242+
self._l_from_z(dz_level, z_location[1] - z_overlap_tl[1]),
243+
)
224244
# Round location down and size up, and add offset of active area
225-
l0_location = tuple(
226-
int(self._l0_from_l(slide_level, l) + l0_off)
227-
for l, l0_off in zip(l_location, self._l0_offset)
245+
l0_location = (
246+
int(self._l0_from_l(slide_level, l_location[0]) + self._l0_offset[0]),
247+
int(self._l0_from_l(slide_level, l_location[1]) + self._l0_offset[1]),
228248
)
229-
l_size = tuple(
230-
int(min(math.ceil(self._l_from_z(dz_level, dz)), l_lim - math.ceil(l)))
231-
for l, dz, l_lim in zip(l_location, z_size, self._l_dimensions[slide_level])
249+
l_size = (
250+
int(
251+
min(
252+
math.ceil(self._l_from_z(dz_level, z_size[0])),
253+
self._l_dimensions[slide_level][0] - math.ceil(l_location[0]),
254+
)
255+
),
256+
int(
257+
min(
258+
math.ceil(self._l_from_z(dz_level, z_size[1])),
259+
self._l_dimensions[slide_level][1] - math.ceil(l_location[1]),
260+
)
261+
),
232262
)
233263

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

237-
def _l0_from_l(self, slide_level, l):
267+
def _l0_from_l(self, slide_level: int, l: float) -> float:
238268
return self._l0_l_downsamples[slide_level] * l
239269

240-
def _l_from_z(self, dz_level, z):
270+
def _l_from_z(self, dz_level: int, z: int) -> float:
241271
return self._l_z_downsamples[dz_level] * z
242272

243-
def _z_from_t(self, t):
273+
def _z_from_t(self, t: int) -> int:
244274
return self._z_t_downsample * t
245275

246-
def get_tile_coordinates(self, level, address):
276+
def get_tile_coordinates(
277+
self, level: int, address: Tuple[int, int]
278+
) -> Tuple[Tuple[int, int], int, Tuple[int, int]]:
247279
"""Return the OpenSlide.read_region() arguments for the specified tile.
248280
249281
Most users should call get_tile() rather than calling
@@ -254,28 +286,30 @@ def get_tile_coordinates(self, level, address):
254286
tuple."""
255287
return self._get_tile_info(level, address)[0]
256288

257-
def get_tile_dimensions(self, level, address):
289+
def get_tile_dimensions(
290+
self, level: int, address: Tuple[int, int]
291+
) -> Tuple[int, int]:
258292
"""Return a (pixels_x, pixels_y) tuple for the specified tile.
259293
260294
level: the Deep Zoom level.
261295
address: the address of the tile within the level as a (col, row)
262296
tuple."""
263297
return self._get_tile_info(level, address)[1]
264298

265-
def get_dzi(self, format):
299+
def get_dzi(self, format: str) -> str:
266300
"""Return a string containing the XML metadata for the .dzi file.
267301
268302
format: the format of the individual tiles ('png' or 'jpeg')"""
269303
image = Element(
270-
'Image',
304+
"Image",
271305
TileSize=str(self._z_t_downsample),
272306
Overlap=str(self._z_overlap),
273307
Format=format,
274-
xmlns='http://schemas.microsoft.com/deepzoom/2008',
308+
xmlns="http://schemas.microsoft.com/deepzoom/2008",
275309
)
276310
w, h = self._l0_dimensions
277-
SubElement(image, 'Size', Width=str(w), Height=str(h))
311+
SubElement(image, "Size", Width=str(w), Height=str(h))
278312
tree = ElementTree(element=image)
279313
buf = BytesIO()
280-
tree.write(buf, encoding='UTF-8')
281-
return buf.getvalue().decode('UTF-8')
314+
tree.write(buf, encoding="UTF-8")
315+
return buf.getvalue().decode("UTF-8")

0 commit comments

Comments
 (0)