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.
// ( )
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);
}
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
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.
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!!!
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.
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
…
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.
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);
}