Don’t know if this is the right place to ask. Anyway I am a programming enthusiast and this is my Stack Overflow profile, and my Code Review profile.
I am extremely intelligent and very experienced in Python and currently learning C++. I want to convert from RGB to LCh and back, as a programming challenge. I want to do this without using any color conversion libraries, and I want to convert BGR float arrays to LCh arrays. The values in my input arrays are scaled down to 0 to 1, and I want the output values to be the same.
I have written Python implementations to convert BGR to HSV/HSL and back using information from Wikipedia:
import numba as nb
import numpy as np
from typing import Callable, Tuple
@nb.njit(cache=True, fastmath=True)
def extrema(a: float, b: float, c: float) -> Tuple[float]:
i = 2
if b > c:
b, c = c, b
i = 1
if a > b:
a, b = b, a
if b > c:
b, c = c, b
i = 0
return i, a, c
@nb.njit(cache=True, fastmath=True)
def hue(b: float, g: float, r: float, d: float, i: float) -> float:
if i == 2:
h = (g - b) / (6 * d)
elif i:
h = 1 / 3 + (b - r) / (6 * d)
else:
h = 2 / 3 + (r - g) / (6 * d)
return h % 1
@nb.njit(cache=True, fastmath=True)
def HSL_pixel(
b: float, g: float, r: float, i: float, x: float, z: float
) -> Tuple[float]:
s = x + z
d = z - x
avg = s / 2
return (hue(b, g, r, d, i), d / (1 - abs(s - 1)), avg) if d else (0, 0, avg)
@nb.njit(cache=True, fastmath=True)
def HSV_pixel(
b: float, g: float, r: float, i: float, x: float, z: float
) -> Tuple[float]:
d = z - x
return (hue(b, g, r, d, i), d / z, z) if d else (0, 0, z)
@nb.njit(cache=True, parallel=True)
def from_BGR(img: np.ndarray, mode: Callable) -> np.ndarray:
height, width = img.shape[:2]
out = np.empty_like(img)
for y in nb.prange(height):
for x in nb.prange(width):
b, g, r = img[y, x]
i, a, c = extrema(b, g, r)
out[y, x] = mode(b, g, r, i, a, c)
return out
@nb.njit
def BGR_to_HSL(img: np.ndarray) -> np.ndarray:
return from_BGR(img, HSL_pixel)
@nb.njit
def BGR_to_HSV(img: np.ndarray) -> np.ndarray:
return from_BGR(img, HSV_pixel)
@nb.njit(cache=True, parallel=True)
def to_BGR(val: np.ndarray, mode: Callable) -> np.ndarray:
height, width = val.shape[:2]
img = np.zeros_like(val)
for y in nb.prange(height):
for x in nb.prange(width):
a, b, c = val[y, x]
img[y, x] = mode(a, b, c)
return img
@nb.njit(cache=True, fastmath=True)
def HSL_helper(h: float, n: float) -> float:
k = (n + 12 * h) % 12
return max(-1, min(k - 3, 9 - k, 1))
@nb.njit(cache=True, fastmath=True)
def BGR_from_HSL_pixel(h: float, s: float, l: float):
a = s * min(l, 1 - l)
return l - a * HSL_helper(h, 4), l - a * HSL_helper(h, 8), l - a * HSL_helper(h, 0)
@nb.njit(cache=True, fastmath=True)
def HSV_helper(h: float, n: float) -> float:
k = (n + 6 * h) % 6
return max(0, min(k, 4 - k, 1))
@nb.njit(cache=True, fastmath=True)
def BGR_from_HSV_pixel(h: float, s: float, v: float):
a = v * s
return v - a * HSV_helper(h, 1), v - a * HSV_helper(h, 3), v - a * HSV_helper(h, 5)
@nb.njit(cache=True)
def HSL_short(h: float, s: float, l: float) -> Tuple[float]:
return BGR_from_HSL_pixel(h, s, l) if s else (l, l, l)
@nb.njit(cache=True)
def HSV_short(h: float, s: float, v: float) -> Tuple[float]:
return BGR_from_HSV_pixel(h, s, v) if s else (v, v, v)
@nb.njit
def HSL_to_BGR(hsl: np.ndarray) -> np.ndarray:
return to_BGR(hsl, HSL_short)
@nb.njit
def HSV_to_BGR(hsv: np.ndarray) -> np.ndarray:
return to_BGR(hsv, HSV_short)
But I have failed to find the formula GIMP uses for the conversion. I learned that there are many specifications of LCh and I want the output of my code to agree with GIMP, because I use GIMP.
I have even downloaded the source code of GIMP and tried to find the code for conversion by searching the files, but there are way too many files and I failed to find the code for conversion.
Here is what I have managed so far:
XYZ_MAT = np.array(
[[0.4124, 0.3576, 0.1805], [0.2126, 0.7152, 0.0722], [0.0193, 0.1192, 0.9505]],
dtype=float,
)
@nb.njit(cache=True, fastmath=True)
def gamma_expand(c: float) -> float:
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
@nb.njit(cache=True, fastmath=True)
def BGR_to_XYZ(bgr: np.ndarray) -> np.ndarray:
xyz = np.zeros(3)
rgb = xyz.copy()
s = 0
for i in (2, 1, 0):
c = gamma_expand(bgr[i])
j = 2 - i
s += XYZ_MAT[0, j] * c
rgb[j] = c
xyz[0] = s
xyz[1] = (XYZ_MAT[1] * rgb).sum()
xyz[2] = (XYZ_MAT[2] * rgb).sum()
return xyz
I used D65 matrix to convert BGR to XYZ, without rescaling XYZ values. I read that GIMP uses D50 in this post, so I may be using the wrong matrix. What formula exactly does GIMP use for the conversion?