ビットマップを高速処理


 VB6がリリースされた後、VBステップアップ講座の第2段を思い立ったのが3年前の98年、月日が経つのは早いものです。その間、更新しなければ...と思いながらも色んな事情が重なって続きを書くことができませんでした。
 2001年も残すところ僅かとなった今日この頃、VBのバージョン7とも言えるVB.NETがリリースされ、VB6もやがてはすてれていくのかなぁなどと感慨に耽る間もなく、日々職業プログラマとしてVBプログラミングに勤しんでいます。
 しかしVB6はリリース当時の私の心配とは裏腹に、まさに大ヒットしたツールだと思います。ビジネスプログラミングの業界ではVBはデファクトスタンダードと言っても過言ではないでしょう。Basicという取っ付きやすい言語で敷居も広く、インターネット上での情報も豊富でVB人口はVB6の発売以来爆発的に増えたようです。しかし裾野は広がったものの、業界ではVBのプログラマはいまだに人手不足状態です。VB.NETでは言語仕様がかなり変更されたようですが、機会があれば取り上げてみたいと思っています。
 昔からBasicといえば「遅い」というのが常識でした。Windows以前のN88-Basic時代から、「Basicといえば入門用にはいいんだけど、実行速度が遅いから実用には向かないね」というのあ通説で、スピードを稼ぐためにアセンブラやC言語に走ったものです。
 Basicも「入門言語」の域を脱し、ビジネスはじめあらゆるアプリケーションの開発に標準で使われれつつあります。一般的なアプリケーションならBasicの最大の欠点である「遅さ」はそれほど目立たなくなてtきました。
 しかし、グラフィックの処理ではやっぱりBasicの「遅さ」が足かせになってしまいます。
 ビットマップ画像のピクセルを処理するには、VBの標準ではPointやPsetメソッドを使用することになっていますが、これらを使って画像全体にエフェクトをかけるような処理をするとうんざりするほど時間がかかってしまいます。

 さてさて、前置きが長くなりましたが、今回はVBで少しでも高速にビットマップを扱えるようAPIを使った例を紹介します。


さて、ここからが本題です。
(今回のサンプルプロジェクトはここを押すとダウンロードできます)
サンプルではファイルサイズを小さくするため、画像を張りつけてない状態で保存しています。最初にプロジェクトファイルを開き、Form1を表示してください。

空のピクチャーボックスがあるので、プロパティのPictureで適当な画像ファイルを指定してピクチャーボックス上に何か画像を表示してください。
選択した画像がピクチャーボックス上に表示されます。。
プログラムを実行してCommand1ボタンを押すと、画像の色が反転します。


プログラムの内容はいたって簡単なものです。

Private Declare Function GetObject Lib "gdi32" Alias "GetObjectA" (ByVal hObject As Long, ByVal nCount As Long, lpObject As Any) As Long
Private Declare Function GetBitmapBits Lib "gdi32" (ByVal hBitmap As Long, ByVal dwCount As Long, lpBits As Any) As Long
Private Declare Function SetBitmapBits Lib "gdi32" (ByVal hBitmap As Long, ByVal dwCount As Long, lpBits As Any) As Long
Private Type BITMAP                                                                     'BITMAP構造体
        bmType As Long
        bmWidth As Long
        bmHeight As Long
        bmWidthBytes As Long
        bmPlanes As Integer
        bmBitsPixel As Integer
        bmBits As Long
End Type
Dim Img() As Byte                                                                       'ピクセルデータ保存用

Private Sub Command1_Click()
    Dim bmp As BITMAP, x As Long, y As Long
    
    GetObject Picture1.Picture.Handle, Len(bmp), bmp                                    'ビットマップ情報取得
    ReDim Img(bmp.bmWidthBytes - 1, bmp.bmHeight - 1)                                   '配列サイズ変更
    GetBitmapBits Picture1.Picture.Handle, bmp.bmWidthBytes * bmp.bmHeight, Img(0, 0)   '画像取込
    For y = 0 To bmp.bmHeight - 1
        For x = 0 To bmp.bmWidthBytes - 1
            Img(x, y) = Not Img(x, y)                                                   '反転処理
        Next
    Next
    SetBitmapBits Picture1.Picture.Handle, bmp.bmWidthBytes * bmp.bmHeight, Img(0, 0)   '画像書込
    Picture1.Refresh
