Skip to content

Commit 6b21afe

Browse files
authored
Add files via upload
1 parent ba4a821 commit 6b21afe

1 file changed

Lines changed: 277 additions & 0 deletions

File tree

ColorCalculus.py

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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

Comments
 (0)