Getting the average HSB from pixels

I have some simple code that is intended to find the average colour in a rectangle. That rectangle moves with the mouse.

All is fine until I try to average two colours as in the example below.

The Hue of half the sample is 247. The Hue of the other half is 1. So, the existing code (see below) gives an average of 124. This is obviously incorrect. The average should be of 247 and (360+1), so 304.

How do I change the code so this works? The images that I intend to analyse are more complex than this example so the solution should not rely on the image being two adjacent rectangles.

TIA

02.jpg

// ( )
PImage img1; // the image to be investigated
PImage img2; // the detail of the image to be averaged

// ( )
int samplesize = 60; // an even number
int half = samplesize/2;


void setup() {
  colorMode(HSB, 360, 100, 100);
  size(1920, 1200);


  img1 = loadImage("02.jpg");


} 

void draw() {
  background(0, 0, 100);

  // (1) display the image under investigation
  image(img1, 0, 0); 

  // (2) the sample but in a new image
  img2 = get(mouseX-half, mouseY-half, samplesize, samplesize); 


  // (3) after averaged by function
  fill(extractColorFromImage(img2)); 
  rect(img1.width-200, img1.height, 200, 200);

  // (4) show border of sample
  rectMode(CENTER);
  stroke(0);
  noFill();
  rect(mouseX, mouseY, samplesize, samplesize); 
  rectMode(CORNER); // revert to default
  noStroke(); // revert to default
}




color extractColorFromImage(PImage img2) {

  img2.loadPixels();
  int h = 0, s = 0, b = 0;
  for (int i=0; i<img2.pixels.length; i++) {
    color c = img2.pixels[i];

    h += hue(c);
    s += saturation(c);
    b += brightness(c);
  }
  h /= img2.pixels.length;
  s /= img2.pixels.length;
  b /= img2.pixels.length;

  fill(0);
  text("HSB "+h+" "+s+" "+b, 20, img1.height+220);

  return color(h, s, b);
}
2 Likes

Here is a simple sketch that includes a small function to calculate the average hue

void setup() {
  averageHue(247, 1);
  averageHue(1, 247);
  averageHue(30, 200);
  averageHue(200, 30);
  averageHue(20, 330);
  averageHue(330, 20);
}

int averageHue(int h0, int h1) {
  int hlow = min(h0, h1);
  int hhigh = max(h0, h1);
  int delta = hhigh - hlow;
  int ha = delta < 180 ? hlow + delta / 2 : hhigh + (360 - delta)/2;
  println(hlow, hhigh, ha);
  return ha;
}
2 Likes

Ha indeed! I guessed it would involve 180 (as this is similar to the interval between two angles) but I was struggling to get the code that clear.

How will this work when there are many values (one value for each pixel in the sample)?
Thank you.

1 Like

Slight change to my code above to prevent hues >= 360

1 Like

Although the method I provided accurately calculates the average hue for 2 values it cannot be used consistently for more than two. For instance this sketch calculates the average for 3 values and gives 3 different averages depending on the order we calculate the intermediate values

void setup() {
  int[] hues = {330, 45, 190};
  println(averageHue(hues[0], averageHue(hues[1], hues[2])));
  println(averageHue(hues[1], averageHue(hues[0], hues[2])));
  println(averageHue(hues[2], averageHue(hues[0], hues[1])));
}

int averageHue(int h0, int h1) {
  int hlow = min(h0, h1);
  int hhigh = max(h0, h1);
  int delta = hhigh - hlow;
  return (delta < 180 ? hlow + delta / 2 : hhigh + (360 - delta) / 2) % 360;
}

Output = 43 332 278
This image shows the output from a sketch that calculates the average hue of an image many times but accessing the pixels in a different order each time. The frequency graph shows a peak about 20/30 but has a wide range of values for the average hue.

In this situation a simple numerical average is probably the best solution because at least the result is repeatable.

3 Likes

@paulstgeorge

My version below:

void setup() 
  {
  //for (int h = 359; h>=0; h--)
  for (int h = 0; h<360; h++)
    {
    print(averageHue(0, h), '\t');
    println(averageHue2(0, h));
    } 
  }

// glv version: 
float averageHue2(int h0, int h1) 
  {
  float h2 = abs(h0+h1)/2f;
  float ha2 = abs(h1-h0)>180 ? h2+180 : h2;

  //if (abs(h1-h0)>180)
  //  ha2 = h2+180;
  //else
  //  ha2 = h2;  

  return ha2;
  } 

