将每个动画 GIF 帧转换为单独的 BufferedImage

Posted

技术标签:

【中文标题】将每个动画 GIF 帧转换为单独的 BufferedImage【英文标题】:Convert each animated GIF frame to a separate BufferedImage 【发布时间】:2012-02-14 13:47:42 【问题描述】:

我希望能够将动画 GIF 作为输入,计算帧数(可能还有其他元数据),并将每个帧转换为 BufferedImage。 我该怎么做?

【问题讨论】:

见:***.com/questions/777947/… 这个库也可能有帮助:github.com/dragon66/icafe/wiki 【参考方案1】:

如果您希望所有帧的大小相同(用于优化的 GIF),请尝试以下操作:

try 
    String[] imageatt = new String[]
            "imageLeftPosition",
            "imageTopPosition",
            "imageWidth",
            "imageHeight"
    ;    

    ImageReader reader = (ImageReader)ImageIO.getImageReadersByFormatName("gif").next();
    ImageInputStream ciis = ImageIO.createImageInputStream(new File("house2.gif"));
    reader.setInput(ciis, false);

    int noi = reader.getNumImages(true);
    BufferedImage master = null;

    for (int i = 0; i < noi; i++)  
        BufferedImage image = reader.read(i);
        IIOMetadata metadata = reader.getImageMetadata(i);

        Node tree = metadata.getAsTree("javax_imageio_gif_image_1.0");
        NodeList children = tree.getChildNodes();

        for (int j = 0; j < children.getLength(); j++) 
            Node nodeItem = children.item(j);

            if(nodeItem.getNodeName().equals("ImageDescriptor"))
                Map<String, Integer> imageAttr = new HashMap<String, Integer>();

                for (int k = 0; k < imageatt.length; k++) 
                    NamedNodeMap attr = nodeItem.getAttributes();
                    Node attnode = attr.getNamedItem(imageatt[k]);
                    imageAttr.put(imageatt[k], Integer.valueOf(attnode.getNodeValue()));
                
                if(i==0)
                    master = new BufferedImage(imageAttr.get("imageWidth"), imageAttr.get("imageHeight"), BufferedImage.TYPE_INT_ARGB);
                
                master.getGraphics().drawImage(image, imageAttr.get("imageLeftPosition"), imageAttr.get("imageTopPosition"), null);
            
        
        ImageIO.write(master, "GIF", new File( i + ".gif")); 
    
 catch (IOException e) 
    e.printStackTrace();

【讨论】:

我用这个保存了我的工作 唯一的东西 - 而不是 ImageIO.write(master, "GIF", new File( i + ".gif")); 我使用 ImageIO.write(master, "PNG", new File( i + ".png"));【参考方案2】:

这里没有一个答案是正确的并且适合动画。每个解决方案都有很多问题,所以我写了一些实际上适用于所有 gif 文件的东西。例如,这会考虑到图像的实际宽度和高度,而不是假设第一帧的宽度和高度会填满整个画布,不,不幸的是它不是那么简单。其次,这不会留下任何透明的泡菜。第三,这考虑了处置方法。第四,这会给你帧之间的延迟(* 10 如果你想在 Thread.sleep() 中使用它)。

