Python

ラズパイとOpenCVで動体検知

スポンサーリンク

どうも。まっきー(@makky_study)です。

自作監視カメラの準備として、動体検知の勉強を行いました

自分なりにまとめてみたので動体検知をやろうと思っている方の参考になればと思います。

本記事の内容

  • 動体検知の流れと仕組み(コードと画像で解説)
  • 動体検出方法①(面積差分)
  • 動体検出方法②(ピクセル誤差)

動体検知の流れ

基本の動体検知用のソースコードを置いておきます。

動画の名前を変えるorカメラモードにするだけで試せます。コメントを参考にしてください。

[su_accordion][su_spoiler title="動体検知ソースコード" open="no" style="default" icon="plus" anchor="" anchor_in_url="no" class=""]

import cv2

filepath = "vtest.avi"#動画の名前
cap = cv2.VideoCapture(filepath)
# Webカメラを使うときはこちら
# cap = cv2.VideoCapture(0)
avg = None

while True:
    # 1フレームずつ取得する。
    ret, frame = cap.read()
    if not ret:
        break

    # グレースケールに変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    # 比較用のフレームを取得する
    if avg is None:
        avg = gray.copy().astype("float")
        continue

    # 現在のフレームと移動平均との差を計算
    cv2.accumulateWeighted(gray, avg, 0.6)
    frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))

    # デルタ画像を閾値処理を行う
    thresh = cv2.threshold(frameDelta, 3, 255, cv2.THRESH_BINARY)[1]
    # 画像の閾値に輪郭線を入れる
    contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    frame = cv2.drawContours(frame, contours, -1, (0, 255, 0), 3)

    # 結果を出力
    cv2.imshow("Frame", frame)
    key = cv2.waitKey(30)
    if key == 27:
        break

cap.release()
cv2.destroyAllWindows()

[/su_spoiler]
[/su_accordion]

以下、動体検知の流れです。

  1. 画像をグレースケールに変換
  2. 前フレームを保存
  3. 移動平均を求める
  4. 現在とのフレームと移動平均の差を計算する
  5. 閾値を設定してフレームを2値化(黒と白に分ける)
  6. 閾値処理された画像上の輪郭を見つける
  7. 輪郭を描写する

最終的に図のように、輪郭が描写できます。

以下、詳しく解説していきます。

画像をグレースケールに変換

まず、処理高速化のために画像をグレースケールに変換します。

gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

前フレームを保存

以下のコードで前のフレームを保存します。

if avg is None:
    avg = gray.copy().astype("float")
    continue

移動平均を求める

グレースケールの画像を前画像と比較して移動平均というものを求めています。

cv2.accumulateWeighted(gray, avg, 0.6)
#accumulateWeighted(入力画像、累算器画像、入力画像の重み(alpha))

累算器画像とは前のフレームのことですね。

alpha は更新速度(どのくらいの早さで以前の画像を「忘れる」か)を調節します。alphaの値を調節することでどれだけ残像を残すかを変えられます。

少しわかりずらいかもしれませんが、alphaの値を変えると残像が大きくなっているのが確認できると思います。

現在とのフレームと移動平均の差を計算する

frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))

動体フレーム = |現在のフレーム - 背景|

上記の式により、現在のフレームから背景を引いた差分frameDeltaを求めます。

閾値を設定してフレームを2値化(黒と白に分ける)

thresh = cv2.threshold(frameDelta, 5, 255, cv2.THRESH_BINARY)[1]
#cv2.threshold(入力画像、閾値、閾値以上の値を持つ画素に対して割り当てられる値、閾値処理のタイプ)

