1+ '''
2+ ColorCalculus.py - Deep color mathematics for perceptual color operations.
3+ Contains the DeepColorMath class with RGB to LAB conversion and CIEDE2000.
4+ '''
5+
6+ import math
7+
8+
9+ class DeepColorMath :
10+ """Advanced color mathematics for perceptual color distance calculations.
11+
12+ This class provides methods for converting between color spaces and
13+ calculating perceptually uniform color differences using CIEDE2000.
14+ """
15+
16+ @staticmethod
17+ def RGBToLab (r , g , b ):
18+ """Convert RGB (0-255) to LAB color space.
19+
20+ LAB is perceptually uniform - equal distances in LAB space correspond
21+ to roughly equal perceived color differences.
22+
23+ Args:
24+ r, g, b: RGB values in range 0-255
25+
26+ Returns:
27+ tuple: (L, a, b) where:
28+ - L is lightness (0-100)
29+ - a is green-red axis
30+ - b is blue-yellow axis
31+ """
32+ # Normalize RGB to 0-1
33+ r , g , b = r / 255.0 , g / 255.0 , b / 255.0
34+
35+ # Convert to linear RGB (remove gamma correction)
36+ def GammaExpand (channel ):
37+ if channel <= 0.04045 :
38+ return channel / 12.92
39+ return math .pow ((channel + 0.055 )/ 1.055 , 2.4 )
40+
41+ r_linear = GammaExpand (r )
42+ g_linear = GammaExpand (g )
43+ b_linear = GammaExpand (b )
44+
45+ # Convert to XYZ using sRGB matrix (D65 illuminant)
46+ x = r_linear * 0.4124564 + g_linear * 0.3575761 + b_linear * 0.1804375
47+ y = r_linear * 0.2126729 + g_linear * 0.7151522 + b_linear * 0.0721750
48+ z = r_linear * 0.0193339 + g_linear * 0.1191920 + b_linear * 0.9503041
49+
50+ # Normalize by D65 white point
51+ x = x / 0.95047
52+ y = y / 1.00000
53+ z = z / 1.08883
54+
55+ # Convert XYZ to LAB
56+ def f (t ):
57+ """LAB conversion function with linear segment for small values."""
58+ delta = 6 / 29
59+ if t > delta ** 3 :
60+ return math .pow (t , 1 / 3 )
61+ return t / (3 * delta ** 2 )+ 4 / 29
62+
63+ fx = f (x )
64+ fy = f (y )
65+ fz = f (z )
66+
67+ L = 116 * fy - 16
68+ a = 500 * (fx - fy )
69+ b_lab = 200 * (fy - fz )
70+
71+ return L , a , b_lab
72+
73+ @staticmethod
74+ def HexToLab (hex_color ):
75+ """Convert hex color to LAB color space.
76+
77+ Args:
78+ hex_color: 6-character hex string (with or without leading '#')
79+
80+ Returns:
81+ tuple: (L, a, b) in LAB color space
82+ """
83+ hex_color = hex_color .lstrip ('#' )
84+ r = int (hex_color [0 :2 ], 16 )
85+ g = int (hex_color [2 :4 ], 16 )
86+ b = int (hex_color [4 :6 ], 16 )
87+ return DeepColorMath .RGBToLab (r , g , b )
88+
89+ @staticmethod
90+ def RGBToHSV (r , g , b ):
91+ """Convert RGB (0-255) to HSV color space.
92+
93+ HSV stands for Hue, Saturation, Value.
94+ - Hue: Color type (0-360 degrees, but returned as 0-1)
95+ - Saturation: Color intensity (0-1, where 0 is gray)
96+ - Value: Brightness (0-1, where 0 is black)
97+
98+ Args:
99+ r, g, b: RGB values in range 0-255
100+
101+ Returns:
102+ tuple: (h, s, v) where:
103+ - h is hue (0-1, multiply by 360 for degrees)
104+ - s is saturation (0-1)
105+ - v is value/brightness (0-1)
106+ """
107+ # Normalize to 0-1
108+ r , g , b = r / 255.0 , g / 255.0 , b / 255.0
109+
110+ max_c = max (r , g , b )
111+ min_c = min (r , g , b )
112+ delta = max_c - min_c
113+
114+ # Value is the maximum
115+ v = max_c
116+
117+ # Saturation
118+ if max_c == 0 :
119+ s = 0
120+ else :
121+ s = delta / max_c
122+
123+ # Hue
124+ if delta == 0 :
125+ h = 0 # Undefined, but we'll use 0
126+ elif max_c == r :
127+ h = (60 * ((g - b )/ delta )+ 360 )% 360
128+ elif max_c == g :
129+ h = (60 * ((b - r )/ delta )+ 120 )% 360
130+ else : # max_c==b
131+ h = (60 * ((r - g )/ delta )+ 240 )% 360
132+
133+ # Normalize hue to 0-1
134+ h = h / 360.0
135+
136+ return h , s , v
137+
138+ @staticmethod
139+ def HexToHSV (hex_color ):
140+ """Convert hex color to HSV color space.
141+
142+ Args:
143+ hex_color: 6-character hex string (with or without leading '#')
144+
145+ Returns:
146+ tuple: (h, s, v) in HSV color space
147+ """
148+ hex_color = hex_color .lstrip ('#' )
149+ r = int (hex_color [0 :2 ], 16 )
150+ g = int (hex_color [2 :4 ], 16 )
151+ b = int (hex_color [4 :6 ], 16 )
152+ return DeepColorMath .RGBToHSV (r , g , b )
153+
154+ @staticmethod
155+ def ciede2000 (hex1 , hex2 ):
156+ """Calculate perceptual color difference using CIEDE2000 formula.
157+
158+ CIEDE2000 is the industry standard for measuring how different two
159+ colors appear to the human eye. A deltaE of:
160+ - < 1.0: Not perceptible by human eyes
161+ - 1-2: Perceptible through close observation
162+ - 2-10: Perceptible at a glance
163+ - 11-49: Colors are more similar than opposite
164+ - 100+: Colors are exact opposite
165+
166+ Args:
167+ hex1: First hex color (6 chars, with or without '#')
168+ hex2: Second hex color (6 chars, with or without '#')
169+
170+ Returns:
171+ float: Delta E (perceptual color difference)
172+ """
173+ hex1 = hex1 .lstrip ('#' )
174+ hex2 = hex2 .lstrip ('#' )
175+
176+ # Convert to LAB
177+ L1 , a1 , b1_lab = DeepColorMath .HexToLab (hex1 )
178+ L2 , a2 , b2_lab = DeepColorMath .HexToLab (hex2 )
179+
180+ # Calculate C (chroma) and h (hue angle)
181+ C1 = math .sqrt (a1 ** 2 + b1_lab ** 2 )
182+ C2 = math .sqrt (a2 ** 2 + b2_lab ** 2 )
183+
184+ # Delta values
185+ dL = L2 - L1
186+ dC = C2 - C1
187+ da = a2 - a1
188+ db = b2_lab - b1_lab
189+
190+ # Calculate dH (delta hue)
191+ # dH² = da² + db² - dC²
192+ dH_squared = da ** 2 + db ** 2 - dC ** 2
193+ dH = math .sqrt (max (0 , dH_squared )) # Ensure non-negative
194+
195+ # Weighting factors (simplified from full CIEDE2000)
196+ # The full formula includes complex corrections for lightness,
197+ # chroma, and hue. This simplified version gives excellent results
198+ # for most practical applications.
199+ kL , kC , kH = 1.0 , 1.0 , 1.0
200+
201+ # Calculate weighted components
202+ L_component = dL / kL
203+ C_component = dC / kC
204+ H_component = dH / kH
205+
206+ # Final delta E
207+ dE = math .sqrt (L_component ** 2 + C_component ** 2 + H_component ** 2 )
208+
209+ return dE
210+
211+ @staticmethod
212+ def AreTheyLookinClose (hex1 , hex2 , threshold = 25 ):
213+ """Check if two colors are perceptually close.
214+
215+ Args:
216+ hex1: First hex color
217+ hex2: Second hex color
218+ threshold: Maximum deltaE to consider colors "close"
219+ (default 25 = clearly different colors)
220+
221+ Returns:
222+ bool: True if colors are close (deltaE < threshold)
223+ """
224+ return DeepColorMath .ciede2000 (hex1 , hex2 )< threshold
225+
226+
227+ if __name__ == "__main__" :
228+ # Demo the color math
229+ print ("=== DeepColorMath Demo ===\n " )
230+
231+ # Test RGB to LAB conversion
232+ print ("RGB to LAB conversions:" )
233+ print (f"Red (255,0,0) -> LAB: { DeepColorMath .RGBToLab (255 , 0 , 0 )} " )
234+ print (f"Green (0,255,0) -> LAB: { DeepColorMath .RGBToLab (0 , 255 , 0 )} " )
235+ print (f"Blue (0,0,255) -> LAB: { DeepColorMath .RGBToLab (0 , 0 , 255 )} " )
236+ print (f"White (255,255,255) -> LAB: { DeepColorMath .RGBToLab (255 , 255 , 255 )} " )
237+ print (f"Black (0,0,0) -> LAB: { DeepColorMath .RGBToLab (0 , 0 , 0 )} " )
238+
239+ print ("\n === RGB to HSV conversions ===" )
240+ print (f"Red (255,0,0) -> HSV: { DeepColorMath .RGBToHSV (255 , 0 , 0 )} " )
241+ print (f"Green (0,255,0) -> HSV: { DeepColorMath .RGBToHSV (0 , 255 , 0 )} " )
242+ print (f"Blue (0,0,255) -> HSV: { DeepColorMath .RGBToHSV (0 , 0 , 255 )} " )
243+ print (f"Purple (128,0,128) -> HSV: { DeepColorMath .RGBToHSV (128 , 0 , 128 )} " )
244+
245+ print ("\n === CIEDE2000 Color Differences ===\n " )
246+
247+ # Test similar colors
248+ color1 = "FF0000" # Red
249+ color2 = "FE0000" # Slightly different red
250+ delta = DeepColorMath .ciede2000 (color1 , color2 )
251+ print (f"Red vs Slightly Different Red: ΔE = { delta :.2f} " )
252+ print (f" -> { 'Perceptible' if delta > 1 else 'Not perceptible' } \n " )
253+
254+ # Test very different colors
255+ color1 = "FF0000" # Red
256+ color2 = "00FF00" # Green
257+ delta = DeepColorMath .ciede2000 (color1 , color2 )
258+ print (f"Red vs Green: ΔE = { delta :.2f} " )
259+ print (f" -> Very different colors\n " )
260+
261+ # Test similar but perceptible
262+ color1 = "FF0000" # Red
263+ color2 = "FF3030" # Light red
264+ delta = DeepColorMath .ciede2000 (color1 , color2 )
265+ print (f"Red vs Light Red: ΔE = { delta :.2f} " )
266+ print (f" -> { 'Close' if delta < 25 else 'Different' } colors\n " )
267+
268+ # Test with hex prefix
269+ color1 = "#3498db" # Blue
270+ color2 = "#2980b9" # Darker blue
271+ delta = DeepColorMath .ciede2000 (color1 , color2 )
272+ print (f"Blue vs Darker Blue: ΔE = { delta :.2f} \n " )
273+
274+ # Test the helper function
275+ print ("=== Using AreTheyLookinClose() ===" )
276+ print (f"Are #FF0000 and #FE0000 close? { DeepColorMath .AreTheyLookinClose ('FF0000' , 'FE0000' )} " )
277+ print (f"Are #FF0000 and #00FF00 close? { DeepColorMath .AreTheyLookinClose ('FF0000' , '00FF00' )} " )
0 commit comments