【Python & OpenCV & OpenGL】フレーム差分による背景除去法で定点カメラの異物検知(動体検知)を判定するプログラムを作ってみた

OpenCV

そもそも何で定点カメラの異物を検知するプログラムを作ろうと思うに至ったかというと、いつの間にか部屋にいる大嫌いなクモの侵入経路を突き止めたいから!!

最終的には、怪しいと思っている玄関に設置して証拠を押さえたいところですが、まずは単純に小さい異物を検知したら映像の左上に赤丸を表示するという物を作ります。

対象がクモなのでかなり小さい異物(動体)を精度よく検知できるかが肝になってきます。

そそたた
そそたた

検知の仕組みに関しては、最もお手軽なフレーム差分による背景除去法を使用しています。

背景に風でゆらめくカーテンなどの動体を含んでいたり、照明変化が大きい場合は、平均背景法などを使って背景を学習する必要がありますが、深夜の玄関を監視するのが目的なので不要かと思われる。

結果

プログラムを実行してキーボードの「s」キーを押すとベースとなる背景を記憶し、検知したら左上に赤丸が表示されます。

「LaQで作ったクモ」、「LaQの最小パーツ」、「紙屑」を試してみたところ、コントラストや輝度をカメラが自動調整していますが、吸収して意図通り検知できています。

背景
LaQで作ったクモ(12cm x 10cm)
LaQの最小パーツ(2cm x 1cm)
紙屑(7mm x 7mm)
そそたた
そそたた

子供にLaQでクモ作ってくれへん?とお願いしたら想像以上のリアルさにちょっと引きました。。

(||゚Д゚)ヒィィィ!

ソースコード

作成したプログラムのソースコードを載せておきます。

開発環境は、MacBook Pro(2016年モデル)+Python3.5.5+OpenCV3.4.2+OpenGL3.1.1a1です。

カメラは、MacBook Pro本体のものを使用しています。

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import cv2
import numpy as np

baseFrame = None
curFrame = None
start = False

cap = cv2.VideoCapture(0)

def draw():
    global start, curFrame, baseFrame

    ret, curFrame = cap.read()
    drawFrame = curFrame.copy()

    if(start == True):
        bf = cv2.cvtColor(baseFrame,cv2.COLOR_BGR2GRAY)
        cf = cv2.cvtColor(curFrame,cv2.COLOR_BGR2GRAY)

        diff = cv2.absdiff(bf, cf)
        ret, diff = cv2.threshold(diff, 35, 255, cv2.THRESH_BINARY)

        kernel = np.ones((5, 5), np.uint8)
        erosion = cv2.erode(diff,kernel,iterations = 1)

        im2, contours, hierarchy = cv2.findContours(erosion, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        #異物を検出したら左上に赤●を描画
        if(len(contours) > 0):
            cv2.circle(drawFrame, (40, 40), 30, (0, 0, 255), -1)

    drawFrame = cv2.cvtColor(drawFrame, cv2.COLOR_BGR2RGB)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, drawFrame.shape[1], drawFrame.shape[0], 0, GL_RGB, GL_UNSIGNED_BYTE, drawFrame)

    glEnable(GL_TEXTURE_2D)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)

    glBegin(GL_QUADS) 
    glTexCoord2d(0.0, 1.0)
    glVertex3d(-1.0, -1.0, 0.0)
    glTexCoord2d(1.0, 1.0)
    glVertex3d(1.0, -1.0, 0.0)
    glTexCoord2d(1.0, 0.0)
    glVertex3d(1.0,  1.0, 0.0)
    glTexCoord2d(0.0, 0.0)
    glVertex3d(-1.0, 1.0, 0.0)
    glEnd()

    glFlush()
    glutSwapBuffers()

def idle():
    glutPostRedisplay()

def keyboard(key, x, y):
    global start, curFrame, baseFrame
    key = key.decode('utf-8')
    if key == 'q':
        exit()
    elif key == 's':
        baseFrame = curFrame.copy()
        start = True
                
if __name__ == "__main__":
    glutInitWindowPosition(0, 0)
    glutInitWindowSize(int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE)
    glutCreateWindow("Camera")
    glutDisplayFunc(draw)
    glutKeyboardFunc(keyboard)
    glutIdleFunc(idle)
    glutMainLoop()

解説

カメラ映像のフレーム描画にはOpenCVでなくOpenGLを使用しています。

何でそんな面倒なことをやっているかというと、そそたたの開発環境でカメラ映像を表示するOpenCVの一般的な方法で実装するとフレームレートが3〜4fpsで使い物にならなかったからです。

↓こんな感じのコードです。

import cv2

