50

If I have two colors defined by their RGB values, can I average the Red, Green and Blue values and then combine to define a third color that looks like a visual average of the two?

ie NewColor = (R1+R2)/2,(G1+G2)/2,(B1+B2)/2

EDIT1: Thanks for all the responses. For my current needs, I am only dealing with color pairs that are shades of the same color so I think that averaging them will work. However, I will try converting to Lab Space to make sure that assumption is true and the technique will be useful in the future.

EDIT2: Here are my results FWIW. Color1 and Color2 are my two colors and the two middle columns are the results of averaging in Lab space and averaging RGB respectively. In this case there is not a lot of difference between the two color and so the differences in the output from the averaging techniques is subtle.

visual comparison of color averaging techniques

Ilmari Karonen
  • 49,047
  • 9
  • 93
  • 153
eft
  • 2,509
  • 6
  • 26
  • 24

9 Answers9

34

Several answers suggest converting to Lab color space - which is probably a good approach for more complex color manipulation.

But if you simply need a quick way to take the average of two colors, this can be done in the RGB space. You just have to mind a caveat: You must square the RGB values before averaging them, and then take the root of the result. (If you simply take the average, the result will tend to be too dark.)

Like this:

NewColor = sqrt((R1^2+R2^2)/2),sqrt((G1^2+G2^2)/2),sqrt((B1^2+B2^2)/2)

Here's a great vid which explains why this method is efficient: https://www.youtube.com/watch?v=LKnqECcg6Gw

