none
DIB形式に BITMAPFILEHEADER を付加してImageを生成したい RRS feed

  • 質問

  • いつもお世話になっております。

    あるメーカーが作ったAPIを使い、DIB形式のハンドルを取得しています。
    その後、そのハンドルから Bitmap データをメモリ内に生成し、それをそのまま PictureBox にセットして、イメージを表示しようと思っていますが、
    PictureBox にセットすると、Bitmap データの生成がなにかおかしいのだと思いますが、PictureBox全体が赤いバツ印で表示されてしまいます。
    特に処理中にエラー等は発生しておりません。

    取得したDIB形式のハンドルには、BITMAPFILEHEADER が含まれていない、という事がAPIリファレンスに書かれてありましたので、
    おそらくそれを付加しないといけないのだと思いますが、具体的にどのように処理すればいいのか悩んでいます。
    それ以外にも問題点があるのかもしれませんが。。。

    ヒント等ありましたら、いただけたらと思います。よろしくお願いいたします。

    /* ビットマップ形式のデータを返す関数を抜粋 */
    
    Dim phDIB As Integer
    
    Try
    
        '画像の情報をセット(サンプルソースのため固定値で値をセット)
        Dim width As Integer = 42037   'ページの横幅。単位は1/100mm
        Dim height As Integer = 29718  'ページの高さ。単位は1/100mm
        Dim nHorRes As Integer = 200  '水平方向の解像度。単位はdpi
        Dim nVerRes As Integer = 200  '垂直方向の解像度。単位はdpi
        Dim inchwidth As Decimal = CDec(width) / 2540D 'インチに変換
        Dim inchheight As Decimal = CDec(height) / 2540D 'インチに変換
        Dim pxwidth As Integer = nHorRes * inchwidth  'ピクセルに変換
        Dim pxheight As Integer = nVerRes * inchheight  'ピクセルに変換
    
        'イメージのハンドルを取得
        phDIB = DIBハンドルを取得する関数()
    
        'ハンドルからビットマップデータ生成
        Dim BMP As Bitmap
        BMP = New Bitmap(pxwidth, pxheight, nHorRes, Imaging.PixelFormat.Format8bppIndexed, phDIB)
    
        Return BMP
    
    Catch ex As Exception
        'エラー処理
        Return Nothing
    Finally
        'メモリを解放
        Marshal.FreeHGlobal(phDIB)
    End Try

    2014年8月27日 8:17

回答

  • phDIB は、CF_DIB 形式のビットマップのようですね。BITMAPINFOHEADER から始まる Packed DIBとも呼ばれるビットマップデータを HGLOBAL に格納したものとなります。

    C言語的には、

    VOID* pTop = GlobalLock( phDIB );
    BITMAPINFOHEADER* pHeader = (BITMAPINFOHEADER*)pTop;
    ...
    GlobalUnlock( phDIB );
    GlobalFree( phDIB );
    

    という形でアクセスしていくことになります(各APIの呼び出しは、C形式のままなのでそのままではVBには当てはまらないので要注意)。


    とっちゃん@わんくま同盟, Microsoft MVP for Visual C++ (Oct 2005-) http://blogs.wankuma.com/tocchann/

    • 回答の候補に設定 星 睦美 2014年8月28日 0:16
    • 回答としてマーク ねむねむ 2014年8月28日 5:59
    2014年8月27日 9:36

