1 // Written in the D programming language. 2 3 /** 4 This module implements the $(LINK2 https://en.wikipedia.org/wiki/Lab_color_space, CIE Lab) and 5 $(LINK2 https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:__CIELCh_or_CIEHLC, LCh) _color types. 6 7 Lab is full-spectrum, absolute, vector _color space, with the specific goal of human perceptual uniformity. 8 9 Authors: Manu Evans 10 Copyright: Copyright (c) 2015, Manu Evans. 11 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0) 12 Source: $(PHOBOSSRC std/experimental/color/_lab.d) 13 */ 14 module std.experimental.color.lab; 15 16 import std.experimental.color; 17 import std.experimental.color.xyz : XYZ, isXYZ; 18 import std.experimental.color.colorspace : WhitePoint; 19 20 import std.traits: isFloatingPoint, Unqual; 21 import std.typetuple: TypeTuple; 22 import std.math : sin, cos, sqrt, atan2, PI, M_1_PI; 23 24 25 @safe: pure: nothrow: @nogc: 26 27 /** 28 Detect whether $(D_INLINECODE T) is a L*a*b* color. 29 */ 30 enum isLab(T) = isInstanceOf!(Lab, T); 31 32 /** 33 Detect whether $(D_INLINECODE T) is an L*C*h° color. 34 */ 35 enum isLCh(T) = isInstanceOf!(LCh, T); 36 37 38 /** 39 A CIE L*a*b* color, parameterised for component type and white point. 40 41 Lab is a color space that describes all colors visible to the human eye and was created to serve as a device-independent model to be used as a reference. 42 L* represents the lightness of the color; L* = 0 yields black and L* = 100 indicates diffuse white, specular white may be higher. 43 a* represents the position between red/magenta and green; negative values indicate green while positive values indicate magenta. 44 b* represents the position between yellow and blue; negative values indicate blue and positive values indicate yellow. 45 46 Lab is often found using default white point D50, but it is also common to use D65 when interacting with sRGB images. 47 */ 48 struct Lab(F = float, alias whitePoint_ = (WhitePoint!F.D50)) if (isFloatingPoint!F) 49 { 50 @safe: pure: nothrow: @nogc: 51 52 /** Type of the color components. */ 53 alias ComponentType = F; 54 55 /** The color components that were specified. */ 56 enum whitePoint = whitePoint_; 57 58 /** L* (lightness) component. */ 59 F L = 0; 60 /** a* component. Negative values indicate green, positive values indicate magenta. */ 61 F a = 0; 62 /** b* component. Negative values indicate blue, positive values indicate yellow. */ 63 F b = 0; 64 65 /** Construct a color from L*a*b* values. */ 66 this(F L, F a, F b) 67 { 68 this.L = L; 69 this.a = a; 70 this.b = b; 71 } 72 73 /** Returns the perceptual distance between the specifies colors. */ 74 F perceptualDistance(G)(Lab!G c) const 75 { 76 alias WT = WorkingType!(F, G); 77 return sqrt((WT(c.L) - WT(L))^^2 + (WT(c.a) - WT(a))^^2 + (WT(c.b) - WT(b))^^2); 78 } 79 80 /** 81 Cast to other color types. 82 83 This cast is a convenience which simply forwards the call to convertColor. 84 */ 85 Color opCast(Color)() const if (isColor!Color) 86 { 87 return convertColor!Color(this); 88 } 89 90 /** Unary operators. */ 91 typeof(this) opUnary(string op)() const if (op == "+" || op == "-" || (op == "~" && is(ComponentType == NormalizedInt!U, U))) 92 { 93 Unqual!(typeof(this)) res = this; 94 foreach (c; AllComponents) 95 mixin(ComponentExpression!("res._ = #_;", c, op)); 96 return res; 97 } 98 /** Binary operators. */ 99 typeof(this) opBinary(string op)(typeof(this) rh) const if (op == "+" || op == "-" || op == "*") 100 { 101 Unqual!(typeof(this)) res = this; 102 foreach (c; AllComponents) 103 mixin(ComponentExpression!("res._ #= rh._;", c, op)); 104 return res; 105 } 106 /** Binary operators. */ 107 typeof(this) opBinary(string op, S)(S rh) const if (isColorScalarType!S && (op == "*" || op == "/" || op == "%" || op == "^^")) 108 { 109 Unqual!(typeof(this)) res = this; 110 foreach (c; AllComponents) 111 mixin(ComponentExpression!("res._ #= rh;", c, op)); 112 return res; 113 } 114 /** Binary assignment operators. */ 115 ref typeof(this) opOpAssign(string op)(typeof(this) rh) if (op == "+" || op == "-" || op == "*") 116 { 117 foreach (c; AllComponents) 118 mixin(ComponentExpression!("_ #= rh._;", c, op)); 119 return this; 120 } 121 /** Binary assignment operators. */ 122 ref typeof(this) opOpAssign(string op, S)(S rh) if (isColorScalarType!S && (op == "*" || op == "/" || op == "%" || op == "^^")) 123 { 124 foreach (c; AllComponents) 125 mixin(ComponentExpression!("_ #= rh;", c, op)); 126 return this; 127 } 128 129 package: 130 131 alias ParentColor = XYZ!ComponentType; 132 133 static To convertColorImpl(To, From)(From color) if (isLab!From && isLab!To) 134 { 135 static if (From.whitePoint == To.whitePoint) 136 { 137 // same whitepoint, just a format conversion 138 return To(To.ComponentType(L), To.ComponentType(a), To.ComponentType(b)); 139 } 140 else 141 { 142 // we'll need to pipe through XYZ to adjust the whitepoint 143 auto xyz = cast(XYZ!(To.ComponentType))this; 144 return cast(To)xyz; 145 } 146 } 147 148 static To convertColorImpl(To, From)(From color) if (isLab!From && isXYZ!To) 149 { 150 alias WT = WorkingType!(From, To); 151 152 enum w = cast(XYZ!WT)whitePoint; 153 154 static WT f(WT v) 155 { 156 if (v > WT(0.206893)) 157 return v^^WT(3); 158 else 159 return (v - WT(16.0/116))*WT(1/7.787); 160 } 161 162 WT Y = (color.L + 16)*WT(1.0/116); 163 WT X = color.a*WT(1.0/500) + Y; 164 WT Z = -color.b*WT(1.0/200) + Y; 165 166 X = w.X * f(X); 167 Y = w.Y * f(Y); 168 Z = w.Z * f(Z); 169 170 return To(X, Y, Z); 171 } 172 173 static To convertColorImpl(To, From)(From color) if (isXYZ!From && isLab!To) 174 { 175 alias WT = WorkingType!(From, To); 176 177 enum w = cast(XYZ!WT)whitePoint; 178 179 static WT f(WT v) 180 { 181 if (v > WT(0.008856)) 182 return v^^WT(1.0/3); 183 else 184 return WT(7.787)*v + WT(16.0/116); 185 } 186 187 WT X = f(color.X / w.X); 188 WT Y = f(color.Y / w.Y); 189 WT Z = f(color.Z / w.Z); 190 191 return To(116*Y - 16, 500*(X - Y), 200*(Y - Z)); 192 } 193 194 private: 195 alias AllComponents = TypeTuple!("L", "a", "b"); 196 } 197 198 199 /** 200 A CIE L*C*h° color, parameterised for component type and white point. 201 The LCh color space is a Lab cube color space, where instead of cartesian coordinates a*, b*, the cylindrical coordinates C* (chroma) and h° (hue angle) are specified. The CIELab lightness L* remains unchanged. 202 */ 203 struct LCh(F = float, alias whitePoint_ = (WhitePoint!F.D50)) if (isFloatingPoint!F) 204 { 205 @safe: pure: nothrow: @nogc: 206 207 /** Type of the color components. */ 208 alias ComponentType = F; 209 210 /** The color components that were specified. */ 211 enum whitePoint = whitePoint_; 212 213 /** L* (lightness) component. */ 214 F L = 0; 215 /** C* (chroma) component. */ 216 F C = 0; 217 /** h° (hue) component, in degrees. */ 218 F h = 0; 219 220 /** Get hue angle in radians. */ 221 @property F radians() const 222 { 223 return h * F((1.0/180)*PI); 224 } 225 /** Set hue angle in radians. */ 226 @property void radians(F angle) 227 { 228 h = angle * F(M_1_PI*180); 229 } 230 231 /** 232 Cast to other color types. 233 234 This cast is a convenience which simply forwards the call to convertColor. 235 */ 236 Color opCast(Color)() const if (isColor!Color) 237 { 238 return convertColor!Color(this); 239 } 240 241 242 package: 243 244 alias ParentColor = Lab!(F, whitePoint_); 245 246 static To convertColorImpl(To, From)(From color) if (isLCh!From && isLCh!To) 247 { 248 static if (From.whitePoint == To.whitePoint) 249 { 250 // same whitepoint, just a format conversion 251 return To(To.ComponentType(L), To.ComponentType(C), To.ComponentType(h)); 252 } 253 else 254 { 255 // we'll need to pipe through XYZ to adjust the whitepoint 256 auto xyz = cast(XYZ!(To.ComponentType))this; 257 return cast(To)xyz; 258 } 259 } 260 261 static To convertColorImpl(To, From)(From color) if (isLCh!From && isLab!To) 262 { 263 alias WT = WorkingType!(From, To); 264 265 WT a = cos(color.h*WT(1.0/180*PI)) * color.C; 266 WT b = sin(color.h*WT(1.0/180*PI)) * color.C; 267 268 return To(color.L, a, b); 269 } 270 271 static To convertColorImpl(To, From)(From color) if (isLab!From && isLCh!To) 272 { 273 alias WT = WorkingType!(From, To); 274 275 WT C = sqrt(color.a^^2 + color.b^^2); 276 WT h = atan2(color.b, color.a); 277 if (h >= 0) 278 h = h*WT(M_1_PI*180); 279 else 280 h = 360 + h*WT(M_1_PI*180); 281 282 return To(color.L, C, h); 283 } 284 }