WEBカメラを使って、パソコンの前に座ったら『お帰りなさいませ、ご主人さまっ』と言ってもらう。(2)

WEBカメラを使って、パソコンの前に座ったら『お帰りなさいませ、ご主人さまっ』と言ってもらう。

やや明後日の方向に向かっているソフトウェアネタですが、よろしくお付き合いくださいませ。

以前、WEBカメラを使って画像処理でパソコンの前に座ったらデスクトップマスコットに『お帰りなさいませ、ご主人さまっ』と言ってもらうという試みをしました。

WEBカメラを使って、パソコンの前に座ったら『お帰りなさいませ、ご主人さまっ』と言ってもらう。

この時はオプションプログラムとして実装したので、デスクトップマスコットを実行しているパソコンで画像処理も行うようになっていたのですが、これが結構重い処理だったりします。そこで今回は、『Fy Mascot』に実装されているネットワーク機能を使って、別のパソコンで画像処理をしてみたいと思います。

流れとしては、画像処理用のパソコンにWEBカメラと画像処理を行うプログラム(クライアント)を実行して、パソコンの前に座ったことを認識したら、TCP/IP経由で『Fy Mascot』にメッセージを通知します。『Fy mascot』側の機能は搭載されているので、今回は画像処理を行うクライアントを作成します。

イメージ

基本的な部分は前回のものを踏襲して、オプションプログラムからクライアントへ移植するような形にしました。

今回はソースコードを2つに分けて、前回のokaeri.cppのうち汎用性の高い画像処理の部分をokaeri2.cppとして纏め、ウィンドウとGICPの部分はsdkのclient.cppをベースに作りました。 

いつもながら粗々ですがご承知おきください。

OpenCVのバージョンは2.1、文字コードはマルチバイトになっています。コンパイルするには、あらかじめOpenCVのヘッダーとライブラリのパスを登録しておく必要があります。

ソースコード(okaeri2.cpp)
//okaeri2.cpp : アプリケーションのエントリ ポイントを定義します。
//
#include <stdio.h>
#include <windows.h>

//なぜかエラーになるのでとりあえず
#define _SH_DENYRW      0x10    /* deny read/write mode */
#define _SH_DENYWR      0x20    /* deny write mode */
#define _SH_DENYRD      0x30    /* deny read mode */
#define _SH_DENYNO      0x40    /* deny none mode */
#define _SH_SECURE      0x80    /* secure mode */

#include "ewclib.h"
#include <cv.h>
#include <highgui.h>

// ライブラリ読み込み
#pragma comment(lib,"cv210.lib")
#pragma comment(lib,"cxcore210.lib")
#pragma comment(lib,"cvaux210.lib")
#pragma comment(lib,"highgui210.lib")
 
extern HWND g_hwnd;

//EWCLIBの初期化
int ewc_initialize( int camera_no, int width , int height , int framerate )
{
    //24bitじゃない場合はewc_getimageも変更すること
    if(EWC_Open( camera_no, width, height, framerate , -1, MEDIASUBTYPE_RGB24)!=0)
    {
        return FALSE;
    }
    return TRUE;
}

//EWCLIBで画像取得
IplImage *ewc_getimage( int camera_no , int width, int height )
{
    IplImage *ipl_image;
    void *buffer;

    ipl_image = cvCreateImage( cvSize( width, height), IPL_DEPTH_8U, 3);
    if(!ipl_image)
    {
        return NULL;
    }

     buffer = malloc( ipl_image->imageSize );
    if(EWC_GetImage( camera_no, buffer )!=0)
    {
        free( buffer );
        cvReleaseImage(&ipl_image);
        return NULL;
    }
 
    //IplImageに画像をコピー
    //カメラからの入力が24bitではない場合や、
    //横ピクセルが4の倍数じゃない場合などは修正が必要なハズ
    memcpy( ipl_image->imageData, buffer, ipl_image->imageSize );
    free(buffer);
    return ipl_image;
}

//EWCLIBの終了処理
int ewc_destroy( int camera_no )
{
    EWC_Close( camera_no );
    return TRUE;
}

//HBITMAP作成
HBITMAP bitmap_create( int width, int height )
{
    BITMAPINFO info;
    HBITMAP hDIBSection;
    void* pBits;     memset( &info, 0, sizeof(BITMAPINFOHEADER) );
    info.bmiHeader.biSize                = sizeof(BITMAPINFOHEADER);    // size of the structure
    info.bmiHeader.biWidth                = width;            // width of the bitmap
    info.bmiHeader.biHeight                = height;            // height of the bitmap
    info.bmiHeader.biPlanes                = 1;                    // must be set to 1
    info.bmiHeader.biBitCount            = 32;                    // maximum of 2^32 colors
    info.bmiHeader.biCompression        = BI_RGB;                // an uncompressed format     // DIBの作成
    hDIBSection = CreateDIBSection( NULL , &info , DIB_RGB_COLORS , (void**)&pBits , NULL , 0 );
    if(!hDIBSection)
    {
        return NULL;
    }
    memset( pBits, 0, width*height*32/8);
    return hDIBSection;
}

