OpenCVで動体検出をしてみた

概要

こんにちは、yoshimです。
今回は「OpenCV」を使って動体検出をしてみたのでご紹介します。

目次

1.やったこと

タイトルの通り、今回は「OpenCV」を使って動体検出をしました。
具体的には、下記のようにカメラを設置して

棚に手を入れた際に「左右、正面のどこから手を入れたか」というのを検出することを目的としています。
正面から手を入れると...

こんな感じに腕の部分を検出します

左から手を入れると

こんな感じです。

上記のように腕を検出した後、最終的には腕が「左右、正面」のどこからきたのかを結果として返却します。
また、今回作成したスクリプトはこちらにあります。
よかったらご参照ください。

2.処理の要点

処理の要点は、下記の3点です。

2-1.前フレーム画像との差分を抽出

「前フレーム,最新フレーム」間の差分を取ることで、「変化のあった部分=動いた部分」を抽出します。
当初は「前フレーム」と比較することを考えていたのですが、最終的には移動平均(「accumulateWeighted」関数)を使いました。

なので、正確には「蓄積されたフレーム,最新フレーム間の差分」となります。

2-2.腕の領域を抽出

「2-1.前フレーム画像との差分を抽出」の抽出結果から「腕の領域」らしきところを抽出します。
今回は、「領域の面積」が「設定した閾値以上に大きかったら腕である」と仮定して絞り込みました。

2-3.「左右、正面」のどこから腕を出したのかを返却

「2-2.腕の領域を抽出」の結果(腕の領域の座標)から、腕が「左右、正面」のどこから出たのかを返却します。

3.処理内容について少し説明

要点となる部分だけ説明します。
スクリプトはGitにあるので、よかったらこちらもご参照ください。

3-1.フレーム間の画像差分を抽出

フレーム間差分を抽出する前に、画像を「グレースケール」に変換しています。
あくまでも「画像として差分があった部分」を抽出することが目的であるため、グレースケールでも十分であり、また処理をシンプルにできます。
グレースケールに変換した後は、「蓄積されたフレーム,最新フレーム間の差分」を取得し、「frameDelta」という変数に格納しています。

while(True):
    ret, frame = cap.read()    
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # 前フレームを保存
    if avg is None:
        avg = gray.copy().astype("float")
        continue

    # 現在のフレームと移動平均との間の差を計算する
    # accumulateWeighted関数の第三引数は「どれくらいの早さで以前の画像を忘れるか」。小さければ小さいほど「最新の画像」を重視する。
    # http://opencv.jp/opencv-2svn/cpp/imgproc_motion_analysis_and_object_tracking.html
    # 小さくしないと前のフレームの残像が残る
    # 重みは蓄積し続ける。
    cv2.accumulateWeighted(gray, avg, 0.00001)
    frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))

3-2.腕の領域を抽出

「3-1.フレーム間の画像差分を抽出」で「差分のあった部分」が抽出できたので、続いて「腕の領域」を抽出します。
「3-1.フレーム間の画像差分を抽出」の抽出結果は、まだ「差分のあった箇所」くらいの粒度の情報なので、 これを「腕の領域(長方形等)」に変換することが目的です。

まずは、「3-1.フレーム間の画像差分を抽出」の結果から閾値(下記の場合は50)以上変化があった部分について値を255に,それ以外を0に変換して2値画像とします。
続いて、findContoursを使って輪郭を検出し、結果として出力しています。

    # 閾値を設定し、フレームを2値化
    thresh = cv2.threshold(frameDelta, 50, 255, cv2.THRESH_BINARY)[1]
    cv2.imwrite('./thresh.jpg', thresh)

    # 輪郭を見つける
    _, contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)

    # 輪郭を「ある程度以上の大きさのものだけ」に絞り込み
    size = 2000
    list_extracted_contours = extract_contours(contours, size)

    # 輪郭を見つけて画像に出力
    thresh_img = cv2.imread('./thresh.jpg')
    dict_box = get_rect(thresh_img, list_extracted_contours)

3-3.「左右、正面」のどこから腕を出したのかを返却

最後は「腕が左右、正面のどこから来たのか」を結果として返却します。
今回は「腕の領域の座標」が左端、右端、上端のいずれかで切れた場合に結果を返すようにしました。

例えば1.やったことの「正面から手を入れた場合」のような結果がでた場合は、腕の上端が切れている状態なので(y座標が0以下)、「正面から手を入れた」と判断させています。

def get_origin_of_arm(dict_box):
    dict_output = {}
    '''
    dict_box:抽出された短径の座標が格納された辞書型変数。
            下記のような変数を期待しています。
            {0: array([[182, 424],
                    [128,   0],
                    [322, -24],
                    [376, 399]]), 
            1: array([[148, 418],
                    [  0, 418],
                    [  0,   0],
                    [148,   0]])}
    返り値:各領域が「左右正面」のいずれから出てきたかの辞書
    '''
    if len(dict_box) != 0:
        # 抽出された領域全てに対してループ
        for i in dict_box:

            # 4角の座標位置から、腕がどこからきているかを評価
            for j in range(len(dict_box[i])):
                if dict_box[i][j][0] <= 0: # いずれかのx座標が0以下なら、右から手が出ている(カメラで上から見ていることを想定)
                    dict_output[i] = 'right'
                    break
                elif dict_box[i][j][0] >= width_of_img: # いずれかのx座標が設定したサイズの最大値以上なら、左から手が出ている(カメラで上から見ていることを想定)
                    dict_output[i] = 'left'
                    break
                elif dict_box[i][j][1] <= 0: # いずれかのy座標が0以下なら、正面から手が出ている(カメラで上から見ていることを想定)
                    dict_output[i] = 'front'
                else:
                    pass

                # 左右正面のいずれでもない場合は...「わかりませんでした」という結果を返そう
                if j + 1 == len(dict_box[i]) and i not in dict_output:
                    dict_output[i] = 'unknown!!!'

    return dict_output

4.まとめ

今回はOpenCVの機能を使って動体検出をしてみました。
最初は機械学習を使ってやってみようと思っていたのですが、「できるだけ機械学習を使わない方法」を考えてみたら今回のような手法を知ることができました。

画像処理に苦戦しているどなたかの参考になれば幸いです。

5.引用

OpenCV:モーション解析と物体追跡
OpenCV:構造解析と形状ディスクリプタ
OpenCVを利用して動画(カメラ)から動体検知をする方法について
OpenCV - findContours() による輪郭抽出
今回紹介したスクリプト