import numpy as np from warnings import warn __all__ = ['img_as_float32', 'img_as_float64', 'img_as_float', 'img_as_int', 'img_as_uint', 'img_as_ubyte', 'img_as_bool', 'dtype_limits'] # For integers Numpy uses `_integer_types` basis internally, and builds a leaky # `np.XintYY` abstraction on top of it. This leads to situations when, for # example, there are two np.Xint64 dtypes with the same attributes but # different object references. In order to avoid any potential issues, # we use the basis dtypes here. For more information, see: # - https://github.com/scikit-image/scikit-image/issues/3043 # For convenience, for these dtypes we indicate also the possible bit depths # (some of them are platform specific). For the details, see: # http://www.unix.org/whitepapers/64bit.html _integer_types = (np.byte, np.ubyte, # 8 bits np.short, np.ushort, # 16 bits np.intc, np.uintc, # 16 or 32 or 64 bits int, np.int_, np.uint, # 32 or 64 bits np.longlong, np.ulonglong) # 64 bits _integer_ranges = {t: (np.iinfo(t).min, np.iinfo(t).max) for t in _integer_types} dtype_range = {bool: (False, True), np.bool_: (False, True), np.bool8: (False, True), float: (-1, 1), np.float_: (-1, 1), np.float16: (-1, 1), np.float32: (-1, 1), np.float64: (-1, 1)} dtype_range.update(_integer_ranges) _supported_types = list(dtype_range.keys()) def dtype_limits(image, clip_negative=False): """Return intensity limits, i.e. (min, max) tuple, of the image's dtype. Parameters ---------- image : ndarray Input image. clip_negative : bool, optional If True, clip the negative range (i.e. return 0 for min intensity) even if the image dtype allows negative values. Returns ------- imin, imax : tuple Lower and upper intensity limits. """ imin, imax = dtype_range[image.dtype.type] if clip_negative: imin = 0 return imin, imax def _dtype_itemsize(itemsize, *dtypes): """Return first of `dtypes` with itemsize greater than `itemsize` Parameters ---------- itemsize: int The data type object element size. Other Parameters ---------------- *dtypes: Any Object accepted by `np.dtype` to be converted to a data type object Returns ------- dtype: data type object First of `dtypes` with itemsize greater than `itemsize`. """ return next(dt for dt in dtypes if np.dtype(dt).itemsize >= itemsize) def _dtype_bits(kind, bits, itemsize=1): """Return dtype of `kind` that can store a `bits` wide unsigned int Parameters: kind: str Data type kind. bits: int Desired number of bits. itemsize: int The data type object element size. Returns ------- dtype: data type object Data type of `kind` that can store a `bits` wide unsigned int """ s = next(i for i in (itemsize, ) + (2, 4, 8) if bits < (i * 8) or (bits == (i * 8) and kind == 'u')) return np.dtype(kind + str(s)) def _scale(a, n, m, copy=True): """Scale an array of unsigned/positive integers from `n` to `m` bits. Numbers can be represented exactly only if `m` is a multiple of `n`. Parameters ---------- a : ndarray Input image array. n : int Number of bits currently used to encode the values in `a`. m : int Desired number of bits to encode the values in `out`. copy : bool, optional If True, allocates and returns new array. Otherwise, modifies `a` in place. Returns ------- out : array Output image array. Has the same kind as `a`. """ kind = a.dtype.kind if n > m and a.max() < 2 ** m: mnew = int(np.ceil(m / 2) * 2) if mnew > m: dtype = "int{}".format(mnew) else: dtype = "uint{}".format(mnew) n = int(np.ceil(n / 2) * 2) warn("Downcasting {} to {} without scaling because max " "value {} fits in {}".format(a.dtype, dtype, a.max(), dtype), stacklevel=3) return a.astype(_dtype_bits(kind, m)) elif n == m: return a.copy() if copy else a elif n > m: # downscale with precision loss if copy: b = np.empty(a.shape, _dtype_bits(kind, m)) np.floor_divide(a, 2**(n - m), out=b, dtype=a.dtype, casting='unsafe') return b else: a //= 2**(n - m) return a elif m % n == 0: # exact upscale to a multiple of `n` bits if copy: b = np.empty(a.shape, _dtype_bits(kind, m)) np.multiply(a, (2**m - 1) // (2**n - 1), out=b, dtype=b.dtype) return b else: a = a.astype(_dtype_bits(kind, m, a.dtype.itemsize), copy=False) a *= (2**m - 1) // (2**n - 1) return a else: # upscale to a multiple of `n` bits, # then downscale with precision loss o = (m // n + 1) * n if copy: b = np.empty(a.shape, _dtype_bits(kind, o)) np.multiply(a, (2**o - 1) // (2**n - 1), out=b, dtype=b.dtype) b //= 2**(o - m) return b else: a = a.astype(_dtype_bits(kind, o, a.dtype.itemsize), copy=False) a *= (2**o - 1) // (2**n - 1) a //= 2**(o - m) return a def _convert(image, dtype, force_copy=False, uniform=False): """ Convert an image to the requested data-type. Warnings are issued in case of precision loss, or when negative values are clipped during conversion to unsigned integer types (sign loss). Floating point values are expected to be normalized and will be clipped to the range [0.0, 1.0] or [-1.0, 1.0] when converting to unsigned or signed integers respectively. Numbers are not shifted to the negative side when converting from unsigned to signed integer types. Negative values will be clipped when converting to unsigned integers. Parameters ---------- image : ndarray Input image. dtype : dtype Target data-type. force_copy : bool, optional Force a copy of the data, irrespective of its current dtype. uniform : bool, optional Uniformly quantize the floating point range to the integer range. By default (uniform=False) floating point values are scaled and rounded to the nearest integers, which minimizes back and forth conversion errors. .. versionchanged :: 0.15 ``_convert`` no longer warns about possible precision or sign information loss. See discussions on these warnings at: https://github.com/scikit-image/scikit-image/issues/2602 https://github.com/scikit-image/scikit-image/issues/543#issuecomment-208202228 https://github.com/scikit-image/scikit-image/pull/3575 References ---------- .. [1] DirectX data conversion rules. https://msdn.microsoft.com/en-us/library/windows/desktop/dd607323%28v=vs.85%29.aspx .. [2] Data Conversions. In "OpenGL ES 2.0 Specification v2.0.25", pp 7-8. Khronos Group, 2010. .. [3] Proper treatment of pixels as integers. A.W. Paeth. In "Graphics Gems I", pp 249-256. Morgan Kaufmann, 1990. .. [4] Dirty Pixels. J. Blinn. In "Jim Blinn's corner: Dirty Pixels", pp 47-57. Morgan Kaufmann, 1998. """ image = np.asarray(image) dtypeobj_in = image.dtype if dtype is np.floating: dtypeobj_out = np.dtype('float64') else: dtypeobj_out = np.dtype(dtype) dtype_in = dtypeobj_in.type dtype_out = dtypeobj_out.type kind_in = dtypeobj_in.kind kind_out = dtypeobj_out.kind itemsize_in = dtypeobj_in.itemsize itemsize_out = dtypeobj_out.itemsize # Below, we do an `issubdtype` check. Its purpose is to find out # whether we can get away without doing any image conversion. This happens # when: # # - the output and input dtypes are the same or # - when the output is specified as a type, and the input dtype # is a subclass of that type (e.g. `np.floating` will allow # `float32` and `float64` arrays through) if np.issubdtype(dtype_in, np.obj2sctype(dtype)): if force_copy: image = image.copy() return image if not (dtype_in in _supported_types and dtype_out in _supported_types): raise ValueError("Can not convert from {} to {}." .format(dtypeobj_in, dtypeobj_out)) if kind_in in 'ui': imin_in = np.iinfo(dtype_in).min imax_in = np.iinfo(dtype_in).max if kind_out in 'ui': imin_out = np.iinfo(dtype_out).min imax_out = np.iinfo(dtype_out).max # any -> binary if kind_out == 'b': return image > dtype_in(dtype_range[dtype_in][1] / 2) # binary -> any if kind_in == 'b': result = image.astype(dtype_out) if kind_out != 'f': result *= dtype_out(dtype_range[dtype_out][1]) return result # float -> any if kind_in == 'f': if kind_out == 'f': # float -> float return image.astype(dtype_out) if np.min(image) < -1.0 or np.max(image) > 1.0: raise ValueError("Images of type float must be between -1 and 1.") # floating point -> integer # use float type that can represent output integer type computation_type = _dtype_itemsize(itemsize_out, dtype_in, np.float32, np.float64) if not uniform: if kind_out == 'u': image_out = np.multiply(image, imax_out, dtype=computation_type) else: image_out = np.multiply(image, (imax_out - imin_out) / 2, dtype=computation_type) image_out -= 1.0 / 2. np.rint(image_out, out=image_out) np.clip(image_out, imin_out, imax_out, out=image_out) elif kind_out == 'u': image_out = np.multiply(image, imax_out + 1, dtype=computation_type) np.clip(image_out, 0, imax_out, out=image_out) else: image_out = np.multiply(image, (imax_out - imin_out + 1.0) / 2.0, dtype=computation_type) np.floor(image_out, out=image_out) np.clip(image_out, imin_out, imax_out, out=image_out) return image_out.astype(dtype_out) # signed/unsigned int -> float if kind_out == 'f': # use float type that can exactly represent input integers computation_type = _dtype_itemsize(itemsize_in, dtype_out, np.float32, np.float64) if kind_in == 'u': # using np.divide or np.multiply doesn't copy the data # until the computation time image = np.multiply(image, 1. / imax_in, dtype=computation_type) # DirectX uses this conversion also for signed ints # if imin_in: # np.maximum(image, -1.0, out=image) else: image = np.add(image, 0.5, dtype=computation_type) image *= 2 / (imax_in - imin_in) return np.asarray(image, dtype_out) # unsigned int -> signed/unsigned int if kind_in == 'u': if kind_out == 'i': # unsigned int -> signed int image = _scale(image, 8 * itemsize_in, 8 * itemsize_out - 1) return image.view(dtype_out) else: # unsigned int -> unsigned int return _scale(image, 8 * itemsize_in, 8 * itemsize_out) # signed int -> unsigned int if kind_out == 'u': image = _scale(image, 8 * itemsize_in - 1, 8 * itemsize_out) result = np.empty(image.shape, dtype_out) np.maximum(image, 0, out=result, dtype=image.dtype, casting='unsafe') return result # signed int -> signed int if itemsize_in > itemsize_out: return _scale(image, 8 * itemsize_in - 1, 8 * itemsize_out - 1) image = image.astype(_dtype_bits('i', itemsize_out * 8)) image -= imin_in image = _scale(image, 8 * itemsize_in, 8 * itemsize_out, copy=False) image += imin_out return image.astype(dtype_out) def convert(image, dtype, force_copy=False, uniform=False): warn("The use of this function is discouraged as its behavior may change " "dramatically in scikit-image 1.0. This function will be removed" "in scikit-image 1.0.", FutureWarning, stacklevel=2) return _convert(image=image, dtype=dtype, force_copy=force_copy, uniform=uniform) if _convert.__doc__ is not None: convert.__doc__ = _convert.__doc__ + """ Warns ----- FutureWarning: .. versionadded:: 0.17 The use of this function is discouraged as its behavior may change dramatically in scikit-image 1.0. This function will be removed in scikit-image 1.0. """ def img_as_float32(image, force_copy=False): """Convert an image to single-precision (32-bit) floating point format. Parameters ---------- image : ndarray Input image. force_copy : bool, optional Force a copy of the data, irrespective of its current dtype. Returns ------- out : ndarray of float32 Output image. Notes ----- The range of a floating point image is [0.0, 1.0] or [-1.0, 1.0] when converting from unsigned or signed datatypes, respectively. If the input image has a float type, intensity values are not modified and can be outside the ranges [0.0, 1.0] or [-1.0, 1.0]. """ return _convert(image, np.float32, force_copy) def img_as_float64(image, force_copy=False): """Convert an image to double-precision (64-bit) floating point format. Parameters ---------- image : ndarray Input image. force_copy : bool, optional Force a copy of the data, irrespective of its current dtype. Returns ------- out : ndarray of float64 Output image. Notes ----- The range of a floating point image is [0.0, 1.0] or [-1.0, 1.0] when converting from unsigned or signed datatypes, respectively. If the input image has a float type, intensity values are not modified and can be outside the ranges [0.0, 1.0] or [-1.0, 1.0]. """ return _convert(image, np.float64, force_copy) def img_as_float(image, force_copy=False): """Convert an image to floating point format. This function is similar to `img_as_float64`, but will not convert lower-precision floating point arrays to `float64`. Parameters ---------- image : ndarray Input image. force_copy : bool, optional Force a copy of the data, irrespective of its current dtype. Returns ------- out : ndarray of float Output image. Notes ----- The range of a floating point image is [0.0, 1.0] or [-1.0, 1.0] when converting from unsigned or signed datatypes, respectively. If the input image has a float type, intensity values are not modified and can be outside the ranges [0.0, 1.0] or [-1.0, 1.0]. """ return _convert(image, np.floating, force_copy) def img_as_uint(image, force_copy=False): """Convert an image to 16-bit unsigned integer format. Parameters ---------- image : ndarray Input image. force_copy : bool, optional Force a copy of the data, irrespective of its current dtype. Returns ------- out : ndarray of uint16 Output image. Notes ----- Negative input values will be clipped. Positive values are scaled between 0 and 65535. """ return _convert(image, np.uint16, force_copy) def img_as_int(image, force_copy=False): """Convert an image to 16-bit signed integer format. Parameters ---------- image : ndarray Input image. force_copy : bool, optional Force a copy of the data, irrespective of its current dtype. Returns ------- out : ndarray of int16 Output image. Notes ----- The values are scaled between -32768 and 32767. If the input data-type is positive-only (e.g., uint8), then the output image will still only have positive values. """ return _convert(image, np.int16, force_copy) def img_as_ubyte(image, force_copy=False): """Convert an image to 8-bit unsigned integer format. Parameters ---------- image : ndarray Input image. force_copy : bool, optional Force a copy of the data, irrespective of its current dtype. Returns ------- out : ndarray of ubyte (uint8) Output image. Notes ----- Negative input values will be clipped. Positive values are scaled between 0 and 255. """ return _convert(image, np.uint8, force_copy) def img_as_bool(image, force_copy=False): """Convert an image to boolean format. Parameters ---------- image : ndarray Input image. force_copy : bool, optional Force a copy of the data, irrespective of its current dtype. Returns ------- out : ndarray of bool (`bool_`) Output image. Notes ----- The upper half of the input dtype's positive range is True, and the lower half is False. All negative values (if present) are False. """ return _convert(image, bool, force_copy)