File size: 3,878 Bytes
346b70f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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)