arntjw
  • 803
  • 7
  • 12
  • 13
    Aha, an answer that recognizes the logarithmic space that images use in the image world! For weighted mixes (i.e. 75% colour A, and 25% colour B), use `NewColor = sqrt(R1^2*w + R2^2*[1 - w]), sqrt(G1^2*w + G2^2*[1 - w]), sqrt(B1^2*w + B2^2*[1 - w])` where w is a weight from 0 to 1. – Dan W Feb 15 '16 at 08:09
  • 5
    This is the answer most people coming to this question via Google are actually looking for. – gd1 Apr 11 '16 at 22:27
  • 1
    How do I generalize this to an arbitrary number of colors? Do I still sqrt(sum of squares) for each channel? – Prashanth Chandra Jan 23 '17 at 09:20
  • 2
    for 3 colors you divide by 3 and use sqrt root as usual. For example `NewColorR = sqrt((R1^2+R2^2+R3^2)/3). This is just for red component. Do same for `green` and `blue` – mitjap Sep 14 '17 at 13:39
  • Anyone knows how to implement this operation using opencv? – gameon67 May 27 '19 at 06:53
  • 1
    In a shorter way, use quadratic mean instead of arithmetic mean. – Ferazhu Sep 12 '20 at 18:18
  • @Ferazhu Sometimes also called Root Mean Square (or RMS). – Brock Brown Jun 09 '23 at 12:29
25

Take a look at the answers to this question.

Basically, you want to convert the colors into something called Lab space, and find their average in that space.

Lab space is a way of representing colours where points that are close to each other are those that look similar to each other to humans.

Community
  • 1
  • 1
Blorgbeard
  • 101,031
  • 48
  • 228
  • 272
  • Interesting. Until now I have been using simply HSL, but Lab looks worth investigating. – MattJ Mar 16 '09 at 08:56
  • Could I use this Lab Space to represent a linear value range to encode values to colours over the full colour range and not only as shades of grey, or different brightness. http://stackoverflow.com/questions/7182318/value-as-colour-representation – Horst Walter Aug 24 '11 at 22:04
  • In other words, colors in Lab space are distributed more uniformly than in most of the other color spaces in terms of human perception. That's why averaged color in Lab represents visual center of the colors better, especially when color difference is large (e.g., Delta E>4). When color difference is smaller, averaging in any color space may be done. – Tae-Sung Shin Jul 26 '17 at 02:17
7

I don't know whether taking a simple average of the components is the "best" from a perceptual point of view (that sounds like a question for a psychologist), but here are a couple of examples using simple component averaging.

alt text

The red-mustard-green one is ugly but the interpolation seems reasonable enough.

Spooky
  • 2,966
  • 8
  • 27
  • 41
7

Averaging in HSL color space might produce better results.

mmcdole
  • 91,488
  • 60
  • 186
  • 222
eugensk
  • 1,882
  • 1
  • 14
  • 20
  • I think he means Lightness, Saturation, Hue. I believe it's the same thing as HSV / HSL - try those in wikipedia. – Blorgbeard Mar 16 '09 at 07:12
5

This is hard. First, a set of RGB values doesn't define a color. They need to be interpreted in light of the color primaries to which they refer (the color space), such as sRGB, Rec.709, Rec.2020, Adobe RGB (1998), etc.

Further, RGB values as we normally encounter them are not proportional to linear light: they are "encoded" using a non-linear function (gamma). And sometimes (in video applications mostly) the value of "black" is not zero, but is offset from zero, usually 16 for 8-bit values. And "white" is not 255 but 235. sRGB and Rec.709 share RGB primaries, but their gamma functions are different.

The color space conversion starts with removing any black offset so that black is zero. If the gamma function has a breakpoint in it (like sRGB and Rec.709 do), you will need to carefully scale the RGB values so that "white" is 1.0.

Then, "decode" the gamma by doing the inverse of the original gamma function. (One answer suggested squaring the values, which is an approximation of gamma decoding.) Now you have linear-light RGB values in some color space. At this point you can convert from that color space to Lab space. Most conversions from RGB to Lab go through an intermediate color space called XYZ.

The steps as nested function calls:

Lab = XYZ2Lab( RGB2XYZ( gamma_decode( offset_and_scale( RGB ), gammaFunction ), RGB color space ) )

(Lab space was developed in the 1976 as an attempt to create a perceptually-uniform warping of the standard CIE XYZ space. (Luv was another attempt.) The idea is that the Euclidean (straight-line) distance between two colors that were just-noticeably different (1 "JND") would be the same distance for any two colors. The distance between two colors in Lab is known as 'delta-E'. The simple delta Euclidean distance formula is now called dE76. See https://en.wikipedia.org/wiki/Color_difference)

In your case, you could average the two Lab colors to get a new Lab color, then reverse all the conversions to get back to RGB in your chosen color space.

This will get you close, but is not guaranteed, simply because "color" is a human perception, not a physical quantity, and has been notoriously difficult to characterize reliably. Lab didn't actually work so well at being perceptually uniform. So rather than fix Lab, they proposed a new, more-complex delta-E function with another warp built-in: DE94. That was better, but not perfect, so another proposal emerged in 2000: DE2000. Also better but not perfect. See that Wiki page above for more info.

If DE2000 is not good enough (or too complex!) you might have a look at an alternative to Lab called ICtCp that is claimed to be more perceptually uniform than Lab.

user1539094
  • 71
  • 1
  • 2
4

Yes. You can average two colors together like that (simple averaging). It's the approach used by OpenGL to blend colors together (e.g., in creating mip maps for rendering distant objects, or rendering a 50% transparent texture). It is fast, simple, and "good enough" for many situations. It isn't completely realistic, however, and probably wouldn't be used on photograph-quality images.

Scott Weaver
  • 7,192
  • 2
  • 31
  • 43
Cybis
  • 9,773
  • 2
  • 36
  • 37
  • Viewing environment and properties of the display have a huge impact on color perception, so I concur that simple averaging is good enough for most situations. – TrayMan Mar 16 '09 at 08:12
1

I think, the answer from arntjw goes in the right direction, and recognizes the logarithmic underlay, as mentioned by Dan W. However, the proper geometric mean is not sqrt((C1^2+C2^2)/2), but sqrt(C1*C2). So the average color would be:

NewColor = sqrt(R1*R2),sqrt(G1*G2),sqrt(B1*B2)

The resulting colors are closer to what we expect. You can generalize to more colors using higher order roots, and weight each color by adding an exponent to its components.

lenov
  • 11
  • 2
  • 2
    Be careful using this for colors that have 0 components. Consider averaging pure red (255, 0, 0) with pure blue (0, 0, 255). The average will be black, when we really expect purple. – Jon Hardy Sep 20 '17 at 14:51
  • 1
    Can you explain more about why this is the proper mean? Also how would you handle averaging more than 2 colors? Would you multiply all of them together and then take the root of how many colors you have? – Kevin Workman Nov 20 '18 at 06:16
0

For the the visually oriented folks, I made this snippet comparing RGB-Average, RGB-root-mean-square, and CIELAB-Average of many colors. (TLDR: they're all about the same color-wise, but since lab() isn't natively supported by most browsers yet, just use the RGB² method)

Observations:

  1. The RGB-squared (quadratic mean) method does tend to produce brighter colors, is just as fast as the regular average method (in this configuration at least), and is thus the overall best choice.
  2. the LAB method timings are much worse because we can't just use lab() in CSS yet (except maybe Safari), and thus the transform chain is rgb > xyz > lab > xyz > rgb. Whenever browsers do implement lab(), this method looks to be an intriguing option.
  3. HSL Colors: I included HSL averaging at first and it doesn't work, even when accounting for revolving angle properly. (averaging two hues seems to produce a complement to the two input colors)

let names = ["RGB mean", "RGB root mean²", "LAB mean"];
let analyzers = [simpleAverage,squaredAverage,labAverage];
let elapsed = [0,0,0];

let c1,c2,r,d1,d2,sp,sp2
for(let i=0;i<50000;i++){    
  
  c1 = i==0 ? [255,0,0]: i==1 ? [0,255,0] : i==2 ? [0,0,255] : i==3 ? [255,255,0]: i==4 ? [255,0,255] : i==5 ? [0,255,255] : randRGB()
  c2 = i==0 ? [0,0,255]: i==1 ? [255,0,0] : i==2 ? [0,255,0] : i==3 ? [0,255,255]: i==4 ? [255,255,0] : i==5 ? [255,0,255]: randRGB()

  if(i<100) {
    r = document.createElement("div")
    r.classList.add("row")
    d1 = document.createElement("div")
    d2 = document.createElement("div")
    d1.style.backgroundColor = rgbStr(c1)
    sp = document.createElement("span")
    sp.style.color = d1.style.backgroundColor
    sp.innerText = `Color 1: ${rgbStr(c1)}`
    d1.appendChild(sp)
    d2.style.backgroundColor = rgbStr(c2)
    sp2 = document.createElement("span")
    sp2.style.color = d2.style.backgroundColor
    sp2.innerText = `Color 2: ${rgbStr(c2)}`
    d2.appendChild(sp2)

    r.appendChild(d1)
    r.appendChild(d2)
  }
    for(let k=0;k<3;k++){
      const t0 = performance.now()
      let ave = analyzers[k](c1,c2)
      elapsed[k] += performance.now()-t0;

      if(i<100){
        let d = document.createElement("div")
        d.style.backgroundColor = rgbStr(ave)
        let sp = document.createElement("span")
        sp.style.color = d.style.backgroundColor
        sp.innerText = `${names[k]}: ${d.style.backgroundColor}`
        d.appendChild(sp)
        r.appendChild(d)
      }

    }
    if(i<100) colorResults.appendChild(r)
  
}
elapsed.forEach((ms,i)=>{
  timingResults.insertAdjacentHTML("beforeEnd", `<div>${names[i]}: ${ms.toFixed(2)}ms</div>`)
})

function rgbStr(c){
  return `rgb(${parseInt(c[0])},${parseInt(c[1])},${parseInt(c[2])})`
}
function labAverage(c1,c2){
  let l1 = XYZtoLAB(RGBtoXYZ(c1))
  let l2 = XYZtoLAB(RGBtoXYZ(c2))
  let ave = simpleAverage(l1,l2)
  let newXYZ = LabToXYZ(ave)
  let newRGB = XYZtoRGB(newXYZ)
  return newRGB
}
function simpleAverage(c1,c2) {
  return [((c1[0]+c2[0])/2), ((c1[1]+c2[1])/2), ((c2[2]+c1[2])/2)]
}
function squaredAverage(c1,c2) {
  return [
    Math.floor(Math.sqrt((c1[0]**2+c2[0]**2)/2)), 
    Math.floor(Math.sqrt((c1[1]**2+c2[1]**2)/2)), 
    Math.floor(Math.sqrt((c2[2]**2+c1[2]**2)/2))
  ];
}
function randRGB(){    
  return [randInt(256),randInt(256), randInt(256)]
}
function randInt(max) { return Math.floor(Math.random()*max)}
function RGBtoXYZ(RGB) {
  //https://stackoverflow.com/questions/15408522/rgb-to-xyz-and-lab-colours-conversion
  let R = RGB[0];
  let G = RGB[1];
  let B = RGB[2];

  var_R = parseFloat(R / 255)        //R from 0 to 255
  var_G = parseFloat(G / 255)        //G from 0 to 255
  var_B = parseFloat(B / 255)        //B from 0 to 255

  if (var_R > 0.04045) var_R = Math.pow((var_R + 0.055) / 1.055, 2.4)
  else var_R = var_R / 12.92
  if (var_G > 0.04045) var_G = Math.pow((var_G + 0.055) / 1.055, 2.4)
  else var_G = var_G / 12.92
  if (var_B > 0.04045) var_B = Math.pow((var_B + 0.055) / 1.055, 2.4)
  else var_B = var_B / 12.92

  var_R = var_R * 100
  var_G = var_G * 100
  var_B = var_B * 100

  //Observer. = 2°, Illuminant = D65
  X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805
  Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722
  Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505
  return [X, Y, Z]
}
function XYZtoLAB(XYZ) {
  //https://stackoverflow.com/questions/15408522/rgb-to-xyz-and-lab-colours-conversion
  let x = XYZ[0];
  let y = XYZ[1];
  let z = XYZ[2];
  var ref_X = 95.047;
  var ref_Y = 100.000;
  var ref_Z = 108.883;
  var_X = x / ref_X     //ref_X =  95.047   Observer= 2°, Illuminant= D65
  var_Y = y / ref_Y     //ref_Y = 100.000
  var_Z = z / ref_Z     //ref_Z = 108.883

  if (var_X > 0.008856) var_X = Math.pow(var_X, (1 / 3))
  else var_X = (7.787 * var_X) + (16 / 116)
  if (var_Y > 0.008856) var_Y = Math.pow(var_Y, (1 / 3))
  else var_Y = (7.787 * var_Y) + (16 / 116)
  if (var_Z > 0.008856) var_Z = Math.pow(var_Z, (1 / 3))
  else var_Z = (7.787 * var_Z) + (16 / 116)

  CIE_L = (116 * var_Y) - 16
  CIE_a = 500 * (var_X - var_Y)
  CIE_b = 200 * (var_Y - var_Z)

  return [CIE_L, CIE_a, CIE_b]
}
function LabToXYZ(lab){
  //adapted from easyRGB.com
  //The tristimulus values are (X, Y, Z) = (109.85, 100.00, 35.58)
  var ref_X = 95.047;
  var ref_Y = 100.000;
  var ref_Z = 108.883;
  let l = lab[0];
  let a = lab[1];
  let b = lab[2];

  let var_Y = ( l + 16 ) / 116
  let var_X = a / 500 + var_Y
  let var_Z = var_Y - b / 200

  if ( var_Y**3  > 0.008856 ) var_Y = var_Y**3
  else                       var_Y = ( var_Y - 16 / 116 ) / 7.787
  if ( var_X**3  > 0.008856 ) var_X = var_X**3
  else                       var_X = ( var_X - 16 / 116 ) / 7.787
  if ( var_Z**3  > 0.008856 ) var_Z = var_Z**3
  else                       var_Z = ( var_Z - 16 / 116 ) / 7.787

  X = var_X * ref_X
  Y = var_Y * ref_Y
  Z = var_Z * ref_Z
  return [X, Y, Z];
}
function XYZtoRGB(xyz) {
  //adapted from easyRGB.com
  let X = xyz[0];
  let Y = xyz[1];
  let Z = xyz[2];

  var_X = X / 100
  var_Y = Y / 100
  var_Z = Z / 100

  var_R = var_X *  3.2406 + var_Y * -1.5372 + var_Z * -0.4986
  var_G = var_X * -0.9689 + var_Y *  1.8758 + var_Z *  0.0415
  var_B = var_X *  0.0557 + var_Y * -0.2040 + var_Z *  1.0570

  if ( var_R > 0.0031308 ) var_R = 1.055 * ( var_R**( 1 / 2.4 ) ) - 0.055
  else                     var_R = 12.92 * var_R
  if ( var_G > 0.0031308 ) var_G = 1.055 * ( var_G**( 1 / 2.4 ) ) - 0.055
  else                     var_G = 12.92 * var_G
  if ( var_B > 0.0031308 ) var_B = 1.055 * ( var_B**( 1 / 2.4 ) ) - 0.055
  else                     var_B = 12.92 * var_B

  sR = var_R * 255
  sG = var_G * 255
  sB = var_B * 255

  return [sR,sG,sB];
}
body {
  font-size:1.3em;
  background-color:#f5f5f5;
}
span {
  filter: invert(100%) grayscale(100%) contrast(1000);
}
.row {
  margin:20px;
}
.row div:nth-child(1), .row div:nth-child(2) {
  padding:7px;
  text-align:center;
}
.row > div {
  margin:1px;
}
50000 iterations (showing first 100 results):
<div id="timingResults"></div>
<div id="colorResults"></div>
Scott Weaver
  • 7,192
  • 2
  • 31
  • 43
-4

There's actually a much simpler way.

  • Scale the image down to 1px by 1px.

    Color of the 1px is the average color of whatever you scaled

jellohead
  • 157
  • 4
  • 2
    This is a weird answer in that it is basically terrible (computationally extremely inefficient, and doesn't answer the question on a fundamental level) but it's really practical. This totally works in certain circumstances. – Newb Dec 16 '14 at 22:19
  • Well, it's an average _something_ anyway. What it's averaging is completely dependent on whatever algorithm the programmer chose to use on the day the image scaler was compiled. If it wasn't compiled, may Poseidon help you... – SO_fix_the_vote_sorting_bug Nov 03 '21 at 19:22