Color to grayscale algorithm

I want to convert RGB colour images to greyscale. I know a number of ways to do this in Processing, but the algorithms are under the bonnet. I want to do the conversion AND know what I am doing!

After some research (Color-to-Grayscale: Does the Method Matter in Image Recognition?), I have found the method I want to use but I need help to do this in Processing. The method is called Gleam.

Here is what the author of the Color-to-Grayscale paper says:

Perhaps the simplest color to grayscale algorithm is Intensity. It is the mean of the RGB channels:

intensity

Although Intensity is calculated using linear channels, in practice gamma correction is often left intact when using datasets containing gamma corrected images. We call this method Gleam:

gleam

In terms of pixel values, Intensity and Gleam produce very different results.

When gamma corrected Intensity and Gleam are both applied to natural images, we found that Gleam produces pixel values around 20–25% smaller on average.

What is gamma correction and how would I implement the Glean color to grayscale algorithm in Processing?

1 Like

RGB in Processing already is gamma-corrected, so simply taking average of the 3 RGB components gives the Gleam metric.

If we linearly interpolate over a range of values in Processing (say black-to-white, 0-255), we get an even change in the perceived brightness of each colour: a black-to-white gradient with grey at the midpoint, even though the physical light intensity is non-linear. This is the expected behavior under gamma correction.

6 Likes

Ah ha. Interesting. Thank you.
So, how would I implement the Intensity colour to grayscale algorithm in Processing? I want to compare the results…

Hello @paulstgeorge,

Example from source code (give it a moment to go to line):
processing/core/src/processing/core/PImage.java at master · processing/processing · GitHub

You can implement your own custom image processing:
Images and Pixels / Processing.org

Bit manipulation is always faster:
& (bitwise AND) / Reference / Processing.org
red() / Reference / Processing.org

Reference:
Gamma correction - Wikipedia
Grayscale - Wikipedia < Shows were the values come from in source code above.
Luma (video) - Wikipedia < Used by Processing filter(GRAY)

:)

1 Like

The technical term you’re looking for is Relative Luminance. It is a specific standard for calculating the brightness of colors as perceived by humans, with scientifically derived weights.

A rather well-illustrated Twitter/X thread on the subject.

Although the Processing algorithm for grayscale conversion (using filter(GRAY)) is not an exact implementation of relative luminance, it’s conceptually similar and a close approximation for general use (only the weights differ slightly).

For comparison, here’s what the grayscale conversion function would look like if it used perceptual precision weightings.

def relative_luminance(img):
    
    """
    Convert the image to grayscale using true relative luminance weights and bit shifting.
    Reference -> https://en.wikipedia.org/wiki/Relative_luminance
    
    """
    
    lum_img = createImage(img.width, img.height, RGB)
    
    img.loadPixels()
    lum_img.loadPixels()
    
    for i in range(len(img.pixels)):
        
        col = img.pixels[i]
        
        # Extract RGB components using bit shifts
        r = (col >> 16) & 0xff  # Red component
        g = (col >> 8) & 0xff   # Green component
        b = col & 0xff          # Blue component
        
        # Calculate the true relative luminance using scaled weights:
        # Luminance = 0.2126 * Red + 0.7152 * Green + 0.0722 * Blue
        # Approximation: 0.2126 * 256 = 54, 0.7152 * 256 = 183, 0.0722 * 256 = 18
        lum = (54 * r + 183 * g + 18 * b) >> 8  # Bit-shift by 8 (dividing by 256)
        
        # Set the grayscale pixel by combining the luminance value into RGB format
        lum_img.pixels[i] = (col & 0xff000000) | (lum << 16) | (lum << 8) | lum
    
    lum_img.updatePixels()
    
    return lum_img

grayscale

3 Likes

Thank you everyone! That was some good reading.
I made a sketch (based on the sketch by @solub) to compare different weightings, and in my humble opinion the Relative Luminance is best (on a screen). You can see for yourselves because I have uploaded the results at the end of the message.
I have also uploaded a conversion made in Adobe Illustrator > Edit Colors > Convert to Grayscale. Seems like Adobe also uses Relative Luminance…?

from __future__ import print_function

def setup():
    size(1070, 892)
    
    global img
    img = loadImage("2018_431_IN3_RET.png")
    
def draw():
   if mouseX <= img.width/4:
       image(img, 0, 0)

   elif mouseX > img.width/4 and mouseX <= img.width/2:   
       weight_R = 54
       weight_G = 183    
       weight_B = 18
       image(relative_luminance(img, weight_R, weight_G, weight_B), 0, 0)
       
   elif mouseX > img.width/2 and mouseX <= (img.width/4)*3:   
       weight_R = 77
       weight_G = 151    
       weight_B = 28
       image(relative_luminance(img, weight_R, weight_G, weight_B), 0, 0) 
   else:
       weight_R = 85
       weight_G = 85   
       weight_B = 85
       image(relative_luminance(img, weight_R, weight_G, weight_B), 0, 0)

   line(img.width/4, img.height - 50, img.width/4, img.height)
   line(img.width/2, img.height - 50, img.width/2, img.height)
   line((img.width/4)*3, img.height - 50, (img.width/4)*3, img.height)
   
   if ((keyPressed) and (key == 'p')):
       save("something.jpg")

