Сообщить об ошибке.

Обработка GIF изображений в Pillow в Python

Базовая поддержка анимированных изображений Pillow

Библиотека Pillow содержит некоторую базовую поддержку последовательностей изображений (также называемых форматами анимации). Поддерживаемые форматы последовательностей включают FLI/FLC, GIF и несколько экспериментальных форматов. Файлы TIFF также могут содержать более одного кадра.

Содержание:


Общие приемы работы с анимированными изображениями.

Когда открывается файл, содержащий последовательность изображений, то Pillow автоматически загружает первый кадр в последовательности. Для перемещения между разными кадрами можно использовать методы Image.seek() и Image.tell() :

from PIL import Image

with Image.open('animation.gif') as img:
    # перейти ко второму кадру
    img.seek(1)  

    try:
        while 1:
            img.seek(img.tell() + 1)
            # здесь делаем что-нибудь 
            # с текущем кадром `img`
    except EOFError:
        pass  # конец последовательности

Из примера видно, что когда последовательность кадров завершится, получаем исключение EOFError.

Следующий класс позволяет использовать оператор for/in для циклического перебора последовательности:

from PIL import ImageSequence

with Image.open('animation.gif') as img:
    for frame in ImageSequence.Iterator(img):
        # здесь делаем что-нибудь 
        # с текущем кадром `frame`
        # `frame` - объект изображения

Pillow читает версии формата файла GIF: GIF87a и GIF89a, а записывает файлы в формате GIF87a по умолчанию, если не используются функции GIF89a. Файлы записываются в кодировке LZW.

Файлы GIF изначально читаются как изображения в оттенках серого (L) или в режиме палитры (P). Поиск более поздних кадров в изображении P изменит изображение на RGB (или RGBA, если первый кадр был прозрачным).

Изображения в режиме P заменяются на RGB, потому что каждый кадр GIF может содержать свою собственную индивидуальную палитру до 256 цветов. Когда новый кадр помещается на предыдущий кадр, эти цвета могут объединяться, и могут превысить ограничение режима P в 256 цветов, по этому изображение преобразуется в RGB.

Если необходимо, чтобы первый кадр изображения в режиме P также был RGB, то нужно применить настройку:

from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS

Чтобы восстановить поведение по умолчанию, когда изображения в режиме P преобразуются в RGB или RGBA только после первого кадра:

from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_FIRST

Кадры GIF не всегда содержат отдельные палитры. Если есть только глобальная палитра, то все цвета могут поместиться в режиме P. Если нужно, чтобы в этом случае кадры сохранялись как P, также доступна настройка:

from PIL import GifImagePlugin
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY

Дополнительные свойства GIF формата.

Функция PIL.Image.open() может иметь следующие свойства GIF формата, содержащихся в словаре img.info:

  • background: Цвет фона по умолчанию (цветовой индекс палитры).
  • transparency: Индекс цвета прозрачности. Этот ключ опускается, если изображение непрозрачно.
  • version: Версия (либо GIF87a, либо GIF89a).
  • duration: Может отсутствовать. Время отображения текущего кадра GIF-файла в миллисекундах.
  • loop: Может отсутствовать. Сколько раз GIF должен зацикливаться. 0 означает, что цикл будет бесконечным.
  • comment: Может отсутствовать. Комментарий к изображению.
  • extension: Может отсутствовать. Содержит информацию о конкретном приложении.

Дополнительный аргументы при сохранении GIF-формата.

При вызове метода Image.save() для записи GIF-файла доступны следующие дополнительные аргумента:

