summaryrefslogtreecommitdiff
path: root/silx/math/colormap.pyx
diff options
context:
space:
mode:
Diffstat (limited to 'silx/math/colormap.pyx')
-rw-r--r--silx/math/colormap.pyx336
1 files changed, 244 insertions, 92 deletions
diff --git a/silx/math/colormap.pyx b/silx/math/colormap.pyx
index c5d3e09..dde249d 100644
--- a/silx/math/colormap.pyx
+++ b/silx/math/colormap.pyx
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,13 +30,16 @@ __license__ = "MIT"
__date__ = "16/05/2018"
+import os
cimport cython
from cython.parallel import prange
cimport numpy as cnumpy
-from libc.math cimport frexp, sqrt
+from libc.math cimport frexp, sinh, sqrt
from .math_compatibility cimport asinh, isnan, isfinite, lrint, INFINITY, NAN
import logging
+import numbers
+
import numpy
__all__ = ['cmap']
@@ -44,6 +47,16 @@ __all__ = ['cmap']
_logger = logging.getLogger(__name__)
+cdef int DEFAULT_NUM_THREADS
+if hasattr(os, 'sched_getaffinity'):
+ DEFAULT_NUM_THREADS = min(4, len(os.sched_getaffinity(0)))
+elif os.cpu_count() is not None:
+ DEFAULT_NUM_THREADS = min(4, os.cpu_count())
+else: # Fallback
+ DEFAULT_NUM_THREADS = 1
+# Number of threads to use for the computation (initialized to up to 4)
+
+
# Supported data types
ctypedef fused data_types:
cnumpy.uint8_t
@@ -84,60 +97,193 @@ ctypedef fused image_types:
float
-# Type of scale function
-ctypedef double (*scale_function)(double) nogil
+# Normalization
+ctypedef double (*NormalizationFunction)(double) nogil
+
+
+cdef class Normalization:
+ """Base class for colormap normalization"""
+
+ def apply(self, data, double vmin, double vmax):
+ """Apply normalization.
+
+ :param Union[float,numpy.ndarray] data:
+ :param float vmin: Lower bound of the range
+ :param float vmax: Upper bound of the range
+ :rtype: Union[float,numpy.ndarray]
+ """
+ cdef int length
+ cdef double[:] result
+
+ if isinstance(data, numbers.Real):
+ return self.apply_double(<double> data, vmin, vmax)
+ else:
+ data = numpy.array(data, copy=False)
+ length = <int> data.size
+ result = numpy.empty(length, dtype=numpy.float64)
+ data1d = numpy.ravel(data)
+ for index in range(length):
+ result[index] = self.apply_double(
+ <double> data1d[index], vmin, vmax)
+ return numpy.array(result).reshape(data.shape)
+
+ def revert(self, data, double vmin, double vmax):
+ """Revert normalization.
+
+ :param Union[float,numpy.ndarray] data:
+ :param float vmin: Lower bound of the range
+ :param float vmax: Upper bound of the range
+ :rtype: Union[float,numpy.ndarray]
+ """
+ cdef int length
+ cdef double[:] result
+
+ if isinstance(data, numbers.Real):
+ return self.revert_double(<double> data, vmin, vmax)
+ else:
+ data = numpy.array(data, copy=False)
+ length = <int> data.size
+ result = numpy.empty(length, dtype=numpy.float64)
+ data1d = numpy.ravel(data)
+ for index in range(length):
+ result[index] = self.revert_double(
+ <double> data1d[index], vmin, vmax)
+ return numpy.array(result).reshape(data.shape)
+
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ """Apply normalization to a floating point value
+
+ Override in subclass
+
+ :param float value:
+ :param float vmin: Lower bound of the range
+ :param float vmax: Upper bound of the range
+ """
+ return value
+
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ """Apply inverse of normalization to a floating point value
+
+ Override in subclass
+
+ :param float value:
+ :param float vmin: Lower bound of the range
+ :param float vmax: Upper bound of the range
+ """
+ return value
+
+
+cdef class LinearNormalization(Normalization):
+ """Linear normalization"""
+
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ return value
+
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ return value
+
+
+cdef class LogarithmicNormalization(Normalization):
+ """Logarithmic normalization using a fast log approximation"""
+ cdef:
+ readonly int lutsize
+ readonly double[::1] lut # LUT used for fast log approximation
+
+ def __cinit__(self, int lutsize=4096):
+ # Initialize log approximation LUT
+ self.lutsize = lutsize
+ self.lut = numpy.log2(
+ numpy.linspace(0.5, 1., lutsize + 1,
+ endpoint=True).astype(numpy.float64))
+ # index_lut can overflow of 1
+ self.lut[lutsize] = self.lut[lutsize - 1]
+
+ def __dealloc__(self):
+ self.lut = None
+
+ @cython.wraparound(False)
+ @cython.boundscheck(False)
+ @cython.nonecheck(False)
+ @cython.cdivision(True)
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ """Return log10(value) fast approximation based on LUT"""
+ cdef double result = NAN # if value < 0.0 or value == NAN
+ cdef int exponent, index_lut
+ cdef double mantissa # in [0.5, 1) unless value == 0 NaN or +/-inf
+
+ if value <= 0.0 or not isfinite(value):
+ if value == 0.0:
+ result = - INFINITY
+ elif value > 0.0: # i.e., value = +INFINITY
+ result = value # i.e. +INFINITY
+ else:
+ mantissa = frexp(value, &exponent)
+ index_lut = lrint(self.lutsize * 2 * (mantissa - 0.5))
+ # 1/log2(10) = 0.30102999566398114
+ result = 0.30102999566398114 * (<double> exponent +
+ self.lut[index_lut])
+ return result
+
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ return 10**value
+
+
+cdef class ArcsinhNormalization(Normalization):
+ """Inverse hyperbolic sine normalization"""
+
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ return asinh(value)
+
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ return sinh(value)
-# Normalization
+cdef class SqrtNormalization(Normalization):
+ """Square root normalization"""
-cdef double linear_scale(double value) nogil:
- """No-Op scaling function"""
- return value
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ return sqrt(value)
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ return value**2
-cdef double asinh_scale(double value) nogil:
- """asinh scaling function
- Wraps asinh as it is defined as a macro for Windows support.
+cdef class PowerNormalization(Normalization):
+ """Gamma correction:
+
+ Linear normalization to [0, 1] followed by power normalization.
+
+ :param gamma: Gamma correction factor
"""
- return asinh(value)
+ cdef:
+ readonly double gamma
-DEF LOG_LUT_SIZE = 4096
-cdef double[::1] _log_lut
-"""LUT used for fast log approximation"""
+ def __cinit__(self, double gamma):
+ self.gamma = gamma
-# Initialize log approximation LUT
-_log_lut = numpy.log2(
- numpy.linspace(0.5, 1., LOG_LUT_SIZE + 1,
- endpoint=True).astype(numpy.float64))
-# index_lut can overflow of 1
-_log_lut[LOG_LUT_SIZE] = _log_lut[LOG_LUT_SIZE - 1]
+ def __init__(self, gamma):
+ # Needed for multiple inheritance to work
+ pass
+ cdef double apply_double(self, double value, double vmin, double vmax) nogil:
+ if vmin == vmax:
+ return 0.
+ elif value <= vmin:
+ return 0.
+ elif value >= vmax:
+ return 1.
+ else:
+ return ((value - vmin) / (vmax - vmin))**self.gamma
-@cython.wraparound(False)
-@cython.boundscheck(False)
-@cython.nonecheck(False)
-@cython.cdivision(True)
-cdef double fast_log10(double value) nogil:
- """Return log10(value) fast approximation based on LUT"""
- cdef double result = NAN # if value < 0.0 or value == NAN
- cdef int exponent, index_lut
- cdef double mantissa # in [0.5, 1) unless value == 0 NaN or +/-inf
-
- if value <= 0.0 or not isfinite(value):
- if value == 0.0:
- result = - INFINITY
- elif value > 0.0: # i.e., value = +INFINITY
- result = value # i.e. +INFINITY
- else:
- mantissa = frexp(value, &exponent)
- index_lut = lrint(LOG_LUT_SIZE * 2 * (mantissa - 0.5))
- # 1/log2(10) = 0.30102999566398114
- result = 0.30102999566398114 * (<double> exponent +
- _log_lut[index_lut])
- return result
+ cdef double revert_double(self, double value, double vmin, double vmax) nogil:
+ if value <= 0.:
+ return vmin
+ elif value >= 1.:
+ return vmax
+ else:
+ return vmin + (vmax - vmin) * value**(1.0/self.gamma)
# Colormap
@@ -149,22 +295,22 @@ cdef double fast_log10(double value) nogil:
cdef image_types[:, ::1] compute_cmap(
default_types[:] data,
image_types[:, ::1] colors,
- double normalized_vmin,
- double normalized_vmax,
- image_types[::1] nan_color,
- scale_function scale_func):
+ Normalization normalization,
+ double vmin,
+ double vmax,
+ image_types[::1] nan_color):
"""Apply colormap to data.
:param data: Input data
:param colors: Colors look-up-table
- :param normalized_vmin: Normalized lower bound of the colormap range
- :param normalized_vmax: Normalized upper bound of the colormap range
+ :param vmin: Lower bound of the colormap range
+ :param vmax: Upper bound of the colormap range
:param nan_color: Color to use for NaN value
- :param scale_func: The function to use to scale data
+ :param normalization: Normalization to apply
:return: Data converted to colors
"""
cdef image_types[:, ::1] output
- cdef double scale, value
+ cdef double scale, value, normalized_vmin, normalized_vmax
cdef int length, nb_channels, nb_colors
cdef int channel, index, lut_index
@@ -175,14 +321,21 @@ cdef image_types[:, ::1] compute_cmap(
output = numpy.empty((length, nb_channels),
dtype=numpy.array(colors, copy=False).dtype)
+ normalized_vmin = normalization.apply_double(vmin, vmin, vmax)
+ normalized_vmax = normalization.apply_double(vmax, vmin, vmax)
+
+ if not isfinite(normalized_vmin) or not isfinite(normalized_vmax):
+ raise ValueError('Colormap range is not valid')
+
if normalized_vmin == normalized_vmax:
scale = 0.
else:
scale = nb_colors / (normalized_vmax - normalized_vmin)
with nogil:
- for index in prange(length):
- value = scale_func(<double> data[index])
+ for index in prange(length, num_threads=DEFAULT_NUM_THREADS):
+ value = normalization.apply_double(
+ <double> data[index], vmin, vmax)
# Handle NaN
if isnan(value):
@@ -212,20 +365,20 @@ cdef image_types[:, ::1] compute_cmap(
cdef image_types[:, ::1] compute_cmap_with_lut(
lut_types[:] data,
image_types[:, ::1] colors,
- double normalized_vmin,
- double normalized_vmax,
- image_types[::1] nan_color,
- scale_function scale_func):
+ Normalization normalization,
+ double vmin,
+ double vmax,
+ image_types[::1] nan_color):
"""Convert data to colors using look-up table to speed the process.
Only supports data of types: uint8, uint16, int8, int16.
:param data: Input data
:param colors: Colors look-up-table
- :param normalized_vmin: Normalized lower bound of the colormap range
- :param normalized_vmax: Normalized upper bound of the colormap range
+ :param vmin: Lower bound of the colormap range
+ :param vmax: Upper bound of the colormap range
:param nan_color: Color to use for NaN values
- :param scale_func: The function to use for scaling data
+ :param normalization: Normalization to apply
:return: The generated image
"""
cdef image_types[:, ::1] output
@@ -255,14 +408,13 @@ cdef image_types[:, ::1] compute_cmap_with_lut(
values = numpy.arange(type_min, type_max + 1, dtype=numpy.float64)
lut = compute_cmap(
- values, colors, normalized_vmin, normalized_vmax,
- nan_color, scale_func)
+ values, colors, normalization, vmin, vmax, nan_color)
output = numpy.empty((length, nb_channels), dtype=colors_dtype)
with nogil:
# Apply LUT
- for index in prange(length):
+ for index in prange(length, num_threads=DEFAULT_NUM_THREADS):
lut_index = data[index] - type_min
for channel in range(nb_channels):
output[index, channel] = lut[lut_index, channel]
@@ -270,13 +422,22 @@ cdef image_types[:, ::1] compute_cmap_with_lut(
return output
+# Normalizations without parameters
+_BASIC_NORMALIZATIONS = {
+ 'linear': LinearNormalization(),
+ 'log': LogarithmicNormalization(),
+ 'arcsinh': ArcsinhNormalization(),
+ 'sqrt': SqrtNormalization(),
+ }
+
+
@cython.wraparound(False)
@cython.boundscheck(False)
@cython.nonecheck(False)
@cython.cdivision(True)
def _cmap(data_types[:] data,
image_types[:, ::1] colors,
- str normalization,
+ Normalization normalization,
double vmin,
double vmax,
image_types[::1] nan_color):
@@ -286,42 +447,20 @@ def _cmap(data_types[:] data,
:param data: Input data
:param colors: Colors look-up-table
- :param normalization: Kind of scaling to apply on data
+ :param normalization: Normalization object to apply
:param vmin: Lower bound of the colormap range
:param vmax: Upper bound of the colormap range
:param nan_color: Color to use for NaN value.
:return: The generated image
"""
- cdef double normalized_vmin, normalized_vmax
- cdef scale_function scale_func
-
- if normalization == 'linear':
- scale_func = linear_scale
- elif normalization == 'log':
- scale_func = fast_log10
- elif normalization == 'arcsinh':
- scale_func = asinh_scale
- elif normalization == 'sqrt':
- scale_func = sqrt
- else:
- raise ValueError('Unsupported normalization %s' % normalization)
-
- normalized_vmin = scale_func(vmin)
- normalized_vmax = scale_func(vmax)
-
- if not isfinite(normalized_vmin) or not isfinite(normalized_vmax):
- raise ValueError('Colormap range is not valid')
-
# Proxy for calling the right implementation depending on data type
if data_types in lut_types: # Use LUT implementation
output = compute_cmap_with_lut(
- data, colors, normalized_vmin, normalized_vmax,
- nan_color, scale_func)
+ data, colors, normalization, vmin, vmax, nan_color)
elif data_types in default_types: # Use default implementation
output = compute_cmap(
- data, colors, normalized_vmin, normalized_vmax,
- nan_color, scale_func)
+ data, colors, normalization, vmin, vmax, nan_color)
else:
raise ValueError('Unsupported data type')
@@ -342,12 +481,14 @@ def cmap(data,
It MUST be of type uint8 or float32
:param vmin: Data value to map to the beginning of colormap.
:param vmax: Data value to map to the end of the colormap.
- :param str normalization: The normalization to apply:
+ :param Union[str,Normalization] normalization:
+ Either a :class:`Normalization` instance or a str in:
- 'linear' (default)
- 'log'
- 'arcsinh'
- 'sqrt'
+ - 'gamma'
:param nan_color: Color to use for NaN value.
Default: A color with all channels set to 0
@@ -357,6 +498,7 @@ def cmap(data,
:rtype: numpy.ndarray
"""
cdef int nb_channels
+ cdef Normalization norm
# Make data a numpy array of native endian type (no need for contiguity)
data = numpy.array(data, copy=False)
@@ -371,6 +513,14 @@ def cmap(data,
colors = numpy.ascontiguousarray(colors,
dtype=colors.dtype.newbyteorder('N'))
+ # Make normalization a Normalization object
+ if isinstance(normalization, str):
+ norm = _BASIC_NORMALIZATIONS.get(normalization, None)
+ if norm is None:
+ raise ValueError('Unsupported normalization %s' % normalization)
+ else:
+ norm = normalization
+
# Check nan_color
if nan_color is None:
nan_color = numpy.zeros((nb_channels,), dtype=colors.dtype)
@@ -382,8 +532,10 @@ def cmap(data,
image = _cmap(
data.reshape(-1),
colors.reshape(-1, nb_channels),
- str(normalization),
- vmin, vmax, nan_color)
+ norm,
+ vmin,
+ vmax,
+ nan_color)
image.shape = data.shape + (nb_channels,)
return image