【Raspberry Pi 2】 RasPiカメラモジュールとOpenCVで顔認識

前回の記事(【Raspberry Pi 2】 RasPiカメラモジュールをOpenCVで使用する)にて、RaspberryPiカメラモジュールをOpenCVから使用するテストを行いました。

今回は定番の顔認識を行ってみましたので、手順などを掲載します。


OpenCV2.4.11のインストール

今回はOpenCVのバージョンは2015年2月26日にリリースされたばかりの2.4.11を使用しました。

2.4.11のインストール手順は前回と同じでOKでした。
インストール手順は下記になります。詳しくは前回の記事を参照ください。

# wget http://sourceforge.net/projects/opencvlibrary/files/opencv-unix/2.4.11/opencv-2.4.11.zip
# unzip opencv-2.4.11.zip
# cd opencv-2.4.11
# mkdir build
# cd build
# cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local -D BUILD_NEW_PYTHON_SUPPORT=ON -D BUILD_EXAMPLES=ON ..
# make -j 4
# make install
# ldconfig

なお、前回の記事で、1時間でコンパイルが終わったと書きましたが、今回は2時間かかりました。もしかすると、前回の1時間は見誤ったかもしれません。

USBカメラでサンプルの顔認識を動かしてみる

まずはUSBカメラで顔認識をしてみます。OpenCVに付属している顔認識のサンプルは以下になります。

opencv-2.4.11/samples/c/facedetect.cpp

まずは試しにそのままビルドしてみます。(※下記例では顔認識のサンプル以外もビルドされます)

# cd ~/opencv-2.4.11/samples/c
# chmod +x build_all.sh
# ./build_all.sh

JPG画像で顔認識するには以下のように実行します。

# ./facedetect --scale=1.5 lena.jpg

USBカメラの映像で顔認識するには以下のように実行します。

# ./facedetect --scale=4

なお、scaleのパラメータは1以上を指定すると画像を縮小して処理を行うため、処理が速くなります。
あまり大きくしすぎると認識しなくなるため、画像の中の顔のサイズと相談(※)ですが、1.5から4ぐらいが良いようです。
※顔のサイズが大きい場合はscaleを大きくできます。

RasPiカメラモジュールで顔認識

さて、いよいよRaspberryPiカメラモジュールで顔認識してみます。

なお、raspicam_cvライブラリが前回の手順でインストールされているものとします。

顔認識のサンプルソースをコピーします。

# mkdir ~/facedetect
# cp ~/opencv-2.4.11/samples/c/facedetect.cpp ~/facedetect/facedetect_raspicam.cpp

raspicam_cvライブラリをコピーします。

# cp ~/git/robidouille/raspicam_cv/RaspiCamCV.h ~/facedetect/
# cp ~/git/robidouille/raspicam_cv/libraspicamcv.a ~/facedetect/

顔認識のサンプルソースを以下のように変更します。★の個所が変更箇所です。
facedetect_raspicam.cpp

#include "opencv2/objdetect/objdetect.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"

#include <cctype>
#include <iostream>
#include <iterator>
#include <stdio.h>

// --------------------------------------------------------------
// ★raspicam対応
// --------------------------------------------------------------
#include "RaspiCamCV.h"


using namespace std;
using namespace cv;

static void help()
{
    cout << "\nThis program demonstrates the cascade recognizer. Now you can use Haar or LBP features.\n"
            "This classifier can recognize many kinds of rigid objects, once the appropriate classifier is trained.\n"
            "It's most known use is for faces.\n"
            "Usage:\n"
            "./facedetect [--cascade=<cascade_path> this is the primary trained classifier such as frontal face]\n"
               "   [--nested-cascade[=nested_cascade_path this an optional secondary classifier such as eyes]]\n"
               "   [--scale=<image scale greater or equal to 1, try 1.3 for example>]\n"
               "   [--try-flip]\n"
               "   [filename|camera_index]\n\n"
            "see facedetect.cmd for one call:\n"
            "./facedetect --cascade=\"../../data/haarcascades/haarcascade_frontalface_alt.xml\" --nested-cascade=\"../../data/haarcascades/haarcascade_eye.xml\" --scale=1.3\n\n"
            "During execution:\n\tHit any key to quit.\n"
            "\tUsing OpenCV version " << CV_VERSION << "\n" << endl;
}

