2

I am attempting to increase the quality of an image produced from a BufferedImage. The final aim is for a JPEG to be input (here it is retrieved from a file on the computer), be converted to a grayscale TIFF and then output as a byte array. I have included code to save the final image to the PC so it is easier to discern the problem.

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.SampleModel;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.media.jai.ImageLayout;
import javax.media.jai.JAI;
import javax.media.jai.KernelJAI;
import javax.media.jai.LookupTableJAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.operator.ErrorDiffusionDescriptor;

public class ByteConversionService {

    private static ByteArrayOutputStream baos;
    private static ImageWriter writer;
    private static ImageOutputStream ios;
    private static ImageWriteParam writeParam;

    public static void main(String args[]) {
        try {
            convertBufferedImageToByteArray();
        } catch (Exception e) {

        }
    }

    private static byte[] convertBufferedImageToByteArray()
            throws Exception {
        byte[] convertedByteArray = null;
        resourceSetup();
        try {                   
            File file = new File("../proj/src/image.jpg");
            BufferedImage image = ImageIo.read(file);
            convertImageToTif(image);
            createImage(baos);
            convertedByteArray = baos.toByteArray();
        } finally {
            resourceCleanup();
        }   
        return convertedByteArray;
    }

    private static void resourceSetup() throws Exception {
        baos = new ByteArrayOutputStream();
        writer = ImageIO.getImageWritersByFormatName(
                "tif").next();
        ios = ImageIO.createImageOutputStream(baos);
        writer.setOutput(ios);
        writeParam = writer.getDefaultWriteParam();
        writeParamSetUp(writeParam);
    }

    private static void writeParamSetUp(ImageWriteParam writeParam) {
        writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        writeParam.setCompressionType("CCITT T.4");
    }

    private static void convertImageToTif(BufferedImage image) throws Exception {
        try {
            BufferedImage blackAndWhiteImage = imageToBlackAndWhite(image);
            writeToByteArrayStream(blackAndWhiteImage);
            IIOImage iioImage = new IIOImage(blackAndWhiteImage, null, null);
            writer.write(null, iioImage, writeParam);
        } finally {
            image.flush();
        }
    }

    private static BufferedImage imageToBlackAndWhite(BufferedImage image) {
        PlanarImage surrogateImage = PlanarImage.wrapRenderedImage(image);
        LookupTableJAI lut = new LookupTableJAI(new byte[][] {
                { (byte) 0x00, (byte) 0xff }, { (byte) 0x00, (byte) 0xff },
                { (byte) 0x00, (byte) 0xff } });
        ImageLayout layout = new ImageLayout();
        byte[] map = new byte[] { (byte) 0x00, (byte) 0xff };
        ColorModel cm = new IndexColorModel(1, 2, map, map, map);
        layout.setColorModel(cm);
        SampleModel sm = new MultiPixelPackedSampleModel(DataBuffer.TYPE_BYTE,
                surrogateImage.getWidth(), surrogateImage.getHeight(), 1);
        layout.setSampleModel(sm);
        RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout);
        PlanarImage op = ErrorDiffusionDescriptor.create(surrogateImage, lut,
                KernelJAI.ERROR_FILTER_FLOYD_STEINBERG, hints);
        BufferedImage blackAndWhiteImage = op.getAsBufferedImage();
        return blackAndWhiteImage;
    }

    private static void writeToByteArrayStream(BufferedImage image) throws Exception {
        ImageIO.write(image, "tif", baos);
    }

    private static void createImage(ByteArrayOutputStream baos) throws Exception {
        ByteArrayInputStream bis = new ByteArrayInputStream(baos.toByteArray());
        ImageReader reader = (ImageReader) ImageIO.getImageReadersByFormatName(
                "tif").next();
        Object source = bis;
        ImageInputStream iis = ImageIO.createImageInputStream(source);
        reader.setInput(iis, true);
        ImageReadParam param = reader.getDefaultReadParam();
        Image image = reader.read(0, param);
        BufferedImage buffered = new BufferedImage(image.getWidth(null),
                image.getHeight(null), BufferedImage.TYPE_INT_RGB);
        Graphics2D g2 = buffered.createGraphics();
        g2.drawImage(image, null, null);
        File file = new File("../proj/src/image2.tif");
        ImageIO.write(buffered, "tif", file);
    }

    private static void resourceCleanup() throws Exception {
        ios.flush();
        ios.close();
        baos.flush();
        baos.close();
        writer.dispose();
    }
}

The current issue is that the final image is of low quality - zooming in shows a lot of white space between the pixels composing the picture. My understanding is this is possibly due to the dithering algorithm used (Floyd-Steinberg) so the image is not technically reproduced in grayscale.

I have attempted multiple solutions which I will post in the comments, but with no success. My question is if the final quality can be increased with my current solution or if my conversion to grayscale is flawed and the imageToBlackAndWhite method is incorrect for my needs.