private ImageFrame[] readGif(InputStream stream) throws IOException
    ArrayList<ImageFrame> frames = new ArrayList<ImageFrame>(2);

    ImageReader reader = (ImageReader) ImageIO.getImageReadersByFormatName("gif").next();
    reader.setInput(ImageIO.createImageInputStream(stream));

    int lastx = 0;
    int lasty = 0;

    int width = -1;
    int height = -1;

    IIOMetadata metadata = reader.getStreamMetadata();

    Color backgroundColor = null;

    if(metadata != null) 
        IIOMetadataNode globalRoot = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());

        NodeList globalColorTable = globalRoot.getElementsByTagName("GlobalColorTable");
        NodeList globalScreeDescriptor = globalRoot.getElementsByTagName("LogicalScreenDescriptor");

        if (globalScreeDescriptor != null && globalScreeDescriptor.getLength() > 0)
            IIOMetadataNode screenDescriptor = (IIOMetadataNode) globalScreeDescriptor.item(0);

            if (screenDescriptor != null)
                width = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenWidth"));
                height = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenHeight"));
            
        

        if (globalColorTable != null && globalColorTable.getLength() > 0)
            IIOMetadataNode colorTable = (IIOMetadataNode) globalColorTable.item(0);

            if (colorTable != null) 
                String bgIndex = colorTable.getAttribute("backgroundColorIndex");

                IIOMetadataNode colorEntry = (IIOMetadataNode) colorTable.getFirstChild();
                while (colorEntry != null) 
                    if (colorEntry.getAttribute("index").equals(bgIndex)) 
                        int red = Integer.parseInt(colorEntry.getAttribute("red"));
                        int green = Integer.parseInt(colorEntry.getAttribute("green"));
                        int blue = Integer.parseInt(colorEntry.getAttribute("blue"));

                        backgroundColor = new Color(red, green, blue);
                        break;
                    

                    colorEntry = (IIOMetadataNode) colorEntry.getNextSibling();
                
            
        
    

    BufferedImage master = null;
    boolean hasBackround = false;

    for (int frameIndex = 0;; frameIndex++) 
        BufferedImage image;
        try
            image = reader.read(frameIndex);
        catch (IndexOutOfBoundsException io)
            break;
        

        if (width == -1 || height == -1)
            width = image.getWidth();
            height = image.getHeight();
        

        IIOMetadataNode root = (IIOMetadataNode) reader.getImageMetadata(frameIndex).getAsTree("javax_imageio_gif_image_1.0");
        IIOMetadataNode gce = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
        NodeList children = root.getChildNodes();

        int delay = Integer.valueOf(gce.getAttribute("delayTime"));

        String disposal = gce.getAttribute("disposalMethod");

        if (master == null)
            master = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            master.createGraphics().setColor(backgroundColor);
            master.createGraphics().fillRect(0, 0, master.getWidth(), master.getHeight());

        hasBackround = image.getWidth() == width && image.getHeight() == height;

            master.createGraphics().drawImage(image, 0, 0, null);
        else
            int x = 0;
            int y = 0;

            for (int nodeIndex = 0; nodeIndex < children.getLength(); nodeIndex++)
                Node nodeItem = children.item(nodeIndex);

                if (nodeItem.getNodeName().equals("ImageDescriptor"))
                    NamedNodeMap map = nodeItem.getAttributes();

                    x = Integer.valueOf(map.getNamedItem("imageLeftPosition").getNodeValue());
                    y = Integer.valueOf(map.getNamedItem("imageTopPosition").getNodeValue());
                
            

            if (disposal.equals("restoreToPrevious"))
                BufferedImage from = null;
                for (int i = frameIndex - 1; i >= 0; i--)
                    if (!frames.get(i).getDisposal().equals("restoreToPrevious") || frameIndex == 0)
                        from = frames.get(i).getImage();
                        break;
                    
                

                
                    ColorModel model = from.getColorModel();
                    boolean alpha = from.isAlphaPremultiplied();
                    WritableRaster raster = from.copyData(null);
                    master = new BufferedImage(model, raster, alpha, null);
                
            else if (disposal.equals("restoreToBackgroundColor") && backgroundColor != null)
                if (!hasBackround || frameIndex > 1)
                    master.createGraphics().fillRect(lastx, lasty, frames.get(frameIndex - 1).getWidth(), frames.get(frameIndex - 1).getHeight());
                
            
            master.createGraphics().drawImage(image, x, y, null);

            lastx = x;
            lasty = y;
        

        
            BufferedImage copy;

            
                ColorModel model = master.getColorModel();
                boolean alpha = master.isAlphaPremultiplied();
                WritableRaster raster = master.copyData(null);
                copy = new BufferedImage(model, raster, alpha, null);
            
            frames.add(new ImageFrame(copy, delay, disposal, image.getWidth(), image.getHeight()));
        

        master.flush();
    
    reader.dispose();

    return frames.toArray(new ImageFrame[frames.size()]);

还有 ImageFrame 类:

import java.awt.image.BufferedImage;
public class ImageFrame 
    private final int delay;
    private final BufferedImage image;
    private final String disposal;
    private final int width, height;

    public ImageFrame (BufferedImage image, int delay, String disposal, int width, int height)
        this.image = image;
        this.delay = delay;
        this.disposal = disposal;
        this.width = width;
        this.height = height;
    

    public ImageFrame (BufferedImage image)
        this.image = image;
        this.delay = -1;
        this.disposal = null;
        this.width = -1;
        this.height = -1;
    

    public BufferedImage getImage() 
        return image;
    

    public int getDelay() 
        return delay;
    

    public String getDisposal() 
        return disposal;
    

    public int getWidth() 
        return width;
    

    public int getHeight() 
            return height;
    

