1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
//! This module implements the CIELUV color specification, which was adopted concurrently with
//! CIELAB. CIELUV is very similar to CIELAB, but with the difference that u and v are roughly
//! equivalent to red and green and luminance is then used to calculate the blue part.
use color::{Color, XYZColor};
use coord::Coord;
use illuminants::Illuminant;
/// A similar color system to CIELAB, adapted at the same time and with similar goals. It attempts to
/// be an easy-to-convert color space from XYZ that approaches perceptual uniformity. U and V
/// represent chromaticity and roughly equate to CIELAB's A and B, but they're scaled differently and
/// act slightly differently. These coordinates are often referred to as the CIE 1976 UCS (uniform
/// chromaticity scale) diagram, and they're good descriptors of chromaticity.
/// # Example
///
/// ```
/// # use scarlet::prelude::*;
/// # use scarlet::colors::{CIELUVColor};
/// # use scarlet::color::XYZColor;
/// // D50 is the implied illuminant and white point
/// let white: CIELUVColor = XYZColor::white_point(Illuminant::D50).convert();
/// assert_eq!(white.l, 100.);
/// assert_eq!(white.u, 0.);
/// assert_eq!(white.v, 0.);
/// ```
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct CIELUVColor {
/// The luminance component of LUV. Ranges from 0 to 100 by definition.
pub l: f64,
/// The component of LUV that roughly equates to how red the color is vs. how green it is. Ranges
/// from 0 to 100 in most visible colors, where 0 is bright green and 100 is bright red.
pub u: f64,
/// The component of LUV that roughly equates to how yellow vs. blue the color is. Ranges from 0 to
/// 100 in most visible colors, where 0 is bright blue and 100 is bright yellow.
pub v: f64,
}
impl Color for CIELUVColor {
/// Given an XYZ color, gets a new CIELUV color. This is CIELUV D50, so anything else is
/// chromatically adapted before conversion.
fn from_xyz(xyz: XYZColor) -> CIELUVColor {
// this is not bad: LUV is meant to be easy from XYZ
// https://en.wikipedia.org/wiki/CIELUV
// do u and v chromaticity conversions on whitepoint and on given color
// because cieluv chromatic adaptation sucks, use the good one
let xyz_c = xyz.color_adapt(Illuminant::D50);
let wp = XYZColor::white_point(Illuminant::D50);
let denom = |color: XYZColor| color.x + 15.0 * color.y + 3.0 * color.z;
let u_func = |color: XYZColor| 4.0 * color.x / denom(color);
let v_func = |color: XYZColor| 9.0 * color.y / denom(color);
let u_prime_n = u_func(wp);
let v_prime_n = v_func(wp);
let u_prime = u_func(xyz_c);
let v_prime = v_func(xyz_c);
let delta: f64 = 6.0 / 29.0; // like CIELAB
// technically this next division should do nothing: idk if it gets factored out at compile
// time, but it's just insurance if someone ever decides not to normalize whitepoints to Y=1
let y_scaled = xyz_c.y / wp.y; // ranges from 0-1
let l = if y_scaled <= delta.powf(3.0) {
(2.0 / delta).powf(3.0) * y_scaled
} else {
116.0 * y_scaled.powf(1.0 / 3.0) - 16.0
};
let u = 13.0 * l * (u_prime - u_prime_n);
let v = 13.0 * l * (v_prime - v_prime_n);
CIELUVColor { l, u, v }
}
/// Returns a new `XYZColor` that matches the given color. Note that Scarlet uses CIELUV D50 to
/// get around compatibility issues, so any other illuminant will be chromatically adapted after
/// initial conversion (using the `color_adapt()` function).
fn to_xyz(&self, illuminant: Illuminant) -> XYZColor {
// https://en.wikipedia.org/wiki/CIELUV literally has the equations in order
// pretty straightforward
let wp = XYZColor::white_point(Illuminant::D50);
let denom = |color: XYZColor| color.x + 15.0 * color.y + 3.0 * color.z;
let u_func = |color: XYZColor| 4.0 * color.x / denom(color);
let v_func = |color: XYZColor| 9.0 * color.y / denom(color);
let u_prime_n = u_func(wp);
let v_prime_n = v_func(wp);
let u_prime = self.u / (13.0 * self.l) + u_prime_n;
let v_prime = self.v / (13.0 * self.l) + v_prime_n;
let delta: f64 = 6.0 / 29.0;
let y = if self.l <= 8.0 {
wp.y * self.l * (delta / 2.0).powf(3.0)
} else {
wp.y * ((self.l + 16.0) / 116.0).powf(3.0)
};
let x = y * 9.0 * u_prime / (4.0 * v_prime);
let z = y * (12.0 - 3.0 * u_prime - 20.0 * v_prime) / (4.0 * v_prime);
XYZColor {
x,
y,
z,
illuminant: Illuminant::D50,
}
.color_adapt(illuminant)
}
}
impl From<Coord> for CIELUVColor {
fn from(c: Coord) -> CIELUVColor {
CIELUVColor {
l: c.x,
u: c.y,
v: c.z,
}
}
}
impl From<CIELUVColor> for Coord {
fn from(val: CIELUVColor) -> Self {
Coord {
x: val.l,
y: val.u,
z: val.v,
}
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
use consts::TEST_PRECISION;
#[test]
fn test_cieluv_xyz_conversion_d50() {
let xyz = XYZColor {
x: 0.3,
y: 0.53,
z: 0.65,
illuminant: Illuminant::D50,
};
let luv: CIELUVColor = xyz.convert();
let xyz2: XYZColor = luv.convert();
assert!(xyz2.approx_equal(&xyz));
assert!(xyz.distance(&xyz2) <= TEST_PRECISION);
}
#[test]
fn test_cieluv_xyz_conversion_d65() {
let xyz = XYZColor {
x: 0.3,
y: 0.53,
z: 0.65,
illuminant: Illuminant::D65,
};
let luv: CIELUVColor = xyz.convert();
let xyz2: XYZColor = luv.convert();
assert!(xyz2.approx_visually_equal(&xyz));
assert!(xyz.distance(&xyz2) <= TEST_PRECISION);
}
}