Повысить качество изображения в оттенках серого, полученного из BufferedImage

Я пытаюсь повысить качество изображения, полученного из BufferedImage. Конечная цель - ввести JPEG-файл (здесь он извлекается из файла на компьютере), преобразовать его в TIFF в градациях серого и затем вывести в виде байтового массива. Я включил код для сохранения окончательного изображения на ПК, чтобы легче было распознать проблему.

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();
    }
}

В настоящее время проблема заключается в том, что конечное изображение имеет низкое качество - при увеличении видно много пустого пространства между пикселями, составляющими изображение. Насколько я понимаю, это, возможно, связано с используемым алгоритмом сглаживания (Флойд-Стейнберг), поэтому изображение технически не воспроизводится в оттенках серого.

Я попытался несколько решений, которые я опубликую в комментариях, но безуспешно. Мой вопрос заключается в том, может ли конечное качество быть улучшено с помощью моего текущего решения или если мое преобразование в оттенки серого имеет недостатки, и imageToBlackAndWhite Метод не подходит для моих нужд.

1 ответ

Решение

Теперь, когда мы установили, что желаемым результатом действительно является изображение в оттенках серого, мы можем исправить код так, чтобы он получал TIFF в оттенках серого.

Необходимо изменить две вещи, сначала преобразование цветового пространства из RGB в серый:

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

Я предпочитаю ColorConvertOp, так как он наиболее "правильный", и на большинстве платформ используется нативный код. Но любой из перечисленных вами методов также должен работать. Вы также можете рассмотреть возможность переименования метода в imageToGrayScale для ясности.

Кроме того, вам необходимо изменить настройку сжатия TIFF, поскольку сжатие CCITT T.4 можно использовать только с двоичными черно-белыми изображениями (оно создано для передачи факсов). Я предлагаю вам использовать сжатие Deflate или LZW, или, возможно, JPEG, если вы можете жить со сжатием с потерями. Все это хорошо работает с данными в градациях серого:

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

PS: Вы также должны избавиться от writeToByteArrayStream метод, так как ваш текущий код записывает TIFF дважды, один раз без сжатия, используя ImageIO.write(...)затем один раз сжал с помощью writer.write(...),

PPS: createImage Метод также может быть значительно упрощен, учитывая, что ByteArrayOutputStream уже содержит полный 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);
}
Другие вопросы по тегам