Is there an algorithm for color mixing that works like mixing real colors?

Viewed 30301

The common mixing of RGB colors is very different from mixing colors for paintings, it's mixing of light instead mixing of pigments.

For example:

Blue (0,0,255) + Yellow (255,255,0) = Grey (128,128,128)

(It should be Blue + Yellow = Green)

Is there any known algorithm for color mixing that works like mixing real colors?


My approach

I've already tried following:

Converting both colors to HSV and mixing hues (multiplied by coefficient computed from saturation), and a simple average for saturation and value channels. Then I computed average luminance from both colors and adjusted the resulting color to match this luminance. This worked quite well, but the hue mixing was sometimes wrong, e. g.:

Red (Hue 0°) + Blue (Hue 240°) = Green (Hue 120°)

I've figured out that sometimes I need to shift the hue value by 360° (when the difference between hues is greater than 180°).

Red (Hue 360°) + Blue (Hue 240°) = Magenta/fuchsia (Hue 300°)

But this shifting wasn't very good too, e.g.:

Cyan (Hue 179°) + Red (Hue 0°) = Hue 89.5°
Cyan (Hue 181°) + Red (Hue 0°) --> shifting is performed (the difference is greater than 180°)
Cyan (Hue 181°) + Red (Hue 360°) = Hue 270.5°

(Hue 179 + Red) and (Hue 181 + Red) results in two completely different colors.


Then I tried CIE Lab color space (as in Photoshop), which is designed to be closer to how humans perceive the colors.

I used just a simple average for each corresponding two channels, but the results weren't satisfying, for example, I got pink (64, 26, -9.5) out of blue (98, -16, 93) and yellow (30, 68, -112). These coefficients were taken from Photoshop.

Maybe if I used some different operation than average, it could work, but I don't know what.


CMYK didn't work too, results are just like in RGB or LAB.


It seems that neither the trivial additive nor subtractive color mixing in any of these color spaces yields natural results.


Working implementations

Krita – Painterly mixer

Raster graphics editor Krita had a working implementation of more realistic color mixing at some point: http://commit-digest.org/issues/2007-08-12/ (Painterly mixer plugin)

They say it is the first public application that implements special technology using Kubelka and Munk equations that describe the behavior of pigments.

Here's a video of Krita color mixing: https://www.youtube.com/watch?v=lyLPZDVdQiQ

Paper by FiftyThree

There's also article about color blending in the Paper app for iOS developed by FiftyThree. They describe how they innovate and experiment in the area and also offer samples of mixing blue and yellow that results in green. However, the actual process or algorithm isn't really described there.

Quoting:

"In searching for a good blending algorithm, we initially tried interpolating across various color-spaces: RGB, HSV, and HSL, then CieLAB and CieLUV. The results were disappointing," says Chen. "We know red and yellow should yield orange, or that red and blue should make purple—but there isn't any way to arrive at these colors no matter what color-space you use. There's an engineering axiom: Do the simplest thing that could possibly work. Well, we had now tried the easiest possible approaches and they didn't feel even remotely right."

It seems that same as Krita, Paper implements the Kubelka-Munk model:

[...] the Kubelka-Munk model had at least six values for each color, including reflection and absorption values for each of the RGB colors. "While the appearance of a color on a screen can be described in three dimensions, the blending of color actually is happening in a six-dimensional space," explains Georg Petschnigg, FiftyThree's co-founder and CEO. The Kubelka-Munk paper had allowed the team to translate an aesthetic problem into a mathematical framework. [...]

From all this information, it seems that implementation based on the Kubelka-Munk model could be the way forward and offer results that are much closer to reality.

Even though it looks like a complicated process, I haven't yet seen much good information on how to implement something like this.


Related questions

These questions were posted after this one all relating to the same thing.

None of them really has the answer.


Other related links and resources

10 Answers

There is a novel (Feb 2022) technique showcased in 2 minute papers.

Autors call it the "Mixbox", and the technique is open source (algorithm can be found on github, mind the license though). Currently Rebelle 5 is implementing this color mixing technique, more programs may follow in the nearest future.

Imo the strongest point of this is its simplicity in implementation and how easily you can use it out of the box. Their paper is also (supposedly -- I didn't read it) well written.

Wondering if calculation of inversion of the RGB value work. Since it's about subtraction of lights, technically the subtraction part can be calculated by simple math.

For example cyan + yellow

cyan = 0x00ffff yellow = 0xffff00

Their inversions are 0xff0000 and 0x0000ff, meaning they absorbed red and blue lights completely. Their 1:1 mixture should absorb half of red and blue lights (since the other half of the mixture can still reflects some red and blue light), which is consistent with (0xff0000 + 0x00ffff) / 2 = 0x7f007f. Now we subtract the value from 0xffffff we have 0x80ff80 which is green!

I would like to add that I have creted a library in python that handles this problem in a way that spares you from having to implement complex interpolation and color system convertion functions. It is called colorir.

Example of blue + yellow:

PolarGrad(["0000ff", "ffff00"]).perc(0.5)

Gives you: "#00c5a1"

enter image description here

Behind the curtains, colorir interpolates the colors in HCLuv, a perceptually uniform color system. This does not mimic a "real" mixture of colors in the sense that it wont account for the subtractive properties of real pigments (i.e. the final mixed color will not look darker than the originals). It will, however, mix the colors as in finding what the almost exact average of the visual properties of the two colors look like. The following steps are all handled automatically by the PolarGrad class:

  1. Interpret the value of the input colors (in this case the hex rgb values "0000ff" - blue and "ffff00" - yellow)
  2. Convert these colors to a perceptually uniform color coordinate system (by default HCLuv, but other color spaces can be used)
  3. Sample a color from the middle of the gradient (0.5)
  4. Convert the sampled color back to RGB or other format that you may want

Mixing Colors like Pigments

  1. Remove white from all colors, keeping the white parts and color parts
  2. Average the RGB values of the white parts removed from the colors
  3. Average the RGB values of the color parts
  4. Take out the white from the averaged color parts
  5. Half the white value removed and add that value to the Green of the averaged color parts
  6. Add the averaged white parts back in and make whole number

Javascript Implementation

// rgbColor is an array of colors,
// where each color is an array with the three color values (RGB, 0 to 255)
function mixRGB(rgbColor) {
        function sumColors(summedColors, nextColor) {
            return summedColors.map(
                (summedColor, i) => nextColor[i] + summedColor,
            );
        }

        function averageColors(colors) {
            return colors
                .reduce(sumColors, [0, 0, 0])
                .map(c => c/colors.length);
        }

        // Remove white from all colors
        const whiteParts = [];
        const colorParts = [];
        rgbColor.forEach(color => {
            const whiteVal = Math.min(...color);
            whiteParts.push([whiteVal, whiteVal, whiteVal]);
            colorParts.push(color.map(val => val - whiteVal));
        });
        
        // Average the whites from each selection
        const averagedWhite = averageColors(whiteParts);
        // Average all non-white colors
        let averagedColor = averageColors(colorParts);

        // Take out the white from the averaged colors
        const whitePart = Math.min(...averagedColor);
        averagedColor = averagedColor.map(color => color - whitePart);

        // Half the white value removed and add that value to the Green
        averagedColor[1] += (whitePart / 2);
        
        // Add the averaged white back in and make whole number
        averagedColor = averagedColor.map((color, i) => Math.floor(color + averagedWhite[i]));
    
        return averagedColor;
  }

I built a color game based off this you can try

Related