//quark version from https://discourse.processing.org/t/getting-the-average-hsb-from-pixels/31965/2  
int averageHue(int h0, int h1) 
  {
  int hlow = min(h0, h1);
  int hhigh = max(h0, h1);
  int delta = hhigh - hlow;
  int ha = delta < 180 ? hlow + delta / 2 : hhigh + (360 - delta)/2;
  return ha;
  }

I added quarks version to compare outputs.

:)

That’s not fair (to either of you)! If Quark’s code is changed from int numbers to float it gives the same result as your code. But, if the setup is changed a little (see below) we get the real test, how does the code cope with two numbers like 247 and 1, where adding together and dividing by 2 would give the wrong result. Your code and Quark’s code both give the correct answer but you do it in fewer lines!!!

int s1 = 247;
int s2 = 1;

//correct result is (247+361)/2, or 304

void setup() {
    print("Q "+averageHue(s1, s2), '\t');
    println("G "+averageHue2(s1, s2));
  }

// glv version: 
float averageHue2(int h0, int h1) 
  {
  float h2 = abs(h0+h1)/2f;
  float ha2 = abs(h1-h0)>180 ? h2+180 : h2;

  return ha2;
  } 

//quark version from https://discourse.processing.org/t/getting-the-average-hsb-from-pixels/31965/2  
float averageHue(int h0, int h1) 
  {
  float hlow = min(h0, h1);
  float hhigh = max(h0, h1);
  float delta = hhigh - hlow;
  float ha = delta < 180 ? hlow + delta / 2 : hhigh + (360 - delta)/2;
  return ha;
  }

I was showing my method which I was working on as a personal challenge.

I looked at @quark’s after and compared outputs and added floats for that extra resolution which I thought may be of interest… I like the details.

That is all there is to that.

:)

Yeh, I know. I am showing that yours was actually even better if we compare like with like.

I am closing in on a solution and thought I would share now in case someone wants to swoop in to assist (or tell me I am wasting my time).

Treat the n numbers like angles between 0 and 360. Then think of the angles as simple vectors.

Get the sine of each angle. Get the mean of all the sines. s
Get the cosine of each angle. Get the mean of all the cosines. c

Then get the atan or arc tangent of (s/c) to find the angle. a

if s < 0 && c > 0, a = a + 360

if c < 0, a = a + 180

if s > 0 && c > 0, a

I am not sure if this is correct yet, but it looks like it might be. It works for any number of values.

@glv @quark

1 Like

How will you know if the value produced by your algorithm is correct?

Good question! I plan to post something soon, then test it.

Here it is for the two test values. I first wanted to see if it gave the correct result for 247 and 1. Obviously I would use arrays, etc. but wanted to present the idea in a clear way so it can be pulled apart. I am not sure I have covered all bases in the if then part.

int a1 = 247;
int a2 = 1;

float sin1;
float sin2;

float cos1;
float cos2;

float meansin;
float meancos;

float ha;

//correct result is (247+361)/2, or 304

void setup() {

  // mean of sine of each angle
  sin1 = sin(radians(a1));
  sin2 = sin(radians(a2));

  meansin = (sin1 + sin2)/2.0;

  // mean of cosine of each angle
  cos1 = cos(radians(a1));
  cos2 = cos(radians(a2));

  meancos = (cos1 + cos2)/2.0;

  // find the angle from the arc tangent
  if (meansin < 0 && meancos > 0) {
    ha = 360 + degrees(atan(meansin/meancos));
  } else if (meancos < 0) {
    ha = 180 + degrees(atan(meansin/meancos));
  } else if (meansin > 0 && meancos > 0) {
    ha = degrees(atan(meansin/meancos));
  }


  println(ha);
}

The problem is not finding the average of 2 values, we already have an algorithm for that. In my previous post I showed that this could not be extended to work consistently with more than 2 values.

I know. I am inching forwards. Now doing for three values, then for any number of values.

When you do the three values try the algorithm see if you get the same result not matter what the order they are in e.g.
240 30 200
240 200 30
200 30 240

I will. Yes. But I think order will not matter for my algorithm. I am adding the values before I do anything to them. Addition is commutative.

Work in Progress.
This gives the same results (no matter what order).

As I suspected, some work needs to be done on the conditions for choosing which of the three values is correct.
50.985184 230.98518 410.9851

int a1 = 240;
int a2 = 30;
int a3 = 200;

float sin1;
float sin2;
float sin3;

float cos1;
float cos2;
float cos3;

float meansin;
float meancos;

float ha;



