OpenCV + Python で動画内の変化点を見つける

## 目的
- 動画をフレーム単位（画像）で扱い、加工および可視化ができるようになる
- カラー画像から白黒画像に変換する
- フレーム間の変化情報から変化点を見つける

### 前提
- 動画が画像の集まりであり、カラー画像とRGBおよびピクセルの関係について知っていること
- <https://ja.wikipedia.org/wiki/RGB>
- <https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%AB>

## 対象動画

下記ページからダウンロードできる「賢く著作物を共有する方法（クリエイティブコモンズの使い方）」という動画を利用する

<http://togotv.dbcls.jp/20091231.html>

> この記事は、クリエイティブ・コモンズ 表示 ライセンスの下で利用可能です。 利用にあたっては、下記のクレジットを必ず表示してください。   
> © 2016 DBCLS TogoTV / CC-BY-4.0

- <https://creativecommons.org/licenses/by/4.0/deed.ja>
- YouTube URL はこちら <https://www.youtube.com/watch?v=e9E1RkMjMo8>

動画として使うには動きがあまりないが、ライセンス上使って問題ないのでとりあえずこれにしました


## インストール
- anaconda 前提 5.3.0: python3.7
- opencv 3.4.2 (conda search で探しても4系がなかった)
- numpy with openblas

```bash
conda install "opencv==3.4.2"
```

### おまけ: 3.4.3以上の場合
conda-forge にしか存在せず、anacondaで入るmklではなくopenblas なのでめんどう

```bash
conda install -c conda-forge "opencv==3.4.4"
    blas:         1.0-mkl                                           --> 1.1-openblas                           conda-f
        
conda install numpy
    The following NEW packages will be INSTALLED:
        libopenblas        pkgs/main/osx-64::libopenblas-0.3.3-hdc02c5d_3
```

<details>
<pre>
import numpy as np
np.show_config()
las_mkl_info:
  NOT AVAILABLE
blis_info:
  NOT AVAILABLE
openblas_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/Users/xxx/.pyenv/versions/anaconda3-5.3.0/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
blas_opt_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/Users/xxx/.pyenv/versions/anaconda3-5.3.0/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
lapack_mkl_info:
  NOT AVAILABLE
openblas_lapack_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/Users/xxx/.pyenv/versions/anaconda3-5.3.0/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
lapack_opt_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/Users/xxx/.pyenv/versions/anaconda3-5.3.0/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
</pre>
</details>

## 動画の読み込みと動画基本情報


```python
import numpy as np
import matplotlib.pyplot as plt
import cv2

%matplotlib inline
```


```python
# download しておく
movie_path = "./091231MBSJ2009_yuko.mov"
cap = cv2.VideoCapture(movie_path)

fps = cap.get(cv2.CAP_PROP_FPS)
w = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
h = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
frame_count = cap.get(cv2.CAP_PROP_FRAME_COUNT)
sec = frame_count / fps

fps, frame_count, round(sec, 3), (h, w)
```




    (29.97, 36540.0, 1219.219, (480.0, 853.0))



## フレーム間の変化情報から変化点を見つける
まずは、フレーム単位の読み込みを行う。
ただし、動画開始から40秒くらいで動きが止まるので、 40sec = n_frames / (fps≒30) より


```python
n_frames = int(40 * fps + 1)
n_frames
```




    1199



だけ取得してあとは利用しない


```python
frames = []
while True:
    # generator にしたほうが使いやすい
    ret, frame = cap.read()
    if not ret:
        print("read error")
        break
    
    frames.append(frame)
    if len(frames) == n_frames:
        break

# context manager として扱えるようにし、releaseを意識しないようにしたほうがいい
if cap.isOpened():
    cap.release()
    
```


```python
frames[0].shape
```




    (480, 853, 3)



フレームはnumpy の配列として取得できる。numpy/pandas等に慣れている人ならあとは加工するだけ。
縦横サイズはOpenCVから取得したサイズと同じで、最後の情報はGBR各チャネルを表している


### フレーム単位の可視化
最初の５フレームを可視化する。ちなみにOpenCVでのカラー画像はRGBではなく、BGRなので変換する必要がある


```python
fig, axes = plt.subplots(1, 5, figsize=(20, 3))
for frame, ax in zip(frames[:5], axes):
    rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    ax.imshow(rgb_frame)
    ax.axis("off")
fig.suptitle("RGB変換後", fontsize=16)

fig, axes = plt.subplots(1, 5, figsize=(20, 3))
for frame, ax in zip(frames[:5], axes):
    ax.imshow(frame)
    ax.axis("off")
fig.suptitle("BGRのまま", fontsize=16)

gray_frames = [cv2.cvtColor(f, cv2.COLOR_BGR2GRAY) for f in frames]
fig, axes = plt.subplots(1, 5, figsize=(20, 3))
for frame, ax in zip(gray_frames[:5], axes):
    ax.imshow(frame, cmap="gray", vmin=0, vmax=255)
    ax.axis("off")
_ = fig.suptitle("Gray Scale", fontsize=16)

```


