Google Colabで画像からGIF動画を作りインライン表示させる3つの方法

# 概要
Google ColabでGIF動画を作りインライン表示させるまでの3つの方法（pillow, imageio, imagemagickのconvert）を紹介する。Google Colabで作成したコードは、[こちら](https://colab.research.google.com/drive/12k8SKvLxyqAIn8t28BTvoFNztLimBv1G?usp=sharing)にあります。








# 方法
1. [GIF動画にしたい画像ファイルをGoogle Colabにアップロード、またはGoogle Colab内で作成](#1-gif動画にしたい画像をgoogle-colabにアップロードまたはgoogle-colab内で作成)
1. [globで画像ファイル名を取得](#2-globで画像ファイル名を取得)
1. [GIF動画を作り保存（pillow、imageio、imagemagickのconvert）](#3-gif動画を作り保存pillowimageioimagemagickのconvert)（3種類紹介しますが、どれか1つ使うと良いです。）
1. [保存したGIF動画をIPythonで開きインライン表示](#4-保存したgif動画をipythonで開きインライン表示)


## 1. GIF動画にしたい画像をGoogle Colabにアップロード、またはGoogle Colab内で作成
1. まず、Google Colabを立ち上げます。
1. 下図の青枠からGIF動画にしたい画像をアップロードするか、Google Colab内でコードを書きその場で画像を作成します。JPG画像を用意した人はPNG画像に変換してください。[pillowの公式サイト](https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html#saving)によると、JPG画像は対応していないためです。変換方法は、[jpgをpngに変換しても透明度がいじれなかったからなんとかした](https://qiita.com/Fox_Kei/items/5e790d32a10f5498c805)を参照してください。
GIF動画に使う画像は、01.png, 02.png, ..., 60.pngのように**GIF画像に使う枚数の最大桁数で0埋めした通し番号の名前を付ける**と良いです。
（組み込み関数``sorted()``を用いた``sorted(glob.glob('*.png'))``で順番を管理しやすくなります。0埋めにしないファイル名で``sorted(glob.glob('*.png'))``を使うと、1.png, 10.png, 2.png, ...となってしまいます。回避する方法があるので、方法の[0埋めにしていないファイル名を読み込みソートする](#0埋めにしていないファイル名を読み込みソートする)で紹介します。）
![図1-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/d717e3b6-c759-d21b-f38b-dbafdcdf5276.png)

1. ここで、画像が置いてあるか確認するため、セルに``!ls``と入力して実行してください。自分が用意した画像ファイル名が表示されて入ればOKです。（セルの先頭に「``!``」を付与するとコマンドとして認識されます。）
<details><summary>用意した画像ファイル名が表示されない場合</summary>画像が置いてあるパスと今いるパスが異なる場合があります。セルに``!pwd``を入力して実行してください。今いるパスが分かります。立ち上げた後パスの移動をしていないと``/content``と返ってくると思います。画像があるパスを確認して異なっていたら、``%cd /画像のパス``で画像のパスまで移動してください。（ここは、「``!``」を使った``!cd /画像のパス``では、パスの移動はできません。詳しくは、[!cd と %cd の違い](https://qiita.com/bear_montblanc/items/64d7efd9e4ea7caa98f9)を参照してください。）</details>
1. ちなみに``!ls *.png``がGIF動画に使う画像です。「``*``」は任意の長さの任意の文字列を表す[ワイルドカード文字](https://support.microsoft.com/ja-jp/office/ワイルドカード文字の例-939e153f-bd30-47e4-a763-61897c87b3f4)の一種で、``.png``で終わるファイル名が表示されます。



今回サンプルとして、時計の秒針をイメージしたも60枚のPNG画像を用意しています。


```Python:サンプルの複数画像
# サンプルの複数画像（時計の秒針をイメージしたもの）
import numpy as np
import matplotlib.pyplot as plt


def get_rotate_coord(angle, coord):
    theta = np.radians(angle)  # °からラジアンに変換
    c, s = np.cos(theta), np.sin(theta)
    R = np.array([[c, -s], [s, c]])  # 2次元の回転行列
    rotate_coord = np.dot(R, coord)  # 回転後の座標を取得
    return rotate_coord

start_coord = np.array([0, 10])  # この座標の原点を軸に回転させる
for sec in range(60):
    fig = plt.figure(figsize=(5, 5))
    ax = fig.add_subplot(1, 1, 1)
    rotate_coord = get_rotate_coord(-6*sec, start_coord)
    ax.plot((0, rotate_coord[0]), (0, rotate_coord[1]), lw=3)
    circle = plt.Circle((0, 0), 11, fill=False)  # 円をプロット
    ax.add_artist(circle)
    ax.set_title(sec)
    ax.set_xlim(-12, 12)
    ax.set_ylim(-12, 12)
    ax.axis('off')
    plt.savefig('{:0>2}.png'.format(sec))  # 0埋め2桁のファイル名
    plt.close()
```


## 2. globで画像ファイル名を取得
### 0埋めにしたファイル名を読み込みソートする
```Python:0埋めにしたファイル名を読み込みソートする
import glob


filenames = sorted(glob.glob('*.png'))
```
``print(filenames)``とすると、``['00.png', '01.png', '02.png',..., '60.png']``と画像ファイル名が読み込まれているのが分かります。

### 0埋めにしていないファイル名を読み込みソートする
0埋めにしていない通し番号の画像ファイル名を読み込む場合は、natsortの[natsorted()](https://natsort.readthedocs.io/en/5.4.1/natsorted.html#natsorted)を使うと順番に読み込めます。

```Python:0埋めにしていないファイル名を読み込みソートする
import glob
from natsort import natsorted


filenames = natsorted(glob.glob('*.png'))
```

## 3. GIF動画を作り保存（pillow、imageio、imagemagickのconvert）
3つの方法を紹介します。どれか1つ使えば良いです。

### pillow編
```Python:GIF動画を作り保存（pillow編）
# pillow編
import PIL


# GIF動画の作成
images = [PIL.Image.open(filename) for filename in filenames]  # PNGファイルを開く
out_filename = 'image_pillow.gif'  # GIF動画のファイル名
'''
パラメータ
--------
image[0] : GIF動画の先頭に使う画像
out_filename : 保存するGIF動画のファイル名
save_all : 画像を全て保存するか（GIF動画を作るので画像を全て保存するTrueを選択する。）
append_images : 2枚目以降のGIF動画に使う画像のリスト
               （images[1:]とスライスを使うと、2枚目以降の画像のリストになる。）
duration : 表示間隔（単位は1／1000秒）
loop : ループ回数（0は無限ループ）
--------
'''
images[0].save(out_filename, 
               save_all=True, append_images=images[1:], duration=1000, loop=0)
```




### imageio編
```Python:GIF動画を作り保存（imageio編）
# imageio編
import imageio


# GIF動画の作成
images = [imageio.imread(filename) for filename in filenames]  # PNGファイルを開く
out_filename = 'image_imageio.gif'  # GIF動画のファイル名
'''
パラメータ
--------
out_filename : 保存するGIF動画のファイル名
images : GIF動画に使う画像のリスト
duration : 表示間隔（単位は秒）
loop : ループ回数（0は無限ループ）
--------
'''
imageio.mimsave(out_filename, images, duration=1.00, loop=0)
```


### imagemagickのconvert編
```Python:GIF動画を作り保存（imagemagickのconvert編）
# imagemagickのconvert編
!sudo apt-get install imagemagick  # imagemagickをインストール


# GIF動画の作成
filenames_str = ' '.join(filenames)  # 00.png 01.png 02.png…60.pngの文字列にする
out_filename = 'image_imagemagick.gif'  # GIF動画のファイル名
'''
パラメータ
--------
delay : 表示間隔（単位は1/100秒）
loop : ループ回数（0は無限ループ）
filenames_str : GIF動画に使う画像の名前が入った文字列
out_filename : 保存するGIF動画のファイル名
--------
'''
!convert -delay 100 -loop 0 $filenames_str $out_filename
```

# 4. 保存したGIF動画をIPythonで開きインライン表示
```Python:保存したGIF動画をIPythonで開きインライン表示
# pillowで作成したGIF動画（3種類のどれでも良い）
import IPython


out_filename = 'image_pillow.gif'
IPython.display.Image(out_filename, format='png')
```


出力結果
![image.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/5a7a37b2-f201-3e98-eb9b-e934e8d5b9a6.gif)
時計の秒針みたいですね。Google Colabにインライン表示できていれば成功です。

# 途中で一定期間静止させたGIF動画の作成方法
一定期間静止させたい画像の表示枚数を増やすことで静止させることができます。つまり、方法の[2. globで画像ファイル名を取得](#2-globで画像ファイル名を取得)のコードのみを書き換え、その他は同じ手順です。

ファイル名と追加したい枚数を指定するための関数``append_filename``を以下のように定義します。

```Python:append_filename
def append_filename(filenames, filename, flame_num):
    '''
    パラメータ
    --------
    filenames : GIF画像用のファイル名が入ったリスト
    filename : 枚数を追加したいファイル名
    flame_num : 追加する枚数
    --------
    '''
    if filename in filenames:  # filenamesにfilenameが存在するか確認（次行のエラー対策）
        index_filename = filenames.index(filename)
        for _ in range(flame_num):
            filenames.insert(index_filename, filename)
    else:
        print('ファイル名{}は、filenamesにはないです。'.format(filename))
    return filenames
```
コードの説明です。

* ``index_filename``で枚数を増やしたいファイル名のindexを取得します。

* ``filenames.insert(index_filename, filename)``でそのindexの1つ前に枚数を増やしたいファイル名と同じファイル名を加えます。この同じファイル名の追加を複数回繰り返すとindexが変わってしまうと一見思いますが、最初に指定したindexの``index_filename``には**追加したファイル名の1つ前に常に追加できる**ので問題ないです。
ここは、今回作ったコードで作るGIF動画が生きる場面です。例えば、``filenames=['00.png', '01.png', '02.png', '03.png']``に対して、``append_filename(filenames=filenames, filename='02.png', flame_num=3)``とするとどうなるでしょうか。
以下のように追加されていく過程が分かります。このように、3枚の02.pngが追加されて、``filenames=['00.png', '01.png', '02.png', '02.png', '02.png', '02.png', '03.png']``となります。
![image_imageio-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/da2927b7-537f-277e-877d-bcd342ef5a5a.gif)

早速、途中で一定期間静止したGIF動画を作っていきます。
[2. globで画像ファイル名を取得](#2-globで画像ファイル名を取得)のコードに一定期間静止させたいファイル名と追加する枚数を指定して、その他は同じ手順です。（例では、pillowを使い``duaration=1000``から``duaration=60``に変更してます。）

```Python:途中で一定期間静止させたGIF動画の例
import glob


filenames = sorted(glob.glob('*.png'))  # ファイル名を読み込みソートする
'''ここから一定期間静止させるために変更したコード'''
filenames = append_filename(filenames, '00.png', 20)  # 00.pngを20枚追加
filenames = append_filename(filenames, '15.png', 5)  # 15.pngを5枚追加
filenames = append_filename(filenames, '30.png', 20)  # 30.pngを20枚追加
```
出力結果
![image_pillow_still.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/182bad58-7844-ce3a-a832-ce9108bcb2eb.gif)
サーボモータを制御しているみたいですね。

# loopの最後に一定期間静止させたGIF動画の作成方法
ここで、loopの最後に一定期間静止させた例も紹介します。[途中で一定期間静止させたGIF動画の作成方法](#途中で一定期間静止させたgif動画の作成方法)と同様に、関数の``append_filename``の引数に最後のファイル名を入力してももちろん良いです。ですが、毎回ファイル名を確認して打ち込むと大変なので、リスト``filenames``の最後ということで**``filenames[-1]``を使うと汎用性が高い**です。（例では、pillowを使い``duaration=1000``から``duaration=60``に変更してます。）

```Python:loopの最後に一定期間静止させたGIF動画の例
import glob


filenames = sorted(glob.glob('*.png'))  # ファイル名を読み込みソートする
'''ここから一定期間静止させるために変更したコード'''
# loopの最後のファイルを20枚追加（ファイル名を入力するよりリストのindexで最後を指定する方が楽）
filenames = append_filename(filenames, filenames[-1], 20)
```
出力結果
![image_pillow_still-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1599258/1fa7d5c4-267b-45fd-19d7-6e0f95556e53.gif)
</div></details>


# 3種類の方法の比較
3種類（pillow、imageio、imagemagickのconvert）の処理速度とデータサイズを比較します。比較には、サンプルで用意した時計の秒針をイメージした60枚のPNG画像を使います。それぞれオプションなしで比較した結果なので、解像度などのオプションを付けると結果が変わる可能性があります。
## 処理速度の比較
処理速度の比較は、方法の[3. GIF動画を作り保存（pillow、imageio、imagemagickのconvert）](#3-gif動画を作り保存pillowimageioimagemagickのconvert)の箇所で、それぞれのモジュールをimport後からします。
計測には[%%timeit](https://ipython.org/ipython-doc/dev/interactive/magics.html#magic-timeit)を使い、セルの中身を10回実行して計測します。

```Python:pillowの処理速度
%%timeit -r10
images = [PIL.Image.open(filename) for filename in filenames]  # ファイルを開く
out_filename = 'image_pillow.gif'  # GIF動画のファイル名
images[0].save(out_filename, 
               save_all=True, append_images=images[1:], duration=1000, loop=0)
# 結果 : 1 loop, best of 10: 454 ms per loop
```
```Python:imageioの処理速度
%%timeit -r10
images = [imageio.imread(filename) for filename in filenames]  # ファイルを開く
out_filename = 'image_imageio.gif'  # GIF動画のファイル名
imageio.mimsave(out_filename, images, duration=1.00, loop=0)
# 結果 : 1 loop, best of 10: 1.38 s per loop
```
````Python:imagemagickのconvertの処理速度
%%timeit -r10
filenames_str = ' '.join(filenames)  # 00.png 01.png 02.png…60.pngの文字列にする
out_filename = 'image_imagemagick.gif'  # GIF動画のファイル名
!convert -delay 100 -loop 0 $filenames_str $out_filename
# 結果 : 1 loop, best of 10: 9.26 s per loop
```
処理速度は、imagemagick（遅い）< imageio < pillow（速い）結果となりました。
## データサイズの比較
データサイズは、[os.path.getsize()](https://docs.python.org/3/library/os.path.html#os.path.getsize)を使いバイト単位で取得します。

```Python:データサイズの比較
import os


print('pillow', os.path.getsize('image_pillow.gif'))
print('imageio', os.path.getsize('image_imageio.gif'))
print('imagemagick', os.path.getsize('image_imagemagick.gif'))
# 出力結果 : pillow 714429
# 出力結果 : imageio 932089
# 出力結果 : imagemagick 220164
```
データサイズは、imagemagick（小さい）< pillow < imageio（大きい）結果となりました。
# 補足
GIF動画の表示間隔の限界速度がブラウザごとに設けられているらしいです。詳しくは、[あなたは大丈夫？高速GIFアニメになってしまう症状](https://at.sachi-web.com/blog-entry-712.html)を参照してください。



# まとめ
Google ColabでGIF動画を作りインライン表示させる方法を3種類紹介しましたが、比較した結果も見つつ好きなものを使いましょう。