void detectAndDraw( Mat& img, CascadeClassifier& cascade,
                    CascadeClassifier& nestedCascade,
                    double scale, bool tryflip );

string cascadeName = "../../data/haarcascades/haarcascade_frontalface_alt.xml";
string nestedCascadeName = "../../data/haarcascades/haarcascade_eye_tree_eyeglasses.xml";

int main( int argc, const char** argv )
{
    // --------------------------------------------------------------
    // ★raspicam対応
    // --------------------------------------------------------------
    //CvCapture* capture = 0;
    RaspiCamCvCapture* capture = 0;
    
    RASPIVID_CONFIG * config = (RASPIVID_CONFIG*)malloc(sizeof(RASPIVID_CONFIG));
    config->width=640;
    config->height=480;
    config->bitrate=0;  // zero: leave as default
    config->framerate=0;
    config->monochrome=0;
    // --------------------------------------------------------------
    
    Mat frame, frameCopy, image;
    const string scaleOpt = "--scale=";
    size_t scaleOptLen = scaleOpt.length();
    const string cascadeOpt = "--cascade=";
    size_t cascadeOptLen = cascadeOpt.length();
    const string nestedCascadeOpt = "--nested-cascade";
    size_t nestedCascadeOptLen = nestedCascadeOpt.length();
    const string tryFlipOpt = "--try-flip";
    size_t tryFlipOptLen = tryFlipOpt.length();
    string inputName;
    bool tryflip = false;


    // --------------------------------------------------------------
    // ★FPS対応
    // --------------------------------------------------------------
    #define FPS_SAMPLING_SIZE 50
    double fps_sampling[FPS_SAMPLING_SIZE];
    int i;
    for (i=0; i<FPS_SAMPLING_SIZE; i++) {
        fps_sampling[i] = 0;
    }
    double freq = cvGetTickFrequency();
    int fps_idx = 0;

    int64 t_pre = cvGetTickCount();
    int64 pre_ping_tick = t_pre;
    // -------------------------------------------
    
    
    help();

    CascadeClassifier cascade, nestedCascade;
    double scale = 1;

    for( int i = 1; i < argc; i++ )
    {
        cout << "Processing " << i << " " <<  argv[i] << endl;
        if( cascadeOpt.compare( 0, cascadeOptLen, argv[i], cascadeOptLen ) == 0 )
        {
            cascadeName.assign( argv[i] + cascadeOptLen );
            cout << "  from which we have cascadeName= " << cascadeName << endl;
        }
        else if( nestedCascadeOpt.compare( 0, nestedCascadeOptLen, argv[i], nestedCascadeOptLen ) == 0 )
        {
            if( argv[i][nestedCascadeOpt.length()] == '=' )
                nestedCascadeName.assign( argv[i] + nestedCascadeOpt.length() + 1 );
            if( !nestedCascade.load( nestedCascadeName ) )
                cerr << "WARNING: Could not load classifier cascade for nested objects" << endl;
        }
        else if( scaleOpt.compare( 0, scaleOptLen, argv[i], scaleOptLen ) == 0 )
        {
            if( !sscanf( argv[i] + scaleOpt.length(), "%lf", &scale ) || scale < 1 )
                scale = 1;
            cout << " from which we read scale = " << scale << endl;
        }
        else if( tryFlipOpt.compare( 0, tryFlipOptLen, argv[i], tryFlipOptLen ) == 0 )
        {
            tryflip = true;
            cout << " will try to flip image horizontally to detect assymetric objects\n";
        }
        else if( argv[i][0] == '-' )
        {
            cerr << "WARNING: Unknown option %s" << argv[i] << endl;
        }
        else
            inputName.assign( argv[i] );
    }

    if( !cascade.load( cascadeName ) )
    {
        cerr << "ERROR: Could not load classifier cascade" << endl;
        help();
        return -1;
    }

    if( inputName.empty() || (isdigit(inputName.c_str()[0]) && inputName.c_str()[1] == '\0') )
    {
        // --------------------------------------------------------------
        // ★raspicam対応
        // --------------------------------------------------------------
        //capture = cvCaptureFromCAM( inputName.empty() ? 0 : inputName.c_str()[0] - '0' );
        capture = (RaspiCamCvCapture *) raspiCamCvCreateCameraCapture2(0, config); 
        // --------------------------------------------------------------

        int c = inputName.empty() ? 0 : inputName.c_str()[0] - '0' ;
        if(!capture) cout << "Capture from CAM " <<  c << " didn't work" << endl;
        
    }
    else if( inputName.size() )
    {
        image = imread( inputName, 1 );
        if( image.empty() )
        {
            // --------------------------------------------------------------
            // ★raspicam対応
            // --------------------------------------------------------------
            //capture = cvCaptureFromAVI( inputName.c_str() );
            
            if(!capture) cout << "Capture from AVI didn't work" << endl;
        }
    }
    else
    {
        image = imread( "lena.jpg", 1 );
        if(image.empty()) cout << "Couldn't read lena.jpg" << endl;
    }

    cvNamedWindow( "result", 1 );

    if( capture )
    {
        cout << "In capture ..." << endl;
        for(;;)
        {
            // -------------------------------------------
            // ★raspicam対応
            // -------------------------------------------
            //IplImage* iplImg = cvQueryFrame( capture );
            IplImage* iplImg = raspiCamCvQueryFrame( capture );
            
            // -------------------------------------------
            // ★FPS対応
            // -------------------------------------------
            int64 t = cvGetTickCount();
            fps_sampling[fps_idx] = (t - t_pre) / freq;
            t_pre = t;
            fps_idx++;
            if (fps_idx>=FPS_SAMPLING_SIZE) {
                fps_idx = 0;
            }
            double total = 0;
            for (i=0; i<FPS_SAMPLING_SIZE; i++) {
                total += fps_sampling[i];
            }
            double fps = 0;
            if (total != 0) {
                fps = FPS_SAMPLING_SIZE * 1000 * 1000/ total;
            }
            printf("FPS: %.0f\n", fps);
            // -------------------------------------------
            
            frame = iplImg;
            if( frame.empty() )
                break;
            if( iplImg->origin == IPL_ORIGIN_TL )
                frame.copyTo( frameCopy );
            else
                flip( frame, frameCopy, 0 );

            detectAndDraw( frameCopy, cascade, nestedCascade, scale, tryflip );

            if( waitKey( 10 ) >= 0 )
                goto _cleanup_;
        }

        waitKey(0);

_cleanup_:

        // -------------------------------------------
        // ★raspicam対応
        // -------------------------------------------
        //cvReleaseCapture( &capture );
        raspiCamCvReleaseCapture(&capture);
    }
    else
    {
        cout << "In image read" << endl;
        if( !image.empty() )
        {
            detectAndDraw( image, cascade, nestedCascade, scale, tryflip );
            waitKey(0);
        }
        else if( !inputName.empty() )
        {
            /* assume it is a text file containing the
            list of the image filenames to be processed - one per line */
            FILE* f = fopen( inputName.c_str(), "rt" );
            if( f )
            {
                char buf[1000+1];
                while( fgets( buf, 1000, f ) )
                {
                    int len = (int)strlen(buf), c;
                    while( len > 0 && isspace(buf[len-1]) )
                        len--;
                    buf[len] = '\0';
                    cout << "file " << buf << endl;
                    image = imread( buf, 1 );
                    if( !image.empty() )
                    {
                        detectAndDraw( image, cascade, nestedCascade, scale, tryflip );
                        c = waitKey(0);
                        if( c == 27 || c == 'q' || c == 'Q' )
                            break;
                    }
                    else
                    {
                        cerr << "Aw snap, couldn't read image " << buf << endl;
                    }
                }
                fclose(f);
            }
        }
    }

    cvDestroyWindow("result");
    
    return 0;
}