img.save(out, save_all=True, append_images=[im1, im2, ...])
  • save_all: Если True, то все кадры изображения будут сохранены. Если отсутствует или False, то будет сохранен только первый кадр изображения.
  • append_images: Список изображений для добавления в качестве дополнительных кадров. Каждое из изображений в списке может быть одиночным или многокадровым. В настоящее время аргумент поддерживается для форматов GIF, PDF, PNG, TIFF и WebP.

    Он также поддерживается для ICO и ICNS. Если передаются изображения соответствующих размеров, то они будут использоваться вместо уменьшения основного изображения.

  • include_color_table: добавлять или не добавлять локальную таблицу цветов.

  • interlace: Является ли изображение чересстрочным. По умолчанию это так, если размер изображения меньше 16 пикселей по ширине или высоте.

  • disposal: Указывает способ обработки графического изображения после его отображения.

    • 0 - Декодеру не требуется предпринимать никаких действий.
    • 1 - Графику оставить на месте.
    • 2 - Восстановите цвет фона. Площадь, используемая изображением должна быть восстановлено до фонового цвета.
    • 3 - Восстановить предыдущее содержимое.

    Способ обработки 2 использует предыдущий кадр в качестве фона и изменяет только непрозрачные пиксели в текущем кадре. Метод 1 скопирует все изображение поверх другого, оставив прозрачность нетронутой.

    Как использовать? Можно передать одно целое число (единый способ обработки для всех кадров) или последовательность, которая будет означать способ обработки для каждого отдельного кадра изображения.

  • palette: Использовать указанную палитру для сохраненного изображения. Палитра должна быть объектом bytes или bytearray, содержащим элементы палитры в формате RGBRGB…. Она должна быть не более 768 байт. В качестве альтернативы палитру можно передать как объект PIL.ImagePalette.ImagePalette.

  • optimize: Если True, то Pillow попытайтесь сжать палитру, удалив неиспользуемые цвета. Это полезно только в том случае, если палитру можно сжать до следующей меньшей степени из 2 элементов.

    Обратите внимание, что если изображение, которое сохраняется, взято из существующего GIF, то оно может иметь перечисленные ниже свойства в своем словаре. Большинство методов объекта Image игнорируют словарь при возврате новых изображений. Если эта свойства понадобится позже (при записи измененного GIF), то необходимо сохранить ссылку на словарь Image.info, возвращаемый функцией PIL.Image.open().

  • transparency: Индекс цвета прозрачности.

  • duration: Может отсутствовать. Время отображения текущего кадра GIF-файла в миллисекундах.

  • loop: Может отсутствовать. Сколько раз GIF должен зацикливаться. 0 означает, что цикл будет бесконечным.

  • comment: Может отсутствовать. Комментарий к изображению.

Пример создания GIF-файла средствами Pillow.

from PIL import Image, ImageDraw

# параметры анимации
frame = 10
width = 200
center = width // 2
max_radius = int(center * 1.5)
fill = '#fbceb1'
color = '#b3b1fb'

frames = []
for i in range(0, max_radius, frame):
    # создаем кадры/фреймы для GIF-изображения   
    img = Image.new('P', (width, width), fill)
    draw = ImageDraw.Draw(img)
    draw.ellipse((center - i, center - i,
                  center + i, center + i), fill=color)
    frames.append(img)

# сохраняем GIF-изображение
frames[0].save('circle.gif', save_all=True, loop=0,
               append_images=frames[1:], optimize=False, duration=3)

Пример обработки GIF-файла средствами Pillow.

from PIL import (Image, ImageOps,
                 ImageSequence)

# список для обработанных фреймов
frames = []
# откроем файл который создали ранее 
with Image.open('circle.gif') as img:
    for frame in ImageSequence.Iterator(img):
        # обрабатываем каждый кадр
        # например инвертируем цвета
        frame = ImageOps.invert(frame.convert('RGB'))
        # и уменьшим в 2 раза
        size = (frame.size[0]//2, frame.size[1]//2)
        frame = frame.resize(size)
        # добавляем обраборанный фрейм в список
        frames.append(frame)
        
# сохраняем обработанное GIF-изображение
frames[0].save('circle-proctss.gif', save_all=True, loop=0,
               append_images=frames[1:], optimize=False, duration=3)

Чтение "локальных изображений" GIF-файла.

with Image.open(...) as img:

if img.tile[0][0] == 'gif':
    # читаем только первое "локальное изображение" 
    # из этого GIF-файла
    box = img.tile[0][1]
    img = img.crop(box)