カラー画像やモノクロ(グレースケール)画像など、複数の色や明るさで表現された画像を、黒と白の2色だけの画像に変換することを二値化と言います。一般的に「しきい値」という値を用いて、それより明るい点を白(255)、暗い色を黒(0)に置換します。二値化した画像の利用目的に応じ、この「しきい値」を適切な値に設定することが大切になります。(参考Python+OpenCVを利用した二値化処理

閾値処理のタイプは以下の5種類あります。使用しているのはBINARYです。

  • cv2.THRESH_BINARY
  • cv2.THRESH_BINARY_INV
  • cv2.THRESH_TRUNC
  • cv2.THRESH_TOZERO
  • cv2.THRESH_TOZERO_INV

実際に閾値の値を変更することでどれぐらいの感度にするかを調節できます。

閾値=5の時
閾値=30の時

閾値の値を大きくしすぎると、見落とすものも出てきてしまいます。

必要に応じて調節する必要がありますね。

閾値処理された画像上の輪郭を見つける

contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
#cv2.findContours(入力画像、contorretrievalmode 、輪郭検出方法指定))
#出力は輪郭画像,輪郭(リスト型),輪郭の階層情報です

先ほどの画像を見ていただければわかるかと思いますが、OpenCVの輪郭検出は、黒い背景から白い物体の輪郭を検出すると仮定しています。

物体は白で背景は黒です。

cv2.CHAIN_APPROX_SIMPLEは輪郭の検出方法を指定しています。このフラグを設定すると、下図の右側のように輪郭を検出します。

これをすることにより、メモリを節約できます!

cv2.RETR_EXTERNALフラグ等の輪郭の階層情報の説明はややこしいので、詳しく知りたい方はOpenCVのチュートリアルを見てください。

輪郭を描写する

frame = cv2.drawContours(frame, contours, -1, (0, 255, 0), 3)
#cv2.drawContours(入力画像、輪郭(list型)、全輪郭の時-1、色、太さ)で輪郭を描写

これで最初のように、動いている人の輪郭を描写できます。

動体検出方法

色々調べていると、個人的な監視カメラに使うための動体検出方法で参考になったものが2つありましたので、2つとも紹介します。

2つのソースコードで大きく異なる点は動体を検知する際の判定部分です。

マーカーで強調してあるのでそこに注目してソースコードを見てみてください。

ラズパイカメラで行うことを前提としておりますので注意してください。USBカメラでもできるとは思いますが、保証できません。すみません。

動体検出方法① 面積を用いる方法

一つ目は動いた分の面積を用いる方法です。

まずソースコードを載せておきます。

import cv2
import datetime
import time

#画像保存ディレクトリ
save_dir = './image/'
file_suffix = 'detect.jpg'

# カメラ映像を取得
cam = cv2.VideoCapture(0)
#Video Graphics Array(略称:VGA)を設定
cam.set(3, 640) # set video 横
cam.set(4, 480) # set video 高さ

#2値化したときのピクセル値
DELTA_MAX = 255
#ドットの変化を検知する閾値
DOT = 8
#比較用のデータを格納
avg = None

while True:
    # 1フレームずつ取得する。
    ret, frame = cam.read()
    if not ret:
        break
    #画像ファイル名用の時間取得
    date = datetime.datetime.now()
    file_name = str(date) + file_suffix 

    # グレースケールに変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # 比較用のフレームを取得するavgがNoneの場合進むつまり、値があればコピーしていく
    if avg is None:
        avg = gray.copy().astype("float")
        continue

    # 現在のフレームと移動平均との差を計算
    cv2.accumulateWeighted(gray, avg, 0.6)
    frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))

    # デルタ画像を閾値処理を行う 閾値処理された後の二値画像が帰ってくる
    thresh = cv2.threshold(frameDelta, DOT, DELTA_MAX, cv2.THRESH_BINARY)[1]

    # 画像の閾値に輪郭線を入れる 戻り値は(画像、輪郭、階層)
    img,contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    #輪郭の面積を求めてデータサイズを超えるものを見つけた場合検知とする
    max_area = 0
    for cnt in contours:
        area = cv2.contourArea(cnt)#この時の輪郭の値を計算して面積を求める
        if max_area < area:
            max_area = area
        if max_area > 1500: #面積は目的に適した値に調節
            frame = cv2.drawContours(frame, contours, -1, (0, 255, 0), 3)
            cv2.imwrite(save_dir + file_name,frame)
            print("動体検知しました")
       break
        else:
            pass
    
    frame = cv2.drawContours(frame, contours, -1, (0, 255, 0), 3)
    # 結果を出力
    cv2.imshow("Frame", frame)
    key = cv2.waitKey(30)
    if key == 27:
        break