void detectAndDraw( Mat& img, CascadeClassifier& cascade,
                    CascadeClassifier& nestedCascade,
                    double scale, bool tryflip )
{
    int i = 0;
    double t = 0;
    vector<Rect> faces, faces2;
    const static Scalar colors[] =  { CV_RGB(0,0,255),
        CV_RGB(0,128,255),
        CV_RGB(0,255,255),
        CV_RGB(0,255,0),
        CV_RGB(255,128,0),
        CV_RGB(255,255,0),
        CV_RGB(255,0,0),
        CV_RGB(255,0,255)} ;
    Mat gray, smallImg( cvRound (img.rows/scale), cvRound(img.cols/scale), CV_8UC1 );

    cvtColor( img, gray, CV_BGR2GRAY );
    resize( gray, smallImg, smallImg.size(), 0, 0, INTER_LINEAR );
    equalizeHist( smallImg, smallImg );

    t = (double)cvGetTickCount();
    cascade.detectMultiScale( smallImg, faces,
        1.1, 2, 0
        //|CV_HAAR_FIND_BIGGEST_OBJECT
        //|CV_HAAR_DO_ROUGH_SEARCH
        |CV_HAAR_SCALE_IMAGE
        ,
        Size(30, 30) );
    if( tryflip )
    {
        flip(smallImg, smallImg, 1);
        cascade.detectMultiScale( smallImg, faces2,
                                 1.1, 2, 0
                                 //|CV_HAAR_FIND_BIGGEST_OBJECT
                                 //|CV_HAAR_DO_ROUGH_SEARCH
                                 |CV_HAAR_SCALE_IMAGE
                                 ,
                                 Size(30, 30) );
        for( vector<Rect>::const_iterator r = faces2.begin(); r != faces2.end(); r++ )
        {
            faces.push_back(Rect(smallImg.cols - r->x - r->width, r->y, r->width, r->height));
        }
    }
    t = (double)cvGetTickCount() - t;
    printf( "detection time = %g ms\n", t/((double)cvGetTickFrequency()*1000.) );
    for( vector<Rect>::const_iterator r = faces.begin(); r != faces.end(); r++, i++ )
    {
        Mat smallImgROI;
        vector<Rect> nestedObjects;
        Point center;
        Scalar color = colors[i%8];
        int radius;

        double aspect_ratio = (double)r->width/r->height;
        if( 0.75 < aspect_ratio && aspect_ratio < 1.3 )
        {
            center.x = cvRound((r->x + r->width*0.5)*scale);
            center.y = cvRound((r->y + r->height*0.5)*scale);
            radius = cvRound((r->width + r->height)*0.25*scale);
            circle( img, center, radius, color, 3, 8, 0 );
        }
        else
            rectangle( img, cvPoint(cvRound(r->x*scale), cvRound(r->y*scale)),
                       cvPoint(cvRound((r->x + r->width-1)*scale), cvRound((r->y + r->height-1)*scale)),
                       color, 3, 8, 0);
        if( nestedCascade.empty() )
            continue;
        smallImgROI = smallImg(*r);
        nestedCascade.detectMultiScale( smallImgROI, nestedObjects,
            1.1, 2, 0
            //|CV_HAAR_FIND_BIGGEST_OBJECT
            //|CV_HAAR_DO_ROUGH_SEARCH
            //|CV_HAAR_DO_CANNY_PRUNING
            |CV_HAAR_SCALE_IMAGE
            ,
            Size(30, 30) );
        for( vector<Rect>::const_iterator nr = nestedObjects.begin(); nr != nestedObjects.end(); nr++ )
        {
            center.x = cvRound((r->x + nr->x + nr->width*0.5)*scale);
            center.y = cvRound((r->y + nr->y + nr->height*0.5)*scale);
            radius = cvRound((nr->width + nr->height)*0.25*scale);
            circle( img, center, radius, color, 3, 8, 0 );
        }
    }
    cv::imshow( "result", img );
}

