0
# Chromatic Adaptation
1
2
Chromatic adaptation provides low-level functions for transforming colors between different illuminants. This compensates for the human visual system's adaptation to different lighting conditions, ensuring accurate color reproduction across varying illuminants.
3
4
## Capabilities
5
6
### Primary Adaptation Function
7
8
Transform XYZ values from one illuminant to another using various adaptation methods.
9
10
```python { .api }
11
def apply_chromatic_adaptation(val_x, val_y, val_z, orig_illum, targ_illum, adaptation='bradford'):
12
"""
13
Apply chromatic adaptation to XYZ values.
14
15
Parameters:
16
- val_x, val_y, val_z: Original XYZ color values (float)
17
- orig_illum: Original illuminant name (str, e.g., 'd65', 'a', 'c')
18
- targ_illum: Target illuminant name (str, e.g., 'd50', 'd65', 'a')
19
- adaptation: Adaptation method (str, default: 'bradford')
20
Options: 'bradford', 'von_kries', 'xyz_scaling'
21
22
Returns:
23
tuple: Adapted XYZ values (X, Y, Z)
24
25
Raises:
26
- InvalidIlluminantError: If illuminant names are invalid
27
- ValueError: If adaptation method is not supported
28
29
Notes:
30
- Bradford method is most accurate for most applications
31
- Von Kries method is simpler but less accurate
32
- XYZ scaling is the simplest method, least accurate
33
"""
34
```
35
36
### Color Object Adaptation
37
38
Apply chromatic adaptation directly to color objects with automatic illuminant handling.
39
40
```python { .api }
41
def apply_chromatic_adaptation_on_color(color, targ_illum, adaptation='bradford'):
42
"""
43
Apply chromatic adaptation to color object.
44
45
Parameters:
46
- color: Color object (XYZ, Lab, or other illuminant-aware color)
47
- targ_illum: Target illuminant name (str)
48
- adaptation: Adaptation method (str, default: 'bradford')
49
50
Returns:
51
Color object: New color object adapted to target illuminant
52
53
Notes:
54
- Automatically handles color space conversions if needed
55
- Preserves original color object type when possible
56
- Updates illuminant metadata in returned object
57
"""
58
```
59
60
## Supported Adaptation Methods
61
62
### Bradford Method (Recommended)
63
64
The most accurate chromatic adaptation transform for most applications.
65
66
```python
67
# Bradford adaptation (default)
68
adapted_xyz = apply_chromatic_adaptation(
69
val_x=50.0, val_y=40.0, val_z=30.0,
70
orig_illum='d65',
71
targ_illum='d50',
72
adaptation='bradford'
73
)
74
```
75
76
**Characteristics:**
77
- High accuracy across wide range of illuminants
78
- Industry standard for color management
79
- Used in ICC color profiles
80
- Best for most practical applications
81
82
### Von Kries Method
83
84
Classical chromatic adaptation based on cone response.
85
86
```python
87
# Von Kries adaptation
88
adapted_xyz = apply_chromatic_adaptation(
89
val_x=50.0, val_y=40.0, val_z=30.0,
90
orig_illum='d65',
91
targ_illum='a',
92
adaptation='von_kries'
93
)
94
```
95
96
**Characteristics:**
97
- Based on human cone cell responses
98
- Simpler than Bradford method
99
- Good for educational purposes
100
- Less accurate for extreme illuminant changes
101
102
### XYZ Scaling Method
103
104
Simple scaling method for basic adaptation needs.
105
106
```python
107
# XYZ scaling adaptation
108
adapted_xyz = apply_chromatic_adaptation(
109
val_x=50.0, val_y=40.0, val_z=30.0,
110
orig_illum='d65',
111
targ_illum='d50',
112
adaptation='xyz_scaling'
113
)
114
```
115
116
**Characteristics:**
117
- Simplest implementation
118
- Fastest computation
119
- Least accurate results
120
- Only suitable for small illuminant differences
121
122
## Usage Examples
123
124
### Basic Illuminant Adaptation
125
126
```python
127
from colormath.chromatic_adaptation import apply_chromatic_adaptation
128
129
# Adapt color from D65 to D50 illuminant
130
original_xyz = (45.0, 40.0, 35.0)
131
132
adapted_xyz = apply_chromatic_adaptation(
133
val_x=original_xyz[0],
134
val_y=original_xyz[1],
135
val_z=original_xyz[2],
136
orig_illum='d65',
137
targ_illum='d50',
138
adaptation='bradford'
139
)
140
141
print(f"Original XYZ (D65): X={original_xyz[0]:.2f}, Y={original_xyz[1]:.2f}, Z={original_xyz[2]:.2f}")
142
print(f"Adapted XYZ (D50): X={adapted_xyz[0]:.2f}, Y={adapted_xyz[1]:.2f}, Z={adapted_xyz[2]:.2f}")
143
```
144
145
### Color Object Adaptation
146
147
```python
148
from colormath.color_objects import XYZColor, LabColor
149
from colormath.chromatic_adaptation import apply_chromatic_adaptation_on_color
150
from colormath.color_conversions import convert_color
151
152
# Create XYZ color under D65
153
xyz_d65 = XYZColor(xyz_x=45.0, xyz_y=40.0, xyz_z=35.0, illuminant='d65')
154
155
# Adapt to D50 illuminant
156
xyz_d50 = apply_chromatic_adaptation_on_color(xyz_d65, 'd50')
157
158
print(f"Original illuminant: {xyz_d65.illuminant}")
159
print(f"Adapted illuminant: {xyz_d50.illuminant}")
160
print(f"Original XYZ: {xyz_d65.get_value_tuple()}")
161
print(f"Adapted XYZ: {xyz_d50.get_value_tuple()}")
162
163
# Convert both to Lab to see the difference
164
lab_d65 = convert_color(xyz_d65, LabColor)
165
lab_d50 = convert_color(xyz_d50, LabColor)
166
167
print(f"Lab (D65): L={lab_d65.lab_l:.2f}, a={lab_d65.lab_a:.2f}, b={lab_d65.lab_b:.2f}")
168
print(f"Lab (D50): L={lab_d50.lab_l:.2f}, a={lab_d50.lab_a:.2f}, b={lab_d50.lab_b:.2f}")
169
```
170
171
### Comparing Adaptation Methods
172
173
```python
174
from colormath.chromatic_adaptation import apply_chromatic_adaptation
175
176
# Test color
177
test_xyz = (60.0, 50.0, 40.0)
178
orig_illum = 'd65'
179
targ_illum = 'a' # Tungsten illuminant (warm)
180
181
# Compare all adaptation methods
182
methods = ['bradford', 'von_kries', 'xyz_scaling']
183
results = {}
184
185
for method in methods:
186
adapted = apply_chromatic_adaptation(
187
val_x=test_xyz[0],
188
val_y=test_xyz[1],
189
val_z=test_xyz[2],
190
orig_illum=orig_illum,
191
targ_illum=targ_illum,
192
adaptation=method
193
)
194
results[method] = adapted
195
196
print(f"Original XYZ ({orig_illum.upper()}): X={test_xyz[0]:.2f}, Y={test_xyz[1]:.2f}, Z={test_xyz[2]:.2f}")
197
print(f"\nAdapted to {targ_illum.upper()}:")
198
for method, xyz in results.items():
199
print(f" {method:12}: X={xyz[0]:.2f}, Y={xyz[1]:.2f}, Z={xyz[2]:.2f}")
200
201
# Calculate differences from Bradford (reference)
202
bradford_result = results['bradford']
203
print(f"\nDifferences from Bradford:")
204
for method, xyz in results.items():
205
if method != 'bradford':
206
diff_x = abs(xyz[0] - bradford_result[0])
207
diff_y = abs(xyz[1] - bradford_result[1])
208
diff_z = abs(xyz[2] - bradford_result[2])
209
print(f" {method:12}: ΔX={diff_x:.3f}, ΔY={diff_y:.3f}, ΔZ={diff_z:.3f}")
210
```
211
212
### Print Production Workflow
213
214
```python
215
from colormath.color_objects import LabColor, XYZColor
216
from colormath.color_conversions import convert_color
217
from colormath.chromatic_adaptation import apply_chromatic_adaptation_on_color
218
219
def adapt_colors_for_print(colors, source_illuminant='d65', target_illuminant='d50'):
220
"""
221
Adapt colors from display viewing (D65) to print viewing (D50).
222
223
Parameters:
224
- colors: List of color objects
225
- source_illuminant: Source illuminant (typically D65 for displays)
226
- target_illuminant: Target illuminant (typically D50 for print)
227
228
Returns:
229
List of adapted color objects
230
"""
231
adapted_colors = []
232
233
for color in colors:
234
# Convert to XYZ if needed
235
if not isinstance(color, XYZColor):
236
xyz_color = convert_color(color, XYZColor)
237
xyz_color.set_illuminant(source_illuminant)
238
else:
239
xyz_color = color
240
241
# Apply chromatic adaptation
242
adapted_xyz = apply_chromatic_adaptation_on_color(
243
xyz_color, target_illuminant, adaptation='bradford'
244
)
245
246
# Convert back to original color space if needed
247
if not isinstance(color, XYZColor):
248
adapted_color = convert_color(adapted_xyz, type(color))
249
adapted_colors.append(adapted_color)
250
else:
251
adapted_colors.append(adapted_xyz)
252
253
return adapted_colors
254
255
# Example usage
256
display_colors = [
257
LabColor(lab_l=50, lab_a=20, lab_b=30, illuminant='d65'),
258
LabColor(lab_l=60, lab_a=-10, lab_b=40, illuminant='d65'),
259
LabColor(lab_l=40, lab_a=30, lab_b=-20, illuminant='d65')
260
]
261
262
print_colors = adapt_colors_for_print(display_colors)
263
264
print("Display to Print Adaptation:")
265
for i, (display, print_color) in enumerate(zip(display_colors, print_colors)):
266
print(f"Color {i+1}:")
267
print(f" Display (D65): L={display.lab_l:.1f}, a={display.lab_a:.1f}, b={display.lab_b:.1f}")
268
print(f" Print (D50): L={print_color.lab_l:.1f}, a={print_color.lab_a:.1f}, b={print_color.lab_b:.1f}")
269
```
270
271
### Photography White Balance Correction
272
273
```python
274
from colormath.color_objects import XYZColor
275
from colormath.chromatic_adaptation import apply_chromatic_adaptation
276
277
def white_balance_correction(image_colors, shot_illuminant, target_illuminant='d65'):
278
"""
279
Apply white balance correction to image colors.
280
281
Parameters:
282
- image_colors: List of XYZ color tuples from image
283
- shot_illuminant: Illuminant under which photo was taken
284
- target_illuminant: Target illuminant for corrected image
285
286
Returns:
287
List of white balance corrected XYZ tuples
288
"""
289
corrected_colors = []
290
291
for xyz in image_colors:
292
corrected = apply_chromatic_adaptation(
293
val_x=xyz[0],
294
val_y=xyz[1],
295
val_z=xyz[2],
296
orig_illum=shot_illuminant,
297
targ_illum=target_illuminant,
298
adaptation='bradford'
299
)
300
corrected_colors.append(corrected)
301
302
return corrected_colors
303
304
# Example: Correct tungsten lighting to daylight
305
tungsten_colors = [
306
(80.0, 70.0, 30.0), # Warm-tinted color
307
(40.0, 35.0, 15.0), # Another warm color
308
(95.0, 85.0, 40.0) # Bright warm color
309
]
310
311
daylight_colors = white_balance_correction(
312
tungsten_colors,
313
shot_illuminant='a', # Tungsten
314
target_illuminant='d65' # Daylight
315
)
316
317
print("White Balance Correction (Tungsten → Daylight):")
318
for i, (tungsten, daylight) in enumerate(zip(tungsten_colors, daylight_colors)):
319
print(f"Pixel {i+1}:")
320
print(f" Tungsten: X={tungsten[0]:.1f}, Y={tungsten[1]:.1f}, Z={tungsten[2]:.1f}")
321
print(f" Daylight: X={daylight[0]:.1f}, Y={daylight[1]:.1f}, Z={daylight[2]:.1f}")
322
```
323
324
### Color Management Pipeline
325
326
```python
327
from colormath.color_objects import XYZColor, sRGBColor
328
from colormath.color_conversions import convert_color
329
from colormath.chromatic_adaptation import apply_chromatic_adaptation_on_color
330
331
class ColorManagementPipeline:
332
"""Color management pipeline with chromatic adaptation."""
333
334
def __init__(self, source_illuminant='d65', target_illuminant='d50'):
335
self.source_illuminant = source_illuminant
336
self.target_illuminant = target_illuminant
337
338
def process_color(self, color, adaptation_method='bradford'):
339
"""
340
Process color through complete color management pipeline.
341
342
Parameters:
343
- color: Input color object
344
- adaptation_method: Chromatic adaptation method
345
346
Returns:
347
dict: Processed color data
348
"""
349
# Convert to XYZ for chromatic adaptation
350
xyz_original = convert_color(color, XYZColor)
351
xyz_original.set_illuminant(self.source_illuminant)
352
353
# Apply chromatic adaptation
354
xyz_adapted = apply_chromatic_adaptation_on_color(
355
xyz_original,
356
self.target_illuminant,
357
adaptation=adaptation_method
358
)
359
360
# Convert back to sRGB for display
361
rgb_result = convert_color(xyz_adapted, sRGBColor)
362
363
return {
364
'original_xyz': xyz_original.get_value_tuple(),
365
'adapted_xyz': xyz_adapted.get_value_tuple(),
366
'final_rgb': rgb_result.get_value_tuple(),
367
'rgb_hex': rgb_result.get_rgb_hex(),
368
'illuminant_change': f"{self.source_illuminant} → {self.target_illuminant}"
369
}
370
371
# Example usage
372
pipeline = ColorManagementPipeline(source_illuminant='d65', target_illuminant='d50')
373
374
# Process an sRGB color
375
input_color = sRGBColor(rgb_r=0.8, rgb_g=0.4, rgb_b=0.2)
376
result = pipeline.process_color(input_color)
377
378
print("Color Management Pipeline Result:")
379
print(f"Original XYZ: {result['original_xyz']}")
380
print(f"Adapted XYZ: {result['adapted_xyz']}")
381
print(f"Final RGB: {result['final_rgb']}")
382
print(f"Hex color: {result['rgb_hex']}")
383
print(f"Illuminant: {result['illuminant_change']}")
384
```
385
386
### Batch Color Adaptation
387
388
```python
389
from colormath.chromatic_adaptation import apply_chromatic_adaptation
390
391
def batch_adapt_colors(color_list, orig_illum, targ_illum, method='bradford'):
392
"""
393
Apply chromatic adaptation to batch of XYZ colors.
394
395
Parameters:
396
- color_list: List of (X, Y, Z) tuples
397
- orig_illum: Original illuminant
398
- targ_illum: Target illuminant
399
- method: Adaptation method
400
401
Returns:
402
List of adapted (X, Y, Z) tuples
403
"""
404
adapted_colors = []
405
406
for xyz in color_list:
407
adapted = apply_chromatic_adaptation(
408
val_x=xyz[0],
409
val_y=xyz[1],
410
val_z=xyz[2],
411
orig_illum=orig_illum,
412
targ_illum=targ_illum,
413
adaptation=method
414
)
415
adapted_colors.append(adapted)
416
417
return adapted_colors
418
419
# Example: Adapt color palette from A illuminant to D65
420
palette_a = [
421
(85.0, 75.0, 25.0), # Warm yellow
422
(45.0, 40.0, 60.0), # Cool blue
423
(70.0, 35.0, 35.0), # Red
424
(25.0, 45.0, 30.0), # Green
425
(90.0, 90.0, 85.0) # Near white
426
]
427
428
palette_d65 = batch_adapt_colors(palette_a, 'a', 'd65')
429
430
print("Palette Adaptation (Illuminant A → D65):")
431
for i, (orig, adapted) in enumerate(zip(palette_a, palette_d65)):
432
print(f"Color {i+1}: ({orig[0]:5.1f}, {orig[1]:5.1f}, {orig[2]:5.1f}) → ({adapted[0]:5.1f}, {adapted[1]:5.1f}, {adapted[2]:5.1f})")
433
```
434
435
## Technical Details
436
437
### Adaptation Matrix Structure
438
439
Each adaptation method uses specific transformation matrices:
440
441
```python
442
# Bradford adaptation matrices (example)
443
BRADFORD_M = [
444
[0.8951, 0.2664, -0.1614],
445
[-0.7502, 1.7135, 0.0367],
446
[0.0389, -0.0685, 1.0296]
447
]
448
449
BRADFORD_M_INV = [
450
[0.9869929, -0.1470543, 0.1599627],
451
[0.4323053, 0.5183603, 0.0492912],
452
[-0.0085287, 0.0400428, 0.9684867]
453
]
454
```
455
456
### Supported Illuminants
457
458
All standard CIE illuminants are supported:
459
- **D series**: D50, D55, D65, D75 (daylight illuminants)
460
- **A**: Tungsten incandescent (2856K)
461
- **B**: Direct sunlight (obsolete)
462
- **C**: Average daylight (obsolete)
463
- **E**: Equal energy illuminant
464
- **F series**: Fluorescent illuminants
465
466
### Performance Characteristics
467
468
| Method | Speed | Accuracy | Use Case |
469
|--------|-------|----------|----------|
470
| Bradford | Fast | High | General purpose |
471
| Von Kries | Fast | Medium | Educational, simple cases |
472
| XYZ Scaling | Fastest | Low | Quick approximations |
473
474
### Error Handling
475
476
```python
477
from colormath.color_exceptions import InvalidIlluminantError
478
479
try:
480
result = apply_chromatic_adaptation(50, 40, 30, 'invalid', 'd65')
481
except InvalidIlluminantError as e:
482
print(f"Invalid illuminant: {e}")
483
```
484
485
Common errors:
486
- **InvalidIlluminantError**: Unknown illuminant name
487
- **ValueError**: Unsupported adaptation method
488
- **TypeError**: Invalid color values