🍃

GIFアニメーションをPythonで処理する方法

2024/01/30に公開

GIFアニメーションファイルには、複数の画像とそれぞれの画像の表示時間などのメタデータが含まれる。GIFアニメーションを編集するためには、そのファイルに含まれるすべての画像を適切に処理する必要がある。PILでの処理方法を以下に紹介する。

対象ファイルがGIFアニメーションか判別する

def is_animated_gif(path: str) -> bool:
    try:
        with Image.open(path) as img:
            if img.format != 'GIF':
                return False
            return img.is_animated
    except UnidentifiedImageError:
        return False

リサイズ

def resize_gif(input_path: str, output_path: str, width: int, height: int) -> None:
    with Image.open(input_path) as image:
        frames: list[Image.Image] = []
        disposal_methods: list[int] = []
        try:
            while True:
                resized_frame = image.resize((width, height))
                disposal_methods.append(image.disposal_method)  # type: ignore
                frames.append(resized_frame)
                image.seek(image.tell() + 1)
        except EOFError:
            pass
        frames[0].save(
            output_path,
            save_all=True,
            append_images=frames[1:],
            disposal=disposal_methods,
        )

ポイントはdisposal_methodを配列でsaveメソッドに渡すこと。これを省くと残像が残ったりして正しくGIFファイルがリサイズされないことがある。disposal_methodとは、アニメーションGIF内の各フレームが表示された後にどのように扱われるかを定義するもの。

矩形切り取り

def crop_gif(
    input_path: str, output_path: str, left: int, upper: int, right: int, lower: int
) -> None:
    box = (left, upper, right, lower)    
    with Image.open(input_path) as image:
        frames: list[Image.Image] = []
        disposal_methods: list[int] = []
        try:
            while True:
                cropped_frame = image.crop(box)
                frames.append(cropped_frame)
                disposal_methods.append(image.disposal_method)  # type: ignore
                image.seek(image.tell() + 1)
        except EOFError:
            pass
        frames[0].save(
            output_path,
            save_all=True,
            append_images=frames[1:],
            disposal=disposal_methods,
        )

上下反転、左右反転

def flip_gif(input_path: str, output_path: str, horizontal: bool) -> None:
    with Image.open(input_path) as image:
        frames: list[Image.Image] = []
        disposal_methods: list[int] = []
        try:
            while True:
                flipped = image.transpose(
                    Image.FLIP_LEFT_RIGHT if horizontal else Image.FLIP_TOP_BOTTOM
                )
                frames.append(flipped)
                disposal_methods.append(image.disposal_method)  # type: ignore
                image.seek(image.tell() + 1)
        except EOFError:
            pass
        frames[0].save(
            output_path,
            save_all=True,
            append_images=frames[1:],
            disposal=disposal_methods,
        )

Discussion