【问题标题】:Image Quality Loss When Drawing One BufferedImage to Another Using Graphics2D使用 Graphics2D 将一个 BufferedImage 绘制到另一个时的图像质量损失
【发布时间】:2016-11-26 16:57:19
【问题描述】:

我正在创建一个显示动画 gif 的程序。因为一些动画 gif 文件只存储与前一帧相比发生变化的像素,所以在显示每一帧之前,它被绘制到名为 master 的主 BufferedImage 对象,然后正在绘制 BufferedImage。问题在于将帧(存储为BufferedImage 对象本身)绘制到master 会降低它们的质量。

我知道框架本身没有问题,如果我只是单独绘制框架而不将它们绘制到master,那么它们看起来很好。这也不是问题,因为有很多帧相互叠加,即使第一帧显示质量下降。我尝试将每个 RenderingHint 设置为每个可能的值,但它没有任何改变。

以下是我的代码,省略了解决此问题的不必要部分:

import java.awt.image.BufferedImage;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import javax.activation.MimetypesFileTypeMap;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.FileImageInputStream;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

@SuppressWarnings("serial")
class A extends javax.swing.JPanel{

    public static final String PATH = "C:/Users/Owner/Desktop/test.gif";
    public B i;


    public A() throws java.io.IOException{
        i = new B(new java.io.File(PATH));
        i.registerComponent(this);
    }

    @Override
    public java.awt.Dimension preferredSize(){
        return i.getSize();
    }

    @Override
    public void paintComponent(java.awt.Graphics g){
        i.draw(g);
    }

    public static void main(String[] args){
        javax.swing.SwingUtilities.invokeLater(new Runnable(){
            public void run(){
                javax.swing.JFrame f = new javax.swing.JFrame();
                f.setDefaultCloseOperation(javax.swing.JFrame.EXIT_ON_CLOSE);
                try{
                    f.add(new A());
                }catch(Exception e){

                }
                f.pack();
                f.setVisible(true);
            }
        });
    }
}

class B{

    private final static String META_FORMAT = "javax_imageio_gif_image_1.0";
    // instance variables
    private final BufferedImage[] frames;
    private BufferedImage master;// Because Gif images can store only the changing
    // pixels, the first frame is drawn to this image, then the next one *on top of it*, etc.
    private final short[] frameDurations; // in 100ths of a second
    private final short[] xOffsets;
    private final short[] yOffsets;
    private int frame = 0;
    private final Dimension size;// the size of the gif (calculated in findSize)
    private final Timer animationTimer;

    // constructor from a File (checked to be a gif)
    public B(File src) throws IOException{
        if (!(new MimetypesFileTypeMap().getContentType(src.getPath()).equals("image/gif"))){
            throw new IOException("File is not a gif.  It's Mime Type is: " + 
                new MimetypesFileTypeMap().getContentType(src.getAbsolutePath()));
        }
        FileImageInputStream stream = new FileImageInputStream(src);
        Iterator<ImageReader> readers = ImageIO.getImageReaders(stream);
        ImageReader reader = null;
        // loop through the availible ImageReaders, find one for .gif
        while (readers.hasNext()){
            reader = readers.next();
            String metaFormat = reader.getOriginatingProvider().getNativeImageMetadataFormatName();
            // if it's a gif
            if ("gif".equalsIgnoreCase(reader.getFormatName()) && META_FORMAT.equals(metaFormat)){
                break;
            }else{
                reader = null;
                continue;
            }
        }// while (readers.hasNext())
        // if no reader for gifs was found
        if (reader == null){
            throw new IOException("File could not be read as a gif");
        }
        reader.setInput(stream, false, false);
        // Lists to be converted to arrays and set as the instance variables
        ArrayList<BufferedImage> listFrames = new ArrayList<BufferedImage>();
        ArrayList<Short> listFrameDurs = new ArrayList<Short>();
        ArrayList<Short> listXs = new ArrayList<Short>();
        ArrayList<Short> listYs = new ArrayList<Short>();
        boolean unknownMeta = false;// asume that the metadata can be read until proven otherwise
        // loop until there are no more frames (since that isn't known, break needs to be used)
        for (int i = 0;true;i++){// equivalent of while(true) with a counter
            IIOImage frame = null;
            try{
                frame = reader.readAll(i, null);
            }catch(IndexOutOfBoundsException e){
                break;// this means theres no more frames
            }
            listFrames.add((BufferedImage)frame.getRenderedImage());
            if (unknownMeta){// if the metadata has already proven to be unreadable
                continue;
            }  
            IIOMetadata metadata = frame.getMetadata();
            IIOMetadataNode rootNode = null;
            try{
                rootNode = (IIOMetadataNode) metadata.getAsTree(META_FORMAT);
            }catch(IllegalArgumentException e){
                // means that the metadata can't be read, it's in an unknown format
                unknownMeta = true;
                continue;
            }
            // get the duration of the current frame
            IIOMetadataNode graphicControlExt = (IIOMetadataNode)rootNode.getElementsByTagName("GraphicControlExtension").item(0);
            listFrameDurs.add(Short.parseShort(graphicControlExt.getAttribute("delayTime")));
            // get the x and y offsets
            try{
                 IIOMetadataNode imageDescrip = (IIOMetadataNode)rootNode.getElementsByTagName("ImageDescriptor").item(0);
                listXs.add(Short.parseShort(imageDescrip.getAttribute("imageLeftPosition")));
                listYs.add(Short.parseShort(imageDescrip.getAttribute("imageTopPosition")));
            }catch(IndexOutOfBoundsException e){
                e.printStackTrace();
                listXs.add((short) 0);
                listYs.add((short) 0);
            }
        }// for loop
        reader.dispose();
        // put the values in the lists into the instance variable arrays
        frames = listFrames.toArray(new BufferedImage[0]);
        // looping must be used because the ArrayList can't contian primitives
        frameDurations = new short[listFrameDurs.size()];
        for (int i = 0;i < frameDurations.length;i++){
            frameDurations[i] = (short)(listFrameDurs.get(i) * 10);
        }
        xOffsets = new short[listXs.size()];
        for (int i = 0;i < xOffsets.length;i++){
            xOffsets[i] = listXs.get(i);
        }
        yOffsets = new short[listYs.size()];
        for (int i = 0;i < yOffsets.length;i++){
            yOffsets[i] = listYs.get(i);
        }
        size = findSize();
        animationTimer = new Timer(frameDurations[0], null);
        clearLayers();
    }

