diff --git a/docs/tilesource_options.rst b/docs/tilesource_options.rst index f04a77ff8..dbcc98640 100644 --- a/docs/tilesource_options.rst +++ b/docs/tilesource_options.rst @@ -41,9 +41,9 @@ A band definition is an object which can contain the following keys: - ``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. 'min:' and 'max:' pick a value that excludes a threshold amount from the histogram; for instance, 'min:0.02' would exclude at most the dimmest 2% of values by using an appropriate value for the minimum based on a computed histogram with some default binning options. 'auto:' works like auto, though it applies the threshold if the reported minimum would otherwise be used. +- ``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. 'min:' and 'max:' pick a value that excludes a threshold amount from the histogram; for instance, 'min:0.02' would exclude at most the dimmest 2% of values by using an appropriate value for the minimum based on a computed histogram with some default binning options. 'auto:' works like auto, though it applies the threshold if the reported minimum would otherwise be used. 'full' is the same as specifying 0. -- ``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. 'min:' and 'max:' pick a value that excludes a threshold amount from the histogram; for instance, 'max:0.02' would exclude at most the brightest 2% of values by using an appropriate value for the maximum based on a computed histogram with some default binning options. 'auto:' works like auto, though it applies the threshold if the reported maximum would otherwise be used. +- ``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. 'min:' and 'max:' pick a value that excludes a threshold amount from the histogram; for instance, 'max:0.02' would exclude at most the brightest 2% of values by using an appropriate value for the maximum based on a computed histogram with some default binning options. 'auto:' works like auto, though it applies the threshold if the reported maximum would otherwise be used. 'full' uses a value based on the data type of the band. This will be 1 for a float data type and 65535 for a uint16 datatype. - ``palette``: This is a list or two or more colors. The values between min and max are interpolated using a piecewise linear algorithm or a nearest value algorithm (depending on the ``scheme``) to map to the specified palette values. It can be specified in a variety of ways: - a list of two or more color values, where the color values are css-style strings (e.g., of the form #RRGGBB, #RRGGBBAA, #RGB, #RGBA, or a css ``rgb``, ``rgba``, ``hsl``, or ``hsv`` string, or a css color name), or, if matplotlib is available, a matplotlib color name, or a list or tuple of RGB(A) values on a scale of [0-1]. @@ -58,6 +58,10 @@ A band definition is an object which can contain the following keys: - ``clamp``: either True to clamp (also called clip or crop) values outside of the [min, max] to the ends of the palette or False to make outside values transparent. +- ``dtype``: if specified, cast the intermediate results to this data type. Only the first such value is used, and this can be specified as a base key if ``bands`` is specified. Normally, if a style is applied, the intermediate data is a numpy float array with values from [0,255]. If this is ``uint16``, the results are multiplied by 65535 / 255 and cast to that dtype. If ``float``, the results are divided by 255. + +- ``axis``: if specified, keep on the specified axis (channel) of the intermediate numpy array. This is typically between 0 and 3 for the red, green, blue, and alpha channels. Only the first such value is used, and this can be specified as a base key if ``bands`` is specified. + Note that some tile sources add additional options to the ``style`` parameter. Examples diff --git a/large_image/tilesource/base.py b/large_image/tilesource/base.py index 0eab16cd9..6e53be092 100644 --- a/large_image/tilesource/base.py +++ b/large_image/tilesource/base.py @@ -81,12 +81,13 @@ def __init__(self, encoding='JPEG', jpegQuality=95, jpegSubsampling=0, 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. + minimum or maximum. 'full' to always use 0. :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. + reported minimum or maximum. 'full' to use the maximum + value of the base data type (either 1, 255, or 65535). :palette: a list of two or more color strings, where color strings are of the form #RRGGBB, #RRGGBBAA, #RGB, #RGBA, or any string parseable by the PIL modules, or, if it is @@ -101,11 +102,21 @@ def __init__(self, encoding='JPEG', jpegQuality=95, jpegSubsampling=0, :clamp: either True to clamp (also called clip or crop) values outside of the [min, max] to the ends of the palette or False to make outside values transparent. + :dtype: convert the results to the specified numpy dtype. + Normally, if a style is applied, the results are + intermediately a float numpy array with a value range of + [0,255]. If this is 'uint16', it will be cast to that and + multiplied by 65535/255. If 'float', it will be divided by + 255. + :axis: keep only the specified axis from the numpy intermediate + results. This can be used to extract a single channel + after compositing. Alternately, the style object can contain a single key of 'bands', which has a value which is a list of style dictionaries as above, excepting that each must have a band that is not -1. Bands are - composited in the order listed. + composited in the order listed. This base object may also contain + the 'dtype' and 'axis' values. """ self.logger = config.getConfig('logger') self.cache, self.cache_lock = getTileCache() @@ -1023,7 +1034,7 @@ def _validateMinMaxValue(self, value, frame, dtype): :returns: the validated value and a threshold from [0-1]. """ threshold = 0 - if value not in {'min', 'max', 'auto'}: + if value not in {'min', 'max', 'auto', 'full'}: try: if ':' in str(value) and value.split(':', 1)[0] in {'min', 'max', 'auto'}: threshold = float(value.split(':', 1)[1]) @@ -1039,7 +1050,7 @@ def _validateMinMaxValue(self, value, frame, dtype): self._scanForMinMax(dtype, frame, onlyMinMax=not threshold) return value, threshold - def _getMinMax(self, minmax, value, dtype, bandidx=None, frame=None): + def _getMinMax(self, minmax, value, dtype, bandidx=None, frame=None): # noqa """ Get an appropriate minimum or maximum for a band. @@ -1057,6 +1068,15 @@ def _getMinMax(self, minmax, value, dtype, bandidx=None, frame=None): """ frame = frame or 0 value, threshold = self._validateMinMaxValue(value, frame, dtype) + if value == 'full': + value = 0 + if minmax != 'min': + if dtype == numpy.uint16: + value = 65535 + elif dtype.kind == 'f': + value = 1 + else: + value = 255 if value == 'auto': if (self._bandRanges.get(frame) and numpy.all(self._bandRanges[frame]['min'] >= 0) and @@ -1102,11 +1122,15 @@ def _applyStyle(self, image, style, x, y, z, frame=None): # noqa :param frame: the frame to use for auto ranging. :returns: a styled image. """ + dtype = style.get('dtype') + axis = style.get('axis') style = style['bands'] if 'bands' in style else [style] output = numpy.zeros((image.shape[0], image.shape[1], 4), float) mainImage = image mainFrame = frame for entry in style: + dtype = dtype if dtype is not None else entry.get('dtype') + axis = axis if axis is not None else entry.get('axis') bandidx = 0 if image.shape[2] <= 2 else 1 band = None if ((entry.get('frame') is None and not entry.get('framedelta')) or @@ -1194,6 +1218,12 @@ def _applyStyle(self, image, style, x, y, z, frame=None): # noqa else: output[:, :, channel] = numpy.maximum( output[:, :, channel], numpy.where(keep, clrs, 0)) + if dtype == 'uint16': + output = (output * 65535 / 255).astype(numpy.uint16) + elif dtype == 'float': + output /= 255 + if axis is not None and 0 <= int(axis) < output.shape[2]: + output = output[:, :, axis:axis + 1] return output def _outputTileNumpyStyle(self, tile, applyStyle, x, y, z, frame=None): diff --git a/test/test_source_tiff.py b/test/test_source_tiff.py index 83044fc3f..477d3d04c 100644 --- a/test/test_source_tiff.py +++ b/test/test_source_tiff.py @@ -654,6 +654,22 @@ def testStyleMinMaxThreshold(): assert numpy.any(image != imageB) assert image[0][0][0] == 252 assert imageB[0][0][0] == 254 + sourceC = large_image_source_tiff.open( + imagePath, style=json.dumps({'min': 'full', 'max': 'full'})) + imageC, _ = sourceC.getRegion( + output={'maxWidth': 256, 'maxHeight': 256}, format=constants.TILE_FORMAT_NUMPY) + assert numpy.any(image != imageC) + assert imageC[0][0][0] == 253 + + +def testStyleDtypeAxis(): + imagePath = datastore.fetch('sample_image.ptif') + source = large_image_source_tiff.open( + imagePath, style=json.dumps({'dtype': 'uint16', 'axis': 1})) + image, _ = source.getRegion( + output={'maxWidth': 456, 'maxHeight': 96}, format=constants.TILE_FORMAT_NUMPY) + assert image.shape[2] == 1 + assert image[0][0][0] == 65021 def testStyleNoData():