Skip to content

Commit

Permalink
Merge pull request #622 from girder/get-band-information
Browse files Browse the repository at this point in the history
Add getBandInformation for all tile sources
  • Loading branch information
manthey authored Jul 7, 2021
2 parents 2b99f53 + 1e245ed commit 73ce429
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 24 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Change Log

## Unreleased

### Features
- Provide band information on all tile sources (#622)

## Version 1.6.2

### Improvements
Expand Down
13 changes: 13 additions & 0 deletions girder/girder_large_image/models/image_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,19 @@ def histogram(self, item, **kwargs):
imageKey=imageKey, pickleCache=True, **kwargs)[0]
return result

def getBandInformation(self, item, statistics=True, **kwargs):
"""
Using a tile source, get band information of the image.
:param item: the item with the tile source.
:param kwargs: optional arguments. See the tilesource
getBandInformation method.
:returns: band information.
"""
tileSource = self._loadTileSource(item, **kwargs)
result = tileSource.getBandInformation(statistics=statistics)
return result

def tileSource(self, item, **kwargs):
"""
Get a tile source for an item.
Expand Down
45 changes: 29 additions & 16 deletions girder/girder_large_image/rest/tiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,29 +103,22 @@ def __init__(self, apiRoot):
apiRoot.item.route('POST', (':itemId', 'tiles', 'convert'), self.convertImage)
apiRoot.item.route('GET', (':itemId', 'tiles'), self.getTilesInfo)
apiRoot.item.route('DELETE', (':itemId', 'tiles'), self.deleteTiles)
apiRoot.item.route('GET', (':itemId', 'tiles', 'thumbnail'),
self.getTilesThumbnail)
apiRoot.item.route('GET', (':itemId', 'tiles', 'region'),
self.getTilesRegion)
apiRoot.item.route('GET', (':itemId', 'tiles', 'pixel'),
self.getTilesPixel)
apiRoot.item.route('GET', (':itemId', 'tiles', 'histogram'),
self.getHistogram)
apiRoot.item.route('GET', (':itemId', 'tiles', 'zxy', ':z', ':x', ':y'),
self.getTile)
apiRoot.item.route('GET', (':itemId', 'tiles', 'thumbnail'), self.getTilesThumbnail)
apiRoot.item.route('GET', (':itemId', 'tiles', 'region'), self.getTilesRegion)
apiRoot.item.route('GET', (':itemId', 'tiles', 'pixel'), self.getTilesPixel)
apiRoot.item.route('GET', (':itemId', 'tiles', 'histogram'), self.getHistogram)
apiRoot.item.route('GET', (':itemId', 'tiles', 'bands'), self.getBandInformation)
apiRoot.item.route('GET', (':itemId', 'tiles', 'zxy', ':z', ':x', ':y'), self.getTile)
apiRoot.item.route('GET', (':itemId', 'tiles', 'fzxy', ':frame', ':z', ':x', ':y'),
self.getTileWithFrame)
apiRoot.item.route('GET', (':itemId', 'tiles', 'images'),
self.getAssociatedImagesList)
apiRoot.item.route('GET', (':itemId', 'tiles', 'images'), self.getAssociatedImagesList)
apiRoot.item.route('GET', (':itemId', 'tiles', 'images', ':image'),
self.getAssociatedImage)
apiRoot.item.route('GET', (':itemId', 'tiles', 'images', ':image', 'metadata'),
self.getAssociatedImageMetadata)
apiRoot.item.route('GET', ('test', 'tiles'), self.getTestTilesInfo)
apiRoot.item.route('GET', ('test', 'tiles', 'zxy', ':z', ':x', ':y'),
self.getTestTile)
apiRoot.item.route('GET', (':itemId', 'tiles', 'dzi.dzi'),
self.getDZIInfo)
apiRoot.item.route('GET', ('test', 'tiles', 'zxy', ':z', ':x', ':y'), self.getTestTile)
apiRoot.item.route('GET', (':itemId', 'tiles', 'dzi.dzi'), self.getDZIInfo)
apiRoot.item.route('GET', (':itemId', 'tiles', 'dzi_files', ':level', ':xandy'),
self.getDZITile)
apiRoot.item.route('GET', (':itemId', 'tiles', 'internal_metadata'),
Expand Down Expand Up @@ -977,6 +970,26 @@ def getHistogram(self, item, params):
entry[key] = float(entry[key])
return result

@describeRoute(
Description('Get band information for a large image item.')
.param('itemId', 'The ID of the item.', paramType='path')
.param('frame', 'For multiframe images, the 0-based frame number. '
'This is ignored on non-multiframe images.', required=False,
dataType='int')
.errorResponse('ID was invalid.')
.errorResponse('Read access was denied for the item.', 403)
)
@access.public
@loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ)
def getBandInformation(self, item, params):
_adjustParams(params)
params = self._parseParams(params, True, [
('frame', int),
])
_handleETag('getBandInformation', item, params)
result = self.imageItemModel.getBandInformation(item)
return result

@describeRoute(
Description('Get a list of additional images associated with a large image.')
.param('itemId', 'The ID of the item.', paramType='path')
Expand Down
13 changes: 13 additions & 0 deletions girder/test_girder/test_tiles_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,19 @@ def testTilesInternalMetadata(server, admin, fsAssetstore):
assert resp.json['tilesource'] == 'tiff'


@pytest.mark.usefixtures('unbindLargeImage')
@pytest.mark.plugin('large_image')
def testTilesBandInformation(server, admin, fsAssetstore):
file = utilities.uploadExternalFile(
'sample_Easy1.png', admin, fsAssetstore)
itemId = str(file['itemId'])
server.request(path='/item/%s/tiles' % itemId, method='POST', user=admin)
resp = server.request(path='/item/%s/tiles/bands' % itemId)
assert len(resp.json) == 4
assert resp.json[0]['interpretation'] == 'red'
assert 'mean' in resp.json[0]


@pytest.mark.usefixtures('unbindLargeImage')
@pytest.mark.plugin('large_image')
def testTilesFromMultipleDotName(boundServer, admin, fsAssetstore, girderWorker):
Expand Down
85 changes: 82 additions & 3 deletions large_image/tilesource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1417,15 +1417,40 @@ def histogram(self, dtype=None, onlyMinMax=False, bins=256,
numpy.amin(tile[:, :, idx]) for idx in range(tile.shape[2])], tile.dtype)
tilemax = numpy.array([
numpy.amax(tile[:, :, idx]) for idx in range(tile.shape[2])], tile.dtype)
tilesum = numpy.array([
numpy.sum(tile[:, :, idx]) for idx in range(tile.shape[2])], float)
tilesum2 = numpy.array([
numpy.sum(numpy.array(tile[:, :, idx], float) ** 2)
for idx in range(tile.shape[2])], float)
tilecount = tile.shape[0] * tile.shape[1]
if results is None:
results = {'min': tilemin, 'max': tilemax}
results['min'] = numpy.minimum(results['min'], tilemin)
results['max'] = numpy.maximum(results['max'], tilemax)
results = {
'min': tilemin,
'max': tilemax,
'sum': tilesum,
'sum2': tilesum2,
'count': tilecount
}
else:
results['min'] = numpy.minimum(results['min'], tilemin)
results['max'] = numpy.maximum(results['max'], tilemax)
results['sum'] += tilesum
results['sum2'] += tilesum2
results['count'] += tilecount
results['mean'] = results['sum'] / results['count']
results['stdev'] = numpy.maximum(
results['sum2'] / results['count'] - results['mean'] ** 2,
[0] * results['sum2'].shape[0]) ** 0.5
results.pop('sum', None)
results.pop('sum2', None)
results.pop('count', None)
if results is None or onlyMinMax:
return results
results['histogram'] = [{
'min': results['min'][idx],
'max': results['max'][idx],
'mean': results['mean'][idx],
'stdev': results['stdev'][idx],
'range': ((results['min'][idx], results['max'][idx] + 1)
if histRange is None else histRange),
'hist': None,
Expand Down Expand Up @@ -1788,6 +1813,9 @@ def getMetadata(self):
:channels: optional. If known, a list of channel names
:channelmap: optional. If known, a dictionary of channel names
with their offset into the channel list.
Note that this does nto include band information, though some tile
sources may do so.
"""
mag = self.getNativeMagnification()
return {
Expand Down Expand Up @@ -1851,6 +1879,57 @@ def getInternalMetadata(self, **kwargs):
"""
return None

def getOneBandInformation(self, band):
"""
Get band information for a single band.
:param band: a 0-based band.
:returns: a dictionary of band information. See getBandInformation.
"""
return self.getBandInformation()[band]

def getBandInformation(self, statistics=False, **kwargs):
"""
Get information about each band in the image.
:param statistics: if True, compute statistics if they don't already
exist.
:returns: a list of one dictionary per band. Each dictionary contains
known values such as interpretation, min, max, mean, stdev.
"""
if not getattr(self, '_bandInfo', None):
bandInterp = {
1: ['gray'],
2: ['gray', 'alpha'],
3: ['red', 'green', 'blue'],
4: ['red', 'green', 'blue', 'alpha']}
if not statistics:
if not getattr(self, '_bandInfoNoStats', None):
tile = self.getSingleTile()['tile']
bands = tile.shape[2] if len(tile.shape) > 2 else 1
interp = bandInterp.get(bands, 3)
bandInfo = [{'interpretation': interp[idx] if idx < len(interp) else 'unknown'}
for idx in range(bands)]
self._bandInfoNoStats = bandInfo
return self._bandInfoNoStats
analysisSize = 2048
histogram = self.histogram(
onlyMinMax=True,
output={'maxWidth': min(self.sizeX, analysisSize),
'maxHeight': min(self.sizeY, analysisSize)},
resample=False,
**kwargs)
bands = histogram['min'].shape[0]
interp = bandInterp.get(bands, 3)
bandInfo = [{'interpretation': interp[idx] if idx < len(interp) else 'unknown'}
for idx in range(bands)]
for key in {'min', 'max', 'mean', 'stdev'}:
if key in histogram:
for idx in range(bands):
bandInfo[idx][key] = histogram[key][idx]
self._bandInfo = bandInfo
return self._bandInfo

def _xyzInRange(self, x, y, z, frame=None, numFrames=None):
"""
Check if a tile at x, y, z is in range based on self.levels,
Expand Down
15 changes: 11 additions & 4 deletions sources/gdal/large_image_source_gdal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,17 @@ def getBounds(self, srs=None):
self._bounds[srs] = bounds
return self._bounds[srs]

def getBandInformation(self, dataset=None):
def getBandInformation(self, statistics=True, dataset=None, **kwargs):
"""
Get information about each band in the image.
:param statistics: if True, compute statistics if they don't already
exist. Ignored: always treated as True.
:param dataset: the dataset. If None, use the main dataset.
:returns: a list of one dictionary per band. Each dictionary contains
known values such as interpretation, min, max, mean, stdev, nodata,
scale, offset, units, categories, colortable, maskband.
"""
if not getattr(self, '_bandInfo', None) or dataset:
with self._getDatasetLock:
cache = not dataset
Expand Down Expand Up @@ -533,9 +543,6 @@ def getBandInformation(self, dataset=None):
self._bandInfo = infoSet
return self._bandInfo

def getOneBandInformation(self, band):
return self.getBandInformation()[band]

def getMetadata(self):
with self._getDatasetLock:
metadata = {
Expand Down
2 changes: 1 addition & 1 deletion sources/mapnik/large_image_source_mapnik/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ def getOneBandInformation(self, band):
if not dataset.get('bands'):
if not dataset.get('dataset'):
dataset['dataset'] = gdal.Open(dataset['name'], gdalconst.GA_ReadOnly)
dataset['bands'] = self.getBandInformation(dataset['dataset'])
dataset['bands'] = self.getBandInformation(dataset=dataset['dataset'])
bandInfo = dataset['bands'][band[1]]
return bandInfo

Expand Down
12 changes: 12 additions & 0 deletions test/test_source_pil.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,15 @@ def testInternalMetadata():
source = large_image_source_pil.open(imagePath)
metadata = source.getInternalMetadata()
assert 'pil' in metadata


def testGetBandInformation():
imagePath = datastore.fetch('sample_Easy1.png')
source = large_image_source_pil.open(imagePath)
bandInfo = source.getBandInformation(False)
assert len(bandInfo) == 4
assert bandInfo[0] == {'interpretation': 'red'}

bandInfo = source.getBandInformation(True)
assert len(bandInfo) == 4
assert 'mean' in bandInfo[0]

0 comments on commit 73ce429

Please sign in to comment.