Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
8
Help us understand the problem. What are the problem?
@seri28

PythonとOpenCVで動体検知をしたい

はじめに

AI、機械学習を勉強するにあたって、前にこちらの本を購入したことがあったのでその中にあったOpenCVでの動体検知をやってみたいと思います。

PythonによるAI・機械学習・深層学習アプリのつくり方

※2020年10月にTensorFlow2に対応した新しいものが出ていました
2020年10月発売版

環境

Anacondaを使ってPython環境を用意します

macOS BigSur 11.2.2
conda 4.9.2
Python 3.8.5
OpenCV 4.5.1.48
コードの実行はターミナルから

Anacondaのインストールは下記を参照
MacOS版Anacondaのインストール

今回は「python_opencv」フォルダをつくってその中にファイルや「img」フォルダを置いています。

OpenCVのテスト

まずはターミナルを開いて、conda環境を使えるように以下のコマンドを実行します

$ conda activate

次にOpenCVをインストールします。

pipでインストールする場合
以下のコマンドだけで完了します。

$ pip install opencv-python

condaでインストールする場合
Anacondaはライブラリの管理にcondaが使われています。
他のライブラリと管理方法を統一させるためにOpenCVもcondaでインストールしようと思ったのですが単純に$ conda install opencvだけではうまくいきませんでした。

teratail python3.7の環境に、condaを使ってopencvをインストールしたいです
上記にあるようにデフォルトの環境(base)とは別の名前の環境をつくってインストールする必要があるようです。

例として「use_cv」という名前の環境をつくってみます。
(ついでにmatplotlibもインストール)

コマンドの説明はこちら→PythonJapan condaコマンド

$ conda create --name use_cv python=3.7
$ conda activate use_cv
$ conda install opencv matplotlib

これでOpenCVのインストールができたのでPCにある適当な画像を読み込んで表示してみましょう。

base(またはuse_cv)環境は有効にしたまま「python_opencv」フォルダに移動してその中のopencv_test.pyを実行します。
(test.jpgは自分のアイコンの画像)

$ cd python_opencv
$ python opencv_test.py
opencv_test.py
import matplotlib.pyplot as plt
import cv2

img = cv2.imread("./img/test.jpg")
plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
plt.show()


ちゃんと画像が表示されました。

なおcvtColor()では色空間をBGRからRGBに変換しています。
OpenCVではカラーコードがBGRの順番でならんでおり、(R:255, G:100, B:0) の色を (B:255, G:100, R:0) として読み込んでしまいます。
そのため、OpenCVで読み込んでそのままmatplotlibで表示させようとすると色が正しく表示されません。

試しにcvtColor()を使わずに表示してみると赤と青が反転した画像になります。

img = cv2.imread("./img/test.jpg")
plt.imshow(img)
plt.show()

notcvt.png

動いている物体を検出する

pixabayから公園を撮影した動画をダウンロードしました。→元の動画はこちら
ezgif.com-gif-maker.gif
歩いている人とリスも写っていますね。
これらの動いているものを赤い四角で囲んで動画内で表示してみます。

全体のコード

move_capt.py
import cv2
import time

movie = cv2.VideoCapture('./movie/park.mp4')

red = (0, 0, 255) # 枠線の色
before = None # 前回の画像を保存する変数
fps = int(movie.get(cv2.CAP_PROP_FPS)) #動画のFPSを取得

while True:
    # 画像を取得
    ret, frame = movie.read()
    # 再生が終了したらループを抜ける
    if ret == False: break
    # 白黒画像に変換
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

    if before is None:
        before = gray.astype("float")
        continue
    #現在のフレームと移動平均との差を計算
    cv2.accumulateWeighted(gray, before, 0.5)
    frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(before))
    #frameDeltaの画像を2値化
    thresh = cv2.threshold(frameDelta, 3, 255, cv2.THRESH_BINARY)[1]
    #輪郭のデータを得る
    contours = cv2.findContours(thresh,
                    cv2.RETR_EXTERNAL,
                    cv2.CHAIN_APPROX_SIMPLE)[0]

    # 差分があった点を画面に描く
    for target in contours:
        x, y, w, h = cv2.boundingRect(target)
        if w < 30: continue # 小さな変更点は無視
        cv2.rectangle(frame, (x, y), (x+w, y+h), red, 2)

    #ウィンドウでの再生速度を元動画と合わせる
    time.sleep(1/fps)
    # ウィンドウで表示
    cv2.imshow('target_frame', frame)
    # Enterキーが押されたらループを抜ける
    if cv2.waitKey(1) == 13: break

