枕头 - 调整 GIF 的大小

Posted

技术标签:

【中文标题】枕头 - 调整 GIF 的大小【英文标题】:Pillow - Resizing a GIF 【发布时间】:2017-06-02 19:12:37 【问题描述】:

我有一个gif,我想用pillow 调整它的大小,以减小它的大小。 gif 的当前大小为 2MB。

我正在尝试

    调整它的大小,使其高度/宽度更小

    降低其质量。

对于 JPEG,以下代码通常足以使大图像的大小急剧减小。

from PIL import Image

im = Image.open("my_picture.jpg")
im = im.resize((im.size[0] // 2, im.size[1] // 2), Image.ANTIALIAS)  # decreases width and height of the image
im.save("out.jpg", optimize=True, quality=85)  # decreases its quality

不过,对于 GIF,它似乎不起作用。以下代码甚至使out.gif 比初始 gif 更大:

im = Image.open("my_gif.gif")
im.seek(im.tell() + 1)  # loads all frames
im.save("out.gif", save_all=True, optimize=True, quality=10)  # should decrease its quality

print(os.stat("my_gif.gif").st_size)  # 2096558 bytes / roughly 2MB
print(os.stat("out.gif").st_size)  # 7536404 bytes / roughly 7.5MB

如果我添加以下行,则只保存 GIF 的第一帧,而不是其所有帧。

im = im.resize((im.size[0] // 2, im.size[1] // 2), Image.ANTIALIAS)  # should decrease its size

我一直在考虑在 im.seek()im.tell() 上调用 resize(),但这些方法都没有返回 Image 对象,因此我不能在它们的输出上调用 resize()

您知道如何使用 Pillow 来减小 GIF 的大小,同时保留所有帧吗?

[编辑] 部分解决方案:

按照Old Bear's response,我做了以下改动:

我正在使用BigglesZX's script 提取所有帧。值得注意的是,这是一个 Python 2 脚本,而我的项目是用 Python 3 编写的(我最初确实提到了这个细节,但它被 Stack Overflow 社区编辑掉了)。运行 2to3 -w gifextract.py 使该脚本与 Python 3 兼容。

我一直在单独调整每一帧:frame.resize((frame.size[0] // 2, frame.size[1] // 2), Image.ANTIALIAS)

我一直将所有帧保存在一起:img.save("out.gif", save_all=True, optimize=True)

新的 gif 现在已保存并可以使用,但有两个主要问题:

我不确定调整大小的方法是否有效,因为out.gif 仍然是 7.5MB。初始 gif 为 2MB。

gif 速度提高,gif 不循环。它在第一次运行后停止。

例子:

原图my_gif.gif:

处理后的 Gif (out.gif) https://i.imgur.com/zDO4cE4.mp4(我无法将其添加到 Stack Overflow 中)。 Imgur 使它变慢(并将其转换为 mp4)。当我从我的电脑打开 gif 文件时,整个 gif 持续大约 1.5 秒。

【问题讨论】:

你能上传你想要调整大小的 GIF 文件吗? @JeruLuke 我已经添加了 GIF 文件。 【参考方案1】:

使用BigglesZX's script,我创建了一个新脚本,它可以使用 Pillow 调整 GIF 的大小。

原始 GIF (2.1 MB):

调整大小后输出 GIF (1.7 MB):

我已保存脚本here。它使用 Pillow 的 thumbnail 方法而不是 resize 方法,因为我发现 resize 方法不起作用。

它并不完美,因此请随意分叉和改进它。以下是一些未解决的问题:

虽然 GIF 在 imgur 托管时显示正常,但当我从计算机打开它时出现速度问题,整个 GIF 只需要 1.5 秒。 同样,虽然 imgur 似乎弥补了速度问题,但当我尝试将 GIF 上传到 stack.imgur 时,它无法正确显示。只显示了第一帧(你可以看到它here)。

完整代码(以上要点是否应删除):

def resize_gif(path, save_as=None, resize_to=None):
    """
    Resizes the GIF to a given length:

    Args:
        path: the path to the GIF file
        save_as (optional): Path of the resized gif. If not set, the original gif will be overwritten.
        resize_to (optional): new size of the gif. Format: (int, int). If not set, the original GIF will be resized to
                              half of its size.
    """
    all_frames = extract_and_resize_frames(path, resize_to)

    if not save_as:
        save_as = path

    if len(all_frames) == 1:
        print("Warning: only 1 frame found")
        all_frames[0].save(save_as, optimize=True)
    else:
        all_frames[0].save(save_as, optimize=True, save_all=True, append_images=all_frames[1:], loop=1000)


def analyseImage(path):
    """
    Pre-process pass over the image to determine the mode (full or additive).
    Necessary as assessing single frames isn't reliable. Need to know the mode
    before processing all frames.
    """
    im = Image.open(path)
    results = 
        'size': im.size,
        'mode': 'full',
    
    try:
        while True:
            if im.tile:
                tile = im.tile[0]
                update_region = tile[1]
                update_region_dimensions = update_region[2:]
                if update_region_dimensions != im.size:
                    results['mode'] = 'partial'
                    break
            im.seek(im.tell() + 1)
    except EOFError:
        pass
    return results


def extract_and_resize_frames(path, resize_to=None):
    """
    Iterate the GIF, extracting each frame and resizing them

    Returns:
        An array of all frames
    """
    mode = analyseImage(path)['mode']

    im = Image.open(path)

    if not resize_to:
        resize_to = (im.size[0] // 2, im.size[1] // 2)

    i = 0
    p = im.getpalette()
    last_frame = im.convert('RGBA')

    all_frames = []

    try:
        while True:
            # print("saving %s (%s) frame %d, %s %s" % (path, mode, i, im.size, im.tile))

            '''
            If the GIF uses local colour tables, each frame will have its own palette.
            If not, we need to apply the global palette to the new frame.
            '''
            if not im.getpalette():
                im.putpalette(p)

            new_frame = Image.new('RGBA', im.size)

            '''
            Is this file a "partial"-mode GIF where frames update a region of a different size to the entire image?
            If so, we need to construct the new frame by pasting it on top of the preceding frames.
            '''
            if mode == 'partial':
                new_frame.paste(last_frame)

            new_frame.paste(im, (0, 0), im.convert('RGBA'))

            new_frame.thumbnail(resize_to, Image.ANTIALIAS)
            all_frames.append(new_frame)

            i += 1
            last_frame = new_frame
            im.seek(im.tell() + 1)
    except EOFError:
        pass

    return all_frames

【讨论】:

对于putpalette,我添加了try ... / except ValueError pass 以避免非法图像模式。从 Pillow 6 开始就有some changes。不确定这是否是最好的方法。【参考方案2】:

根据 Pillow 4.0x,Image.resize 函数仅适用于单个图像/帧。

为了达到你想要的效果,我相信你必须首先从 .gif 文件中提取每一帧,一次调整每一帧的大小,然后重新组合起来。

要做第一步,似乎需要注意一些细节。例如。每个 gif 帧是使用局部调色板还是全局调色板应用于所有帧,以及 gif 是使用完整帧还是部分帧替换每个图像。 BigglesZX 开发了一个脚本来解决这些问题,同时从 gif 文件中提取每一帧,以便利用它。

接下来,您必须编写脚本来调整每个提取帧的大小,并使用 PIL.Image.resize() 和 PIL.Image.save() 将它们全部组合成一个新的 .gif。

我注意到你写了“im.seek(im.tell() + 1) # load all frames”。我认为这是不正确的。相反,它用于在 .gif 文件的帧之间递增。我注意到您在 .gif 文件的保存功能中使用了 quality=10 。我没有找到PIL documentation 中提供的这个。您可以通过阅读此link了解更多关于 BiggleZX 脚本中提到的 tile 属性的信息

【讨论】:

您对quality 是正确的,它是jpg 文件的有效参数,但不适用于gif 文件。当我尝试将代码从调整gif 的大小更改为调整jpg 的大小时,我忘记了删除它。我已经编辑了我的帖子以回复您答案的其他元素,因为我没有足够的空间来回复评论。 @Pauline 如果您将提取的单个帧的大小与等效的调整大小的帧进行比较,您是否发现调整后的帧的大小更小?如果它更小,我认为这意味着调整大小功能起作用并且文件大小的增加来自重新编译过程。对于您的循环问题,Image.save 命令中提到了一个“循环”选项,可以解决您的问题。至于速度,我认为“持续时间”选项可以控制它。您可以通过我之前回答中给出的链接查看它。 另一个想法。如果减小文件大小会增加大小,这可能是由您的过滤器类型引起的。见pillow.readthedocs.io/en/4.0.x/handbook/concepts.html#filters。我注意到您正在使用 Image.ANTIALIAS 过滤器,但没有发现它是列出的用于调整大小的过滤器之一。汉明滤波器看起来很有吸引力。 我发现resize 方法不起作用,因此我切换到thumbnail 起作用。我不知道为什么resize 不起作用。我现在使用循环属性,但最初我想要一个无限循环而不是固定数量的循环。不过,我没有找到实现这一点的方法,所以我将循环数设置为 1000(它只在GIF 的总大小上增加了几个字节。我还没有找到速度的解决方案'不需要我手动输入速度。理想情况下,我会从原始 GIF 中读取速度,但还没有找到实现此目的的方法。 @Pauline 很高兴您实现了自己的目标,也很高兴成为您旅程的一部分。在 ANTIALIAS 上注明。遇到PIL docs 推荐的 BOX 过滤器,用于按比例缩小。【参考方案3】:

我正在使用下面的函数来调整和裁剪图像,包括动画图像(GIF、WEBP)简单地说,我们需要迭代 gif 或 webp 中的每一帧。

from math import floor, fabs
from PIL import Image, ImageSequence

def transform_image(original_img, crop_w, crop_h):
  """
  Resizes and crops the image to the specified crop_w and crop_h if necessary.
  Works with multi frame gif and webp images also.

  args:
  original_img is the image instance created by pillow ( Image.open(filepath) )
  crop_w is the width in pixels for the image that will be resized and cropped
  crop_h is the height in pixels for the image that will be resized and cropped

  returns:
  Instance of an Image or list of frames which they are instances of an Image individually
  """
  img_w, img_h = (original_img.size[0], original_img.size[1])
  n_frames = getattr(original_img, 'n_frames', 1)

  def transform_frame(frame):
    """
    Resizes and crops the individual frame in the image.
    """
    # resize the image to the specified height if crop_w is null in the recipe
    if crop_w is None:
      if crop_h == img_h:
        return frame
      new_w = floor(img_w * crop_h / img_h)
      new_h = crop_h
      return frame.resize((new_w, new_h))

    # return the original image if crop size is equal to img size
    if crop_w == img_w and crop_h == img_h:
      return frame

    # first resize to get most visible area of the image and then crop
    w_diff = fabs(crop_w - img_w)
    h_diff = fabs(crop_h - img_h)
    enlarge_image = True if crop_w > img_w or crop_h > img_h else False
    shrink_image = True if crop_w < img_w or crop_h < img_h else False

    if enlarge_image is True:
      new_w = floor(crop_h * img_w / img_h) if h_diff > w_diff else crop_w
      new_h = floor(crop_w * img_h / img_w) if h_diff < w_diff else crop_h

    if shrink_image is True:
      new_w = crop_w if h_diff > w_diff else floor(crop_h * img_w / img_h)
      new_h = crop_h if h_diff < w_diff else floor(crop_w * img_h / img_w)

    left = (new_w - crop_w) // 2
    right = left + crop_w
    top = (new_h - crop_h) // 2
    bottom = top + crop_h

    return frame.resize((new_w, new_h)).crop((left, top, right, bottom))

  # single frame image
  if n_frames == 1:
    return transform_frame(original_img)
  # in the case of a multiframe image
  else:
    frames = []
    for frame in ImageSequence.Iterator(original_img):
      frames.append( transform_frame(frame) )
    return frames

【讨论】:

【参考方案4】:

我尝试使用所选答案中给出的脚本,但正如 Pauline 评论的那样,它存在一些问题,例如速度问题。

问题是保存新 gif 时没有给出速度。为了解决这个问题,您必须从原始 gif 中获取速度,并在保存时将其传递给新的。

这是我的脚本:

from PIL import Image


def scale_gif(path, scale, new_path=None):
    gif = Image.open(path)
    if not new_path:
        new_path = path
    old_gif_information = 
        'loop': bool(gif.info.get('loop', 1)),
        'duration': gif.info.get('duration', 40),
        'background': gif.info.get('background', 223),
        'extension': gif.info.get('extension', (b'NETSCAPE2.0')),
        'transparency': gif.info.get('transparency', 223)
    
    new_frames = get_new_frames(gif, scale)
    save_new_gif(new_frames, old_gif_information, new_path)

def get_new_frames(gif, scale):
    new_frames = []
    actual_frames = gif.n_frames
    for frame in range(actual_frames):
        gif.seek(frame)
        new_frame = Image.new('RGBA', gif.size)
        new_frame.paste(gif)
        new_frame.thumbnail(scale, Image.ANTIALIAS)
        new_frames.append(new_frame)
    return new_frames

def save_new_gif(new_frames, old_gif_information, new_path):
    new_frames[0].save(new_path,
                       save_all = True,
                       append_images = new_frames[1:],
                       duration = old_gif_information['duration'],
                       loop = old_gif_information['loop'],
                       background = old_gif_information['background'],
                       extension = old_gif_information['extension'] ,
                       transparency = old_gif_information['transparency'])

我还注意到,您必须使用 new_frames[0] 保存新的 gif,而不是创建新的 Image Pillow 对象以避免在 gif 中添加黑框。

如果您想查看在此脚本上使用 pytest 进行的测试,您可以查看my GitHub's repo。

【讨论】:

以上是关于枕头 - 调整 GIF 的大小的主要内容,如果未能解决你的问题,请参考以下文章

python python django枕头缩略图调整大小

使用枕头和python3.8调整img大小时出错

当文件大小大于 2mb 时,Imagick 无法调整 GIF 图像的大小

调整动画 GIF 大小的 Python 问题

同时使用FFMPEG调整GIF的大小时的分带/结尾

调整 GIF 动画、pil/imagemagick、python 的大小