Source code for colorex.cex_keras

# Copyright 2021 Alex Harvill
# SPDX-License-Identifier: Apache-2.0
'colorex keras layers'
import tensorflow.keras as keras
import tensorflow.keras.backend as K

from colorex.cex_constants import (
    REC_709_LUMA_WEIGHTS,
    MAX_COMPONENT_VALUE,
    SMALL_COMPONENT_VALUE,
    XYZ_D65_2A_WHITEPOINT,
    M_RGB_TO_XYZ_T,
    M_XYZ_TO_RGB_T,
    M_RGB_TO_YCBCR_T,
    M_YCBCR_TO_RGB_T,
    YCBCR_MIN,
    YCBCR_YMAX,
    YCBCR_CMAX,
    YCBCR_OFFSET,
)

from colorex.cex_constants import S
import numpy as np


[docs]def srgb_to_rgb(srgb): 'convert from a gamma 2.4 color space to linear rgb' srgb = K.clip(srgb, SMALL_COMPONENT_VALUE, MAX_COMPONENT_VALUE) linear_mask = K.cast(srgb <= 0.04045, dtype='float32') exponential_mask = K.cast(srgb > 0.04045, dtype='float32') linear_pixels = srgb / 12.92 exponential_pixels = K.pow((srgb + 0.055) / 1.055, 2.4) return linear_pixels * linear_mask + exponential_pixels * exponential_mask
[docs]def rgb_to_srgb(rgb): 'convert from linear rgb to a gamma 2.4 color space' rgb = K.clip(rgb, SMALL_COMPONENT_VALUE, MAX_COMPONENT_VALUE) linear_mask = K.cast(rgb <= 0.0031308, dtype='float32') exponential_mask = K.cast(rgb > 0.0031308, dtype='float32') linear_pixels = rgb * 12.92 exponential_pixels = 1.055 * K.pow(rgb, 1.0 / 2.4) - 0.055 return linear_pixels * linear_mask + exponential_pixels * exponential_mask
[docs]def rgb_to_xyz(rgb): 'convert from gamma 1.0 RGB color space to XYZ' return K.dot(rgb, K.constant(M_RGB_TO_XYZ_T))
[docs]def xyz_to_rgb(xyz): 'convert from XYZ to a gamma 1.0 RGB color space' return K.dot(xyz, K.constant(M_XYZ_TO_RGB_T))
[docs]def xyz_to_lab(xyz): 'convert from a CIEXYZ space to CIELa*b*' xyz = xyz / K.constant(XYZ_D65_2A_WHITEPOINT) xyz = K.clip(xyz, SMALL_COMPONENT_VALUE, MAX_COMPONENT_VALUE) epsilon = 0.008856 #(6.0 / 29.0)**3 # use hardcoded value to match skimage for validation linear_mask = K.cast(xyz <= epsilon, dtype='float32') cuberoot_mask = K.cast(xyz > epsilon, dtype='float32') linear_pixels = 7.787 * xyz + 16.0 / 116.0 cuberoot_pixels = K.pow(xyz, 1.0 / 3.0) xyz = linear_pixels * linear_mask + cuberoot_pixels * cuberoot_mask x, y, z = xyz[..., 0], xyz[..., 1], xyz[..., 2] # Vector scaling L = (116.0 * y) - 16.0 a = 500.0 * (x - y) b = 200.0 * (y - z) return K.stack([L, a, b], axis=-1)
[docs]def lab_to_xyz(lab): 'convert from lab to xyz color space assuming a D65 whitepoint + 2deg angle' l, a, b = lab[..., 0], lab[..., 1], lab[..., 2] y = (l + 16.0) / 116.0 x = (a / 500.0) + y z = y - (b / 200.0) z = K.clip(z, 0.0, 1e20) xyz = K.stack([x, y, z], axis=-1) epsilon = 6.0 / 29.0 linear_mask = K.cast(xyz < epsilon, dtype='float32') cube_mask = K.cast(xyz >= epsilon, dtype='float32') linear_pixels = (xyz - 16.0 / 116.) / 7.787 cube_pixels = K.pow(xyz, 3.0) xyz = linear_pixels * linear_mask + cube_pixels * cube_mask xyz = xyz * K.constant(XYZ_D65_2A_WHITEPOINT) return xyz
[docs]def rgb_to_luminance(rgb, luma_weights=REC_709_LUMA_WEIGHTS): 'luminance of a color array, or higher dim color images' r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2] return r * luma_weights[0] + g * luma_weights[1] + b * luma_weights[2]
[docs]def xyz_to_xyy(XYZ): ''' convert from XYZ color space to xyY XYZ: consistent units for each component xyY: normalized chromaticity with xy in 0-1, Y in 0-inf https://en.wikipedia.org/wiki/CIE_1931_color_space http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_xyY.html ''' X, Y, Z = XYZ[..., 0], XYZ[..., 1], XYZ[..., 2] XYZ_sum = X + Y + Z epsilon = 1.0 / 1000.0 unit_mask = K.cast(XYZ_sum < epsilon, dtype='float32') XYZ_sum = unit_mask + (1.0 - unit_mask) * XYZ_sum x = X / XYZ_sum y = Y / XYZ_sum return K.stack([x, y, Y], axis=-1)
[docs]def xyy_to_xyz(xyY): ''' convert from xyY color space to XYZ xyY: normalized chromaticity with xy in 0-1, Y in 0-inf XYZ: consistent units for each component https://en.wikipedia.org/wiki/CIE_1931_color_space http://www.brucelindbloom.com/index.html?Eqn_xyY_to_XYZ.html ''' x, y, Y = xyY[..., 0], xyY[..., 1], xyY[..., 2] invalid_mask = K.cast(y < SMALL_COMPONENT_VALUE, dtype='float32') valid_mask = 1.0 - invalid_mask y = invalid_mask + valid_mask * y norm = Y / y X = x * norm Z = (1 - x - y) * norm X *= valid_mask Y *= valid_mask Z *= valid_mask return K.stack([X, Y, Z], axis=-1)
[docs]def rgb_to_ycbcr(rgb): ''' convert from rgb color space to YCbCr rgb: rgb color space YCbCr: luminance with 2 chroma channels https://en.wikipedia.org/wiki/YCbCr http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_xyY.html ''' ycbcr = K.dot(rgb, K.constant(M_RGB_TO_YCBCR_T)) y, cb, cr = ycbcr[..., 0], ycbcr[..., 1], ycbcr[..., 2] # y += YCBCR_MIN # y -= YCBCR_MIN y /= YCBCR_YMAX - YCBCR_MIN cb += YCBCR_OFFSET cb -= YCBCR_MIN cb /= YCBCR_CMAX - YCBCR_MIN cr += YCBCR_OFFSET cr -= YCBCR_MIN cr /= YCBCR_CMAX - YCBCR_MIN return K.stack([y, cb, cr], axis=-1)
[docs]def ycbcr_to_rgb(ycbcr): ''' convert from YCbCr color space to srgb YCbCr: luminance with 2 chroma channels srgb: rgb color space https://en.wikipedia.org/wiki/YCbCr http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_xyY.html ''' y, cb, cr = ycbcr[..., 0], ycbcr[..., 1], ycbcr[..., 2] y *= YCBCR_YMAX - YCBCR_MIN # y += YCBCR_MIN # y -= YCBCR_MIN cb *= YCBCR_CMAX - YCBCR_MIN cb += YCBCR_MIN cb -= YCBCR_OFFSET cr *= YCBCR_CMAX - YCBCR_MIN cr += YCBCR_MIN cr -= YCBCR_OFFSET ycbcr = K.stack([y, cb, cr], axis=-1) return K.dot(ycbcr, K.constant(M_YCBCR_TO_RGB_T))
#### following transforms are macros using the above primitive transforms
[docs]def xyz_to_srgb(xyz): 'xyz > rgb > srgb' tmp = xyz_to_rgb(xyz) return rgb_to_srgb(tmp)
[docs]def srgb_to_xyz(srgb): 'srgb > rgb > xyz' tmp = srgb_to_rgb(srgb) return rgb_to_xyz(tmp)
[docs]def srgb_to_lab(srgb): 'srgb -> xyz -> lab' tmp = srgb_to_xyz(srgb) return xyz_to_lab(tmp)
[docs]def lab_to_srgb(lab): 'lab > xyz > srgb' tmp = lab_to_xyz(lab) return xyz_to_srgb(tmp)
[docs]def rgb_to_lab(rgb): 'rgb -> xyz -> lab' tmp = rgb_to_xyz(rgb) return xyz_to_lab(tmp)
[docs]def lab_to_rgb(lab): 'lab > xyz > rgb' tmp = lab_to_xyz(lab) return xyz_to_rgb(tmp)
[docs]def rgb_to_xyy(rgb): 'rgb > xyz > xyy' tmp = rgb_to_xyz(rgb) return xyz_to_xyy(tmp)
[docs]def srgb_to_xyy(srgb): 'srgb > rgb > xyz > xyy' tmp = srgb_to_rgb(srgb) return rgb_to_xyy(tmp)
[docs]def lab_to_xyy(lab): 'lab > xyz > xyy' tmp = lab_to_xyz(lab) return xyz_to_xyy(tmp)
[docs]def xyy_to_rgb(xyy): 'xyy > xyz > rgb' tmp = xyy_to_xyz(xyy) return xyz_to_rgb(tmp)
[docs]def xyy_to_srgb(xyy): 'xyy > xyz > rgb > srgb' tmp = xyy_to_rgb(xyy) return rgb_to_srgb(tmp)
[docs]def xyy_to_lab(xyy): 'xyz > xyz > lab' tmp = xyy_to_xyz(xyy) return xyz_to_lab(tmp)
class Bias(keras.layers.Layer): 'simple layer for testing' def __init__(self, **kwargs): self.bias = None super(Bias, self).__init__(**kwargs) def build(self, input_shape): 'creates trainable weight variable for this bias layer' self.bias = self.add_weight( name='weights', shape=(1,), initializer='zeros', trainable=True, ) super(Bias, self).build(input_shape) # will set self.built = True def call(self, inputs, **kwargs): 'builds an output tensor for this op' return inputs + self.bias TRANSFORMS = { (S.SRGB, S.RGB): srgb_to_rgb, (S.SRGB, S.XYZ): srgb_to_xyz, (S.SRGB, S.LAB): srgb_to_lab, (S.SRGB, S.LUM): None, (S.SRGB, S.xyY): srgb_to_xyy, (S.RGB, S.SRGB): rgb_to_srgb, (S.RGB, S.XYZ): rgb_to_xyz, (S.RGB, S.LAB): rgb_to_lab, (S.RGB, S.LUM): rgb_to_luminance, (S.RGB, S.xyY): rgb_to_xyy, (S.RGB, S.YCbCr): rgb_to_ycbcr, (S.XYZ, S.RGB): xyz_to_rgb, (S.XYZ, S.LAB): xyz_to_lab, (S.XYZ, S.SRGB): xyz_to_srgb, (S.XYZ, S.LUM): None, (S.XYZ, S.xyY): xyz_to_xyy, (S.LAB, S.XYZ): lab_to_xyz, (S.LAB, S.SRGB): lab_to_srgb, (S.LAB, S.RGB): lab_to_rgb, (S.LAB, S.LUM): None, (S.LAB, S.xyY): lab_to_xyy, (S.xyY, S.SRGB): xyy_to_srgb, (S.xyY, S.RGB): xyy_to_rgb, (S.xyY, S.XYZ): xyy_to_xyz, (S.xyY, S.LAB): xyy_to_lab, (S.xyY, S.LUM): None, (S.YCbCr, S.RGB): ycbcr_to_rgb, }
[docs]def color_space(from_space, to_space, values): ''' lookup color transform from_space to_space apply transform and return output tensor short circuit compute if from_space == to_space ''' if from_space == to_space: result = values else: t = TRANSFORMS.get((from_space, to_space)) if t is None: ValueError(f'bad transform[{from_space.name},{to_space.name}]') result = t(values) return result
[docs]def color_space_numpy(from_space, to_space, values): 'numpy wrapper for backend color transform' result = color_space(from_space, to_space, K.constant(values)) if hasattr(result, 'numpy'): result = result.numpy() return result
class ColorSpace(keras.layers.Layer): 'convert from_space to_space' def __init__(self, from_space, to_space, **kwargs): self.from_space = S[from_space] self.to_space = S[to_space] super(ColorSpace, self).__init__(**kwargs) def call(self, inputs, **kwargs): 'builds an output tensor for this op' return color_space(self.from_space, self.to_space, inputs) def get_config(self): 'save from and to attributes' return dict( super(ColorSpace, self).get_config(), from_space=self.from_space.name, to_space=self.to_space.name, ) def compute_output_shape(self, input_shape): 'some transforms remove the color dimension' result = input_shape if self.to_space in (S.LUM,): result = input_shape[:-1] return result