Skip to content

Commit b1ee827

Browse files
authored
Merge pull request #129 from bgilbert/cache
Support OpenSlide cache management API
2 parents 8895762 + 29395e8 commit b1ee827

File tree

7 files changed

+227
-16
lines changed

7 files changed

+227
-16
lines changed

doc/index.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,35 @@ OpenSlide objects
177177
:param tuple size: the maximum size of the thumbnail as a
178178
``(width, height)`` tuple
179179

180+
.. method:: set_cache(cache)
181+
182+
Use the specified :class:`OpenSlideCache` to store recently decoded
183+
slide tiles. By default, the :class:`OpenSlide` has a private cache
184+
with a default size.
185+
186+
:param OpenSlideCache cache: a cache object
187+
:raises OpenSlideVersionError: if OpenSlide is older than version 3.5.0
188+
180189
.. method:: close()
181190

182191
Close the OpenSlide object.
183192

184193

194+
Caching
195+
-------
196+
197+
.. class:: OpenSlideCache(capacity)
198+
199+
An in-memory tile cache.
200+
201+
Tile caches can be attached to one or more :class:`OpenSlide` objects
202+
with :meth:`OpenSlide.set_cache` to cache recently-decoded tiles. By
203+
default, each :class:`OpenSlide` has its own cache with a default size.
204+
205+
:param int capacity: the cache capacity in bytes
206+
:raises OpenSlideVersionError: if OpenSlide is older than version 3.5.0
207+
208+
185209
.. _standard-properties:
186210

187211
Standard properties
@@ -259,6 +283,11 @@ Exceptions
259283
OpenSlide does not support the requested file. Subclass of
260284
:exc:`OpenSlideError`.
261285

286+
.. exception:: OpenSlideVersionError
287+
288+
This version of OpenSlide does not support the requested functionality.
289+
Subclass of :exc:`OpenSlideError`.
290+
262291

263292
Wrapping a PIL Image
264293
====================

examples/deepzoom/deepzoom_multiserver.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# deepzoom_multiserver - Example web application for viewing multiple slides
44
#
55
# Copyright (c) 2010-2015 Carnegie Mellon University
6+
# Copyright (c) 2021 Benjamin Gilbert
67
#
78
# This library is free software; you can redistribute it and/or modify it
89
# under the terms of version 2.1 of the GNU Lesser General Public License
@@ -43,11 +44,12 @@
4344
else:
4445
import openslide
4546

46-
from openslide import OpenSlide, OpenSlideError
47+
from openslide import OpenSlide, OpenSlideCache, OpenSlideError
4748
from openslide.deepzoom import DeepZoomGenerator
4849

4950
SLIDE_DIR = '.'
5051
SLIDE_CACHE_SIZE = 10
52+
SLIDE_TILE_CACHE_MB = 128
5153
DEEPZOOM_FORMAT = 'jpeg'
5254
DEEPZOOM_TILE_SIZE = 254
5355
DEEPZOOM_OVERLAP = 1
@@ -60,11 +62,13 @@
6062

6163

6264
class _SlideCache:
63-
def __init__(self, cache_size, dz_opts):
65+
def __init__(self, cache_size, tile_cache_mb, dz_opts):
6466
self.cache_size = cache_size
6567
self.dz_opts = dz_opts
6668
self._lock = Lock()
6769
self._cache = OrderedDict()
70+
# Share a single tile cache among all slide handles
71+
self._tile_cache = OpenSlideCache(tile_cache_mb * 1024 * 1024)
6872

6973
def get(self, path):
7074
with self._lock:
@@ -75,6 +79,7 @@ def get(self, path):
7579
return slide
7680

7781
osr = OpenSlide(path)
82+
osr.set_cache(self._tile_cache)
7883
slide = DeepZoomGenerator(osr, **self.dz_opts)
7984
try:
8085
mpp_x = osr.properties[openslide.PROPERTY_NAME_MPP_X]
@@ -121,7 +126,9 @@ def _setup():
121126
'DEEPZOOM_LIMIT_BOUNDS': 'limit_bounds',
122127
}
123128
opts = {v: app.config[k] for k, v in config_map.items()}
124-
app.cache = _SlideCache(app.config['SLIDE_CACHE_SIZE'], opts)
129+
app.cache = _SlideCache(
130+
app.config['SLIDE_CACHE_SIZE'], app.config['SLIDE_TILE_CACHE_MB'], opts
131+
)
125132

126133

127134
def _get_slide(path):

openslide/__init__.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# openslide-python - Python bindings for the OpenSlide library
33
#
44
# Copyright (c) 2010-2014 Carnegie Mellon University
5+
# Copyright (c) 2021 Benjamin Gilbert
56
#
67
# This library is free software; you can redistribute it and/or modify it
78
# under the terms of version 2.1 of the GNU Lesser General Public License
@@ -30,7 +31,11 @@
3031

