Search code examples
pythoncolors

How to perform weighted sum of colors in python?


Let's say I have a palette of n colors and I want to mix all colors with a specific weight assigned to each color. Think of the weight as the percent of ink I want to take from each color. For simplification, let's use 3 primary colors:

palette = [(1,0,0),(0,1,0),(0,0,1)]
weight = (0.1,0.1,0.1)

Naturally, the least amount of ink will lead to lighter color. So if I use 10% of each ink, I would expect somewhat closer to white color. But it's not the case in the rgb color scheme:

import numpy as np
import matplotlib.colors as colors
mixed_colors = np.sum([np.multiply(weight[i],palette[i]) for i in range(len(palette))],axis=1)
print(colors.to_hex(mixed_colors))

In this case, I would get #1a1a1a which is closer to the black color. How can I perform weighted sum with the weight represent the "amount of ink" for each color?

For now, please ignore the fact that the weighted summation can result in number greater than 1 for each rgb field.


Solution

  • The issue isn't the color model, it's assumptions about the model and medium.

    A simple linear model for mixing may not be accurate, but it may suffice, depending on the code's purpose.

    In the case of printing ink, a large part of the perceived color is the surface you're printing on, which isn't part of a color model (though it may be part of a color profile).

    Pigments are also subtractive, rather than additive; this can be considered an attribute of the color model. Note RGB is additive, which is one reason it isn't a good model for inks. CMYK is the most-used color model in printing, but even then it's not a direct representation of printed colors. Instead, CMYK colors should be mapped to a profile that's particular to the printer, inks and paper used.

    With your current implementation (and assumption of a subtractive linear color model), you would need to subtract the weighted sum from white.

    white = np.array((1,) * 3)
    # subtractive:
    printed_color = white - mixed_colors
    

    You could also pre-process the palette and transform it into an additive space:

    palette = [white - color for color in palette]
    # or
    palette = white - np.array(palette)
    

    In the case of rendering on screen, what you have as "weight" is the channel opacity. In image formats that have separate layers for each channel, mixing as you require is easily accomplished simply by setting each layer's opacity. For other formats, you can separate the channels and then apply an opacity to each.