CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-colormath

Color math and conversion library for comprehensive color space transformations and color difference calculations.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

chromatic-adaptation.mddocs/

Chromatic Adaptation

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.

Capabilities

Primary Adaptation Function

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
    """

Color Object Adaptation

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
    """

Supported Adaptation Methods

Bradford Method (Recommended)

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:

  • High accuracy across wide range of illuminants
  • Industry standard for color management
  • Used in ICC color profiles
  • Best for most practical applications

Von Kries Method

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:

  • Based on human cone cell responses
  • Simpler than Bradford method
  • Good for educational purposes
  • Less accurate for extreme illuminant changes

XYZ Scaling Method

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:

  • Simplest implementation
  • Fastest computation
  • Least accurate results
  • Only suitable for small illuminant differences

Usage Examples

Basic Illuminant Adaptation

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}")

Color Object Adaptation

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}")

Comparing Adaptation Methods

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}")

Print Production Workflow

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}")

Photography White Balance Correction

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}")

Color Management Pipeline

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']}")

Batch Color Adaptation

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})")

Technical Details

Adaptation Matrix Structure

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]
]

Supported Illuminants

All standard CIE illuminants are supported:

  • D series: D50, D55, D65, D75 (daylight illuminants)
  • A: Tungsten incandescent (2856K)
  • B: Direct sunlight (obsolete)
  • C: Average daylight (obsolete)
  • E: Equal energy illuminant
  • F series: Fluorescent illuminants

Performance Characteristics

MethodSpeedAccuracyUse Case
BradfordFastHighGeneral purpose
Von KriesFastMediumEducational, simple cases
XYZ ScalingFastestLowQuick approximations

Error Handling

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:

  • InvalidIlluminantError: Unknown illuminant name
  • ValueError: Unsupported adaptation method
  • TypeError: Invalid color values

Install with Tessl CLI

npx tessl i tessl/pypi-colormath

docs

chromatic-adaptation.md

color-appearance-models.md

color-conversions.md

color-diff.md

color-objects.md

constants-standards.md

index.md

spectral-density.md

tile.json