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

Support frame options for the test tile source. #632

Merged
merged 1 commit into from
Aug 11, 2021
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

### Features
- Provide band information on all tile sources (#622, #623)
- Add a tileFrames method to tile sources and appropriate endpoints to composite regions from multiple frames to a single output image (#629)
- The test tile source now support frames (#631)

### Improvements
- Better handle TIFFs with missing levels and tiles (#624, #627)
- Better report inefficient TIFFs (#626)

## Version 1.6.2

Expand Down
3 changes: 3 additions & 0 deletions girder/girder_large_image/rest/tiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ def _parseTestParams(cls, params):
('sizeX', int),
('sizeY', int),
('fractal', lambda val: val == 'true'),
('frame', int),
('frames', str),
('monochrome', lambda val: val == 'true'),
('encoding', str),
])

Expand Down
97 changes: 79 additions & 18 deletions sources/test/large_image_source_test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
##############################################################################

import colorsys
import itertools

from PIL import Image, ImageDraw, ImageFont
from pkg_resources import DistributionNotFound, get_distribution
Expand Down Expand Up @@ -45,7 +46,7 @@ class TestTileSource(TileSource, metaclass=LruCacheMetaclass):

def __init__(self, ignored_path=None, minLevel=0, maxLevel=9,
tileWidth=256, tileHeight=256, sizeX=None, sizeY=None,
fractal=False, **kwargs):
fractal=False, frames=None, monochrome=False, **kwargs):
"""
Initialize the tile class. See the base class for other available
parameters.
Expand All @@ -57,10 +58,13 @@ def __init__(self, ignored_path=None, minLevel=0, maxLevel=9,
:param tileHeight: tile height in pixels
:param sizeX: image width in pixels at maximum level. Computed from
maxLevel and tileWidth if None.
:param sizeY: image height in pixels at maximum level. Computer from
:param sizeY: image height in pixels at maximum level. Computed from
maxLevel and tileHeight if None.
:param fractal: if True, and the tile size is square and a power of
two, draw a simple fractal on the tiles.
:param frames: if present, this is either a single number for generic
frames, or comma-separated list of c,z,t,xy.
:param monochrome: if True, return single channel tiles.
"""
if not kwargs.get('encoding'):
kwargs = kwargs.copy()
Expand All @@ -76,17 +80,46 @@ def __init__(self, ignored_path=None, minLevel=0, maxLevel=9,
self.fractal = (fractal and self.tileWidth == self.tileHeight and
not (self.tileWidth & (self.tileWidth - 1)))
self.sizeX = (((2 ** self.maxLevel) * self.tileWidth)
if sizeX is None else sizeX)
if not sizeX else sizeX)
self.sizeY = (((2 ** self.maxLevel) * self.tileHeight)
if sizeY is None else sizeY)
if not sizeY else sizeY)
self.frameSpec = frames or None
self.monochrome = bool(monochrome)
# Used for reporting tile information
self.levels = self.maxLevel + 1
print(frames)
if frames:
frameList = []
counts = [int(part) for part in str(frames).split(',')]
self._framesParts = len(counts)
for fidx in itertools.product(*(range(part) for part in counts[::-1])):
curframe = {}
if len(fidx) > 1:
for idx, (k, v) in enumerate(zip([
'IndexC', 'IndexZ', 'IndexT', 'IndexXY'], list(fidx)[::-1])):
if counts[idx] > 1:
curframe[k] = v
else:
curframe['Index'] = fidx[0]
frameList.append(curframe)
if len(frameList) > 1:
self._frames = frameList

@classmethod
def canRead(cls, *args, **kwargs):
return True

def fractalTile(self, image, x, y, widthCount, color=(0, 0, 0)):
"""
Draw a simple fractal in a tile image.

:param image: a Pil image to draw on. Modified.
:param x: the tile x position
:param y: the tile y position
:param widthCount: 2 ** z; the number of tiles across for a "full size"
image at this z level.
:param color: an rgb tuple on a scale of [0-255].
"""
imageDraw = ImageDraw.Draw(image)
x *= self.tileWidth
y *= self.tileHeight
Expand All @@ -105,6 +138,19 @@ def fractalTile(self, image, x, y, widthCount, color=(0, 0, 0)):
], color, None)
sq //= 2

def getMetadata(self):
"""
Return a dictionary of metadata containing levels, sizeX, sizeY,
tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

:returns: metadata dictionary.
"""
result = super().getMetadata()
if hasattr(self, '_frames') and len(self._frames) > 1:
result['frames'] = self._frames
self._addMetadataFrameInformation(result)
return result

def getInternalMetadata(self, **kwargs):
"""
Return additional known metadata about the tile source. Data returned
Expand All @@ -113,27 +159,26 @@ def getInternalMetadata(self, **kwargs):

:returns: a dictionary of data or None.
"""
return {'fractal': self.fractal}
return {'fractal': self.fractal, 'monochrome': self.monochrome}

@methodcache()
def getTile(self, x, y, z, *args, **kwargs):
widthCount = 2 ** z
frame = int(kwargs.get('frame') or 0)
self._xyzInRange(x, y, z, frame, len(self._frames) if hasattr(self, '_frames') else None)

if not (0 <= x < float(self.sizeX) / self.tileWidth * 2 ** (
z - self.maxLevel)):
raise TileSourceException('x is outside layer')
if not (0 <= y < float(self.sizeY) / self.tileHeight * 2 ** (
z - self.maxLevel)):
raise TileSourceException('y is outside layer')
if not (self.minLevel <= z <= self.maxLevel):
raise TileSourceException('z layer does not exist')

