From 20af0f017ded309178ace3321ff25948ab69ee69 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 31 Mar 2020 12:13:43 -0400 Subject: [PATCH] Standardize frame metadata. Any tile source that exposes frames will have a frames list. This list now will always contain Frame and Index values, and, where appropriate IndexC, IndexT, IndexZ, IndexXY values. The top-level metadata will always have IndexRange and IndexStride entries. When known, the top-level metadata will have channels and channelmap entries with channel names. --- large_image/tilesource/base.py | 22 ++++++++++ .../nd2/large_image_source_nd2/__init__.py | 25 +++++++++-- .../large_image_source_ometiff/__init__.py | 44 +++++++++++++++++++ test/test_source_nd2.py | 7 +++ test/test_source_ometiff.py | 10 +++++ 5 files changed, 104 insertions(+), 4 deletions(-) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index e2cc1cdc2..1afb7a0ec 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -1454,6 +1454,28 @@ def canRead(cls, *args, **kwargs): return False def getMetadata(self): + """ + Return metadata about this tile source. In addition to the keys that + are listed in this template function, tile sources that expose multiple + frames will also contain: + - frames: a list of frames. Each frame entry is a dictionary with + - Frame: a 0-values frame index (the location in the list) + - Channel (optional): the name of the channel, if known + - IndexC (optional if unique): a 0-based index into the channel list + - IndexT (optional if unique): a 0-based index for time values + - IndexZ (optional if unique): a 0-based index for z values + - IndexXY (optional if unique): a 0-based index for view (xy) values + - Index: a 0-based index of non-channel unique sets. If the frames + vary only by channel and are adjacent, they will have the same + index. + - IndexRange: a dictionary of the number of unique index values from + frames if greater than 1 (e.g., if an entry like IndexXY is not + present, then all frames either do not have that value or have a + value of 0). + - channels (optional): if known, a list of channel names + - channelmap (optional): if known, a dictionary of channel names with + their offset into the channel list. + """ mag = self.getNativeMagnification() return { 'levels': self.levels, diff --git a/sources/nd2/large_image_source_nd2/__init__.py b/sources/nd2/large_image_source_nd2/__init__.py index 03d511d9e..6122bb9ae 100644 --- a/sources/nd2/large_image_source_nd2/__init__.py +++ b/sources/nd2/large_image_source_nd2/__init__.py @@ -259,13 +259,17 @@ def getMetadata(self): # noqa # } axes = self._nd2.iter_axes[::-1] result['frames'] = frames = [] + maxref = {} for idx in range(len(self._nd2)): - frame = {'Frame': idx, 'TheZ': 0, 'TheV': 0} + frame = {'Frame': idx, 'IndexZ': 0, 'IndexXY': 0} basis = 1 ref = {} for axis in axes: ref[axis] = (idx // basis) % sizes[axis] - frame['The' + axis.upper()] = (idx // basis) % sizes[axis] + frame['Index' + (axis.upper() if axis != 'v' else 'XY')] = ( + idx // basis) % sizes[axis] + if ref[axis] + 1 > maxref.get(axis, 0): + maxref[axis] = ref[axis] + 1 basis *= sizes.get(axis, 1) if ('channels' in self._metadata and 'c' in ref and ref['c'] < len(self._metadata['channels'])): @@ -289,11 +293,24 @@ def getMetadata(self): # noqa ]: if mkey in self._metadata: frame[fkey] = self._metadata[mkey][cdidx % len(self._metadata[mkey])] - frame['IndexXY'] = ref.get('v', 0) - frame['IndexZ'] = ref.get('z', 0) frames.append(frame) if self._framecount and len(frames) == self._framecount: break + if ('channels' in self._metadata and + len(self._metadata['channels']) >= maxref.get('c', 1) and + len(set(self._metadata['channels'])) == len(self._metadata['channels'])): + result['channels'] = self._metadata['channels'][:maxref.get('c', 1)] + result['channelmap'] = { + cname: c for c, cname in enumerate(self._metadata['channels'][:maxref.get('c', 1)])} + if any(val > 1 for val in maxref.values()): + result['IndexRange'] = { + 'Index' + (axis.upper() if axis != 'v' else 'XY'): value + for axis, value in maxref.items() if value > 1 + } + result['IndexStride'] = { + key: [idx for idx, frame in enumerate(result['frames']) if frame[key] == 1][0] + for key in result['IndexRange'] + } return result @methodcache() diff --git a/sources/ometiff/large_image_source_ometiff/__init__.py b/sources/ometiff/large_image_source_ometiff/__init__.py index 52936c798..9c11894a3 100644 --- a/sources/ometiff/large_image_source_ometiff/__init__.py +++ b/sources/ometiff/large_image_source_ometiff/__init__.py @@ -20,6 +20,7 @@ import numpy import PIL.Image import six +from collections import OrderedDict from pkg_resources import DistributionNotFound, get_distribution from six.moves import range @@ -215,6 +216,49 @@ def getMetadata(self): result = super(OMETiffFileTileSource, self).getMetadata() # We may want to reformat the frames to standardize this across sources result['frames'] = self._omebase.get('Plane', self._omebase['TiffData']) + # Expose channel information + channels = [] + for img in self._omeinfo['Image']: + try: + channels = [channel['Name'] for channel in img['Pixels']['Channel']] + if len(channels) > 1: + break + except Exception: + pass + if len(set(channels)) != len(channels): + channels = [] + # Standardize "TheX" to "IndexX" values + reftbl = OrderedDict([ + ('TheC', 'IndexC'), ('TheZ', 'IndexZ'), ('TheT', 'IndexT'), + ('FirstC', 'IndexC'), ('FirstZ', 'IndexZ'), ('FirstT', 'IndexT'), + ]) + maxref = {} + index = 0 + for idx, frame in enumerate(result['frames']): + for key in reftbl: + if key in frame and not reftbl[key] in frame: + frame[reftbl[key]] = int(frame[key]) + if frame[reftbl[key]] + 1 > maxref.get(reftbl[key], 0): + maxref[reftbl[key]] = frame[reftbl[key]] + 1 + frame['Frame'] = idx + if (idx and ( + frame.get('IndexV') != result['frames'][idx - 1].get('IndexV') or + frame.get('IndexZ') != result['frames'][idx - 1].get('IndexZ'))): + index += 1 + frame['Index'] = index + if any(val > 1 for val in maxref.values()): + result['IndexRange'] = {key: value for key, value in maxref.items() if value > 1} + result['IndexStride'] = { + key: [idx for idx, frame in enumerate(result['frames']) if frame[key] == 1][0] + for key in result['IndexRange'] + } + # Add channel information + if len(channels) >= maxref.get('IndexC', 1): + result['channels'] = channels[:maxref.get('IndexC', 1)] + result['channelmap'] = { + cname: c for c, cname in enumerate(channels[:maxref.get('IndexC', 1)])} + for frame in result['frames']: + frame['Channel'] = channels[frame.get('IndexC', 0)] result['omeinfo'] = self._omeinfo return result diff --git a/test/test_source_nd2.py b/test/test_source_nd2.py index eddfdec7c..97b6af07a 100644 --- a/test/test_source_nd2.py +++ b/test/test_source_nd2.py @@ -19,4 +19,11 @@ def testTilesFromND2(): assert tileMetadata['levels'] == 3 assert tileMetadata['magnification'] == pytest.approx(47, 1) assert len(tileMetadata['frames']) == 232 + assert tileMetadata['frames'][201]['Frame'] == 201 + assert tileMetadata['frames'][201]['Index'] == 50 + assert tileMetadata['frames'][201]['IndexC'] == 1 + assert tileMetadata['frames'][201]['IndexXY'] == 1 + assert tileMetadata['frames'][201]['IndexZ'] == 21 + assert tileMetadata['channels'] == ['Brightfield', 'YFP', 'A594', 'DAPI'] + assert tileMetadata['IndexRange'] == {'IndexC': 4, 'IndexXY': 2, 'IndexZ': 29} utilities.checkTilesZXY(source, tileMetadata) diff --git a/test/test_source_ometiff.py b/test/test_source_ometiff.py index d11063a79..6b6d9a954 100644 --- a/test/test_source_ometiff.py +++ b/test/test_source_ometiff.py @@ -20,6 +20,10 @@ def testTilesFromOMETiff(): assert tileMetadata['sizeY'] == 2016 assert tileMetadata['levels'] == 3 assert len(tileMetadata['frames']) == 3 + assert tileMetadata['frames'][1]['Frame'] == 1 + assert tileMetadata['frames'][1]['Index'] == 0 + assert tileMetadata['frames'][1]['IndexC'] == 1 + assert tileMetadata['IndexRange'] == {'IndexC': 3} utilities.checkTilesZXY(source, tileMetadata) @@ -34,6 +38,12 @@ def testTilesFromStripOMETiff(): assert tileMetadata['sizeY'] == 1022 assert tileMetadata['levels'] == 3 assert len(tileMetadata['frames']) == 145 + assert tileMetadata['frames'][101]['Frame'] == 101 + assert tileMetadata['frames'][101]['Index'] == 20 + assert tileMetadata['frames'][101]['IndexC'] == 1 + assert tileMetadata['frames'][101]['IndexZ'] == 20 + assert tileMetadata['channels'] == ['Brightfield', 'CY3', 'A594', 'CY5', 'DAPI'] + assert tileMetadata['IndexRange'] == {'IndexC': 5, 'IndexZ': 29} utilities.checkTilesZXY(source, tileMetadata)