jj1190
  • 23
  • 6
  • http://stackoverflow.com/q/2209766/7395270 http://www.jguru.com/faq/view.jsp?EID=221919 http://codesquire.com/post/GrayScaleJava http://stackoverflow.com/questions/9131678/convert-a-rgb-image-to-grayscale-image-reducing-the-memory-in-java – jj1190 Jan 11 '17 at 11:03
  • This looks much better! I'm still confused about one thing though, do you want the resulting picture to be full gray scale? Or pure black and pure white only? Your `imageToBlackAndWhite` method will only do the latter (and the output looks as I would expect from such a conversion). So I would guess you want the other... – Harald K Jan 11 '17 at 13:06
  • Absolutely the former. When first researching and implementing this code I evidently googled the incorrect terminology for the colour conversion I wanted. I suspected that the issue may lie with the `imageToBlackAndWhite` method and that it was actually performing as expected. Combining your compression suggestion with the conversion on the first link resulted in a grayscale image of high quality so thank you. Would you want to provide an answer so I can credit you or shall I just answer myself? – jj1190 Jan 11 '17 at 14:15
  • 1
    Ummm... off-topic, but to you **really** want a scan of your passport be publicly available? This looks horrible from a privacy point of view (and entirely unacceptable if you are not the owner of this passport, but I hope that you are...) – Marco13 Jan 11 '17 at 17:05
  • The original image is the second result when searching Google Images for "passport scan" so the damage is already done. My logic for using it was the passport is long expired however you are still absolutely correct and I will remove the links. – jj1190 Jan 11 '17 at 19:12
  • @Marco13 That *is* a good point. I was assuming this was just test data. – Harald K Jan 11 '17 at 19:25
  • If so, you should also remove it from flickr - the link is still in the revision history. – Marco13 Jan 11 '17 at 20:44
  • Done. Will choose better test data in future. – jj1190 Jan 11 '17 at 21:13

1 Answers1

1

Now that we've established that the desired outcome is indeed a gray scale image, we can fix the code so that is produces a gray scale TIFF.

Two things needs to be changed, first the color space conversion from RGB to Gray:

private static BufferedImage imageToBlackAndWhite(BufferedImage image) {
    ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
    ColorConvertOp op = new ColorConvertOp(cs, null);  
    return op.filter(image, null);
}

I prefer the ColorConvertOp, as it is the most "correct", and on most platforms uses native code. But any of the other methods you listed should also work. You may also want to consider renaming the method to imageToGrayScale for clarity.

In addition, you need to change the TIFF compression setting, as CCITT T.4 compression can only be used with binary black/white images (it's created for FAX transmissions). I suggest you use the Deflate or LZW compression, or perhaps JPEG, if you can live with a lossy compression. These all work well with grayscale data:

private static void writeParamSetUp(ImageWriteParam writeParam) {
    writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
    writeParam.setCompressionType("Deflate"); // or LZW or JPEG
} 

PS: You should also get rid of the writeToByteArrayStream method, as your current code writes the TIFF twice, once uncompressed using ImageIO.write(...), then once compressed using writer.write(...).

PPS: The createImage method can also be simplified a lot, given that the ByteArrayOutputStream already contains a full TIFF.

private static void createImage(ByteArrayOutputStream baos) throws Exception {
    File file = new File("../proj/src/image2.tif");
    Files.write(file.toPath(), baos.toByteArray(), StandardOpenOption.CREATE);
}
Harald K
  • 26,314
  • 7
  • 65
  • 111
  • Answers here gave the desired results so many thanks. Removal of the `writeToByteArrayStream` method seems to result in `baos.toByteArray()` returning a null so I will have to look into that further. For future readers, the `ColorSpace` option given results in a darker gray scale image than that produced by the first link in the comments (manipulation of the `BufferedImage`). – jj1190 Jan 11 '17 at 15:34
  • I don't think `baos.toByteArray()` can return `null`. Do you mean an empty array? I think the problem might be that you invoke `flush()` on the stream (in `resourceCleanUp`) *after* you create the new file. So, it works by accident, as `ImageIO.write()` closes the stream (which also flushes it). But the byte array is about 3 times as large as it needs to, and contains two separate TIFFs concatenated... PS: I didn't see this in my test, as I use my own TIFF plugin, not the one from JAI, and it flushes the stream after `write()`.. :-P – Harald K Jan 11 '17 at 19:45
  • My mistake, it is absolutely an empty array. However, debugging after removing `writeToByteArrayStream` and any reference to `flush()` in `resourceCleanUp()` still shows an empty array. As suggested though there was redundant code as removing `writer`, `ios` and `writeParam` still results in the same output. Thank you for the help, it is appreciated as the JAI documentation can be hard to interpret at times. – jj1190 Jan 12 '17 at 09:58