Merge pull request #360 from rodrigogiraoserrao/color-palette

Add conversion to/from the CIE-L*ab color space.
This commit is contained in:
Will McGugan
2022-04-06 11:04:12 +01:00
committed by GitHub
2 changed files with 112 additions and 1 deletions

View File

@@ -33,6 +33,14 @@ class HSV(NamedTuple):
v: float
class Lab(NamedTuple):
"""A color in CIE-L*ab format."""
L: float
a: float
b: float
RE_COLOR = re.compile(
r"""^
\#([0-9a-fA-F]{6})$|
@@ -367,6 +375,60 @@ class ColorPair(NamedTuple):
)
def rgb_to_lab(rgb: Color) -> Lab:
"""Convert an RGB color to the CIE-L*ab format.
Uses the standard RGB color space with a D65/2⁰ standard illuminant.
Conversion passes through the XYZ color space.
Cf. http://www.easyrgb.com/en/math.php.
"""
r, g, b = rgb.r / 255, rgb.g / 255, rgb.b / 255
r = pow((r + 0.055) / 1.055, 2.4) if r > 0.04045 else r / 12.92
g = pow((g + 0.055) / 1.055, 2.4) if g > 0.04045 else g / 12.92
b = pow((b + 0.055) / 1.055, 2.4) if b > 0.04045 else b / 12.92
x = (r * 41.24 + g * 35.76 + b * 18.05) / 95.047
y = (r * 21.26 + g * 71.52 + b * 7.22) / 100
z = (r * 1.93 + g * 11.92 + b * 95.05) / 108.883
off = 16 / 116
x = pow(x, 1 / 3) if x > 0.008856 else 7.787 * x + off
y = pow(y, 1 / 3) if y > 0.008856 else 7.787 * y + off
z = pow(z, 1 / 3) if z > 0.008856 else 7.787 * z + off
return Lab(116 * y - 16, 500 * (x - y), 200 * (y - z))
def lab_to_rgb(lab: Lab) -> Color:
"""Convert a CIE-L*ab color to RGB.
Uses the standard RGB color space with a D65/2⁰ standard illuminant.
Conversion passes through the XYZ color space.
Cf. http://www.easyrgb.com/en/math.php.
"""
y = (lab.L + 16) / 116
x = lab.a / 500 + y
z = y - lab.b / 200
off = 16 / 116
y = pow(y, 3) if y > 0.2068930344 else (y - off) / 7.787
x = 0.95047 * pow(x, 3) if x > 0.2068930344 else 0.122059 * (x - off)
z = 1.08883 * pow(z, 3) if z > 0.2068930344 else 0.139827 * (z - off)
r = x * 3.2406 + y * -1.5372 + z * -0.4986
g = x * -0.9689 + y * 1.8758 + z * 0.0415
b = x * 0.0557 + y * -0.2040 + z * 1.0570
r = 1.055 * pow(r, 1 / 2.4) - 0.055 if r > 0.0031308 else 12.92 * r
g = 1.055 * pow(g, 1 / 2.4) - 0.055 if g > 0.0031308 else 12.92 * g
b = 1.055 * pow(b, 1 / 2.4) - 0.055 if b > 0.0031308 else 12.92 * b
return Color(int(r * 255), int(g * 255), int(b * 255))
if __name__ == "__main__":
from rich import print

View File

@@ -3,7 +3,7 @@ import pytest
from rich.color import Color as RichColor
from rich.text import Text
from textual.color import Color, ColorPair
from textual.color import Color, ColorPair, Lab, lab_to_rgb, rgb_to_lab
@pytest.mark.parametrize(
@@ -76,3 +76,52 @@ def test_hls():
assert red.hls == pytest.approx(
(0.9888888888888889, 0.43137254901960786, 0.818181818181818)
)
# Computed with http://www.easyrgb.com/en/convert.php,
# (r, g, b) values in sRGB to (L*, a*, b*) values in CIE-L*ab.
RGB_LAB_DATA = [
(10, 23, 73, 10.245, 15.913, -32.672),
(200, 34, 123, 45.438, 67.750, -8.008),
(0, 0, 0, 0, 0, 0),
(255, 255, 255, 100, 0, 0),
]
@pytest.mark.parametrize(
"r, g, b, L_, a_, b_",
RGB_LAB_DATA,
)
def test_rgb_to_lab(r, g, b, L_, a_, b_):
"""Test conversion from the RGB color space to CIE-L*ab."""
rgb = Color(r, g, b)
lab = rgb_to_lab(rgb)
assert lab.L == pytest.approx(L_, abs=0.1)
assert lab.a == pytest.approx(a_, abs=0.1)
assert lab.b == pytest.approx(b_, abs=0.1)
@pytest.mark.parametrize(
"r, g, b, L_, a_, b_",
RGB_LAB_DATA,
)
def test_lab_to_rgb(r, g, b, L_, a_, b_):
"""Test conversion from the CIE-L*ab color space to RGB."""
lab = Lab(L_, a_, b_)
rgb = lab_to_rgb(lab)
assert rgb.r == pytest.approx(r, abs=1)
assert rgb.g == pytest.approx(g, abs=1)
assert rgb.b == pytest.approx(b, abs=1)
def test_rgb_lab_rgb_roundtrip():
"""Test RGB -> CIE-L*ab -> RGB color conversion roundtripping."""
for r in range(0, 256, 4):
for g in range(0, 256, 4):
for b in range(0, 256, 4):
c_ = lab_to_rgb(rgb_to_lab(Color(r, g, b)))
assert c_.r == pytest.approx(r, abs=1)
assert c_.g == pytest.approx(g, abs=1)
assert c_.b == pytest.approx(b, abs=1)