ChatGPT解决这个技术问题 Extra ChatGPT

Algorithm for Additive Color Mixing for RGB Values

I'm looking for an algorithm to do additive color mixing for RGB values.

Is it as simple as adding the RGB values together to a max of 256?

(r1, g1, b1) + (r2, g2, b2) =
    (min(r1+r2, 256), min(g1+g2, 256), min(b1+b2, 256))  
Why do you need this? Simple addition may not always give the results you want.
Ideally it would represent color blending as if it were colored light. I may be incorrect, but I thought that's what additive color mixing represents. Maybe I'm wrong.
You absolutly right, it should be min.
Typically, the color channel values range from 0 to 255, not 1 to 256.

M
Markus Jarderot

To blend using alpha channels, you can use these formulas:

r = new Color();
r.A = 1 - (1 - fg.A) * (1 - bg.A);
if (r.A < 1.0e-6) return r; // Fully transparent -- R,G,B not important
r.R = fg.R * fg.A / r.A + bg.R * bg.A * (1 - fg.A) / r.A;
r.G = fg.G * fg.A / r.A + bg.G * bg.A * (1 - fg.A) / r.A;
r.B = fg.B * fg.A / r.A + bg.B * bg.A * (1 - fg.A) / r.A;

fg is the paint color. bg is the background. r is the resulting color. 1.0e-6 is just a really small number, to compensate for rounding errors.

NOTE: All variables used here are in the range [0.0, 1.0]. You have to divide or multiply by 255 if you want to use values in the range [0, 255].

For example, 50% red on top of 50% green:

// background, 50% green
var bg = new Color { R = 0.00, G = 1.00, B = 0.00, A = 0.50 };
// paint, 50% red
var fg = new Color { R = 1.00, G = 0.00, B = 0.00, A = 0.50 };
// The result
var r = new Color();
r.A = 1 - (1 - fg.A) * (1 - bg.A); // 0.75
r.R = fg.R * fg.A / r.A + bg.R * bg.A * (1 - fg.A) / r.A; // 0.67
r.G = fg.G * fg.A / r.A + bg.G * bg.A * (1 - fg.A) / r.A; // 0.33
r.B = fg.B * fg.A / r.A + bg.B * bg.A * (1 - fg.A) / r.A; // 0.00

Resulting color is: (0.67, 0.33, 0.00, 0.75), or 75% brown (or dark orange).

You could also reverse these formulas:

var bg = new Color();
if (1 - fg.A <= 1.0e-6) return null; // No result -- 'fg' is fully opaque
if (r.A - fg.A < -1.0e-6) return null; // No result -- 'fg' can't make the result more transparent
if (r.A - fg.A < 1.0e-6) return bg; // Fully transparent -- R,G,B not important
bg.A = 1 - (1 - r.A) / (1 - fg.A);
bg.R = (r.R * r.A - fg.R * fg.A) / (bg.A * (1 - fg.A));
bg.G = (r.G * r.A - fg.G * fg.A) / (bg.A * (1 - fg.A));
bg.B = (r.B * r.A - fg.B * fg.A) / (bg.A * (1 - fg.A));

or

var fg = new Color();
if (1 - bg.A <= 1.0e-6) return null; // No result -- 'bg' is fully opaque
if (r.A - bg.A < -1.0e-6) return null; // No result -- 'bg' can't make the result more transparent
if (r.A - bg.A < 1.0e-6) return bg; // Fully transparent -- R,G,B not important
fg.A = 1 - (1 - r.A) / (1 - bg.A);
fg.R = (r.R * r.A - bg.R * bg.A * (1 - fg.A)) / fg.A;
fg.G = (r.G * r.A - bg.G * bg.A * (1 - fg.A)) / fg.A;
fg.B = (r.B * r.A - bg.B * bg.A * (1 - fg.A)) / fg.A;

The formulas will calculate that background or paint color would have to be to produce the given resulting color.

If your background is opaque, the result would also be opaque. The foreground color could then take a range of values with different alpha values. For each channel (red, green and blue), you have to check which range of alphas results in valid values (0 - 1).


