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

Convert to svs #580

Merged
merged 2 commits into from
Mar 31, 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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ node_modules/
.eslintcache
test/externaldata

docs/source
docs/source/*
!docs/source/*.py
!docs/source/*.rst
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features
- Allow converting a single frame of a multiframe image (#579)
- Add a convert endpoint to the Girder plugin (#578)
- Added support for creating Aperio svs files (#580)

### Improvements
- More untiled tiff files are handles by the bioformats reader (#569)
Expand Down
2 changes: 2 additions & 0 deletions docs/make_docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pip install -e . -r requirements-dev.txt --find-links https://girder.github.io/l
popd
# git clean -fxd .

large_image_converter --help > source/large_image_converter.txt

sphinx-apidoc -f -o source/large_image ../large_image
sphinx-apidoc -f -o source/large_image_source_dummy ../sources/dummy/large_image_source_dummy
sphinx-apidoc -f -o source/large_image_source_gdal ../sources/gdal/large_image_source_gdal
Expand Down
10 changes: 10 additions & 0 deletions docs/source/image_conversion.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Image Conversion
================

The large_image library can read a variety of images with the various tile source modules. Some image files that cannot be read directly can be converted into a format that can be read by the large_image library. Additionally, some images that can be read are very slow to handle because they are stored inefficiently, and converting them will make a equivalent file that is more efficient.

Installing the ``large-image-converter`` module adds a ``large_image_converter`` command to the local environment. Running ``large_image_converter --help`` displays the various options.

.. include:: large_image_converter.txt
:literal:

1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ large_image also works as a Girder plugin with optional annotation support.
:caption: Contents:

tilesource_options
image_conversion
large_image/modules
large_image_source_dummy/modules
large_image_source_gdal/modules
Expand Down
2 changes: 1 addition & 1 deletion docs/source/tilesource_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ A band definition is an object which can contain the following keys:

- ``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.

- ``frameidelta``: if specified, and ``frame`` is not specified, override the frame parameter used in the tile query for this band by adding the value to the current frame number. If many different frames are being requested, all with the same ``framedelta``, this is more efficient than varying the ``frame`` within the style.
- ``framedelta``: if specified, and ``frame`` is not specified, override the frame parameter used in the tile query for this band by adding the value to the current frame number. If many different frames are being requested, all with the same ``framedelta``, this is more efficient than varying the ``frame`` within the style.

- ``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.

Expand Down
2 changes: 2 additions & 0 deletions girder/girder_large_image/rest/tiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ def createTiles(self, item, params):
.param('onlyFrame', 'Only convert a specific 0-based frame of a '
'multiframe file. If not specified, all frames are converted.',
dataType='int', required=False)
.param('format', 'File format', required=False,
enum=['tiff', 'aperio'])
.param('compression', 'Internal compression format', required=False,
enum=['none', 'jpeg', 'deflate', 'lzw', 'zstd', 'packbits', 'webp', 'jp2k'])
.param('quality', 'JPEG compression quality where 0 is small and 100 '
Expand Down
2 changes: 1 addition & 1 deletion large_image/tilesource/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def isGeospatial(path):
"""
Check if a path is likely to be a geospatial file.

:params path: The path to the file
:param path: The path to the file
:returns: True if geospatial.
"""
try:
Expand Down
1 change: 1 addition & 0 deletions sources/openslide/large_image_source_openslide/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ def _getAssociatedImage(self, imageKey):
return None
if images[imageKey] == 'openslide':
try:
print('YYYYYYY')
return self._openslide.associated_images[imageKey]
except openslide.lowlevel.OpenSlideError:
return None
Expand Down
43 changes: 38 additions & 5 deletions sources/tiff/large_image_source_tiff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,36 @@ def _initWithTiffTools(self): # noqa
'It will be inefficient to read lower resolution tiles.', maxMissing)
return True

def _reorient_numpy_image(self, image, orientation):
"""
Reorient a numpy image array based on a tiff orientation.

:param image: the numpy array to reorient.
:param orientation: one of the tiff orientation constants.
:returns: an image with top-left orientation.
"""
if len(image.shape) == 2:
image = numpy.resize(image, (image.shape[0], image.shape[1], 1))
if orientation in {
tifftools.constants.Orientation.LeftTop.value,
tifftools.constants.Orientation.RightTop.value,
tifftools.constants.Orientation.LeftBottom.value,
tifftools.constants.Orientation.RightBottom.value}:
image = image.transpose(1, 0, 2)
if orientation in {
tifftools.constants.Orientation.BottomLeft.value,
tifftools.constants.Orientation.BottomRight.value,
tifftools.constants.Orientation.LeftBottom.value,
tifftools.constants.Orientation.RightBottom.value}:
image = image[::-1, ::, ::]
if orientation in {
tifftools.constants.Orientation.TopRight.value,
tifftools.constants.Orientation.BottomRight.value,
tifftools.constants.Orientation.RightTop.value,
tifftools.constants.Orientation.RightBottom.value}:
image = image[::, ::-1, ::]
return image

def _addAssociatedImage(self, largeImagePath, directoryNum, mustBeTiled=False, topImage=None):
"""
Check if the specified TIFF directory contains an image with a sensible
Expand All @@ -335,9 +365,11 @@ def _addAssociatedImage(self, largeImagePath, directoryNum, mustBeTiled=False, t
try:
associated = TiledTiffDirectory(largeImagePath, directoryNum, mustBeTiled)
id = ''
if associated._tiffInfo.get('imagedescription'):
id = associated._tiffInfo.get(
'imagedescription').strip().split(None, 1)[0].lower()
desc = associated._tiffInfo.get('imagedescription')
if desc:
id = desc.strip().split(None, 1)[0].lower()
if b'\n' in desc:
id = desc.split(b'\n', 1)[1].strip().split(None, 1)[0].lower() or id
elif mustBeTiled:
id = 'dir%d' % directoryNum
if not len(self._associatedImages):
Expand All @@ -357,6 +389,7 @@ def _addAssociatedImage(self, largeImagePath, directoryNum, mustBeTiled=False, t
if image.tobytes()[:6] == b'<?xml ':
self._parseImageXml(image.tobytes().rsplit(b'>', 1)[0] + b'>', topImage)
return
image = self._reorient_numpy_image(image, associated._tiffInfo.get('orientation'))
self._associatedImages[id] = image
except (TiffException, AttributeError):
# If we can't validate or read an associated image or it has no
Expand All @@ -374,8 +407,8 @@ def _parseImageXml(self, xml, topImage):
Parse metadata stored in arbitrary xml and associate it with a specific
image.

:params xml: the xml as a string or bytes object.
:params topImage: the image to add metadata to.
:param xml: the xml as a string or bytes object.
:param topImage: the image to add metadata to.
"""
if not topImage or topImage.pixelInfo.get('magnificaiton'):
return
Expand Down
18 changes: 18 additions & 0 deletions test/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import shutil
import tifftools

import large_image
from large_image import constants
import large_image_source_tiff

Expand Down Expand Up @@ -200,6 +201,23 @@ def testConvertFromMultiframeImageOnlyOneFrame(tmpdir):
assert len(info['ifds']) == 5


def testConvertToAperio(tmpdir):
imagePath = utilities.externaldata('data/huron.image2_jpeg2k.tif.sha512')
outputPath = os.path.join(tmpdir, 'out.svs')
large_image_converter.convert(imagePath, outputPath, format='aperio')
source = large_image.open(outputPath)
assert 'openslide' in source.name
assert 'label' in source.getAssociatedImagesList()


def testConvertMultiframeToAperio(tmpdir):
imagePath = utilities.externaldata('data/sample.ome.tif.sha512')
outputPath = os.path.join(tmpdir, 'out.tiff')
large_image_converter.convert(imagePath, outputPath, format='aperio', compression='jp2k')
source = large_image.open(outputPath)
assert 'label' in source.getAssociatedImagesList()


# Test main program

def testConverterMain(tmpdir):
Expand Down
Loading