Skip to content

Commit

Permalink
Merge pull request #554 from girder/style-with-frames
Browse files Browse the repository at this point in the history
Support compositing multiple frames together.
  • Loading branch information
manthey authored Feb 24, 2021
2 parents 3846e21 + b9258dc commit 155e53e
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 10 deletions.
13 changes: 9 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# Change Log

## Version 1.4.0
## Unreleased

### Changes
- The image conversion task has been split into two packages, large_image_converter and large_image_tasks. The tasks module is used with Girder and Girder Worker for converting images and depends on the converter package. The converter package can be used as a stand-alone command line tool (#518)
### Features
- Multiple frames can be composited together with the style option

## Version 1.4.0

### Features
- Added a `canRead` method to the core module (#512)
- Image conversion supports JPEG 2000 (jp2k) compression (#522)
- Image conversion can now convert images readable by large_image sources but not by vips (#529)
- Added an `open` method to the core module as an alias to `getTileSource` (#550)
- Added an `open` method to each file source module (#550)
- Numerous improvement to image converversion (#533, #535, #537, #541, #544, #545, #546, #549)
- Numerous improvement to image conversion (#533, #535, #537, #541, #544, #545, #546, #549)

### Improvements
- Better release bioformats resources (#502)
Expand All @@ -21,6 +23,9 @@
- Support decoding JP2k compressed tiles in the tiff tile source (#514)
- Hardened tests against transient timing issues (#532, #536)

### Changes
- The image conversion task has been split into two packages, large_image_converter and large_image_tasks. The tasks module is used with Girder and Girder Worker for converting images and depends on the converter package. The converter package can be used as a stand-alone command line tool (#518)

### Bug Fixes
- Harden updates of the item view after making a large image (#508, #515)
- Tiles in an unexpected color mode weren't consistently adjusted (#510)
Expand Down
2 changes: 2 additions & 0 deletions docs/source/tilesource_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ A band definition is an object which can contain the following keys:

- ``band``: if -1 or None, the greyscale value is used. Otherwise, a 1-based numerical index into the channels of the image or a string that matches the interpretation of the band ('red', 'green', 'blue', 'gray', 'alpha'). Note that 'gray' on an RGB or RGBA image will use the green band.

- ``frame``: if specified, override the frame parameter used in the tile query for this band. Note that it is more efficient to have at least one band not specify a frame parameter or use the same value as the basic query. Defaults to the frame value of the core query.

- ``min``: the value to map to the first palette value. Defaults to 0. 'auto' to use 0 if the reported minimum and maximum of the band are between [0, 255] or use the reported minimum otherwise. 'min' or 'max' to always uses the reported minimum or maximum.

- ``max``: the value to map to the last palette value. Defaults to 255. 'auto' to use 0 if the reported minimum and maximum of the band are between [0, 255] or use the reported maximum otherwise. 'min' or 'max' to always uses the reported minimum or maximum.
Expand Down
39 changes: 33 additions & 6 deletions large_image/tilesource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import PIL.Image
import PIL.ImageColor
import PIL.ImageDraw
import random
import threading
import xml.etree.ElementTree
from collections import defaultdict
Expand Down Expand Up @@ -500,6 +499,12 @@ def __init__(self, encoding='JPEG', jpegQuality=95, jpegSubsampling=0,
matches the interpretation of the band ('red', 'green',
'blue', 'gray', 'alpha'). Note that 'gray' on an RGB or
RGBA image will use the green band.
frame: if specified, override the frame value for this band.
When used as part of a bands list, this can be used to
composite multiple frames together. It is most efficient
if at least one band either doesn't specify a frame
parameter or specifies the same frame value as the primary
query.
min: the value to map to the first palette value. Defaults to
0. 'auto' to use 0 if the reported minimum and maximum of
the band are between [0, 255] or use the reported minimum
Expand Down Expand Up @@ -1269,7 +1274,7 @@ def _scanForMinMax(self, dtype, frame=None, analysisSize=1024, **kwargs):
self._skipStyle = True
# Divert the tile cache while querying unstyled tiles
classkey = self._classkey
self._classkey = 'nocache' + str(random.random())
self._classkey = self._classkey + '__unstyled'
try:
self._bandRanges[frame] = self.histogram(
dtype=dtype,
Expand Down Expand Up @@ -1333,20 +1338,39 @@ def _getMinMax(self, minmax, value, dtype, bandidx=None, frame=None):
value = 255
return float(value)

def _applyStyle(self, image, style, frame=None):
def _applyStyle(self, image, style, x, y, z, frame=None):
"""
Apply a style to a numpy image.
:param image: the image to modify.
:param style: a style object.
:param x: the x tile position; used for multi-frame styles.
:param y: the y tile position; used for multi-frame styles.
:param z: the z tile position; used for multi-frame styles.
:param frame: the frame to use for auto ranging.
:returns: a styled image.
"""
style = style['bands'] if 'bands' in style else [style]
output = numpy.zeros((image.shape[0], image.shape[1], 4), numpy.float)
mainImage = image
mainFrame = frame
for entry in style:
bandidx = 0 if image.shape[2] <= 2 else 1
band = None
if entry.get('frame') is None or entry.get('frame') == frame:
image = mainImage
frame = mainFrame
else:
frame = entry['frame']
self._skipStyle = True
# Divert the tile cache while querying unstyled tiles
classkey = self._classkey
self._classkey = self._classkey + '__unstyled'
try:
image = self.getTile(x, y, z, frame=frame, numpyAllowed=True)
finally:
del self._skipStyle
self._classkey = classkey
if (isinstance(entry.get('band'), int) and
entry['band'] >= 1 and entry['band'] <= image.shape[2]):
bandidx = entry['band'] - 1
Expand Down Expand Up @@ -1394,21 +1418,24 @@ def _applyStyle(self, image, style, frame=None):
output[:, :, channel], numpy.where(keep, clrs, 0))
return output

def _outputTileNumpyStyle(self, tile, applyStyle, frame=None):
def _outputTileNumpyStyle(self, tile, applyStyle, x, y, z, frame=None):
"""
Convert a tile to a NUMPY array. Optionally apply the style to a tile.
Always returns a NUMPY tile.
:param tile: the tile to convert.
:param applyStyle: if True and there is a style, apply it.
:param x: the x tile position; used for multi-frame styles.
:param y: the y tile position; used for multi-frame styles.
:param z: the z tile position; used for multi-frame styles.
:param frame: the frame to use for auto-ranging.
:returns: a numpy array and a target PIL image mode.
"""
tile, mode = _imageToNumpy(tile)
if applyStyle and getattr(self, 'style', None):
with self._styleLock:
if not getattr(self, '_skipStyle', False):
tile = self._applyStyle(tile, self.style, frame)
tile = self._applyStyle(tile, self.style, x, y, z, frame)
if tile.shape[0] != self.tileHeight or tile.shape[1] != self.tileWidth:
extend = numpy.zeros(
(self.tileHeight, self.tileWidth, tile.shape[2]),
Expand Down Expand Up @@ -1450,7 +1477,7 @@ def _outputTile(self, tile, tileEncoding, x, y, z, pilImageAllowed=False,
mode = None
if (numpyAllowed == 'always' or tileEncoding == TILE_FORMAT_NUMPY or
(applyStyle and getattr(self, 'style', None)) or isEdge):
tile, mode = self._outputTileNumpyStyle(tile, applyStyle, kwargs.get('frame'))
tile, mode = self._outputTileNumpyStyle(tile, applyStyle, x, y, z, kwargs.get('frame'))
if isEdge:
contentWidth = min(self.tileWidth,
sizeX - (maxX - self.tileWidth))
Expand Down
34 changes: 34 additions & 0 deletions test/test_source_ometiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,40 @@ def testStyleAutoMinMax():
assert image[240][128][0] < imageB[240][128][0]


def testStyleFrame():
imagePath = utilities.externaldata('data/sample.ome.tif.sha512')
source = large_image_source_ometiff.open(
imagePath, style=json.dumps({'bands': [{
'palette': ['#000000', '#0000ff'],
}, {
'palette': ['#000000', '#00ff00'],
}]}))
image, _ = source.getRegion(
output={'maxWidth': 256, 'maxHeight': 256}, format=TILE_FORMAT_NUMPY, frame=1)
sourceB = large_image_source_ometiff.open(
imagePath, style=json.dumps({'bands': [{
'palette': ['#000000', '#0000ff'],
}, {
'frame': 1,
'palette': ['#000000', '#00ff00'],
}]}))
imageB, _ = sourceB.getRegion(
output={'maxWidth': 256, 'maxHeight': 256}, format=TILE_FORMAT_NUMPY, frame=1)
assert numpy.all(image == imageB)
assert image.shape == imageB.shape
sourceC = large_image_source_ometiff.open(
imagePath, style=json.dumps({'bands': [{
'palette': ['#000000', '#0000ff'],
}, {
'frame': 2,
'palette': ['#000000', '#00ff00'],
}]}))
imageC, _ = sourceC.getRegion(
output={'maxWidth': 256, 'maxHeight': 256}, format=TILE_FORMAT_NUMPY, frame=1)
assert numpy.any(image != imageC)
assert image.shape == imageC.shape


def testInternalMetadata():
imagePath = utilities.externaldata('data/sample.ome.tif.sha512')
source = large_image_source_ometiff.open(imagePath)
Expand Down

0 comments on commit 155e53e

Please sign in to comment.