OpenCVで勉強中にスマホをいじったら警告するアプリをつくった

# はじめに
PythonとOpenCVの勉強を始めました．
その際，Qiitaの情報にはとてもお世話になったので勉強したことの整理も兼ねて投稿します．
投稿するのも初めてなので拙いところがあると思いますが，これを読んだ方の参考になれば幸いです．

# できたもの
![k2d6r-vmxjj.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570879/c1a8d678-6fee-1f7b-2f39-3501510fc817.gif)

机の上に置いてあるスマホをテンプレートマッチングにより認識して，中央付近の明度から電源が入っているかを判定し，一定時間電源がついていたら警告するアプリです．

# 使ったもの
- WEBカメラ　UCAM-C520FEBK
- Python 3.7.5
- pip 20.0.1
    - numpy 1.18.1
    - opencv-python 4.1.2.30
    - PyInstaller 3.6

各モジュールはpipを用いて簡単にインストールできます．
やり方は以下のサイトを参考にしました．
https://qiita.com/fiftystorm36/items/1a285b5fbf99f8ac82eb (opencv-python, numpy)
https://techacademy.jp/magazine/18963 (PyInstaller)


# 開発の流れ
1. 警告に使用する画像とテンプレートマッチングに使う画像を用意する
2. WEBカメラにより取得した画像と用意した画像でテンプレートマッチングを行い，スマホの位置を取得できるようにする
3. スマホの中央付近の明度がある閾値以上のとき電源が入っていると判断し，カウントを行い一定回数カウントした場合警告する画像を表示するようにする
4. できたファイルのexe化

# 画像の準備
まず，警告用の画像はオンライン上のものを適当にダウンロードしました．
![studyordie2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570879/cc33d13d-f5f1-8568-90ca-f6ce7516cbbf.jpeg)
>https://ameblo.jp/ufcrekishi/entry-10967280575.html

また，テンプレート画像は以下のプログラムを用いて取得しました．

```Python:temp.py
import numpy as np
import cv2

cap = cv2.VideoCapture(0)

while(True):
	# webカメラの画像を読み込み回転
	ret, frame = cap.read()
	img1 = cv2.flip(frame, -1)
	cv2.imshow("img", img1)

	# Escキーを押したら保存して終了
	k = cv2.waitKey(0)

	if k == 27:
		cv2.imwrite('model.png',img1)
		break

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

これにより model.png が得られるはずです．
これからスマホの部分のみをトリミングすれば以下のようなものが得られます．
![model.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570879/0b229ee1-bb76-9bc2-bcd0-b90e1dee5d50.png)

これで画像の準備は終了です．

# スマホの位置の取得
スマホの位置は以下のようなプログラムで取得しました．

```Python:caution.py
import numpy as np
import cv2

cap = cv2.VideoCapture(0)

while(True):
	# webカメラの画像を読み込み，回転
	ret, frame = cap.read()
	img = cv2.flip(frame, -1)
	
	# あらかじめ撮影したスマホの画像を読み込み
	temp = cv2.imread('model.png')
	
	# マッチングテンプレートを実行
	# 比較方法はcv2.TM_CCOEFF_NORMEDを選択
	result = cv2.matchTemplate(img, temp, cv2.TM_CCOEFF_NORMED)

	# 検出結果から検出領域の位置と値を取得
	# 類似度が高い領域の左上の位置がmax_loc
	min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

	# 類似度が高い領域の右下と中央の位置を取得
	# マッチングがうまくいかなかったときは前のループの位置を使用
	if max_loc[0] < img.shape[1] - temp.shape[1] and \
	max_loc[1] < img.shape[0] - temp.shape[0]:
		topleft_x, topleft_y = max_loc
		h, w = temp.shape[:2]
		bottom_right = (topleft_x + w, topleft_y + h)
		center = (topleft_x + w / 2, topleft_y + h / 2)
		
	print(center)

	# 検出領域を青い長方形で囲んで表示
	result = img
	cv2.rectangle(result,(topleft_x, topleft_y), bottom_right, (255, 0, 0), 2)
	cv2.imshow("img", result)

	# 1秒待機してEscキーの入力があると終了
	k = cv2.waitKey(1000)
	if k == 27:
		cv2.imwrite("temp.png", result)
		break

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

