United Statesからアクセスのようです。言語設定をEnglishに切り替えますか?
Switch to English site
Skip to main content

ラズベリーパイでモニタリング!!

この記事ではRaspberry Pi 4、カメラ、OpenCV、C++言語を用いた画像処理の方法を紹介します。本記事では主にRaspberry Pi 4スターターキットを使用します。まだお求めでない方はこの記事に掲載されているリンクからお求め頂けます。

部品リスト

Qty Product 品番
1 Raspberry Pi 4 Model B Starter Kit 206-7510
1 Raspberry Pi Camera Module V2 913-2664

突然、大事にとっておいたお菓子が食べられた事などはありませんか?ビスケットがたった数日でなくなるのを不思議に思ったことはありませんか?そんな不思議を解決するのがRaspberry Piです!お菓子ではありませよ!!お菓子みたいに小さいコンピュータです。Raspberry Pi用カメラモジュールをRaspberry Pi 4に接続しプログラムを書くことでそんな“お菓子泥棒”を見つけることができます。今回はそんなお菓子泥棒をどのように見つけるのかを紹介します。

もちろんRaspberry Pi 4では他にもたくさんのアプリケーションや開発が可能です。その他のラズパイプロジェクトに関してはこちらを参照してみてください。この記事では「Raspberry Pi 4を用いてカメラで撮影したデジタル画像をどのように処理できるのか」をテーマにしています。Open CVライブラリやC++を使ったプログラミングなど”はじめて”触れる方に読んでもらえたらと思います。

Raspberry Pi 4のセットアップ

この記事での”絶対的な主役”はRaspberry Pi 4 (206-7510) スターターキットです。電源ACアダプタ・Micro HDMI-HDMIケーブル2本・OS入りSDカード、ヒートシンク、ファン内蔵ケースなど使い始めるのに必要なものがすべて含まれています。スターターキットのセットアップ手順についてはこのOKdo公式ガイドに従ってください. 

私がRaspberry Pi 4と初めて出会ったのは2016年、学部の学期プロジェクトを行っていた時でした。正直に言ってボードのセットアップはかなり苦労しました。オペレーティングシステムとプロジェクトに必要なライブラリをすべてインストールするだけで約2週間かかったことを覚えています。しかし紹介しているスターターキットにはRasbianオペレーティングシステム(NOOBS v3.1.1)が事前インストールされた32GB Micro SDカードが含まれていますので私が苦労したようにはならないでしょう。

2019-12-19-191902_1920x1080_scrot_576cea4b708b5f670f191664f8fd86d5d4ba7746.png

ボードでの操作の多くは画面左上にあるターミナルを通じて行います。ライブラリやソフトのインストールを開始する前に次のコマンドを実行します。このコマンドはラズパイのパッケージを最新の状態にするものです。

$ sudo apt-get update
$ sudo apt-get upgrade

上記コマンドを実行しアップデートがあった場合は再起動が必要です。以下が再起動のコマンドです。

sudo reboot

ボードは自動的にWiFiに繋がりますが接続に問題が発生する場合はraserrypi.orgに掲載されているこちらの手順書に従って解決してみてください。

OpenCVライブラリとCodeBlockのインストール

OpenCV (オープンソースのコンピュータビジョンライブラリ)には数百の画像処理アルゴリズムが含まれており顔認識・ジェスチャー認識・テキスト認識・物体検出・機械学習をはじめとする様々な用途に使用できます。OpenCVライブラリはC++・Python・Matlabを用いたプログラミングが使用できます。

Raspberry Pi 4にOpenCVをインストールする方法はすでにオンライン上でたくさん見つけることができますので検索してみてください。注意すべき点はRaspberry Pi 4にインストールするOpenCVやOSのバージョンだけです。ここではOpenCV 4.1.2をインストールします。スターターキットに含まれるRaspberry Piには前述したようにNOOBS 3.1.1が事前にインストールされていますのでOpenCVをインストールするだけで大丈夫です。個人的にはQ-Engineeringのこちらの記事を見て作業してみるとよいと思います。 今回は記事にあるように仮想環境はインストールしませんでした。ですがそれ以外は上記の記事と同じ手順に従っています。途中クラッシュがありましたが3~4回の試行を経てインストールできました。

Raspberry Pi 4にOpenCVをインストールしたらコードをC++プログラミング言語で書き、デバッグするためのIDEツールをインストールします。私はCode::Blocksという無償のIDEを選びました。もし同じIDEを使用したい場合は次のコマンドを実行してください。

sudo apt-get install codeblocks

Raspberry Piカメラからのビデオキャプチャ

これでRaspberry Pi 4上でOpenCVライブラリを用いてC++コードを実行する準備が整いました。簡単な例としてカメラからのビデオストリーミングをしてみましょう。下記のコードは公式のOpenCVドキュメンテーションに記載されているものです。このドキュメンテーションではVideoCaptureクラスを使用してビデオをキャプチャする方法を説明しています。このコードは後で1行ずつ説明します。

