2013. 7.30.
石立 喬

Visual Studio 2012 Expressの易しい使い方(6)

――― Win32 で、画像を読み込み、簡単な画像処理を行う ――

 Win32 での、ごく入門的な画像処理の例として、ビットマップ画像を読み込んで表示し、次いで、簡単で分かり易いGetPixelとSetPixelVを用いて、画像の反転(ネガ化)やモノクロ化を試みる。
 しかし、GetPixel関数とSetPixel関数の遅さには定評があり、それを避ける方法が多く知られている。ここでは、ビットマップ画像を、個々のピクセルデータとして配列化するGetDIBits関数を使用し、得られたピクセルデータに対して画像処理を行った後に、SetDIBitsToDevice関数でピクセルデータを画面に直接表示させる方法を紹介する。
 GetPixelとSetPixelVを用いて約3秒かかった処理が、GetDIBitsとSetDIBitsToDeviceでは、一瞬で終了した。

画像の読込み方
 .NET Frameworkでは、たとえば、
   Bitmap^ bmap1=gcnew Bitmap("abcd.jpg");
、あるいはJavaでは、
   Image image1=getImage(getDocumentBase(),"abcd.gif");
などで、比較的容易に、各種の画像フォーマットをファイルから読み込むことができる。しかし、Win32アプリケーションでは、特別なライブラリを導入する以外には、Bitmapしか使えない。ここでは、画像処理が主目的であるので、画像は、ビットマップ形式で用意する。読み込みに使用するのは、LoadImage関数で、
HANDLE LoadImage(
HINSTANCE hinst, インスタンスのハンドルであるが、NULLで良い
lpszName, 画像のファイル名、拡張子も含める
uType, 画像のタイプ、IMAGE_BITMAPとする
cxDesired, 希望するX座標、0にする
cyDesired,     希望するY座標、0にする
fuLoad 取り込みのオプション、LR_FROMFILEとする
);
となっている。
 返り値は、一般的なHANDLE型なので、Bitmapのハンドルに合わせるため、頭に(HBITMAP)を付けてキャストする。
 LoadImageが失敗すると、NULLが返されるので、これで、MessageBoxを表示する。

画像の準備
 簡単に使用できる画像の種類はビットマップに限られる。プログラムを簡単にするために、ここでは、画像のサイズを、幅320ピクセル、高さ240ピクセルに定める。ここで使用する画像ファイルは、
   「ライブラリ」→「ドキュメント」→「Visual Studio 2012」→「Win32Project4」→「Win32Project4」
の中に入れておく。「Win32Project4」は、ここで使用しているプロジェクト名である。

画像を画面へ表示する方法
 図1は、画像の読込みから、画面へ表示するまでの過程を示したもので、以下の関数を順次使用する。
 1)既に述べたLoadImageで、ファイルから画像を読み込み、ビットマップ(ハンドルはhBitmap)を生成する。
 2)CreateCompatibleDCで、デバイスコンテキスト(DC)と等価なメモリ領域であるメモリDC(ハンドルはhMemoryDC)を生成する。
 3)SelectObjectで、hBitmapをhMemoryDC上に展開し、それまでのビットマップをhOldBitmapに格納する。
 4)以下に示すBitBlt関数で、hMemoryDC上の内容を、hdcにコピー(画面上に表示)する。
BOOL BitBlt(  
HDC hdcDest, 表示先のデバイスコンテキスト(DC)のハンドル、ここではhdc
int nXDest, 表示先のX座標、0にする
int nYDest, 表示先のY座標、0にする
int nWidth, 画像の幅、ここではWIDTH
int nHeight,    画像の高さ、ここではHEIGHT
HDC hdcSrc, コピー元のDCのハンドル、ここではhMemoryDC
int nXSrc, コピー元のX座標、0にする
int nYSrc, コピー元のY座標、0にする
DWORD dwRop ラスターオペレーションの指定で、SRCCOPYとする
);

 X)後始末をする。SelectObjectで、元のビットマップに戻し、DeleteObjectで、hBitmapを破棄し、DeleteDCで、hMemoryDCを破棄する。このプログラムの場合、これらは念のためであり、順序は任意である。
 hdcは、UserProgramの外で、BeginPaintによって生成され、描画の終了後は、EndPaintによって破棄される。


図1 画像の読込みから、画面への表示まで


