调整GIF大小的枕头

2024-04-25 17:44:20 发布

您现在位置:Python中文网/ 问答频道 /正文

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

我正在努力

  1. 调整其大小,使其高度/宽度更小

  2. 降低其质量

对于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(),但这两个方法都不返回图像对象,因此我无法对其输出调用resize()

你知道我如何使用枕头来减少GIF的大小,同时保留所有的框架吗

[编辑]部分解决方案:

Old Bear's response之后,我做了以下更改:

  • 我正在使用BigglesZX's script提取所有帧。值得注意的是,这是一个Python2脚本,我的项目是用Python3编写的(我最初确实提到了这个细节,但它被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现在已保存并工作,但存在两个主要问题:

  • 我不确定resize方法是否有效,因为out.gif仍然是7.5MB。最初的gif是2MB

  • gif速度增加,gif不会循环。它在第一次运行后停止

例如:

原始gifmy_gif.gif

Original gif

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


Tags: imagetruesizemysavealloutgif
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

使用BigglesZX's script,我创建了一个新脚本,它使用枕头调整GIF的大小

原始GIF(2.1MB):

Original gif

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

Output gif

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

这是不完美的,所以请随意叉和改善它。以下是一些尚未解决的问题:

  • 当由imgur托管时,GIF显示正常,但当我从计算机上打开它时,整个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

根据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属性的更多信息

相关问题 更多 >