""" A collection of image utilities using the Python Imaging Library (PIL). """ # Copyright (c) 2001-2002 Enthought, Inc. 2003-2019, SciPy Developers. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from __future__ import division, print_function, absolute_import import numpy from PIL import Image from numpy import (amin, amax, ravel, asarray, arange, ones, newaxis, transpose, iscomplexobj, uint8, issubdtype, array) if not hasattr(Image, 'frombytes'): Image.frombytes = Image.fromstring __all__ = ['fromimage', 'toimage', 'imsave', 'imread', 'bytescale', 'imrotate', 'imresize'] def bytescale(data, cmin=None, cmax=None, high=255, low=0): """ Byte scales an array (image). Byte scaling means converting the input image to uint8 dtype and scaling the range to ``(low, high)`` (default 0-255). If the input image already has dtype uint8, no scaling is done. This function is only available if Python Imaging Library (PIL) is installed. Parameters ---------- data : ndarray PIL image data array. cmin : scalar, optional Bias scaling of small values. Default is ``data.min()``. cmax : scalar, optional Bias scaling of large values. Default is ``data.max()``. high : scalar, optional Scale max value to `high`. Default is 255. low : scalar, optional Scale min value to `low`. Default is 0. Returns ------- img_array : uint8 ndarray The byte-scaled array. Examples -------- >>> img = numpy.array([[ 91.06794177, 3.39058326, 84.4221549 ], ... [ 73.88003259, 80.91433048, 4.88878881], ... [ 51.53875334, 34.45808177, 27.5873488 ]]) >>> bytescale(img) array([[255, 0, 236], [205, 225, 4], [140, 90, 70]], dtype=uint8) >>> bytescale(img, high=200, low=100) array([[200, 100, 192], [180, 188, 102], [155, 135, 128]], dtype=uint8) >>> bytescale(img, cmin=0, cmax=255) array([[91, 3, 84], [74, 81, 5], [52, 34, 28]], dtype=uint8) """ if data.dtype == uint8: return data if high > 255: raise ValueError("`high` should be less than or equal to 255.") if low < 0: raise ValueError("`low` should be greater than or equal to 0.") if high < low: raise ValueError("`high` should be greater than or equal to `low`.") if cmin is None: cmin = data.min() if cmax is None: cmax = data.max() cscale = cmax - cmin if cscale < 0: raise ValueError("`cmax` should be larger than `cmin`.") elif cscale == 0: cscale = 1 scale = float(high - low) / cscale bytedata = (data - cmin) * scale + low return (bytedata.clip(low, high) + 0.5).astype(uint8) def imread(name, flatten=False, mode=None): """ Read an image from a file as an array. This function is only available if Python Imaging Library (PIL) is installed. Parameters ---------- name : str or file object The file name or file object to be read. flatten : bool, optional If True, flattens the color layers into a single gray-scale layer. mode : str, optional Mode to convert image to, e.g. ``'RGB'``. See the Notes for more details. Returns ------- imread : ndarray The array obtained by reading the image. Notes ----- `imread` uses the Python Imaging Library (PIL) to read an image. The following notes are from the PIL documentation. `mode` can be one of the following strings: * 'L' (8-bit pixels, black and white) * 'P' (8-bit pixels, mapped to any other mode using a color palette) * 'RGB' (3x8-bit pixels, true color) * 'RGBA' (4x8-bit pixels, true color with transparency mask) * 'CMYK' (4x8-bit pixels, color separation) * 'YCbCr' (3x8-bit pixels, color video format) * 'I' (32-bit signed integer pixels) * 'F' (32-bit floating point pixels) PIL also provides limited support for a few special modes, including 'LA' ('L' with alpha), 'RGBX' (true color with padding) and 'RGBa' (true color with premultiplied alpha). When translating a color image to black and white (mode 'L', 'I' or 'F'), the library uses the ITU-R 601-2 luma transform:: L = R * 299/1000 + G * 587/1000 + B * 114/1000 When `flatten` is True, the image is converted using mode 'F'. When `mode` is not None and `flatten` is True, the image is first converted according to `mode`, and the result is then flattened using mode 'F'. """ im = Image.open(name) return fromimage(im, flatten=flatten, mode=mode) def imsave(name, arr, format=None): """ Save an array as an image. This function is only available if Python Imaging Library (PIL) is installed. .. warning:: This function uses `bytescale` under the hood to rescale images to use the full (0, 255) range if ``mode`` is one of ``None, 'L', 'P', 'l'``. It will also cast data for 2-D images to ``uint32`` for ``mode=None`` (which is the default). Parameters ---------- name : str or file object Output file name or file object. arr : ndarray, MxN or MxNx3 or MxNx4 Array containing image values. If the shape is ``MxN``, the array represents a grey-level image. Shape ``MxNx3`` stores the red, green and blue bands along the last dimension. An alpha layer may be included, specified as the last colour band of an ``MxNx4`` array. format : str Image format. If omitted, the format to use is determined from the file name extension. If a file object was used instead of a file name, this parameter should always be used. Examples -------- Construct an array of gradient intensity values and save to file: >>> x = numpy.zeros((255, 255), dtype=numpy.uint8) >>> x[:] = numpy.arange(255) >>> imsave('gradient.png', x) Construct an array with three colour bands (R, G, B) and store to file: >>> rgb = numpy.zeros((255, 255, 3), dtype=numpy.uint8) >>> rgb[..., 0] = numpy.arange(255) >>> rgb[..., 1] = 55 >>> rgb[..., 2] = 1 - numpy.arange(255) >>> imsave('rgb_gradient.png', rgb) """ im = toimage(arr, channel_axis=2) if format is None: im.save(name) else: im.save(name, format) return def fromimage(im, flatten=False, mode=None): """ Return a copy of a PIL image as a numpy array. This function is only available if Python Imaging Library (PIL) is installed. Parameters ---------- im : PIL image Input image. flatten : bool If true, convert the output to grey-scale. mode : str, optional Mode to convert image to, e.g. ``'RGB'``. See the Notes of the `imread` docstring for more details. Returns ------- fromimage : ndarray The different colour bands/channels are stored in the third dimension, such that a grey-image is MxN, an RGB-image MxNx3 and an RGBA-image MxNx4. """ if not Image.isImageType(im): raise TypeError("Input is not a PIL image.") if mode is not None: if mode != im.mode: im = im.convert(mode) elif im.mode == 'P': # Mode 'P' means there is an indexed "palette". If we leave the mode # as 'P', then when we do `a = array(im)` below, `a` will be a 2-D # containing the indices into the palette, and not a 3-D array # containing the RGB or RGBA values. if 'transparency' in im.info: im = im.convert('RGBA') else: im = im.convert('RGB') if flatten: im = im.convert('F') elif im.mode == '1': # Workaround for crash in PIL. When im is 1-bit, the call array(im) # can cause a seg. fault, or generate garbage. See # https://github.com/scipy/scipy/issues/2138 and # https://github.com/python-pillow/Pillow/issues/350. # # This converts im from a 1-bit image to an 8-bit image. im = im.convert('L') a = array(im) return a _errstr = "Mode is unknown or incompatible with input array shape." def toimage(arr, high=255, low=0, cmin=None, cmax=None, pal=None, mode=None, channel_axis=None): """Takes a numpy array and returns a PIL image. This function is only available if Python Imaging Library (PIL) is installed. The mode of the PIL image depends on the array shape and the `pal` and `mode` keywords. For 2-D arrays, if `pal` is a valid (N,3) byte-array giving the RGB values (from 0 to 255) then ``mode='P'``, otherwise ``mode='L'``, unless mode is given as 'F' or 'I' in which case a float and/or integer array is made. .. warning:: This function uses `bytescale` under the hood to rescale images to use the full (0, 255) range if ``mode`` is one of ``None, 'L', 'P', 'l'``. It will also cast data for 2-D images to ``uint32`` for ``mode=None`` (which is the default). Notes ----- For 3-D arrays, the `channel_axis` argument tells which dimension of the array holds the channel data. For 3-D arrays if one of the dimensions is 3, the mode is 'RGB' by default or 'YCbCr' if selected. The numpy array must be either 2 dimensional or 3 dimensional. """ data = asarray(arr) if iscomplexobj(data): raise ValueError("Cannot convert a complex-valued array.") shape = list(data.shape) valid = len(shape) == 2 or ((len(shape) == 3) and ((3 in shape) or (4 in shape))) if not valid: raise ValueError("'arr' does not have a suitable array shape for " "any mode.") if len(shape) == 2: shape = (shape[1], shape[0]) # columns show up first if mode == 'F': data32 = data.astype(numpy.float32) image = Image.frombytes(mode, shape, data32.tostring()) return image if mode in [None, 'L', 'P']: bytedata = bytescale(data, high=high, low=low, cmin=cmin, cmax=cmax) image = Image.frombytes('L', shape, bytedata.tostring()) if pal is not None: image.putpalette(asarray(pal, dtype=uint8).tostring()) # Becomes a mode='P' automagically. elif mode == 'P': # default gray-scale pal = (arange(0, 256, 1, dtype=uint8)[:, newaxis] * ones((3,), dtype=uint8)[newaxis, :]) image.putpalette(asarray(pal, dtype=uint8).tostring()) return image if mode == '1': # high input gives threshold for 1 bytedata = (data > high) image = Image.frombytes('1', shape, bytedata.tostring()) return image if cmin is None: cmin = amin(ravel(data)) if cmax is None: cmax = amax(ravel(data)) data = (data*1.0 - cmin)*(high - low)/(cmax - cmin) + low if mode == 'I': data32 = data.astype(numpy.uint32) image = Image.frombytes(mode, shape, data32.tostring()) else: raise ValueError(_errstr) return image # if here then 3-d array with a 3 or a 4 in the shape length. # Check for 3 in datacube shape --- 'RGB' or 'YCbCr' if channel_axis is None: if (3 in shape): ca = numpy.flatnonzero(asarray(shape) == 3)[0] else: ca = numpy.flatnonzero(asarray(shape) == 4) if len(ca): ca = ca[0] else: raise ValueError("Could not find channel dimension.") else: ca = channel_axis numch = shape[ca] if numch not in [3, 4]: raise ValueError("Channel axis dimension is not valid.") bytedata = bytescale(data, high=high, low=low, cmin=cmin, cmax=cmax) if ca == 2: strdata = bytedata.tobytes() # .tostring() shape = (shape[1], shape[0]) elif ca == 1: strdata = transpose(bytedata, (0, 2, 1)).tobytes() #.tostring() shape = (shape[2], shape[0]) elif ca == 0: strdata = transpose(bytedata, (1, 2, 0)).tobytes() #.tostring() shape = (shape[2], shape[1]) else: raise ValueError("Unexpected channel axis.") if mode is None: if numch == 3: mode = 'RGB' else: mode = 'RGBA' if mode not in ['RGB', 'RGBA', 'YCbCr', 'CMYK']: raise ValueError(_errstr) if mode in ['RGB', 'YCbCr']: if numch != 3: raise ValueError("Invalid array shape for mode.") if mode in ['RGBA', 'CMYK']: if numch != 4: raise ValueError("Invalid array shape for mode.") # Here we know data and mode is correct image = Image.frombytes(mode, shape, strdata) return image def imrotate(arr, angle, interp='bilinear'): """ Rotate an image counter-clockwise by angle degrees. This function is only available if Python Imaging Library (PIL) is installed. .. warning:: This function uses `bytescale` under the hood to rescale images to use the full (0, 255) range if ``mode`` is one of ``None, 'L', 'P', 'l'``. It will also cast data for 2-D images to ``uint32`` for ``mode=None`` (which is the default). Parameters ---------- arr : ndarray Input array of image to be rotated. angle : float The angle of rotation. interp : str, optional Interpolation - 'nearest' : for nearest neighbor - 'bilinear' : for bilinear - 'lanczos' : for lanczos - 'cubic' : for bicubic - 'bicubic' : for bicubic Returns ------- imrotate : ndarray The rotated array of image. """ arr = asarray(arr) func = {'nearest': 0, 'lanczos': 1, 'bilinear': 2, 'bicubic': 3, 'cubic': 3} im = toimage(arr) im = im.rotate(angle, resample=func[interp]) return fromimage(im) def imresize(arr, size, interp='bilinear', mode=None): """ Resize an image. This function is only available if Python Imaging Library (PIL) is installed. .. warning:: This function uses `bytescale` under the hood to rescale images to use the full (0, 255) range if ``mode`` is one of ``None, 'L', 'P', 'l'``. It will also cast data for 2-D images to ``uint32`` for ``mode=None`` (which is the default). Parameters ---------- arr : ndarray The array of image to be resized. size : int, float or tuple * int - Percentage of current size. * float - Fraction of current size. * tuple - Size of the output image (height, width). interp : str, optional Interpolation to use for re-sizing ('nearest', 'lanczos', 'bilinear', 'bicubic' or 'cubic'). mode : str, optional The PIL image mode ('P', 'L', etc.) to convert `arr` before resizing. If ``mode=None`` (the default), 2-D images will be treated like ``mode='L'``, i.e. casting to long integer. For 3-D and 4-D arrays, `mode` will be set to ``'RGB'`` and ``'RGBA'`` respectively. Returns ------- imresize : ndarray The resized array of image. See Also -------- toimage : Implicitly used to convert `arr` according to `mode`. scipy.ndimage.zoom : More generic implementation that does not use PIL. """ im = toimage(arr, mode=mode) ts = type(size) if issubdtype(ts, numpy.signedinteger): percent = size / 100.0 size = tuple((array(im.size)*percent).astype(int)) elif issubdtype(type(size), numpy.floating): size = tuple((array(im.size)*size).astype(int)) else: size = (size[1], size[0]) func = {'nearest': 0, 'lanczos': 1, 'bilinear': 2, 'bicubic': 3, 'cubic': 3} imnew = im.resize(size, resample=func[interp]) return fromimage(imnew)