この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
概要
こんにちは、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() による輪郭抽出
今回紹介したスクリプト