25
25
26
26
from io import BytesIO
27
27
import math
28
+ from typing import List , Tuple
28
29
from xml .etree .ElementTree import Element , ElementTree , SubElement
29
30
30
31
from PIL import Image
@@ -44,7 +45,13 @@ class DeepZoomGenerator:
44
45
openslide .PROPERTY_NAME_BOUNDS_HEIGHT ,
45
46
)
46
47
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
+ ):
48
55
"""Create a DeepZoomGenerator wrapping an OpenSlide object.
49
56
50
57
osr: a slide object.
@@ -79,13 +86,13 @@ def __init__(self, osr, tile_size=254, overlap=1, limit_bounds=False):
79
86
for prop , l0_lim in zip (self .BOUNDS_SIZE_PROPS , osr .dimensions )
80
87
)
81
88
# 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 ])),
86
93
)
87
94
for l_size in osr .level_dimensions
88
- )
95
+ ]
89
96
else :
90
97
self ._l_dimensions = osr .level_dimensions
91
98
self ._l0_offset = (0 , 0 )
@@ -94,25 +101,28 @@ def __init__(self, osr, tile_size=254, overlap=1, limit_bounds=False):
94
101
z_size = self ._l0_dimensions
95
102
z_dimensions = [z_size ]
96
103
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
+ )
98
108
z_dimensions .append (z_size )
99
- self ._z_dimensions = tuple (reversed (z_dimensions ))
109
+ self ._z_dimensions = list (reversed (z_dimensions ))
100
110
101
111
# Tile
102
- def tiles (z_lim ) :
112
+ def tiles (z_lim : int ) -> int :
103
113
return int (math .ceil (z_lim / self ._z_t_downsample ))
104
114
105
- self ._t_dimensions = tuple (
115
+ self ._t_dimensions = [
106
116
(tiles (z_w ), tiles (z_h )) for z_w , z_h in self ._z_dimensions
107
- )
117
+ ]
108
118
109
119
# Deep Zoom level count
110
120
self ._dz_levels = len (self ._z_dimensions )
111
121
112
122
# Total downsamples for each Deep Zoom level
113
- l0_z_downsamples = tuple (
123
+ l0_z_downsamples : List [ int ] = [
114
124
2 ** (self ._dz_levels - dz_level - 1 ) for dz_level in range (self ._dz_levels )
115
- )
125
+ ]
116
126
117
127
# Preferred slide levels for each Deep Zoom level
118
128
self ._slide_from_dz_level = tuple (
@@ -121,19 +131,19 @@ def tiles(z_lim):
121
131
122
132
# Piecewise downsamples
123
133
self ._l0_l_downsamples = self ._osr .level_downsamples
124
- self ._l_z_downsamples = tuple (
134
+ self ._l_z_downsamples = [
125
135
l0_z_downsamples [dz_level ]
126
136
/ self ._l0_l_downsamples [self ._slide_from_dz_level [dz_level ]]
127
137
for dz_level in range (self ._dz_levels )
128
- )
138
+ ]
129
139
130
140
# 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"
133
143
)
134
144
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 (
137
147
self .__class__ .__name__ ,
138
148
self ._osr ,
139
149
self ._z_t_downsample ,
@@ -142,26 +152,26 @@ def __repr__(self):
142
152
)
143
153
144
154
@property
145
- def level_count (self ):
155
+ def level_count (self ) -> int :
146
156
"""The number of Deep Zoom levels in the image."""
147
157
return self ._dz_levels
148
158
149
159
@property
150
- def level_tiles (self ):
160
+ def level_tiles (self ) -> List [ Tuple [ int , int ]] :
151
161
"""A list of (tiles_x, tiles_y) tuples for each Deep Zoom level."""
152
162
return self ._t_dimensions
153
163
154
164
@property
155
- def level_dimensions (self ):
165
+ def level_dimensions (self ) -> List [ Tuple [ int , int ]] :
156
166
"""A list of (pixels_x, pixels_y) tuples for each Deep Zoom level."""
157
167
return self ._z_dimensions
158
168
159
169
@property
160
- def tile_count (self ):
170
+ def tile_count (self ) -> int :
161
171
"""The total number of Deep Zoom tiles in the image."""
162
172
return sum (t_cols * t_rows for t_cols , t_rows in self ._t_dimensions )
163
173
164
- def get_tile (self , level , address ) :
174
+ def get_tile (self , level : int , address : Tuple [ int , int ]) -> Image . Image :
165
175
"""Return an RGB PIL.Image for a tile.
166
176
167
177
level: the Deep Zoom level.
@@ -171,25 +181,27 @@ def get_tile(self, level, address):
171
181
# Read tile
172
182
args , z_size = self ._get_tile_info (level , address )
173
183
tile = self ._osr .read_region (* args )
174
- profile = tile .info .get (' icc_profile' )
184
+ profile = tile .info .get (" icc_profile" )
175
185
176
186
# Apply on solid background
177
- bg = Image .new (' RGB' , tile .size , self ._bg_color )
187
+ bg = Image .new (" RGB" , tile .size , self ._bg_color )
178
188
tile = Image .composite (tile , bg , tile )
179
189
180
190
# Scale to the correct size
181
191
if tile .size != z_size :
182
192
# Image.Resampling added in Pillow 9.1.0
183
193
# 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 )
185
195
186
196
# Reference ICC profile
187
197
if profile is not None :
188
- tile .info [' icc_profile' ] = profile
198
+ tile .info [" icc_profile" ] = profile
189
199
190
200
return tile
191
201
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 ]]:
193
205
# Check parameters
194
206
if dz_level < 0 or dz_level >= self ._dz_levels :
195
207
raise ValueError ("Invalid level" )
@@ -208,42 +220,62 @@ def _get_tile_info(self, dz_level, t_location):
208
220
)
209
221
210
222
# 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 ],
215
233
)
234
+ + z_overlap_tl [1 ]
235
+ + z_overlap_br [1 ],
216
236
)
217
237
218
238
# 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
+ )
224
244
# 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 ]),
228
248
)
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
+ ),
232
262
)
233
263
234
264
# Return read_region() parameters plus tile size for final scaling
235
265
return ((l0_location , slide_level , l_size ), z_size )
236
266
237
- def _l0_from_l (self , slide_level , l ) :
267
+ def _l0_from_l (self , slide_level : int , l : float ) -> float :
238
268
return self ._l0_l_downsamples [slide_level ] * l
239
269
240
- def _l_from_z (self , dz_level , z ) :
270
+ def _l_from_z (self , dz_level : int , z : int ) -> float :
241
271
return self ._l_z_downsamples [dz_level ] * z
242
272
243
- def _z_from_t (self , t ) :
273
+ def _z_from_t (self , t : int ) -> int :
244
274
return self ._z_t_downsample * t
245
275
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 ]]:
247
279
"""Return the OpenSlide.read_region() arguments for the specified tile.
248
280
249
281
Most users should call get_tile() rather than calling
@@ -254,28 +286,30 @@ def get_tile_coordinates(self, level, address):
254
286
tuple."""
255
287
return self ._get_tile_info (level , address )[0 ]
256
288
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 ]:
258
292
"""Return a (pixels_x, pixels_y) tuple for the specified tile.
259
293
260
294
level: the Deep Zoom level.
261
295
address: the address of the tile within the level as a (col, row)
262
296
tuple."""
263
297
return self ._get_tile_info (level , address )[1 ]
264
298
265
- def get_dzi (self , format ) :
299
+ def get_dzi (self , format : str ) -> str :
266
300
"""Return a string containing the XML metadata for the .dzi file.
267
301
268
302
format: the format of the individual tiles ('png' or 'jpeg')"""
269
303
image = Element (
270
- ' Image' ,
304
+ " Image" ,
271
305
TileSize = str (self ._z_t_downsample ),
272
306
Overlap = str (self ._z_overlap ),
273
307
Format = format ,
274
- xmlns = ' http://schemas.microsoft.com/deepzoom/2008' ,
308
+ xmlns = " http://schemas.microsoft.com/deepzoom/2008" ,
275
309
)
276
310
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 ))
278
312
tree = ElementTree (element = image )
279
313
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