【讨论】:

其他方法产生的输出略有损坏,效果很好。 这是几年前的哇。不管怎样,我用javascript写了一个完整的gif实现。如果您仍然遇到问题,我可以为您移植它,并且保证可以正常工作。我确实注意到内置的 gif 图像读取器在解码实际像素数据时确实存在一些问题,除了重写之外,没有什么可以解决的。 我不得不将几段 SO 代码串在一起,以在动画 GIF 上实现调整大小和裁剪以适应。我让它完美地与这个样本一起工作。首先我尝试了 Francesco 的方法,但输出的前几帧有一些噪音。如果其他一些迷失的灵魂试图做类似的事情,我会在接下来的几天里把我的解决方案放在 github 上。 Java 和 GIF 有点像狂野的西部。 没有。该算法错误地决定,它不是插入透明像素,而是插入指定的背景颜色,这与规范的想法有点偏离。 这有时会在随机帧上出现故障。【参考方案3】:

是的,我以前从未做过任何类似这样的事情,但是在 Java 中进行一些谷歌搜索和摆弄让我得到了这个:

public ArrayList<BufferedImage> getFrames(File gif) throws IOException
    ArrayList<BufferedImage> frames = new ArrayList<BufferedImage>();
    ImageReader ir = new GIFImageReader(new GIFImageReaderSpi());
    ir.setInput(ImageIO.createImageInputStream(gif));
    for(int i = 0; i < ir.getNumImages(true); i++)
        frames.add(ir.getRawImageType(i).createBufferedImage(ir.getWidth(i), ir.getHeight(i)));
    return frames;

编辑:请参阅Ansel Zandegran's modification 我的回答。

【讨论】:

【参考方案4】:

要将动画 GIF 拆分为单独的 BufferedImage 帧:

try 
    ImageReader reader = ImageIO.getImageReadersByFormatName("gif").next();
    File input = new File("input.gif");
    ImageInputStream stream = ImageIO.createImageInputStream(input);
    reader.setInput(stream);

    int count = reader.getNumImages(true);
    for (int index = 0; index < count; index++) 
        BufferedImage frame = reader.read(index);
        // Here you go
    
 catch (IOException ex) 
    // An I/O problem has occurred

【讨论】:

【参考方案5】:

Alex 的回答涵盖了大多数情况,但确实存在一些问题。它没有正确处理透明度(至少根据通用约定),并且它正在将当前帧的处理方法应用于不正确的前一帧。这是一个可以正确处理这些情况的版本:

private ImageFrame[] readGIF(ImageReader reader) throws IOException 
    ArrayList<ImageFrame> frames = new ArrayList<ImageFrame>(2);

    int width = -1;
    int height = -1;

    IIOMetadata metadata = reader.getStreamMetadata();
    if (metadata != null) 
        IIOMetadataNode globalRoot = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());

        NodeList globalScreenDescriptor = globalRoot.getElementsByTagName("LogicalScreenDescriptor");

        if (globalScreenDescriptor != null && globalScreenDescriptor.getLength() > 0) 
            IIOMetadataNode screenDescriptor = (IIOMetadataNode) globalScreenDescriptor.item(0);

            if (screenDescriptor != null) 
                width = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenWidth"));
                height = Integer.parseInt(screenDescriptor.getAttribute("logicalScreenHeight"));
            
        
    

    BufferedImage master = null;
    Graphics2D masterGraphics = null;

    for (int frameIndex = 0;; frameIndex++) 
        BufferedImage image;
        try 
            image = reader.read(frameIndex);
         catch (IndexOutOfBoundsException io) 
            break;
        

        if (width == -1 || height == -1) 
            width = image.getWidth();
            height = image.getHeight();
        

        IIOMetadataNode root = (IIOMetadataNode) reader.getImageMetadata(frameIndex).getAsTree("javax_imageio_gif_image_1.0");
        IIOMetadataNode gce = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
        int delay = Integer.valueOf(gce.getAttribute("delayTime"));
        String disposal = gce.getAttribute("disposalMethod");

        int x = 0;
        int y = 0;

        if (master == null) 
            master = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
            masterGraphics = master.createGraphics();
            masterGraphics.setBackground(new Color(0, 0, 0, 0));
         else 
            NodeList children = root.getChildNodes();
            for (int nodeIndex = 0; nodeIndex < children.getLength(); nodeIndex++) 
                Node nodeItem = children.item(nodeIndex);
                if (nodeItem.getNodeName().equals("ImageDescriptor")) 
                    NamedNodeMap map = nodeItem.getAttributes();
                    x = Integer.valueOf(map.getNamedItem("imageLeftPosition").getNodeValue());
                    y = Integer.valueOf(map.getNamedItem("imageTopPosition").getNodeValue());
                
            
        
        masterGraphics.drawImage(image, x, y, null);

        BufferedImage copy = new BufferedImage(master.getColorModel(), master.copyData(null), master.isAlphaPremultiplied(), null);
        frames.add(new ImageFrame(copy, delay, disposal));

        if (disposal.equals("restoreToPrevious")) 
            BufferedImage from = null;
            for (int i = frameIndex - 1; i >= 0; i--) 
                if (!frames.get(i).getDisposal().equals("restoreToPrevious") || frameIndex == 0) 
                    from = frames.get(i).getImage();
                    break;
                
            

            master = new BufferedImage(from.getColorModel(), from.copyData(null), from.isAlphaPremultiplied(), null);
            masterGraphics = master.createGraphics();
            masterGraphics.setBackground(new Color(0, 0, 0, 0));
         else if (disposal.equals("restoreToBackgroundColor")) 
            masterGraphics.clearRect(x, y, image.getWidth(), image.getHeight());
        
    
    reader.dispose();

    return frames.toArray(new ImageFrame[frames.size()]);


