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

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

いつもの比較的まじめなハードウェアネタの内容に比べて、若干壊れ気味のソフトウェアネタですが、よろしくお付き合いくださいませ。

家に帰ったらメイドさんに『お帰りなさいませ、ご主人さまっ』と言ってもらうのは、一般市民の手の届く世界ではないでしょうけれども、パソコンの前に座ったらデスクトップマスコットに『お帰りなさいませ、ご主人さまっ』と言ってもらうくらいならば、科学の限界を超えてやってこなくても実現できそうです。

最近では、WEBカメラも画質に拘らなければ、それこそ1000円でおつりがくるレベルまで低価格化してきました。ということで、この安いWEBカメラを使って、一つ遊んでみました。使用した機材はロジクールの超低価格WEBカメラ『UCAM-DLF30』。なんか500円くらいで買ってきました。値段が値段だけに文句は言えないのですが、買った個体はUSB端子の調子が悪いようで刺し方にコツが入りますw。

流れとしては『Fy Mascot』に、ディスプレイの上に置いたWEBカメラの映像から顔を検出して通知するオプションプログラムを追加します。

イメージ

人の顔を検知する仕組みは、肌の色を利用するとか、学習された特徴を使うなど様々ですが、今回はメンドーだったのでOpenCV (http://opencv.jp/)というインテルが開発したフリーのライブラリを使わせていただくことにしました。これは、色ではなくて、特徴を使っているみたいです。

以下のサイトなどを参考に、WEBカメラからの取り込みと、顔認識をするプログラムを作成しました。サイト制作者のみなさま、ありがとうございました。

また、利用したWEBカメラ『UCAM-DLF30』は『Video For Windows』に対応していなかったので、DirectXを使うことになったのですが、これもまたメンドーだったので上のサイトにあるEWCLIBというライブラリを使わせていただいてサクッと使えるようにしました。

ということで、作成しました。まずは、独立したスタンドアローンのアプリケーションとして作成したものが以下のコードです。

突貫工事だったので、かなり雑ですがあらかじめご承知おきください。

上のサイトにあるコードのOpenCVのウィンドウがなぜか旨く表示できなかったので、ここでは普通のウィンドウにHBITMAPを使って表示しています。そのため、OpenCVのイメージ形式であるIplImage *とHBITMAPの変換を行っています。HBITMAPはWin32 APIではおなじみなので、応用もきくのではないでしょうか。なお、OpenCVのバージョンは2.1、文字コードはマルチバイトになっています (Unicodeでも問題なくコンパイル可能なコードです)。コンパイルするには、あらかじめOpenCVのヘッダーとライブラリのパスを登録しておく必要があります。

ソースコード

// okaeri.cpp
//
#include <stdio.h>
#include <windows.h>
#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")
// 定義
#define WINDOW_WIDTH    320
#define WINDOW_HEIGHT 240
#define FRAMERATE    15
#define ID_START    1
#define ID_END        2
int ewc_initialize( int , int , int , int  );    //EWCLIBの初期化
IplImage *ewc_getimage( int , int , int );        //EWCLIBで画像取得
int ewc_destroy( int );                            //EWCLIBの終了
HBITMAP bitmap_create( int , int );                    //HBITMAPの作成
int bitmap_trancecode( HBITMAP , IplImage * );        //IplImageをHBITMAPに変換
int bitmap_show( HDC , HBITMAP , RECT * , DWORD );    //HBITMAPを表示
int detect_processing( IplImage * , IplImage * , int * );    //顔認識処理
LRESULT CALLBACK window_procedure( HWND , UINT , WPARAM , LPARAM );
int APIENTRY WinMain( HINSTANCE hInstance , HINSTANCE hPrevInstance, char *lpCmdLine, int nCmdShow )
{
     // TODO: ここにコードを挿入してください。
    MSG msg;
    WNDCLASSEXA wcex;
    HWND hWnd;
memset( &wcex, 0, sizeof(WNDCLASSEX) );
    wcex.cbSize            = sizeof(WNDCLASSEX);
    wcex.style            = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = window_procedure;
    wcex.hInstance        = hInstance;
    wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground    = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszClassName    = "WindowClass";
    RegisterClassExA(&wcex);
hWnd = CreateWindowA( "WindowClass" , "Title", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 400, 320, NULL, NULL, hInstance, NULL);
    if (!hWnd)
    {
        return 0;
    }
ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);
// メイン メッセージ ループ:
    while (GetMessage(&msg, NULL, 0, 0))
    {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
    }
    return (int) msg.wParam;
}
//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_recorderror』はしない。
    }
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_processing( 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;
    }
