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() による輪郭抽出
今回紹介したスクリプト