2011/02/16

魔法使いの弟子(その7-1)

【ミッション: Windows プログラミングに挑戦せよ】

中学の頃、ぼくは生まれて初めて C 言語の入門書を買った。

しかしそこに書いてあったのは、退屈なコマンドラインアプリケーションばかり。 GUI アプリケーションの構築を夢見ていたぼくにとって、それはあまりにもショッキングな現実であった。

そんなぼくも、やがて C 言語をひととおり学び(全然身についていないが)、ついに GUI アプリに手を付ける日が来た。
ソースコードは以下のようになる。これは初等的な Windows アプリケーションのソースコードであり、Charls Petzold著『プログラミング Windows』等でも解説されているため、詳細は省く。

【MainWindow.cpp】

基本的なコードであるとはいえ、C 言語の知識だけでは「とりあえず意味不明な構造体をひたすら初期化し、それらをがちがち組み合わせている」くらいにしか見えない(解読には Windows という OS の基礎的な知識も必要)。

最近はフロントエンド構築を可能な限り省力化しようという動きによって、Windows API を直叩きするコーディングスタイルは、もはやレガシーと化しつつあるようだが、ぼくの周りには未だに Windows API を直に叩いてアプリケーションを作る人が多い。.NET なんてもってのほか、MFC ですら許し難いというぶっちぎり硬派な Windows ユーザばかりで、こういうのが苦手なぼくにとっては針の筵である。



というわけで今回は、Windows プログラミングの練習がてら、上記のコードをいじくって OpenCV 2.0 と連繋してみる事にする。OpenCV は極めて強力な画像処理ライブラリであり、高度な処理をいとも簡単に実装する事ができるスグレモノだ。

まずは練習のため画像を表示するだけのプログラムを作ってみよう。一見簡単そうに見えるが、OpenCV と Windows API の連携では画像を表現するデータフォーマットが各々異なっている点がネックとなる。

OpenCV では IplImage 構造体で画像を表現するが、一方 Windows API では HBITMAP 構造体である(画像の実体は BITMAP 構造体だが)。両者に互換性は無いので、その橋渡しをしてやらねばならない。

これを超てきとうに解決していこう。なお、以降は OpenCV 2.0 環境が導入されている事を前提に話を進める。



まず、 cv200.lib、cxcore200.lib、highgui200.lib をリンク。
次に、ソースの冒頭で OpenCV の各ヘッダファイルをインクルードする。

  1. #include <windows.h>  
  2. #include <string>  
  3.   
  4. #include <cv.h>  
  5. #include <cxcore.h>  
  6. #include <highgui.h>  

これで、OpenCV の関数が利用できるようになった。

つづいて、アプリケーションデータ構造体を以下のように書き換える。

  1. typedef struct {  
  2.     // ウィンドウハンドル  
  3.     HWND hWnd;  
  4.       
  5.     // メモリデバイスコンテキストとビットマップハンドル  
  6.     HDC hMemDC;  
  7.     HBITMAP hBitmap;  
  8.   
  9.     // 画像ファイル名  
  10.     std::string imageFileName;  
  11.       
  12.     // IplImage構造体へのポインタ  
  13.     cv::Ptr<IplImage> iplImage;  
  14. } AppData;  

言い忘れたが、このアプリケーションデータ構造体は、「アプリケーション全体に亘って保持しておきたい変数」をまとめておくためのものである。グローバル変数や静的変数 にすると、プログラムの安全性が著しく低下するだけでなく、多重起動に対応できない(複数のウィンドウが同じグローバル領域を参照するため)という問題点も生じる。

そこで、保持したいグローバルデータを構造体に包み、ウィンドウ毎に(構造体分の)動的領域を確保する事で上記の問題点を回避する。

なお、構造体の中では IplImageHBITMAP の両方を保持する事にした。



さらに、コールバック関数 WndProc の中を大改造する。

