【问题标题】:Increase quality of grayscale image produced from BufferedImage提高 BufferedImage 生成的灰度图像的质量
【发布时间】:2017-01-11 11:02:51
【问题描述】:

我正在尝试提高从 BufferedImage 生成的图像的质量。最终目标是输入 JPEG(这里是从计算机上的文件中检索),转换为灰度 TIFF,然后作为字节数组输出。我已经包含了将最终图像保存到 PC 的代码,以便更容易识别问题。

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

当前的问题是最终图像质量低下 - 放大显示构成图片的像素之间存在大量空白。我的理解是,这可能是由于使用了抖动算法(Floyd-Steinberg),所以图像在技术上不是以灰度再现的。

我尝试了多种解决方案,我将在 cmets 中发布这些解决方案,但均未成功。我的问题是,我当前的解决方案是否可以提高最终质量,或者我的灰度转换是否存在缺陷,imageToBlackAndWhite 方法不适合我的需求。

【问题讨论】:

  • 这看起来好多了!我仍然对一件事感到困惑,你想要得到的图片是全灰度的吗?还是只有纯黑和纯白?您的 imageToBlackAndWhite 方法只会执行后者(并且输出看起来与我对此类转换的期望一样)。所以我猜你想要另一个......
  • 绝对是前者。当第一次研究和实现这段代码时,我显然用谷歌搜索了我想要的颜色转换的错误术语。我怀疑问题可能出在imageToBlackAndWhite 方法上,并且它实际上按预期执行。将您的压缩建议与第一个链接上的转换相结合,得到了高质量的灰度图像,所以谢谢。你想提供一个答案,以便我可以信任你,还是我应该自己回答?
  • 嗯...题外话了,但对您来说真的想要公开扫描您的护照吗?从隐私的角度来看,这看起来很可怕(如果您不是这本护照的所有者,这是完全不可接受的,但我希望您是……)
  • 原始图像是在 Google 图片中搜索“护照扫描”时的第二个结果,因此损坏已经造成。我使用它的逻辑是护照早已过期,但是您仍然是绝对正确的,我将删除链接。

标签: java bufferedimage javax.imageio image-conversion image-quality


【解决方案1】:

现在我们已经确定所需的结果确实是灰度图像,我们可以修复代码以生成灰度 TIFF。

有两件事需要改变,首先是从RGB到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);
}

我更喜欢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);
}

【讨论】:

  • 这里的答案给出了预期的结果,非常感谢。删除writeToByteArrayStream 方法似乎会导致baos.toByteArray() 返回一个空值,因此我将不得不进一步研究。对于未来的读者,给定的ColorSpace 选项导致的灰度图像比 cmets 中第一个链接生成的图像更暗(操纵BufferedImage)。
  • 我不认为baos.toByteArray() 可以返回null。你的意思是一个空数组?我认为问题可能是您在创建新文件之后在流上调用flush()(在resourceCleanUp 中)。所以,它是偶然的,因为ImageIO.write() 关闭了流(这也刷新了它)。但是字节数组大约是它需要的 3 倍大,并且包含两个连接的单独 TIFF... PS:我在测试中没有看到这个,因为我使用自己的 TIFF 插件,而不是来自 JAI 的插件,并在write().. 之后刷新流:-P
  • 我的错,它绝对是一个空数组。但是,在删除 writeToByteArrayStream 以及在 resourceCleanUp() 中对 flush() 的任何引用后进行调试仍会显示一个空数组。正如所建议的那样,尽管删除 writerioswriteParam 时存在冗余代码,但仍会产生相同的输出。感谢您的帮助,我们非常感谢 JAI 文档有时难以解释。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-09-04
  • 1970-01-01
  • 1970-01-01
  • 2016-12-03
  • 2018-08-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多