def relative_luminance(img, weight_R, weight_G, weight_B):
    
    """
    Convert the image to grayscale using true relative luminance weights and bit shifting.
    Reference -> https://en.wikipedia.org/wiki/Relative_luminance
    
    """
    
    lum_img = createImage(img.width, img.height, RGB)
    
    img.loadPixels()
    lum_img.loadPixels()
    
    for i in range(len(img.pixels)):
        
        col = img.pixels[i]
        
        # Extract RGB components using bit shifts
        r = (col >> 16) & 0xff  # Red component
        g = (col >> 8) & 0xff   # Green component
        b = col & 0xff          # Blue component
        
        # Calculate the true relative luminance using scaled weights:
        lum = (weight_R * r + weight_G * g + weight_B * b) >> 8  # Bit-shift by 8 (dividing by 256)
        
        # Set the grayscale pixel by combining the luminance value into RGB format
        lum_img.pixels[i] = (col & 0xff000000) | (lum << 16) | (lum << 8) | lum
    
    lum_img.updatePixels()
    
    return lum_img

@solub
If the RGB components are extracted using

        r = red(col)  
        g = green(col)
        b = blue(col)

What would these line then be?

        # Calculate the true relative luminance using scaled weights:
        lum = (weight_R * r + weight_G * g + weight_B * b) >> 8  # Bit-shift by 8 (dividing by 256)
        
        # Set the grayscale pixel by combining the luminance value into RGB format
        lum_img.pixels[i] = (col & 0xff000000) | (lum << 16) | (lum << 8) | lum

Please and thank you,

Processing Java version:

/*
 Project:   Grayscale
 Author:    GLV
 Date:      2024-10-12
 Version:   1.0.0
*/

// References:
// https://github.com/benfry/processing4/blob/main/core/src/processing/core/PImage.java#L874
// https://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale

String url = "http://learningprocessing.com/code/assets/sunflower.jpg";
PImage img, img0; 
int h, w;

void settings()
  {
  img0 = loadImage(url);
  img0.resize(400, 400);
  w = img0.width;
  h = img0.height;
  println(w, h);
  size(w, h);
  }

//void setup()
//  {
//  }

void draw()
  {
  img = img0.copy();   
  img.loadPixels();
  int lum = 0;
  int mx = mouseX;
  
  for(int y=0; y<img.width; y++)
  //for(int y=mouseY; y<img.width; y++) // If you want to see color!
    {
    for(int x=0; x<img.width; x++)
      {
      int loc = x+y*img.width;  
      
      // Beginner friendly.
      // Below is much faster with bit shifting!
      float r = red(img.pixels[loc]);
      float g  = green(img.pixels[loc]);
      float b  = blue(img.pixels[loc]);
       
      if (x < mx)
        lum = (int) (77*r + 151*g + 28*b)/256;  // 0.299  0.587  0.114   weights
      else
        lum = (int) (54*r + 183*g + 18*b)/256;   // 0.2126 0.7152 0.0722  weights
        //lum = (int) (r + g + b)/3;            // Averaging 
      img.pixels[loc] = color(lum); 
      } 
    }
  img.updatePixels(); 
  
  image(img, 0, 0);
  stroke(255);
  line(mouseX, 0, mouseX, height);
  }

image

:)

1 Like

@glv Thanks ever so. I am trying to understand.
What is (int)?

I would love to know what is going on in the Python line:
lum_img.pixels[i] = (col & 0xff000000) | (lum << 16) | (lum << 8) | lum

I understand bit shifting is faster, but I like to develop in a plonky clear way and then make the code efficient at the end. I plan to add in some other features such as turning the image into a photographic negative, white point, etc.

1 Like

https://runestone.academy/ns/books/published/csjava/Unit1-Getting-Started/topic-1-6-casting.html

Thanks @glv

I am familiar with casting a double to a float in a similar way:

double dopple = 6.28318530717958647693d;
float boat = (float) dopple;

because float() expects an integer and not a double
float ship = float(dopple); //does not work

But why (int) somenumber and not int(somenumber)???

Sketch:

int i = int(3.3333);
println(i);

int j = (int) 3.3333;
println(j);

int k = parseInt(3.3333);
println(k);

This is what Processing generates:

/* autogenerated by Processing revision 1293 on 2024-10-16 */
import processing.core.*;
import processing.data.*;
import processing.event.*;
import processing.opengl.*;

import java.util.HashMap;
import java.util.ArrayList;
import java.io.File;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;

public class sketch_241016a extends PApplet {

  public void setup() {
int i = PApplet.parseInt(3.3333f);
println(i);

int j = (int) 3.3333f;
println(j);

int k = parseInt(3.3333f);
println(k);

    noLoop();
  }

  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "sketch_241016a" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}


Source may provide some insight:
processing4/core/src/processing/core/PApplet.java at main · benfry/processing4 · GitHub

:)

1 Like

Hello @micycle,

It is not clear what is meant by this.

An sRGB image is gamma encoded and will display correctly on a gamma-corrected display (LCD monitors do this).

:)

Hello @solub,

# Calculate the true relative luminance using scaled weights:
        # Luminance = 0.2126 * Red + 0.7152 * Green + 0.0722 * Blue
        # Approximation: 0.2126 * 256 = 54, 0.7152 * 256 = 183, 0.0722 * 256 = 18
        lum = (54 * r + 183 * g + 18 * b) >> 8  # Bit-shift by 8 (dividing by 256)

Your code is applied to an sRGB image (assumed and these are gamma-compressed) and meets the definition of luma as per:

Grayscale - Wikipedia states:

Coding the above (formulas in the Wikipedia page) was a fun exercise and results are quite striking compared to the other methods.

References:

:)

1 Like

Have you done this?? Can we see (please)?