Skip to content

Commit

Permalink
Standardize frame metadata.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
manthey committed Mar 31, 2020
1 parent 6ea920a commit 4888393
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 14 deletions.
22 changes: 22 additions & 0 deletions large_image/tilesource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
44 changes: 30 additions & 14 deletions sources/nd2/large_image_source_nd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ def getMetadata(self): # noqa
result['nd2'].pop('image_metadata', None)
result['nd2'].pop('image_metadata_sequence', None)
result['nd2_sizes'] = sizes = self._nd2.sizes
result['nd2_axes'] = baseaxes = self._nd2.axes
result['nd2_axes'] = self._nd2.axes
result['nd2_iter_axes'] = self._nd2.iter_axes
# We may want to reformat the frames to standardize this across sources
# An example of frames from OMETiff: {
Expand All @@ -259,27 +259,30 @@ def getMetadata(self): # noqa
# }
axes = self._nd2.iter_axes[::-1]
result['frames'] = frames = []
maxref = {}
index = 0
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'])):
frame['Channel'] = self._metadata['channels'][ref['c']]
if 'z_coordinates' in self._metadata:
frame['PositionZ'] = self._metadata['z_coordinates'][ref.get('z', 0)]
cdidx = 0
basis = 1
for axis in baseaxes:
if axis not in {'x', 'y', 'c'}:
cdidx += ref.get(axis, 0) * basis
if axis in ref:
basis *= sizes[axis]
frame['Index'] = cdidx
if (idx and (
frame.get('IndexV') != result['frames'][idx - 1].get('IndexV') or
frame.get('IndexXY') != result['frames'][idx - 1].get('IndexXY') or
frame.get('IndexZ') != result['frames'][idx - 1].get('IndexZ'))):
index += 1
frame['Index'] = index
for mkey, fkey in [
('x_data', 'PositionX'),
('y_data', 'PositionY'),
Expand All @@ -288,12 +291,25 @@ def getMetadata(self): # noqa
('camera_exposure_time', 'ExposureTime'),
]:
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)
frame[fkey] = self._metadata[mkey][index % len(self._metadata[mkey])]
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()
Expand Down
44 changes: 44 additions & 0 deletions sources/ometiff/large_image_source_ometiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions test/test_source_nd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
10 changes: 10 additions & 0 deletions test/test_source_ometiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand All @@ -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)


Expand Down

0 comments on commit 4888393

Please sign in to comment.