From 78fb12f488e381c4928949e6e4c17890828c9cf2 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 3 May 2023 11:14:37 -0400 Subject: [PATCH 01/13] Add tile source _dtype attribute --- large_image/tilesource/base.py | 6 ++++++ .../bioformats/large_image_source_bioformats/__init__.py | 1 + sources/deepzoom/large_image_source_deepzoom/__init__.py | 2 ++ sources/dicom/large_image_source_dicom/__init__.py | 1 + sources/gdal/large_image_source_gdal/__init__.py | 1 + sources/mapnik/large_image_source_mapnik/__init__.py | 2 ++ sources/multi/large_image_source_multi/__init__.py | 1 + sources/nd2/large_image_source_nd2/__init__.py | 1 + sources/ometiff/large_image_source_ometiff/__init__.py | 2 ++ sources/openjpeg/large_image_source_openjpeg/__init__.py | 1 + sources/openslide/large_image_source_openslide/__init__.py | 2 ++ sources/pil/large_image_source_pil/__init__.py | 1 + sources/pil/large_image_source_pil/girder_source.py | 2 ++ sources/test/large_image_source_test/__init__.py | 2 ++ sources/tiff/large_image_source_tiff/__init__.py | 4 ++++ sources/tiff/large_image_source_tiff/tiff_reader.py | 1 + sources/tifffile/large_image_source_tifffile/__init__.py | 1 + sources/vips/large_image_source_vips/__init__.py | 1 + 18 files changed, 32 insertions(+) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index f88b91e3f..98a0c095c 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -132,6 +132,8 @@ def __init__(self, encoding='JPEG', jpegQuality=95, jpegSubsampling=0, self.sizeX = None self.sizeY = None self._styleLock = threading.RLock() + self._dtype = None + self._predicted_dtype = None if encoding not in TileOutputMimeTypes: raise ValueError('Invalid encoding "%s"' % encoding) @@ -202,6 +204,10 @@ def _setStyle(self, style): def style(self): return self._style + @property + def dtype(self): + return self._dtype or self._predicted_dtype + @staticmethod def getLRUHash(*args, **kwargs): """ diff --git a/sources/bioformats/large_image_source_bioformats/__init__.py b/sources/bioformats/large_image_source_bioformats/__init__.py index b3a7994d1..fdf3ee596 100644 --- a/sources/bioformats/large_image_source_bioformats/__init__.py +++ b/sources/bioformats/large_image_source_bioformats/__init__.py @@ -638,6 +638,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): retile[0:min(tile.shape[0], finalHeight), 0:min(tile.shape[1], finalWidth)] = tile[ 0:min(tile.shape[0], finalHeight), 0:min(tile.shape[1], finalWidth)] tile = retile + self._dtype = tile.dtype return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) def getAssociatedImagesList(self): diff --git a/sources/deepzoom/large_image_source_deepzoom/__init__.py b/sources/deepzoom/large_image_source_deepzoom/__init__.py index 5f9e6268f..21d1213eb 100644 --- a/sources/deepzoom/large_image_source_deepzoom/__init__.py +++ b/sources/deepzoom/large_image_source_deepzoom/__init__.py @@ -6,6 +6,7 @@ from xml.etree import ElementTree import PIL.Image +import numpy from large_image.cache_util import LruCacheMetaclass, methodcache from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority @@ -113,6 +114,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): overlap if x else 0, overlap if y else 0, self.tileWidth + (overlap if x else 0), self.tileHeight + (overlap if y else 0))) + self._dtype = numpy.array(tile) return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/dicom/large_image_source_dicom/__init__.py b/sources/dicom/large_image_source_dicom/__init__.py index a2c236efd..18c96138f 100644 --- a/sources/dicom/large_image_source_dicom/__init__.py +++ b/sources/dicom/large_image_source_dicom/__init__.py @@ -246,6 +246,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): tile = _imageToPIL(tile) if bw > self.tileWidth or bh > self.tileHeight: tile = tile.resize((self.tileWidth, self.tileHeight)) + self._dtype = numpy.asarray(tile).dtype return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/gdal/large_image_source_gdal/__init__.py b/sources/gdal/large_image_source_gdal/__init__.py index 2a62772c5..b2765aace 100644 --- a/sources/gdal/large_image_source_gdal/__init__.py +++ b/sources/gdal/large_image_source_gdal/__init__.py @@ -809,6 +809,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): tile = ds.ReadAsArray() if len(tile.shape) == 3: tile = numpy.rollaxis(tile, 0, 3) + self._dtype = tile.dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/mapnik/large_image_source_mapnik/__init__.py b/sources/mapnik/large_image_source_mapnik/__init__.py index 9a48a765d..c395a5f88 100644 --- a/sources/mapnik/large_image_source_mapnik/__init__.py +++ b/sources/mapnik/large_image_source_mapnik/__init__.py @@ -17,6 +17,7 @@ import functools import mapnik +import numpy import PIL.Image from large_image_source_gdal import GDALFileTileSource, InitPrefix from osgeo import gdal, gdalconst @@ -390,6 +391,7 @@ def getTile(self, x, y, z, **kwargs): pilimg = PIL.Image.frombytes('RGBA', (img.width(), img.height()), img.tostring()) if overscan: pilimg = pilimg.crop((1, 1, pilimg.width - overscan, pilimg.height - overscan)) + self._dtype = numpy.asarray(pilimg).dtype return self._outputTile(pilimg, TILE_FORMAT_PIL, x, y, z, applyStyle=False, **kwargs) diff --git a/sources/multi/large_image_source_multi/__init__.py b/sources/multi/large_image_source_multi/__init__.py index 013a2fcdf..8e58c3043 100644 --- a/sources/multi/large_image_source_multi/__init__.py +++ b/sources/multi/large_image_source_multi/__init__.py @@ -1059,6 +1059,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): colors = self._info.get('backgroundColor', [0]) if colors: tile = numpy.full((self.tileHeight, self.tileWidth, len(colors)), colors) + self._dtype = tile.dtype # We should always have a tile return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/nd2/large_image_source_nd2/__init__.py b/sources/nd2/large_image_source_nd2/__init__.py index cff5edc17..99a4eaf25 100644 --- a/sources/nd2/large_image_source_nd2/__init__.py +++ b/sources/nd2/large_image_source_nd2/__init__.py @@ -279,6 +279,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): with self._tileLock: # Have dask use single-threaded since we are using a lock anyway. tile = tileframe[y0:y1:step, x0:x1:step].compute(scheduler='single-threaded').copy() + self._dtype = tile.dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/ometiff/large_image_source_ometiff/__init__.py b/sources/ometiff/large_image_source_ometiff/__init__.py index 28f075822..764ded556 100644 --- a/sources/ometiff/large_image_source_ometiff/__init__.py +++ b/sources/ometiff/large_image_source_ometiff/__init__.py @@ -351,8 +351,10 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, format = 'JPEG' if isinstance(tile, PIL.Image.Image): format = TILE_FORMAT_PIL + self._dtype = numpy.asarray(tile).dtype if isinstance(tile, numpy.ndarray): format = TILE_FORMAT_NUMPY + self._dtype = tile.dtype return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) except InvalidOperationTiffException as e: diff --git a/sources/openjpeg/large_image_source_openjpeg/__init__.py b/sources/openjpeg/large_image_source_openjpeg/__init__.py index 06ae15180..3a4e0a398 100644 --- a/sources/openjpeg/large_image_source_openjpeg/__init__.py +++ b/sources/openjpeg/large_image_source_openjpeg/__init__.py @@ -264,6 +264,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): self._openjpegHandles.put(openjpegHandle) if scale: tile = tile[::scale, ::scale] + self._dtype = tile.dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/openslide/large_image_source_openslide/__init__.py b/sources/openslide/large_image_source_openslide/__init__.py index 385494837..26ddac299 100644 --- a/sources/openslide/large_image_source_openslide/__init__.py +++ b/sources/openslide/large_image_source_openslide/__init__.py @@ -19,6 +19,7 @@ import openslide import PIL +import numpy import tifftools from large_image.cache_util import LruCacheMetaclass, methodcache @@ -302,6 +303,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): if svslevel['scale'] != 1: tile = tile.resize((self.tileWidth, self.tileHeight), getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS) + self._dtype = numpy.asarray(tile).dtype return self._outputTile(tile, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/pil/large_image_source_pil/__init__.py b/sources/pil/large_image_source_pil/__init__.py index 6dc3ac6f1..7184ea5ee 100644 --- a/sources/pil/large_image_source_pil/__init__.py +++ b/sources/pil/large_image_source_pil/__init__.py @@ -269,6 +269,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, numpy.asarray(img), self._factor))) else: img = self._pilImage + self._dtype = numpy.asarray(img).dtype return self._outputTile(img, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/pil/large_image_source_pil/girder_source.py b/sources/pil/large_image_source_pil/girder_source.py index 9706e4ff7..fea2c9d4a 100644 --- a/sources/pil/large_image_source_pil/girder_source.py +++ b/sources/pil/large_image_source_pil/girder_source.py @@ -15,6 +15,7 @@ ############################################################################# import cherrypy +import numpy from girder_large_image.constants import PluginSettings from girder_large_image.girder_tilesource import GirderTileSource @@ -64,5 +65,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, url = '%s/api/v1/file/%s/download' % ( cherrypy.request.base, self.item['largeImage']['fileId']) raise cherrypy.HTTPRedirect(url) + self._dtype = numpy.asarray(self._pilImage).dtype return self._outputTile(self._pilImage, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/test/large_image_source_test/__init__.py b/sources/test/large_image_source_test/__init__.py index 6b9e48c92..4619f325b 100644 --- a/sources/test/large_image_source_test/__init__.py +++ b/sources/test/large_image_source_test/__init__.py @@ -302,6 +302,7 @@ def getTile(self, x, y, z, *args, **kwargs): if self.monochrome: image = image.convert('L') format = TILE_FORMAT_PIL + self._dtype = numpy.asarray(image).dtype else: image = numpy.zeros( (self.tileHeight, self.tileWidth, len(self._bands)), dtype=self._dtype) @@ -320,6 +321,7 @@ def getTile(self, x, y, z, *args, **kwargs): bandimg = bandimg.astype(self._dtype) image[:, :, bandnum] = bandimg[:, :, bandnum % bandimg.shape[2]] format = TILE_FORMAT_NUMPY + self._dtype = image.dtype return self._outputTile(image, format, x, y, z, **kwargs) @staticmethod diff --git a/sources/tiff/large_image_source_tiff/__init__.py b/sources/tiff/large_image_source_tiff/__init__.py index c9c7de9b5..7fd666ff1 100644 --- a/sources/tiff/large_image_source_tiff/__init__.py +++ b/sources/tiff/large_image_source_tiff/__init__.py @@ -593,10 +593,13 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, else: tile = dir.getTile(x, y) format = 'JPEG' + self._dtype = numpy.uint8 if isinstance(tile, PIL.Image.Image): format = TILE_FORMAT_PIL + self._dtype = numpy.asarray(tile).dtype if isinstance(tile, numpy.ndarray): format = TILE_FORMAT_NUMPY + self._dtype = tile.dtype return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, applyStyle=allowStyle, **kwargs) except InvalidOperationTiffException as e: @@ -646,6 +649,7 @@ def getTileIOTiffError(self, x, y, z, pilImageAllowed=False, image = image.resize((self.tileWidth, self.tileHeight)) else: image = PIL.Image.new('RGBA', (self.tileWidth, self.tileHeight)) + self._dtype = image.dtype return self._outputTile(image, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, applyStyle=False, **kwargs) raise TileSourceError('Internal I/O failure: %s' % exception.args[0]) diff --git a/sources/tiff/large_image_source_tiff/tiff_reader.py b/sources/tiff/large_image_source_tiff/tiff_reader.py index 2bec9b926..73d93c1b5 100644 --- a/sources/tiff/large_image_source_tiff/tiff_reader.py +++ b/sources/tiff/large_image_source_tiff/tiff_reader.py @@ -825,6 +825,7 @@ def getTile(self, x, y): # multiple times, which sometimes throws an exception in PIL's JPEG # 2000 module. image = image.convert('RGB') + self._dtype = numpy.asarray(image).dtype return image def parse_image_description(self, meta=None): # noqa diff --git a/sources/tifffile/large_image_source_tifffile/__init__.py b/sources/tifffile/large_image_source_tifffile/__init__.py index c4cc769c1..d8340f016 100644 --- a/sources/tifffile/large_image_source_tifffile/__init__.py +++ b/sources/tifffile/large_image_source_tifffile/__init__.py @@ -459,6 +459,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): if baxis not in {'YXS', 'YX'}: tile = numpy.moveaxis( tile, [baxis.index(a) for a in 'YXS' if a in baxis], range(len(baxis))) + self._dtype = tile.dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/vips/large_image_source_vips/__init__.py b/sources/vips/large_image_source_vips/__init__.py index 0d88eb2b0..ff4e4103c 100644 --- a/sources/vips/large_image_source_vips/__init__.py +++ b/sources/vips/large_image_source_vips/__init__.py @@ -212,6 +212,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): buffer=tileimg.write_to_memory(), dtype=GValueToDtype[tileimg.format], shape=[tileimg.height, tileimg.width, tileimg.bands]) + self._dtype = tile.dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) From be4c75497704f5ec37a4cecb2165aa29c45fc421 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 3 May 2023 11:21:11 -0400 Subject: [PATCH 02/13] Lint fix --- sources/deepzoom/large_image_source_deepzoom/__init__.py | 2 +- sources/openslide/large_image_source_openslide/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sources/deepzoom/large_image_source_deepzoom/__init__.py b/sources/deepzoom/large_image_source_deepzoom/__init__.py index 21d1213eb..89d8d8f2f 100644 --- a/sources/deepzoom/large_image_source_deepzoom/__init__.py +++ b/sources/deepzoom/large_image_source_deepzoom/__init__.py @@ -5,8 +5,8 @@ import os from xml.etree import ElementTree -import PIL.Image import numpy +import PIL.Image from large_image.cache_util import LruCacheMetaclass, methodcache from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority diff --git a/sources/openslide/large_image_source_openslide/__init__.py b/sources/openslide/large_image_source_openslide/__init__.py index 26ddac299..0efff25d8 100644 --- a/sources/openslide/large_image_source_openslide/__init__.py +++ b/sources/openslide/large_image_source_openslide/__init__.py @@ -17,9 +17,9 @@ import math import os +import numpy import openslide import PIL -import numpy import tifftools from large_image.cache_util import LruCacheMetaclass, methodcache From 4e76bd048c3780e7de9826ed57fb3ba9b014aa45 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 3 May 2023 12:38:10 -0400 Subject: [PATCH 03/13] Update sources/deepzoom/large_image_source_deepzoom/__init__.py Co-authored-by: Bane Sullivan --- sources/deepzoom/large_image_source_deepzoom/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/deepzoom/large_image_source_deepzoom/__init__.py b/sources/deepzoom/large_image_source_deepzoom/__init__.py index 89d8d8f2f..c0cfbb618 100644 --- a/sources/deepzoom/large_image_source_deepzoom/__init__.py +++ b/sources/deepzoom/large_image_source_deepzoom/__init__.py @@ -114,7 +114,7 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): overlap if x else 0, overlap if y else 0, self.tileWidth + (overlap if x else 0), self.tileHeight + (overlap if y else 0))) - self._dtype = numpy.array(tile) + self._dtype = numpy.array(tile).dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) From 0f9816316d6d2ba627b0e1b043dfa637ca299ae6 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 3 May 2023 14:03:00 -0400 Subject: [PATCH 04/13] Add dtype to getMetadata result --- large_image/tilesource/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index 98a0c095c..9936f2fab 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -1601,6 +1601,7 @@ def getMetadata(self): :magnification: if known, the magnificaiton of the image. :mm_x: if known, the width of a pixel in millimeters. :mm_y: if known, the height of a pixel in millimeters. + :dtype: if known, the type of values in this image. In addition to the keys that listed above, tile sources that expose multiple frames will also contain @@ -1631,7 +1632,7 @@ def getMetadata(self): :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 + Note that this does not include band information, though some tile sources may do so. """ mag = self.getNativeMagnification() @@ -1644,6 +1645,7 @@ def getMetadata(self): 'magnification': mag['magnification'], 'mm_x': mag['mm_x'], 'mm_y': mag['mm_y'], + 'dtype': self.dtype, }) @property From 06eb77b1146bacc2afc2257df5632520d691a3a7 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Thu, 11 May 2023 12:25:37 -0400 Subject: [PATCH 05/13] Remove predicted dtype --- large_image/tilesource/base.py | 22 ++++++++++++++++--- .../tiff/large_image_source_tiff/__init__.py | 12 ++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index 9936f2fab..cd9ab5875 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -133,7 +133,6 @@ def __init__(self, encoding='JPEG', jpegQuality=95, jpegSubsampling=0, self.sizeY = None self._styleLock = threading.RLock() self._dtype = None - self._predicted_dtype = None if encoding not in TileOutputMimeTypes: raise ValueError('Invalid encoding "%s"' % encoding) @@ -206,7 +205,22 @@ def style(self): @property def dtype(self): - return self._dtype or self._predicted_dtype + if not self._dtype: + with self._styleLock: + if not hasattr(self, '_skipStyle'): + self._setSkipStyle(True) + try: + sample, format = self.getRegion( + width=1, height=1, + region=dict(left=0, right=0, regionWidth=1, regionHeight=1), + format=TILE_FORMAT_NUMPY) + self._dtype = sample.dtype + finally: + self._setSkipStyle(False) + else: + raise exceptions.TileSourceError("NO!") + + return self._dtype @staticmethod def getLRUHash(*args, **kwargs): @@ -1645,7 +1659,9 @@ def getMetadata(self): 'magnification': mag['magnification'], 'mm_x': mag['mm_x'], 'mm_y': mag['mm_y'], - 'dtype': self.dtype, + # Use private attribute; public property + # incurs calcuation cost if _dtype is still None + 'dtype': self._dtype, }) @property diff --git a/sources/tiff/large_image_source_tiff/__init__.py b/sources/tiff/large_image_source_tiff/__init__.py index 7fd666ff1..42ff01aca 100644 --- a/sources/tiff/large_image_source_tiff/__init__.py +++ b/sources/tiff/large_image_source_tiff/__init__.py @@ -159,6 +159,12 @@ def __init__(self, path, **kwargs): # noqa raise TileSourceError( 'Tiff image must have at least two levels.') + sampleformat = highest._tiffInfo.get("sampleformat") + bitspersample = highest._tiffInfo.get("bitspersample") + self._dtype = numpy.dtype('%s%d' % ( + tifftools.constants.SampleFormat[sampleformat or 1].name, + bitspersample + )) # Sort the directories so that the highest resolution is the last one; # if a level is missing, put a None value in its place. self._tiffDirectories = [directories.get(key) for key in @@ -285,6 +291,12 @@ def _initWithTiffTools(self): # noqa self.levels = max(1, int(math.ceil(math.log(max( dir0.imageWidth / dir0.tileWidth, dir0.imageHeight / dir0.tileHeight)) / math.log(2))) + 1) + sampleformat = dir0._tiffInfo.get("sampleformat") + bitspersample = dir0._tiffInfo.get("bitspersample") + self._dtype = numpy.dtype('%s%d' % ( + tifftools.constants.SampleFormat[sampleformat or 1].name, + bitspersample + )) info = _cached_read_tiff(self._largeImagePath) frames = [] associated = [] # for now, a list of directories From 007b61a76ec226829fa6947ee8842b824f71f5c0 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Thu, 11 May 2023 13:06:37 -0400 Subject: [PATCH 06/13] Lint fix --- large_image/tilesource/base.py | 4 +++- sources/tiff/large_image_source_tiff/__init__.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index cd9ab5875..ef12277b1 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -218,7 +218,9 @@ def dtype(self): finally: self._setSkipStyle(False) else: - raise exceptions.TileSourceError("NO!") + raise exceptions.TileSourceError( + 'Cannot determine dtype while style is locked.' + ) return self._dtype diff --git a/sources/tiff/large_image_source_tiff/__init__.py b/sources/tiff/large_image_source_tiff/__init__.py index 42ff01aca..158483610 100644 --- a/sources/tiff/large_image_source_tiff/__init__.py +++ b/sources/tiff/large_image_source_tiff/__init__.py @@ -159,8 +159,8 @@ def __init__(self, path, **kwargs): # noqa raise TileSourceError( 'Tiff image must have at least two levels.') - sampleformat = highest._tiffInfo.get("sampleformat") - bitspersample = highest._tiffInfo.get("bitspersample") + sampleformat = highest._tiffInfo.get('sampleformat') + bitspersample = highest._tiffInfo.get('bitspersample') self._dtype = numpy.dtype('%s%d' % ( tifftools.constants.SampleFormat[sampleformat or 1].name, bitspersample @@ -291,8 +291,8 @@ def _initWithTiffTools(self): # noqa self.levels = max(1, int(math.ceil(math.log(max( dir0.imageWidth / dir0.tileWidth, dir0.imageHeight / dir0.tileHeight)) / math.log(2))) + 1) - sampleformat = dir0._tiffInfo.get("sampleformat") - bitspersample = dir0._tiffInfo.get("bitspersample") + sampleformat = dir0._tiffInfo.get('sampleformat') + bitspersample = dir0._tiffInfo.get('bitspersample') self._dtype = numpy.dtype('%s%d' % ( tifftools.constants.SampleFormat[sampleformat or 1].name, bitspersample From e015deff103cedccef31b6f1ed9d634240e2b2ae Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Tue, 16 May 2023 12:22:21 -0400 Subject: [PATCH 07/13] Fix region specification in dtype getter --- large_image/tilesource/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index ef12277b1..04a3b594c 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -211,8 +211,7 @@ def dtype(self): self._setSkipStyle(True) try: sample, format = self.getRegion( - width=1, height=1, - region=dict(left=0, right=0, regionWidth=1, regionHeight=1), + region=dict(left=0, top=0, width=1, height=1), format=TILE_FORMAT_NUMPY) self._dtype = sample.dtype finally: From 4f41f620606f4dc3408a1102341eec65d3ec3e41 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Tue, 16 May 2023 12:33:37 -0400 Subject: [PATCH 08/13] Set _dtype in base _outputTile rather than subclass getTile --- large_image/tilesource/base.py | 9 +++++++++ .../bioformats/large_image_source_bioformats/__init__.py | 1 - sources/deepzoom/large_image_source_deepzoom/__init__.py | 2 -- sources/dicom/large_image_source_dicom/__init__.py | 1 - sources/gdal/large_image_source_gdal/__init__.py | 1 - sources/mapnik/large_image_source_mapnik/__init__.py | 2 -- sources/multi/large_image_source_multi/__init__.py | 1 - sources/nd2/large_image_source_nd2/__init__.py | 1 - sources/ometiff/large_image_source_ometiff/__init__.py | 2 -- sources/openjpeg/large_image_source_openjpeg/__init__.py | 1 - .../openslide/large_image_source_openslide/__init__.py | 2 -- sources/pil/large_image_source_pil/__init__.py | 1 - sources/pil/large_image_source_pil/girder_source.py | 2 -- sources/test/large_image_source_test/__init__.py | 2 -- sources/tiff/large_image_source_tiff/__init__.py | 4 ---- sources/tiff/large_image_source_tiff/tiff_reader.py | 1 - sources/tifffile/large_image_source_tifffile/__init__.py | 1 - sources/vips/large_image_source_vips/__init__.py | 1 - 18 files changed, 9 insertions(+), 26 deletions(-) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index 04a3b594c..9d767c7b6 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -1553,6 +1553,15 @@ def _outputTile(self, tile, tileEncoding, x, y, z, pilImageAllowed=False, numpyAllowed != 'always' and tileEncoding == self.encoding and not isEdge and (not applyStyle or not hasStyle)): return tile + + if self._dtype is None: + if tileEncoding == TILE_FORMAT_NUMPY: + self._dtype = tile.dtype + elif tileEncoding == TILE_FORMAT_PIL: + self._dtype = numpy.uint8 if ';16' not in tile.mode else numpy.uint16 + else: + self._dtype = _imageToNumpy(tile)[0].dtype + mode = None if (numpyAllowed == 'always' or tileEncoding == TILE_FORMAT_NUMPY or (applyStyle and hasStyle) or isEdge): diff --git a/sources/bioformats/large_image_source_bioformats/__init__.py b/sources/bioformats/large_image_source_bioformats/__init__.py index fdf3ee596..b3a7994d1 100644 --- a/sources/bioformats/large_image_source_bioformats/__init__.py +++ b/sources/bioformats/large_image_source_bioformats/__init__.py @@ -638,7 +638,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): retile[0:min(tile.shape[0], finalHeight), 0:min(tile.shape[1], finalWidth)] = tile[ 0:min(tile.shape[0], finalHeight), 0:min(tile.shape[1], finalWidth)] tile = retile - self._dtype = tile.dtype return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) def getAssociatedImagesList(self): diff --git a/sources/deepzoom/large_image_source_deepzoom/__init__.py b/sources/deepzoom/large_image_source_deepzoom/__init__.py index c0cfbb618..5f9e6268f 100644 --- a/sources/deepzoom/large_image_source_deepzoom/__init__.py +++ b/sources/deepzoom/large_image_source_deepzoom/__init__.py @@ -5,7 +5,6 @@ import os from xml.etree import ElementTree -import numpy import PIL.Image from large_image.cache_util import LruCacheMetaclass, methodcache @@ -114,7 +113,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): overlap if x else 0, overlap if y else 0, self.tileWidth + (overlap if x else 0), self.tileHeight + (overlap if y else 0))) - self._dtype = numpy.array(tile).dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/dicom/large_image_source_dicom/__init__.py b/sources/dicom/large_image_source_dicom/__init__.py index 18c96138f..a2c236efd 100644 --- a/sources/dicom/large_image_source_dicom/__init__.py +++ b/sources/dicom/large_image_source_dicom/__init__.py @@ -246,7 +246,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): tile = _imageToPIL(tile) if bw > self.tileWidth or bh > self.tileHeight: tile = tile.resize((self.tileWidth, self.tileHeight)) - self._dtype = numpy.asarray(tile).dtype return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/gdal/large_image_source_gdal/__init__.py b/sources/gdal/large_image_source_gdal/__init__.py index e3f3bcbc7..8d9b99d35 100644 --- a/sources/gdal/large_image_source_gdal/__init__.py +++ b/sources/gdal/large_image_source_gdal/__init__.py @@ -814,7 +814,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): tile = ds.ReadAsArray() if len(tile.shape) == 3: tile = numpy.rollaxis(tile, 0, 3) - self._dtype = tile.dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/mapnik/large_image_source_mapnik/__init__.py b/sources/mapnik/large_image_source_mapnik/__init__.py index c395a5f88..9a48a765d 100644 --- a/sources/mapnik/large_image_source_mapnik/__init__.py +++ b/sources/mapnik/large_image_source_mapnik/__init__.py @@ -17,7 +17,6 @@ import functools import mapnik -import numpy import PIL.Image from large_image_source_gdal import GDALFileTileSource, InitPrefix from osgeo import gdal, gdalconst @@ -391,7 +390,6 @@ def getTile(self, x, y, z, **kwargs): pilimg = PIL.Image.frombytes('RGBA', (img.width(), img.height()), img.tostring()) if overscan: pilimg = pilimg.crop((1, 1, pilimg.width - overscan, pilimg.height - overscan)) - self._dtype = numpy.asarray(pilimg).dtype return self._outputTile(pilimg, TILE_FORMAT_PIL, x, y, z, applyStyle=False, **kwargs) diff --git a/sources/multi/large_image_source_multi/__init__.py b/sources/multi/large_image_source_multi/__init__.py index 8e58c3043..013a2fcdf 100644 --- a/sources/multi/large_image_source_multi/__init__.py +++ b/sources/multi/large_image_source_multi/__init__.py @@ -1059,7 +1059,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): colors = self._info.get('backgroundColor', [0]) if colors: tile = numpy.full((self.tileHeight, self.tileWidth, len(colors)), colors) - self._dtype = tile.dtype # We should always have a tile return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/nd2/large_image_source_nd2/__init__.py b/sources/nd2/large_image_source_nd2/__init__.py index 99a4eaf25..cff5edc17 100644 --- a/sources/nd2/large_image_source_nd2/__init__.py +++ b/sources/nd2/large_image_source_nd2/__init__.py @@ -279,7 +279,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): with self._tileLock: # Have dask use single-threaded since we are using a lock anyway. tile = tileframe[y0:y1:step, x0:x1:step].compute(scheduler='single-threaded').copy() - self._dtype = tile.dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/ometiff/large_image_source_ometiff/__init__.py b/sources/ometiff/large_image_source_ometiff/__init__.py index 764ded556..28f075822 100644 --- a/sources/ometiff/large_image_source_ometiff/__init__.py +++ b/sources/ometiff/large_image_source_ometiff/__init__.py @@ -351,10 +351,8 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, format = 'JPEG' if isinstance(tile, PIL.Image.Image): format = TILE_FORMAT_PIL - self._dtype = numpy.asarray(tile).dtype if isinstance(tile, numpy.ndarray): format = TILE_FORMAT_NUMPY - self._dtype = tile.dtype return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) except InvalidOperationTiffException as e: diff --git a/sources/openjpeg/large_image_source_openjpeg/__init__.py b/sources/openjpeg/large_image_source_openjpeg/__init__.py index 3a4e0a398..06ae15180 100644 --- a/sources/openjpeg/large_image_source_openjpeg/__init__.py +++ b/sources/openjpeg/large_image_source_openjpeg/__init__.py @@ -264,7 +264,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): self._openjpegHandles.put(openjpegHandle) if scale: tile = tile[::scale, ::scale] - self._dtype = tile.dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/openslide/large_image_source_openslide/__init__.py b/sources/openslide/large_image_source_openslide/__init__.py index 0efff25d8..385494837 100644 --- a/sources/openslide/large_image_source_openslide/__init__.py +++ b/sources/openslide/large_image_source_openslide/__init__.py @@ -17,7 +17,6 @@ import math import os -import numpy import openslide import PIL import tifftools @@ -303,7 +302,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): if svslevel['scale'] != 1: tile = tile.resize((self.tileWidth, self.tileHeight), getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS) - self._dtype = numpy.asarray(tile).dtype return self._outputTile(tile, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/pil/large_image_source_pil/__init__.py b/sources/pil/large_image_source_pil/__init__.py index 7184ea5ee..6dc3ac6f1 100644 --- a/sources/pil/large_image_source_pil/__init__.py +++ b/sources/pil/large_image_source_pil/__init__.py @@ -269,7 +269,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, numpy.asarray(img), self._factor))) else: img = self._pilImage - self._dtype = numpy.asarray(img).dtype return self._outputTile(img, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/pil/large_image_source_pil/girder_source.py b/sources/pil/large_image_source_pil/girder_source.py index fea2c9d4a..9706e4ff7 100644 --- a/sources/pil/large_image_source_pil/girder_source.py +++ b/sources/pil/large_image_source_pil/girder_source.py @@ -15,7 +15,6 @@ ############################################################################# import cherrypy -import numpy from girder_large_image.constants import PluginSettings from girder_large_image.girder_tilesource import GirderTileSource @@ -65,6 +64,5 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, url = '%s/api/v1/file/%s/download' % ( cherrypy.request.base, self.item['largeImage']['fileId']) raise cherrypy.HTTPRedirect(url) - self._dtype = numpy.asarray(self._pilImage).dtype return self._outputTile(self._pilImage, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/test/large_image_source_test/__init__.py b/sources/test/large_image_source_test/__init__.py index 4619f325b..6b9e48c92 100644 --- a/sources/test/large_image_source_test/__init__.py +++ b/sources/test/large_image_source_test/__init__.py @@ -302,7 +302,6 @@ def getTile(self, x, y, z, *args, **kwargs): if self.monochrome: image = image.convert('L') format = TILE_FORMAT_PIL - self._dtype = numpy.asarray(image).dtype else: image = numpy.zeros( (self.tileHeight, self.tileWidth, len(self._bands)), dtype=self._dtype) @@ -321,7 +320,6 @@ def getTile(self, x, y, z, *args, **kwargs): bandimg = bandimg.astype(self._dtype) image[:, :, bandnum] = bandimg[:, :, bandnum % bandimg.shape[2]] format = TILE_FORMAT_NUMPY - self._dtype = image.dtype return self._outputTile(image, format, x, y, z, **kwargs) @staticmethod diff --git a/sources/tiff/large_image_source_tiff/__init__.py b/sources/tiff/large_image_source_tiff/__init__.py index 158483610..024b8ae92 100644 --- a/sources/tiff/large_image_source_tiff/__init__.py +++ b/sources/tiff/large_image_source_tiff/__init__.py @@ -605,13 +605,10 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, else: tile = dir.getTile(x, y) format = 'JPEG' - self._dtype = numpy.uint8 if isinstance(tile, PIL.Image.Image): format = TILE_FORMAT_PIL - self._dtype = numpy.asarray(tile).dtype if isinstance(tile, numpy.ndarray): format = TILE_FORMAT_NUMPY - self._dtype = tile.dtype return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, applyStyle=allowStyle, **kwargs) except InvalidOperationTiffException as e: @@ -661,7 +658,6 @@ def getTileIOTiffError(self, x, y, z, pilImageAllowed=False, image = image.resize((self.tileWidth, self.tileHeight)) else: image = PIL.Image.new('RGBA', (self.tileWidth, self.tileHeight)) - self._dtype = image.dtype return self._outputTile(image, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, numpyAllowed, applyStyle=False, **kwargs) raise TileSourceError('Internal I/O failure: %s' % exception.args[0]) diff --git a/sources/tiff/large_image_source_tiff/tiff_reader.py b/sources/tiff/large_image_source_tiff/tiff_reader.py index 73d93c1b5..2bec9b926 100644 --- a/sources/tiff/large_image_source_tiff/tiff_reader.py +++ b/sources/tiff/large_image_source_tiff/tiff_reader.py @@ -825,7 +825,6 @@ def getTile(self, x, y): # multiple times, which sometimes throws an exception in PIL's JPEG # 2000 module. image = image.convert('RGB') - self._dtype = numpy.asarray(image).dtype return image def parse_image_description(self, meta=None): # noqa diff --git a/sources/tifffile/large_image_source_tifffile/__init__.py b/sources/tifffile/large_image_source_tifffile/__init__.py index d8340f016..c4cc769c1 100644 --- a/sources/tifffile/large_image_source_tifffile/__init__.py +++ b/sources/tifffile/large_image_source_tifffile/__init__.py @@ -459,7 +459,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): if baxis not in {'YXS', 'YX'}: tile = numpy.moveaxis( tile, [baxis.index(a) for a in 'YXS' if a in baxis], range(len(baxis))) - self._dtype = tile.dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) diff --git a/sources/vips/large_image_source_vips/__init__.py b/sources/vips/large_image_source_vips/__init__.py index ff4e4103c..0d88eb2b0 100644 --- a/sources/vips/large_image_source_vips/__init__.py +++ b/sources/vips/large_image_source_vips/__init__.py @@ -212,7 +212,6 @@ def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): buffer=tileimg.write_to_memory(), dtype=GValueToDtype[tileimg.format], shape=[tileimg.height, tileimg.width, tileimg.bands]) - self._dtype = tile.dtype return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs) From fbcd68e21b14a134a98b5e9fe7062e124532c1d9 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Thu, 25 May 2023 13:27:19 -0400 Subject: [PATCH 09/13] Use public property in metadata, will incur negligible calculation cost --- large_image/tilesource/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index 5c8777ed8..d8c0b0c58 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -1669,9 +1669,7 @@ def getMetadata(self): 'magnification': mag['magnification'], 'mm_x': mag['mm_x'], 'mm_y': mag['mm_y'], - # Use private attribute; public property - # incurs calcuation cost if _dtype is still None - 'dtype': self._dtype, + 'dtype': self.dtype, }) @property From cea2edcf63352a59242a3934d086f35799a4e452 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Thu, 25 May 2023 13:58:15 -0400 Subject: [PATCH 10/13] Cast dtype to string --- large_image/tilesource/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index d8c0b0c58..e1d4d1393 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -1669,7 +1669,7 @@ def getMetadata(self): 'magnification': mag['magnification'], 'mm_x': mag['mm_x'], 'mm_y': mag['mm_y'], - 'dtype': self.dtype, + 'dtype': str(self.dtype), }) @property From dce7602966dc579d0b76fd9bf8b1d244376f5ebf Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Thu, 25 May 2023 15:14:05 -0400 Subject: [PATCH 11/13] Return None instead of raising exception when style lock is set --- large_image/tilesource/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index e1d4d1393..94b52bf6c 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -217,9 +217,7 @@ def dtype(self): finally: self._setSkipStyle(False) else: - raise exceptions.TileSourceError( - 'Cannot determine dtype while style is locked.' - ) + return None return self._dtype From cd06bfa82ffb4adc0681fbfb20a6f012d2372b34 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 25 May 2023 16:31:53 -0400 Subject: [PATCH 12/13] Handle tile sources with scant information. The dummy tile source doesn't have a _classkey or report level information. Work anyway. --- large_image/tilesource/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index 94b52bf6c..d98f9f27c 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -1336,6 +1336,8 @@ def _applyICCProfile(self, sc, frame): return sc.iccimage def _setSkipStyle(self, setSkip=False): + if not hasattr(self, '_classkey'): + self._classkey = self.getState() if setSkip: self._unlocked_classkey = self._classkey if hasattr(self, 'cache_lock'): @@ -1667,7 +1669,7 @@ def getMetadata(self): 'magnification': mag['magnification'], 'mm_x': mag['mm_x'], 'mm_y': mag['mm_y'], - 'dtype': str(self.dtype), + 'dtype': self.dtype, }) @property @@ -2445,7 +2447,7 @@ def getLevelForMagnification(self, magnification=None, exact=False, # Perform some slight rounding to handle numerical precision issues ratios = [round(ratio, 4) for ratio in ratios] if not len(ratios): - return mag['level'] + return mag.get('level', 0) if exact: if any(int(ratio) != ratio or ratio != ratios[0] for ratio in ratios): From 36d2438b534b3a2de1cc489ed8be1bbd1b5d0710 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 25 May 2023 21:38:18 -0400 Subject: [PATCH 13/13] Better handle zero sized tiles --- large_image/tilesource/utilities.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/large_image/tilesource/utilities.py b/large_image/tilesource/utilities.py index 9c7239bfd..9153af45e 100644 --- a/large_image/tilesource/utilities.py +++ b/large_image/tilesource/utilities.py @@ -226,7 +226,10 @@ def _imageToNumpy(image): if image.mode not in ('L', 'LA', 'RGB', 'RGBA'): image = image.convert('RGBA') mode = image.mode - image = numpy.asarray(image) + if not image.width or not image.height: + image = numpy.zeros((image.height, image.width, len(mode))) + else: + image = numpy.asarray(image) else: if len(image.shape) == 3: mode = ['L', 'LA', 'RGB', 'RGBA'][(image.shape[2] - 1) if image.shape[2] <= 4 else 3]