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

### 概要

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

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

### GIF ファイル保存

```python
img1.save('ファイル名.gif', save_all=True, append_images=[img2, img3, ...])
```

##### 1. `image.save(fp, format=None, **params)`

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

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

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

```python
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 ファイル保存オプション](https://pillow.readthedocs.io/en/5.1.x/handbook/image-file-formats.html#saving)）

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


##### サンプルコード：

```python
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84890/330676f2-73ba-c230-6de3-262b8a64c818.gif)



### GIF ファイル読み込み


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

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

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

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

```python
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))
```

```bash
<class 'PIL.GifImagePlugin.GifImageFile'>
<class 'PIL.Image.Image'>  # 普通の Image オブジェクト（他フレームのデータが消える） 
<class 'PIL.Image.Image'>
```

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


<img width="1332" alt="スクリーンショット 2020-02-09 14.12.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84890/daccd226-7949-bb72-4144-6bb7bfbcbb2d.png">



### 読み込んだ GIFデータ（GifImageFile）の取り扱い

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

<img width="1132" alt="スクリーンショット 2020-02-09 14.12.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84890/db3aa829-5be0-2dad-b017-310e4f0bd819.png">

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

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

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

##### GifImageFileクラス特有の関数・アトリビュート

| 名前 | 説明 |
|---|----|
| seek(index) | 参照フレームを index の位置に変更する。 |
| tell() | 自分が今参照しているフレームの位置を返す。 |
| n_frames | 保持しているGIF ファイルのフレーム数 | 

*他にもあります（[公式](https://pillow.readthedocs.io/en/5.1.x/reference/plugins.html?highlight=seek#module-PIL.GifImagePlugin)）

### チュートリアル　読み込んでクロップして保存してみる
上の内容から、以下の手順となる。

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


```python
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84890/9f78685d-e219-91c4-bbca-b9aba941f2ed.gif)

（元 GIF データは[こちらの記事](https://qiita.com/Takarasawa_/items/1556bf8e0513dca34a19)。）


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