switch 文で振り分けられる各メッセージについて、各々以下のように書き換えた。

  1. case WM_CREATE:  
  2.         pAppData = (AppData*)new AppData();  
  3.       
  4.             /** ここにアプリの初期化処理を書くよ **/  
  5.             /** たとえばデータ構造体の初期化とか **/  
  6.             pAppData->hBitmap = NULL;  
  7.             pAppData->hWnd = hWnd;  
  8.             pAppData->iplImage = NULL;  
  9.   
  10.             // ウィンドウの追加領域にポインタを保存  
  11.             SetWindowLongPtr(hWnd, 0, (LONG_PTR)pAppData);  
  12.   
  13.   
  14.   
  15.         } else {  
  16.             MessageBox(hWnd,   
  17.                 TEXT("アプリケーションの初期化に失敗しますた"),  
  18.                 TEXT("死の宣告"),  
  19.                 MB_OK);  
  20.             return -1; // ウィンドウ破棄  
  21.         }  
  22.         break;  

WM_CREATE ではデータ構造体の初期化を行う。本来であればコンストラクタにまとめるべきところなのだが、試行錯誤の結果つぎはぎだらけのコードになってしまった。ご容赦。

尚、ポインタ変数の初期値は問答無用で NULL を入れるように心がけている。これはメモリ周りの厄介なバグを検出するためである。

つづいて、描画処理の WM_PAINT を以下のように書いた。

  1. case WM_PAINT:  
  2.     /** 描画処理 **/  
  3.     GetClientRect(hWnd, &rect);  
  4.     hdc = BeginPaint(hWnd, &ps);  
  5.   
  6.     // ビットブロックを転送  
  7.     if(pAppData->hMemDC != NULL   
  8.         && pAppData->hBitmap != NULL) {  
  9.                       
  10.             BitBlt(hdc, 0, 0,   
  11.                 pAppData->iplImage->width,  
  12.                 pAppData->iplImage->height,  
  13.                 pAppData->hMemDC,  
  14.                 0, 0, SRCCOPY);  
  15.                   
  16.     }  
  17.     EndPaint(hWnd, &ps);  
  18.     break;  

アプリケーションデータ構造体からビットマップデータを取り出して、そのビットブロックを BitBlt 関数でディスプレイに転送する。もしもアプリケーションデータ構造体にしかるべき値が格納されていれば、これで画像をディスプレイに表示する事が可能となる。

最後に、WM_DESTROY メッセージの処理である。

  1. case WM_DESTROY:  
  2.     // 終了時にメモリを解放  
  3.     if (pAppData != NULL) {  
  4.                   
  5.         if(pAppData->hMemDC != NULL) {  
  6.             DeleteDC(pAppData->hMemDC);  
  7.             pAppData->hMemDC = NULL;  
  8.         }  
  9.   
  10.         if(pAppData->hBitmap != NULL) {  
  11.             DeleteObject(pAppData->hBitmap);  
  12.             pAppData->hBitmap = NULL;  
  13.         }  
  14.   
  15.         delete pAppData;  
  16.     }  
  17.     SetWindowLongPtr(hWnd, 0, NULL);  
  18.     PostQuitMessage(0);  
  19.     break;  

データ構造体のメンバである各ポインタ変数の解放が主な処理だ。



さて、ようやっと画像の読み込み関数を定義する。

まずは前処理として、cvConvertImage 関数によって、IplImage を 1 チャネルまたは 3 チャネルの 8 ビット画像に変換する。 OpenCV の 3 チャネル画像データは、通常 BGR の順で輝度が格納されているが、これも cvConvertImage によって RGB の順に変換される。

この関数は、OpenCV で画像をウィンドウに表示する関数 cvShowImage の中でも暗黙的に呼ばれるものである。

次に、IplImage の画像データのアクセスだが、これは Gary Bradski, Adrian Kaehler著『詳解 OpenCV』でポインタを駆使した有用なコードが示されている。

画像の全ピクセルを走査して RGB の輝度値を取得し、それを SetPixel によってビットマップデータにちくちく書きこんでいく。SetPixel は割と重たい関数だが、別にゲームのようなリアルタイム処理をしたいわけではないし、比較的直感的にピクセル操作が可能なので、ここでは躊躇なく使う事にする。

以上を踏まえ、画像のファイルパス及びアプリケーションデータ構造体へのポインタを引数に取り、 OpenCV の機能を用いて読み込んだ画像を GDI 経由で画面に表示する LoadImage 関数を以下のように定義した。

