Source code for rnets.colors.utils

# -*- coding: utf-8 -*-
"""Utils for Color parsing, writing and interpolation.

Attributes:
    Color (type): Type synonym to define a color.

Note:
    This library only works with 3-D colorspaces. Its extension N-D colorspaces
    should be straightforward, however, it has been decided not to include it
    in benefit of a strict definition of Color.
"""
from collections.abc import Callable, Sequence
from colorsys import rgb_to_hls, hls_to_rgb
from itertools import repeat, starmap
from typing import Literal

type Color = tuple[float, float, float]
type ColorSpace = Literal["rgb", "lab", "hsl"]


[docs] def ensure_color( c: Color ) -> bool: """Tests wether a :obj:`Color` is valid or not. Arg: c (:obj:`Color`): Color to be tested. Returns: bool: wether the color is valid or not. Note: :obj:`Color` follows the colorsys representation, meaning that it consists of 3 float values ranging from [0, 1]. """ return all(map( lambda x: 0. <= x <= 1. , c ))
[docs] def rgb_to_hexstr( c: Color , inc_hash: bool = True ) -> str: """Returns the given color as an hexadecimal string of the form #RRGGBB where R, G and B are represented from a number from 0 to 9 and a letter from a (10) to f (15). Args: c (:obj:`Color`): :obj:`Color` to transform into string. inc_hash (bool, optional): If True, include the hash character at the beggining of the color string. Defaults to True. Returns: str: Color string with the RRGGBB form. Note: Note that :obj:`Color` follows the :obj:`colorsys` representation, meaning that it consists of 3 float values ranging from [0, 1]. """ return ('#' * inc_hash + ''.join(starmap( format , zip( map(lambda x: round(x * 255), c) , repeat('02x')) )))
[docs] def hexstr_to_rgb( s: str ) -> Color | None: """Parses a color string of the form #RRGGBB into a :obj:`Color`. Args: s (str): Color string to be parsed. Returns: :obj:`Color`: With the values of the parsed string None: If the conversion fails. Note: Note that :obj:`Color` follows the :obj:`colorsys` representation, meaning that it consists of 3 float values ranging from [0, 1]. """ if len(s) != 7 or s[0] != '#': return None try: # Force only three values in the tuple a, b, c =map( lambda x: float(int("".join(x), 16)) / 255. , zip(s[1::2], s[2::2]) ) return (a, b, c) except ValueError: return None
[docs] def cyclic_conditions( x: float , y: float ) -> tuple[float, float]: """Assuming that the range [0, 1] represent a periodic one-dimensional space, find the representation of the values that minimizes the distance between them. Args: x (float): First value. y (float): Second value. Returns: A tuple of the form (x, y) containing the new representation of the values. """ dist: float = x - y if abs(dist) <= 0.5: return (x, y) if dist < 0: return (x + 1, y) else: return (x, y + 1)
[docs] def interp_c_seq( x: float , cs: Sequence[Color] , cyclic: tuple[bool, bool, bool] = (False, False, False) ) -> Color: """Given a value float x in the range [0., 1.] and a sequence of colors, interpolate a color being represented by x, being 0 the first color in the list and 1. the last. Args: x (float): Floating point number in the range of [0., 1.] representing the position in the color sequence. cs (Sequence of :obj:`Color`): Sequence of colors to be interpolated. cyclic (bool, optional): Index of the components of the color that are cyclic. In a cyclic component, the 0 and 1 values are connected, and thus the algorithm will consider the shortest path. Returns: :obj:`Color`: Interpolation using the :obj:colorsys implementation (3 float values in [0. ,1.]) Note: Note that this function will perform the interpolation withing the color space provided. i. e. if RGB is used, the interpolation will be done within the RGB colorspace. If x overflows on the roof, the last color of the sequence is returned while if x overflows on the floor, the first color of the sequence is returned. """ # Ensure x in [0, 1] if x >= 1.: return cs[-1] if x <= 0.: return cs[0] val: float = (x * (len(cs) - 1)) idx: int = int(val) p: float = val - idx # Force 3 values in the tuple comp = map( lambda c: (c[0] * (1. - p)) + (c[1] * p) , map( lambda xs: cyclic_conditions(*xs[:2]) if xs[2] else tuple(xs[:2]) , zip(cs[idx], cs[idx+1], cyclic) ) ) a, b, c = map( lambda xs: xs[0] - int(xs[0]) if xs[1] else xs[0] , zip(comp, cyclic) ) return (a, b, c)
[docs] def rgb_achromatize( c: Color ) -> Color: """Achromatize the given RGB color. Args: c (:obj:`Color`): Color that will be achromatized. Returns: :obj:`Color` Achromatized color. Note: Formula taken from the pillow library: https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.convert """ Y: float = ( c[0] * 0.299 + c[1] * 0.587 + c[2] * 0.114 ) return (Y, Y, Y)
[docs] def interp_fn( cs: Sequence ) -> Callable[[float], Color]: """Given a sequence of :obj:`Color`, returns an interpolated function based on it. Args: cs (Sequence of :obj:`Color`): Sequence of colors to be interpolated. Returns: :obj:Callable[[float], :obj:`Color`]: A function that takes a float in the range [0., 1.] as an argument and returns the interpolated color from the given Sequence of :obj:`Color`. Note: This function acts as a wrapper of :obj:`interp`. """ def fn(x: float) -> Color: return interp_c_seq(x, cs) return fn
[docs] def calc_relative_luminance( c: Color ) -> float: """Calculate the relative luminance of a :obj:`Color` in the RGB space. Args: c (:obj:`Color`): Color in the RGB colorspace to compute its luminance. Returns: float: Computed luminance. References: https://www.w3.org/WAI/GL/wiki/Relative_luminance """ return ( 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] )
[docs] def color_sel_lum( c1: Color , c2: Color , dc: Color , threshold: float = 0.5 ) -> Color: """Given a pair of colors, select a color from it based on the relative luminance of a third one. Args: c1 (:obj:`Color`): Color to select if the luminance is below the threshold. c2 (:obj:`Color`): Color to select if the luminance is above the threshold. dc (:obj:`Color`): Color to use to compute the relative luminance. threshold (float, optional): Threshold to make the decision. Ideally, a value between 0. and 1. Defaults to 0.5. Returns: :obj:`Color`: c1 or c2. """ return (c1, c2)[calc_relative_luminance(dc) < threshold]
[docs] def interp_fn_cspace( cs: Sequence[Color] , target: Callable[[float, float, float], Color] = lambda a, b, c: (a,b,c) , origin: Callable[[float, float, float], Color] = lambda a, b, c: (a,b,c) , cyclic: tuple[bool, bool, bool] = (False, False, False) ) -> Callable[[float], Color]: """Given a sequence of :obj:`Color` in an arbitrary colorspace, convert them to another space and create an interpolation function. The value returned by the interpolation function will be converted again to the original colorspace. Args: cs (Sequence of `Color`): Sequence of colors in the RGB to be converted HSL and interpolated. target (function of taking three floats as input and returning a :obj:`Color`, optional): Function that projects the given color components to the colorspace in which the interpolation will be performed. Defaults to the identity function. origin (function of taking three floats as input and returning a :obj:`Color`, optional): Function that returns the interpolated color to the original colorspace. Defaults to the identity function. cyclic (tuple of three bools, optional): During the interpolation, treat the dimension marked as True as cyclic. Defaults to (False, False, False) Returns: :obj:Callable[[float], :obj:`Color`]: A function that takes a float in the range [0., 1.] as an argument and returns the interpolated color from the given Sequence of :obj:`Color` using the specified colorspaces. Note: The :obj:`origin` and :obj:`target` take 3 floats as input for to ease the integration with the colorsys library. """ cs_hls: Sequence[Color] = tuple(starmap(target, cs)) def fn(x: float) -> Color: return origin(*interp_c_seq( x , cs_hls , cyclic=cyclic)) return fn
[docs] def interp_fn_rgb_hls( cs: Sequence[Color] ) -> Callable[[float], Color]: """Given a sequence of :obj:`Color` representing the RGB colorspace, convert them to the CIEL*ab space and create an interpolation function. The value returned by the interpolation function will be converted again to RGB. Args: cs (Sequence of `Color`): Sequence of colors in the RGB to be converted CIEL*ab and interpolated. Returns: :obj:Callable[[float], :obj:Color]: A function that takes a float in the range [0., 1.] as an argument and returns the CIEL*ab interpolated color from the given Sequence of RGB :obj:`Color`. """ return interp_fn_cspace( cs , target=rgb_to_hls , origin=hls_to_rgb , cyclic=(True, False, False))
[docs] def adjust_range( x: float ) -> float: """Given an float, adjust it to the [0., 1.] interval, making underflow values 0 and overflow values 1. Args: x (float): Value to adjust. Returns: Adjusted float value. """ if x < 0.: return 0. if x > 1.: return 1. else: return x
[docs] def interp_fn_rgb_lab( cs: Sequence[Color] ) -> Callable[[float], Color]: """Given a sequence of :obj:`Color` representing the RGB colorspace, convert them to the HSL space and create an interpolation function. The value returned by the interpolation function will be converted again to rgb. Args: cs (Sequence of `Color`): Sequence of colors in the RGB to be converted HSL and interpolated. Returns: :obj:Callable[[float], :obj:Color]: A function that takes a float in the range [0., 1.] as an argument and returns the HSL interpolated color from the given Sequence of RGB :obj:`Color`. Note: D65 illuminant. Gamma correction assuming sRGB. Sometimes the obtained color components are negative numbers near to 0. To assure that the colors are within the [0., 1.] interval, :func:`adjust_range` is applied to the output values. References: CIE Colorimetry 15 (Third ed.). CIE. 2004. ISBN 3-901-906-33-9. """ def t( r: float , g: float , b: float ) -> Color: l, a, b = xyz_to_lab(*rgb_to_xyz(*map( lambda x: apply_gamma(x, None, 1) , (r,g,b)))) return (l, a, b) def o( l: float , a: float , b: float ) -> Color: r, g, b = map( lambda x: adjust_range(unapply_gamma(x, None, 1)) , xyz_to_rgb(*lab_to_xyz(l, a, b))) return (r, g, b) return interp_fn_cspace( cs , target=t , origin=o , cyclic=(False, False, False))
[docs] def apply_gamma( x: float , g: float | None = None , A: float = 1. ) -> float: """Apply gamma to color component. Args: x (float): Component to which the Gamma will be applied. g (float, optional): Gamma value to apply. If None, assume sRGB gamma. Defaults to None. A (float, optional): A value for the gamma function. In most cases 1. Defaults to 1. Returns: Component with the new Gamma value. Note: The input and the output value are assumed to be a float within the [0, 1] interval. """ if g is None: return x/12.92 if x < 0.04045 else ((x+0.055)/1.055)**2.4 else: return x**g
[docs] def unapply_gamma( x: float , g: float | None = None , A: float = 1. ) -> float: """Unapply gamma to color component. Args: x (float): Component to which the Gamma will be unaapplied. g (float, optional): Gamma value to apply. If None, assume sRGB gamma. Defaults to None. A (float, optional): A value for the gamma function. In most cases 1. Defaults to 1. Returns: Ungamma component. Note: The input and the output value are assumed to be a float within the [0, 1] interval. """ if g is None: return x*12.92 if x <= 0.0031308 else 1.055*(x**((1./2.4))-0.055) else: return x**(1./g)
[docs] def rgb_to_xyz( r: float , g: float , b: float ) -> Color: """Projects the given RGB :obj:`Color` value to the L*a*b* colorspace. Args: r, g, b (float): Floating point number in the interval [0, 1] representing the red, green and blue components respectively. Returns: Projected :obj:`Color`. Note: Obeserver. = 2, Illuminant = D65 References: Smith, Thomas; Guild, John (1931–32). "The C.I.E. colorimetric standards and their use". Transactions of the Optical Society. 33 (3): 73–134. DOI 10.1088/1475-4878/33/3/301 """ return ( r * 0.4124 + g * 0.3576 + b * 0.1805 # X , r * 0.2126 + g * 0.7152 + b * 0.0722 # Y , r * 0.0193 + g * 0.1192 + b * 0.9505 # Z )
[docs] def xyz_to_rgb( x: float , y: float , z: float ) -> Color: """Projects the given xyz :obj:`Color` value to the rgb colorspace. Args: x, y, z (float): Floating point number in the interval [0, 1] representing the X, Y and Z components respectively. Returns: Projected :obj:`Color`. Note: Obeserver. = 2, Illuminant = D65 References: Smith, Thomas; Guild, John (1931–32). "The C.I.E. colorimetric standards and their use". Transactions of the Optical Society. 33 (3): 73–134. DOI 10.1088/1475-4878/33/3/301 """ return ( x * 3.2406 + y * -1.5372 + z * -0.4968 # R , x * -0.9689 + y * 1.8758 + z * 0.0415 # G , x * 0.0557 + y * -0.2040 + z * 1.0570 # B )
[docs] def xyz_to_lab( x: float , y: float , z: float ) -> Color: """Projects the given XYZ :obj:`Color` value to the L*a*b* colorspace. Args: x, y, z (float): Floating point number in the interval [0, 1] representing the X, Y, Z components respectively. Returns: Projected :obj:`Color`. References: Smith, Thomas; Guild, John (1931–32). "The C.I.E. colorimetric standards and their use". Transactions of the Optical Society. 33 (3): 73–134. DOI 10.1088/1475-4878/33/3/301 CIE Colorimetry 15 (Third ed.). CIE. 2004. ISBN 3-901-906-33-9. Note: Adapted from OpenCV. """ x = x / 0.950456 z = z / 1.088754 f = lambda t: t**(1./3.) if t > 0.008856 else 7.787*t + 16/116 return ( (116.*(y**(1./3.)))-16. if y > 0.008856 else 903.3*y # L , 500.*(f(x) - f(y)) # a , 200.*(f(y) - f(z)) # b )
[docs] def lab_to_xyz( l: float , a: float , b: float ) -> Color: """Projects the given L*a*b :obj:`Color` value to the XYZ colorspace. Args: l, a, b (float): Floating point number in the interval [0, 1] representing the L, a, b components respectively. Returns: Projected :obj:`Color`. References: Smith, Thomas; Guild, John (1931–32). "The C.I.E. colorimetric standards and their use". Transactions of the Optical Society. 33 (3): 73–134. DOI 10.1088/1475-4878/33/3/301 CIE Colorimetry 15 (Third ed.). CIE. 2004. ISBN 3-901-906-33-9. Note: Adapted from OpenCV. """ y = (l+16.)/116. x = (a/500.)+y z = y-(b/200.) f = lambda t: t**3 if t > 0.008856 else (t-16/116)/7.787 return( f(x) * 0.950456 # X , f(y) # Y , f(z) * 1.088754 # Z )
[docs] def interp_cs( cs: Sequence[Color] , interp: ColorSpace = "lab" ) -> Callable[[float], Color]: """Given a sequence of :obj:`Color` in an arbitrary colorspace, convert them to another space and create an interpolation function. The value returned by the interpolation function will be converted again to the original colorspace. Args: cs (Sequence of `Color`): Sequence of colors in the RGB to be converted HSL and interpolated. interp (Literal: rgb, lab or hsl, optional): The colorspace in which the interpolation will be perform. Returns: :obj:Callable[[float], :obj:`Color`]: A function that takes a float in the range [0., 1.] as an argument and returns the interpolated color from the given Sequence of :obj:`Color` using the specified colorspaces. Note: Wrapper for :obj:`interp_fn_cspace`. """ match interp: case "rgb": return lambda x: interp_c_seq(x, cs, (False, False, False)) case "lab": return interp_fn_rgb_lab(cs) case "hsl": return interp_fn_rgb_hls(cs)