Grayscale and Indexed Color PNG Images on Android

Grayscale and Indexed Color PNG Images on Android

By August 10, 2015Android

I recently had to improve the quality of heads up navigation images on an embedded Android system and found an interesting solution using uncommon color modes for the images on Android. The mapping SDK used provided an image like this, 300-500 pixels, full color, with a byte each for red, green, blue, and alpha when decompressed into bitmap form:

Example image (16KB)

French_Road_Sign_Priority_Turn

The previous implementation scaled the image down to 30×30 using the Android APIs before sending it to the embedded system and scaling it back up to 100×100 for display:

Example image (3KB)

French_Road_Sign_Priority_Turn_30

 

This looked horrible, but the small file size was needed so the image could be sent quickly, parsed quickly, and displayed quickly with minimum processing time and battery use. A heads up navigation image that shows up 3 seconds late isn’t nearly as useful as one that shows up in half a second.

When suitable, indexed color and grayscale can half the byte size of your image and half it again without impacting quality. These are difficult to generate natively on Android, however, because there are no constants or arguments for the Bitmap and PNG compression utilities for these modes. Fortunately someone wrote up a really good example using the PNGJ library and it works fine on Android.

Here’s the indexed color version (100×100, 2KB):

indexed256_100

And for these signs, grayscale is fine, so grayscale version I wrote (100×100, 1KB):

grayscale_100

This allows full resolution instead of blocky scaling, but still keeps the size down for quick, low battery usage display. I extracted the code from the Android app and put it in a sample Eclipse Java project here: https://github.com/lnanek/ExampleGenerateLowColorPngImage

Relevant code:

 private static int extractLuminance(final int r, final int g, final int b) {
        return (int) (0.299 * r + 0.587 * g + 0.114 * b);
    }

    public static void toGrayscale(final String inputFilename,
            final String outputFilename, final boolean preserveMetaData) {
        // Read input
        final PngReader inputPngReader = new PngReader(new File(inputFilename));
        System.out.println("Read input: " + inputPngReader.toString());

        // Confirm compatible
        final int inputChannels = inputPngReader.imgInfo.channels;
        if (inputChannels < 3 || inputPngReader.imgInfo.bitDepth != 8) {
            throw new RuntimeException("This method is for RGB8/RGBA8 images");
        }

        // Setup output
        final ImageInfo outputImageSettings = new ImageInfo(
                inputPngReader.imgInfo.cols, inputPngReader.imgInfo.rows, 8,
                false, true, false);
        final PngWriter outputPngWriter = new PngWriter(
                new File(outputFilename), outputImageSettings, true);
        final ImageLineInt outputImageLine = new ImageLineInt(
                outputImageSettings);

        // Copy meta data if desired
        if (preserveMetaData) {
            outputPngWriter.copyChunksFrom(inputPngReader.getChunksList(),
                    ChunkCopyBehaviour.COPY_ALL_SAFE);
        }

        // For each row of input
        for (int rowIndex = 0; rowIndex < inputPngReader.imgInfo.rows; rowIndex++) {

            final IImageLine inputImageLine = inputPngReader.readRow();
            final int[] scanline = ((ImageLineInt) inputImageLine)
                    .getScanline(); // to save typing

            // For each column
            for (int columnIndex = 0; columnIndex < inputPngReader.imgInfo.cols; columnIndex++) {
                outputImageLine.getScanline()[columnIndex] = extractLuminance(
                        scanline[columnIndex * inputChannels],
                        scanline[columnIndex * inputChannels + 1],
                        scanline[columnIndex * inputChannels] + 2);
            }
            outputPngWriter.writeRow(outputImageLine, rowIndex);
        }
        inputPngReader.end(); // it's recommended to end the reader first, in
                                // case there are trailing chunks to read
        outputPngWriter.end();
    }

 

So take each line of the input image, calculate the brightness from the red, green, and blue, then output that to a new image. In the future, if I can adapt the indexed color version to average the colors and get them down to only 16, instead of 256, it may be possible to get an even better result!

 

Lance Nanek

About Lance Nanek

Veteran software engineer who has shipped on every major mobile platform and written server software for both in house servers and cloud.

Leave a Reply