GetPixelとSetPixelVを使用する場合のUserProgram
◎画像の読み込みと表示
 図2は、UserProgramの最初の部分を示す。まず、LoadImageで画像を読み込み、それを、BitBlt関数で画面の左上に表示する。扱う画像のサイズを、WIDTH=320、HEIGHT=240で定義する。X0〜X3、Y0〜Y3は、複数(4個)の画像を画面上に並べて配置するための座標である。
 なお、ここでは、SelectObjectによる、旧ビットマップの回復は省略してある。


図2 UserProgram の最初の部分(画像の読み込みと表示)


◎簡単な画像処理
 図3は、GetPixel関数とSetPixelV関数を使った、簡単な画像処理のプログラムを示す。これらの関数は遅いのが有名であるが、わかりやすく、プログラムが簡単である。 SetPixelV 関数は、SetPixel関数より若干速いとされ、指定した画面上の点 に、指定したCOLORREFを書き込む。
 まず、GetPixelで、特定点のピクセルの値をcrColorとして取得し(217行)、それを、そのまま、SetPixelV関数で描画する(画面の座標位置を変えて)(218行)。これは、画像のコピーに相当する。
 次に、画像処理に取り掛かる。crColorはCOLORREF型なので、GetRValue()などのマクロで、R、G、B成分に分解できる(220行〜222行)。R、G、B各成分を、255-R、255-G、255-Bのように逆転させ、RGB()マクロで、再びCOLORREF型に合成して、新しいcrColorを生成し(223行)、SetPixelVで描画する(224行)と、画像の反転ができる。
 一方、簡単な計算式を用いて明るさを求め(226行)、それをR、G、Bに同じ値で与えて、RGB()マクロでcrColorに変換し(227行)、SetPixelVで描画する(228行)と、画像のモノクロ化ができる。


図3 UserProgram の後半(簡単な画像処理と結果の表示)


得られた結果
 図4は、得られた結果を示す。左上は原画像、右上は、参考のために、一旦分解してから復元した原画像のコピー、左下は、反転(ネガ)画像、右下はモノクロ化画像である。
 GetPixelやSetPixelV の実行回数を減らす工夫をしたが、それでも、かなり遅い(約3秒かかった)。


図4 GetPixel関数とSetPixelV関数で得られた結果


GetDIBitsとSetDIBitsToDeviceを使用する高速なUserProgram
◎ピクセルデータ配列lpcrPixelDataの準備と、ビットマップヘッダ情報の設定
 作成したUserProgramの最初の部分を図5に示す。GetDIBits関数を使用する場合は、ビットマップ情報をピクセルデータ配列に変換するので、COLORREF型の一次元配列lpcrPixelDataを用意する。さらに、それを画像処理した後のピクセルデータを格納するために、lpcrPixelData1、lpcrPixelData2も用意する(197行〜199行)。
 また、ビットマップ画像データに含まれるヘッダ部分も必要になるので、これをbmpInfoとして用意する。図5のbmpInfoの設定は、BITMAPINFOHEADER構造体の要素のすべてではないが、省略しても支障が無い。
 GetDIBits関数とSetDIBitsToDevice関数は以下の通り(Microsoftの資料による)。
int GetDIBits(
HDC hdc, DCのハンドル
HBITMAP hbmp, ビットマップのハンドル、ここではhBitmap
UINT uStartScan, ビットマップのスタート行、0にする
UINT cScanLines, ビットマップのスキャン行数、ここではHEIGHT
LPVOID lpvBits,    取得先の配列のポインタ、ここではlpcrPixelData
LPBITMAPINFO lpbi, ビットマップ情報のポインタ、ここでは&bmpInfo
UINT uUsage RGBかパレットか?ここではDIB_RGB_COLORS
);
     
int SetDIBitsToDevice(
HDC hdc, DCのハンドル、ここではhdc
int nXDest, 表示先のX座標、0にする
int nYDest, 表示先のY座標、0にする
DWORD dwWidth, 画像の幅、ここではWIDTH
DWORD dwHeight,    画像の高さ、ここではHEIGHT
int nXSrc, 元のX座標、0にする
int nYSrc, 元のY座標、0にする
UINT uStartScan, 元のスタート行、0にする
UINT cScanLines, 元のスキャン行数、ここではHEIGHT
CONST VOID *lpvBits 元の配列のポインタ、ここではlpcrPixelData1など
CONST BITMAPINFO *lpbmi, ビットマップ情報のポインタ、ここでは&bmpInfo
UINT fuColorUse RGBかパレットか?ここではDIB_RGB_COLORS
);
 ※なぜか、GetDIBitsと、表現の統一性がない