cv2.destroyAllWindows() # ウィンドウを破棄

実行結果
ezgif.com-gif-maker (1).gif
小さいリスの動きもちゃんと検出されてます。

各コードの説明

VideoCapture

movie = cv2.VideoCapture('./movie/park.mp4')

今回はカッコ内に動画のパスを入れていますが
movie = cv2.VideoCapture(0)
とすることでPCの内蔵カメラの映像を使えます。
動きの検出もリアルタイムで可能です。
内蔵カメラを使う場合はcv2.destroyAllWindows()の前にmovie.release()を入れてカメラを解放するのを忘れずに。

accumulateWeighted と absdiff

cv2.accumulateWeighted(gray, before, 0.5)
frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(before))

accumulateWeightedでフレームの移動平均値を更新し、absdiffで現在のフレームと移動平均との差を計算しています。

beforeにはgrayの1つ前のフレームだけではなく、それ以前の全てのフレームに重み(第3引数)が加わった情報が蓄積されています。

重みを小さくする
 →以前のフレームの情報を少しだけ引き継ぐ
 →現在のフレームと直近の数フレームとの差を重要視する
 →小さな動きも検知できる
重みを大きくする
 →以前のフレームの情報を多く引き継ぐ
 →現在のフレームとそれ以前のフレームの全体的な動きとの差を見る
 →大きな動きがあった場合のみ検知する

という感じです。

比較動画
weight = 0.05
奥の道路を走る車の動きも検出
weight_0.05.gif
weight = 0.8
一番手前の男性とリスが走り出した時のみ検出
weight_0.8.gif

threshold

thresh = cv2.threshold(frameDelta, 3, 255, cv2.THRESH_BINARY)[1]

ここではframeDeltaのフレーム情報を白と黒の二値画像に変換しています。
詳しくはこちら→OpenCV-Python 画像のしきい値処理

今回のコードではframeDeltaの中で画素値が3より大きい部分を255(白)に置き換えます。

試しにframeDeltaとそれを二値化したthreshの画像を比べてみましょう。

比較動画
frameDelta
うっすらとモヤのような感じです。
framedelta.gif
thresh
モヤの部分がはっきりとした白になって表れました。
threshold.gif

これを行うことでどの部分が動いているのかという輪郭をきれいに捉えることができます。

findContours

#輪郭のデータを得る
contours = cv2.findContours(thresh,
                    cv2.RETR_EXTERNAL,
                    cv2.CHAIN_APPROX_SIMPLE)[0]

# 差分があった点を画面に描く
for target in contours:
    x, y, w, h = cv2.boundingRect(target)
    if w < 30: continue # 小さな変更点は無視
    cv2.rectangle(frame, (x, y), (x+w, y+h), red, 2)

threshの画像を元に輪郭のデータを取得しています。
findContoursの説明はこちら→OpenCV-Python 輪郭: 初めの一歩

contoursは抽出された輪郭のリストで各輪郭はnumpy配列になっています。
その配列をboundingRectで輪郭の4点の位置として受け取り、rectangleで元のフレーム上に赤い四角形として描写します。


今回は動画内で動いているものを赤い四角で囲むところまでやってみました。
次は検出した結果を使って何かやってみたいですね。

ご意見や間違いなどがございましたらコメントよろしくお願いいたします。

参考サイト、書籍

PythonによるAI・機械学習・深層学習アプリのつくり方
Python+OpenCVとWebカメラを使って動体検知する話
OpenCVで手っ取り早く動体検知してみた
OpenCVで動体検出をしてみた
OpenCV-Pythonチュートリアル

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
8
Help us understand the problem. What are the problem?