Java color image to grayscale conversion algorithm(s)

Black and white images have a very special feeling to them. Maybe that’s why all image processing programs have a grayscale feature built into them. Before we get started, two quotes on black and white images:

“I love that it’s a format that suits almost any type of photography. Portraits, landscapes, urban landscapes, architecture. Not only that, it’s a medium that adapts really well to all lighting situations. Whereas color photography often works best on sunny days or in brightly lit studios – low light just makes a black and white image moody.” – Sol

“I find that colors can be terribly distracting in some images and can take the focus away from your subject. I do portrait work and find that taking the color out of an image lets the subject speak for themselves. Its raw, it’s stripped back, it’s honest and it allows you to show the true person.” – Shane

While searching the internet for how to’s, I’ve discovered two websites that covered this topic, so this article pulls some knowledge from these resources 1, 2.

So, the thing is, we have several algorithms that convert color images to black and white:

  • Averaging
  • Luminosity
  • Desaturation
  • Minimal and maximal decomposition
  • The “single color channel” method
  • Java built in method

Averaging

You take all the color components (R, G and B) and divide them by three.

p_{new} = \frac{R + G + B}{3}

While the algorithm itself is pretty simple, it does a very good job, but it still has some downsides (it does a poor job representing how humans view luminosity).

    // The average grayscale method
    private static BufferedImage avg(BufferedImage original) {

        int alpha, red, green, blue;
        int newPixel;

        BufferedImage avg_gray = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());
        int[] avgLUT = new int[766];
        for(int i=0; i<avgLUT.length; i++) avgLUT[i] = (int) (i / 3);

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                newPixel = red + green + blue;
                newPixel = avgLUT[newPixel];
                // Return back to original format
                newPixel = colorToRGB(alpha, newPixel, newPixel, newPixel);

                // Write pixels into image
                avg_gray.setRGB(i, j, newPixel);

            }
        }

        return avg_gray;

    }

First we create a new image with the same height, width and attributes. Then we create a lookup table. The maximum value of the sum of three pixels can be 765 (because the maximum value of each pixel can be 255). So we create a table of summed values, and at the i-th position we put our averaged value (this gets us less computational time):

lc = im_{width} \times im_{height}

So if the images is 500×500, that means we do 250 000 less computations. Our result (yeah, the dude on the picture is me; and if you’re wondering what’s the plushie I’m holding 3 ):

 

Luminosity

The second method relies on calculating the values based on luminosity. The luminosity method is a more sophisticated version of the average method. It also averages the values, but it forms a weighted average to account for human perception. We’re more sensitive to green than other colors, so green is weighted most heavily. There are various formulas for calculating the new pixel values (our algorithm uses the first one, but you can also use any other formula; the last one is used by Photoshop):

p_{new} = 0.21 \times R + 0.71 \times G + 0.07 \times B

p_{new} = 0.2125 \times R + 0.7154 \times G + 0.0721 \times B

p_{new} = 0.50 \times R + 0.419 G + 0.081 \times B

p_{new} = 0.299 \times R + 0.587 \times G + 0.114 \times B

So what we do, we multiply the red, green and blue pixel values with a number and sum them up. The code for the method:

    // The luminance method
    private static BufferedImage luminosity(BufferedImage original) {

        int alpha, red, green, blue;
        int newPixel;

        BufferedImage lum = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                red = (int) (0.21 * red + 0.71 * green + 0.07 * blue);
                // Return back to original format
                newPixel = colorToRGB(alpha, red, red, red);

                // Write pixels into image
                lum.setRGB(i, j, newPixel);

            }
        }

        return lum;

    }

Our result:

 

Desaturation

Desaturating an image takes advantage of the ability to treat the RGB colorspace as a 3-dimensional cube. Desaturation approximates a luminance value for each pixel by choosing a corresponding point on the neutral axis of the cube. Now the calculations are a bit “hard”, but I’ve found a simpler method that says, that a pixel can be desaturated by finding the midpoint between the maximum of RGB and the minimum of RGB 2, if you want more detailed calculations use the method in 4.