//IplImageをHBITMAPに変換
int bitmap_trancecode( HBITMAP hBitmap , IplImage *ipl_image )
{
    BITMAP bmp;
    int x, y;
    RGBQUAD *lpbits;

    GetObject( hBitmap, sizeof(BITMAP) ,&bmp );
    lpbits = (RGBQUAD*)bmp.bmBits;     //IplImageから画像をコピー

    //IplImageの色数が24bitではない場合や、
    //横ピクセルが4の倍数じゃない場合などは修正が必要なハズ
    for( y=0 ; y<bmp.bmHeight ; y++ )
    {       
        for( x=0 ; x<bmp.bmWidth ; x++ )
        {
         lpbits[(bmp.bmHeight-y-1)*bmp.bmWidth+x].rgbRed = ipl_image->imageData[(y*bmp.bmWidth+x)*3];
         lpbits[(bmp.bmHeight-y-1)*bmp.bmWidth+x].rgbGreen = ipl_image->imageData[(y*bmp.bmWidth+x)*3+1];
         lpbits[(bmp.bmHeight-y-1)*bmp.bmWidth+x].rgbBlue = ipl_image->imageData[(y*bmp.bmWidth+x)*3+2];
        }
    }
    return TRUE;
}

//HBITMAPを表示
int bitmap_show( HDC hDC , HBITMAP hBitmap , RECT *rt  , DWORD dwRop )
{
    HDC hMemDC;
    BITMAP bmp;     if(!hBitmap)
    {
        return FALSE;    //これは実行側がわるいのでここでは『error_log』はしない。
    }

    memset( &bmp , 0 , sizeof(BITMAP) );
    GetObject( hBitmap , sizeof(BITMAP) , &bmp );
    hMemDC = CreateCompatibleDC( hDC );
    SelectObject( hMemDC , hBitmap );
    SetStretchBltMode( hDC , COLORONCOLOR );
    StretchBlt( hDC , rt->left, rt->top, rt->right-rt->left , rt->bottom-rt->top , hMemDC , 0 , 0 , bmp.bmWidth , bmp.bmHeight , dwRop );
    DeleteDC( hMemDC );
    return TRUE;
}

//顔認識処理
int detect( IplImage* ipl_image , IplImage *ipl_face , int *lp_flag )
{
    CvSeq* faces;
    int i;
    CvRect* r;
    CvPoint center;
    int radius;
    CvScalar color;
    CvMemStorage* storage;
    CvHaarClassifierCascade* cascade;
    IplImage *ipl_gray;

    //パターンファイルをロードする
    cascade = (CvHaarClassifierCascade*)cvLoad( "haarcascade_frontalface_default.xml" );
    if(!cascade)
    {
        return FALSE;
    }

    //画像をグレーにする。
    ipl_gray = cvCreateImage (cvGetSize(ipl_image), IPL_DEPTH_8U, 1);     //グレー化
    cvCvtColor (ipl_image, ipl_gray, CV_BGR2GRAY);

    //ヒストグラムを平滑化
    cvEqualizeHist (ipl_gray, ipl_gray);
    //処理用のメモリを確保する
    storage = cvCreateMemStorage(0);
    //メモリストレージをクリアする
    cvClearMemStorage( storage );  

   //cvhaarDetectObject→顔検出
    faces = cvHaarDetectObjects( ipl_gray, cascade, storage, 1.1, 2, 0/*CV_HAAR_DO_CANNY_PRUNING*/, cvSize(40, 40) );

    if(!faces)
    {
        //失敗
        cvReleaseMemStorage (&storage);
        cvReleaseImage( &ipl_gray );
        return FALSE;
    }

    if(faces->total<=0)
    {
        //顔はなかった
        //エラーではない

         //フラグの確認
        if(lp_flag)
        {
            *lp_flag = FALSE;
        }
    }
    else
    {
        //フラグの確認
        if(lp_flag)
        {
            *lp_flag = TRUE;
        }

        //画像を処理するか(ipl_faceがあれば処理する、なければ不要)
        if(ipl_face)
        {
            //色
            color.val[0] = 255.0;
            color.val[1] = 0.0;
            color.val[2] = 0.0;
            color.val[3] = 0.0;

            /* 顔領域の描画 */
            for(i=0; i<faces->total; i++ )
            {
                r = (CvRect*)cvGetSeqElem( faces, i );
                center.x = r->x+r->width*0.5;
                center.y = r->y+r->height*0.5;
                radius = (r->width+r->height)/2;
                cvCircle( ipl_face, center, radius, color, 3, 8, 0 );
            }
        }
    }

    //処理完了なので解放する
    cvReleaseMemStorage (&storage);
    cvReleaseImage( &ipl_gray );
    return TRUE;
}  

