Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support reading OME Tiff files from sub-ifds. #469

Merged
merged 5 commits into from
Sep 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 36 additions & 19 deletions sources/ometiff/large_image_source_ometiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,14 @@ def __init__(self, path, **kwargs):
self._omeLevels = [omebylevel.get(key) for key in range(max(omebylevel.keys()) + 1)]
if base._tiffInfo.get('istiled'):
self._tiffDirectories = [
TiledTiffDirectory(largeImagePath, int(entry['TiffData'][0]['IFD']))
TiledTiffDirectory(largeImagePath, int(entry['TiffData'][0].get('IFD', 0)))
if entry else None
for entry in self._omeLevels]
else:
self._tiffDirectories = [
TiledTiffDirectory(largeImagePath, 0, mustBeTiled=None)
if entry else None
for entry in self._omeLevels]
self._directoryCache = {}
self._directoryCacheMaxSize = max(20, len(self._omebase['TiffData']) * 3)
self.tileWidth = base.tileWidth
self.tileHeight = base.tileHeight
self.levels = len(self._tiffDirectories)
Expand Down Expand Up @@ -184,37 +182,47 @@ def _checkForOMEZLoop(self, largeImagePath):
info['Image']['Pixels']['PlanesFromZloop'] = 'true'
info['Image']['Pixels']['SizeZ'] = str(zloop)

def _parseOMEInfo(self):
def _parseOMEInfo(self): # noqa
if isinstance(self._omeinfo['Image'], dict):
self._omeinfo['Image'] = [self._omeinfo['Image']]
for img in self._omeinfo['Image']:
if isinstance(img['Pixels'].get('TiffData'), dict):
img['Pixels']['TiffData'] = [img['Pixels']['TiffData']]
if isinstance(img['Pixels'].get('Plane'), dict):
img['Pixels']['Plane'] = [img['Pixels']['Plane']]
if isinstance(img['Pixels'].get('Channels'), dict):
img['Pixels']['Channels'] = [img['Pixels']['Channels']]
try:
self._omebase = self._omeinfo['Image'][0]['Pixels']
if isinstance(self._omebase.get('Plane'), dict):
self._omebase['Plane'] = [self._omebase['Plane']]
if ((not len(self._omebase['TiffData']) or
len(self._omebase['TiffData']) == 1) and
len(self._omebase['Plane'])):
(len(self._omebase.get('Plane', [])) or
len(self._omebase.get('Channel', [])))):
if not len(self._omebase['TiffData']) or self._omebase['TiffData'][0] == {}:
self._omebase['TiffData'] = self._omebase['Plane']
self._omebase['TiffData'] = self._omebase.get(
'Plane', self._omebase.get('Channel'))
elif (int(self._omebase['TiffData'][0].get('PlaneCount', 0)) ==
len(self._omebase['Plane'])):
planes = copy.deepcopy(self._omebase['Plane'])
len(self._omebase.get('Plane', self._omebase.get('Channel', [])))):
planes = copy.deepcopy(self._omebase.get('Plane', self._omebase.get('Channel')))
for idx, plane in enumerate(planes):
plane['IFD'] = plane.get(
'IFD', int(self._omebase['TiffData'][0].get('IFD', 0)) + idx)
self._omebase['TiffData'] = planes
if isinstance(self._omebase['TiffData'], dict):
self._omebase['TiffData'] = [self._omebase['TiffData']]
if len({entry.get('UUID', {}).get('FileName', '')
for entry in self._omebase['TiffData']}) > 1:
raise TileSourceException('OME Tiff references multiple files')
if (len(self._omebase['TiffData']) != int(self._omebase['SizeC']) *
int(self._omebase['SizeT']) * int(self._omebase['SizeZ']) or
len(self._omebase['TiffData']) != len(
self._omebase.get('Plane', self._omebase['TiffData']))):
raise TileSourceException('OME Tiff contains frames that contain multiple planes')
raise TileSourceException(
'OME Tiff contains frames that contain multiple planes')
except (KeyError, ValueError, IndexError):
print('B')
raise TileSourceException('OME Tiff does not contain an expected record')