最初にテンプレートマッチングができないとcenterをprintできずエラーが出るため，最初は電源を切って置いておく必要があります．
うまくテンプレートマッチングができていれば以下のようになるはずです．
![temp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/570879/584ed821-9d4a-4cd5-3915-e0823fa03f87.png)

# 明度から電源が入っているか判定
以下のプログラムで電源が入っているか判定し，長時間電源が入っていると警告する画像を表示します．

```Python:caution.py
import numpy as np
import cv2

cap = cv2.VideoCapture(0)
count = 0

while(True):
	# webカメラの画像を読み込み，回転
	ret, frame = cap.read()
	img = cv2.flip(frame, -1)
	
	# あらかじめ撮影したスマホの画像を読み込み
	temp = cv2.imread('model.png')
	
	# マッチングテンプレートを実行
	# 比較方法はcv2.TM_CCOEFF_NORMEDを選択
	result = cv2.matchTemplate(img, temp, cv2.TM_CCOEFF_NORMED)

	# 検出結果から検出領域の位置と値を取得
	# 類似度が高い領域の左上の位置がmax_loc
	min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

	# 類似度が高い領域の右下と中央の位置を取得
	# マッチングがうまくいかなかったときは前のループの位置を使用
	if max_loc[0] < img.shape[1] - temp.shape[1] and \
	max_loc[1] < img.shape[0] - temp.shape[0]:
		topleft_x, topleft_y = max_loc
		h, w = temp.shape[:2]
		bottom_right = (topleft_x + w, topleft_y + h)
		center = (topleft_x + w / 2, topleft_y + h / 2)
		
	print(center)

	# HSV変換
	hsv_image = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

	# 取得した点の中央の明度によりスマホを使っているか判定し，使っているときカウント
	# 誤検出防止のため周囲の明度も使用
	value = 0

	for i in range(-15, 16):
		for j in range(-15, 16):
			value = value + hsv_image[int(center[0]) + i][int(center[1]) + j][2]
	v_ave = value / 961

	if v_ave > 200:
		print("ON, 明度：", v_ave)
		count = count + 5
	else:
		print("OFF, 明度：", v_ave)

	# 検出領域を青い長方形で囲んで表示
	# 中央の位置は赤い円で表示
	result = img
	cv2.rectangle(result,(topleft_x, topleft_y), bottom_right, (255, 0, 0), 2)
	cv2.circle(result, (int(center[0]), int(center[1])), 5, (0, 0, 255))
	cv2.imshow("img", result)

	# カウントが一定以上されると画像を表示
	if count > 50:
		img4 = cv2.imread('studyordie.jpg')
		cv2.imshow('studyordie', img4)
		cv2.moveWindow('studyordie', 555, 0)
		cv2.waitKey(0)
		count = 0
		break

	print("count = ", count)
	count = count - 1
	if count < 0:
		count = 0

	# 1秒待機してEscキーの入力があると終了
	k = cv2.waitKey(1000)
	if k == 27:
		break

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

これを実行すればマッチングの画像と同時に以下のようなコンソールが見られるはずです．
このcountの値が50を超えるときに警告の画像が表示されます．

```
(157.0, 193.0)
OFF, 明度： 159.49739854318418
count =  0
```

```
(225.0, 260.0)
ON, 明度： 203.90426638917793
count =  15
```

ここで明度を求めるときにマッチングの中心を中心とした31×31の正方形での平均としていますが，これは一点のみだと蛍光灯の反射？で大きな値が返されてしまったからです．
使用する環境によってここと，閾値の値は変わると思います．

# exe化
デスクトップのショートカットをダブルクリックするだけで使えるようにしたかったのでexe化をしました．
Pyinstallerが入った状態で

```
pyinstaller caution.py --onefile
```
のコマンドでexe化できます．
exe化した後は用意した画像をdistフォルダ内に入れる必要があるので注意してください．
ショートカットを作成してデスクトップに貼れば終わりです．