Skip to content

Instantly share code, notes, and snippets.

@facelessuser
Last active September 15, 2021 18:12
Show Gist options
  • Save facelessuser/f4a154e4d74f17ae7049f9e548ab09ea to your computer and use it in GitHub Desktop.
Save facelessuser/f4a154e4d74f17ae7049f9e548ab09ea to your computer and use it in GitHub Desktop.
Luv and Lchuv for Coloraide
"""
Luv and Lch class.
https://en.wikipedia.org/wiki/CIELUV
"""
from coloraide.spaces import Space, RE_DEFAULT_MATCH, GamutUnbound, Percent, WHITES, Cylindrical, Angle
from coloraide.spaces.xyz import XYZ
from coloraide import util
from coloraide import Color as ColorOrig
import copy
import re
import math
ACHROMATIC_THRESHOLD = 0.000000000002
def xyz_to_luv(xyz, white):
"""XYZ to Luv."""
u, v = util.xyz_to_uv(xyz)
wp = util.xy_to_xyz(*WHITES[white])
un, vn = util.xyz_to_uv(wp)
y = xyz[1] / wp[1]
l = 116 * util.nth_root(y, 3) - 16 if y > ((6 / 29) ** 3) else ((29 / 3) ** 3) * y
return [
l,
13 * l * (u - un),
13 * l * (v - vn),
]
def luv_to_xyz(luv, white):
"""Luv to XYZ."""
l, u, v = luv
wp = util.xy_to_xyz(*WHITES[white])
un, vn = util.xyz_to_uv(wp)
if l != 0:
up = (u / ( 13 * l)) + un
vp = (v / ( 13 * l)) + vn
else:
up = vp = 0
y = wp[1] * ((l + 16) / 116) ** 3 if l > 8 else wp[1] * l * ((3 / 29) ** 3)
if vp != 0:
x = y * ((9 * up) / (4 * vp))
z = y * ((12 - 3 * up - 20 * vp) / (4 * vp))
else:
x = z = 0
return [x, y, z]
class Luv(Space):
"""Oklab class."""
SPACE = "luv"
SERIALIZE = ("--luv",)
CHANNEL_NAMES = ("lightness", "u", "v", "alpha")
DEFAULT_MATCH = re.compile(RE_DEFAULT_MATCH.format(color_space='|'.join(SERIALIZE), channels=3))
WHITE = "D65"
RANGE = (
GamutUnbound([Percent(0), Percent(100.0)]),
GamutUnbound([-175.0, 175.0]),
GamutUnbound([-175.0, 175.0])
)
@property
def lightness(self):
"""L channel."""
return self._coords[0]
@lightness.setter
def lightness(self, value):
"""Get true luminance."""
self._coords[0] = self._handle_input(value)
@property
def u(self):
"""U channel."""
return self._coords[1]
@u.setter
def u(self, value):
"""U axis."""
self._coords[1] = self._handle_input(value)
@property
def v(self):
"""V channel."""
return self._coords[2]
@v.setter
def v(self, value):
"""V axis."""
self._coords[2] = self._handle_input(value)
@classmethod
def _to_xyz(cls, parent, luv):
"""To XYZ."""
return parent.chromatic_adaptation(cls.WHITE, XYZ.WHITE, luv_to_xyz(luv, cls.WHITE))
@classmethod
def _from_xyz(cls, parent, xyz):
"""From XYZ."""
return xyz_to_luv(parent.chromatic_adaptation(XYZ.WHITE, cls.WHITE, xyz), cls.WHITE)
def luv_to_lchuv(luv):
"""Luv to Lch(uv)."""
l, u, v = luv
c = math.sqrt(u ** 2 + v ** 2)
h = math.degrees(math.atan2(v, u))
# Achromatic colors will often get extremely close, but not quite hit zero.
# Essentially, we want to discard noise through rounding and such.
if c < ACHROMATIC_THRESHOLD:
h = util.NaN
return [l, c, util.constrain_hue(h)]
def lchuv_to_luv(lchuv):
"""Lch(uv) to Luv."""
l, c, h = lchuv
h = util.no_nan(h)
# If, for whatever reason (mainly direct user input),
# if chroma is less than zero, clamp to zero.
if c < 0.0:
c = 0.0
return (
l,
c * math.cos(math.radians(h)),
c * math.sin(math.radians(h))
)
class Lchuv(Cylindrical, Space):
"""Lch(uv) class."""
SPACE = "lchuv"
SERIALIZE = ("--lchuv",)
CHANNEL_NAMES = ("lightness", "chroma", "hue", "alpha")
DEFAULT_MATCH = re.compile(RE_DEFAULT_MATCH.format(color_space='|'.join(SERIALIZE), channels=3))
WHITE = "D65"
RANGE = (
GamutUnbound([Percent(0), Percent(100.0)]),
GamutUnbound([0.0, 176.0]),
GamutUnbound([Angle(0.0), Angle(360.0)]),
)
@property
def lightness(self):
"""Lightness."""
return self._coords[0]
@lightness.setter
def lightness(self, value):
"""Get true luminance."""
self._coords[0] = self._handle_input(value)
@property
def chroma(self):
"""Chroma."""
return self._coords[1]
@chroma.setter
def chroma(self, value):
"""chroma."""
self._coords[1] = self._handle_input(value)
@property
def hue(self):
"""Hue."""
return self._coords[2]
@hue.setter
def hue(self, value):
"""Shift the hue."""
self._coords[2] = self._handle_input(value)
@classmethod
def null_adjust(cls, coords, alpha):
"""On color update."""
if coords[1] < ACHROMATIC_THRESHOLD:
coords[2] = util.NaN
return coords, alpha
@classmethod
def _to_luv(cls, parent, lchuv):
"""To Luv."""
return lchuv_to_luv(lchuv)
@classmethod
def _from_luv(cls, parent, luv):
"""To Luv."""
return luv_to_lchuv(luv)
@classmethod
def _to_xyz(cls, parent, lchuv):
"""To XYZ."""
return Luv._to_xyz(parent, cls._to_luv(parent, lchuv))
@classmethod
def _from_xyz(cls, parent, xyz):
"""From XYZ."""
return cls._from_luv(parent, Luv._from_xyz(parent, xyz))
class Color(ColorOrig):
"""Color with Luv."""
CS_MAP = copy.copy(ColorOrig.CS_MAP)
CS_MAP["luv"] = Luv
CS_MAP["lchuv"] = Lchuv
Color("blue").interpolate(
"white",
space='lab'
)
Color("blue").interpolate(
"white",
space='luv'
)
Color("blue").interpolate(
"white",
space='jzazbz'
)
Color("blue").interpolate(
"white",
space='oklab'
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment