Google Colaboratory+OpenCVでWebカメラ画像からリアルタイム顔検出

# はじめに

この記事は下記の**リアルタイム動作版**です．一部の説明を省略しています．

https://qiita.com/uranishi/items/85184baf1df33192e00a

# 本記事のコードが書かれているColabノートブック

本記事のコードは下記のノートブックでも公開されています．

https://colab.research.google.com/drive/17zBbdEf6AkBnkOgfKYMPovRg-eetqv-m?usp=sharing

# 動作環境

2022.11.25時点の最新版です．

- Google Colaboratory https://colab.research.google.com/
- MacBook Air (M2, 2022)
- Chrome 107.0.5304.121（Official Build） （arm64）

# コードの紹介

本記事では，Google Colaboratory (以下Colab) でWebカメラをリアルタイムキャプチャし，フレームごとに`dnn`モジュールにより顔検出を適用するプログラムを紹介しています．本記事のコードは，下記の記事を大いに参考にしております．ありがとうございます．

https://qiita.com/a2kiti/items/f32de4f51a31d609e5a5

ネットワークと学習済みのモデルを入手します．今回はColab上でコマンドを実行する場合の例を挙げます．

```terminal
!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
# 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

import IPython
from google.colab import output
from PIL import Image
from io import BytesIO
import base64

def face_detection(_img):
	# 幅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()

	# confidenceが0.5を超える領域をマーク
	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)
	 
	return _img

def run(img_str):
    #decode to image
    decimg = base64.b64decode(img_str.split(',')[1], validate=True)
    decimg = Image.open(BytesIO(decimg))
    decimg = np.array(decimg, dtype=np.uint8); 
    decimg = cv2.cvtColor(decimg, cv2.COLOR_BGR2RGB)

    out_img = face_detection(decimg)

    #encode to string
    _, encimg = cv2.imencode(".jpg", out_img, [int(cv2.IMWRITE_JPEG_QUALITY), 80])
    img_str = encimg.tobytes()
    img_str = "data:image/jpeg;base64," + base64.b64encode(img_str).decode('utf-8')
    return IPython.display.JSON({'img_str': img_str})

output.register_callback('notebook.run', run)

def use_cam(quality=0.8):
  js = Javascript('''
    async function useCam(quality) {
      const div = document.createElement('div');
      document.body.appendChild(div);
      //video element
      const video = document.createElement('video');
      video.style.display = 'None';
      const stream = await navigator.mediaDevices.getUserMedia({video: true});
      div.appendChild(video);
      video.srcObject = stream;
      await video.play();

      //canvas for display. frame rate is depending on display size and jpeg quality.
      display_size = 500 
      const src_canvas = document.createElement('canvas');
      src_canvas.width  = display_size;
      src_canvas.height = display_size * video.videoHeight / video.videoWidth;
      const src_canvasCtx = src_canvas.getContext('2d');
      src_canvasCtx.translate(src_canvas.width, 0);
      src_canvasCtx.scale(-1, 1);
      div.appendChild(src_canvas);

      const dst_canvas = document.createElement('canvas');
      dst_canvas.width  = src_canvas.width;
      dst_canvas.height = src_canvas.height;
      const dst_canvasCtx = dst_canvas.getContext('2d');
      div.appendChild(dst_canvas);

      //exit button
      const btn_div = document.createElement('div');
      document.body.appendChild(btn_div);
      const exit_btn = document.createElement('button');
      exit_btn.textContent = 'Exit';
      var exit_flg = true
      exit_btn.onclick = function() {exit_flg = false};
      btn_div.appendChild(exit_btn);

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

      var send_num = 0
      // loop
      _canvasUpdate();
      async function _canvasUpdate() {
            src_canvasCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, src_canvas.width, src_canvas.height);     
            if (send_num<1){
                send_num += 1
                const img = src_canvas.toDataURL('image/jpeg', quality);
                const result = google.colab.kernel.invokeFunction('notebook.run', [img], {});
                result.then(function(value) {
                    parse = JSON.parse(JSON.stringify(value))["data"]
                    parse = JSON.parse(JSON.stringify(parse))["application/json"]
                    parse = JSON.parse(JSON.stringify(parse))["img_str"]
                    var image = new Image()
                    image.src = parse;
                    image.onload = function(){dst_canvasCtx.drawImage(image, 0, 0)}
                    send_num -= 1
                })
            }
            if (exit_flg){
                requestAnimationFrame(_canvasUpdate);   
            }else{
                stream.getVideoTracks()[0].stop();
            }
      };
    }
    ''')
  display(js)
  data = eval_js('useCam({})'.format(quality))

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

use_cam()
```

# 動作結果
(顔検出部でなく) カメラキャプチャ部がフレームレートに影響しています．M2 MacBook Airでは概ね1fps程度でした．
![face_dnn_realtime.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1942789/78db00e7-31a5-dfc5-6dfa-1bff9b8b89c9.gif)