client.cppで変更したのはウィンドウプロシージャーの部分とgicp_okaeri関数の部分です。

ソースコード(client.cpp) //変更・追加部分を抜粋
//変更・追加した宣言

/* メニュー */
#define ID_START    1
#define ID_END        2
#define ID_EXIT        3

/* 子ウィンドウのID */
#define ID_STATIC 1

//関数
int gicp_okaeri();

// 定数
#define WINDOW_WIDTH    320
#define WINDOW_HEIGHT 240
#define FRAMERATE    15

//画像処理の関数
int ewc_initialize( int , int , int , int );
IplImage *ewc_getimage( int , int , int );
int ewc_destroy( int );
HBITMAP bitmap_create( int , int );
int bitmap_trancecode( HBITMAP , IplImage * );
int bitmap_show( HDC , HBITMAP , RECT * , DWORD );
int detect( IplImage* , IplImage * , int * );

/* ウィンドウのプロシージャー */
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HBITMAP hBitmap = NULL;    //表示用画像

    switch (message)
    {
    case WM_CREATE:
        {
            HMENU hMenu;
            HMENU hSubMenu;

            /* ウィンドウハンドルをグローバルへコピー */
            g_hwnd = hWnd;

            /* メニューの作成 */
            hMenu = CreateMenu();
            hSubMenu = CreateMenu();
            AppendMenu( hMenu , MF_POPUP, (UINT_PTR)hSubMenu , "メニュー" );
            AppendMenu( hSubMenu , MF_STRING , ID_START , "開始" );    //メニュー追加
            AppendMenu( hSubMenu , MF_STRING , ID_END , "停止" );        //メニュー追加
            AppendMenu( hSubMenu , MF_STRING , ID_EXIT , "終了" );
            SetMenu( hWnd , hMenu );

            //カメラ初期化
            if(!ewc_initialize( 0, WINDOW_WIDTH, WINDOW_HEIGHT, FRAMERATE ))
            {
                return FALSE;
            }

            //ビットマップ作成
            hBitmap = bitmap_create( WINDOW_WIDTH, WINDOW_HEIGHT );

            //子ウィンドウの作成
            //SS_OWNERDRAWを追加したことに注意する
            CreateWindow("STATIC","",WS_CHILD|WS_VISIBLE|SS_OWNERDRAW,
0,0,320,240,hWnd,(HMENU)ID_STATIC,((CREATESTRUCT*)lParam)->hInstance,NULL);

            /* サーバー起動 */
            server( 1 );
            return TRUE;
        }
        break;
    case WM_TIMER:
        {
            //タイマーで指定した周期で画像処理を行う
            IplImage *ipl_image;
            IplImage *ipl_face;
            int flag;
            static int status = FALSE;

            //カメラ画像取得
            ipl_image = ewc_getimage( 0, WINDOW_WIDTH, WINDOW_HEIGHT );
            if(!ipl_image)
            {
                return FALSE;
            }

            ipl_face = cvCreateImage ( cvGetSize(ipl_image), IPL_DEPTH_8U, 3 );
            cvCopy( ipl_image, ipl_face );

            //顔認識
            detect( ipl_image , ipl_face , &flag );

            //bitmapに変換!
            if(hBitmap)
            {
                bitmap_trancecode( hBitmap , ipl_face );
            }
            if(flag)
            {
                if(!status)
                {
                    status = TRUE;
                    gicp_okaeri();
                }
            }
            else
            {
                status = FALSE;
            }

            cvReleaseImage( &ipl_face );
            cvReleaseImage( &ipl_image );
            InvalidateRect( hWnd, NULL, FALSE );
            return TRUE;
        }
        break;
    case WM_DRAWITEM:
        {
            //WEBカメラの映像(画像処理済み)を表示する
            //子ウィンドウはSS_OWNERDRAWとしたので描画処理は親側で行う
            switch(LOWORD(wParam))
            {
            case ID_STATIC:
                {
                    if(hBitmap)
                    {
                        bitmap_show( ((DRAWITEMSTRUCT*)lParam)->hDC, hBitmap ,
 &(((DRAWITEMSTRUCT*)lParam)->rcItem), SRCCOPY );
                    }
                    return TRUE;
                }
                break;
            default:
                return FALSE;
                break;
            }
        }
        break;
    case WM_SIZE:
        {
            /* 親ウィンドウの大きさに合わせて子ウィンドウをリサイズ */
            RECT rect;
            GetClientRect(hWnd,&rect);
            MoveWindow(GetDlgItem(hWnd,ID_STATIC),0,0,rect.right-rect.left,rect.bottom-rect.top,FALSE);
            return TRUE;
        }
        break;
    case WM_COMMAND:
        {
            /* コマンド(メニュー)処理 */
            unsigned short wmId, wmEvent;
            wmId    = LOWORD(wParam);
            wmEvent = HIWORD(wParam);
            switch (wmId)
            {
            //追加したメニューでタイマーの開始、停止を処理する
            case ID_START:
                {
                    SetTimer( hWnd, 1, 33, NULL );
                    return TRUE;
                }
                break;
            case ID_END:
                {
                    KillTimer( hWnd, 1 );
                    return TRUE;
                }
                break;
            case ID_EXIT:
                {
                    /* 終了が選択された場合の処理 */
                    DestroyWindow( hWnd );
                    return TRUE;
                }
                break;
            default:
                return DefWindowProc(hWnd, message, wParam, lParam);
                break;
            }
        }
        break;
    case WM_PAINT:
        {
            /* 描画 */
            /* このプログラムの本質とは関係なし。 */
            PAINTSTRUCT ps;
            HDC hDC;
            hDC = BeginPaint(hWnd, &ps);
            EndPaint(hWnd, &ps);
            return TRUE;
        }
        break;
    case WM_DESTROY:
        {
            //WEBカメラの後処理と画像の解放を行う
            ewc_destroy( 0 );
            DeleteObject( hBitmap );
            hBitmap = NULL;
            /* サーバー終了 */
            server( 2 );
            /* メッセージループの終了 */
            PostQuitMessage(0);
            return 0;
        }
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
        break;
    }
}
/* 顔が認識された!*/
int gicp_okaeri()
{
    char *master;
    char *follower;
    char *witchcraft;
    char *incantation;
    char gicp[2048];
    char ip[4];
    /* 各要素を作る */
    /* IPアドレスは実行するPCのアドレスで置き換えて下さい */
    /* この例ではマルチバイト to UTF8 の変換を行っていません */
    /* ASCII文字以外を送信する場合は変換してください */
    master = "<master><place>NETWORK</place><detail><address>xxx.xxx.xxx.xxx:xxxx</address></detail></master>";
    follower = "<follower><place>MASCOT</place><detail></detail></follower>";
    witchcraft = "<witchcraft><class>MESSAGE</class><order>EXECUTION</order></witchcraft>";
    incantation = "<incantation><type>MESSAGE</type><sender>Client</sender>
<string>Welcome home, my master.</string>
<userdata><exclusive><name>CLIENT</name><attribute>WELCOMEHOME</attribute></exclusive></userdata>
</incantation>";
   
    sprintf( gicp , "<\?xml version=\"1.0\" encoding=\"UTF-8\"\?>\r\n<gicp version=\"1.0\">\r\n%s\r\n%s\r\n%s\r\n%s\r\n</gicp>" , master , follower , witchcraft , incantation );

    /* GICPを送信!*/
    ip[0] = xxx;
    ip[1] = xxx;
    ip[2] = xxx;
    ip[3] = xxx;
    gicp_send(ip,xxxx,gicp);
    return TRUE;
}

