DIBを扱う


Win3.1の頃のビットマップはDevice Dependent Bitmap(以下DDB)と呼ばれており, メモリ上に展開されるビットマップの形式はビデオドライバに依存していた. 少ないメモリ上に大きなビットマップデータを保持するためいろいろと工夫する必要があり, その方法が各社のビデオドライバで異なっていたからである. また,DDBでは,ピクセルの色情報などのバイトデータがカーネルモードで管理されていたため, バイトデータに直接アクセスすることは許されておらず, API経由でアクセスするしかなかった.

Device Indenendent Bitmap(以下DIB)は形式がきちんと定義されたビットマップである. DIBのバイトデータはアプリケーションから自由にアクセスできるユーザーモードのメモリ上にあるため, 高速な処理が可能である. 32ビットのメモリ空間が使えるようになってからは,DDBと言えどもDIBと同じ形式を採用していることが多いらしい. DIBと同じ形式を採用することでGDIを使った描画が可能になり,ドライバ開発の労力が軽減されるからである.

DelphiのTBitmapでは特に何も指定しなければDDBが作られる. これはver.2.0までは内部形式がDDBだったので,それとの互換性を保つためらしい. 内部形式をDIBに変更するには,HandleType プロパティでbmDIBに指定すればよい. ただし,中村さんの解説によると, LoadFromFileメソッドでファイルからビットマップを読み込んだ場合は 自動的にbmDIBになる. ファイルの形式は常にDIBであるため,読み込んだ後にわざわざDDBに変換しないらしい.

DIBを作るには,ビットマップの幅,高さ,1ピクセルあたりのビット数等を格納したBITMAPINFO構造体と バイトデータを格納した配列が必要である. CreateDIBitmapはこれらの情報からDIBを作成し,そのハンドル(ビットマップハンドル)を返す. 例えば以下のように使う.

procedure TForm1.Button5Click(Sender: TObject);
var
  BmpInfo: TBitmapInfo;
  ImageWidth, ImageHeight, ImageSize, X, Y, Index: Integer;
  ColorBits: PRGBTriple;
  Bmp: HBITMAP;
  P: Pointer;
begin
   // ビットマップの幅,高さ,サイズ
   ImageWidth := 256;
   ImageHeight := 256;
   ImageSize := ImageWidth * ImageHeight * 3;

   // BmpInfoの各メンバを0で初期化
   FillChar( BmpInfo, SizeOf( BmpInfo ), 0 );
   // 必要な情報を格納
   with BmpInfo.bmiHeader do
   begin
      biSize := SizeOf( TBitmapInfoHeader );
      biWidth := ImageWidth;
      biHeight := ImageHeight;
      biPlanes := 1;
      biBitCount := 24; // R,G,B各8ビットの24ビットとする.これが一番簡単.
      biCompression := BI_RGB;
      biSizeImage := ImageSize;
      biClrUsed := 0;
   end;

   // ピクセルの色情報を格納する配列
   GetMem( P, ImageSize );
   ColorBits := PRGBTRIPLE( P );
   Index := 0;
   for Y := 0 to ImageWidth - 1 do
   begin
      for X := 0 to ImageHeight - 1 do
      begin
         ColorBits^.rgbtBlue := X;
         ColorBits^.rgbtGreen := X;
         ColorBits^.rgbtRed := X;
         Inc( ColorBits );
      end;
   end;
   
   // DIBを作成
   Bmp := CreateDIBitmap( Image.Canvas.Handle, BmpInfo.bmiHeader,
           CBM_INIT, P, BmpInfo, DIB_RGB_COLORS );
   if Bmp <> 0 then
   begin
      Image.Picture.Bitmap.Handle := Bmp;
      Image.Refresh;
   end;
   FreeMem( P );
end;

このようにフルカラー,すなわち24bppのDIBを作成するのは意外と簡単だ. それ以外の色形式ならばカラーテーブルの作成など少々面倒な操作が必要になる. 上記コードを実行すれば以下のようなグラデーションがImageコンポーネントに描かれる.

StretchDIBitsを使えばビットマップハンドルを生成せずに直接 デバイスコンテキストに描くことができる. 上の例なら,CreateDIBitmapを呼び出す代わりに,

   StretchDIBits( Image.Canvas.Handle, 0, 0, ImageWidth, ImageHeight,
                  0, 0, ImageWidth, ImageHeight, P, BmpInfo, DIB_RGB_COLORS, SRCCOPY );

とすればよい.

また,GetObjectを使えば,すでにメモリ上にロードされたDIBのバイトデータへのポインタを 得ることができる. 例えば以下のようにする.

procedure TForm1.Button2Click(Sender: TObject);
var
  DIBInfo: TDIBSection;
  Capacity, Pos: Integer;
  RGBInfo: PRGBTriple;
  Tone: Byte;
begin
   with Image.Picture do
   begin
      Bitmap.HandleType := bmDIB;
      Bitmap.PixelFormat := pf24bit;
      GetObject( Bitmap.Handle, SizeOf( DIBInfo ), @DIBInfo );
      with DIBInfo.dsBm do
      begin
         Capacity := bmWidth * bmHeight;
         Pointer( RGBInfo ) := bmBits;
      end;
   end;

   Pos := 0;
   while Pos < Capacity do
   begin
      Tone := Round( ( RGBInfo^.rgbtRed + RGBInfo^.rgbtGreen + RGBInfo^.rgbtBlue  ) / 3 );
      RGBInfo^.rgbtRed := Tone;
      RGBInfo^.rgbtGreen := Tone;
      RGBInfo^.rgbtBlue := Tone;
      Inc( RGBInfo );
      Inc( Pos );
   end;
   Image.Refresh;
end;