End Sub

今回使用するAPI関数は、GetObject、GetBitmapBits、SetBitmapBitsの3つです。関数の内容は順を追って説明します。
また、ビットマップ情報を入れるためにBITMAP構造体を使用します。これらはVB6に付属のAPIビューアからコピー&ペーストすることができます。
では、Command1_Click()の中を見てみましょう。
GetObject Picture1.Picture.Handle, Len(bmp), bmp

GeObjectは、オブジェクトに関する情報を取り出す関数です。取り出せるオブジェクトは、ビットマップ、 ブラシ、 フォント、 パレット、 ペン、DIB(デバイス独立ビットマップ)で、オブジェクトのハンドルを第1引数に渡すことでそれに見合った情報を返してくれます。
ビットマップのハンドルであるPicture1.Picture.Handleを渡すと、ビットマップに関する情報が返ってくるのでBITMAP構造体型のあるbmpという変数も渡してやります。2番目のLen(bmp)は、関数に受け取る領域の大きさを知らせることで不正なメモリエラーを防ぐためです。

ReDim Img(bmp.bmWidthBytes - 1, bmp.bmHeight - 1)

ここでは、画像に必要なバイト配列を再定義しています。
BITMAP構造体のbmWidth、 bmHeightにはビットマップの幅と高さが入りますが、 実はビットマップの1ライン分の幅=1ライン分のデータ量とはなりません。画像がモノクロなら1ピクセルは2ビットで表せますし、24ビットカラーなら1ピクセルを表現するのに3バイト(24ビット)必要になります。ビットマップの色数はbmBitsPixelに入ります。bmBitsPixelが8であれば画像は8ビットカラーつまり256色ということです。また、bmWidthBytesには1ラインに必要なバイト数が入ります。ただし、この数字は2の倍数になります。

GetBitmapBits  Picture1.Picture.Handle, bmp.bmWidthBytes * bmp.bmHeight, Img(0, 0)

GetBitmapBitsはビットマップのピクセル値をバッファにコピーする関数です。ヘルプではGetBitmapBitsは16ビットバージョンのWindows(3.x以前)との互換用に残されたもので、Windows95以降ではGetDIBits関数を使うことを奨励されていますが、GetDIBitsは扱いが少し面倒になります。Windows2000でもGetBitmapBitsの動作は確認できましたので、ここではより簡単なGetBitmapBitsを使用しています。
3番目の引数でImg(0, 0)としているのは、Img配列の(0,0)番目の要素、つまり配列の先頭を渡しています。ここは実際にはAPI呼び出し時にはImg配列の先頭のアドレスが渡されます。

For文の中では、ビットマップ中のピクセルに対して縦・横順に色を反転しています。

Img(x, y) = Not Img(x, y)

は、Img(x,y)=255-Img(x,y)と書いても構いませんが、ここでNotを使っているのは主に高速化のためです。
この程度のプログラムなら高速化といっても目に見えるほどの効果は上がりませんが、ただでさえ遅いVBを少しでも速くするためのテクニックとでも理解してください。

SetBitmapBits Picture1.Picture.Handle, bmp.bmWidthBytes * bmp.bmHeight, Img(0, 0)

Img配列内で操作したデータをビットマップに返してやります。

Picture1.Refresh)

最後にピクチャーボックスの変更を反映させます。


いかがでしょうか?
もし余裕があればこれと同じ処理をするプログラムをPoint、Pset関数を使って書いて比較してみてください。
Delphiのページで書いたようにメモリ内のビットマップを直接操作することはできませんが、かなり高速な処理ができるようになったと思います。
ただ、上記の例のように簡単な処理なら良いのですが、画像にフィルターをかけるような(つまりループ回数が何倍にもなる)処理では、まだまだVBの遅さは実用に耐えられないですが、工夫次第では十分使える用途があると思います。

戻る