3132
# For the benefit of library users
3233
from openslide._version import __version__ # noqa: F401 module-imported-but-unused
33-
from openslide.lowlevel import OpenSlideError, OpenSlideUnsupportedFormatError
34+
from openslide.lowlevel import ( # noqa: F401 module-imported-but-unused
35+
OpenSlideError,
36+
OpenSlideUnsupportedFormatError,
37+
OpenSlideVersionError,
38+
)
3439

3540
__library_version__ = lowlevel.get_version()
3641

@@ -119,6 +124,12 @@ def read_region(self, location, level, size):
119124
size: (width, height) tuple giving the region size."""
120125
raise NotImplementedError
121126

127+
def set_cache(self, cache):
128+
"""Use the specified cache to store recently decoded slide tiles.
129+
130+
cache: an OpenSlideCache object."""
131+
raise NotImplementedError
132+
122133
def get_thumbnail(self, size):
123134
"""Return a PIL.Image containing an RGB thumbnail of the image.
124135
@@ -226,6 +237,18 @@ def read_region(self, location, level, size):
226237
self._osr, location[0], location[1], level, size[0], size[1]
227238
)
228239

240+
def set_cache(self, cache):
241+
"""Use the specified cache to store recently decoded slide tiles.
242+
243+
By default, the object has a private cache with a default size.
244+
245+
cache: an OpenSlideCache object."""
246+
try:
247+
llcache = cache._openslide_cache
248+
except AttributeError:
249+
raise TypeError('Not a cache object')
250+
lowlevel.set_cache(self._osr, llcache)
251+
229252

230253
class _OpenSlideMap(Mapping):
231254
def __init__(self, osr):
@@ -266,6 +289,23 @@ def __getitem__(self, key):
266289
return lowlevel.read_associated_image(self._osr, key)
267290

268291

292+
class OpenSlideCache:
293+
"""An in-memory tile cache.
294+
295+
Tile caches can be attached to one or more OpenSlide objects with
296+
OpenSlide.set_cache() to cache recently-decoded tiles. By default,
297+
each OpenSlide object has its own cache with a default size.
298+
"""
299+
300+
def __init__(self, capacity):
301+
"""Create a tile cache with the specified capacity in bytes."""
302+
self._capacity = capacity
303+
self._openslide_cache = lowlevel.cache_create(capacity)
304+
305+
def __repr__(self):
306+
return f'{self.__class__.__name__}({self._capacity!r})'
307+
308+
269309
class ImageSlide(AbstractSlide):
270310
"""A wrapper for a PIL.Image that provides the OpenSlide interface."""
271311

@@ -378,6 +418,14 @@ def read_region(self, location, level, size):
378418
tile.paste(crop, tile_offset)
379419
return tile
380420

421+
def set_cache(self, cache):
422+
"""Use the specified cache to store recently decoded slide tiles.
423+
424+
ImageSlide does not support caching, so this method does nothing.
425+
426+
cache: an OpenSlideCache object."""
427+
pass
428+
381429

382430
def open_slide(filename):
383431
"""Open a whole-slide or regular image.

openslide/lowlevel.py

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# openslide-python - Python bindings for the OpenSlide library
33
#
44
# Copyright (c) 2010-2013 Carnegie Mellon University
5-
# Copyright (c) 2016 Benjamin Gilbert
5+
# Copyright (c) 2016-2021 Benjamin Gilbert
66
#
77
# This library is free software; you can redistribute it and/or modify it
88
# under the terms of version 2.1 of the GNU Lesser General Public License
@@ -37,6 +37,7 @@
3737
c_double,
3838
c_int32,
3939
c_int64,
40+
c_size_t,
4041
c_uint32,
4142
c_void_p,
4243
cdll,
@@ -90,6 +91,17 @@ class OpenSlideError(Exception):
9091
"""
9192

9293