![output_14_0.png](https://qiita-image-store.s3.amazonaws.com/0/117427/05691d5e-5c96-09d8-4226-d07a57c833fc.png)
![output_14_1.png](https://qiita-image-store.s3.amazonaws.com/0/117427/50a8b27f-56e7-9a6a-97b4-9546a7d8f7ab.png)
![output_14_2.png](https://qiita-image-store.s3.amazonaws.com/0/117427/c34a7dba-bd8a-d8df-9582-67257d740bb2.png)

### 動きが少ないので圧縮する
- 音声を利用しないのであれば、同じようなフレームでは情報が増えない
- 同じかどうかは差分をとればわかるが
- カラーの差分では解釈しにくいので、グレースケールしたフレームを利用する

0〜255 のピクセル値からもう片方も同じ範囲の値との差分をとるので、差分の範囲は-255〜255。差分もグレースケール画像として見るとき、解釈は差分=0ならグレーとして出力され、差分が大きいほど白や黒に近づく。


```python
fig, axes = plt.subplots(1, 5, figsize=(20, 3))
for bef, aft, ax in zip(gray_frames[:5], gray_frames[1:5+1], axes):
    diff_frame = aft - bef
    ax.imshow(diff_frame, cmap="gray", vmin=-255, vmax=255)
    ax.axis("off")

_ = fig.suptitle("diff", fontsize=16)

```

![output_16_0.png](https://qiita-image-store.s3.amazonaws.com/0/117427/3c64eaa1-78ee-acc6-755e-6bec02cab5fb.png)

### 差分から異なるピクセルの定義
とりあえずはフレーム間のピクセル差分は1でも違っていたら異なるとする


```python
diff_frames = [aft - bef for bef, aft in zip(gray_frames, gray_frames[1:])]
n_diff_pixels = (diff_frames[0] != 0).sum()
n_diff_pixels
```




    5054



フレーム間でどれくらいの割合で異なっていたかは、下記のとおり


```python
area = w * h
"{:.2%}".format(n_diff_pixels / area)
```




    '1.23%'



ほとんど変わらないと認識していても、1%は変わっている。全体としてどこでフレーム間の違いが大きいかを可視化する


```python
fig = plt.figure(figsize=(20, 5))

diff_rates = np.array([(d != 0).sum() / area for d in diff_frames])
mean = diff_rates.mean()
median = np.median(diff_rates)

plt.plot(diff_rates)
plt.axhline(mean, color="red")
_ = plt.axhline(median, color="green")
```


![output_22_1.png](https://qiita-image-store.s3.amazonaws.com/0/117427/b5a67ae0-946e-2ebf-243d-6c00e40d2771.png)


中央値と平均値はほぼ同じであり、0.7を超えれば極端に変化のある部分が特定できそう


```python
idx, = np.where(diff_rates >= 0.7)
idx, idx.shape
```




    (array([ 198,  199,  201,  203,  204,  205,  206,  207,  209,  210,  211,
             212,  213,  214,  215,  216,  217,  218,  219,  220,  221,  222,
             223,  224,  225,  266,  267, 1189, 1190]), (29,))




```python
for j in range(6):
    fig, axes = plt.subplots(1, 5, figsize=(20, 3))
    for i, ax in zip(idx[j*5:(j+1)*5], axes):
        ax.imshow(cv2.cvtColor(frames[i], cv2.COLOR_BGR2RGB))
        ax.axis("off")
        ax.set_title(f"{i+1} frame, {i / fps:.2f} sec")
    
```

![output_25_0.png](https://qiita-image-store.s3.amazonaws.com/0/117427/bb810f58-cefe-0694-2076-929cef7322af.png)
![output_25_1.png](https://qiita-image-store.s3.amazonaws.com/0/117427/c31b119e-3cde-8204-fdb4-04c11b509541.png)
![output_25_2.png](https://qiita-image-store.s3.amazonaws.com/0/117427/9383c267-1bcf-f356-6948-ed15d501636f.png)
![output_25_3.png](https://qiita-image-store.s3.amazonaws.com/0/117427/a2d74e85-be62-e303-7306-dfdb09a1cee1.png)
![output_25_4.png](https://qiita-image-store.s3.amazonaws.com/0/117427/190fd438-2dab-c12a-13d4-ab2bbd410848.png)
![output_25_5.png](https://qiita-image-store.s3.amazonaws.com/0/117427/0a9aea98-a1e0-1b38-92cf-5faa852dc41e.png)


動画と比較してみると、下記のときに差分が大きい

- 文字だけの情報から人物が現れる場面
- 光があるとき消えるとき
- スライドが写ったり、切り替わったりしたとき



## まとめ
- 動画を読み込み、フレームのサイズやFPSおよび長さなどを取得できた
- １フレームごとに、カラー画像グレースケールとして可視化できた
- フレーム間の違いを算出し、動画内における変化点を見つけた

今回は変化の度合い（ピクセル差分）や変化率を少しでも変わったら変化と判定しているため、。
この各変化に関する閾値をパラメータとして設定すれば、本当の変化点を（設定されていれば）見つけられるかもしれない。


## 参考
- <https://docs.opencv.org/trunk/d6/d00/tutorial_py_root.html>
- <https://opencv-python-tutroals.readthedocs.io/en/latest/>
- <https://docs.opencv.org/3.4.2/d8/dfe/classcv_1_1VideoCapture.html>
- <https://matplotlib.org/gallery/subplots_axes_and_figures/figure_title.html>
- <https://matplotlib.org/api/_as_gen/matplotlib.pyplot.imshow.html>