(Python:OpenCV)動画をリアルタイムで二値化処理させつつ、領域間の距離を示す値を出力させてみた

#はじめに

　製造業の非ＩＴ部門で働く私でも、ＡＩ、ＩｏＴ化を意識して仕事をするようになりました。製造現場に近い生産技術職寄りの仕事をしておりますが、システム化を行う上での課題の一つに工場への利益と釣り合わないことが挙げられます。
　やりたいことはあるのだけど、見積もると導入コストが高くなってしまい（人件費が高い場合が多い。。）断念する場合があります。
　そこで、スキルアップの観点からも私が作ってしまうことが良いのでは、と思い進めていることが下記の内容です。

1. 画像処理をするプログラムを作成（画像ver.）
2. **今回：画像処理をするプログラムを作成（動画ver.）**
3. Raspberry Piに環境構築＋プログラムを格納
4. 撮影した画像でリアルタイムで処理できるか確認
5. 工場の改善に繋がる良い処理方法の探索
6. KPI値を改善して、成果

　製造現場で使用している撮影動画について、リアルタイムで処理（二値化、ある距離の演算・表示など）を行ってその結果表示させることを目指します。
　意外と現場の作業者にとって、ビデオ画像を簡単に処理したものでも製造しやすくなる手助けになり得ます。

　さて長くなりましたが、今回の概要は下記です。

+ 動画表示画面に値を表示させる
+ 画面表示画面の値が出るタイミングを調整する


前回記事はこちらです。

>画像を二値化処理させる。さらに、二領域間の最短距離を算出できるようにした（Ver1.1）。
https://qiita.com/Fumio-eisan/items/10c54af7a925b403f59f


##単純に動画を表示させる
　
　まずは動画を表示させる処理を行います。今回は7秒ほどの下記キャプチャーでの動画を処理します。
※実際の動画は一番下に記載されているgithub上に格納しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/2610816a-6465-20b5-1420-42d0cf4308a3.png)



```python:video.ipynb

#単純に表示
import cv2
import sys

file_path = 'sample_.mov'
delay = 1
window_name = 'frame'

cap = cv2.VideoCapture(file_path)
text = 'text.wmv'

if not cap.isOpened():
    sys.exit()

while True:
    ret, frame = cap.read()
   # if not ret: #この2行を入れ込むと動画再生1回で終了します。
   #     break
    if ret:
        frame = cv2.resize(frame, dsize=(600, 400))
        cv2.imshow(window_name, frame)
        if cv2.waitKey(delay) & 0xFF == ord('q'):
            break
    else:
        cap.set(cv2.CAP_PROP_POS_FRAMES, 0)

cv2.destroyWindow(window_name)
```
file_pathに表示させたい動画のパスを記述すればＯＫです。今回のプログラムの場合は無限ループで動画を再生し続けます。qキーを押すことで動画再生を止めることができます。

動画の表示に関しては、下記の手順で撮影を行っています。

1. cv2.VideoCapture()メソッドにて動画を読み込み
2. while内の構文で再生
3. frameごと（30fpsであれば1sec間に30frame）に表示させ続ける
4. （処理によって異なるが）無限ループor一回再生が完了したら終了等

となっています。後ほど注意点を書きますが、3.のフレームごとで処理しているため、1秒ごとや数秒ごとに表示させたいテロップ（表示させる値等）を変えたい場合は注意が必要です。


###字幕を入れたい


```python:video.ipynb
cv2.putText(frame, text,(100, 30), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), thickness=2)
```

この表示により字幕出力が可能です。引数は以下のようになっています。
第一引数：動画フレーム、第二引数：表示させたいテキスト、第三引数：ｘ、ｙ位置、第四引数：フォント、第五引数：文字サイズ、第六引数：ＢＧＲの色情報、第７引数：文字の太さ




##二値化させたい

　さて、二値化させて白黒表示とします。方法は画像と同じく抽出する色の上下限を定義して、cv2.inRange()メソッドで定義してあげればＯＫです。


