Custom Color Spaces

Colorium is designed to be extensible. This guide shows you how to add custom color spaces to the library.

Overview

Adding a custom color space involves:

  1. Defining conversion functions
  2. Adding factory methods
  3. Adding conversion methods
  4. Testing your implementation

Basic Custom Space

Simple Linear Space

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from colorium import Color

# Define conversion functions
def custom_to_rgb(c1, c2, c3):
    """Convert custom space to RGB"""
    # Example: Simple linear transformation
    r = int(c1 * 255)
    g = int(c2 * 255)
    b = int(c3 * 255)
    return {'r': r, 'g': g, 'b': b}

def rgb_to_custom(r, g, b):
    """Convert RGB to custom space"""
    return {
        'c1': r / 255.0,
        'c2': g / 255.0,
        'c3': b / 255.0
    }

# Add to Color class
def add_custom_space(name, to_rgb_func, from_rgb_func):
    """Add a custom color space to Colorium"""

    # Factory method
    def from_func(cls, v1, v2, v3, opacity=1.0):
        rgb = to_rgb_func(v1, v2, v3)
        return cls(rgb['r'], rgb['g'], rgb['b'], opacity)

    # Conversion method
    def to_func(self):
        return from_rgb_func(self.red, self.green, self.blue)

    setattr(Color, f'from_{name}', classmethod(from_func))
    setattr(Color, f'to_{name}', to_func)

# Add custom space
add_custom_space('custom', custom_to_rgb, rgb_to_custom)

# Usage
color = Color.from_custom(1.0, 0.5, 0.0)
custom = color.to_custom()
print(custom)  # {'c1': 1.0, 'c2': 0.5, 'c3': 0.0}

HSV Color Space

Full Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
from colorium import Color

def hsv_to_rgb(h, s, v):
    """Convert HSV to RGB"""
    h = h % 360
    c = v * s
    x = c * (1 - abs((h / 60) % 2 - 1))
    m = v - c

    if 0 <= h < 60:
        r, g, b = c, x, 0
    elif 60 <= h < 120:
        r, g, b = x, c, 0
    elif 120 <= h < 180:
        r, g, b = 0, c, x
    elif 180 <= h < 240:
        r, g, b = 0, x, c
    elif 240 <= h < 300:
        r, g, b = x, 0, c
    else:
        r, g, b = c, 0, x

    return {
        'r': int((r + m) * 255),
        'g': int((g + m) * 255),
        'b': int((b + m) * 255)
    }

def rgb_to_hsv(r, g, b):
    """Convert RGB to HSV"""
    r_n = r / 255.0
    g_n = g / 255.0
    b_n = b / 255.0

    max_val = max(r_n, g_n, b_n)
    min_val = min(r_n, g_n, b_n)
    delta = max_val - min_val

    # Calculate hue
    if delta == 0:
        h = 0
    elif max_val == r_n:
        h = 60 * (((g_n - b_n) / delta) % 6)
    elif max_val == g_n:
        h = 60 * (((b_n - r_n) / delta) + 2)
    else:
        h = 60 * (((r_n - g_n) / delta) + 4)

    # Calculate saturation
    s = 0 if max_val == 0 else delta / max_val

    # Calculate value
    v = max_val

    return {
        'h': round(h, 2),
        's': round(s, 2),
        'v': round(v, 2)
    }

# Add HSV space
def add_hsv_space():
    """Add HSV color space to Colorium"""

    @classmethod
    def from_hsv(cls, h, s, v, opacity=1.0):
        rgb = hsv_to_rgb(h, s, v)
        return cls(rgb['r'], rgb['g'], rgb['b'], opacity)

    def to_hsv(self):
        return rgb_to_hsv(self.red, self.green, self.blue)

    Color.from_hsv = from_hsv
    Color.to_hsv = to_hsv

add_hsv_space()

# Usage
red = Color.from_hsv(0, 1.0, 1.0)
print(red.to_hex_string())  # #FF0000

hsv = red.to_hsv()
print(hsv)  # {'h': 0, 's': 1.0, 'v': 1.0}

# Create color from HSV
pastel = Color.from_hsv(200, 0.3, 0.8)
print(pastel.to_hex_string())

YUV Color Space

YUV Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from colorium import Color

def yuv_to_rgb(y, u, v):
    """Convert YUV to RGB"""
    # Standard YUV to RGB conversion
    r = y + 1.13983 * v
    g = y - 0.39465 * u - 0.58060 * v
    b = y + 2.03211 * u

    return {
        'r': int(max(0, min(255, r * 255))),
        'g': int(max(0, min(255, g * 255))),
        'b': int(max(0, min(255, b * 255)))
    }