def getMetadata(self):
Expand Down Expand Up @@ -282,23 +290,32 @@ def getNativeMagnification(self):
@methodcache()
def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False,
sparseFallback=False, **kwargs):
if (z < 0 or z >= len(self._omeLevels) or self._omeLevels[z] is None or
kwargs.get('frame') in (None, 0, '0', '')):
if (z < 0 or z >= len(self._omeLevels) or (
self._omeLevels[z] is not None and kwargs.get('frame') in (None, 0, '0', ''))):
return super(OMETiffFileTileSource, self).getTile(
x, y, z, pilImageAllowed=pilImageAllowed,
numpyAllowed=numpyAllowed, sparseFallback=sparseFallback,
**kwargs)
frame = int(kwargs['frame'])
frame = int(kwargs.get('frame') or 0)
if frame < 0 or frame >= len(self._omebase['TiffData']):
raise TileSourceException('Frame does not exist')
dirnum = int(self._omeLevels[z]['TiffData'][frame].get('IFD', frame))
if dirnum in self._directoryCache:
dir = self._directoryCache[dirnum]
subdir = None
if self._omeLevels[z] is not None:
dirnum = int(self._omeLevels[z]['TiffData'][frame].get('IFD', frame))
else:
if len(self._directoryCache) >= self._directoryCacheMaxSize:
self._directoryCache = {}
dir = TiledTiffDirectory(self._getLargeImagePath(), dirnum, mustBeTiled=None)
self._directoryCache[dirnum] = dir
dirnum = int(self._omeLevels[-1]['TiffData'][frame].get('IFD', frame))
subdir = self.levels - 1 - z
dir = self._getDirFromCache(dirnum, subdir)
if subdir:
scale = int(2 ** subdir)
if (dir is None or
dir.tileWidth != self.tileWidth or dir.tileHeight != self.tileHeight or
abs(dir.imageWidth * scale - self.sizeX) > scale or
abs(dir.imageHeight * scale - self.sizeY) > scale):
return super(OMETiffFileTileSource, self).getTile(
x, y, z, pilImageAllowed=pilImageAllowed,
numpyAllowed=numpyAllowed, sparseFallback=sparseFallback,
**kwargs)
try:
tile = dir.getTile(x, y)
format = 'JPEG'
Expand Down
107 changes: 73 additions & 34 deletions sources/tiff/large_image_source_tiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,41 +83,12 @@ def __init__(self, path, **kwargs):
super(TiffFileTileSource, self).__init__(path, **kwargs)

largeImagePath = self._getLargeImagePath()
lastException = None
# Associated images are smallish TIFF images that have an image
# description and are not tiled. They have their own TIFF directory.
# Individual TIFF images can also have images embedded into their
# directory as tags (this is a vendor-specific method of adding more
# images into a file) -- those are stored in the individual
# directories' _embeddedImages field.
self._associatedImages = {}
try:
alldir = self._scanDirectories()
except (ValidationTiffException, TiffException) as exc:
alldir = []
lastException = exc

