Skip to content

Commit

Permalink
Merge pull request #957 from girder/frames-as-axes
Browse files Browse the repository at this point in the history
Add a framesAsAxes parameter to the multisource.
  • Loading branch information
manthey authored Sep 6, 2022
2 parents 830c94a + 0abaa0c commit 2f4ed61
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Improvements
- Reduce rest calls to get settings ([953](../../pull/953))
- Add an endpoint to delete all annotations in a folder ([954](../../pull/954))
- Support relabeling axes in the multi source ([957](../../pull/957))

### Bug Fixes
- Harden adding images to the item list ([955](../../pull/955))
Expand Down
5 changes: 3 additions & 2 deletions large_image/tilesource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1424,9 +1424,10 @@ def _addMetadataFrameInformation(self, metadata, channels=None):
if key in frame and frame[key] + 1 > maxref.get(key, 0):
maxref[key] = frame[key] + 1
frame['Frame'] = idx
if idx and any(
if idx and (any(
frame.get(key) != metadata['frames'][idx - 1].get(key)
for key in refkeys if key != 'IndexC'):
for key in refkeys if key != 'IndexC') or not any(
metadata['frames'][idx].get(key) for key in refkeys)):
index += 1
frame['Index'] = index
if any(val > 1 for val in maxref.values()):
Expand Down
47 changes: 45 additions & 2 deletions sources/multi/large_image_source_multi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,20 @@
'type': 'integer',
'exclusiveMinimum': 0,
},
'framesAsAxes': {
'description':
'An object with keys as axes and values as strides to '
'interpret the source frames. This overrides the internal '
'metadata for frames.',
'type': 'object',
'patternProperties': {
'^(c|t|z|xy)$': {
'type': 'integer',
'exclusiveMinimum': 0,
}
},
'additionalProperties': False,
},
'position': {
'type': 'object',
'additionalProperties': False,
Expand Down Expand Up @@ -531,6 +545,33 @@ def _axisKey(self, source, value, key):
(value - len(vals) + source.get(key, 0)))
return axisKey

def _adjustFramesAsAxes(self, frames, idx, framesAsAxes):
"""
Given a dictionary of axes and strides, relabel the indices in a frame
as if it was based on those strides.
:param frames: a list of frames from the tile source.
:param idx: 0-based index of the frame to adjust.
:param framesAsAxes: dictionary of axes and strides to apply.
:returns: the adjusted frame record.
"""
axisRange = {}
slen = len(frames)
check = 1
for stride, axis in sorted([[v, k] for k, v in framesAsAxes.items()], reverse=True):
axisRange[axis] = slen // stride
slen = stride
check *= axisRange[axis]
if check != len(frames) and not hasattr(self, '_warnedAdjustFramesAsAxes'):
self.logger.warning('framesAsAxes strides do not use all frames.')
self._warnedAdjustFramesAsAxes = True
frame = frames[idx].copy()
for axis in ['c', 'z', 't', 'xy']:
frame.pop('Index' + axis.upper(), None)
for axis, stride in framesAsAxes.items():
frame['Index' + axis.upper()] = (idx // stride) % axisRange[axis]
return frame

def _addSourceToFrames(self, tsMeta, source, sourceIdx, frameDict):
"""
Add a source to the all appropriate frames.
Expand Down Expand Up @@ -561,6 +602,8 @@ def _addSourceToFrames(self, tsMeta, source, sourceIdx, frameDict):
for frameIdx, frame in enumerate(frames):
if 'frames' in source and frameIdx not in source['frames']:
continue
if source.get('framesAsAxes'):
frame = self._adjustFramesAsAxes(frames, frameIdx, source.get('framesAsAxes'))
fKey = self._axisKey(source, frameIdx, 'frame')
cIdx = frame.get('IndexC', 0)
zIdx = frame.get('IndexZ', 0)
Expand Down Expand Up @@ -588,8 +631,8 @@ def _addSourceToFrames(self, tsMeta, source, sourceIdx, frameDict):
'frame': frameIdx,
'kwargs': kwargs,
})
frameDict['axesAllowed'] = frameDict['axesAllowed'] and (
len(frames) <= 1 or 'IndexRange' in tsMeta)
frameDict['axesAllowed'] = (frameDict['axesAllowed'] and (
len(frames) <= 1 or 'IndexRange' in tsMeta)) or aKey != (0, 0, 0, 0)
frameDict['byAxes'].setdefault(aKey, [])
frameDict['byAxes'][aKey].append({
'sourcenum': sourceIdx,
Expand Down
5 changes: 3 additions & 2 deletions sources/tifffile/large_image_source_tifffile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,9 @@ def getMetadata(self):
for idx in range(self._framecount):
frame = {'Frame': idx}
for axis, (basis, _pos, count) in self._basis.items():
frame['Index' + (axis.upper() if axis.upper() != 'P' else 'XY')] = (
idx // basis) % count
if axis != 'I':
frame['Index' + (axis.upper() if axis.upper() != 'P' else 'XY')] = (
idx // basis) % count
frames.append(frame)
self._addMetadataFrameInformation(result, getattr(self, '_channels', None))
if any(v != self._seriesShape[0] for v in self._seriesShape):
Expand Down
32 changes: 32 additions & 0 deletions test/test_source_multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,35 @@ def testMultiBand():
assert len(metadata['bands']) == 6
image, mimeType = source.getThumbnail(encoding='PNG')
assert image[:len(utilities.PNGHeader)] == utilities.PNGHeader


def testFramesAsAxes():
baseSource = {'sources': [{
'sourceName': 'test', 'path': '__none__', 'params': {
'sizeX': 1000, 'sizeY': 1000, 'frames': 60}}]}
source = large_image_source_multi.open(json.dumps(baseSource))
tileMetadata = source.getMetadata()
assert len(tileMetadata['frames']) == 60
assert 'IndexZ' not in tileMetadata['frames'][0]

asAxesSource1 = {'sources': [{
'sourceName': 'test', 'path': '__none__', 'params': {
'sizeX': 1000, 'sizeY': 1000, 'frames': 60},
'framesAsAxes': {'c': 1, 'z': 5}}]}
source = large_image_source_multi.open(json.dumps(asAxesSource1))
tileMetadata = source.getMetadata()
assert len(tileMetadata['frames']) == 60
assert 'IndexZ' in tileMetadata['frames'][0]
assert tileMetadata['IndexRange']['IndexC'] == 5
assert tileMetadata['IndexRange']['IndexZ'] == 12

asAxesSource1 = {'sources': [{
'sourceName': 'test', 'path': '__none__', 'params': {
'sizeX': 1000, 'sizeY': 1000, 'frames': 60},
'framesAsAxes': {'c': 1, 'z': 7}}]}
source = large_image_source_multi.open(json.dumps(asAxesSource1))
tileMetadata = source.getMetadata()
assert len(tileMetadata['frames']) == 56
assert 'IndexZ' in tileMetadata['frames'][0]
assert tileMetadata['IndexRange']['IndexC'] == 7
assert tileMetadata['IndexRange']['IndexZ'] == 8

0 comments on commit 2f4ed61

Please sign in to comment.