以下のようにコンパイルします。”-L .”より後ろがraspicam_cvライブラリを使用するための指定となります。

# g++ `pkg-config --cflags opencv` -o facedetect_raspicam facedetect_raspicam.cpp `pkg-config --libs opencv` -L . libraspicamcv.a -L ~/git/raspberrypi/userland/build/lib -lmmal_core -lmmal -lmmal_util -lvcos -lbcm_host

以下のように実行します。

# ./facedetect_raspicam --scale=4

モニタに映ったクエリちゃんを認識させてみました。クエリちゃんのバルーン集めのタイトル画面です。
※画像が汚いのはVNC経由のためです。色合いがおかしいのはRasPiカメラの性質と思います。
facedetect05

なお、上記クエリちゃんの顔認識では以下のようにcascade(検出器)にhaarcascade_frontalface_default.xml を指定しました。クエリちゃんはデフォルトのhaarcascade_frontalface_alt.xmlよりこちらの方が認識されやすかったので。

# ./facedetect_raspicam --cascade="~/opencv-2.4.11/data/haarcascades/haarcascade_frontalface_default.xml" --scale=4

(時間があれば)Raspberry Pi 2の4コアを活かすべく顔認識を別スレッドにするようにしてみたいです。


Query-Chan_license_logo

本格派対局将棋 ぴよ将棋
本格派対局将棋アプリ ぴよ将棋
[Android] [iOS]

