diff --git a/CHANGELOG.md b/CHANGELOG.md index 04a0f7b8f..6a81ddee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index f83475db3..cf9ce030c 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -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()): diff --git a/sources/multi/large_image_source_multi/__init__.py b/sources/multi/large_image_source_multi/__init__.py index e924a3bc9..cfb6b939b 100644 --- a/sources/multi/large_image_source_multi/__init__.py +++ b/sources/multi/large_image_source_multi/__init__.py @@ -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, @@ -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. @@ -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) @@ -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, diff --git a/sources/tifffile/large_image_source_tifffile/__init__.py b/sources/tifffile/large_image_source_tifffile/__init__.py index a56b6f732..7afb1db97 100644 --- a/sources/tifffile/large_image_source_tifffile/__init__.py +++ b/sources/tifffile/large_image_source_tifffile/__init__.py @@ -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): diff --git a/test/test_source_multi.py b/test/test_source_multi.py index a71802047..1e1db475d 100644 --- a/test/test_source_multi.py +++ b/test/test_source_multi.py @@ -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