p_{new} = \frac{min(R, G, B) + max(R, G, B)}{2}

So we only take the minimum of the RGB values and the maximum of the RGB values and divide them by two. We’ll create a lookup table for the division part, so that we’ll have less values; theoretically the biggest minimum element we can have is 255 (if all the values are the same), so we must create a lookup table for 511 values (255 is minimum, 255 is maximum, the sum is 510, if we add 0 we have 511). We’ll also need  to write the min and max method, but that’s simple as pie.

    // The desaturation method
    private static BufferedImage desaturation(BufferedImage original) {

        int alpha, red, green, blue;
        int newPixel;

        int[] pixel = new int[3];

        BufferedImage des = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());
        int[] desLUT = new int[511];
        for(int i=0; i<desLUT.length; i++) desLUT[i] = (int) (i / 2);

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                pixel[0] = red;
                pixel[1] = green;
                pixel[2] = blue;

                int newval = (int) (findMax(pixel) + findMin(pixel));
                newval = desLUT[newval];

                // Return back to original format
                newPixel = colorToRGB(alpha, newval, newval, newval);

                // Write pixels into image
                des.setRGB(i, j, newPixel);

            }
        }

        return des;

    }    

    private static int findMin(int[] pixel) {

        int min = pixel[0];

        for(int i=0; i<pixel.length; i++) {
            if(pixel[i] < min)
                    min = pixel[i];
        }

        return min;

    }

    private static int findMax(int[] pixel) {

        int max = pixel[0];

        for(int i=0; i<pixel.length; i++) {
            if(pixel[i] > max)
                    max = pixel[i];
        }

        return max;

    }

So our result:

 

Minimal and maximal decomposition

Decomposition takes the highest or the lowest pixel of the RGB channel and sets that value. The maximal decomposition produces bright black and white images while minimal produces darker ones. The algorithm is really pretty simple as we only call the methods for calculating the min and max values from the previous method.

Maximal decomposition:

p_{new} = max(R, G, B)

Minimal decomposition:

p_{new} = min(R, G, B)

    // The minimal decomposition method
    private static BufferedImage decompMin(BufferedImage original) {

        int alpha, red, green, blue;
        int newPixel;

        int[] pixel = new int[3];

        BufferedImage decomp = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                pixel[0] = red;
                pixel[1] = green;
                pixel[2] = blue;

                int newval = findMin(pixel);

                // Return back to original format
                newPixel = colorToRGB(alpha, newval, newval, newval);

                // Write pixels into image
                decomp.setRGB(i, j, newPixel);

            }
        }

        return decomp;

    }    

    // The maximum decomposition method
    private static BufferedImage decompMax(BufferedImage original) {

        int alpha, red, green, blue;
        int newPixel;

        int[] pixel = new int[3];

        BufferedImage decomp = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                pixel[0] = red;
                pixel[1] = green;
                pixel[2] = blue;

                int newval = findMax(pixel);

                // Return back to original format
                newPixel = colorToRGB(alpha, newval, newval, newval);

                // Write pixels into image
                decomp.setRGB(i, j, newPixel);

            }

        }

        return decomp;

    }

Our results (original image, minimal decomposition and maximal decomposition):

  

The “single color channel” method

This is also one of the simplest methods to produce a black and white image, and interesting enough, our cameras use it because it uses the least resources. For our output image we only set the values from a certain color, for instance if we choose red, the outputted values would be the red values of the pixel. So we’ll create a method that accepts an extra int value (0 for R, 1 for G and 2 for B).

    // The "pick the color" method
    private static BufferedImage rgb(BufferedImage original, int color) {

        int alpha, red, green, blue;
        int newPixel;

        int[] pixel = new int[3];

        BufferedImage rgb = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                pixel[0] = red;
                pixel[1] = green;
                pixel[2] = blue;

                int newval = pixel[color];

                // Return back to original format
                newPixel = colorToRGB(alpha, newval, newval, newval);

                // Write pixels into image
                rgb.setRGB(i, j, newPixel);

            }

        }

        return rgb;        

    }