かわいい「ひよこ」と対局する将棋アプリ。かわいいけどAIは本格派!
対局後の検討機能や棋譜管理機能も充実!棋譜解析機能も搭載!

「【Raspberry Pi 2】 RasPiカメラモジュールとOpenCVで顔認識」への12件のフィードバック

  1. 初めまして、ラズベリパイを勉強中のものです。
    サンプルプログラム拝見致し、同じように実行しましたがエラーが発生します。 ★FPS対応からエラーが発生しますが、プログラムスキールがなくてどのように直せば良いか分かりません。ご指導よろしくお願いします。

  2. kskさん
    エラーメッセージを送信いただければ何かアドバイスができるかもしれません。

  3. すみません、返信が遅くなりました。コンパイルする際に下記のようなエラーが発生します。
    ご指導よろしくお願いします。

    facedetect_raspicam.cpp: In function ‘int main(int, const char**)’:
    facedetect_raspicam.cpp:78:34: error: expected ‘;’ before ‘)’ token
    facedetect_raspicam.cpp:79:17: error: ‘fps_idx’ was not declared in this scope
    facedetect_raspicam.cpp:82:23: error: ‘iorigin’ was not declared in this scope
    facedetect_raspicam.cpp:82:48: error: expected ‘;’ before ‘)’ token
    facedetect_raspicam.cpp:84:13: error: ‘else’ without a previous ‘if’
    facedetect_raspicam.cpp:87:39: error: ‘cascade’ was not declared in this scope
    facedetect_raspicam.cpp:87:48: error: ‘nestedCascade’ was not declared in this scope
    facedetect_raspicam.cpp:87:63: error: ‘scale’ was not declared in this scope
    facedetect_raspicam.cpp:90:22: error: label ‘_cleanup_’ used but not defined
    facedetect_raspicam.cpp: At global scope:
    facedetect_raspicam.cpp:93:16: error: expected constructor, destructor, or type conversion before ‘(’ token
    facedetect_raspicam.cpp:95:10: error: found ‘:’ in nested-name-specifier, expected ‘::’
    facedetect_raspicam.cpp:95:1: error: ‘_cleanup_’ does not name a type
    facedetect_raspicam.cpp:102:5: error: expected declaration before ‘}’ token

  4. kskさん
    貼りつけていたソースですが、ブラウザによっては表示が崩れてしまう場合があったようです。ソースを表示するためのツールを導入してみましたので、再度試してみてください。

  5. ありがとうございます。
    再度試した結果、問題無くpiカメラで顔認識出来ました。

    今後も「PONTAの工房 ~プログラミング・電子工作~」を拝見しながらラズベリパイの勉強に力をつけたいと思います。どうぞよろしくお願いします。

  6. はじめまして。
    RasPiで画像処理を行いたく、管理人様の手順を参考にさせていただいております。

    初心者ながらraspicam_cvを導入してOpenCVでカメラモジュール操作を行うことは出来たのですが、
    当記事のカメラモジュールで顔認証を行うプログラムを実行しようとすると、エラーがでてしまいます。
    ターミナルで

    ./facedetect_raspicam –scale=4

    を打ち込むと

    Processing 1 –scale=4
    from which we read scale = 4
    ERROR: Could not load classifier cascade

    と出てきます。
    ビルド自体は出来ているみたいなのですが、実行がどうもうまくいきません。
    原因がお分かりでしたらご指導お願いします。

    1. KTさん
      「ERROR: Could not load classifier cascade」とのことですので、cascadeファイルが読み込めないのだと思います。本文中の「facedetect_raspicam.cpp」のソースだと40行目、41行目で相対パスでファイルを指定していますので、ファイルを参照できるディレクトリに移動してから実行する必要があります。または、–cascadeオプションおよび–nested-cascadeオプションで明示的に指定してあげればよいと思います。

  7. 回答ありがとうございます。
    ご指摘の通り、cascadeファイルが読み込めていませんでした。
    facedetect.cppがあった場所に移動させて実行してみると正常に動作しました。
    また当記事の「facedetect_raspicam.cpp」ソースの40,41行目のパスを変えたところ、別のフォルダでも動作を確認することができました。

    管理人様、丁寧に答えていただいて本当にありがとうございました。
    今後も当ブログの更新をがんばってください。楽しみにしております。

  8. はじめまして 上記のプログラムをコンパイルして実効したら以下のような文が出ます。
    This program demonstrates the cascade recognizer. Now you can use Haar or LBP features.
    This classifier can recognize many kinds of rigid objects, once the appropriate classifier is trained.
    It’s most known use is for faces.
    Usage:
    ./facedetect [–cascade= this is the primary trained classifier such as frontal face]
    [–nested-cascade[=nested_cascade_path this an optional secondary classifier such as eyes]]
    [–scale=]
    [–try-flip]
    [filename|camera_index]

    see facedetect.cmd for one call:
    ./facedetect –cascade=”../../data/haarcascades/haarcascade_frontalface_alt.xml” –nested-cascade=”../../data/haarcascades/haarcascade_eye.xml” –scale=1.3

    During execution:
    Hit any key to quit.
    Using OpenCV version 2.4.10

    ERROR: Could not load classifier cascade

    This program demonstrates the cascade recognizer. Now you can use Haar or LBP features.
    This classifier can recognize many kinds of rigid objects, once the appropriate classifier is trained.
    It’s most known use is for faces.
    Usage:
    ./facedetect [–cascade= this is the primary trained classifier such as frontal face]
    [–nested-cascade[=nested_cascade_path this an optional secondary classifier such as eyes]]
    [–scale=]
    [–try-flip]
    [filename|camera_index]

    see facedetect.cmd for one call:
    ./facedetect –cascade=”../../data/haarcascades/haarcascade_frontalface_alt.xml” –nested-cascade=”../../data/haarcascades/haarcascade_eye.xml” –scale=1.3

    During execution:
    Hit any key to quit.
    Using OpenCV version 2.4.10

    opencv2.4.10を使っていることが原因なのでしょうか?

    1. ERROR: Could not load classifier cascade
      とのエラーが出ているようですので、
      cascade=”../../data/haarcascades/haarcascade_frontalface_alt.xml” –nested-cascade=”../../data/haarcascades/haarcascade_eye.xml”
      で指定しているパスが正しいか確認してみてください。
      カレントパスから
      ls ../../data/haarcascades/haarcascade_frontalface_alt.xml
      などで参照できるでしょうか?
      絶対パスにしてみても良いかもしれません。

      1. ご指摘の通りパスの部分を直したら実行できました。
        ありがとうございました

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です