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 compositing multiple frames together. #554

Merged
merged 1 commit into from
Feb 24, 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
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