import enum import math import colorsys from ..nvector import NVector def from_uint8(r, g, b, a=255): return Color(r, g, b, a) / 255 class ColorMode(enum.Enum): ## sRGB, Components in [0, 1] RGB = enum.auto() ## HSV, components in [0, 1] HSV = enum.auto() ## HSL, components in [0, 1] HSL = enum.auto() ## CIE XYZ with Illuminant D65. Components in [0, 1] XYZ = enum.auto() ## CIE L*u*v* LUV = enum.auto() ## CIE Lch(uv), polar version of LUV where C is the radius and H an angle in radians LCH_uv = enum.auto() ## CIE L*a*b* LAB = enum.auto() ## CIE LCh(ab), polar version of LAB where C is the radius and H an angle in radians #LCH_ab = enum.auto() def _clamp(x): return max(0, min(1, x)) class Conversion: _conv_paths = { (ColorMode.RGB, ColorMode.RGB): [], (ColorMode.RGB, ColorMode.HSV): [], (ColorMode.RGB, ColorMode.HSL): [], (ColorMode.RGB, ColorMode.XYZ): [], (ColorMode.RGB, ColorMode.LUV): [ColorMode.XYZ], (ColorMode.RGB, ColorMode.LAB): [ColorMode.XYZ], (ColorMode.RGB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV], #(ColorMode.RGB, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB], (ColorMode.HSV, ColorMode.RGB): [], (ColorMode.HSV, ColorMode.HSV): [], (ColorMode.HSV, ColorMode.HSL): [], (ColorMode.HSV, ColorMode.XYZ): [ColorMode.RGB], (ColorMode.HSV, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ], (ColorMode.HSV, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ], (ColorMode.HSV, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV], #(ColorMode.HSV, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB], (ColorMode.HSL, ColorMode.RGB): [], (ColorMode.HSL, ColorMode.HSV): [], (ColorMode.HSL, ColorMode.HSL): [], (ColorMode.HSL, ColorMode.XYZ): [ColorMode.RGB], (ColorMode.HSL, ColorMode.LUV): [ColorMode.RGB, ColorMode.XYZ], (ColorMode.HSL, ColorMode.LAB): [ColorMode.RGB, ColorMode.XYZ], (ColorMode.HSL, ColorMode.LCH_uv): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LUV], #(ColorMode.HSL, ColorMode.LCH_ab): [ColorMode.RGB, ColorMode.XYZ, ColorMode.LAB], (ColorMode.XYZ, ColorMode.RGB): [], (ColorMode.XYZ, ColorMode.HSV): [ColorMode.RGB], (ColorMode.XYZ, ColorMode.HSL): [ColorMode.RGB], (ColorMode.XYZ, ColorMode.XYZ): [], (ColorMode.XYZ, ColorMode.LUV): [], (ColorMode.XYZ, ColorMode.LAB): [], (ColorMode.XYZ, ColorMode.LCH_uv): [ColorMode.LUV], #(ColorMode.XYZ, ColorMode.LCH_ab): [ColorMode.LAB], (ColorMode.LCH_uv, ColorMode.RGB): [ColorMode.LUV, ColorMode.XYZ], (ColorMode.LCH_uv, ColorMode.HSV): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB], (ColorMode.LCH_uv, ColorMode.HSL): [ColorMode.LUV, ColorMode.XYZ, ColorMode.RGB], (ColorMode.LCH_uv, ColorMode.XYZ): [ColorMode.LUV], (ColorMode.LCH_uv, ColorMode.LUV): [], (ColorMode.LCH_uv, ColorMode.LAB): [ColorMode.LUV, ColorMode.XYZ], (ColorMode.LCH_uv, ColorMode.LCH_uv): [], #(ColorMode.LCH_uv, ColorMode.LCH_ab): [ColorMode.LUV, ColorMode.XYZ, ColorMode.LAB], (ColorMode.LUV, ColorMode.RGB): [ColorMode.XYZ], (ColorMode.LUV, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB], (ColorMode.LUV, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB], (ColorMode.LUV, ColorMode.XYZ): [], (ColorMode.LUV, ColorMode.LUV): [], (ColorMode.LUV, ColorMode.LAB): [ColorMode.XYZ], (ColorMode.LUV, ColorMode.LCH_uv): [], #(ColorMode.LUV, ColorMode.LCH_ab): [ColorMode.XYZ, ColorMode.LAB], (ColorMode.LAB, ColorMode.RGB): [ColorMode.XYZ], (ColorMode.LAB, ColorMode.HSV): [ColorMode.XYZ, ColorMode.RGB], (ColorMode.LAB, ColorMode.HSL): [ColorMode.XYZ, ColorMode.RGB], (ColorMode.LAB, ColorMode.XYZ): [], (ColorMode.LAB, ColorMode.LUV): [ColorMode.XYZ], (ColorMode.LAB, ColorMode.LAB): [], (ColorMode.LAB, ColorMode.LCH_uv): [ColorMode.XYZ, ColorMode.LUV], #(ColorMode.LAB, ColorMode.LCH_ab): [], #(ColorMode.LCH_ab, ColorMode.RGB): [ColorMode.LAB, ColorMode.XYZ], #(ColorMode.LCH_ab, ColorMode.HSV): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB], #(ColorMode.LCH_ab, ColorMode.HSL): [ColorMode.LAB, ColorMode.XYZ, ColorMode.RGB], #(ColorMode.LCH_ab, ColorMode.XYZ): [ColorMode.LAB], #(ColorMode.LCH_ab, ColorMode.LUV): [ColorMode.LAB, ColorMode.XYZ], #(ColorMode.LCH_ab, ColorMode.LAB): [], #(ColorMode.LCH_ab, ColorMode.LCH_uv): [ColorMode.LAB, ColorMode.XYZ, ColorMode.LUV], #(ColorMode.LCH_ab, ColorMode.LCH_ab): [], } @staticmethod def rgb_to_hsv(r, g, b): return colorsys.rgb_to_hsv(r, g, b) @staticmethod def hsv_to_rgb(r, g, b): return colorsys.hsv_to_rgb(r, g, b) @staticmethod def hsl_to_hsv(h, s_hsl, l): v = l + s_hsl * min(l, 1 - l) s_hsv = 0 if v == 0 else 2 - 2 * l / v return (h, s_hsv, v) @staticmethod def hsv_to_hsl(h, s_hsv, v): l = v - v * s_hsv / 2 s_hsl = 0 if l in (0, 1) else (v - l) / min(l, 1 - l) return (h, s_hsl, l) @staticmethod def rgb_to_hsl(r, g, b): h, l, s = colorsys.rgb_to_hls(r, g, b) return (h, s, l) @staticmethod def hsl_to_rgb(h, s, l): return colorsys.hls_to_rgb(h, l, s) # http://w3.uqo.ca/missaoui/Publications/TRColorSpace.zip #@staticmethod #def rgb_to_hcl(r, g, b, gamma=3, y0=100): #maxc = max(r, g, b) #minc = min(r, g, b) #if maxc > 0: #alpha = 1/y0 * minc / maxc #else: #alpha = 0 #q = math.e ** (alpha * gamma) #h = math.atan2(g - b, r - g) #if h < 0: #h += 2*math.pi #h /= 2*math.pi #c = q / 3 * (abs(r-g) + abs(g-b) + abs(b-r)) #l = (q * maxc + (q-1) * minc) / 2 #return (h, c, l) #@staticmethod #def hcl_to_rgb(h, c, l, gamma=3, y0=100): #h *= 2*math.pi #q = math.e ** ((1 - 2*c / 4*l) * gamma / y0) #minc = (4*l - 3*c) / (4*q - 2) #maxc = minc + 3*c / 2*q #if h <= math.pi * 1 / 3: #tan = math.tan(3/2*h) #r = maxc #b = minc #g = (r * tan + b) / (1 + tan) #elif h <= math.pi * 2 / 3: #tan = math.tan(3/4*(h-math.pi)) #g = maxc #b = minc #r = (g * (1+tan) - b) / tan #elif h <= math.pi * 3 / 3: #tan = math.tan(3/4*(h-math.pi)) #g = maxc #r = minc #b = g * (1+tan) - r * tan #elif h <= math.pi * 4 / 3: #tan = math.tan(3/2*(h+math.pi)) #b = maxc #r = minc #g = (r * tan + b) / (1 + tan) #elif h <= math.pi * 5 / 3: #tan = math.tan(3/4*h) #b = maxc #g = minc #r = (g * (1+tan) - b) / tan #else: #tan = math.tan(3/4*h) #r = maxc #g = minc #b = g * (1+tan) - r * tan #return _clamp(r), _clamp(g), _clamp(b) @staticmethod def rgb_to_xyz(r, g, b): def _gamma(v): return v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 rgb = (_gamma(r), _gamma(g), _gamma(b)) matrix = [ [0.4124564, 0.3575761, 0.1804375], [0.2126729, 0.7151522, 0.0721750], [0.0193339, 0.1191920, 0.9503041], ] return tuple( sum(rgb[i] * c for i, c in enumerate(row)) for row in matrix ) @staticmethod def xyz_to_rgb(x, y, z): def _gamma1(v): return _clamp(v * 12.92 if v <= 0.0031308 else v ** (1/2.4) * 1.055 - 0.055) matrix = [ [+3.2404542, -1.5371385, -0.4985314], [-0.9692660, +1.8760108, +0.0415560], [+0.0556434, -0.2040259, +1.0572252], ] xyz = (x, y, z) return tuple(map(_gamma1, ( sum(xyz[i] * c for i, c in enumerate(row)) for row in matrix ))) @staticmethod def xyz_to_luv(x, y, z): u1r = 0.2009 v1r = 0.4610 yr = 100 kap = (29/3)**3 eps = (6/29)**3 try: u1 = 4*x / (x + 15*y + 3*z) v1 = 9*y / (x + 15*y + 3*z) except ZeroDivisionError: return 0, 0, 0 y_r = y/yr l = 166 * y_r ** (1/3) - 16 if y_r > eps else kap * y_r u = 13 * l * (u1 - u1r) v = 13 * l * (v1 - v1r) return l, u, v @staticmethod def luv_to_xyz(l, u, v): u1r = 0.2009 v1r = 0.4610 yr = 100 kap = (29/3)**3 if l == 0: u1 = u1r v1 = v1r else: u1 = u / (13 * l) + u1r v1 = v / (13 * l) + v1r y = yr * l / kap if l <= 8 else yr * ((l + 16) / 116) ** 3 x = y * 9*u1 / (4*v1) z = y * (12 - 3*u1 - 20*v1) / (4*v1) return x, y, z @staticmethod def luv_to_lch_uv(l, u, v): c = math.hypot(u, v) h = math.atan2(v, u) if h < 0: h += math.tau return l, c, h @staticmethod def lch_uv_to_luv(l, c, h): u = math.cos(h) * c v = math.sin(h) * c return l, u, v @staticmethod def xyz_to_lab(x, y, z): # D65 Illuminant aka sRGB(1,1,1) xn = 0.950489 yn = 1 zn = 108.8840 delta = 6 / 29 def f(t): return t ** (1/3) if t > delta ** 3 else t / (3*delta**2) + 4/29 fy = f(y/yn) l = 116 * fy - 16 a = 500 * (f(x/xn) - fy) b = 200 * (fy - f(z/zn)) return l, a, b @staticmethod def lab_to_xyz(l, a, b): # D65 Illuminant aka sRGB(1,1,1) xn = 0.950489 yn = 1 zn = 108.8840 delta = 6 / 29 def f1(t): return t**3 if t > delta else 3*delta**2*(t-4/29) l1 = (l+16) / 116 x = xn * f1(l1+a/500) y = yn * f1(l1) z = zn * f1(l1-b/200) return x, y, z #@staticmethod #def lab_to_lch_ab(l, a, b): #c = math.hypot(a, b) #h = math.atan2(b, a) #if h < 0: #h += math.tau #return l, c, h #@staticmethod #def lch_ab_to_lab(l, c, h): #a = math.cos(h) * c #b = math.sin(h) * c #return l, a, b @staticmethod def conv_func(mode_from, mode_to): return getattr(Conversion, "%s_to_%s" % (mode_from.name.lower(), mode_to.name.lower()), None) @staticmethod def convert(tuple, mode_from, mode_to): if mode_from == mode_to: return tuple if len(tuple) == 4: alpha = tuple[3] tuple = tuple[:3] else: alpha = None func = Conversion.conv_func(mode_from, mode_to) if func: return func(*tuple) if (mode_from, mode_to) in Conversion._conv_paths: steps = Conversion._conv_paths[(mode_from, mode_to)] + [mode_to] for step in steps: func = Conversion.conv_func(mode_from, step) if not func: raise ValueError("Missing definition for conversion from %s to %s" % (mode_from, step)) tuple = func(*tuple) mode_from = step if alpha is not None: tuple += (alpha,) return tuple raise ValueError("No conversion path from %s to %s" % (mode_from, mode_to)) class Color(NVector): Mode = ColorMode def __init__(self, c1=0, c2=0, c3=0, a=1, *, mode=ColorMode.RGB): if isinstance(a, ColorMode): raise TypeError("Please update the Color constructor") super().__init__(c1, c2, c3, a) self._mode = mode @property def mode(self): return self._mode def convert(self, v): if v == self._mode: return self self.components = list(Conversion.convert(self.components, self._mode, v)) self._mode = v return self def clone(self): return Color(*self.components, mode=self._mode) def converted(self, mode): return self.clone().convert(mode) def to_rgb(self): return self.converted(ColorMode.RGB) def __repr__(self): return "<%s %s [%.3f, %.3f, %.3f, %.3f]>" % ( (self.__class__.__name__, self.mode.name) + tuple(self.components) ) def component_names(self): comps = None if self._mode == ColorMode.RGB: comps = ({"r", "red"}, {"g", "green"}, {"b", "blue"}) elif self._mode == ColorMode.HSV: comps = ({"h", "hue"}, {"s", "saturation"}, {"v", "value"}) elif self._mode == ColorMode.HSL: comps = ({"h", "hue"}, {"s", "saturation"}, {"l", "lightness"}) elif self._mode == ColorMode.LCH_uv: # in (ColorMode.LCH_uv, ColorMode.LCH_ab): comps = ({"l", "luma", "luminance"}, {"c", "choma"}, {"h", "hue"}) elif self._mode == ColorMode.XYZ: comps = "xyz" elif self._mode == ColorMode.LUV: comps = "luv" elif self._mode == ColorMode.LAB: comps = "lab" return comps def _attrindex(self, name): comps = self.component_names() if comps: for i, vals in enumerate(comps): if name in vals: return i return None def __getattr__(self, name): if name not in vars(self) and name not in {"_mode", "components"}: i = self._attrindex(name) if i is not None: return self.components[i] raise AttributeError(name) def __setattr__(self, name, value): if name not in vars(self) and name not in {"_mode", "components"}: i = self._attrindex(name) if i is not None: self.components[i] = value return return super().__setattr__(name, value)