What can you do about the "division by zero" problem, if fg.A and bg.A is both 0.0? This results is f.A = 0. And then I got a division by zero problem i.e. here: r.R = fg.R * fg.A / r.A + bg.R * bg.A * (1 - fg.A) / r.A;
How do these formulas needed to be adjusted to blend a fg color with alpha onto an opaque bg? I have my desired result color and the bg color and with that information I need to determine what the fg color needs to be.
M
Mark Ransom

It depends on what you want, and it can help to see what the results are of different methods.

If you want

Red + Black        = Red
Red + Green        = Yellow
Red + Green + Blue = White
Red + White        = White 
Black + White      = White

then adding with a clamp works (e.g. min(r1 + r2, 255)) This is more like the light model you've referred to.

If you want

Red + Black        = Dark Red
Red + Green        = Dark Yellow
Red + Green + Blue = Dark Gray
Red + White        = Pink
Black + White      = Gray

then you'll need to average the values (e.g. (r1 + r2) / 2) This works better for lightening/darkening colors and creating gradients.


Am I wrong, or your second option is not an "Additive Color Mixing" but a "Substractive Color Mixing"? (and the title of the question is wrong, but doesn't matter)
@AlfonsoNishikawa no, it's not subtractive - the second option would best be described as "Color Blending". Subtractive would only be appropriate if you were working in CMYK.
D
Downgoat

Fun fact: Computer RGB values are derived from the square root of photon flux. So as a general function, your math should take that into account. The general function for this for a given channel is:

blendColorValue(a, b, t)
    return sqrt((1 - t) * a^2 + t * b^2)

Where a and b are the colors to blend, and t is a number from 0-1 representing the point in the blend you want between a and b.

The alpha channel is different; it doesn't represent photon intensity, just the percent of background that should show through; so when blending alpha values, the linear average is enough:

blendAlphaValue(a, b, t)
    return (1-t)*a + t*b;

So, to handle blending two colors, using those two functions, the following pseudocode should do you good:

blendColors(c1, c2, t)
    ret
    [r, g, b].each n ->
        ret[n] = blendColorValue(c1[n], c2[n], t)
    ret.alpha = blendAlphaValue(c1.alpha, c2.alpha, t)
    return ret

Incidentally, I long for a programming language and keyboard that both permits representing math that (or more) cleanly (the combining overline unicode character doesn't work for superscripts, symbols, and a vast array of other characters) and interpreting it correctly. sqrt((1-t)*pow(a, 2) + t * pow(b, 2)) just doesn't read as clean.


Thank you, this was what I was looking for when searching for color mixing algorithm. The problem with the answers above (using linear mixing of rgb-values) is that they yield unnatural looking transitions between colours, with spots that are too dark.
Square root is not quite correct. The numbers being manipulated in our programs have been subjected to gamma correction because our eyes respond to light non-linearly, and this makes better use of the limited range. The typical gamma correction is 2.2, which is close to but not quite the same as the 2.0 you recommend in this answer. And in some cases it's better to work in gamma-corrected space, to generate a gray-scale gradient for example.
I've been playing with a toy that alternates two colors in rapid succession until they converge on a single color, and this mixing algorithm looks perfect! Clearly this is the correct math, I never knew it! Thank you!!
D
Dani van der Meer

Few points:

I think you want to use min instead of max

I think you want to use 255 instead of 256

This will give:

(r1, g1, b1) + (r2, g2, b2) = (min(r1+r2, 255), min(g1+g2, 255), min(b1+b2, 255))

However, The "natural" way of mixing colors is to use the average, and then you don't need the min:

(r1, g1, b1) + (r2, g2, b2) = ((r1+r2)/2, (g1+g2)/2, (b1+b2)/2)


Sorry. I am halucingating. min(a,b) will give the least value of a and b. min(a,constant) will constrain a below the constant.
@Markus, they are both the same! (will give the least value).
C
Community

Javascript function to blend rgba colors

c1,c2 and result - JSON's like c1={r:0.5,g:1,b:0,a:0.33}

    var rgbaSum = function(c1, c2){
       var a = c1.a + c2.a*(1-c1.a);
       return {
         r: (c1.r * c1.a  + c2.r * c2.a * (1 - c1.a)) / a,
         g: (c1.g * c1.a  + c2.g * c2.a * (1 - c1.a)) / a,
         b: (c1.b * c1.a  + c2.b * c2.a * (1 - c1.a)) / a,
         a: a
       }
     } 

Could this be modified to handle more than two colors?
Or would you just rgbaSum(rgbaSum(c1,c2),rgbaSum(c3,c4)). Would that be accurate?
p
patapouf_ai

PYTHON COLOUR MIXING THROUGH ADDITION IN CMYK SPACE

One possible way to do this is to first convert the colours to CMYK format, add them there and then reconvert to RGB.

Here is an example code in Python:

rgb_scale = 255
cmyk_scale = 100


def rgb_to_cmyk(self,r,g,b):
    if (r == 0) and (g == 0) and (b == 0):
        # black
        return 0, 0, 0, cmyk_scale

    # rgb [0,255] -> cmy [0,1]
    c = 1 - r / float(rgb_scale)
    m = 1 - g / float(rgb_scale)
    y = 1 - b / float(rgb_scale)

    # extract out k [0,1]
    min_cmy = min(c, m, y)
    c = (c - min_cmy) 
    m = (m - min_cmy) 
    y = (y - min_cmy) 
    k = min_cmy

    # rescale to the range [0,cmyk_scale]
    return c*cmyk_scale, m*cmyk_scale, y*cmyk_scale, k*cmyk_scale

def cmyk_to_rgb(self,c,m,y,k):
    """
    """
    r = rgb_scale*(1.0-(c+k)/float(cmyk_scale))
    g = rgb_scale*(1.0-(m+k)/float(cmyk_scale))
    b = rgb_scale*(1.0-(y+k)/float(cmyk_scale))
    return r,g,b

def ink_add_for_rgb(self,list_of_colours):
    """input: list of rgb, opacity (r,g,b,o) colours to be added, o acts as weights.
    output (r,g,b)
    """
    C = 0
    M = 0
    Y = 0
    K = 0

    for (r,g,b,o) in list_of_colours:
        c,m,y,k = rgb_to_cmyk(r, g, b)
        C+= o*c
        M+=o*m
        Y+=o*y 
        K+=o*k 

    return cmyk_to_rgb(C, M, Y, K)

The result to your question would then be (assuming a half-half mixture of your two colours:

r_mix, g_mix, b_mix = ink_add_for_rgb([(r1,g1,b1,0.5),(r2,g2,b2,0.5)])

where the 0.5's are there to say that we mix 50% of the first colour with 50% of the second colour.


J
John Gietzen

Yes, it is as simple as that. Another option is to find the average (for creating gradients).

It really just depends on the effect you want to achieve.

However, when Alpha gets added, it gets complicated. There are a number of different methods to blend using an alpha.

An example of simple alpha blending: http://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending


n
normanius

Find here the mixing methods suggested by Fordi and Markus Jarderot in one python function that gradually mixes or blends between two colors A and B.

The "mix" mode is useful to interpolate between two colors. The "blend" mode (with t=0) is useful to compute the resulting color if one translucent color is painted on top of another (possibly translucent) color. gamma correction leads to nicer results because it takes into consideration the fact that physical light intensity and perceived brightness (by humans) are related non-linearly.

import numpy as np

def mix_colors_rgba(color_a, color_b, mode="mix", t=None, gamma=2.2):
    """
    Mix two colors color_a and color_b.

    Arguments:
        color_a:    Real-valued 4-tuple. Foreground color in "blend" mode.
        color_b:    Real-valued 4-tuple. Background color in "blend" mode.
        mode:       "mix":   Interpolate between two colors.
                    "blend": Blend two translucent colors.
        t:          Mixing threshold.
        gamma:      Parameter to control the gamma correction.

    Returns: 
        rgba:       A 4-tuple with the result color.

    To reproduce Markus Jarderot's solution:
            mix_colors_rgba(a, b, mode="blend", t=0, gamma=1.)
    To reproduce Fordi's solution:
            mix_colors_rgba(a, b, mode="mix", t=t, gamma=2.)
    To compute the RGB color of a translucent color on white background:
            mix_colors_rgba(a, [1,1,1,1], mode="blend", t=0, gamma=None)
    """
    assert(mode in ("mix", "blend"))
    assert(gamma is None or gamma>0)
    t = t if t is not None else (0.5 if mode=="mix" else 0.)
    t = max(0,min(t,1))
    color_a = np.asarray(color_a)
    color_b = np.asarray(color_b)
    if mode=="mix" and gamma in (1., None):
        r, g, b, a = (1-t)*color_a + t*color_b
    elif mode=="mix" and gamma > 0:
        r,g,b,_ = np.power((1-t)*color_a**gamma + t*color_b**gamma, 1/gamma)
        a = (1-t)*color_a[-1] + t*color_b[-1]
    elif mode=="blend":
        alpha_a = color_a[-1]*(1-t)
        a = 1 - (1-alpha_a) * (1-color_b[-1])
        s = color_b[-1]*(1-alpha_a)/a
        if gamma in (1., None):
            r, g, b, _ = (1-s)*color_a + s*color_b
        elif gamma > 0:
            r, g, b, _ = np.power((1-s)*color_a**gamma + s*color_b**gamma,
                                  1/gamma)

    return tuple(np.clip([r,g,b,a], 0, 1))

See below how this can be used. In "mix" mode the left and right colors match exactly color_a and color_b. In "blend" mode, the left color at t=0 is the color that results if color_a is blended over color_b (and a white background). In the example, color_a then is made increasingly translucent until one arrives at color_b.

Note that blending and mixing are equivalent if the alpha values are 1.0.

https://i.stack.imgur.com/4Y2fQ.png

For completeness, here the code to reproduce the above plot.

import matplotlib.pyplot as plt
import matplotlib as mpl

def plot(pal, ax, title):
    n = len(pal)
    ax.imshow(np.tile(np.arange(n), [int(n*0.20),1]),
              cmap=mpl.colors.ListedColormap(list(pal)),
              interpolation="nearest", aspect="auto")
    ax.set_xticks([])
    ax.set_yticks([])
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.set_title(title)

_, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=4,ncols=1)

n = 101
ts = np.linspace(0,1,n)
color_a = [1.0,0.0,0.0,0.7] # transparent red
color_b = [0.0,0.0,1.0,0.8] # transparent blue

plot([mix_colors_rgba(color_a, color_b, t=t, mode="mix", gamma=None)
      for t in ts], ax=ax1, title="Linear mixing")
plot([mix_colors_rgba(color_a, color_b, t=t, mode="mix", gamma=2.2)
      for t in ts], ax=ax2, title="Non-linear mixing (gamma=2.2)")
plot([mix_colors_rgba(color_a, color_b, t=t, mode="blend", gamma=None)
      for t in ts], ax=ax3, title="Linear blending")
plot([mix_colors_rgba(color_a, color_b, t=t, mode="blend", gamma=2.2)
      for t in ts], ax=ax4, title="Non-linear blending (gamma=2.2)")
plt.tight_layout()
plt.show()

Formulas:
    Linear mixing (gamma=1):
                r,g,b,a:    (1-t)*x + t*y
    Non-linear mixing (gama≠1):
                r,g,b:      pow((1-t)*x**gamma + t*y**gamma, 1/gamma)
                a:          (1-t)*x + t*y
    Blending (gamma=1):
                a:          1-(1-(1-t)*x)*(1-y)
                s:          alpha_b*(1-alpha_a)*a
                r,g,b:      (1-s)*x + s*y
    Blending (gamma≠1):
                a:          1-(1-(1-t)*x)*(1-y)
                s:          alpha_b*(1-alpha_a)/a
                r,g,b:      pow((1-s)*x**gamma + s*y**gamma, 1/gamma)

And finally, here a useful read about gamma correction.


m
marsze

When I came here I didn't find the "additive color mixing" algorithm I was actually looking for, which is also available in Photoshop and is described as "Screen" on Wikipedia. (Aka "brighten" or "invert multiply".) It produces a result similar to two light sources being combined.

With Screen blend mode the values of the pixels in the two layers are inverted, multiplied, and then inverted again. This yields the opposite effect to multiply. The result is a brighter picture.

Here it is:

// (rgb values are 0-255)
function screen(color1, color2) {
    var r = Math.round((1 - (1 - color1.R / 255) * (1 - color2.R / 255)) * 255);
    var g = Math.round((1 - (1 - color1.G / 255) * (1 - color2.G / 255)) * 255);
    var b = Math.round((1 - (1 - color1.B / 255) * (1 - color2.B / 255)) * 255);
    return new Color(r, g, b);
}

I hope this is what I am looking for, would this work for blending two different shades / tones of colors? For example, two blue values defined as: blue1= (17, 43, 154) and blue2 = (22, 56, 186)
@Mohd Depends on what result you expect. Since this is additive mixing, the result color will always be brighter, because it adds both colors. If you want to "blend" the colors, resulting in a tone that is somewhere in-between, then no. For that, maybe use b3 = {r: (b1.r + b2.r) / 2, g: (b1.g + b2.g) /2, ...
You are correct; indeed the average over each channel did the job.
T
Top-Master

Have written/used something like @Markus Jarderot's sRGB blending answer (which is not gamma corrected since that is the default legacy) using C++

//same as Markus Jarderot's answer
float red, green, blue;
alpha = (1.0 - (1.0 - back.alpha)*(1.0 - front.alpha));
red   = (front.red   * front.alpha / alpha + back.red   * back.alpha * (1.0 - front.alpha));
green = (front.green * front.alpha / alpha + back.green * back.alpha * (1.0 - front.alpha));
blue  = (front.blue  * front.alpha / alpha + back.blue  * back.alpha * (1.0 - front.alpha));

//faster but equal output
alpha = (1.0 - (1.0 - back.alpha)*(1.0 - front.alpha));
red   = (back.red   * (1.0 - front.alpha) + front.red   * front.alpha);
green = (back.green * (1.0 - front.alpha) + front.green * front.alpha);
blue  = (back.blue  * (1.0 - front.alpha) + front.blue  * front.alpha);

//even faster but only works when all values are in range 0 to 255
int red, green, blue;
alpha = (255 - (255 - back.alpha)*(255 - front.alpha));
red   = (back.red   * (255 - front.alpha) + front.red   * front.alpha) / 255;
green = (back.green * (255 - front.alpha) + front.green * front.alpha) / 255;
blue  = (back.blue  * (255 - front.alpha) + front.blue  * front.alpha) / 255;

more info: what-every-coder-should-know-about-gamma


n
ngontjar

I was working on a similar problem and ended up here, but had to write my own implementation in the end. I wanted to basically "overlay" the new foreground color over the existing background color. (And without using an arbitrary midpoint like t. I believe my implementation is still "additive.") This also seems to blend very cleanly in all of my test-cases.

Here, new_argb just converts the int into a struct with 4 unsigned char so I can reduce the amount of bit-shifts.

int blend_argb(int foreground, int background)
{
    t_argb fg;
    t_argb bg;
    t_argb blend;
    double ratio;

    fg = new_argb(foreground);
    bg = new_argb(background);

    // If background is transparent,
    // use foreground color as-is and vice versa.
    if (bg.a == 255)
        return (foreground);
    if (fg.a == 255)
        return (background);

    // If the background is fully opaque,
    // ignore the foreground alpha. (Or the color will be darker.)
    // Otherwise alpha is additive.
    blend.a = ((bg.a == 0) ? 0 : (bg.a + fg.a));

    // When foreground alpha == 0, totally covers background color.
    ratio = fg.a / 255.0;
    blend.r = (fg.r * (1 - ratio)) + (bg.r * ratio);
    blend.g = (fg.g * (1 - ratio)) + (bg.g * ratio);
    blend.b = (fg.b * (1 - ratio)) + (bg.b * ratio);

    return (blend.a << 24 | blend.r << 16 | blend.g << 8 | blend.b);
}

For context, in my environment I'm writing color ints into a 1D pixel array, which is initialized with 0-bytes and increasing the alpha will make the pixel tend towards black. (0 0 0 0 would be opaque black and 255 255 255 255 would be transparent white... aka black.)


f
fyngyrz

Here's a highly optimized, standalone c++ class, public domain, with floating point and two differently optimized 8-bit blending mechanisms in both function and macro formats, as well as a technical discussion of both the problem at hand and how to, and the importance of, optimization of this issue:

https://github.com/fyngyrz/colorblending


Link-only answers aren't a good idea, links go bad and you're left with something worthless. If you can't post the code here, at least describe and summarize it.
I'd be happy to post the code here if SO had even slightly as good a mechanism as Github does. But it doesn't. Not to mention SO's obnoxious licensing policies. As for a link to Github going bad, that's about as likely as a link to SO going bad. Links are what the web is all about. What I posted is 100% relevant to this question, and a superb set of solutions to this specific problem, which I have placed in the public domain. You are not being in any way helpful or useful with your remarks, you're just trying to make trouble.
No, I'm trying to establish that StackOverflow has answers, not links to the answers. Google already has plenty of those.
I gave an answer relevant to the question. The answer contains a link to the code. SO doesn't have a mechanism to include a significant c class, nor does it have acceptable licensing for code that appears on the site, so what you want simply isn't possible. The choice is between answering including a link, or not answering at all. The latter does no one any good. The former is relevant, informative, and highly useful. As for Google, results there are a cesspit of irrelevancy and ads. So perhaps you should consider rethinking your position. Last word is yours. I'm content with my answer.
It does seem a bit superficial to downvote a guy for posting a link to a solution residing in his own github repository.
N
Ningrong Ye

Thank you Markus Jarderot, Andras Zoltan and hkurabko; here is the Python code for blending a list of RGB images.

Using Markus Jarderot's code we can generate RGBA color, then i use Andras Zoltan and hkurabko's method to trans RGBA to RGB.

Thank you!

import numpy as np
def Blend2Color(C1,C2):
    c1,c1a=C1
    c2,c2a=C2
    A = 1 - (1 - c1a) * (1 - c2a);
    if (A < 1.0e-6): 
        return (0,0,0) #Fully transparent -- R,G,B not important
    Result=(np.array(c1)*c1a+np.array(c2)*c2a*(1-c1a))/A
    return Result,A
def RGBA2RGB(RGBA,BackGround=(1,1,1)):# whilt background
    A=RGBA[-1]
    RGB=np.add(np.multiply(np.array(RGBA[:-1]),A),
               np.multiply(np.array(BackGround),1-A))
    return RGB

def BlendRGBList(Clist,AlphaList=None,NFloat=2,ReturnRGB=True,
                 RGB_BackGround=(1,1,1)):
    N=len(Clist)
    if AlphaList==None:
        ClistUse=Clist.copy()
    else:
        if len(AlphaList)==N:
            AlphaListUse=np.multiply(AlphaList,10**NFloat).astype(int)
            ClistUse=np.repeat(np.array(Clist), AlphaListUse, axis=0)
        else:
            raise('len of AlphaList must equal to len of Clist!')
    while N!=1:
        temp=ClistUse.copy()
        ClistUse=[]
        for C in temp[:-1]:
            c1,a1=C
            c2,a2=temp[-1]
            ClistUse.append(Blend2Color(C1=(c1,a1*(1-1/N)),C2=(c2,a2*1/N)))
        N=len(ClistUse)
    Result=np.append(ClistUse[0][0],ClistUse[0][1])
    if ReturnRGB:
        Result=RGBA2RGB(Result,BackGround=RGB_BackGround)

    return Result

Test

BlendRGBList([[(1,0,0),1],[(0,1,0),1]],ReturnRGB=True)
#array([0.75, 0.5 , 0.25])
BlendRGBList([[(1,0,0),1],[(0,1,0),1]],ReturnRGB=False)
#array([0.66666667, 0.33333333, 0.        , 0.75      ])