From 6b7929a855bd91cadd34f052b027bb918b1b3fbf Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 1 Oct 2020 10:04:36 -0400 Subject: [PATCH] Show additional associated image for some openslides. Openslide only exposes associated images from Aperio if they have an ImageDescription with their name. For Python>3.6, if you also have appropriate tiff libraries installed (for instance, from the tiff tile source), this will expose label and macro images based on other indicators. For Hamamatsu files, only the macro image is exposed. There is another image that can be present, but this is the non-empty region map. --- Dockerfile | 16 +---- .../large_image_source_openslide/__init__.py | 64 ++++++++++++++++--- sources/openslide/setup.py | 1 + 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/Dockerfile b/Dockerfile index 59ad2b005..28b7b5ba8 100755 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,6 @@ ENV DEBIAN_FRONTEND=noninteractive \ LANG=en_US.UTF-8 \ PYENV_ROOT="/.pyenv" \ PATH="/.pyenv/bin:/.pyenv/shims:$PATH" \ - GOSU_VERSION=1.10 \ PYTHON_VERSIONS="3.7.9 2.7.18 3.5.9 3.6.12 3.8.6" \ LOCAL_PYTHON_VERSION="3.7.9" # PYTHON_VERSIONS="2.7.18 3.5.9 3.6.12 3.7.9 3.8.6 pypy2.7-7.3.1 pypy3.5-7.0.0 pypy3.6-7.3.1" @@ -26,6 +25,7 @@ RUN apt-get update && \ fonts-dejavu \ fuse \ git \ + gosu \ gpg-agent \ libbz2-dev \ libffi-dev \ @@ -61,22 +61,10 @@ RUN pyenv update && \ find $PYENV_ROOT/versions -type f '(' -name '*.py[co]' -o -name '*.exe' ')' -exec rm -fv '{}' + && \ echo $PYTHON_VERSIONS | tr " " "\n" > $PYENV_ROOT/version +# Create a user that can be used with gosu or chroot when running tox RUN groupadd -r tox --gid=999 && \ useradd -m -r -g tox --uid=999 tox -# Install gosu to run tox as the "tox" user instead of as root. -# https://github.com/tianon/gosu#from-debian -RUN set -x && \ - dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" && \ - wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" && \ - wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc" && \ - export GNUPGHOME="$(mktemp -d)" && \ - gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 && \ - gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu && \ - rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc && \ - chmod +x /usr/local/bin/gosu && \ - gosu nobody true - RUN pyenv local ${PYTHON_VERSIONS%% *} && \ python -m pip install -U pip && \ python -m pip install tox && \ diff --git a/sources/openslide/large_image_source_openslide/__init__.py b/sources/openslide/large_image_source_openslide/__init__.py index 186a9273b..8593b43bb 100644 --- a/sources/openslide/large_image_source_openslide/__init__.py +++ b/sources/openslide/large_image_source_openslide/__init__.py @@ -25,12 +25,22 @@ import PIL from pkg_resources import DistributionNotFound, get_distribution +try: + import tifftools +except ImportError: + tifftools = None + from large_image import config from large_image.cache_util import LruCacheMetaclass, methodcache from large_image.constants import SourcePriority, TILE_FORMAT_PIL from large_image.exceptions import TileSourceException from large_image.tilesource import FileTileSource, nearPowerOfTwo +try: + from libtiff import libtiff_ctypes +except ImportError: + libtiff_ctypes = False + try: __version__ = get_distribution(__name__).version @@ -85,6 +95,11 @@ def __init__(self, path, **kwargs): raise TileSourceException('File cannot be opened via OpenSlide.') except openslide.lowlevel.OpenSlideError: raise TileSourceException('File will not be opened via OpenSlide.') + if tifftools and libtiff_ctypes: + try: + self._tiffinfo = tifftools.read_tiff(largeImagePath) + except Exception: + pass svsAvailableLevels = self._getAvailableLevels(largeImagePath) if not len(svsAvailableLevels): @@ -309,16 +324,38 @@ def getPreferredLevel(self, level): scale /= 2 return level + def _getAssociatedImagesDict(self): + images = {} + try: + for key in self._openslide.associated_images: + images[key] = 'openslide' + except openslide.lowlevel.OpenSlideError: + pass + if hasattr(self, '_tiffinfo'): + vendor = self._openslide.properties['openslide.vendor'] + for ifdidx, ifd in enumerate(self._tiffinfo['ifds']): + key = None + if vendor == 'hamamatsu': + if tifftools.Tag.NDPI_SOURCELENS.value in ifd['tags']: + lens = ifd['tags'][tifftools.Tag.NDPI_SOURCELENS.value]['data'][0] + key = {-1: 'macro', -2: 'nonempty'}.get(lens) + elif vendor == 'aperio': + if (ifd['tags'].get(tifftools.Tag.NewSubfileType.value) and + ifd['tags'][tifftools.Tag.NewSubfileType.value]['data'][0] & + tifftools.Tag.NewSubfileType.bitfield.Page.value): + key = 'label' if ifd['tags'][ + tifftools.Tag.NewSubfileType.value]['data'][0] == 1 else 'macro' + if key and key not in images: + images[key] = ifdidx + return images + def getAssociatedImagesList(self): """ Get a list of all associated images. :return: the list of image keys. """ - try: - return sorted(self._openslide.associated_images) - except openslide.lowlevel.OpenSlideError: - return [] + return sorted(self._getAssociatedImagesDict().keys()) def _getAssociatedImage(self, imageKey): """ @@ -327,9 +364,18 @@ def _getAssociatedImage(self, imageKey): :param imageKey: the key of the associated image. :return: the image in PIL format or None. """ - try: - if imageKey in self._openslide.associated_images: + images = self._getAssociatedImagesDict() + if imageKey not in images: + return None + if images[imageKey] == 'openslide': + try: return self._openslide.associated_images[imageKey] - except openslide.lowlevel.OpenSlideError: - pass - return None + except openslide.lowlevel.OpenSlideError: + return None + bytePath = self._getLargeImagePath() + if not isinstance(bytePath, six.binary_type): + bytePath = bytePath.encode('utf8') + _tiffFile = libtiff_ctypes.TIFF.open(bytePath) + _tiffFile.SetDirectory(images[imageKey]) + img = _tiffFile.read_image() + return PIL.Image.fromarray(img) diff --git a/sources/openslide/setup.py b/sources/openslide/setup.py index 3628f3ba8..15ee8aedf 100644 --- a/sources/openslide/setup.py +++ b/sources/openslide/setup.py @@ -44,6 +44,7 @@ def prerelease_local_scheme(version): install_requires=[ 'large-image>=1.0.0', 'openslide-python>=1.1.0', + 'tifftools;python_version>="3.6"', ], extras_require={ 'girder': 'girder-large-image>=1.0.0',