Source code for dmtools.colorspace

import numpy as np

# Referenced colorconv.py from scikit-image for more efficient implementation
# of colorspace transformations. Will continue to maintain an independent
# implementation for educational purposes but scikit-image is the standard.

# https://wikipedia.org/wiki/Standard_illuminant
# These values assume the 2 degree point of view
illuminants = \
    {'D50': (96.4212, 100.0, 82.5188),
     'D65': (95.0489, 100.0, 108.8840)}

# http://poynton.ca/PDFs/ColorFAQ.pdf
rgb_to_gray = np.array([0.2125, 0.7154, 0.0721])

# https://wikipedia.org/wiki/CIE_1931_color_space
b_21 = 0.17697
rgb_to_xyz = np.array([[0.49000, 0.31000, 0.20000],
                       [0.17697, 0.81240, 0.01063],
                       [0.00000, 0.01000, 0.99000]]) / b_21
xyz_to_rgb = np.linalg.inv(rgb_to_xyz)

# https://wikipedia.org/wiki/YUV
rgb_to_yuv = np.array([[+0.29900, +0.58700, +0.11400],
                       [-0.14713, -0.28886, +0.43600],
                       [+0.61500, -0.51499, -0.10001]])
yuv_to_rgb = np.linalg.inv(rgb_to_yuv)

# Used to normalize an image in a colorspace to [0,1]
norm = \
    {'RGB': {'scale': (1.0, 1.0, 1.0),
             'shift': (0.0, 0.0, 0.0)},
     'Lab': {'scale': (1.0, 1.0, 1.0),
             'shift': (0.0, -0.5, -0.5)},
     'YUV': {'scale': (1.0, 1.0, 1.0),
             'shift': (0.0, -0.5, -0.5)}}


