2
votes

There is a mismatch between light colors and paint: While a physicist will say that the three primary colors are red, green and blue, a painter will give red, blue and yellow as primary colors. Indeed, when painting with watercolors, you can't mix a yellow from red, green and blue, and instead of mixing orange you'll only get brown.

Here is what I'm trying to do: From two given RGB colors I'd like to calculate the RGB code for the combined color, and I'd like the colors to blend like watercolor would on paper. As I understood the calculation normally would look like this:

  • #FF0000 + #0000FF = #880088 ((FF+00)/2 = 88, (00+00)/2 = 00, (00+FF)/2 = 88), so red and blue gives purple (as it should)
  • #FF0000 + #FFFF00 = #FF8800 ((FF+FF)/2 = FF, (00+FF)/2 = 88, (00+00)/2 = 00), so red and yellow gives orange (as it should)

However, when mixing blue and yellow, the result is grey:

  • #0000FF + #FFFF00 = #888888 ((00+FF)/2 = 88, (00+FF)/2 = 88, (FF+00)/2 = 88) = GREY

while on paper you'd expect to get green (#008800) and could never get grey when mixing colors.

So my question is, how can I kind of exchange green with yellow as primary color, and how can I then calculate mixed colors which follow the laws of paint rather than those of light colors?

4

4 Answers

5
votes

There's no simple physical model which will do this, the painter's colors have very elaborate interactions with light. Fortunately we have computers, which are not limited to modelling the physical world - we can make them do any arbitrary thing we'd like!

The first step is to create a color wheel with the hue distribution that we require, with red, yellow, and blue at 120 degree increments. There are many examples on the web. I've created one here that only has fully saturated colors so that it can be used to generate the full RGB gamut. The colors on the wheel are completely arbitrary; I've set Orange (60°) to (255,160,0) because the midpoint between Red and Yellow was too red, and I've moved pure Blue (0,0,255) to 250° instead of 240° so that the 240° Blue would look better.

RYB hue color wheel

Remembering the experiments of my childhood, when you mix equal amounts of Red, Yellow, and Blue together you get an indistinct brownish gray. I've chosen a suitable color which you can see at the center of the color wheel; in the code I affectionately call it "mud".

To get every conceivable color you need more than Red, Yellow, and Blue, you also need to mix White and Black. For example you get Pink by mixing Red and White, and you get Brown by mixing Orange (Yellow+Red) with Black.

The conversion works with ratios, not absolute numbers. As with real paint there's no difference between mixing 1 part red and 1 part yellow, versus 100 parts red and 100 parts yellow.

The code is presented in Python but it shouldn't be hard to convert to other languages. The trickiest part is adding the Red, Yellow and Blue to create a hue angle. I use vector addition and convert back to an angle with atan2. Almost everything else is done with linear interpolation (lerp).

# elementary_colors.py
from math import degrees, radians, atan2, sin, cos

red = (255, 0, 0)
orange = (255, 160, 0)
yellow = (255, 255, 0)
green = (0, 255, 0)
cyan = (0, 255, 255)
blue = (0, 0, 255)
magenta = (255, 0, 255)
white = (255, 255, 255)
black = (0, 0, 0)
mud = (94, 81, 74)

colorwheel = [(0, red), (60, orange), (120, yellow), (180, green),
              (215, cyan), (250, blue), (330, magenta), (360, red)]

red_x, red_y = cos(radians(0)), sin(radians(0))
yellow_x, yellow_y = cos(radians(120)), sin(radians(120))
blue_x, blue_y = cos(radians(240)), sin(radians(240))

def lerp(left, right, left_part, total):
    if total == 0:
        return left
    ratio = float(left_part) / total
    return [l * ratio + r * (1.0 - ratio) for l,r in zip(left, right)]

def hue_to_rgb(deg):
    deg = deg % 360
    previous_angle, previous_color = colorwheel[0]
    for angle, color in colorwheel:
        if deg <= angle:
            return lerp(previous_color, color, angle - deg, angle - previous_angle)
        previous_angle = angle
        previous_color = color

def int_rgb(rgb):
    return tuple(int(c * 255.99 / 255) for c in rgb)

def rybwk_to_rgb(r, y, b, w, k):
    if r == 0 and y == 0 and b == 0:
        rgb = white
    else:
        hue = degrees(atan2(r * red_y + y * yellow_y + b * blue_y,
                            r * red_x + y * yellow_x + b * blue_x))
        rgb = hue_to_rgb(hue)
        rgb = lerp(mud, rgb, min(r, y, b), max(r, y, b))
    gray = lerp(white, black, w, w+k)
    rgb = lerp(rgb, gray, r+y+b, r+y+b+w+k)
    return int_rgb(rgb)
1
votes

If you mix red, blue and yellow paints together, you get a murky brown, not black. That's because "red, blue and yellow" are not really the exact primary colours there. Cyan, Magenta and Yellow are, which is why printers work in CMYK (K being black).

So what you're actually asking is how to convert an RGB colour to CMYK, in which case any of these links would help you.

1
votes

I suggest backing away from the notion of primaries, whether RGB or RYB or CMY, etc.

As I understand it, your goal is to mix colors (that are described in RGB color space) in a way that simulates how paint will mix, i.e., mix subtractively. Converting from RGB to CMY or CMYK won't be productive, because you still have to deal with the subtractive mixture calculation, which isn't any easier to do in CMY space.

Instead, let me suggest that you think about converting the RGB colors to spectral reflectance curves. The conversion is fairly simple, and once you've done it, you can do a true subtractive mixture of the reflectance curves, and then convert the result back to RGB.

There is another similar question: https://stackguides.com/questions/10254022/, where this process is discussed in more detail.

0
votes

Cyan, Magenta and Yellow is what it should be Red, Yellow and Blue is a medieval convention.

Apparently it's not an easy switch, and I fell asleep in the middle of the math, but here's one way.

RYB RGB Conversion