def rgb_to_yuv(r, g, b):
    """Convert RGB to YUV"""
    r_n = r / 255.0
    g_n = g / 255.0
    b_n = b / 255.0

    y = 0.299 * r_n + 0.587 * g_n + 0.114 * b_n
    u = -0.14713 * r_n - 0.28886 * g_n + 0.436 * b_n
    v = 0.615 * r_n - 0.51499 * g_n - 0.10001 * b_n

    return {
        'y': round(y, 3),
        'u': round(u, 3),
        'v': round(v, 3)
    }

# Add YUV space
def add_yuv_space():
    """Add YUV color space to Colorium"""

    @classmethod
    def from_yuv(cls, y, u, v, opacity=1.0):
        rgb = yuv_to_rgb(y, u, v)
        return cls(rgb['r'], rgb['g'], rgb['b'], opacity)

    def to_yuv(self):
        return rgb_to_yuv(self.red, self.green, self.blue)

    Color.from_yuv = from_yuv
    Color.to_yuv = to_yuv

add_yuv_space()

# Usage
color = Color.from_yuv(0.5, 0.1, 0.1)
yuv = color.to_yuv()
print(yuv)

XYZ Color Space

XYZ Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from colorium import Color

def xyz_to_rgb(x, y, z):
    """Convert XYZ to RGB"""
    # Linear RGB from XYZ (D65 illuminant)
    r = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z
    g = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z
    b = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z

    # Apply gamma correction (sRGB)
    def gamma_correct(c):
        if c <= 0.0031308:
            return 12.92 * c
        return 1.055 * (c ** (1/2.4)) - 0.055

    r = max(0, min(1, gamma_correct(r)))
    g = max(0, min(1, gamma_correct(g)))
    b = max(0, min(1, gamma_correct(b)))

    return {
        'r': int(r * 255),
        'g': int(g * 255),
        'b': int(b * 255)
    }

def rgb_to_xyz(r, g, b):
    """Convert RGB to XYZ"""
    # Linearize RGB
    def linearize(c):
        c = c / 255.0
        if c <= 0.04045:
            return c / 12.92
        return ((c + 0.055) / 1.055) ** 2.4

    r_lin = linearize(r)
    g_lin = linearize(g)
    b_lin = linearize(b)

    # RGB to XYZ (D65 illuminant)
    x = 0.4124564 * r_lin + 0.3575761 * g_lin + 0.1804375 * b_lin
    y = 0.2126729 * r_lin + 0.7151522 * g_lin + 0.0721750 * b_lin
    z = 0.0193339 * r_lin + 0.1191920 * g_lin + 0.9503041 * b_lin

    return {
        'x': round(x, 4),
        'y': round(y, 4),
        'z': round(z, 4)
    }

# Add XYZ space
def add_xyz_space():
    """Add XYZ color space to Colorium"""

    @classmethod
    def from_xyz(cls, x, y, z, opacity=1.0):
        rgb = xyz_to_rgb(x, y, z)
        return cls(rgb['r'], rgb['g'], rgb['b'], opacity)

    def to_xyz(self):
        return rgb_to_xyz(self.red, self.green, self.blue)

    Color.from_xyz = from_xyz
    Color.to_xyz = to_xyz

add_xyz_space()

# Usage
color = Color.from_xyz(0.5, 0.4, 0.3)
xyz = color.to_xyz()
print(xyz)

Custom Space with Parameters

Parameterized Color Space

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from colorium import Color

