Permalink
511 lines (424 sloc) 16.6 KB
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
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 = {np.bool_: (False, True),
np.bool8: (False, True),
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 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.
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
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)
type_out = dtype if isinstance(dtype, type) else dtypeobj_out
if np.issubdtype(dtypeobj_in, type_out):
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))
def sign_loss():
warn("Possible sign loss when converting negative image of type "
"{} to positive image of type {}."
.format(dtypeobj_in, dtypeobj_out))
def prec_loss():
warn("Possible precision loss when converting from {} to {}"
.format(dtypeobj_in, dtypeobj_out))
def _dtype_itemsize(itemsize, *dtypes):
# Return 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
def compare(x, y, kind='u'):
if kind == 'u':
return x <= y
else:
return x < y
s = next(i for i in (itemsize, ) + (2, 4, 8) if compare(bits, i * 8,
kind=kind))
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))
return a.astype(_dtype_bits(kind, m))
elif n == m:
return a.copy() if copy else a
elif n > m:
# downscale with precision loss
prec_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
prec_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
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':
if kind_in in "fi":
sign_loss()
prec_loss()
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
if itemsize_in > itemsize_out:
prec_loss()
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
prec_loss()
# 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':
if itemsize_in >= itemsize_out:
prec_loss()
# 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':
sign_loss()
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 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 uint16
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, np.bool_, force_copy)