    // finds the size of the image in constructors
    private final Dimension findSize(){
        int greatestX = -1;
        int greatestY = -1;
        // loop through the frames and offsets, finding the greatest combination of the two
        for (int i = 0;i < frames.length;i++){
            if (greatestX < frames[i].getWidth() + xOffsets[i]){
                greatestX = frames[i].getWidth() + xOffsets[i];
            }
            if (greatestY < frames[i].getHeight() + yOffsets[i]){
                greatestY = frames[i].getHeight() + yOffsets[i];
            }
        }// loop
        return new Dimension(greatestX, greatestY);
    }// findSize

    private BufferedImage getFrame(){
        /*  returning frames[frame] gives a perfect rendering of each frame (but only changed 
         *  pixels), but when master is returned, even the first frame shows quality reduction
         *  (seen by slowing down the framerate). The issue is with drawing images to master
         */
        Graphics2D g2d = master.createGraphics();
        g2d.drawImage(frames[frame], xOffsets[frame], yOffsets[frame], null);
        g2d.dispose();
        return master;
    }

    public Dimension getSize(){
        return size;
    }

    // adds a FrameChangeListener associated with a component to the Timer
    public void registerComponent(Component c){
        FrameChangeListener l = new FrameChangeListener(c);
        animationTimer.addActionListener(l);
        if (!animationTimer.isRunning()){
            animationTimer.start();
        }
    }

    // draws the image to the given Graphics context (registerComponent must be used for the image
    // to animate properly)
    public void draw(Graphics g){
        g.drawImage(getFrame(), 0, 0, null);
    }

    // resets master
    private void clearLayers(){
        master = new BufferedImage((int)size.getWidth(), (int)size.getHeight(), frames[0].getType());
    }

    // class that listens for the Swing Timer.  
    private class FrameChangeListener implements ActionListener{

        private final Component repaintComponent;

        // the Components repaint method will be invoked whenever the animation changes frame
        protected FrameChangeListener(Component c){
            repaintComponent = c;
        }

        public void actionPerformed(ActionEvent e){
            frame++;
            int delay;
            try{
                delay = frameDurations[frame] * 10;
            }catch(ArrayIndexOutOfBoundsException x){
                frame = 0;
                clearLayers();
                delay = frameDurations[frame] * 10;
            }
            animationTimer.setDelay(delay);
            repaintComponent.repaint();
        }// actionPerformed

    }// FrameChangeListener

}

这是我用来测试的图像文件:

这是它的显示方式:

如果有人能帮我解决这个问题,我们将不胜感激

【问题讨论】:

    标签: java swing bufferedimage graphics2d animated-gif


    【解决方案1】:

    问题在于clearLayers() 方法中的这一行:

    master = new BufferedImage((int)size.getWidth(), (int)size.getHeight(), frames[0].getType());
    

    由于 GIF 使用调色板,BufferedImage 类型将为 TYPE_BYTE_INDEXED。但是,如果您将此参数传递给 BufferedImage 构造函数,它将使用 默认 IndexColorModel(内置的固定 256 调色板),不是来自您的 GIF 的调色板。因此,来自 GIF 的帧将不得不抖动到目的地,因为颜色不匹配。

    改为使用 TYPE_INT_RGB/TYPE_INT_ARGB 作为类型,使用 the constructor that also takes an IndexColorModel parameter 并从 GIF 的帧中传递 IndexColorModel

    在代码中:

    master = new BufferedImage((int)size.getWidth(), (int)size.getHeight(), BufferedImage.TYPE_INT_ARGB);
    

    或者,如果 GIF 的所有帧都使用相同的调色板(不一定是这种情况),以下 应该 也可以工作:

    master = new BufferedImage((int)size.getWidth(), (int)size.getHeight(), frames[0].getType(), (IndexColorModel) frames[0].getColorModel());
    

    但是,由于 OP 报告后一个选项对他不起作用,所以第一个选项可能更安全。 :-)

    【讨论】:

    • 非常感谢!这一直困扰着我这么久!您的第一个解决方案(使用TYPE_INT_ARGB)运行良好,但是使用带有IndexColorModel 参数的构造函数并没有改变任何东西。这不是问题,因为第一个解决方案有效,但我认为最好让您知道
    猜你喜欢
    • 2013-08-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-03
    • 1970-01-01
    • 1970-01-01
    • 2015-04-04
    • 2012-10-08
    相关资源
    最近更新 更多