OpenCVでコマ撮りカメラを作る　～PILとOpenCVの画像変換＆PILでアニメGIF作成～

# はじめに
ディープラーニングの物体検出のデモを見て不思議に思ったのは、物体検出の仕組みそのものではなく「どうやって動画に四角形や文字を描いているのだろう」ということだった。そして実際にディープラーニングの勉強をはじめ、その一環としてOpenCVを学んだ瞬間、『アカギ』に出てくる八木のごとく電流が走ったものだ。
インプットがあって、処理があって、アウトプットがある。つまり、何でもできるってことじゃん！
そのことに気づいたら、面白いアイデアが次から次へと頭に浮かんできた。
~~それはもう、ディープラーニングのことなどどうでもいいと思えるほどに。~~

# OpenCVでの画像表示基本2例
## 画像の表示
私の記事の中では[既出](https://qiita.com/mo256man/items/8912934578025adb5ce5)。

```Python:
import cv2

filename="hoge.jpg"
img=cv2.imread(filename)

cv2.imshow("image window", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

cv2.waitKey(*delay*)はキーが押されるまで待機する関数。
*delay*は遅延時間で単位はミリ秒。0もしくはマイナスの値の場合は無限に待つ。
往年のBASICで言うところのINPUT$(1)に近い。
日本語の解説ではあまり目にしないが、**cv2.waitKey()は、cv2.imshow()などで作成されるOpenCVのウィンドウ（HighGUIという）がアクティブになっているときのみ機能する**ことは覚えておく必要がある。


## カメラ映像の表示
こちらも[チュートリアル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_gui/py_video_display/py_video_display.html#display-video)にある基本的なプログラム。
おこなっていることは基本的に同じ。無限ループで都度映像を取得して表示しているだけだ。
カメラの設定は[こちら](https://qiita.com/Kazuhito/items/cc6b8a0bd75cf9689bf9)を参考にするとよい。

```Python:

import cv2

cap = cv2.VideoCapture(0)

while(True):
    ret, frame = cap.read()
    cv2.imshow("frame", frame)
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()
```

VideoCapture型のオブジェクトを用意し、カメラの映像を取得する。
cap.read()は二つの値を返す。0番目の戻り値はフレームの取り込みが成功したかどうかでTrue/Falseのブーリアン。1番目の戻り値が取り込んだフレーム。

で。
無限ループで回っているのでキー入力を待ってなどいられない。画像表示のときは永遠に待つcv2.waitKey(0)だったのが、こちらではcv2.waitKey(1)になっている。1ミリ秒だけ待って、入力があろうとなかろうと次に進む。BASICでゲームを作る際に必須だった関数、INKEY$に相当する。
ord()はUnicodeコードポイントを取得する関数。BASICで言うところのASCで、要するにqが押されたらループを抜けるということ。
「& 0xFF」の部分は何をしているかというと、0xFFすなわち0b11111111 とのANDを取ることで下位8ビットを取得している。公式の記述が見つからなかったのだがcv2.waitKey()は32ビット整数を返すらしく、こうやって0～255の数値として認識させる必要があるわけだ。

最後には行儀正しくVideoCaptureオブジェクトとimshowウィンドウを破棄していますな。

# 応用例：コマ撮りカメラ
この二つのサンプルを見て「ゲームが作れるじゃん！」と思ったものだが、皆さんにお見せできるようなゲームはまだできていないのでもっと簡単で実用的なプログラムを紹介する。

```Python:komadori.py
import cv2
from PIL import Image

def cv2pil(imgCV):
    imgCV_RGB = imgCV[:, :, ::-1]
    imgPIL = Image.fromarray(imgCV_RGB)
    return imgPIL    

cameraID = 0 # お好きにどうぞ
cap = cv2.VideoCapture(cameraID)
imgs = []

while(True):
    ret, imgCV = cap.read()
    cv2.imshow("camera", imgCV)

    key = cv2.waitKey(1) & 0xFF
    if key == ord("q"):
        break
    elif key == ord(" "):
        imgPIL = cv2pil(imgCV)
        imgs.append(imgPIL)

imgs[0].save("anim.gif", save_all=True, append_images=imgs[1:], 
            optimize=False, duration=1000, loop=0)
cap.release()
cv2.destroyAllWindows()
```

これによりこんなアニメGIFができる。ドヤれるほどのフィギュアは持っていないのでBJ先生にご登場いただいた。カメラ位置が悪いがノートPCのカメラなので仕方がない。ターンテーブル、買おうかな。
![anim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/24a5cc1d-0c15-59bc-504b-031a071f816a.gif)



# OpenCVとPILの変換
今回はRGB画像に限定して話をする。
グレースケール画像なら？ 透過情報を持つRGBA画像なら？ それはまた今度書く…かもしれない。

## PIL → OpenCV
PIL画像をOpenCVに変換するにはPILの画像データをnumpy.arrayに突っ込んでやるだけだけでよい。
このとき各成分は8ビットの符号なし整数ですよと一言添えてやるのが気遣いのできるオトコというもの。
PILの色の並びはRGBでOpenCVはBGRなのでRGBをBGRに直すのを忘れないように。

```Python:
import numpy as np

def pil2cv(imgPIL):
    imgCV_RGB = np.array(imgPIL, dtype=np.uint8)
    imgCV_BGR = np.array(imgPIL)[:, :, ::-1] # H方向とW方向はそのままに、RGBを逆順にする
    return imgCV_BGR


# 以下、使用例
from PIL import Image
import cv2
imgPIL = Image.open("hoge.jpg")

imgCV = pil2cv(imgPIL)  # cv2.imreadしてないぞ
cv2.imshow("",imgCV)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

## OpenCV → PIL
上の例で使っている。
OpenCVの画像をPILにするにはImage.fromarray()を使う。
OpenCVの色の並びはBGRでPILはRGBなのでBGRをRGBに直すのを忘れないように。

``` Python:
from PIL import Image

def cv2pil(imgCV):
    imgCV_RGB = imgCV[:, :, ::-1] # H方向とW方向はそのままに、BGRを逆順にする
    imgPIL = Image.fromarray(imgCV_RGB)
    return imgPIL


# 以下、使用例
import numpy as np
import cv2

imgCV = cv2.imread("hoge.jpg")
imgPIL = cv2pil(imgCV)  # Image.openしてないぞ
imgPIL.show()
```

### 注意事項
OpenCVの画像データは8ビット整数でなくそれを255で割った0～1の浮動小数点数でも良いのだが、Image.fromarray()で変換する場合は元のOpenCV画像は8ビット整数である必要がある。
もっとも、既存画像をcv2.imread()で読み込めば普通に8ビット整数になっている。0～1の小数を使うのはディープラーニングで学習するときくらいか。そのときはPILは使わないので深く気にする必要はないだろう。


# PILでアニメーションGIFを作成する
OpenCVはコンピュータビジョンが専門分野なのでアニメーションGIFを作成することができない。というかOpenCVはそもそもGIFに対応しておらず、cv2.imread()で読み取ることもできない。

## PILで画像を保存　Image.save()
Image.save(*filename*, *format*, *params*)という使い方をする。
*filename*はファイル名。
*format*は画像フォーマット。これを指定しない場合、ファイル名の拡張子で自動的に決まる。
*params*は各種パラメーター。アニメーションGIFに関して以下で述べる。

### PILでアニメーションGIFを保存
もう一度該当部分を書いておこう。

```Python:
imgs[0].save("anim.gif", save_all=True, append_images=imgs[1:], 
            optimize=False, duration=1000, loop=0)
```


各画像はリスト形式で配列化しておく。imgs = [&nbsp;] で空配列を作成し、imgs.append() で次々と追加していく。
そして0番目のPIL画像に対してsave()をおこなう。

このときパラメーターには以下のようなものがある。
　*save_all*　GIFやtiffやpdfに対してsave_all=Trueとすれば複数画像を1ファイルにまとめることができる。
　*append_images*　追加する画像をリスト形式で指定する。0番目の画像を保存する際の追加画像だから、0番目の画像そのものは含まないのが普通。
　*optimize*　フルカラーの元画像を256色のGIFフォーマットにする際の最適化の有無。
　*duration*　フレームの表示時間。単位はミリ秒。アニメGIFをブラウザで表示させるのなら[あまり小さい数値は指定しないようにしよう](https://at.sachi-web.com/blog-entry-712.html)。
　*loop*　ループ回数。0ならば無限。
　*transparency*　カラーインデックスを指定しその色を透明色にする。何色がカラーインデックス何番なのかを把握するのは容易ではないので使いこなすのは難しいだろう。

# 終わりに
OpenCVがメインなのかPILがメインなのか、えらくとっちらかった内容になってしまった。

面白いことをしたい、面白いものを作りたいという気持ちは勉強のモチベーションアップにつながるものだ。
早くOpenCVの面白ネタを吐き出して、真面目に機械学習の勉強をしなくては。