//画像をグレーにする。
    //どうやらOpenCVでは、顔を色ではなく形(?)で判定しているようだ。
    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;
}
LRESULT CALLBACK window_procedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HBITMAP hBitmap = NULL;
switch (message)
    {
    case WM_CREATE:
        {
            //カメラ初期化
            if(!ewc_initialize( 0, WINDOW_WIDTH, WINDOW_HEIGHT, FRAMERATE ))
            {
                return FALSE;
            }
//ビットマップ作成
            hBitmap = bitmap_create( WINDOW_WIDTH, WINDOW_HEIGHT );
//ウィンドウ作成
            CreateWindowExA( WS_EX_STATICEDGE , "STATIC" , "" , WS_CHILD|WS_VISIBLE|SS_OWNERDRAW, 40, 10, 320, 240 , hWnd , (HMENU)1, ((CREATESTRUCT*)lParam)->hInstance, NULL );
            CreateWindowExA( 0 , "BUTTON" , "開始" , WS_CHILD|WS_VISIBLE, 60, 260, 100, 30 , hWnd , (HMENU)1, ((CREATESTRUCT*)lParam)->hInstance, NULL );
            CreateWindowExA( 0 , "BUTTON" , "停止" , WS_CHILD|WS_VISIBLE, 240, 260, 100, 30 , hWnd , (HMENU)2, ((CREATESTRUCT*)lParam)->hInstance, NULL );
            return TRUE;
        }
        break;
    case WM_COMMAND:
        {
            switch(LOWORD(wParam))
            {
            case ID_START:
                {
                    SetTimer( hWnd, 1, 33, NULL );
                    return TRUE;
                }
                break;
            case ID_END:
                {
                    KillTimer( hWnd, 1 );
                    return TRUE;
                }
                break;
            default:
                return FALSE;
                break;
            }
        }
        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_processing( ipl_image , ipl_face , &flag );
//bitmapに変換!
            if(hBitmap)
            {
                bitmap_trancecode( hBitmap , ipl_face );
            }
if(flag)
            {
                if(!status)
                {
                    status = TRUE;
                    //ここで『おかえりなさいませ、ご主人様!』と言わせる。
                }
            }
            else
            {
                status = FALSE;
            }
cvReleaseImage( &ipl_face );
            cvReleaseImage( &ipl_image );
InvalidateRect( hWnd, NULL, FALSE );
            return TRUE;
        }
        break;
    case WM_DRAWITEM:
        {
            switch(LOWORD(wParam))
            {
            case 1:
                {
                    if(hBitmap)
                    {
                        bitmap_show( ((DRAWITEMSTRUCT*)lParam)->hDC, hBitmap , &(((DRAWITEMSTRUCT*)lParam)->rcItem), SRCCOPY );
                    }
                    return TRUE;
                }
                break;
            default:
                return FALSE;
                break;
            }
        }
        break;
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hDC;
            hDC = BeginPaint(hWnd, &ps);
            EndPaint(hWnd, &ps);
            return TRUE;
        }
        break;
    case WM_DESTROY:
        {
            ewc_destroy( 0 );
            PostQuitMessage(0);
            return TRUE;
        }
        break;
    default:
        return DefWindowProcA(hWnd, message, wParam, lParam);
        break;
    }
}

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

イメージ

WEBカメラからの映像から、顔を認識して赤丸で囲みます。

こんな感じで、席に座るとダイアログを表示してくれます。

ただ、角度が悪かったりすると顔を見失い、再び認識するとダイアログが表示されます。顔を見失っていた時間で判断するなどの処理を追加する必要があるかもしれません。

今回は軽い実験なので、このままオプションプログラムとして『Fy Mascot』に実装します。コードはほぼ変更なしで、オプションプログラムのサンプルコードのウィンドウ部分を差し替えた程度です。

なお、実行にあたっては、オプションプログラムではなく、『Fy Mascot』のプログラムと同じフォルダにcv210.dll、cvaux210.dll、cxcore210.dll、 highgui210.dll、haarcascade_frontalface_default.xmlの各ファイルをコピーしておいてください。

イメージ 席に座ったら、『お帰りなさいませっ』と言ってくれました。顔出しNGということで(笑)。

こんな感じで、期待するような素晴らしいモノでもないですが、応用すれば色々できるかもしれません。

ちなみに、気づいた方も多いと思いますが、このプログラムは『人がいること』は認識はしていますが、『人』を認識はしていません。つまり、『ご主人さまっ』と言っているものの、人ならば誰でもそう言う節操のないプログラムになっています。その辺を改良すると、もうちょっと面白いことができるようになるかもしれませんね。