Skip to content

Commit

Permalink
Provide an option to output large regions as tiled tiffs.
Browse files Browse the repository at this point in the history
This also will geotag geospatial outputs.

Specifically, when calling the getRegion function or endpoint, if an
encoding of `TILED` is used (previously this could have been one of
`JPEG`, `PNG`, or `TIFF`), a tiled tiff is output.  For non-geospatial
sources, this is tiled using vips.  For geospatial sources, this is
tiled using gdal.

When using the `TILED` option, a variety of parameters that can be used
in conversions can be used: `compression`, `quality`.  For
compatibility, these can also be `tiffCompression` and `jpegQuality`.
  • Loading branch information
manthey committed Apr 20, 2021
1 parent cbe3081 commit 32b2299
Show file tree
Hide file tree
Showing 12 changed files with 651 additions and 184 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Add a convert endpoint to the Girder plugin (#578)
- Added support for creating Aperio svs files (#580)
- Added support for geospatial files with GCP (#588)
- Regions can be output as tiled tiffs with scale or geospatial metadata (#594)

### Improvements
- More untiled tiff files are handles by the bioformats reader (#569)
Expand Down
32 changes: 27 additions & 5 deletions girder/girder_large_image/rest/tiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import hashlib
import math
import os
import pathlib
import re
import urllib

Expand All @@ -29,6 +30,7 @@
from girder.exceptions import RestException
from girder.models.file import File
from girder.models.item import Item
from girder.utility.progress import setResponseTimeLimit

from large_image.constants import TileInputUnits
from large_image.exceptions import TileGeneralException
Expand Down Expand Up @@ -627,7 +629,6 @@ def getDZITile(self, item, level, xandy, params):
@loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.WRITE)
def deleteTiles(self, item, params):
deleted = self.imageItemModel.delete(item)
# TODO: a better response
return {
'deleted': deleted
}
Expand All @@ -654,8 +655,11 @@ def deleteTiles(self, item, params):
.param('frame', 'For multiframe images, the 0-based frame number. '
'This is ignored on non-multiframe images.', required=False,
dataType='int')
.param('encoding', 'Thumbnail output encoding', required=False,
enum=['JPEG', 'PNG', 'TIFF'], default='JPEG')
.param('encoding', 'Output image encoding. TILED generates a tiled '
'tiff without the upper limit on image size the other options '
'have. For geospatial sources, TILED will also have '
'appropriate tagging.', required=False,
enum=['JPEG', 'PNG', 'TIFF', 'TILED'], default='JPEG')
.param('contentDisposition', 'Specify the Content-Disposition response '
'header disposition-type value.', required=False,
enum=['inline', 'attachment'])
Expand Down Expand Up @@ -752,8 +756,11 @@ def getTilesThumbnail(self, item, params):
.param('frame', 'For multiframe images, the 0-based frame number. '
'This is ignored on non-multiframe images.', required=False,
dataType='int')
.param('encoding', 'Output image encoding', required=False,
enum=['JPEG', 'PNG', 'TIFF'], default='JPEG')
.param('encoding', 'Output image encoding. TILED generates a tiled '
'tiff without the upper limit on image size the other options '
'have. For geospatial sources, TILED will also have '
'appropriate tagging.', required=False,
enum=['JPEG', 'PNG', 'TIFF', 'TILED'], default='JPEG')
.param('jpegQuality', 'Quality used for generating JPEG images',
required=False, dataType='int', default=95)
.param('jpegSubsampling', 'Chroma subsampling used for generating '
Expand Down Expand Up @@ -809,6 +816,7 @@ def getTilesRegion(self, item, params):
('contentDisposition', str),
])
_handleETag('getTilesRegion', item, params)
setResponseTimeLimit(86400)
try:
regionData, regionMime = self.imageItemModel.getRegion(
item, **params)
Expand All @@ -819,6 +827,20 @@ def getTilesRegion(self, item, params):
self._setContentDisposition(
item, params.get('contentDisposition'), regionMime, 'region')
setResponseHeader('Content-Type', regionMime)
if isinstance(regionData, pathlib.Path):
BUF_SIZE = 65536

def stream():
try:
with regionData.open('rb') as f:
while True:
data = f.read(BUF_SIZE)
if not data:
break
yield data
finally:
regionData.unlink()
return stream
setRawResponse()
return regionData

Expand Down
2 changes: 1 addition & 1 deletion girder/test_girder/girder_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from girder.models.upload import Upload

from test.datastore import datastore
from test.utilities import JFIFHeader, JPEGHeader, PNGHeader # noqa
from test.utilities import JFIFHeader, JPEGHeader, PNGHeader, TIFFHeader, BigTIFFHeader # noqa


def namedFolder(user, folderName='Public'):
Expand Down
9 changes: 9 additions & 0 deletions girder/test_girder/test_tiles_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,15 @@ def testRegions(server, admin, fsAssetstore):
assert width == 500
assert height == 375

# Get a tiled image
params = {'regionWidth': 1000, 'regionHeight': 1000,
'left': 48000, 'top': 3000, 'encoding': 'TILED'}
resp = server.request(path='/item/%s/tiles/region' % itemId,
user=admin, isJson=False, params=params)
assert utilities.respStatus(resp) == 200
image = origImage = utilities.getBody(resp, text=False)
assert image[:len(utilities.BigTIFFHeader)] == utilities.BigTIFFHeader


@pytest.mark.usefixtures('unbindLargeImage')
@pytest.mark.plugin('large_image')
Expand Down
17 changes: 16 additions & 1 deletion large_image/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ class SourcePriority:
'JPEG': 'image/jpeg',
'PNG': 'image/png',
'TIFF': 'image/tiff',
# TILED indicates the region output should be generated as a tiled TIFF
'TILED': 'image/tiff',
}
TileOutputPILFormat = {
'JFIF': 'JPEG'
}


TileInputUnits = {
None: 'base_pixels',
'base': 'base_pixels',
Expand All @@ -64,3 +65,17 @@ class SourcePriority:
'millimeters': 'mm',
'fraction': 'fraction',
}

# numpy dtype to pyvips GValue
dtypeToGValue = {
'b': 'char',
'B': 'uchar',
'd': 'double',
'D': 'dpcomplex',
'f': 'float',
'F': 'complex',
'h': 'short',
'H': 'ushort',
'i': 'int',
'I': 'uint',
}
Loading

0 comments on commit 32b2299

Please sign in to comment.