void setup() {

  // mean of sine of each angle
  sin1 = sin(radians(a1));
  sin2 = sin(radians(a2));
  sin3 = sin(radians(a3));

  meansin = (sin1 + sin2 + sin3)/3.0;

  // mean of cosine of each angle
  cos1 = cos(radians(a1));
  cos2 = cos(radians(a2));
  cos3 = cos(radians(a3));

  meancos = (cos1 + cos2 + cos3)/3.0;

  // find the angle from the arc tangent

  ha = degrees(atan(meansin/meancos));


  print(ha, ha+180, ha+360);
}

If you have hues 0-360, then you can imagine them as degree angles, and solve the mean angle question – which also gives you a mean hue.

EDIT fixed typos:

float getMeanAngle(float... anglesDeg) {
  float x = 0.0;
  float y = 0.0;
  for (float angleD : anglesDeg) {
    float angleR = radians(angleD);
    x += cos(angleR);
    y += sin(angleR);
  }
  float avgR = atan2(y / anglesDeg.length, x / 
  anglesDeg.length);
  return degrees(avgR);
}

For more, see:

https://rosettacode.org/wiki/Averages/Mean_angle

If you want a simple approximation of perceptual, intuitive hue averaging, it will probably work more like either clustering or just peak detection. That is, some color distributions have one peak, some have two or three, and the correct answer simplifies to one or more average hues.

Also, the pixel is usually the wrong unit for perceptual, because our eyes blend textures of color points into new colors – so hue averaging on pixels can give us really unintuitive answers if the image contains noise / dithered textures. You can change what you measure by first averaging your pixels into small neighborhoods blocks like 3x3 or 4x4, then computing the clustering / peak on those – or first using blur.

2 Likes

Here is a simple mean HSB extractor on an image (pixels array). It doesn’t do clustering or suggest multiple hues, and it doesn’t do local averaging / blurring / scaling – so it gives satisfactory results on some images, and not on others. Still, it demonstrates the concept.

/**
 * MeanHSB
 * 2021-08-28 Jeremy Douglass - Processing 3.5.4 
 *
 * Compute the mean HSB (hue, saturation, brightness)
 * for an array of colors -- for example, the pixels
 * of an image. The bottom bar displays the HSB values
 * as a new color.
 *
 * Hue is calculated as a mean angle using getMeanDegrees().
 *
 * https://discourse.processing.org/t/getting-the-average-hsb-from-pixels/31965
 */

PImage img;

void setup() {
  size(640, 480);
  img = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/64_365_Color_Macro_%285498808099%29.jpg/640px-64_365_Color_Macro_%285498808099%29.jpg");
  img.resize(640, 480);
  noLoop();
}

void draw() {
  image(img, 0, 0);
  float[] meanHSB = getMeanHSB(img.pixels);
  colorMode(HSB, 360, 100, 100);
  fill(meanHSB[0], meanHSB[1], meanHSB[2]);
  print(meanHSB[0], meanHSB[1], meanHSB[2]);
  rect(0, height-50, width, height);
}

/**
 * Calculate the mean HSB values of a pixel array
 * using mean angle in degrees for hue (0-360)
 * and mean for saturation / brightness (0-100).
 * 
 * @param pixels   the array of pixels to measure
 * @return a float array of three means, {H, S, B}
 */
float[] getMeanHSB(int... pixels) {
  double sumS = 0;
  double sumB = 0;
  float anglesDeg[] = new float[pixels.length];
  // convert 
  push();
  colorMode(HSB, 360, 100, 100);
  for(int i=0; i<pixels.length; i++){
    anglesDeg[i] = hue(pixels[i]);
    sumS += saturation(pixels[i]);
    sumB += brightness(pixels[i]);
  }
  pop();
  return new float[] {
    // convert list of hues into mean hue
    getMeanDegrees(anglesDeg),
    // convert sums of sat/bright into means
    (float)(sumS/pixels.length),
    (float)(sumB/pixels.length)
  };
}

/**
 * Calculate the mean angle from any number
 * of angles expressed in degrees 0-360.
 * 
 * @param anglesDeg   a list of angles in degrees
 * @return the average angle in degrees (0-360)
 */
float getMeanDegrees(float... anglesDeg) {
  float x = 0.0;
  float y = 0.0;
  for (float angleD : anglesDeg) {
    float angleR = radians(angleD);
    x += cos(angleR);
    y += sin(angleR);
  }
  float avgR = atan2(y / anglesDeg.length, x / 
  anglesDeg.length);
  return degrees(avgR);
}

screenshot--MeanHSB-1

1 Like