MeshPalettizer / src /palette /color_space.py
dylanebert's picture
initial commit
346b70f
import numpy as np
RGB_MAX_VALUE = 255.0
SRGB_GAMMA = 2.4
SRGB_LINEAR_THRESHOLD = 0.04045
SRGB_OFFSET = 0.055
SRGB_SCALE = 1.055
SRGB_LINEAR_SCALE = 12.92
D65_ILLUMINANT_X = 0.95047
D65_ILLUMINANT_Z = 1.08883
LAB_EPSILON = 216 / 24389
LAB_KAPPA = 24389 / 27
LAB_DELTA = 16 / 116
def rgb_to_lab(rgb):
normalized_rgb = rgb.astype(np.float32) / RGB_MAX_VALUE
linear_rgb = apply_inverse_srgb_gamma(normalized_rgb)
xyz_values = convert_linear_rgb_to_xyz(linear_rgb)
normalized_xyz = normalize_xyz_by_d65_illuminant(xyz_values)
return convert_xyz_to_lab(normalized_xyz)
def apply_inverse_srgb_gamma(normalized_rgb):
above_threshold = normalized_rgb > SRGB_LINEAR_THRESHOLD
linearized_high_values = ((normalized_rgb + SRGB_OFFSET) / SRGB_SCALE) ** SRGB_GAMMA
linearized_low_values = normalized_rgb / SRGB_LINEAR_SCALE
return np.where(above_threshold, linearized_high_values, linearized_low_values)
def convert_linear_rgb_to_xyz(linear_rgb):
srgb_to_xyz_matrix = np.array(
[
[0.4124564, 0.3575761, 0.1804375],
[0.2126729, 0.7151522, 0.0721750],
[0.0193339, 0.1191920, 0.9503041],
],
dtype=np.float32,
)
return linear_rgb @ srgb_to_xyz_matrix.T
def normalize_xyz_by_d65_illuminant(xyz):
normalized = xyz.copy()
normalized[:, 0] /= D65_ILLUMINANT_X
normalized[:, 2] /= D65_ILLUMINANT_Z
return normalized
def convert_xyz_to_lab(normalized_xyz):
above_epsilon = normalized_xyz > LAB_EPSILON
cubic_root_values = normalized_xyz ** (1 / 3)
linear_scaled_values = (LAB_KAPPA * normalized_xyz + 16) / 116
f_transformed = np.where(above_epsilon, cubic_root_values, linear_scaled_values)
lab_values = np.zeros_like(normalized_xyz)
lab_values[:, 0] = 116 * f_transformed[:, 1] - 16
lab_values[:, 1] = 500 * (f_transformed[:, 0] - f_transformed[:, 1])
lab_values[:, 2] = 200 * (f_transformed[:, 1] - f_transformed[:, 2])
return lab_values
def lab_to_rgb(lab):
xyz_values = convert_lab_to_xyz(lab)
linear_rgb = convert_xyz_to_linear_rgb(xyz_values)
normalized_rgb = apply_srgb_gamma(linear_rgb)
return convert_normalized_to_8bit_rgb(normalized_rgb)
def convert_lab_to_xyz(lab):
f_y = (lab[:, 0] + 16) / 116
f_x = lab[:, 1] / 500 + f_y
f_z = f_y - lab[:, 2] / 200
x_above_epsilon = f_x**3 > LAB_EPSILON
y_above_epsilon = lab[:, 0] > LAB_KAPPA * LAB_EPSILON
z_above_epsilon = f_z**3 > LAB_EPSILON
xyz_values = np.zeros((len(lab), 3), dtype=np.float32)
x_cubic = f_x**3
x_linear = (116 * f_x - 16) / LAB_KAPPA
xyz_values[:, 0] = np.where(x_above_epsilon, x_cubic, x_linear) * D65_ILLUMINANT_X
y_cubic = f_y**3
y_linear = lab[:, 0] / LAB_KAPPA
xyz_values[:, 1] = np.where(y_above_epsilon, y_cubic, y_linear)
z_cubic = f_z**3
z_linear = (116 * f_z - 16) / LAB_KAPPA
xyz_values[:, 2] = np.where(z_above_epsilon, z_cubic, z_linear) * D65_ILLUMINANT_Z
return xyz_values
def convert_xyz_to_linear_rgb(xyz):
xyz_to_srgb_matrix = np.array(
[
[3.2404542, -1.5371385, -0.4985314],
[-0.9692660, 1.8760108, 0.0415560],
[0.0556434, -0.2040259, 1.0572252],
],
dtype=np.float32,
)
return xyz @ xyz_to_srgb_matrix.T
def apply_srgb_gamma(linear_rgb):
SRGB_LINEAR_CUTOFF = 0.0031308
above_cutoff = linear_rgb > SRGB_LINEAR_CUTOFF
gamma_corrected_high = SRGB_SCALE * (linear_rgb ** (1 / SRGB_GAMMA)) - SRGB_OFFSET
gamma_corrected_low = SRGB_LINEAR_SCALE * linear_rgb
return np.where(above_cutoff, gamma_corrected_high, gamma_corrected_low)
def convert_normalized_to_8bit_rgb(normalized_rgb):
scaled_values = normalized_rgb * RGB_MAX_VALUE
clipped_values = np.clip(scaled_values, 0, RGB_MAX_VALUE)
return clipped_values.astype(np.uint8)