LoginSignup
5
6

More than 3 years have passed since last update.

Python GIFファイル読み書き・編集をさっくりと(PIL/Pillow)

Last updated at Posted at 2020-12-05

対象の人

Python で GIF ファイル作成したい。作成後も読み込んで少し編集したりしたい。

内容:

  • GIF ファイル保存
  • GIF ファイル読み込み
  • 読み込んだ GIFデータ(GifImageFile)の取り扱い
  • 読み込んでクロップして保存してみる

GIF ファイル保存

img1.save('ファイル名.gif', save_all=True, append_images=[img2, img3, ...])
1. image.save(fp, format=None, **params)

関数は通常の画像保存と同じ、 image.save() を使用します。

2. 保存ファイル名 ***.gif

save() は、保存ファイル名の拡張子からファイル形式を判断してくれます。(format で明示的に指定することも可能。)つまり保存の際にファイル名を ***.gif とすればオーケーです。

image.save('sample.png') # 拡張子から png ファイルと認識
image.save('sample.gif') # 拡張子から gif ファイルと認識
3. 引数 save_all, append_images

1つの GIF 動画のために複数の画像を保存するので追加で引数を指定してあげます。

引数 必須 説明
save_all True に。全てのフレームを保存するか否か。
append_images 2フレーム目以降のフレームをリストで渡す。
duration GIF 再生時の各フレームインターバル。ミリ秒。
loop GIF 再生時のループ回数。0 で無限ループ。

*引数は他にもあります(公式:GIF ファイル保存オプション


最低限 save_allappend_images が必要で、最初以外のフレームを append_images で付け加えながら、最初のフレームを通常の画像のように保存するイメージです。

サンプルコード:
from PIL import Image

sample_image = Image.open('sample.jpg')

# transpose: 画像を反転や回転する関数
# https://pillow.readthedocs.io/en/3.1.x/reference/Image.html#PIL.Image.Image.transpose
image2 = sample_image.transpose(Image.ROTATE_90)
image3 = sample_image.transpose(Image.ROTATE_180)
image4 = sample_image.transpose(Image.ROTATE_270)

sample_image.save(
    'sample_rotation.gif', 
    save_all=True, 
    append_images=[image2, image3, image4],
    duration=500,  # 各フレーム 0.5sec
    loop=0,  # 無限ループ
)

サンプルコード保存結果

kraken_rotation.gif

GIF ファイル読み込み

通常の画像と同様に Image.open() で読み込むことができ、これだけできちんとGIFファイル(PIL.GifImagePlugin.GifImageFileクラス)として読み込んでくれています。

from PIL import Image
image = Image.open('sample.gif')
print(type(image))
<class 'PIL.GifImagePlugin.GifImageFile'>

ただ、 こちらのプラグインは関数が十分に揃っていないようで、 クロップしたり通常の関数を使用すると、クラスが自動で通常の画像データに変更され、最初フレームのデータだけとなってしまいます

GIFファイルとして読み込んでいることを意識していないと意外と躓きます。

from PIL import Image
image = Image.open('sample.gif')
print(type(image))

# 通常の関数を適用
image2 = image.copy()
print(type(image2))
image3 = image.crop((0, 0, 100, 100))
print(type(image3))
<class 'PIL.GifImagePlugin.GifImageFile'>
<class 'PIL.Image.Image'>  # 普通の Image オブジェクト(他フレームのデータが消える) 
<class 'PIL.Image.Image'>

*クラスの階層構造は図のよう。PIL.GifImagePlugin.GifImageFile でも通常の関数の適用自体は可能です。

スクリーンショット 2020-02-09 14.12.55.png

読み込んだ GIFデータ(GifImageFile)の取り扱い

GifImageFileは、下図のように複数画像を保持した上でとあるフレームをポインタ的に参照するデータ形式を取っています。

スクリーンショット 2020-02-09 14.12.36.png

そのため基本的にGIFデータを編集したい場合、

  1. 参照フレームの位置を変更(image.seek()
  2. 画像データと同様に関数を適用

のようにして通常画像データとして扱えます。

GifImageFileクラス特有の関数・アトリビュート
名前 説明
seek(index) 参照フレームを index の位置に変更する。
tell() 自分が今参照しているフレームの位置を返す。
n_frames 保持しているGIF ファイルのフレーム数

*他にもあります(公式

チュートリアル 読み込んでクロップして保存してみる

上の内容から、以下の手順となる。

  1. open で読み込んで
  2. seek で順番に参照して処理をおこない、
  3. save で保存
from PIL import Image

image = Image.open('sample.gif')

cropped_images = []
crop_box = (400, 0, 700, 300)
for index in range(image.n_frames):
    image.seek(index) # ここで image が参照しているフレームが変わっている
    cropped = image.crop(crop_box)
    cropped_images.append(cropped)

# 初期フレームに残りのフレームを追加するように保存
cropped_images[0].save(
    'sample_cropped.gif', 
    save_all=True, 
    append_images=cropped_images[1:],
    duration=500,
    loop=0,
)

結果。左が元 GIF 画像で、右がクロップ後の GIF 画像です。いい感じにできています。
sample_concat.gif

(元 GIF データはこちらの記事。)

PIL の GIF プラグインはやや発展途上感があり、GIF ファイルとして保存する際に画像が劣化したりアーティファクトが生じたりする不備があるようです。save の際に PIL が自動で走らせる optimize を False にすることで治ることもあれば、一旦 RGBA に convert してから optimize=False とすると治ることもあったりします。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6