94+
class OpenSlideVersionError(OpenSlideError):
95+
"""This version of OpenSlide does not support the requested functionality.
96+
97+
Import this from openslide rather than from openslide.lowlevel.
98+
"""
99+
100+
def __init__(self, minimum_version):
101+
super().__init__(f'OpenSlide >= {minimum_version} required')
102+
self.minimum_version = minimum_version
103+
104+
93105
class OpenSlideUnsupportedFormatError(OpenSlideError):
94106
"""OpenSlide does not support the requested file.
95107
@@ -125,6 +137,27 @@ def from_param(cls, obj):
125137
return obj
126138

127139

140+
class _OpenSlideCache:
141+
"""Wrapper class to make sure we correctly pass an OpenSlide cache."""
142+
143+
def __init__(self, ptr):
144+
self._as_parameter_ = ptr
145+
# Retain a reference to cache_release() to avoid GC problems during
146+
# interpreter shutdown
147+
self._cache_release = cache_release
148+
149+
def __del__(self):
150+
self._cache_release(self)
151+
152+
@classmethod
153+
def from_param(cls, obj):
154+
if obj.__class__ != cls:
155+
raise ValueError("Not an OpenSlide cache reference")
156+
if not obj._as_parameter_:
157+
raise ValueError("Passing undefined cache object")
158+
return obj
159+
160+
128161
class _utf8_p:
129162
"""Wrapper class to convert string arguments to bytes."""
130163

@@ -138,6 +171,18 @@ def from_param(cls, obj):
138171
raise TypeError('Incorrect type')
139172

140173

174+
class _size_t:
175+
"""Wrapper class to convert size_t arguments to c_size_t."""
176+
177+
@classmethod
178+
def from_param(cls, obj):
179+
if not isinstance(obj, int):
180+
raise TypeError('Incorrect type')
181+
if obj < 0:
182+
raise ValueError('Value out of range')
183+
return c_size_t(obj)
184+
185+
141186
def _load_image(buf, size):
142187
'''buf must be a mutable buffer.'''
143188
_convert.argb2rgba(buf)
@@ -160,6 +205,11 @@ def _check_close(_result, _func, args):
160205
args[0].invalidate()
161206

162207

208+
# wrap the handle returned when creating a cache
209+
def _check_cache_create(result, _func, _args):
210+
return _OpenSlideCache(c_void_p(result))
211+
212+
163213
# Convert returned byte array, if present, into a string
164214
def _check_string(result, func, _args):
165215
if func.restype is c_char_p and result is not None:
@@ -189,8 +239,18 @@ def _check_name_list(result, func, args):
189239

190240

191241
# resolve and return an OpenSlide function with the specified properties
192-
def _func(name, restype, argtypes, errcheck=_check_error):
193-
func = getattr(_lib, name)
242+
def _func(name, restype, argtypes, errcheck=_check_error, minimum_version=None):
243+
try:
244+
func = getattr(_lib, name)
245+
except AttributeError:
246+
if minimum_version is None:
247+
raise
248+
249+
# optional function doesn't exist; fail at runtime
250+
def function_unavailable(*_args):
251+
raise OpenSlideVersionError(minimum_version)
252+
253+
return function_unavailable
194254
func.argtypes = argtypes
195255
func.restype = restype
196256
if errcheck is not None:
@@ -201,7 +261,7 @@ def _func(name, restype, argtypes, errcheck=_check_error):
201261
try:
202262
detect_vendor = _func('openslide_detect_vendor', c_char_p, [_utf8_p], _check_string)
203263
except AttributeError:
204-
raise OpenSlideError('OpenSlide >= 3.4.0 required')
264+
raise OpenSlideVersionError('3.4.0')
205265

206266
open = _func('openslide_open', c_void_p, [_utf8_p], _check_open)
207267

@@ -295,3 +355,23 @@ def read_associated_image(slide, name):
295355

296356

297357
get_version = _func('openslide_get_version', c_char_p, [], _check_string)
358+
359+
cache_create = _func(
360+
'openslide_cache_create',
361+
c_void_p,
362+
[_size_t],
363+
_check_cache_create,
364+
minimum_version='3.5.0',
365+
)
366+
367+
set_cache = _func(
368+
'openslide_set_cache',
369+
None,
370+
[_OpenSlide, _OpenSlideCache],
371+
None,
372+
minimum_version='3.5.0',
373+
)
374+
375+
cache_release = _func(
376+
'openslide_cache_release', None, [_OpenSlideCache], None, minimum_version='3.5.0'
377+
)

tests/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#
22
# openslide-python - Python bindings for the OpenSlide library
33
#
4-
# Copyright (c) 2016 Benjamin Gilbert
4+
# Copyright (c) 2016-2021 Benjamin Gilbert
55
#
66
# This library is free software; you can redistribute it and/or modify it
77
# under the terms of version 2.1 of the GNU Lesser General Public License
@@ -17,6 +17,7 @@
1717
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
1818
#
1919

20+
from functools import wraps
2021
import os
2122
from pathlib import Path
2223

@@ -42,6 +43,7 @@
4243

4344
os.environ['PATH'] = _orig_path
4445

46+
from openslide import OpenSlideVersionError
4547

4648
# PIL.Image cannot have zero width or height on Pillow 3.4.0 - 3.4.2
4749
# https://github.com/python-pillow/Pillow/issues/2259
@@ -54,3 +56,17 @@
5456

5557
def file_path(name):
5658
return Path(__file__).parent / name
59+
60+
61+
def maybe_supported(f):
62+
'''Decorator to ignore test failures caused by an OpenSlide version that
63+
doesn't support the tested functionality.'''
64+
65+
@wraps(f)
66+
def wrapper(*args, **kwargs):
67+
try:
68+
return f(*args, **kwargs)
69+
except OpenSlideVersionError:
70+
pass
71+
72+
return wrapper

0 commit comments

Comments
 (0)