Подушка - Изменение размера GIF

У меня есть gif, размер которого я хотел бы изменить с помощью pillow, чтобы его размер уменьшился. Текущий размер gif - 2 МБ.

я пытаюсь

  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

Я думал о вызове resize() на im.seek() или im.tell(), но ни один из этих методов не возвращает объект Image, и поэтому я не могу вызвать resize() на их выходе.

Знаете ли вы, как я могу использовать Pillow, чтобы уменьшить размер GIF-файла, сохранив при этом все его кадры?

[править] Частичное решение:

Следуя ответу старого медведя, я внес следующие изменения:

  • Я использую скрипт BigglesZX для извлечения всех фреймов. Полезно отметить, что это сценарий Python 2, а мой проект написан на Python 3 (я упомянул эту деталь изначально, но она была отредактирована сообществом Stack Overflow Community). Запуск 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 теперь сохранен и работает, но есть 2 основные проблемы:

  • Я не уверен, что метод изменения размера работает, поскольку out.gif по-прежнему занимает 7,5 МБ. Первоначальный размер гифки был 2 МБ.

  • Скорость гифки увеличивается, и гифка не зацикливается. Он останавливается после первого запуска.

Пример:

оригинальный gif my_gif.gif:

Оригинальный gif

Gif после обработки (out.gif) https://i.imgur.com/zDO4cE4.mp4 (I не смог добавить его в Stack Overflow). Imgur сделал его медленнее (и преобразовал в mp4). Когда я открываю gif-файл со своего компьютера, весь gif-файл длится около 1,5 секунд.


person Pauline    schedule 18.01.2017    source источник
comment
вы можете загрузить файл GIF, размер которого вы пытаетесь изменить?   -  person Jeru Luke    schedule 19.01.2017
comment
@JeruLuke Я добавил файл GIF.   -  person Pauline    schedule 23.01.2017


Ответы (3)


Используя скрипт BigglesZX, я создал новый скрипт, который изменяет размер GIF с помощью Pillow.

Исходный GIF (2,1 МБ):

Оригинальный gif

Выходной GIF после изменения размера (1,7 МБ):

Выходной gif

Я сохранил сценарий здесь. Он использует thumbnail метод подушки, а не метод resize, поскольку я обнаружил, что метод resize не работает.

Он не идеален, поэтому не стесняйтесь его улучшать. Вот несколько нерешенных проблем:

  • Хотя GIF-файл отлично отображается при размещении на imgur, возникает проблема со скоростью, когда я открываю его со своего компьютера, когда весь GIF-файл занимает всего 1,5 секунды.
  • Точно так же, хотя imgur, похоже, решает проблему со скоростью, GIF некорректно отображается, когда я пытался загрузить его в stack.imgur. Отображался только первый кадр (его можно увидеть здесь).

Полный код (следует удалить суть выше):

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
person Pauline    schedule 24.01.2017

Согласно Pillow 4.0x функция Image.resize работает только с одним изображением / кадром.

Я считаю, что для достижения желаемого вам нужно сначала извлечь каждый кадр из файла .gif, изменить размер каждого кадра по одному, а затем снова собрать их.

Чтобы сделать первый шаг, необходимо уделить внимание некоторым деталям. Например. использует ли каждый кадр gif локальную палитру или глобальную палитру применяется ко всем кадрам, и заменяет ли gif каждое изображение, используя полный или частичный кадр. BigglesZX разработал сценарий для решения этих проблем при извлечении каждого кадра из файла gif, так что используйте его .

Затем вам нужно написать сценарии для изменения размера каждого извлеченного кадра и собрать их все как новый .gif, используя PIL.Image.resize () и PIL.Image.save ().

Я заметил, что вы написали "im.seek(im.tell() + 1) # load all frames". Я считаю это неверным. Скорее он используется для увеличения между кадрами файла .gif. Я заметил, что вы использовали quality = 10 в своей функции сохранения для своего файла .gif. Я не нашел этого, как указано в документации PIL . Вы можете узнать больше об атрибуте плитки, упомянутом в скрипте BiggleZX, прочитав этот ссылка

person Sun Bear    schedule 20.01.2017
comment
Вы правы для quality, это допустимый аргумент для jpg файлов, но не для gif файлов. Я забыл удалить его, когда пытался изменить свой код с изменения размера gif на изменение размера jpg. Я отредактировал свое сообщение, чтобы ответить на другие элементы вашего ответа, так как у меня не было достаточно места для ответа в качестве комментария. - person Pauline; 23.01.2017
comment
@Pauline, если вы сравните размер отдельного извлеченного кадра с эквивалентным кадром с измененным размером, обнаружите ли вы, что размер кадра с измененным размером меньше? Если он меньше, я думаю, это означает, что функция изменения размера работает, а увеличение размера файла связано с процессом перекомпиляции. Что касается вашей проблемы с зацикливанием, в команде Image.save есть опция зацикливания, которая может решить вашу проблему. Что касается скорости, я думаю, что это контролирует параметр продолжительности. Вы можете увидеть это по ссылке, указанной в моем ответе ранее. - person Sun Bear; 23.01.2017
comment
Еще одна мысль. Если размер файла увеличивается при уменьшении размера файла, это может быть вызвано типом фильтра. См. Pillow.readthedocs.io/en/4.0.x/handbook. /concepts.html#filters. Я заметил, что вы используете фильтр Image.ANTIALIAS, но не нашел его в списке фильтров для изменения размера. Фильтр Хэмминга выглядит привлекательно. - person Sun Bear; 23.01.2017
comment
Я обнаружил, что метод resize не работает, поэтому я переключился на thumbnail, который работает. Я не знаю, почему resize не сработало. Сейчас я использую атрибут цикла, но изначально мне нужен был бесконечный цикл, а не фиксированное количество циклов. Однако я не нашел способа добиться этого, поэтому я установил количество циклов равным 1000 (это добавляет всего несколько байтов к общему размеру GIF. Я не нашел решения для скорости, которое бы не требуют, чтобы я вручную ввел скорость. В идеале я бы считал скорость из оригинального GIF, но не нашел способа добиться этого. - person Pauline; 24.01.2017
comment
Image.ANTIALIAS является устаревшим (см. это примечание к выпуску), однако его все еще можно использовать в новых версиях подушки, так как она связана с LANCZOS. Поскольку использование LANCZOS в более старой версии pillow вызовет исключение, и поскольку использование ANTIALIAS дает те же результаты, что и использование LANCZOS, я обнаружил, что лучше оставить его как ANTIALIAS, но я могу ошибаться и буду счастлив исправленный. Спасибо за вашу помощь, я нашел способ заставить его работать сейчас и опубликовал его в качестве ответа. - person Pauline; 24.01.2017
comment
@Pauline Рада, что вы достигли своей цели, и рада, что стала частью вашего путешествия. Отмечено на АНТИАЛИАСЕ. Наткнулся на PIL docs, рекомендация фильтра BOX по уменьшению масштаба для определенного масштаба. - person Sun Bear; 25.01.2017

Я использую функцию ниже, чтобы изменить размер и обрезать изображения, включая анимированные (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
person muratgozel    schedule 01.12.2018