このコードはImageコンポーネントに読み込んだビットマップをグレースケール変換するものである. GetObjectにDIBのビットマップハンドルとDIBSECTION構造体のアドレスを渡せば, dsBmメンバのbmBitsにバイトデータへのポインタが格納されて戻ってくる. ただし,TBitmapのScanLineプロパティを使えばわざわざこんなことをする必要はない.

ちなみに,CanvasのPixelsプロパティを使えば簡単にピクセルの色情報を取得することができる. このプロパティは内部的にGetPixelおよびSetPixelを呼び出しているだけだ. これらのAPIは8bppや24bppといったビットマップの形式に拘わらずピクセルのRGB値を 返したり設定したりしてくれるので大変ありがたいのだが,その分オーバーヘッドも大きい. グレースケール変換などの画像処理では一度に大量のピクセルを処理しなければならないから, バイトデータに直接アクセスする方がはるかに高速だ.

試しに上のグレースケール変換を,Pixelsプロパティを使って

procedure TForm1.Button7Click(Sender: TObject);
var
  ThePixel: TColor;
  Started: DWORD;
  X, Y: Integer;
  R, G, B, Tone: Byte;
begin
   Started := GetCurrentTime;
   with Image.Picture do
   begin
      Bitmap.HandleType := bmDIB;
      Bitmap.PixelFormat := pf24bit;
      for Y := 0 to Bitmap.Height - 1 do
      begin
         for X := 0 to Bitmap.Width - 1 do
         begin
            ThePixel := Bitmap.Canvas.Pixels[ X, Y ];
            R := GetRValue( ThePixel );
            G := GetGValue( ThePixel );
            B := GetBValue( ThePixel );
            Tone := Round( ( R + G + B ) / 3 );
            Bitmap.Canvas.Pixels[ X, Y ] := RGB( Tone, Tone, Tone );
         end;
      end;
   end;
   Image.Refresh;
   ShowMessage( IntToStr( GetCurrentTime - Started ) );
end;

のように書き,PentimIII 600MHzのマシンで960ピクセル X 720ピクセルの フルカラー画像を処理すると約9.2秒要した. 同じ画像を先のGetObjectを使うコードで処理すると,要した時間は約70ミリ秒であった. OSのタスクスイッチング等を考えると実際の処理に要した時間はこの半分以下だろう.

さて,以上のようにDIBは各ピクセルへの高速なアクセスを可能にする大変結構なビットマップなのだが, 逆に言えばピクセル単位でのアクセスしかできないので,複雑な図形の描画には全く不便である. DIBSectionは,デバイスコンテキスト(DC)とDIBとを関連付け, 各種GDIを使ってDIBに自由に描画することができる手段を提供するものである. CreateDIBSectionを使ってDIBを作成し,これをSelectObjectで DCに選択すれば,以降のDCに対する描画がDIBにも反映される. 以下に具体例を示す.

procedure TForm1.Button4Click(Sender: TObject);
var
  BmpInfo: TBitmapInfo;
  DIB, OldBmp: HBITMAP;
  DC: HDC;
  P: Pointer;
  OldBrush, NewBrush: HBRUSH;
  ImageWidth, ImageHeight, ImageSize, X, Y, Count: Integer;
  R, G, B: Byte;
begin
   ImageWidth := 256;
   ImageHeight := 256;
   ImageSize := ImageWidth * ImageHeight * 3;

   FillChar( BmpInfo, SizeOf( BmpInfo ), 0 );
   with BmpInfo.bmiHeader do
   begin
      biSize := SizeOf( TBitmapInfoHeader );
      biWidth := ImageWidth;
      biHeight := ImageHeight;
      biPlanes := 1;
      biBitCount := 24;
      biCompression := BI_RGB;
      biSizeImage := ImageSize;
      biClrUsed := 0;
   end;
   // ImageのCanvasと互換性のあるメモリデバイスコンテキストを作成
   DC := CreateCompatibleDC( Image.Canvas.Handle );
   // DIBSectionを作成
   DIB := CreateDIBSection( DC, BmpInfo, DIB_RGB_COLORS, P, 0, 0 );
   // DCに選択
   OldBmp := SelectObject( DC, DIB );

   // DC に描画
   NewBrush := CreateSolidBrush( clYellow );
   OldBrush := SelectObject( DC, NewBrush );
   Rectangle( DC, 0, 0, ImageWidth, ImageHeight );
   SelectObject( DC, OldBrush );
   DeleteObject( NewBrush );
   for Count := 0 to 999 do
   begin
      R := Round( Random * 255 );
      G := Round( Random * 255 );
      B := Round( Random * 255 );
      NewBrush := CreateSolidBrush( RGB( R, G, B ) );
      OldBrush := SelectObject( DC, NewBrush );
      X := Round( Random * ImageWidth );
      Y := Round( Random * ImageHeight );
      Ellipse( DC, X - 10, Y - 10, X + 10, Y + 10 );
      SelectObject( DC, OldBrush );
      DeleteObject( NewBrush );
   end;

   // DIB を転送
   StretchDIBits( Image.Canvas.Handle, 0, 0, ImageWidth, ImageHeight,
                  0, 0, ImageWidth, ImageHeight, P, BmpInfo, DIB_RGB_COLORS, SRCCOPY );

   Image.Refresh;
   // DCへの選択を解除
   SelectObject( DC, OldBmp );
   DeleteObject( DIB );
   DeleteDC( DC );
end;

実行結果は以下のようになる.

描画自体はGDIを使って行うが,Imageへの描画はStretchDIBitsを使っている. DCへの描画結果がDIBSectionのバイトデータに反映されていることがわかる.


お問い合わせはメールにて: akasaka@klc.ac.jp

戻る
SEO [PR] 爆速!無料ブログ 無料ホームページ開設 無料ライブ放送