简体   繁体   English

枕头 - 调整 GIF 大小

[英]Pillow - Resizing a GIF

I have a gif that I would like to resize with pillow so that its size decreases.我有一个gif ,我想用pillow调整它的大小,以减小它的大小。 The current size of the gif is 2MB. gif的当前大小为 2MB。

I am trying to我在尝试着

  1. resize it so its height / width is smaller调整它的大小,使其高度/宽度更小

  2. decrease its quality.降低其质量。

With JPEG, the following piece of code is usually enough so that large image drastically decrease in size.对于 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

With a GIF, though, it does not seem to work.但是,对于 GIF,它似乎不起作用。 The following piece of code even makes the out.gif bigger than the initial 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

If I add the following line, then only the first frame of the GIF is saved, instead of all of its frame.如果我添加以下行,则仅保存 GIF 的第一帧,而不是其所有帧。

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

I've been thinking about calling resize() on im.seek() or im.tell() but neither of these methods return an Image object, and therefore I cannot call resize() on their output.我一直在考虑在im.seek()im.tell() ) 上调用resize() ,但这些方法都没有返回 Image object,因此我不能在他们的 output 上调用resize()

Would you know how I can use Pillow to decrease the size of my GIF while keeping all of its frames?您知道如何使用 Pillow 来减小 GIF 的大小,同时保留所有帧吗?

[edit] Partial solution: [编辑] 部分解决方案:

