Color math and conversion library for comprehensive color space transformations and color difference calculations.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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.
Transform XYZ values from one illuminant to another using various adaptation methods.
def apply_chromatic_adaptation(val_x, val_y, val_z, orig_illum, targ_illum, adaptation='bradford'):
"""
Apply chromatic adaptation to XYZ values.
Parameters:
- val_x, val_y, val_z: Original XYZ color values (float)
- orig_illum: Original illuminant name (str, e.g., 'd65', 'a', 'c')
- targ_illum: Target illuminant name (str, e.g., 'd50', 'd65', 'a')
- adaptation: Adaptation method (str, default: 'bradford')
Options: 'bradford', 'von_kries', 'xyz_scaling'
Returns:
tuple: Adapted XYZ values (X, Y, Z)
Raises:
- InvalidIlluminantError: If illuminant names are invalid
- ValueError: If adaptation method is not supported
Notes:
- Bradford method is most accurate for most applications
- Von Kries method is simpler but less accurate
- XYZ scaling is the simplest method, least accurate
"""Apply chromatic adaptation directly to color objects with automatic illuminant handling.
def apply_chromatic_adaptation_on_color(color, targ_illum, adaptation='bradford'):
"""
Apply chromatic adaptation to color object.
Parameters:
- color: Color object (XYZ, Lab, or other illuminant-aware color)
- targ_illum: Target illuminant name (str)
- adaptation: Adaptation method (str, default: 'bradford')
Returns:
Color object: New color object adapted to target illuminant
Notes:
- Automatically handles color space conversions if needed
- Preserves original color object type when possible
- Updates illuminant metadata in returned object
"""The most accurate chromatic adaptation transform for most applications.
# Bradford adaptation (default)
adapted_xyz = apply_chromatic_adaptation(
val_x=50.0, val_y=40.0, val_z=30.0,
orig_illum='d65',
targ_illum='d50',
adaptation='bradford'
)Characteristics:
Classical chromatic adaptation based on cone response.
# Von Kries adaptation
adapted_xyz = apply_chromatic_adaptation(
val_x=50.0, val_y=40.0, val_z=30.0,
orig_illum='d65',
targ_illum='a',
adaptation='von_kries'
)Characteristics:
Simple scaling method for basic adaptation needs.
# XYZ scaling adaptation
adapted_xyz = apply_chromatic_adaptation(
val_x=50.0, val_y=40.0, val_z=30.0,
orig_illum='d65',
targ_illum='d50',
adaptation='xyz_scaling'
)Characteristics:
from colormath.chromatic_adaptation import apply_chromatic_adaptation
# Adapt color from D65 to D50 illuminant
original_xyz = (45.0, 40.0, 35.0)
adapted_xyz = apply_chromatic_adaptation(
val_x=original_xyz[0],
val_y=original_xyz[1],
val_z=original_xyz[2],
orig_illum='d65',
targ_illum='d50',
adaptation='bradford'
)
print(f"Original XYZ (D65): X={original_xyz[0]:.2f}, Y={original_xyz[1]:.2f}, Z={original_xyz[2]:.2f}")
print(f"Adapted XYZ (D50): X={adapted_xyz[0]:.2f}, Y={adapted_xyz[1]:.2f}, Z={adapted_xyz[2]:.2f}")from colormath.color_objects import XYZColor, LabColor
from colormath.chromatic_adaptation import apply_chromatic_adaptation_on_color
from colormath.color_conversions import convert_color
# Create XYZ color under D65
xyz_d65 = XYZColor(xyz_x=45.0, xyz_y=40.0, xyz_z=35.0, illuminant='d65')
# Adapt to D50 illuminant
xyz_d50 = apply_chromatic_adaptation_on_color(xyz_d65, 'd50')
print(f"Original illuminant: {xyz_d65.illuminant}")
print(f"Adapted illuminant: {xyz_d50.illuminant}")
print(f"Original XYZ: {xyz_d65.get_value_tuple()}")
print(f"Adapted XYZ: {xyz_d50.get_value_tuple()}")
# Convert both to Lab to see the difference
lab_d65 = convert_color(xyz_d65, LabColor)
lab_d50 = convert_color(xyz_d50, LabColor)
print(f"Lab (D65): L={lab_d65.lab_l:.2f}, a={lab_d65.lab_a:.2f}, b={lab_d65.lab_b:.2f}")
print(f"Lab (D50): L={lab_d50.lab_l:.2f}, a={lab_d50.lab_a:.2f}, b={lab_d50.lab_b:.2f}")from colormath.chromatic_adaptation import apply_chromatic_adaptation
# Test color
test_xyz = (60.0, 50.0, 40.0)
orig_illum = 'd65'
targ_illum = 'a' # Tungsten illuminant (warm)
# Compare all adaptation methods
methods = ['bradford', 'von_kries', 'xyz_scaling']
results = {}
for method in methods:
adapted = apply_chromatic_adaptation(
val_x=test_xyz[0],
val_y=test_xyz[1],
val_z=test_xyz[2],
orig_illum=orig_illum,
targ_illum=targ_illum,
adaptation=method
)
results[method] = adapted
print(f"Original XYZ ({orig_illum.upper()}): X={test_xyz[0]:.2f}, Y={test_xyz[1]:.2f}, Z={test_xyz[2]:.2f}")
print(f"\nAdapted to {targ_illum.upper()}:")
for method, xyz in results.items():
print(f" {method:12}: X={xyz[0]:.2f}, Y={xyz[1]:.2f}, Z={xyz[2]:.2f}")
# Calculate differences from Bradford (reference)
bradford_result = results['bradford']
print(f"\nDifferences from Bradford:")
for method, xyz in results.items():
if method != 'bradford':
diff_x = abs(xyz[0] - bradford_result[0])
diff_y = abs(xyz[1] - bradford_result[1])
diff_z = abs(xyz[2] - bradford_result[2])
print(f" {method:12}: ΔX={diff_x:.3f}, ΔY={diff_y:.3f}, ΔZ={diff_z:.3f}")from colormath.color_objects import LabColor, XYZColor
from colormath.color_conversions import convert_color
from colormath.chromatic_adaptation import apply_chromatic_adaptation_on_color
def adapt_colors_for_print(colors, source_illuminant='d65', target_illuminant='d50'):
"""
Adapt colors from display viewing (D65) to print viewing (D50).
Parameters:
- colors: List of color objects
- source_illuminant: Source illuminant (typically D65 for displays)
- target_illuminant: Target illuminant (typically D50 for print)
Returns:
List of adapted color objects
"""
adapted_colors = []
for color in colors:
# Convert to XYZ if needed
if not isinstance(color, XYZColor):
xyz_color = convert_color(color, XYZColor)
xyz_color.set_illuminant(source_illuminant)
else:
xyz_color = color
# Apply chromatic adaptation
adapted_xyz = apply_chromatic_adaptation_on_color(
xyz_color, target_illuminant, adaptation='bradford'
)
# Convert back to original color space if needed
if not isinstance(color, XYZColor):
adapted_color = convert_color(adapted_xyz, type(color))
adapted_colors.append(adapted_color)
else:
adapted_colors.append(adapted_xyz)
return adapted_colors
# Example usage
display_colors = [
LabColor(lab_l=50, lab_a=20, lab_b=30, illuminant='d65'),
LabColor(lab_l=60, lab_a=-10, lab_b=40, illuminant='d65'),
LabColor(lab_l=40, lab_a=30, lab_b=-20, illuminant='d65')
]
print_colors = adapt_colors_for_print(display_colors)
print("Display to Print Adaptation:")
for i, (display, print_color) in enumerate(zip(display_colors, print_colors)):
print(f"Color {i+1}:")
print(f" Display (D65): L={display.lab_l:.1f}, a={display.lab_a:.1f}, b={display.lab_b:.1f}")
print(f" Print (D50): L={print_color.lab_l:.1f}, a={print_color.lab_a:.1f}, b={print_color.lab_b:.1f}")from colormath.color_objects import XYZColor
from colormath.chromatic_adaptation import apply_chromatic_adaptation
def white_balance_correction(image_colors, shot_illuminant, target_illuminant='d65'):
"""
Apply white balance correction to image colors.
Parameters:
- image_colors: List of XYZ color tuples from image
- shot_illuminant: Illuminant under which photo was taken
- target_illuminant: Target illuminant for corrected image
Returns:
List of white balance corrected XYZ tuples
"""
corrected_colors = []
for xyz in image_colors:
corrected = apply_chromatic_adaptation(
val_x=xyz[0],
val_y=xyz[1],
val_z=xyz[2],
orig_illum=shot_illuminant,
targ_illum=target_illuminant,
adaptation='bradford'
)
corrected_colors.append(corrected)
return corrected_colors
# Example: Correct tungsten lighting to daylight
tungsten_colors = [
(80.0, 70.0, 30.0), # Warm-tinted color
(40.0, 35.0, 15.0), # Another warm color
(95.0, 85.0, 40.0) # Bright warm color
]
daylight_colors = white_balance_correction(
tungsten_colors,
shot_illuminant='a', # Tungsten
target_illuminant='d65' # Daylight
)
print("White Balance Correction (Tungsten → Daylight):")
for i, (tungsten, daylight) in enumerate(zip(tungsten_colors, daylight_colors)):
print(f"Pixel {i+1}:")
print(f" Tungsten: X={tungsten[0]:.1f}, Y={tungsten[1]:.1f}, Z={tungsten[2]:.1f}")
print(f" Daylight: X={daylight[0]:.1f}, Y={daylight[1]:.1f}, Z={daylight[2]:.1f}")from colormath.color_objects import XYZColor, sRGBColor
from colormath.color_conversions import convert_color
from colormath.chromatic_adaptation import apply_chromatic_adaptation_on_color
class ColorManagementPipeline:
"""Color management pipeline with chromatic adaptation."""
def __init__(self, source_illuminant='d65', target_illuminant='d50'):
self.source_illuminant = source_illuminant
self.target_illuminant = target_illuminant
def process_color(self, color, adaptation_method='bradford'):
"""
Process color through complete color management pipeline.
Parameters:
- color: Input color object
- adaptation_method: Chromatic adaptation method
Returns:
dict: Processed color data
"""
# Convert to XYZ for chromatic adaptation
xyz_original = convert_color(color, XYZColor)
xyz_original.set_illuminant(self.source_illuminant)
# Apply chromatic adaptation
xyz_adapted = apply_chromatic_adaptation_on_color(
xyz_original,
self.target_illuminant,
adaptation=adaptation_method
)
# Convert back to sRGB for display
rgb_result = convert_color(xyz_adapted, sRGBColor)
return {
'original_xyz': xyz_original.get_value_tuple(),
'adapted_xyz': xyz_adapted.get_value_tuple(),
'final_rgb': rgb_result.get_value_tuple(),
'rgb_hex': rgb_result.get_rgb_hex(),
'illuminant_change': f"{self.source_illuminant} → {self.target_illuminant}"
}
# Example usage
pipeline = ColorManagementPipeline(source_illuminant='d65', target_illuminant='d50')
# Process an sRGB color
input_color = sRGBColor(rgb_r=0.8, rgb_g=0.4, rgb_b=0.2)
result = pipeline.process_color(input_color)
print("Color Management Pipeline Result:")
print(f"Original XYZ: {result['original_xyz']}")
print(f"Adapted XYZ: {result['adapted_xyz']}")
print(f"Final RGB: {result['final_rgb']}")
print(f"Hex color: {result['rgb_hex']}")
print(f"Illuminant: {result['illuminant_change']}")from colormath.chromatic_adaptation import apply_chromatic_adaptation
def batch_adapt_colors(color_list, orig_illum, targ_illum, method='bradford'):
"""
Apply chromatic adaptation to batch of XYZ colors.
Parameters:
- color_list: List of (X, Y, Z) tuples
- orig_illum: Original illuminant
- targ_illum: Target illuminant
- method: Adaptation method
Returns:
List of adapted (X, Y, Z) tuples
"""
adapted_colors = []
for xyz in color_list:
adapted = apply_chromatic_adaptation(
val_x=xyz[0],
val_y=xyz[1],
val_z=xyz[2],
orig_illum=orig_illum,
targ_illum=targ_illum,
adaptation=method
)
adapted_colors.append(adapted)
return adapted_colors
# Example: Adapt color palette from A illuminant to D65
palette_a = [
(85.0, 75.0, 25.0), # Warm yellow
(45.0, 40.0, 60.0), # Cool blue
(70.0, 35.0, 35.0), # Red
(25.0, 45.0, 30.0), # Green
(90.0, 90.0, 85.0) # Near white
]
palette_d65 = batch_adapt_colors(palette_a, 'a', 'd65')
print("Palette Adaptation (Illuminant A → D65):")
for i, (orig, adapted) in enumerate(zip(palette_a, palette_d65)):
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})")Each adaptation method uses specific transformation matrices:
# Bradford adaptation matrices (example)
BRADFORD_M = [
[0.8951, 0.2664, -0.1614],
[-0.7502, 1.7135, 0.0367],
[0.0389, -0.0685, 1.0296]
]
BRADFORD_M_INV = [
[0.9869929, -0.1470543, 0.1599627],
[0.4323053, 0.5183603, 0.0492912],
[-0.0085287, 0.0400428, 0.9684867]
]All standard CIE illuminants are supported:
| Method | Speed | Accuracy | Use Case |
|---|---|---|---|
| Bradford | Fast | High | General purpose |
| Von Kries | Fast | Medium | Educational, simple cases |
| XYZ Scaling | Fastest | Low | Quick approximations |
from colormath.color_exceptions import InvalidIlluminantError
try:
result = apply_chromatic_adaptation(50, 40, 30, 'invalid', 'd65')
except InvalidIlluminantError as e:
print(f"Invalid illuminant: {e}")Common errors:
Install with Tessl CLI
npx tessl i tessl/pypi-colormath