private class ImageFrame 
    private final int delay;
    private final BufferedImage image;
    private final String disposal;

    public ImageFrame(BufferedImage image, int delay, String disposal) 
        this.image = image;
        this.delay = delay;
        this.disposal = disposal;
    

    public BufferedImage getImage() 
        return image;
    

    public int getDelay() 
        return delay;
    

    public String getDisposal() 
        return disposal;
    

this ImageMagick tutorial 中很好地描述了 GIF 动画的工作原理。

【讨论】:

您需要对此代码进行一些更改。如果延迟为零,则将其设置为 100,或者如果它低于 30 毫秒,则将其乘以 10。这对我有用。 这个工作与这里的大多数其他人不同,它以某种方式提出了问题。【参考方案6】:

使用c24w's solution,替换:

frames.add(ir.getRawImageType(i).createBufferedImage(ir.getWidth(i), ir.getHeight(i)));

与:

frames.add(ir.read(i));

【讨论】:

【参考方案7】:

我自己编写了一个 GIF 图像解码器,并在 GitHub 上根据 Apache License 2.0 发布。你可以在这里下载:https://github.com/DhyanB/Open-Imaging。用法示例:

void example(final byte[] data) throws Exception 
    final GifImage gif = GifDecoder .read(data);
    final int width = gif.getWidth();
    final int height = gif.getHeight();
    final int background = gif.getBackgroundColor();
    final int frameCount = gif.getFrameCount();
    for (int i = 0; i < frameCount; i++) 
        final BufferedImage img = gif.getFrame(i);
        final int delay = gif.getDelay(i);
        ImageIO.write(img, "png", new File(OUTPATH + "frame_" + i + ".png"));
    

解码器支持GIF87a、GIF89a、动画、透明和隔行扫描。框架将具有图像本身的宽度和高度,并放置在画布上的正确位置。它尊重框架透明度和处置方法。查看项目描述以获取更多详细信息,例如背景颜色的处理。

此外,解码器不会出现ImageIO 错误:ArrayIndexOutOfBoundsException: 4096 while reading gif file。

我很乐意得到一些反馈。我一直在用一组具有代表性的图像进行测试,但是,一些真实的现场测试会很好。

【讨论】:

以上是关于将每个动画 GIF 帧转换为单独的 BufferedImage的主要内容,如果未能解决你的问题,请参考以下文章

使用 Python 将 MP4视频 转换为GIF动画

将两个动画情节组合成一个 GIF/MP4

如何在 Pygame 中加载动画 GIF 并获取所有单独的帧?

循环中的 MATLAB getframe 冲突以创建动画 gif

用Python编写录屏程序将播放的视频用截屏方法转换为多帧图像编辑后保存为GIF格式动图文件

用Python编写录屏程序将播放的视频用截屏方法转换为多帧图像编辑后保存为GIF格式动图文件