【Python】動画からGIFをつくる

普段はWebサービスを使っていますが、時間がかかりすぎたり失敗したりするんで自分で作ってみる。

### 普段使ってるやつ
GIFにテキスト埋め込んだりできるので好き。
[GIF Maker - Video to GIF Creator Tools | GIPHY](https://giphy.com/create/gifmaker)

### 使うライブラリ
- Pillow
- OpenCV

### コード
```python:make_gif.py
import math

import cv2
from PIL import Image


def get_fps_n_count(video_path):
    """動画のfpsとフレーム数を返す"""
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        return (None, None)
    
    count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    fps = round(cap.get(cv2.CAP_PROP_FPS))

    cap.release()
    cv2.destroyAllWindows()
    return (fps, count)


def aspect_ratio(width, height):
    """アスペクト比を返す"""
    gcd = math.gcd(width, height)
    ratio_w = width // gcd
    ratio_h = height // gcd
    return (ratio_w, ratio_h)


def resize_based_on_aspect_ratio(aspect_ratio, base_width, max_width=400):
    """アスペクト比を元にリサイズ後のwidth, heightを求める"""
    if base_width < max_width:
        return None

    base = max_width / aspect_ratio[0]
    new_w = int(base * aspect_ratio[0])
    new_h = int(base * aspect_ratio[1])
    return (new_w, new_h)


def get_frame_range(video_path, start_frame, stop_frame, step_frame):
    """指定された範囲の画像をPillowのimage objectのリストにする"""
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        return None
    
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    asp = aspect_ratio(width, height)
    # でかすぎてもあれなので最大幅を400にしとく
    width_height = resize_based_on_aspect_ratio(asp, width, max_width=400)

    im_list = []
    for n in range(start_frame, stop_frame, step_frame):
        cap.set(cv2.CAP_PROP_POS_FRAMES, n)
        ret, frame = cap.read()
        if ret:
            if width_height is not None:
                frame = cv2.resize(frame, dsize=width_height)
            # BGRをRGBにする
            img_array = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            # numpyのarrayからPillowのimage objectを作る
            im = Image.fromarray(img_array)
            im_list.append(im)

    cap.release()
    cv2.destroyAllWindows()
    return im_list


def make_gif(filename, im_list):
    """gifを作る"""
    im_list[0].save(filename, save_all=True, append_images=im_list[1:], loop=0)


def main():
    """メイン処理"""
    video_file = "Wildlife.wmv"

    fps, count = get_fps_n_count(video_file)
    if fps is None:
        print("動画ファイルを開けませんでした")
        return
    
    # gifにしたい範囲を指定
    start_sec = 0
    stop_sec = 8
    
    start_frame = int(start_sec * fps)
    stop_frame = int(stop_sec * fps)
    # 適当(fpsに応じてうまいことやれるようにしたい)
    step_frame = 3

    print("開始(けっこう時間がかかる)")
    im_list = get_frame_range(video_file, start_frame, stop_frame, step_frame)
    if im_list is None:
        print("動画ファイルを開けませんでした")
        return
    
    make_gif('どうぶつ.gif', im_list)
    print("終了")


if __name__ == "__main__":
    main()
```
#### BGRとRGB問題
```python:上のコードから抜粋
# BGRをRGBにする
img_array = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
```
動画から画像を読むのにOpenCV、GIFをつくるのにPillowを使っているわけですが、この処理がないと色が変な感じになります。  
調べたところcv2.read()ではBGRで読み込まれるようで、これが色がおかしくなる原因だった。
### 出来たGIF
![u2egKgw.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/407268/fbd49cd2-1727-844e-a4fe-f2173408e089.gif)

### 参考
- [Python, PillowでアニメーションGIFを作成、保存](https://note.nkmk.me/python-pillow-gif/)
- [[Python]PythonでOpenCVを使う (基本編)](https://qiita.com/supersaiakujin/items/54a4671142d80ca962ec)

　 
おわり