[docs]def RGB_to_gray(image: np.ndarray) -> np.ndarray: """Convert an image in CIE RGB space to grayscale. For details about the implemented conversion, see `FAQs about Color <http://poynton.ca/PDFs/ColorFAQ.pdf>`_. Args: image (np.ndarray): Image in CIE RGB space. Returns: np.ndarray: Image in grayscale. """ image = np.copy(image) # TODO: this causes opacity of 4-channel images to be lost image = image[:,:,:3] @ rgb_to_gray.T return image
[docs]def gray_to_RGB(image: np.ndarray) -> np.ndarray: """Convert an image in grayscale to CIE RGB space. Args: image (np.ndarray): Image in grayscale. Returns: np.ndarray: Image in CIE RGB space. """ return np.stack(3 * (image,), axis=-1)
[docs]def add_alpha(image: np.ndarray, a: float = 1) -> np.ndarray: """Add an alpha channel to a three color channel image. Args: image (np.ndarray): Image with three color channels. a (float): Alpha value to use in the image. Returns: np.ndarray: Four channel image with alpha channel. """ n,m,*_ = image.shape return np.concatenate((image, a * np.ones((n,m,1))), axis=-1)
[docs]def RGB_to_XYZ(image: np.ndarray) -> np.ndarray: """Convert an image in CIE RGB space to XYZ space. For details about the implemented conversion, see `CIE 1931 color space <https://wikipedia.org/wiki/CIE_1931_color_space>`_. Args: image (np.ndarray): Image in CIE RGB space. Returns: np.ndarray: Image in CIE XYZ space. """ image = np.copy(image) image[:,:,:3] = image[:,:,:3] @ rgb_to_xyz.T return image
[docs]def XYZ_to_RGB(image: np.ndarray) -> np.ndarray: """Convert an image in CIE XYZ space to RGB space. For details about the implemented conversion, see `CIE 1931 color space <https://wikipedia.org/wiki/CIE_1931_color_space>`_. Args: image (np.ndarray): Image in CIE XYZ space. Returns: np.ndarray: Image in CIE RGB space. """ image = np.copy(image) image[:,:,:3] = image[:,:,:3] @ xyz_to_rgb.T return image
[docs]def RGB_to_YUV(image: np.ndarray) -> np.ndarray: """Convert an image in CIE RGB space to YUV space. For details about the implemented conversion, see `YUV <https://wikipedia.org/wiki/YUV>`_. Args: image (np.ndarray): Image in CIE RGB space. Returns: np.ndarray: Image in YUV space. """ image = np.copy(image) image[:,:,:3] = image[:,:,:3] @ rgb_to_yuv.T return image
[docs]def YUV_to_RGB(image: np.ndarray) -> np.ndarray: """Convert an image in YUV space to CIE RGB space. For details about the implemented conversion, see `YUV <https://wikipedia.org/wiki/YUV>`_. Args: image (np.ndarray): Image in YUV space. Returns: np.ndarray: Image in CIE RGB space. """ image = np.copy(image) image[:,:,:3] = image[:,:,:3] @ yuv_to_rgb.T return image
[docs]def XYZ_to_Lab(image: np.ndarray, illuminant: str = 'D65') -> np.ndarray: """Convert an image in CIE XYZ space to Lab space. For details about the implemented conversion, see `CIELAB color space <https://wikipedia.org/wiki/CIELAB_color_space>`_. Args: image (np.ndarray): Image in CIE XYZ space. illuminant (str): Standard illuminant {D65, D50} Returns: np.ndarray: Image in Lab space. """ X_n, Y_n, Z_n = illuminants[illuminant] delta = 6 / 29 def f(t): return t**(1/3) if t > delta**3 else (t/(3*delta**2)) + (4/29) def to_Lab(x): X, Y, Z = x L = 116*f(Y/Y_n) - 16 a = 500*(f(X/X_n) - f(Y/Y_n)) b = 200*(f(Y/Y_n) - f(Z/Z_n)) return np.array([L,a,b]) image = np.copy(image) n,m,k = image.shape p = np.reshape(image[:,:,:3], (n*m,3)).astype(float) p = np.apply_along_axis(to_Lab, 1, p) image[:,:,:3] = np.reshape(p, (n,m,3)) return image
[docs]def Lab_to_XYZ(image: np.ndarray, illuminant: str = 'D65') -> np.ndarray: """Convert an image in Lab space to CIE XYZ space. For details about the implemented conversion, see `CIELAB color space <https://wikipedia.org/wiki/CIELAB_color_space>`_. Args: image (np.ndarray): Image in Lab space. illuminant (str): Standard illuminant {D65, D50} Returns: np.ndarray: Image in CIE XYZ space. """ X_n, Y_n, Z_n = illuminants[illuminant] delta = 6 / 29 def f_inv(t): return t**3 if t > delta else 3*delta**2*(t-(4/29)) def to_XYZ(x): L, a, b = x X = X_n*f_inv(((L + 16)/116) + a/500) Y = Y_n*f_inv((L + 16)/116) Z = Z_n*f_inv(((L + 16)/116) - b/200) return np.array([X,Y,Z]) image = np.copy(image) n,m,k = image.shape p = np.reshape(image[:,:,:3], (n*m,3)).astype(float) p = np.apply_along_axis(to_XYZ, 1, p) image[:,:,:3] = np.reshape(p, (n,m,3)) return image
[docs]def RGB_to_Lab(image: np.ndarray, illuminant: str = 'D65') -> np.ndarray: """Convert an image in CIE RGB space to Lab space. For details about the implemented conversion, see `CIE 1931 color space <https://wikipedia.org/wiki/CIE_1931_color_space>`_ and `CIELAB color space <https://wikipedia.org/wiki/CIELAB_color_space>`_. Args: image (np.ndarray): Image in CIE RGB space. illuminant (str): Standard illuminant {D65, D50} Returns: np.ndarray: Image in Lab space. """ return XYZ_to_Lab(RGB_to_XYZ(image), illuminant)
[docs]def Lab_to_RGB(image: np.ndarray, illuminant: str = 'D65') -> np.ndarray: """Convert an image in Lab space to CIE RGB space. For details about the implemented conversion, see `CIE 1931 color space <https://wikipedia.org/wiki/CIE_1931_color_space>`_ and `CIELAB color space <https://wikipedia.org/wiki/CIELAB_color_space>`_. Args: image (np.ndarray): Image in Lab space. illuminant (str): Standard illuminant {D65, D50} Returns: np.ndarray: Image in CIE RGB space. """ return XYZ_to_RGB(Lab_to_XYZ(image, illuminant))
[docs]def normalize(image: np.ndarray, color_space: str) -> np.ndarray: """Normalize the image in the given color space. Args: image (np.ndarray): Image in the given color space. color_space (str): Color space {RGB, Lab, YUV}. Returns: np.ndarray: Normalized image with values in [0,1]. """ scale = norm[color_space]['scale'] shift = norm[color_space]['shift'] shift_mat = np.ones(image.shape) @ np.diag(shift) normalized_image = (image - shift_mat) @ np.diag(1 / np.array(scale)) return normalized_image.clip(0,1)
[docs]def denormalize(image: np.ndarray, color_space: str) -> np.ndarray: """Denormalize the image in the given color space. Args: image (np.ndarray): Normalized image in the given color space. color_space (str): Color space {RGB, Lab, YUV}. Returns: np.ndarray: Denormalized image in the given color space. """ scale = norm[color_space]['scale'] shift = norm[color_space]['shift'] shift_mat = np.ones(image.shape) @ np.diag(shift) return (image @ np.diag(scale)) + shift_mat