```python:video.ipynb
#二値化処理
import cv2
import sys

camera_id = 0
delay = 1
window_name = 'frame'
file_path = 'sample_.mov'

cap = cv2.VideoCapture(file_path)

import numpy as np
bgrLower = np.array([0, 100, 100])    # 抽出する色の下限(BGR)
bgrUpper = np.array([250,250, 250])

if not cap.isOpened():
    sys.exit()

while True:
    ret, frame = cap.read()
    if not ret:
        break
    frame = cv2.resize(frame, dsize=(600, 400))

    img_mask = cv2.inRange(frame, bgrLower, bgrUpper) 
    contours, hierarchy = cv2.findContours(img_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    contours.sort(key=lambda x: cv2.contourArea(x), reverse=True)
    #target_contour = max(contours, key=lambda x: cv2.contourArea(x))
    #img_mask = cv2.line(img_mask, (250,300), (350,300), (120,120,120), 10) #第2引数が始点、第3引数が終点、第4引数が色、第5引数が線の太さ
    #img_mask=cv2.drawContours(img_mask,contours[0:2],-1,(120,120,120),5)

    cv2.imshow(window_name, img_mask)
    #cv2.imshow(window_name,img_mask, [contours[0],contours[1]])

    if cv2.waitKey(delay) & 0xFF == ord('q'):
        break

cv2.destroyWindow(window_name)

```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/5f6186ae-d4f1-1b45-ca4a-ef823714a486.png)

無事二値化することができました。

##囲われた領域間の距離を出力させたい

　さて、本題です。下記のようにある囲まれた領域間を定義してその間の距離を求めます。そして適宜その値を出力させるようにしたい思います。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/f892e2f6-bcad-892a-fcad-de5bc2179058.png)

###二点間の距離を求める

```python:video.ipynb
    contours, hierarchy = cv2.findContours(img_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)#境界引き
    contours.sort(key=lambda x: cv2.contourArea(x), reverse=True)#境界をソート

```

cv2.findContours()メソッドにて白い領域を囲みます。その戻り値がcontoursに格納されます。そして、contoursを面積順にソートします。

###二領域の座標を取得する


```python:video.ipynb
    x1=np.unravel_index(np.argmax(contours[0],axis=0), contours[0].shape)
    x2=np.unravel_index(np.argmax(contours[1],axis=0), contours[0].shape)
    
    img_mask = cv2.line(img_mask, tuple(x1[0][0]), tuple(x2[0][0]), (120,120,120), 3)

```

領域を囲んでいる座標を取得します。x1,x2にcontours[0],[1]における座標の最大値（x,yどちらかが最大となる値）を取るargmaxで返します。argmaxの場合は1次元に平坦化した場合（x,y座標関係なく1次元で判断）なので、unravel_index()メソッドにて座標としてそのインデックスを返します。

そして、実際にその座標をcv2.line()メソッドにて入れ込むことで座標間を結びます。


（補足）contoursに格納されている数値を理解する
+ contours[n]:n番目の領域を形成する座標群のarrayが格納
+ contours[n][m]:n番目の領域におけるm番目の座標のarrayが格納
+ contours[n][m][0]:n番目の領域におけるm番目のarrayが格納
+ contours[n][m][0]:n番目の領域におけるm番目のx座標が表示
+ contours[n][m][1]:n番目の領域におけるm番目のy座標が表示

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/f70717f0-f7b8-9f3b-2eaf-1f2e417e6b4c.png)

こんな感じで、ややこしいのです。


###距離の値を画面に表示させたい

　さて、この計算した値を画面に表示させます。通常、cv2.putText()メソッドにて表示させることが可能です。ただ、そのままだとフレームごとの値を計算して表示させてしまいます。これでは値がチカチカして見づらくなります。
　対策として、あるフレーム数ごとに値を更新して表示させるようにすれば良いです。
　if構文を使って30フレーム（≒今回では大よそ１秒ごと）で値を更新するように下記を行っています。

```python:video.ipynb
    if idx % 30 == 0:
        text =str(math.floor(np.linalg.norm(x1[0][0]-x2[0][0])))
    cv2.putText(img_mask, text,(300, 100), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (255, 255, 255), thickness=3)
    
```

###実際の結果

このようにして処理して出力させた結果がこちらです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/567823/21c4a013-a6e4-d9f5-03af-ae4b09b966d7.png)


本来は白と黒の二領域をそれぞれ白か黒に値を出せたらよかったのですが、うまく調整できませんでした。
また、白の部分であるアダプタの領域間（今回は一つの物ですが二つの領域に分かれています）で線を引けており、かつ距離を表す数値も出力させることができました。



#終わりに

　さて、今回動画を処理させながら再生するプログラムを作りました。フレームごとに処理させるためあまり重たい処理をさせることができない点がポイントでした。
　本来は領域間の最短距離を求めさせてその距離を引くことをしたかったのですが、そこまで実装できませんでした。
　あと、OpenCV自体がc++でまとめられている記事が多かったため、検索に苦労しました。。


プログラムは下記に格納しています。
https://github.com/Fumio-eisan/movie_20200406