10分くらいで作ったのでバグってるかも知れないが、いい加減眠いのでこの程度で勘弁してほしい。

  1. HRESULT LoadImage(std::string fileName, AppData* appData) {  
  2.       
  3.     // OpenCV の関数を使って指定されたファイルを読み込み  
  4.     cv::Ptr<IplImage> img = cvLoadImage(fileName.c_str());  
  5.   
  6.     // 読み込みに失敗したらエラーを返す  
  7.     if(img == NULL) return E_FAIL;  
  8.   
  9.     // アプリデータにファイル名をセット  
  10.     appData->imageFileName = fileName;  
  11.   
  12.     // 画像を1または3チャネルの8ビット画像に変換  
  13.     // さらに画像の各ピクセルの格納順をRGBに調整  
  14.     // 必要に応じて、上下反転して、データ構造体に格納  
  15.     appData->iplImage = img;  
  16.     cvConvertImage(appData->iplImage, appData->iplImage,  
  17.         CV_CVTIMG_SWAP_RB | (img->origin == 0 ? 0 : CV_CVTIMG_FLIP));  
  18.       
  19.     HDC hdc = GetDC(appData->hWnd);  
  20.   
  21.     // メモリデバイスコンテキストを作るよ!  
  22.     if(appData->hMemDC != NULL) {  
  23.         DeleteDC(appData->hMemDC);  
  24.         appData->hMemDC = NULL;  
  25.     }  
  26.     appData->hMemDC = CreateCompatibleDC(hdc);  
  27.   
  28.     // ビットマップを作るよ!  
  29.     if(appData->hBitmap != NULL) {  
  30.         DeleteObject(appData->hBitmap);  
  31.         appData->hBitmap = NULL;  
  32.     }  
  33.     appData->hBitmap = CreateCompatibleBitmap(hdc,  
  34.         appData->iplImage->width,  
  35.         appData->iplImage->height);  
  36.   
  37.     ReleaseDC(appData->hWnd, hdc);  
  38.   
  39.     SelectObject(appData->hMemDC, appData->hBitmap);  
  40.   
  41.     // IplImage → HBITMAP  
  42.     COLORREF color;  
  43.     for(int y = 0; y < appData->iplImage->height; ++y) {  
  44.         uchar* ptr = (uchar*)(appData->iplImage->imageData   
  45.             + y * appData->iplImage->widthStep);  
  46.   
  47.         for(int x = 0; x < appData->iplImage->width; ++x) {  
  48.             if(appData->iplImage->nChannels == 3)  
  49.                 color = RGB(ptr[3*x], ptr[3*x+1], ptr[3*x+2]);  
  50.             else  
  51.                 color = RGB(ptr[3*x], ptr[3*x], ptr[3*x]);  
  52.             SetPixel(appData->hMemDC, x, y, color);  
  53.         }  
  54.     }  
  55.       
  56.     return S_OK;  
  57. }  



LoadImage 関数をテストするために、コード冒頭に LoadImage のプロトタイプ宣言を追加し、ウィンドウプロシージャ内の WM_CREATE メッセージを処理する部分を以下のように変更した。

  1. case WM_CREATE:  
  2.     pAppData = (AppData*)new AppData();  
  3.   
  4.     if(pAppData != NULL) {  
  5.       
  6.         /** ここにアプリの初期化処理を書くよ **/  
  7.         /** たとえばデータ構造体の初期化とか **/  
  8.         pAppData->hBitmap = NULL;  
  9.         pAppData->hWnd = hWnd;  
  10.         pAppData->iplImage = NULL;  
  11.   
  12.         // ウィンドウの追加領域にポインタを保存  
  13.         SetWindowLongPtr(hWnd, 0, (LONG_PTR)pAppData);  
  14.   
  15.         /*** テスト:: 画像の読み込み ***/  
  16.         LoadImage("C:\\Users\\tercel\\Pictures\\IMG_1031.JPG", pAppData);  
  17.   
  18.         // ...以下略  

これを実行した結果は以下の通り。
なるほど、確かに画像を読み込んで表示する事に成功している。テスト不足だからどこにバグが潜在しているか怖くて仕方ないけど、とりあえず一回分のネタもできたからこれでよい事にしよう。

最後に、現時点での全ソースコードを掲載する。折りたたんでおくので適宜展開してご覧いただきたい。