#include <stdio.h>
#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/videoio/videoio_c.h>

using namespace cv;
using namespace std;

int main(int argv, char** argc)
{
    Mat frame;
    VideoCapture cap(0);
   
    if (!cap.isOpened()){
        cerr << "ERROR! Unable to open camera\n";
        return -1;
    }
    cout << "Start grabbing" << endl
        << "Press any key to terminate" << endl;

    for (;;)
    {
        cap.read(frame);
        if (frame.empty()){
            cerr << "ERROR! Blank frame grabbed \n";
            break;
            }
        imshow("Live", frame);
        if (waitKey (5) >= 0)
            break;
    }
    return 0;
}

C++ではまずすべての必要なディレクトリをコード内に"include" する必要があります。通常の<stdio.h>は勿論ですがそのほかに3つのOpenCV関連ライブラリ<opencv2/core.hpp>、<opencv2/highgui.hpp>、<opencv2/videoio/videoio_c.h>を追加します。これらのライブラリはOpenCVのコア・ユーザーインターフェース・ビデオキャプチャ機能に対応します。

OpenCVには数多くの関数とクラスが含まれておりいずれにもcv名前空間(namespace)を通してアクセスします。

using namespace cv;

main関数ではまず着信フレームをMatクラスとして初期化します。これにより画像をマトリックスとして扱うことができます。先に述べたようにカメラのビデオストリーミングには VideoCapture クラスを使用します。次の構文を使い VideoCapture オブジェクトを作成します。

    VideoCapture cap(0);

capはオブジェクト名で0はカメラIDです。OpenCVでは0から始まる整数でカメラを指定します。例えば0が1台目のデバイスを表します。この記事ではRaspberry Piカメラ (913-2664) を使用していますがこのカメラはOKdoでも入手できます。クラスやオブジェクトの作成に慣れていない、不安を感じる場合はこちらのリソースを参照してください。 

プログラムによってはカメラが正常に開いたかどうかを確認したい場合がありますがその際は下記の様にブール型のisOpenedを使用します。初期化が行われなかった場合はエラーメッセージが出力されます。

if (!cap.isOpened()){
        cerr << "ERROR! Unable to open camera\n";
        return -1;
    }
    cout << "Start grabbing" << endl
        << "Press any key to terminate" << endl;

個々のフレームにアクセスするにはVideoCapture.read関数を使用します。ただしここでは1つのフレームだけでなくフレームのシーケンス(ビデオ)にするためにループを行う必要があります。使用するループはfor/while/do-whileのいずれで問題ありません。.empty 関数を使用するとキャプチャしているフレームから空のフレームを除外できます。そしてループ内の imshow 関数を使用するとビデオフレームが表示できます。

for (;;)
    {
        cap.read(frame);
        if (frame.empty()){
            cerr << "ERROR! Blank frame grabbed \n";
            break;
            }
        imshow("Live", frame);
        if (waitKey (5) >= 0)
            break;
    }

背景差分とモーション検出

カメラからビデオフレームを取得することはできましたでしょうか?では本格的にお菓子泥棒を見つけていきましょう。1つの方法としてビスケットの缶の近くで動くものを検出しそれを録画するようRaspberry Piに頼む(命令する)方法が挙げられます。この方法ではごく基本的なモーション検出およびトラッキングアルゴリズムを使用します。

下のコードでは今説明したことを実行するもの物です。詳しいプロセスを下に記しました:

  • まずカメラからビデオフレームをキャプチャします
  • 次に背景差分を実行してその場面で動く物体を検出します
  • 次に検出した動く物体の輪郭を見つけます
  • 輪郭の範囲をプリセット値と比較し値が小さい、つまり小規模な変化(光の変化など)を考慮しないようにします
  • 最後に動く物体の範囲に境界線を引き、動体物の正体を特定します
#include <iostream>
#include <sstream>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/video.hpp>
using namespace cv;
using namespace std;

vector<vector<Point> > contours;
vector<Vec4i> hierarchy;