Our results (original, red values, green values, blue values):

   

We can see that the green one produces the best result (as the human eye sees green better; the reason is that we have more green-sensitive cone cell photoreceptors in the retina than for the other colors – red and blue).

Java built in method

If you’re not even remotely interested how color to grayscale conversion works, Java has a built in function that outputs a grayscaled image:

    // The simplest way to convert in Java
    public static BufferedImage javaWay(BufferedImage source) {
        BufferedImageOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
        return op.filter(source, null);
    }

The method produces the following result (which isn’t as good as our algorithms):

 

So we’ve covered most of the grayscale algorithms; now lets sum it up and and write a workable application. We call our application using:

java Grayscale image_name

The program outputs black and white images with the same name using all of our algorithms (please note that the code is written for example purposes and it could be optimized in some places).

import java.awt.Color;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorConvertOp;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;

/**
 * Image to grayscale algorithm(s)
 *
 * Author: Bostjan Cigan (http://zerocool.is-a-geek.net)
 *
 */

public class Grayscale {

    private static BufferedImage original, grayscale;

    public static void main(String[] args) throws IOException {

        File original_f = new File(args[0]+".jpg");
        String output_f = args[0]+"_bw";
        original = ImageIO.read(original_f);
        grayscale = avg(original);
        writeImage(output_f+"_1_avg");
        grayscale = luminosity(original);
        writeImage(output_f+"_2_lum");
        grayscale = desaturation(original);
        writeImage(output_f+"_3_lig");
        grayscale = decompMin(original);
        writeImage(output_f+"_4_decmin");
        grayscale = decompMax(original);
        writeImage(output_f+"_5_decmax");
        grayscale = rgb(original, 0);
        writeImage(output_f+"_6_1r");
        grayscale = rgb(original, 1);
        writeImage(output_f+"_6_2g");
        grayscale = rgb(original, 2);
        writeImage(output_f+"_6_3b");
        grayscale = javaWay(original);
        writeImage(output_f+"_7_java");

    }

    private static void writeImage(String output) throws IOException {
        File file = new File(output+".jpg");
        ImageIO.write(grayscale, "jpg", file);
    }

    // The average grayscale method
    private static BufferedImage avg(BufferedImage original) {

        int alpha, red, green, blue;
        int newPixel;

        BufferedImage avg_gray = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());
        int[] avgLUT = new int[766];
        for(int i=0; i<avgLUT.length; i++) avgLUT[i] = (int) (i / 3);

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                newPixel = red + green + blue;
                newPixel = avgLUT[newPixel];
                // Return back to original format
                newPixel = colorToRGB(alpha, newPixel, newPixel, newPixel);

