Skip to content

Commit

Permalink
Merge pull request #764 from girder/multi-source
Browse files Browse the repository at this point in the history
Add multi-source source
  • Loading branch information
manthey authored Jan 28, 2022
2 parents df2ea8d + c2b89c0 commit f69b1ef
Show file tree
Hide file tree
Showing 26 changed files with 1,505 additions and 9 deletions.
2 changes: 2 additions & 0 deletions .circleci/make_wheels.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ cd "$ROOTPATH/sources/gdal"
pip wheel . --no-deps -w ~/wheels && rm -rf build
cd "$ROOTPATH/sources/mapnik"
pip wheel . --no-deps -w ~/wheels && rm -rf build
cd "$ROOTPATH/sources/multi"
pip wheel . --no-deps -w ~/wheels && rm -rf build
cd "$ROOTPATH/sources/nd2"
pip wheel . --no-deps -w ~/wheels && rm -rf build
cd "$ROOTPATH/sources/ometiff"
Expand Down
6 changes: 6 additions & 0 deletions .circleci/release_pypi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ cp "$ROOTPATH/LICENSE" .
python setup.py sdist
pip wheel . --no-deps -w dist
twine upload --verbose dist/*
cd "$ROOTPATH/sources/multi"
cp "$ROOTPATH/README.rst" .
cp "$ROOTPATH/LICENSE" .
python setup.py sdist
pip wheel . --no-deps -w dist
twine upload --verbose dist/*
cd "$ROOTPATH/sources/nd2"
cp "$ROOTPATH/README.rst" .
cp "$ROOTPATH/LICENSE" .
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

### Features
- Initial implementation of multi-source tile source ([#764](../../pull/764))

### Improvements
- Add more opacity support for image overlays ([#761](../../pull/761))
- Make annotation schema more uniform ([#763](../../pull/763))
Expand Down
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ Large Image consists of several Python modules designed to work together. These

- ``large-image-source-deepzoom``: A tile source for reading Deepzoom tiles.

- ``large-image-source-multi``: A tile source for compisiting other tile sources into a single multi-frame source.

- ``large-image-source-test``: A tile source that generates test tiles, including a simple fractal pattern. Useful for testing extreme zoom levels.

- ``large-image-source-dummy``: A tile source that does nothing.
Expand Down
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
_build/large_image_source_dummy/modules
_build/large_image_source_gdal/modules
_build/large_image_source_mapnik/modules
_build/large_image_source_multi/modules
multi_source_specification
_build/large_image_source_nd2/modules
_build/large_image_source_ometiff/modules
_build/large_image_source_openjpeg/modules
Expand Down
2 changes: 2 additions & 0 deletions docs/make_docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ ln -s ../build/docs-work _build

large_image_converter --help > _build/large_image_converter.txt
python -c 'from girder_large_image_annotation.models import annotation;import json;print(json.dumps(annotation.AnnotationSchema.annotationSchema, indent=2))' > _build/annotation_schema.json
python -c 'import large_image_source_multi, json;print(json.dumps(large_image_source_multi.MultiSourceSchema, indent=2))' > _build/multi_source_schema.json

sphinx-apidoc -f -o _build/large_image ../large_image
sphinx-apidoc -f -o _build/large_image_source_bioformats ../sources/bioformats/large_image_source_bioformats
sphinx-apidoc -f -o _build/large_image_source_deepzoom ../sources/deepzoom/large_image_source_deepzoom
sphinx-apidoc -f -o _build/large_image_source_dummy ../sources/dummy/large_image_source_dummy
sphinx-apidoc -f -o _build/large_image_source_gdal ../sources/gdal/large_image_source_gdal
sphinx-apidoc -f -o _build/large_image_source_mapnik ../sources/mapnik/large_image_source_mapnik
sphinx-apidoc -f -o _build/large_image_source_multi ../sources/multi/large_image_source_multi
sphinx-apidoc -f -o _build/large_image_source_nd2 ../sources/nd2/large_image_source_nd2
sphinx-apidoc -f -o _build/large_image_source_ometiff ../sources/ometiff/large_image_source_ometiff
sphinx-apidoc -f -o _build/large_image_source_openjpeg ../sources/openjpeg/large_image_source_openjpeg
Expand Down
6 changes: 6 additions & 0 deletions docs/multi_source_specification.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.. include:: ../sources/multi/docs/specification.rst

This returns the following:

.. include:: ../build/docs-work/multi_source_schema.json
:literal:
15 changes: 6 additions & 9 deletions large_image/tilesource/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
TileOutputPILFormat, dtypeToGValue)
from .tiledict import LazyTileDict
from .utilities import (_encodeImage, _gdalParameters, # noqa: F401
_imageToNumpy, _imageToPIL, _letterboxImage, _vipsCast,
_vipsParameters, dictToEtree, etreeToDict,
getPaletteColors, nearPowerOfTwo)
_imageToNumpy, _imageToPIL, _letterboxImage,
_makeSameChannelDepth, _vipsCast, _vipsParameters,
dictToEtree, etreeToDict, getPaletteColors,
nearPowerOfTwo)


class TileSource:
Expand Down Expand Up @@ -1668,12 +1669,8 @@ def _addRegionTileToImage(
raise exceptions.TileSourceError(
'Insufficient memory to get region of %d x %d pixels.' % (
width, height))
if subimage.shape[2] > image.shape[2]:
newimage = numpy.ones((image.shape[0], image.shape[1], subimage.shape[2]))
newimage[:, :, :image.shape[2]] = image
image = newimage
image[y:y + subimage.shape[0], x:x + subimage.shape[1],
:subimage.shape[2]] = subimage
image, subimage = _makeSameChannelDepth(image, subimage)
image[y:y + subimage.shape[0], x:x + subimage.shape[1], :] = subimage
return image

def _vipsAddAlphaBand(self, vimg, *otherImages):
Expand Down
43 changes: 43 additions & 0 deletions large_image/tilesource/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,3 +543,46 @@ def getAvailableNamedPalettes(includeColors=True, reduced=False):
(key.rsplit('_', 1)[0] + '_' + str(int(
key.rsplit('_', 1)[-1]) + 1)) not in palettes)}
return sorted(palettes)


def _makeSameChannelDepth(arr1, arr2):
"""
Given two numpy arrays that are either two or three dimensions, make the
third dimension the same for both of them. Specifically, if they are two
dimensions, first convert to trhee dimensions with a single final value.
Otherwise, the dimensions are assumed to be channels of L, LA, RGB, RGBA,
or <all colors>. If L is needed to change to RGB, it is repeated (LLL).
Missing A channels are filled with 1.
:param arr1: one array to compare.
:param arr2: a second array to compare.
:returns: the two arrays, possibly modified.
"""
arrays = {
'arr1': arr1,
'arr2': arr2,
}
# Make sure we have 3 dimensional arrays
for key, arr in arrays.items():
if len(arr.shape) == 2:
arrays[key] = numpy.resize(arr, (arr.shape[0], arr.shape[1], 1))
# If any array is RGB, make sure all arrays are RGB.
for key, arr in arrays.items():
other = arrays['arr1' if key == 'arr2' else 'arr2']
if arr.shape[2] < 3 and other.shape[2] >= 3:
newarr = numpy.ones((arr.shape[0], arr.shape[1], arr.shape[2] + 2))
newarr[:, :, 0:1] = arr[:, :, 0:1]
newarr[:, :, 1:2] = arr[:, :, 0:1]
newarr[:, :, 2:3] = arr[:, :, 0:1]
if arr.shape[2] == 2:
newarr[:, :, 3:4] = arr[:, :, 1:2]
arrays[key] = newarr
# If only one array has an A channel, make sure all arrays have an A
# channel
for key, arr in arrays.items():
other = arrays['arr1' if key == 'arr2' else 'arr2']
if arr.shape[2] < other.shape[2]:
newarr = numpy.ones((arr.shape[0], arr.shape[1], other.shape[2]))
newarr[:, :, :arr.shape[2]] = arr
arrays[key] = newarr
return arrays['arr1'], arrays['arr2']
1 change: 1 addition & 0 deletions requirements-dev-core.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
-e sources/deepzoom
-e sources/dummy
-e sources/gdal
-e sources/multi
-e sources/nd2
-e sources/openjpeg
-e sources/openslide
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ girder-jobs>=3.0.3
-e sources/deepzoom
-e sources/dummy
-e sources/gdal
-e sources/multi
-e sources/nd2
-e sources/openjpeg
-e sources/openslide
Expand Down
1 change: 1 addition & 0 deletions requirements-worker.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
-e sources/deepzoom
-e sources/dummy
-e sources/gdal
-e sources/multi
-e sources/nd2
-e sources/openjpeg
-e sources/openslide
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'dummy': ['large-image-source-dummy'],
'gdal': ['large-image-source-gdal'],
'mapnik': ['large-image-source-mapnik'],
'multi': ['large-image-source-multi'],
'nd2': ['large-image-source-nd2'],
'ometiff': ['large-image-source-ometiff'],
'openjpeg': ['large-image-source-openjpeg'],
Expand Down
136 changes: 136 additions & 0 deletions sources/multi/docs/specification.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
Multi Source Schema
===================

A multi-source tile source is used to composite multiple other sources into a
single conceptual tile source. It is specified by a yaml or json file that
conforms to the appropriate schema.

Examples
--------

All of the examples presented here are in yaml; json works just as well.

Multi Z-position
~~~~~~~~~~~~~~~~

For example, if you have a set of individual files that you wish to treat as
multiple z slices in a single file, you can do something like:

::

---
sources:
- path: ./test_orient1.tif
z: 0
- path: ./test_orient2.tif
z: 1
- path: ./test_orient3.tif
z: 2
- path: ./test_orient4.tif
z: 3
- path: ./test_orient5.tif
z: 4
- path: ./test_orient6.tif
z: 5
- path: ./test_orient7.tif
z: 6
- path: ./test_orient8.tif
z: 7

Here, each of the files is explicitly listed with a specific ``z`` value.
Since these files are ordered, this could equivalently be done in a simpler
manner using a ``pathPattern``, which is a regular expression that can match
multiple files.

::

---
sources:
- path: .
pathPattern: 'test_orient[1-8]\.tif'
zStep: 1

Since the ``z`` value will default to 0, this works. The files are sorted in
C-sort order (lexically using the ASCII or UTF code points). This sorting will
break down if you have files with variable length numbers (e.g., ``file10.tif``
will appear before ``file9.tiff``. You can instead assign values from the
file name using named expressions:

::

---
sources:
- path: .
pathPattern: 'test_orient(?P<z1>[1-8])\.tif'

Note that the name in the expression (``z1`` in this example) is the name of
the value in the schema. If a ``1`` is added, then it is assumed to be 1-based
indexed. Without the ``1``, it is assumed to be zero-indexed.

Composite To A Single Frame
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Multiple sources can be made to appear as a single frame. For instance:

::

---
width: 360
height: 360
sources:
- path: ./test_orient1.tif
position:
x: 0
y: 0
- path: ./test_orient2.tif
position:
x: 180
y: 0
- path: ./test_orient3.tif
position:
x: 0
y: 180
- path: ./test_orient4.tif
position:
x: 180
y: 180

Here, the total width and height of the final image is specified, along with
the upper-left position of each image in the frame.

Composite With Scaling
~~~~~~~~~~~~~~~~~~~~~~

Transforms can be applied to scale the individual sources:

::

---
width: 720
height: 720
sources:
- path: ./test_orient1.tif
position:
scale: 2
- path: ./test_orient2.tif
position:
scale: 2
x: 360
- path: ./test_orient3.tif
position:
scale: 2
y: 360
- path: ./test_orient4.tif
position:
scale: 360
x: 180
y: 180

Note that the zero values from the previous example have been omitted as they
are unnecessary.

Full Schema
-----------

The full schema (jsonschema Draft6 standard) can be obtained by referencing the
Python at ``large_image_source_multi.MultiSourceSchema``.
Loading

0 comments on commit f69b1ef

Please sign in to comment.