cap = cv2.VideoCapture(0)
start = time.time()
while(True):
    ret, frame = cap.read()
    cv2.imshow('camera',frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

遅い原因は、cv2.imshow()に時間がかかっているためでした。

OpenGLでのフレーム描画は次のサイトを参考にさせていただきました。

PythonでOpenCVの画像をOpenGLで表示する - Qiita
はじめにOpenCVは非常に便利な画像処理のライブラリであるが,画像の描画はあまり得意ではないようである。また,imshowで表示すると,waitKey(1)が引数に関係なく15ms程度は少なくと…

それでは、ソースの解説です。

ライブラリインポート、変数宣言、カメラのビデオキャプチャデバイス取得

ここは特に説明不要ですね。

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import cv2
import numpy as np

baseFrame = None
curFrame = None
start = False

cap = cv2.VideoCapture(0)
そそたた
そそたた

Pythonだと変数の宣言が不要なのですが、baseFrame、curFrame、startをグローバル変数として使用したくてこのようにしています。

OpenGLの準備、メインループ関数実行

OpenGLの描画用ウィンドウ作成、ディスプレイコールバック(再描画)、キー入力コールバック、アイドルコールバックを登録してメインループ関数を実行します。

メインループ関数は、描画用ウィンドウが破棄されるまで戻ってきません。

フレーム描画に関しては、描画効率とかを考慮せずIdleコールバックで再描画するようになっています。

「再描画コールバック(draw)→Idleコールバック(idle)→再描画コールバック(draw)→・・・」が無限ループするイメージです。

if __name__ == "__main__":
    glutInitWindowPosition(0, 0)
    glutInitWindowSize(int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)))
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE)
    glutCreateWindow("Camera")
    glutDisplayFunc(draw)
    glutKeyboardFunc(keyboard)
    glutIdleFunc(idle)
    glutMainLoop()

キー入力処理

「q」キーでプログラム終了。

「s」キーで最新のフレームを背景画像のベースとするためコピーを作成して異物検知が始まるようにstartフラグをONにしています。

def keyboard(key, x, y):
    global start, curFrame, baseFrame
    key = key.decode('utf-8')
    if key == 'q':
        exit()
    elif key == 's':
        baseFrame = curFrame.copy()
        start = True

異物検知(動体検知)処理

再描画のタイミングで最新のフレームを取得して描画用にコピーを作成しています。

def draw():
    global start, curFrame, baseFrame

    ret, curFrame = cap.read()
    drawFrame = curFrame.copy()
そそたた
そそたた

コピーを作成している理由は、ここで取得したフレームを背景画像のベースとして使用するため、描画用に加工するものと分けるためです。

フレーム間差分による背景除去法で異物を検知します。

簡単に説明すると、absdiffで差分の絶対値を求めてthresholdの閾値により二値化します。

この状態だと多くの小さいノイズ(小さい差分)が存在するので、erodeにより除去してたものを対象にfindContoursで輪郭を1つ以上抽出したら異物検知と判定して左上に赤丸を描画しています。

    if(start == True):
        bf = cv2.cvtColor(baseFrame,cv2.COLOR_BGR2GRAY)
        cf = cv2.cvtColor(curFrame,cv2.COLOR_BGR2GRAY)

        diff = cv2.absdiff(bf, cf)
        ret, diff = cv2.threshold(diff, 35, 255, cv2.THRESH_BINARY)

        kernel = np.ones((5, 5), np.uint8)
        erosion = cv2.erode(diff,kernel,iterations = 1)

        im2, contours, hierarchy = cv2.findContours(erosion, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        #異物を検出したら左上に赤●を描画
        if(len(contours) > 0):
            cv2.circle(drawFrame, (40, 40), 30, (0, 0, 255), -1)

フレーム描画

ここで、OpenGLを使ってフレームを高速描画しています。

    drawFrame = cv2.cvtColor(drawFrame, cv2.COLOR_BGR2RGB)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, drawFrame.shape[1], drawFrame.shape[0], 0, GL_RGB, GL_UNSIGNED_BYTE, drawFrame)

    glEnable(GL_TEXTURE_2D)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)

    glBegin(GL_QUADS) 
    glTexCoord2d(0.0, 1.0)
    glVertex3d(-1.0, -1.0, 0.0)
    glTexCoord2d(1.0, 1.0)
    glVertex3d(1.0, -1.0, 0.0)
    glTexCoord2d(1.0, 0.0)
    glVertex3d(1.0,  1.0, 0.0)
    glTexCoord2d(0.0, 0.0)
    glVertex3d(-1.0, 1.0, 0.0)
    glEnd()

    glFlush()
    glutSwapBuffers()
そそたた
そそたた

OpenGLの細かい部分は、記載しているとキリがないのでGoogle先生に丸投げします。

クモ侵入経路監視システム構築に向けての課題

最終目標であるクモ侵入経路監視システムを構築するには、次のような課題が山積みであることが分かりました。

  1. 検知時にどのように証拠を残す?
  2. 誤判定を少なくする工夫は不要か?
  3. クモだけを限定して監視する?
  4. 毎フレームで検知処理しているが性能的に問題ないか?
  5. 深夜の玄関で本当に異物の検知が可能か?
  6. MacBook Pro本体のカメラで監視ポイントに設置できる?

コメント

タイトルとURLをコピーしました