class ParametricColorSpace:
    def __init__(self, name, transform_matrix, inverse_matrix):
        self.name = name
        self.transform = transform_matrix
        self.inverse = inverse_matrix

        # Add to Color class
        self._add_to_color()

    def _add_to_color(self):
        """Add this space to Color class"""
        name = self.name

        @classmethod
        def from_space(cls, v1, v2, v3, opacity=1.0):
            # Apply inverse matrix
            r = self.inverse[0][0] * v1 + self.inverse[0][1] * v2 + self.inverse[0][2] * v3
            g = self.inverse[1][0] * v1 + self.inverse[1][1] * v2 + self.inverse[1][2] * v3
            b = self.inverse[2][0] * v1 + self.inverse[2][1] * v2 + self.inverse[2][2] * v3

            r = max(0, min(1, r)) * 255
            g = max(0, min(1, g)) * 255
            b = max(0, min(1, b)) * 255

            return cls(int(r), int(g), int(b), opacity)

        def to_space(self):
            # Apply transform matrix
            v1 = self.transform[0][0] * self.red + self.transform[0][1] * self.green + self.transform[0][2] * self.blue
            v2 = self.transform[1][0] * self.red + self.transform[1][1] * self.green + self.transform[1][2] * self.blue
            v3 = self.transform[2][0] * self.red + self.transform[2][1] * self.green + self.transform[2][2] * self.blue

            return {
                'v1': v1 / 255,
                'v2': v2 / 255,
                'v3': v3 / 255
            }

        setattr(Color, f'from_{name}', classmethod(from_space))
        setattr(Color, f'to_{name}', to_space)

# Usage
# Define a custom linear transform
transform = [
    [0.5, 0.25, 0.25],
    [0.25, 0.5, 0.25],
    [0.25, 0.25, 0.5]
]

inverse = [
    [1.0, -0.5, -0.5],
    [-0.5, 1.0, -0.5],
    [-0.5, -0.5, 1.0]
]

space = ParametricColorSpace('my_space', transform, inverse)
color = Color.from_my_space(1.0, 0.5, 0.0)
result = color.to_my_space()
print(result)

Testing Custom Spaces

Unit Tests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import unittest
from colorium import Color

class TestCustomSpace(unittest.TestCase):
    def test_roundtrip_conversion(self):
        """Test that roundtrip conversion preserves color"""
        original = Color(100, 150, 200)

        # Convert to HSV and back
        hsv = original.to_hsv()
        recreated = Color.from_hsv(hsv['h'], hsv['s'], hsv['v'])

        # Check with tolerance
        self.assertAlmostEqual(original.red, recreated.red, delta=2)
        self.assertAlmostEqual(original.green, recreated.green, delta=2)
        self.assertAlmostEqual(original.blue, recreated.blue, delta=2)

    def test_known_values(self):
        """Test known color values"""
        # Red in HSV should be (0, 1.0, 1.0)
        red = Color(255, 0, 0)
        hsv = red.to_hsv()
        self.assertEqual(hsv['h'], 0)
        self.assertEqual(hsv['s'], 1.0)
        self.assertEqual(hsv['v'], 1.0)

    def test_custom_space(self):
        """Test custom space implementation"""
        # Test that from_xxx and to_xxx work correctly
        color = Color.from_custom(1.0, 0.5, 0.0)
        custom = color.to_custom()

        self.assertAlmostEqual(custom['c1'], 1.0, delta=0.01)
        self.assertAlmostEqual(custom['c2'], 0.5, delta=0.01)
        self.assertAlmostEqual(custom['c3'], 0.0, delta=0.01)

if __name__ == '__main__':
    unittest.main()

Best Practices

Conversion Accuracy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from colorium import Color

def test_accuracy(color, iterations=100):
    """Test conversion accuracy over multiple iterations"""
    current = color

    for i in range(iterations):
        # Convert to custom space and back
        hsv = current.to_hsv()
        current = Color.from_hsv(hsv['h'], hsv['s'], hsv['v'])

    # Check final color
    print(f"Original: {color.to_hex_string()}")
    print(f"Final: {current.to_hex_string()}")
    print(f"Difference: {color.delta_e(current, 'cie2000'):.2f}")

# Test accuracy
test = Color(100, 150, 200)
test_accuracy(test)

Performance Considerations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from colorium import Color
import time

def benchmark_conversion(color, iterations=10000):
    """Benchmark color conversion performance"""
    start = time.time()

    for _ in range(iterations):
        hsv = color.to_hsv()
        color = Color.from_hsv(hsv['h'], hsv['s'], hsv['v'])

    end = time.time()
    print(f"Time for {iterations} conversions: {end - start:.3f}s")

# Benchmark
color = Color(100, 150, 200)
benchmark_conversion(color)

Error Handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from colorium import Color

def safe_custom_conversion(color):
    """Safely convert with error handling"""
    try:
        hsv = color.to_hsv()
        return hsv
    except Exception as e:
        print(f"Conversion failed: {e}")
        return None

# Usage
color = Color(100, 150, 200)
hsv = safe_custom_conversion(color)
if hsv:
    print(f"HSV: {hsv}")

Next Steps


Previous: Advanced Topics Next: Performance Tips →

On this page
18 sections