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

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

【问题讨论】:

标签: java gif bufferedimage javax.imageio


【解决方案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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-06-19
          • 1970-01-01
          • 2019-09-26
          • 2021-02-01
          • 2012-05-03
          • 2011-06-21
          相关资源
          最近更新 更多