Skip to content

Commit

Permalink
Merge pull request #1778 from girder/axes-to-frames
Browse files Browse the repository at this point in the history
Add utility functions for converting between frame and axes
  • Loading branch information
manthey authored Jan 17, 2025
2 parents dc1480a + 9cc4a14 commit 5e77521
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 1.30.7

### Features

- Add utility functions for converting between frame and axes ([#1777](../../pull/1777))

### Improvements

- Better report if rasterized vector files are geospatial ([#1769](../../pull/1769))
Expand Down
3 changes: 3 additions & 0 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,9 @@ Many image formats (such as TIFF) can contain multiple images within a single fi

By default, the ``getTile``, ``getRegion``, and ``tileIterator`` methods will return all of the bands of a single frame. The specific bands returned can be modified using the ``style`` parameter. The specific frame, including any channel or other axes, is specified with the ``frame`` parameter.

Since if can be useful to ask for a specific frame based on the axes values there are ``frameFromAxes`` and ``axesFromFrame`` utility functions.


Styles - Changing colors, scales, and other properties
------------------------------------------------------

Expand Down
6 changes: 5 additions & 1 deletion large_image/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ class TileSourceAssetstoreError(TileSourceError):
pass


class TileSourceXYZRangeError(TileSourceError):
class TileSourceRangeError(TileSourceError):
pass


class TileSourceXYZRangeError(TileSourceRangeError):
pass


Expand Down
46 changes: 46 additions & 0 deletions large_image/tilesource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2540,6 +2540,52 @@ def frames(self) -> int:
self._frameCount = len(self.getMetadata().get('frames', [])) or 1
return self._frameCount

def frameToAxes(self, frame: int) -> Dict[str, int]:
"""
Given a frame number, return a dictionary of axes values. If unknown,
this is just 'frame': frame.
:param frame: a frame number.
:returns: a dictionary of axes that specify the frame.
"""
if frame >= self.frames:
msg = 'frame is outside of range'
raise exceptions.TileSourceRangeError(msg)
meta = self.metadata
if self.frames == 1 or 'IndexStride' not in meta:
return {'frame': frame}
axes = {
key[5:].lower(): (frame // stride) % meta['IndexRange'][key]
for key, stride in meta['IndexStride'].items()}
return axes

def axesToFrame(self, **kwargs: int) -> int:
"""
Given values on some or all of the axes, return the corresponding frame
number. Any unspecified axis is 0. If one of the specified axes is
'frame', this is just returned and the other values are ignored.
:param kwargs: axes with position values.
:returns: a frame number.
"""
meta = self.metadata
frame = 0
for key, value in kwargs.items():
if key.lower() == 'frame':
if value < 0 or value >= self.frames:
msg = f'{value} is out of range for frame'
raise exceptions.TileSourceRangeError(msg)
return value
ikey = 'Index' + key.upper()
if ikey not in meta.get('IndexStride', {}):
msg = f'{key} is not a known axis'
raise exceptions.TileSourceRangeError(msg)
if value < 0 or value >= meta['IndexRange'][ikey]:
msg = f'{value} is out of range for axis {key}'
raise exceptions.TileSourceRangeError(msg)
frame += value * meta['IndexStride'][ikey]
return frame


class FileTileSource(TileSource):

Expand Down
26 changes: 26 additions & 0 deletions test/test_source_multi.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,3 +342,29 @@ def testTilesWithSampleScaling():
assert tileMetadata['sizeX'] == 2000
assert tileMetadata['sizeY'] == 1250
utilities.checkTilesZXY(source, tileMetadata)


def testAxesToFrameAndFrameToAxes():
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))
assert source.frameToAxes(7) == {'c': 2, 'z': 1}
with pytest.raises(large_image.exceptions.TileSourceRangeError):
source.frameToAxes(60)
assert source.axesToFrame(c=2, z=1) == 7
assert source.axesToFrame(C=2, Z=1) == 7
assert source.axesToFrame(frame=7) == 7
assert source.axesToFrame(z=1) == 5
with pytest.raises(large_image.exceptions.TileSourceRangeError):
source.axesToFrame(c=5, z=1)
with pytest.raises(large_image.exceptions.TileSourceRangeError):
source.axesToFrame(frame=-1)
with pytest.raises(large_image.exceptions.TileSourceRangeError):
source.axesToFrame(x=5)
asAxesSource2 = {'sources': [{
'sourceName': 'test', 'path': '__none__', 'params': {
'sizeX': 1000, 'sizeY': 1000, 'frames': 60}}]}
source = large_image_source_multi.open(json.dumps(asAxesSource2))
assert source.frameToAxes(0) == {'frame': 0}

0 comments on commit 5e77521

Please sign in to comment.