cam.release()
cv2.destroyAllWindows()

cv2.contourArea()を用いて、輪郭の面積を求めます。イメージとしては下の写真のような感じです。

動いた面積のしきい値を自分で設定して、if文を作り、動体を検知する仕組みです。

上の画像はかなり大きな面積なので1万とかいう値になっていますが、実際の防犯カメラは遠くからの撮影となるので1000ぐらいの値でいいと思います。

この方法は比較的わかりやすかったです。

動体検出方法② ピクセルの誤差を用いる方法

画像ピクセルの誤差を検知するという面白い方法もありました。

import cv2
import datetime
import time

#画像保存ディレクトリ
save_dir = './image/'
file_suffix = 'detect.jpg'

# カメラ映像を取得
cam = cv2.VideoCapture(0)
#Video Graphics Array(ビデオ グラフィックス アレイ、略称:VGA)お決まりの設定のような感じ
cam.set(3, 640) # set video width
cam.set(4, 480) # set video height

#2値化したときのピクセル値
DELTA_MAX = 255
#ドットの変化を検知する閾値
DOT = 3
#どのくらいの変化があったか
MOTHON_FACTOR_TH = 0.30
#比較用のデータを格納
avg = None

while True:
    # 1フレームずつ取得する。
    ret, frame = cam.read()
    motion_detected = False
    if not ret:
        break

    #ファイル名用の時間取得
    date = datetime.datetime.now()
    file_name = str(date) + file_suffix 

    # グレースケールに変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    # 比較用のフレームを取得する
    if avg is None:
        avg = gray.copy().astype("float")
        continue

    # 現在のフレームと移動平均との差を計算
    cv2.accumulateWeighted(gray, avg, 0.6)
    frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))

    # デルタ画像を閾値処理を行う 閾値処理された後の二値画像が帰ってくる
    thresh = cv2.threshold(frameDelta, DOT, DELTA_MAX, cv2.THRESH_BINARY)[1]

    #輪郭の面積を求めてデータサイズを超えるものを見つけた場合検知とする
    #モーションファクターを計算する。全体としてどれくらいの割合が変化したか。
    motion_factor = thresh.sum() * 1.0 / thresh.size / DELTA_MAX 
    motion_factor_str = '{:.08f}'.format(motion_factor)

    #モーションファクターがしきい値を超えていれば動きを検知したことにする
    if motion_factor > MOTHON_FACTOR_TH:
        motion_detected = True

    # 動き検出していれば画像を保存する
    if motion_detected  == True:
        #save
        cv2.imwrite(save_dir + file_name, frame)
        print("DETECTED:" + file_name)
        time.sleep(2)

    # ここからは画面表示する画像の処理
    # 画像の閾値に輪郭線を入れる
    img,contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    frame = cv2.drawContours(frame, contours, -1, (0, 255, 0), 3)

    # 結果を出力
    cv2.imshow("Frame", frame)
    key = cv2.waitKey(30)
    if key == 27:
        break

cam.release()
cv2.destroyAllWindows()

この処理方法はとても面白いなと思いました。

二値化処理した画像のピクセル値に変化があった場合動体を検知するという仕組みです。

ちょっとわかりずらいかもしれないので、画像を用いて説明します。

このような変化の程度をMOTION_FACTOR_THという閾値を超えるか超えないかで判断しています。

監視カメラは基本的には定点で用いられると思うのでこの方法も便利ですね。

2種類の方法どちらも動体検知ができるので好きな方を使って監視カメラを作っていきたいと思います。

おわりに

いかがでしたでしょうか。OpenCVを使えば、簡単に動体検知をすることができます。

この機能を応用すれば自宅用監視カメラも自作できますね!

ぜひ試してみてください!

次回はオリジナル監視カメラの作成に挑戦してみたいと思います。

追記(2021年2月)

オリジナル監視カメラを作成しました。

>>ラズパイとOpenCVで監視カメラを自作してみた【前編】

>>ラズパイとOpenCVで監視カメラを自作してみた【後編】

参考

スポンサーリンク

-Python