int main(int argc, char* argv[])
{

    //create Background Subtractor objects
    Ptr<BackgroundSubtractor> pBackSub;
    pBackSub = createBackgroundSubtractorMOG2();

    VideoCapture capture(0);
    if (!capture.isOpened()){
        //error in opening the video input
        cerr << "Unable to open: " << endl;
        return 0;
    }
    Mat frame, fgMask;
    while (true) {
        capture >> frame;
        if (frame.empty())
            break;
        //update the background model
        pBackSub->apply(frame, fgMask);
        
        imshow("FG Mask", fgMask);

        RNG rng(12345);
        findContours(fMask, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE,Point(0, 0));

        vector<Rect>boundRect (contours.size());
        vector<vector<Point> > contours_poly( contours.size() );

        for (int i = 0; i < contours.size();i++) {
            if( contourArea(contours[i])< 500)
            {
             continue;
            }
            putText(frame, "Motion Detected", Point(10,20), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(0,0,255),2);
            approxPolyDP( contours[i], contours_poly[i], 3, true );
            boundRect[i] = boundingRect( contours_poly[i] );
            Scalar color = Scalar( rng.uniform(0, 256), rng.uniform(0,256), rng.uniform(0,256) );
            rectangle( frame, boundRect[i].tl(), boundRect[i].br(), color, 2 );
            }

        imshow("Frame", frame);
        int keyboard = waitKey(30);
        if (keyboard == 'q' || keyboard == 27)
            break;
    }
    return 0;
}

背景差分は動く物体があるかどうかを判別するために用いられるごく一般的な技術です。何もない状態の背景(前景マスク)を記録しその情報と照らし合わせることで動く物体のバイナリイメージを作成します。

Background_Subtraction_Tutorial_Scheme_512f93151992e77545b0ec42e1e1822a6b05bd4c.png

Background subtraction (docs.opencv.org)

OpenCVクラス内のBackgroundSubtractorクラスには背景差分を実行するためのアルゴリズムがいくつか含まれています。ここではMOG2と呼ばれるアプローチと対応するBackgroundSubtractorMOG2クラスを使用します。 このMOG2の詳細についてはOpenCVのドキュメンテーションを参照してください。

 今回のシミュレーションにはデフォルトのパラメータを使用しました。

    Ptr<BackgroundSubtractor> pBackSub;
    pBackSub = createBackgroundSubtractorMOG2();

前景マスクがFG Maskウインドウに表示されます。人の目では前景マスクを見れば物体の輪郭がどのようになっているかを自然に見分けることができます。しかしコンピュータ(ここではラズパイ)では輪郭を認識するための特定の操作が必要です。findContoursクラスにより8ビットのバイナリイメージを取り込みその輪郭を出力できます。下のコードは外側の輪郭のみを選択しシンプルな鎖近似メソッドを作成します。

findContours(fMask, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE,Point(0, 0));

コードの次のセクションでは検出したそれぞれの輪郭の領域(面積)を見つける指示をします。領域(面積)が特定の値(今回のケースでは500)よりも小さい場合その輪郭は無視されます。動く物体を特定するためには領域が大きい輪郭のみに注目する必要があります。検出された輪郭が小さいものは単に照明が変化しただけかもしれないためです。

for (int i = 0; i < contours.size();i++) {
            if( contourArea(contours[i])< 500)
            {
             continue;
            }
            putText(frame, "Motion Detected", Point(10,20), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(0,0,255),2);
            approxPolyDP( contours[i], contours_poly[i], 3, true );
            boundRect[i] = boundingRect( contours_poly[i] );
            Scalar color = Scalar( rng.uniform(0, 256), rng.uniform(0,256), rng.uniform(0,256) );
            rectangle(frame, boundRect[i].tl(), boundRect[i].br(), color, 2 );
            }

検出した物体を追跡するにはその周囲を長方形で囲みます。まずいくつかの折れ線によってある程度正確に輪郭を囲みます。これにはapproxPolyDPクラスを使用します。その後boundingRectを使用して折れ線を囲む領域の最小の大きさを計算します。最後にこの計算に従って長方形が作図されます。

Raspberry Pi 4上でのC++/OpenCVを用いた物体検出およびトラッキングの短いビデオを用意しました。Raspberry Piのケースに取り付けたファンとBluetoothスピーカーのせいで雑音が入っていますが無視してください。ご覧の通り私の手がカメラ付近に現れるたびに右側のウインドウに「Motion detected(動作が検出されました)」というメッセージが表示されます。バイナリイメージが表示されている左のウインドウは背景差分によって作成された前景マスクです。

まとめ

Raspberry Piは最近、第4世代へとアップグレードし性能も初期に比べ大幅に向上しました。皆さんは特にストレスを感じずにRaspberry Pi 4を使えるでしょう。実際、私はこの記事をRaspberry Piから書いていますし他方ではコードも書いています。OpenCVライブラリは大量の演算リソースを消耗しますがコードの実行時間は特別な処理を実行しない限りは最大で4秒です。と言っても私はMatlabには馴染みがるもののC++にはあまり詳しくないため「特別な処理」を実行したくてもできないのです...。そこで私から皆さんへ情報共有をお願いしたいです。私のような初心者がC++/OpenCVを学習するためにお勧めのノウハウなどがあれば下にコメント投稿お願いします。初心者の方は次回どのようなコンピュータビジョンのトピックを取り上げてほしいかご希望をお知らせください。

では!!

I am an electronics engineer turned data engineer who likes creating content around IoT, machine learning, computer vision and everything in between.