図5 UserProgramの最初の部分(ピクセルデータの配列の準備とビットマップ情報の作成)


 ビットマップは、そのままでは、画面の左下が座標の起点となる。このままでも、特に不都合はないが、HIGHTをマイナスで与えると、通常通り、左上を起点にすることができる。非圧縮の場合、biCompressionはBI_RGBとする。
 なお、BITMAPINFO構造体は、BITMAPINFOHEADER構造体とRGBQUAD型から成るので、上記の代わりに、図6のようにしても良い。カラーパレットを使用しない場合は、RGBQUAD型は不要である。


図6 BITMAPINFOの別の設定方法(間接的)


◎画像のコピー
 図7に示す通り、GetDIBitsでlpcrPixelDataを取得し(224行)、それをそのまま使用して、SetDIBitsToDeviceで画面に表示する(227行)。それに先立つ、画像の読み込みと表示の部分は、GetPixel、SetPixelVを使った場合と全く同じである。


図7 画像をコピーするプログラム(読込みと表示の部分も含む) 


◎簡単な画像処理
図8は、画像を処理してから、画面へ表示するまでをフローチャートで示したもので、以下の関数を順次使用する。
 1)既に述べたGetDIBits関数で、hBitmapを、lpcrPixelData一次元配列に変換して読み込む。
 2)lpcrPixelDataの添え字を、0からWIDTH*HEIGHT-1まで一次元的に変化させて逐次読取り、画像処理をして、新しくlpcrPixelData(別の配列lpcrPixelData1等でも良い)に格納する。
 3)SetDIBitsToDevive関数で、lpcrPixelData配列を、直接hdcに描き出す。hMemoryDCは、関与しないので、これの破棄後でも使用できる。


図8 GetDIBitsとSetDIBitsToDeviceを用いるフローチャート


 図9は、GetDIBitsとSetDIBitsToDeviceを使った、簡単な画像処理のプログラムを示す。
 まず、hBitmapのビットマップ情報を、lpcrPixelDataの一次元配列に変換する。その際、対象とするビットマップの構成を指示し、フルRGBに変換することも指示する(224行)。
 得られたlpcrPixelData一次元配列を、何の操作を加えずに、そのまま、SetDIBitsToDevice関数に渡す。ビットマップ構成と、フルRGBであることも指示する(227行)。これにより、原画像がコピーされる。
 lpcrPixelData一次元配列を、順次読み出し、GetRValue()などのマクロで、R、G、B成分に分解する(231行〜233行)。
 R、G、B各成分を、255-R、255-G、255-Bのように逆転させ、RGB()マクロで、COLORREF型に合成して、lpcrPixelData1配列を生成して(235行)、逆転画像のデータを用意する。
 簡単な計算式を用いて明るさを求め、それをRGB()マクロでCOLORREF型に変換し、lpcrPixelData2配列に入れて(237行〜238行)、モノクロ化画像のデータを用意する。
 以上で作成したlpcrPixelData1とlpcrPixelData2の配列を、SetDIBitsToDevice関数で画面上に描画する(242行〜243行)。
 最後に、取得したメモリー領域を解放しておく(245行〜251行)。


図9 簡単な画像処理と結果の表示


 PixelDataを画面に表示するのに、SetDIBitsToDeviceを用いたが、SetDIBits関数を用いてビットマップ化してから、BitBlt関数で画面に表示してもよい。図10は、確認のために、書き換えたものを示す。SelectObject(hBitmap,hMemoryDC); は、既に設定されているので、必要ない。
 画像処理後の画像を、ファイルなどに書き出す場合には、このような、ビットマップ化が必要になる。
 図11は、SetDIBits関数を用いる場合のフローチャートである。


図10 SetDIBitsToDeviceの代わりにSetDIBitsを使用するプログラム



図11 少し面倒だが、SetDIBitsとBitBltの併用でも良い


得られた結果
 図12は、GetDIBits関数とSetDIBitsToDevice関数を使用して得られた結果を示す。実行時間は一瞬であった。


図12 GetDIBits関数とSetDIBitsToDevice関数で得られた結果