xFraction = float(x) / (widthCount - 1) if z != 0 else 0
yFraction = float(y) / (widthCount - 1) if z != 0 else 0
fFraction = yFraction
if hasattr(self, '_frames'):
fFraction = float(frame) / (len(self._frames) - 1)

backgroundColor = colorsys.hsv_to_rgb(
h=(0.9 * xFraction),
s=(0.3 + (0.7 * yFraction)),
s=(0.3 + (0.7 * fFraction)),
v=(0.3 + (0.7 * yFraction)),
)
rgbColor = tuple(int(val * 255) for val in backgroundColor)
Expand All @@ -148,21 +193,34 @@ def getTile(self, x, y, z, *args, **kwargs):
if self.fractal:
self.fractalTile(image, x, y, widthCount, rgbColor)

fontsize = 0.15
text = 'x=%d\ny=%d\nz=%d' % (x, y, z)
if hasattr(self, '_frames'):
if self._framesParts == 1:
text += '\nf=%d' % frame
else:
for k1, k2 in [('C', 'IndexC'), ('Z', 'IndexZ'),
('T', 'IndexT'), ('XY', 'IndexXY')]:
if k2 in self._frames[frame]:
text += '\n%s=%d' % (k1, self._frames[frame][k2])
fontsize = min(fontsize, 0.8 / len(text.split('\n')))
try:
# the font size should fill the whole tile
imageDrawFont = ImageFont.truetype(
font='/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf',
size=int(0.15 * min(self.tileWidth, self.tileHeight))
size=int(fontsize * min(self.tileWidth, self.tileHeight))
)
except OSError:
imageDrawFont = ImageFont.load_default()
imageDraw.multiline_text(
xy=(10, 10),
text='x=%d\ny=%d\nz=%d' % (x, y, z),
text=text,
fill=(0, 0, 0),
font=imageDrawFont
)
_counters['tiles'] += 1
if self.monochrome:
image = image.convert('L')
return self._outputTile(image, TILE_FORMAT_PIL, x, y, z, **kwargs)

@staticmethod
Expand All @@ -172,12 +230,15 @@ def getLRUHash(*args, **kwargs):
*args, **kwargs),
kwargs.get('minLevel'), kwargs.get('maxLevel'),
kwargs.get('tileWidth'), kwargs.get('tileHeight'),
kwargs.get('fractal'))
kwargs.get('fractal'), kwargs.get('sizeX'), kwargs.get('sizeY'),
kwargs.get('frames'), kwargs.get('monochrome'),
)

def getState(self):
return 'test %r %r %r %r %r %r' % (
super().getState(), self.minLevel,
self.maxLevel, self.tileWidth, self.tileHeight, self.fractal)
return 'test %r %r %r %r %r %r %r %r %r %r' % (
super().getState(), self.minLevel, self.maxLevel, self.tileWidth,
self.tileHeight, self.fractal, self.sizeX, self.sizeY,
self.frameSpec, self.monochrome)


def open(*args, **kwargs):
Expand Down
23 changes: 23 additions & 0 deletions test/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,26 @@ def testConverterMissingTiles(tmpdir):
large_image_converter.convert(imagePath, outputPath)
info = tifftools.read_tiff(outputPath)
assert len(info['ifds']) == 6


def testConvertFromTestSourceFrames(tmpdir):
outputPath = os.path.join(tmpdir, 'out.tiff')
large_image_converter.convert('large_image://test?maxLevel=3&frames=4', outputPath)
source = large_image_source_tiff.open(outputPath)
metadata = source.getMetadata()
assert metadata['levels'] == 4
assert len(metadata['frames']) == 4
info = tifftools.read_tiff(outputPath)
assert len(info['ifds']) == 4


def testConvertFromTestSourceFrameArray(tmpdir):
outputPath = os.path.join(tmpdir, 'out.tiff')
large_image_converter.convert(
'large_image://test?maxLevel=3&frames=2,3&monochrome=true', outputPath)
source = large_image_source_tiff.open(outputPath)
metadata = source.getMetadata()
assert metadata['levels'] == 4
assert len(metadata['frames']) == 6
info = tifftools.read_tiff(outputPath)
assert len(info['ifds']) == 6
17 changes: 13 additions & 4 deletions utilities/converter/large_image_converter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,19 @@ def _data_from_large_image(path, outputPath, **kwargs):
readable by large_image.
"""
_import_pyvips()
try:
ts = large_image.getTileSource(path)
except Exception:
return
if not path.startswith('large_image://test'):
try:
ts = large_image.getTileSource(path)
except Exception:
return
else:
import urllib.parse

tsparams = {
k: int(v[0]) if v[0].isdigit() else v[0]
for k, v in urllib.parse.parse_qs(
path.split('?', 1)[1] if '?' in path else '').items()}
ts = large_image.getTileSource('large_image://test', **tsparams)
results = {
'metadata': ts.getMetadata(),
'internal_metadata': ts.getInternalMetadata(),
Expand Down
2 changes: 1 addition & 1 deletion utilities/converter/large_image_converter/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def main(args=sys.argv[1:]):
except ImportError:
pass
logger.debug('Command line options: %r' % opts)
if not os.path.isfile(opts.source):
if not os.path.isfile(opts.source) and not opts.source.startswith('large_image://test'):
logger.error('Source is not a file (%s)', opts.source)
return 1
if opts.compression == 'zip':
Expand Down