こんな感じです。では早速コンパイルして実行してみます。

なお、OpenCVを使うためにcv210.dll、cvaux210.dll、cxcore210.dll、 highgui210.dll、haarcascade_frontalface_default.xmlの各ファイルをプログラムのフォルダにコピーします。(dllの方はWindowsのシステムフォルダにインストールされているのであれば不要です)

WEBカメラを付けたパソコン上で画像処理を行うクライアントを実行します。ちゃんと顔を認識しています。CPU占有率の方はCore i7 2.8GHz (HT無効4コア)で26%を占めておりシングルスレッドだとすればほぼ限界までマシンパワーが使われているのが分ります。

イメージ  イメージ

そしてTCP/IP経由で『Fy Mascot』が実行されているパソコンにメッセージが送られて、ちゃんと『お帰りなさいませ』のメッセージが表示されています。(このスクリーンショットだけではわかりにくいですが。。。)

イメージ

今回は画像処理うんぬんというよりも、ネットワークを使って複数のパソコンで処理を分散するのが目的でした。この程度の簡単な処理ではそれほど問題にはならないですが、より高度な画像処理などをする場合に(デスクトップマスコットに限らず)有効ではないでしょうか。

ちなみに、ネットワークにつながっていれば通信できるので、離れた場所にセンサー(ウェブカメラ)を設置するなどの応用次第では色々な可能性があるのではないでしょうか。