                // Write pixels into image
                avg_gray.setRGB(i, j, newPixel);

            }
        }

        return avg_gray;

    }

    // The luminance method
    private static BufferedImage luminosity(BufferedImage original) {

        int alpha, red, green, blue;
        int newPixel;

        BufferedImage lum = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                red = (int) (0.21 * red + 0.71 * green + 0.07 * blue);
                // Return back to original format
                newPixel = colorToRGB(alpha, red, red, red);

                // Write pixels into image
                lum.setRGB(i, j, newPixel);

            }
        }

        return lum;

    }    

    // The desaturation method
    private static BufferedImage desaturation(BufferedImage original) {

        int alpha, red, green, blue;
        int newPixel;

        int[] pixel = new int[3];

        BufferedImage des = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());
        int[] desLUT = new int[511];
        for(int i=0; i<desLUT.length; i++) desLUT[i] = (int) (i / 2);

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                pixel[0] = red;
                pixel[1] = green;
                pixel[2] = blue;

                int newval = (int) (findMax(pixel) + findMin(pixel));
                newval = desLUT[newval];

                // Return back to original format
                newPixel = colorToRGB(alpha, newval, newval, newval);

                // Write pixels into image
                des.setRGB(i, j, newPixel);

            }
        }

        return des;

    }    

    // The minimal decomposition method
    private static BufferedImage decompMin(BufferedImage original) {

        int alpha, red, green, blue;
        int newPixel;

        int[] pixel = new int[3];

        BufferedImage decomp = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                pixel[0] = red;
                pixel[1] = green;
                pixel[2] = blue;

                int newval = findMin(pixel);

                // Return back to original format
                newPixel = colorToRGB(alpha, newval, newval, newval);

                // Write pixels into image
                decomp.setRGB(i, j, newPixel);

            }
        }

        return decomp;

    }    

    // The maximum decomposition method
    private static BufferedImage decompMax(BufferedImage original) {

        int alpha, red, green, blue;
        int newPixel;

        int[] pixel = new int[3];

        BufferedImage decomp = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                pixel[0] = red;
                pixel[1] = green;
                pixel[2] = blue;

                int newval = findMax(pixel);

                // Return back to original format
                newPixel = colorToRGB(alpha, newval, newval, newval);

                // Write pixels into image
                decomp.setRGB(i, j, newPixel);

            }

        }

        return decomp;

    }    

    // The "pick the color" method
    private static BufferedImage rgb(BufferedImage original, int color) {

        int alpha, red, green, blue;
        int newPixel;

        int[] pixel = new int[3];

        BufferedImage rgb = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());

        for(int i=0; i<original.getWidth(); i++) {
            for(int j=0; j<original.getHeight(); j++) {

                // Get pixels by R, G, B
                alpha = new Color(original.getRGB(i, j)).getAlpha();
                red = new Color(original.getRGB(i, j)).getRed();
                green = new Color(original.getRGB(i, j)).getGreen();
                blue = new Color(original.getRGB(i, j)).getBlue();

                pixel[0] = red;
                pixel[1] = green;
                pixel[2] = blue;

                int newval = pixel[color];

                // Return back to original format
                newPixel = colorToRGB(alpha, newval, newval, newval);

                // Write pixels into image
                rgb.setRGB(i, j, newPixel);

            }

        }

        return rgb;        

    }

    // The simplest way to convert in Java
    public static BufferedImage javaWay(BufferedImage source) {
        BufferedImageOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null);
        return op.filter(source, null);
    }

    // Convert R, G, B, Alpha to standard 8 bit
    private static int colorToRGB(int alpha, int red, int green, int blue) {

        int newPixel = 0;
        newPixel += alpha;
        newPixel = newPixel << 8;
        newPixel += red; newPixel = newPixel << 8;
        newPixel += green; newPixel = newPixel << 8;
        newPixel += blue;

        return newPixel;

    }

    private static int findMin(int[] pixel) {

        int min = pixel[0];

        for(int i=0; i<pixel.length; i++) {
            if(pixel[i] < min)
                    min = pixel[i];
        }

        return min;

    }

    private static int findMax(int[] pixel) {

        int max = pixel[0];

        for(int i=0; i<pixel.length; i++) {
            if(pixel[i] > max)
                    max = pixel[i];
        }

        return max;

    }

}
References:
  1. Three algorithms for converting color to grayscale: http://www.johndcook.com/blog/2009/08/24/algorithms-convert-color-grayscale   []
  2. Seven unique image grayscale conversion algorithms: http://www.tannerhelland.com/3643/grayscale-image-algorithm-vb6  [] []
  3. http://www.dokosmuskrtkem.cz/wordpress/?page_id=528 []
  4. Better calculations for HSV: http://www.easyrgb.com/index.php?X=MATH&H=18 []

About this article