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

Standardize frame metadata. #433

Merged
merged 1 commit into from
Apr 1, 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
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