将 GIF 切割成帧并将其转换为像素的程序占用了太多内存

Posted

技术标签:

【中文标题】将 GIF 切割成帧并将其转换为像素的程序占用了太多内存【英文标题】:A program that cuts GIFs into frames and converts them into pixels is using too much memory 【发布时间】:2021-11-28 22:21:58 【问题描述】:

我编写了一个将 gif 图像拆分为帧的代码。 然后,它们被缩小成更小的尺寸,分辨率为 128 * 128 像素。然后他们将全帧与特定分辨率组合在一起。

getFrames(String path) 方法从 jar 中获取沿路径的 GIF 文件并创建图像帧。

getResizedimages (...) 方法将帧切割成更小的 128 x 128 图像。

getPixels (Bufferedlmage image) 方法从图像中获取像素数组。

问题在于,在执行这段代码期间(通过测试方法),即使在执行 getFrames (...) 方法或 getFramesRaw (...) 方法的阶段,也会使用非常大量的内存。

import com.sun.imageio.plugins.gif.GIFImageReader;
import com.sun.imageio.plugins.gif.GIFImageReaderSpi;
import org.apache.commons.io.FileUtils;
import org.bukkit.map.MapPalette;

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

public class Gif 

static void test(String path, int height, int width) 
    List<BufferedImage> images = getFrames(path);
    for (int x = 0; x < width; x ++) for (int y = 0; y < height; y ++)
        getResizedFrames(images, width, height, x, y).forEach(image -> getPixels(image));


@SuppressWarnings("deprecation")
public static byte[] getPixels(BufferedImage image) 
    int pixelCount = image.getWidth() * image.getHeight();
    int[] pixels = new int[pixelCount];
    image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels, 0, image.getWidth());
    byte[] colors = new byte[pixelCount];
    for (int i = 0; i < pixelCount; i++)
        colors[i] = MapPalette.matchColor(new Color(pixels[i], true));
    return colors;


public static BufferedImage getScaledImage(BufferedImage image) 
    BufferedImage newImg = new BufferedImage(128, 128, 3);
    Graphics2D g2 = newImg.createGraphics();
    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    g2.drawImage(image, 0, 0, 128, 128, null);
    g2.dispose();
    return newImg;


public static BufferedImage cropImage(BufferedImage completeImg, int width, int height, int xCoord, int yCoord) 
    int coordWidth = completeImg.getWidth() / width;
    int coordHeight = completeImg.getHeight() / height;
    BufferedImage croppedImg = new BufferedImage(completeImg.getWidth(), completeImg.getHeight(), 1);
    Graphics2D g2 = (Graphics2D)croppedImg.getGraphics();
    g2.drawImage(completeImg, 0, 0, completeImg.getWidth(), completeImg.getHeight(), null);
    g2.dispose();
    BufferedImage image = croppedImg.getSubimage(coordWidth * xCoord, coordHeight * yCoord, coordWidth, coordHeight);
    return getScaledImage(image);


public static java.util.List<BufferedImage> getFramesRaw(String path) 
    java.util.List<BufferedImage> frames = new ArrayList<>();
    try 
        File file = File.createTempFile("data", ".gif");
        InputStream stream = Main.class.getResourceAsStream(path);
        FileUtils.copyInputStreamToFile(stream, file);
        stream.close();

        ImageInputStream inputStream = ImageIO.createImageInputStream(file);
        ImageReader ir = new GIFImageReader(new GIFImageReaderSpi());
        ir.setInput(inputStream);

        int number = ir.getNumImages(true);
        for (int i = 0; i < number; i++) frames.add(ir.read(i));
        inputStream.close();
        System.gc();
     catch (Exception e) 
        e.printStackTrace();
    
    return frames;


public static java.util.List<BufferedImage> getFrames(String path) 
    java.util.List<BufferedImage> copies = new ArrayList<>();
    java.util.List<BufferedImage> frames = getFramesRaw(path);
    copies.add(frames.remove(0));
    int width = copies.get(0).getWidth(), height = copies.get(0).getHeight();
    for (BufferedImage frame : frames) 
        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = img.getGraphics();
        g.drawImage(copies.get(copies.size()-1),0,0,null);
        g.drawImage(frame,0,0,null);
        copies.add(img);
    
    return copies;


public static java.util.List<BufferedImage> getResizedFrames(java.util.List<BufferedImage> frames, int width, int height, int x, int y) 
    List<BufferedImage> copies = new ArrayList<>();
    for (BufferedImage image : frames)
        copies.add(cropImage(image, width, height, width - 1 - x, height - 1 - y));
    return copies;

我需要你的帮助。

【问题讨论】:

想想getFrames() 返回的内容:它是原始大小的每个帧的未压缩内存表示。 TYPE_INT_RGB 每个像素 IIRC 需要 3 个字节,所以这是 width * height * 3 * numberOfFrames 字节至少。一个 1000 帧的中型 GIF(不太可能)很快就会变得非常大。 @JoachimSauer 好点,我编辑了我的答案以澄清这一点。 【参考方案1】:

您的程序使用大量内存的原因是您的代码在每一步(在许多小循环中)在所有帧上运行。所有帧的所有解码图像数据将同时保存在内存中,导致不必要的内存使用。虽然这听起来合乎逻辑,但肯定效率不高。

相反,创建一个循环,为一个单个帧执行所有需要的操作,然后再继续下一帧。

类似这样的:

public static void test(String path) throws IOException 
    List<BufferedImage> thumbnails = new ArrayList<>();

    readFrames(path, image -> thumbnails.add(getScaledImage(image)));


private static void readFrames(String path, Consumer<BufferedImage> forEachFrame) throws IOException 
    try (ImageInputStream inputStream = ImageIO.createImageInputStream(GifSplitter.class.getResourceAsStream(path))) 
        Iterator<ImageReader> readers = ImageIO.getImageReaders(inputStream); // ImageIO detects format, no need to hardcode GIF plugin
        if (!readers.hasNext()) 
            return;
        

        ImageReader ir = readers.next();
        ir.setInput(inputStream);

        // For GIF format (which does not contain frame count in header),
        // it's more efficient to just read each frame until IOOBE occurs,
        // instead of invoking getNumImages(true)
        int i = 0;
        while (i >= 0) 
            try 
                BufferedImage image = ir.read(i++);

                // crop, scale, etc, for a SINGLE image
                forEachFrame.accept(image);
            
            catch (IndexOutOfBoundsException endOfGif) 
                // No more frames
                break;
            
        

        ir.dispose();
    

【讨论】:

您的方法输出正常的第一帧,但其余的问题 你能详细说明“剩下的问题”吗?我的代码需要更少的堆内存,因为它一次运行一帧。只有缩略图保存在内存中(但那是来自您的原始代码,也许您也不需要它?)。另外,请确保您测量正确使用的内存。

以上是关于将 GIF 切割成帧并将其转换为像素的程序占用了太多内存的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 GD 调整上传图像的大小并将其转换为 PNG?

使用 FreeImage 库读取 gif 彩色图像并将其转换为 OpenCV Mat

Android 将 gif 转换为动画 webp

FFMPEG PHP - 将 gif 转换为 mpeg

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

在 Swift 中使用 AlamofireImage 将 GIF 图像转换为 UIImageView