将每个动画 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的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Pygame 中加载动画 GIF 并获取所有单独的帧?
循环中的 MATLAB getframe 冲突以创建动画 gif