Skip to content

Commit 13aaeb8

Browse files
committed
Add ICC profile in _AssociatedImageMap
If an associated image has a profile and it's equal to the one from the main image, reuse the latter to avoid keeping multiple copies in RAM. Add a synthetic DICOM fixture so we can use OpenSlide's DICOM driver to test this. The fixture was created by hand-hacking an ICC profile into small.svs and then converting it to DICOM with com.pixelmed.convert.TIFFToDicom. For openslide/openslide#477. Signed-off-by: Benjamin Gilbert <[email protected]>
1 parent 1cb52ce commit 13aaeb8

File tree

6 files changed

+59
-3
lines changed

6 files changed

+59
-3
lines changed

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
include *.md pytest.ini
22
recursive-include doc *.py *.rst
33
recursive-include examples *.html *.js *.png *.py
4-
recursive-include tests *.png *.py *.svs *.tiff
4+
recursive-include tests *.dcm *.png *.py *.svs *.tiff

openslide/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def associated_images(self):
222222
223223
Unlike in the C interface, the images accessible via this property
224224
are not premultiplied."""
225-
return _AssociatedImageMap(self._osr)
225+
return _AssociatedImageMap(self._osr, self._profile)
226226

227227
def get_best_level_for_downsample(self, downsample):
228228
"""Return the best level for displaying the given downsample."""
@@ -288,13 +288,25 @@ def __getitem__(self, key):
288288

289289

290290
class _AssociatedImageMap(_OpenSlideMap):
291+
def __init__(self, osr, profile):
292+
_OpenSlideMap.__init__(self, osr)
293+
self._profile = profile
294+
291295
def _keys(self):
292296
return lowlevel.get_associated_image_names(self._osr)
293297

294298
def __getitem__(self, key):
295299
if key not in self._keys():
296300
raise KeyError()
297-
return lowlevel.read_associated_image(self._osr, key)
301+
image = lowlevel.read_associated_image(self._osr, key)
302+
if lowlevel.read_associated_image_icc_profile.available:
303+
profile = lowlevel.read_associated_image_icc_profile(self._osr, key)
304+
if profile == self._profile:
305+
# reuse profile copy from main image to save memory
306+
profile = self._profile
307+
if profile is not None:
308+
image.info['icc_profile'] = profile
309+
return image
298310

299311

300312
class OpenSlideCache:

openslide/lowlevel.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,33 @@ def read_associated_image(slide, name):
405405
return _load_image(buf, (w, h))
406406

407407

408+
get_associated_image_icc_profile_size = _func(
409+
'openslide_get_associated_image_icc_profile_size',
410+
c_int64,
411+
[_OpenSlide, _utf8_p],
412+
minimum_version='4.0.0',
413+
)
414+
415+
_read_associated_image_icc_profile = _func(
416+
'openslide_read_associated_image_icc_profile',
417+
None,
418+
[_OpenSlide, _utf8_p, POINTER(c_char)],
419+
minimum_version='4.0.0',
420+
)
421+
422+
423+
@_wraps_funcs(
424+
[get_associated_image_icc_profile_size, _read_associated_image_icc_profile]
425+
)
426+
def read_associated_image_icc_profile(slide, name):
427+
size = get_associated_image_icc_profile_size(slide, name)
428+
if size == 0:
429+
return None
430+
buf = (size * c_char)()
431+
_read_associated_image_icc_profile(slide, name, buf)
432+
return buf.raw
433+
434+
408435
get_version = _func('openslide_get_version', c_char_p, [], _check_string)
409436

410437
cache_create = _func(

tests/fixtures/boxes_0.dcm

5.37 KB
Binary file not shown.

tests/fixtures/boxes_1.dcm

5.53 KB
Binary file not shown.

tests/test_openslide.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,23 @@ def test_color_profile(self):
196196
self.assertNotIn(
197197
'icc_profile', self.osr.read_region((0, 0), 0, (100, 100)).info
198198
)
199+
self.assertNotIn('icc_profile', self.osr.associated_images['thumbnail'].info)
200+
201+
202+
# Requires DICOM support in OpenSlide. Use associated image ICC support as
203+
# a proxy.
204+
@unittest.skipUnless(
205+
lowlevel.read_associated_image_icc_profile.available, "requires OpenSlide 4.0.0"
206+
)
207+
class TestDicomSlide(_SlideTest, unittest.TestCase):
208+
FILENAME = 'boxes_0.dcm'
209+
210+
def test_color_profile(self):
211+
main_profile = self.osr.read_region((0, 0), 0, (100, 100)).info['icc_profile']
212+
associated_profile = self.osr.associated_images['thumbnail'].info['icc_profile']
213+
self.assertEqual(len(main_profile), 456)
214+
self.assertEqual(main_profile, associated_profile)
215+
self.assertIs(main_profile, associated_profile)
199216

200217

201218
class TestUnreadableSlide(_SlideTest, unittest.TestCase):

0 commit comments

Comments
 (0)