Following Old Bear's response , I have done the following changes:根据老熊的回应,我做了以下更改:

  • I am using BigglesZX's script to extract all frames.我正在使用BigglesZX 的脚本来提取所有帧。 It is useful to note that this is a Python 2 script, and my project is written in Python 3 (I did mention that detail initially, but it was edited out by the Stack Overflow Community).值得注意的是,这是一个 Python 2 脚本,我的项目是用 Python 3 编写的(我最初确实提到了这个细节,但它被 Stack Overflow 社区编辑掉了)。 Running 2to3 -w gifextract.py makes that script compatible with Python 3.运行2to3 -w gifextract.py使该脚本与 Python 3 兼容。

  • I have been resicing each frame individually: frame.resize((frame.size[0] // 2, frame.size[1] // 2), Image.ANTIALIAS)我一直在单独调整每一帧: frame.resize((frame.size[0] // 2, frame.size[1] // 2), Image.ANTIALIAS)

  • I've been saving all the frames together: img.save("out.gif", save_all=True, optimize=True) .我一直将所有帧保存在一起: img.save("out.gif", save_all=True, optimize=True)

The new gif is now saved and works, but there is 2 main problems:新的 gif 现在已保存并可以使用,但有两个主要问题:

  • I am not sure that the resize method works, as out.gif is still 7.5MB.我不确定调整大小方法是否有效,因为out.gif仍然是 7.5MB。 The initial gif was 2MB.最初的 gif 是 2MB。

  • The gif speed is increased and the gif does not loop. gif速度增加,gif不循环。 It stops after its first run.它在第一次运行后停止。

Example:例子:

original gif my_gif.gif :原始 gif my_gif.gif

原图

Gif after processing ( out.gif ) https://i.imgur.com/zDO4cE4.mp4 (I could not add it to Stack Overflow ).处理后的 Gif ( out.gif ) https://i.imgur.com/zDO4cE4.mp4 (我无法将其添加到 Stack Overflow)。 Imgur made it slower (and converted it to mp4). Imgur 使它变慢(并将其转换为 mp4)。 When I open the gif file from my computer, the entire gif lasts about 1.5 seconds.当我从我的电脑打开 gif 文件时,整个 gif 持续大约 1.5 秒。

Using BigglesZX's script , I have created a new script which resizes a GIF using Pillow.使用BigglesZX 的脚本,我创建了一个使用 Pillow 调整 GIF 大小的新脚本。

Original GIF (2.1 MB):原始 GIF (2.1 MB):

原始 gif

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

输出gif

I have saved the script here .我已经在这里保存了脚本。 It is using the thumbnail method of Pillow rather than the resize method as I found the resize method did not work.它使用 Pillow 的thumbnail方法而不是resize方法,因为我发现resize方法不起作用。

The is not perfect so feel free to fork and improve it.它并不完美,所以请随意分叉并改进它。 Here are a few unresolved issues:这里有几个未解决的问题:

  • While the GIF displays just fine when hosted by imgur, there is a speed issue when I open it from my computer where the entire GIF only take 1.5 seconds.虽然 GIF 在由 imgur 托管时显示得很好,但当我从我的计算机打开它时存在速度问题,整个 GIF 只需要 1.5 秒。
  • Likewise, while imgur seems to make up for the speed problem, the GIF wouldn't display correctly when I tried to upload it to stack.imgur .同样,虽然 imgur 似乎弥补了速度问题,但当我尝试将其上传到stack.imgur时,GIF 无法正确显示。 Only the first frame was displayed (you can see it here ).只显示了第一帧(您可以在此处查看)。

Full code (should the above gist be deleted):完整代码(是否应删除上述要点):

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

According to Pillow 4.0x, the Image.resize function only works on a single image/frame.根据 Pillow 4.0x,Image.resize 函数仅适用于单个图像/帧。

To achieve what you want, I believe you have to first extract every frame from the .gif file, resize each frame one at a time and then reassemble them up again.为了实现你想要的,我相信你必须首先从 .gif 文件中提取每一帧,一次调整每一帧的大小,然后重新组合它们。

To do the first step, there appears to be some detail that needs to be attended to.要进行第一步,似乎需要注意一些细节。 Eg whether each gif frame uses a local palette or a global palette is applied over all frames, and whether gif replace each image using a full or partial frame.例如,每个 gif 帧是使用本地调色板还是全局调色板应用于所有帧,以及 gif 是使用完整帧还是部分帧替换每个图像。 BigglesZX has developed a script to address these issues while extracting every frame from a gif file so leverage on that. BigglesZX开发了一个脚本来解决这些问题,同时从 gif 文件中提取每一帧,以便利用它。

Next, you have to write the scripts to resize each of the extracted frame and assemble them all as a new .gif using the PIL.Image.resize() and PIL.Image.save().接下来,您必须编写脚本来调整每个提取帧的大小,并使用 PIL.Image.resize() 和 PIL.Image.save() 将它们全部组合为新的 .gif。

I noticed you wrote " im.seek(im.tell() + 1) # load all frames ".我注意到你写了“ im.seek(im.tell() + 1) # load all frames ”。 I think this is incorrect.我认为这是不正确的。 Rather it is use to increment between frames of a .gif file.相反,它用于在 .gif 文件的帧之间递增。 I noticed you used quality=10 in your save function for your .gif file.我注意到您在 .gif 文件的保存功能中使用了 quality=10。 I did not find this as provided in the PIL documentation .我没有找到PIL 文档中提供的内容 You can learn more about the tile attribute mentioned in BiggleZX's script by reading this link您可以通过阅读此链接了解有关 BiggleZX 脚本中提到的 tile 属性的更多信息

I am using the function below to resize and crop images including animated ones (GIF, WEBP) Simply, we need to iterate each frame in the gif or webp.我正在使用下面的函数来调整大小和裁剪图像,包括动画图像(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

I tried to use the script given in the chosen answer but as Pauline commented, it had some problems such as speed issue.我尝试使用所选答案中给出的脚本,但正如 Pauline 评论的那样,它存在一些问题,例如速度问题。

The problem was that the speed wasn't given when saving the new gif.问题是保存新gif时没有给出速度。 To solve that you must take the speed from the original gif and pass it to the new one when saving it.要解决这个问题,您必须从原始 gif 中获取速度,并在保存时将其传递给新的 gif。

Here is my script:这是我的脚本:

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'])

Also I noticed that you must save the new gif using new_frames[0] instead of creating a new Image Pillow's object to avoid adding a black frame to the gif.我还注意到您必须使用new_frames[0]保存新的 gif,而不是创建一个新的 Image Pillow 的对象,以避免在 gif 中添加黑框。

If you want to see a test using pytest on this script you can check my GitHub's repo .如果您想在此脚本上查看使用 pytest 的测试,您可以查看我的 GitHub 的 repo

I wrote a simple code that resize Gif with the same speed and background transparency.我编写了一个简单的代码,以相同的速度和背景透明度调整 Gif 的大小。 I think it could be helpful.我认为这可能会有所帮助。

"""
# Resize an animated GIF
Inspired from https://gist.github.com/skywodd/8b68bd9c7af048afcedcea3fb1807966
Useful links:
    * https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#saving
    * https://stackoverflow.com/a/69850807
Example:
    ```
    python resize_gif.py input.gif output.gif 400,300
    ```
"""

import sys

from PIL import Image
from PIL import ImageSequence


def resize_gif(input_path, output_path, max_size):
    input_image = Image.open(input_path)
    frames = list(_thumbnail_frames(input_image))
    output_image = frames[0]
    output_image.save(
        output_path,
        save_all=True,
        append_images=frames[1:],
        disposal=input_image.disposal_method,
        **input_image.info,
    )


def _thumbnail_frames(image):
    for frame in ImageSequence.Iterator(image):
        new_frame = frame.copy()
        new_frame.thumbnail(max_size, Image.Resampling.LANCZOS)
        yield new_frame


if __name__ == "__main__":
    max_size = [int(px) for px in sys.argv[3].split(",")]  # "150,100" -> (150, 100)
    resize_gif(sys.argv[1], sys.argv[2], max_size)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM