1. Introduction

This article is going to cover how to use Java to create grayscale images from their colour counter-parts. When I started looking into this topic I did a quick Google search and found many different answers using many different libraries with considerably different outputs. So I decided to put together this post describing the outcome of my Quest For the Holy Gray.

 

2. Getting Started on Using Java to Create Grayscale Images

For the purposes of this article I’m going to use the same method signatures I used in my code, so I will receive the image as a byte array and return the image as a byte array. You can easily change this to accept a BufferedImage and return one too. The code will also convert the image mime type to PNG but you could use any converter you like (e.g. bmp, jpg, png or gif).

Below is the base line code without any of the gray goodies yet:

public byte[] convertToGrayAndPng( byte[] imageBytes ) throws IOException {

	// Turn the byte array into a BufferedImage
	InputStream in = new ByteArrayInputStream( imageBytes );
	BufferedImage sourceImg = ImageIO.read(in);

	// We'll be doing some gray scale magic here soon...

	// Write the icon as a PNG
	ByteArrayOutputStream out = new ByteArrayOutputStream();
	ImageIO.write( image, "png", out );

	// Return the grayscale PNG as a byte array
	return out.toByteArray();
}

Now onto the different options.

 

3. Option 1

The first option is to use the ColorConvertOp class which will perform a pixel-by-pixel color conversion of the source image using the gray color space.

BufferedImageOp op = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); 
BufferedImage image = op.filter(sourceImg, null);

The results of this option were the following:

SourceOutputExecution Time
source option1 158ms

 

4. Option 2

The second option is to use the GrayFilter on the source image and then draw it into a new BufferedImage. Notice that there is a second parameter to the GrayFilter, this is a number between 0 and 100 which defines the percentage of gray to use (100 being the darkest and 0 the lightest). The results table below contains the output of using 80 (as below) and using 50.

ImageFilter filter = new GrayFilter( true, 80 );  
ImageProducer producer = new FilteredImageSource( sourceImg.getSource(), filter );  
Image grayImg = Toolkit.getDefaultToolkit().createImage( producer );

BufferedImage image = new BufferedImage( sourceImg.getWidth(), sourceImg.getHeight(), BufferedImage.TYPE_INT_ARGB );  
Graphics g = image.getGraphics();  
g.drawImage( grayImg, 0, 0, null );  
g.dispose();

The results of this option were the following:

SourceOutputExecution Time% of Gray
source option2 - method 80 828ms80
sourceoption2 - method 50827ms50

 

5. Option 3

The third option is a variation of option 2 which makes use of the createDisabledImage() utility method. Notice that there is a huge improvement in execution time compared to option 2.

Image grayImg = GrayFilter.createDisabledImage( sourceImg );

BufferedImage image = new BufferedImage( sourceImg.getWidth(), sourceImg.getHeight(), BufferedImage.TYPE_INT_ARGB );  
Graphics g = image.getGraphics();  
g.drawImage( grayImg, 0, 0, null );  
g.dispose();

The results of this option were the following:

SourceOutputExecution Time
source option3 221ms

 

6. Option 4

The fourth option is to write the source image into a new BufferedImage which is initialised with a TYPE_BYTE_GRAY image type. This is incredibly fast but unfortunately doesn’t maintain the transparent background of the source image. If anyone knows how to change that behaviour I would love to hear about it in the comments.

BufferedImage image = new BufferedImage( sourceImg.getWidth(), sourceImg.getHeight(), BufferedImage.TYPE_BYTE_GRAY );  
Graphics g = image.getGraphics();  
g.drawImage( sourceImg, 0, 0, null );  
g.dispose();

The results of this option were the following:

SourceOutputExecution Time
source option4 14ms

 

7. Option 5

The fifth option is to once again write the source image into a new BufferedImage, except this time we use the ColorConvertOp to do that. Once again, the performance is very good but the transparency is lost.

BufferedImage image = new BufferedImage( sourceImg.getWidth(), sourceImg.getHeight(), BufferedImage.TYPE_BYTE_GRAY );  
ColorConvertOp op = new ColorConvertOp( sourceImg.getColorModel().getColorSpace(), image.getColorModel().getColorSpace(), null );
op.filter( sourceImg, image );

The results of this option were the following:

SourceOutputExecution Time
source option5 17ms

 

8. Option 6

The sixth option is to write the source image into a new BufferedImage which uses a TYPE_4BYTE_ABGR_PRE image type. This image is then passed through a ColorConvertOp filter using the CS_GRAY colour space as the source colour space.

Integer width = sourceImg.getWidth();
Integer height = sourceImg.getHeight();

BufferedImage image = new BufferedImage( width, height, BufferedImage.TYPE_4BYTE_ABGR_PRE );
image.createGraphics().drawImage( sourceImg, 0, 0, width, height, null );

ColorSpace grayColorSpace = ColorSpace.getInstance( ColorSpace.CS_GRAY );
ColorConvertOp op = new ColorConvertOp( grayColorSpace, image.getColorModel().getColorSpace(), null );
op.filter( image, image );

The results of this option were the following:

SourceOutputExecution Time
source option6 531ms

 

9. Conclusions

So we used many different methods to try and achieve the same goal. Overall the best quality gray scale image was the one from Option 6, however this was by no means the fastest. I couldn’t find anything on avoiding the transparent to black issue, otherwise I would have probably opted for Option 4. To summarise I’ve put all the options in a single table so that you can compare and decide which is the best option for you:

 

OptionSourceOutputExecution Time% of Gray
1sourceoption1 158msN/A
2asourceoption2 - method 80828ms80
2bsourceoption2 - method 50827ms50
3sourceoption3221msN/A
4sourceoption414msN/A
5sourceoption517msN/A
6sourceoption6531msN/A

 

 

Alessandro