Google Colaboratory+OpenCVでWebカメラ画像から顔検出

# はじめに
この記事は，OpenCVとGoogle Colaboratory (以下Colab) を使って，PCに接続されている**Webカメラで画像を一枚キャプチャし，顔を検出する**Pythonプログラムを作成した備忘録です．

Colabの詳細は他の情報源が多数存在するので譲りますが，ColabではPython+OpenCVが動作する上に，PCに接続されているWebカメラからの画像ストリームをキャプチャするコードスニペットを利用できます．OpenCV3.3以降の`dnn`モジュールでは学習済みモデルを提供しているため，学習に時間を費やすことなく手軽に物体検出などのプログラムを実行できます．

# 本記事のコードが書かれているColabノートブック
本記事のコードは下記のノートブックの形でも公開されています．

https://colab.research.google.com/drive/1d1paILhYQLis58hm-66y7ZRbezI1bpBZ?usp=sharing

# 参考文献
本記事中のコードは，Colabにより提供されているコードスニペットと下記のFace detectionコードを改変しつつ利用しています．

https://colab.research.google.com/github/dortmans/ml_notebooks/blob/master/face_detection.ipynb#scrollTo=UZp-xlhYI1SL

# 動作環境
2021.08.27時点の最新版です．

* Google Colaboratory https://colab.research.google.com/
* MacBook Air (M1, 2020)
* Chrome 92.0.4515.159 (Official Build) (arm64)

# 解説付きのコード

## 前処理
まず，ヘッダとして様々なパッケージをインポートします．

```python
# Google Driveをマウント
from google.colab import drive
drive.mount('/content/gdrive')

# imshowサポートパッチのインポート
from google.colab.patches import cv2_imshow

# dnn用
from IPython.display import display, Javascript
from google.colab.output import eval_js
from base64 import b64decode

# その他パッケージ
import imutils
import numpy as np
import matplotlib.pyplot as plt
import cv2
```

次に， Webカメラからストリームをキャプチャするコードスニペットです．この部分はGoogle Colabにより提供されています．

```python
def take_photo(filename='photo.jpg', quality=0.8):
  js = Javascript('''
    async function takePhoto(quality) {
      const div = document.createElement('div');
      const capture = document.createElement('button');
      capture.textContent = 'Capture';
      div.appendChild(capture);

      const video = document.createElement('video');
      video.style.display = 'block';
      const stream = await navigator.mediaDevices.getUserMedia({video: true});

      document.body.appendChild(div);
      div.appendChild(video);
      video.srcObject = stream;
      await video.play();

      // Resize the output to fit the video element.
      google.colab.output.setIframeHeight(document.documentElement.scrollHeight, true);

      // Wait for Capture to be clicked.
      await new Promise((resolve) => capture.onclick = resolve);

      const canvas = document.createElement('canvas');
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      canvas.getContext('2d').drawImage(video, 0, 0);
      stream.getVideoTracks()[0].stop();
      div.remove();
      return canvas.toDataURL('image/jpeg', quality);
    }
    ''')
  display(js)
  data = eval_js('takePhoto({})'.format(quality))
  binary = b64decode(data.split(',')[1])
  with open(filename, 'wb') as f:
    f.write(binary)
  return filename
```

ネットワークと学習済みのモデルをダウンロードします．今回はCaffeのモデルを利用します．

:::note
以下の2行はColab，もしくはJupyter Notebook環境特有の記述です．
:::

```python
!wget -N https://raw.githubusercontent.com/opencv/opencv/master/samples/dnn/face_detector/deploy.prototxt
!wget -N https://raw.githubusercontent.com/opencv/opencv_3rdparty/dnn_samples_face_detector_20170830/res10_300x300_ssd_iter_140000.caffemodel
```

## メインプログラム

まず，ストリームから画像を一枚撮影します．下記はColabにより提供されているカメラキャプチャのコードスニペットの改変です．

```python
try:
  filename = take_photo()
  img = cv2.imread(filename)
  print('Saved to {}'.format(filename))
  
  # Show the image which was just taken.
  #display(Image(filename))
except Exception as err:
  # Errors will be thrown if the user does not have a webcam or if they do not
  # grant the page permission to access it.
  print(str(err))
```

次に，画像からblobを作成し，検出器にblobを適用します．

```python
# ネットワークと学習済みモデルをロードする
print("[INFO] loading model...")
prototxt = 'deploy.prototxt'
model = 'res10_300x300_ssd_iter_140000.caffemodel'
net = cv2.dnn.readNetFromCaffe(prototxt, model)

# 幅400画素になるようにリサイズする
img = imutils.resize(img, width=400)
(h, w) = img.shape[:2]
blob = cv2.dnn.blobFromImage(cv2.resize(img, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0))

# 物体検出器にblobを適用する
print("[INFO] computing object detections...")
net.setInput(blob)
detections = net.forward()
```

最後にネットワークの出力を描画し，出力結果を表示して保存します．

```python
for i in range(0, detections.shape[2]):

	# ネットワークが出力したconfidenceの値を抽出する
	confidence = detections[0, 0, i, 2]

	# confidenceの値が0.5以上の領域のみを検出結果として描画する
	if confidence > 0.5:
		# 対象領域のバウンディングボックスの座標を計算する
		box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
		(startX, startY, endX, endY) = box.astype("int")
		# バウンディングボックスとconfidenceの値を描画する
		text = "{:.2f}%".format(confidence * 100)
		y = startY - 10 if startY - 10 > 10 else startY + 10
		cv2.rectangle(img, (startX, startY), (endX, endY), (0, 0, 255), 2)
		cv2.putText(img, text, (startX, y),
			cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 0, 255), 2)

# 出力結果を表示して保存
cv2_imshow(img)
cv2.imwrite("/content/gdrive/My Drive/Works_OpenCV/OpenCV_Colaboratory/dnn_face_out.jpg", img)
```

# 動作結果
confidenceが高すぎて逆に怪しい結果ですが．
![dnn_face_out.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1942789/8f89c4e8-91be-ef46-3d2d-b014de3d6e4b.jpeg)

# 今後の改善予定

* 動画への対応
* 他のモデルの利用