すべての返信

  • Bitmap constructorの解説によると

    However, the memory should not be released until the related Bitmap is released.

    だそうですが(日本語のページだとshouldの訳が抜けてる…)、Marshal.FreeHGlobalを(このタイミングでは)行わないようにすればどうでしょうか?

    // メモリの管理がめんどくさくなりますが…。

    2014年8月27日 8:23
  • 試しに Marshal.FreeHGlobal の処理をコメントアウトしてみましたが、改善しませんでした。

    また、Bitmapデータを生成した直後に、
    BMP.Save("C:\aaa.bmp")
    という感じでファイルとして保存を試みてみましたが、ExternalException (GDI+ で汎用エラーが発生しました) のエラーが発生していまいましたので、やはりビットマップが上手く作れていないのだと思います。


    2014年8月27日 8:40
  • この Bitmap クラスのコンストラクタの場合、最後は、Scan0 のバッファへのポインタを渡します(詳しくは、リファレンスを参照)。

    pxwidth, pxheight は、画像データのピクセル値(それぞれ、幅と高さ)。

    nHorRes は、phDIB が次のラインの先頭ピクセルへ移動するためのオフセット値(ボトムアップなら通常マイナスになる)。

    PixelFormat.Format8bppIndexed は、phDIB の色深度にあったDIB形式(この場合は、パレットインデックス形式の8ビットカラーとなる)。

    phDIB は、画像のビットイメージを格納した配列データの y = 0 を指すポインタ(C言語的意味でのポインタ)。

    DIBハンドルを取得する関数がどのようなものかわかりませんが、DIBのハンドルで FreeHGlobal で解放されるとすると、CF_DIB 形式のデータなのではないか?と思われます。もしそうだとすると phDIB をそのまま渡すのではなく、BITMAPINFOHEADER 分や、必要があれば RGBQUAD 分などをスキップしたところにビットマップの画像データの先頭(Scan0ではない。バッファ上の数値的意味での最小値位置)があるので、そこから Stride に沿った形で Scan0 の位置を割り出す必要があります。


    とっちゃん@わんくま同盟, Microsoft MVP for Visual C++ (Oct 2005-) http://blogs.wankuma.com/tocchann/

    2014年8月27日 8:52
  • DIBハンドルを取得する関数()  で返ってくる値ですが、
    Win32API の HGLOBAL と同等の型のものが返ってくるようです。

    以下、APIリファレンスの説明ですが、

    イメージに必要なメモリー領域を関数内部で GlobalAlloc() によりアロケートし、それへのハンドルを phDIB を通して帰す。
    返されるメモリー領域には BITMAPINFOHEADER から始まる DIB形式でイメージが格納される(BITMAPFILEHEADER は含まれない)。
    返されたハンドルは、GlobalFree() により解放しなければならない。

    との事です。

    phDIBはC言語的意味でのポインタだと思っていましたが、CF_DIB形式なるものでしょうか?
    画像処理は初めてなものので、コメントいただいた内容をまだ全部呑み込めていないので、少し勉強してまいります。


    • 編集済み ねむねむ 2014年8月27日 9:44 ×CF_BID ○CF_DIB
    2014年8月27日 9:17
  • phDIB は、CF_DIB 形式のビットマップのようですね。BITMAPINFOHEADER から始まる Packed DIBとも呼ばれるビットマップデータを HGLOBAL に格納したものとなります。

    C言語的には、

    VOID* pTop = GlobalLock( phDIB );
    BITMAPINFOHEADER* pHeader = (BITMAPINFOHEADER*)pTop;
    ...
    GlobalUnlock( phDIB );
    GlobalFree( phDIB );
    

    という形でアクセスしていくことになります(各APIの呼び出しは、C形式のままなのでそのままではVBには当てはまらないので要注意)。


    とっちゃん@わんくま同盟, Microsoft MVP for Visual C++ (Oct 2005-) http://blogs.wankuma.com/tocchann/

    • 回答の候補に設定 星 睦美 2014年8月28日 0:16
    • 回答としてマーク ねむねむ 2014年8月28日 5:59
    2014年8月27日 9:36
  • ありがとうございます、少し時間がかかるかもしれませんが、VB.net に置き換えて頑張ってみます。
    進展ありましたら報告させていただきます。
    2014年8月27日 9:51
  • Image.FromHbitmapを使ってHBITMAPからImageに変換できませんか? その後はSaveメソッドを使えるかと。

    # 「DIBハンドル」がHBITMAPのことでなかったらごめんなさい。

    2014年8月27日 10:45
  • Dim BMP As Bitmap
    BMP = Image.FromHbitmap(phDIB)

    を試してみましたが、ExternalException (GDI+ で汎用エラーが発生しました) がキャッチされてしまいました。
    「DIBハンドル」はHBITMAPの事ではないようです。

    引き続きとっちゃんさんの手法でぐりぐり作ってみます。

    2014年8月27日 11:03
  • よく読んでませんでした。DIBハンドルではなく(DIBを格納した)HGLOBALですね。

    1点、補足で、Marshal.FreeHGlobalメソッドは名前に反してLocalFree()を呼び出します。GlobalFree()ではないので気を付けてください。

    2014年8月28日 1:42
  • 何をやればいいかは何となく把握できましたが、時間がかかりそうなので、とりあえずとっちゃんさんの回答をマークとして、完了扱いにさせていただきます。

    実はAPI集の中に、ビットマップファイル作ってくれるAPIがあるので、今回はそちらに逃げます(^_^;)
    ビットマップファイルを作ってまた読み込むよりかは、CF_DIBをImageに変換して表示する方が速度的に早そうなので、チャレンジしてはみましたが、中々難しいですね。

    以下、作りかけてダメだったソースをUpします。
    方向性は合っている・・・と信じたいですがいかがでしょうか。

    /* ビットマップ形式のデータを返す関数を抜粋 */
    
    Dim phDIB As Integer
    Dim lpBuffer As Integer
    Dim iMemSize As Integer
    Try
    
        '画像の情報をセット(サンプルソースのため固定値で値をセット)
        Dim width As Integer = 42037   'ページの横幅。単位は1/100mm
        Dim height As Integer = 29718  'ページの高さ。単位は1/100mm
        Dim nHorRes As Integer = 200  '水平方向の解像度。単位はdpi
        Dim nVerRes As Integer = 200  '垂直方向の解像度。単位はdpi
        Dim inchwidth As Decimal = CDec(width) / 2540D 'インチに変換
        Dim inchheight As Decimal = CDec(height) / 2540D 'インチに変換
        Dim pxwidth As Integer = nHorRes * inchwidth  'ピクセルに変換
        Dim pxheight As Integer = nVerRes * inchheight  'ピクセルに変換
    
        'イメージのハンドルを取得
        phDIB = DIBハンドルを取得する関数()
    
        'グローバルメモリのロック(GlobalLock()はWindows APIを定義して使用)
        lpBuffer = GlobalLock(phDIB)
    
        'グローバルメモリのサイズを取得(GlobalSize()はWindows APIを定義して使用)
        iMemSize = GlobalSize(phDIB)
    
        'BITMAPINFOHEADERの設定(BITMAPINFOHEADER構造体を定義して使用)
        Dim bmi As BITMAPINFOHEADER
        WIth bmi
            .biWidth = pxwidth
            .biHeight = pxheight
            .biPlanes = 1
            .biBitCount = 8  '256色
            .biCompression = 0  '圧縮なし
            .biSizeImage = 0
            .biXPelsPerMeter = 0
            .biYPelsPerMeter = 0
            .biClrImportant = 0
            .biSize = Marshal.SizeOf(bmi)
        End With
    
        Dim mem(iMemSize - 1) As Byte
        'CF_DIBをbyte配列に格納
        Marshal.Copy(lpBuffer, mem, 0, mem.Length)
        'BITMAPINFOHEADERのサイズ分をスキップしてメモリ内に保管し直す
        Marshal.Copy(mem, bmi.biSize, lpBuffer, mem.Length - bmi.biSize)
    
        'ビットマップデータ生成
        Dim BMP As Bitmap
        BMP = New Bitmap(pxwidth, pxheight, nHorRes, Imaging.PixelFormat.Format8bppIndexed, lpBuffer)
    
        Return BMP
    
    Catch ex As Exception
        'エラー処理
        Return Nothing
    Finally
        'グローバルメモリのアンロック(GlobalUnlock()はWindows APIを定義して使用)
      GlobalUnlock(phDIB)
        'メモリを解放
        Marshal.FreeHGlobal(phDIB)
    End Try


    2014年8月28日 6:55
  • API集に GDI+ で作成するというオプションはないのでしょうか?もしそれがあるなら、そのほうが現実的かもしれません。

    ざっくりとコードを見てみましたが、あっているようで近くない。。。という感じですね。

    まず、BITMAPINFOHEADER は自分で作るのではなく、phDIB の先頭にあるものを参照するだけになります。

    DIBの作成のために用意した各種値から、Bitmap クラスを構築するのではなく、実際に存在しているDIBの情報を参照してBitmapクラスを作る必要があります。

    それ以外の部分では、Strideに当たる値をきちんと求めて、なおかつ、コピーしてデータを渡すのではなく、実際の先頭ラインを渡せるようにすればうまくいくと思います。

    言葉で書くと要所だけで大したことはないように見えますが、これをコードにすると最低限(エラーなしのもの)でも、100行程度は必要になるものです。

    探していないのでわかりませんが、もしかしたらCF_DIBからBitmapイメージを作るライブラリがどこかに公開されてるかもしれません。

    それを探してみるというのも一つの解決策かもしれません。

    C++レベルで、簡易的なものですが、ざっとコードを起こしてみて、正直これは .NET でやるものじゃないんじゃね?と思ったんで。


    とっちゃん@わんくま同盟, Microsoft MVP for Visual C++ (Oct 2005-) http://blogs.wankuma.com/tocchann/

    2014年8月28日 8:42
  • DIBからなら、CreateDIBitmap関数でHBITMAP作ってImage.FromHbitmapでどうにかなるかな?

    lpbmih, lpbmiはphDIBそのまま、lpbInitはphDIB + Marshal.ReadInt32(phDIB)。(phDIBにパレットが含まれている可能性があるならその分も必要ですけど)

    2014年8月28日 9:23
  • >API集に GDI+ で作成するというオプションはないのでしょうか?
    無いのです、残念ながら。。。

    VBAのマクロですが、私がやりたい事と近そう(?)なソースをWeb上で発見いたしました。
    しかし、これを理解しつつ.netに置き換えるのは時間的余裕もないので、出来るという事が分かっただけで今回は良しとします。
    一応そのページのリンクを貼らせていただきます。


    エクセルの入り口
    (トップページにあるExcel関係の仕事の欄の、Personal.xlsをクリック→Save Picture as BMP をクリック)


    コメントいただいた皆様、ありがとうございました。

    2014年8月28日 10:56