# Query all know directories in the tif file. Only keep track of
# directories that contain tiled images.
alldir = []
for directoryNum in itertools.count(): # pragma: no branch
try:
td = TiledTiffDirectory(largeImagePath, directoryNum)
except ValidationTiffException as exc:
lastException = exc
self._addAssociatedImage(largeImagePath, directoryNum)
continue
except TiffException as exc:
if not lastException:
lastException = exc
break
if not td.tileWidth or not td.tileHeight:
continue
# Calculate the tile level, where 0 is a single tile, 1 is up to a
# set of 2x2 tiles, 2 is 4x4, etc.
level = int(math.ceil(math.log(max(
float(td.imageWidth) / td.tileWidth,
float(td.imageHeight) / td.tileHeight)) / math.log(2)))
if level < 0:
continue
# Store information for sorting with the directory.
alldir.append((level > 0, td.tileWidth * td.tileHeight, level,
td.imageWidth * td.imageHeight, directoryNum, td))
# If there are no tiled images, raise an exception.
if not len(alldir):
msg = "File %s didn't meet requirements for tile source: %s" % (
Expand Down Expand Up @@ -162,6 +133,57 @@ def __init__(self, path, **kwargs):
self.sizeX = highest.imageWidth
self.sizeY = highest.imageHeight

def _scanDirectories(self):
largeImagePath = self._getLargeImagePath()
lastException = None
# Associated images are smallish TIFF images that have an image
# description and are not tiled. They have their own TIFF directory.
# Individual TIFF images can also have images embedded into their
# directory as tags (this is a vendor-specific method of adding more
# images into a file) -- those are stored in the individual
# directories' _embeddedImages field.
self._associatedImages = {}

dir = None
# Query all know directories in the tif file. Only keep track of
# directories that contain tiled images.
alldir = []
associatedDirs = []
for directoryNum in itertools.count(): # pragma: no branch
try:
if dir is None:
dir = TiledTiffDirectory(largeImagePath, directoryNum, validate=False)
else:
dir._setDirectory(directoryNum)
dir._loadMetadata()
dir._validate()
except ValidationTiffException as exc:
lastException = exc
associatedDirs.append(directoryNum)
continue
except TiffException as exc:
if not lastException:
lastException = exc
break
if not dir.tileWidth or not dir.tileHeight:
continue
# Calculate the tile level, where 0 is a single tile, 1 is up to a
# set of 2x2 tiles, 2 is 4x4, etc.
level = int(math.ceil(math.log(max(
float(dir.imageWidth) / dir.tileWidth,
float(dir.imageHeight) / dir.tileHeight)) / math.log(2)))
if level < 0:
continue
td, dir = dir, None
# Store information for sorting with the directory.
alldir.append((level > 0, td.tileWidth * td.tileHeight, level,
td.imageWidth * td.imageHeight, directoryNum, td))
if not alldir and lastException:
raise lastException
for directoryNum in associatedDirs:
self._addAssociatedImage(largeImagePath, directoryNum)
return alldir

def _addAssociatedImage(self, largeImagePath, directoryNum, mustBeTiled=False, topImage=None):
"""
Check if the specified TIFF directory contains an image with a sensible
Expand Down Expand Up @@ -349,6 +371,23 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False,
numpyAllowed=numpyAllowed, sparseFallback=sparseFallback,
exception=e, **kwargs)

def _getDirFromCache(self, dirnum, subdir=None):
if not hasattr(self, '_directoryCache'):
self._directoryCache = {}
self._directoryCacheMaxSize = max(20, self.levels * 3)
key = (dirnum, subdir)
result = self._directoryCache.get(key)
if result is None:
if len(self._directoryCache) >= self._directoryCacheMaxSize:
self._directoryCache = {}
try:
result = TiledTiffDirectory(
self._getLargeImagePath(), dirnum, mustBeTiled=None, subDirectoryNum=subdir)
except IOTiffException:
result = None
self._directoryCache[key] = result
return result

def getTileIOTiffException(self, x, y, z, pilImageAllowed=False,
numpyAllowed=False, sparseFallback=False,
exception=None, **kwargs):
Expand Down
47 changes: 34 additions & 13 deletions sources/tiff/large_image_source_tiff/tiff_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,55 +99,62 @@ class ValidationTiffException(TiffException):
class TiledTiffDirectory(object):

CoreFunctions = [
'SetDirectory', 'GetField', 'LastDirectory', 'GetMode', 'IsTiled',
'IsByteSwapped', 'IsUpSampled', 'IsMSB2LSB', 'NumberOfStrips'
'SetDirectory', 'SetSubDirectory', 'GetField',
'LastDirectory', 'GetMode', 'IsTiled', 'IsByteSwapped', 'IsUpSampled',
'IsMSB2LSB', 'NumberOfStrips',
]

def __init__(self, filePath, directoryNum, mustBeTiled=True):
def __init__(self, filePath, directoryNum, mustBeTiled=True, subDirectoryNum=0, validate=True):
"""
Create a new reader for a tiled image file directory in a TIFF file.

:param filePath: A path to a TIFF file on disk.
:type filePath: str
:param directoryNum: The number of the TIFF image file directory to
open.
open.
:type directoryNum: int
:param mustBeTiled: if True, only tiled images validate. If False,
only non-tiled images validate. None validates both.
:type mustBeTiled: bool
:param subDirectoryNum: if set, the number of the TIFF subdirectory.
:type subDirectoryNum: int
:param validate: if False, don't validate that images can be read.
:type mustBeTiled: bool
:raises: InvalidOperationTiffException or IOTiffException or
ValidationTiffException
"""
# TODO how many to keep in the cache
# create local cache to store Jpeg tables and
# getTileByteCountsType

# create local cache to store Jpeg tables and getTileByteCountsType
self.cache = LRUCache(10)
self._mustBeTiled = mustBeTiled

self._tiffFile = None
self._tileLock = threading.RLock()

self._open(filePath, directoryNum)
self._open(filePath, directoryNum, subDirectoryNum)
self._loadMetadata()
config.getConfig('logger').debug(
'TiffDirectory %d Information %r', directoryNum, self._tiffInfo)
'TiffDirectory %d:%d Information %r',
directoryNum, subDirectoryNum or 0, self._tiffInfo)
try:
self._validate()
if validate:
self._validate()
except ValidationTiffException:
self._close()
raise

def __del__(self):
self._close()

def _open(self, filePath, directoryNum):
def _open(self, filePath, directoryNum, subDirectoryNum=0):
"""
Open a TIFF file to a given file and IFD number.

:param filePath: A path to a TIFF file on disk.
:type filePath: str
:param directoryNum: The number of the TIFF IFD to be used.
:type directoryNum: int
:param subDirectoryNum: The number of the TIFF sub-IFD to be used.
:type subDirectoryNum: int
:raises: InvalidOperationTiffException or IOTiffException
"""
self._close()
Expand All @@ -170,12 +177,26 @@ def _open(self, filePath, directoryNum):
hasattr(self._tiffFile, func.lower())):
setattr(self._tiffFile, func, getattr(
self._tiffFile, func.lower()))
self._setDirectory(directoryNum, subDirectoryNum)

def _setDirectory(self, directoryNum, subDirectoryNum=0):
self._directoryNum = directoryNum
if self._tiffFile.SetDirectory(self._directoryNum) != 1:
self._tiffFile.close()
raise IOTiffException(
'Could not set TIFF directory to %d' % directoryNum)
self._subDirectoryNum = subDirectoryNum
if self._subDirectoryNum:
subifds = self._tiffFile.GetField('subifd')
if (subifds is None or self._subDirectoryNum < 1 or
self._subDirectoryNum > len(subifds)):
raise IOTiffException(
'Could not set TIFF subdirectory to %d' % subDirectoryNum)
subifd = subifds[self._subDirectoryNum - 1]
if self._tiffFile.SetSubDirectory(subifd) != 1:
self._tiffFile.close()
raise IOTiffException(
'Could not set TIFF subdirectory to %d' % subDirectoryNum)

def _close(self):
if self._tiffFile:
Expand Down Expand Up @@ -277,7 +298,7 @@ def _loadMetadata(self):
'Loading field "%s" in directory number %d resulted in TypeError - "%s"',
field, self._directoryNum, err)

for func in self.CoreFunctions[2:]:
for func in self.CoreFunctions[3:]:
if hasattr(self._tiffFile, func):
value = getattr(self._tiffFile, func)()
if value:
Expand Down
1 change: 1 addition & 0 deletions test/data/sample.subifd.ome.tif.sha512
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
35ec252c94b1ad0b9d5bd42c89c1d15c83065d6734100d6f596237ff36e8d4495bcfed2c9ea24ab0b4a35aef59871da429dbd48faf0232219dc4391215ba59ce
10 changes: 5 additions & 5 deletions test/test_cached_tiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,20 @@ def countInit(*args, **kwargs):
self.delCount = 0
source = large_image.getTileSource(imagePath)
assert source is not None
assert self.initCount == 14
assert self.delCount < 14
assert self.initCount == 12
assert self.delCount < 12
# Create another source; we shouldn't init it again, as it should be
# cached.
source = large_image.getTileSource(imagePath)
assert source is not None
assert self.initCount == 14
assert self.delCount < 14
assert self.initCount == 12
assert self.delCount < 12
source = None
# Clear the cache to free references and force garbage collection
cachesClear()
gc.collect(2)
cachesClear()
assert self.delCount == 14
assert self.delCount == 12


class TestMemcachedCache(LargeImageCachedTilesTest):
Expand Down
Loading