ビットマップ画像ファイルをヘッダファイルに変換するExcel VBAマクロ

VBAマクロ
スポンサーリンク

本記事の概要

概要

ビットマップ画像ファイルをヘッダファイルとして出力するVBAマクロを作成。作成したVBAマクロでは、ビットマップ画像ファイルからバイナリデータを取得し、以下の情報をそれぞれをヘッダファイルに書き出している。

  • 画像の幅と高さ
  • 画素値

※ヘッダファイルをZynq HDMI出力デモに実装し、ヘッダファイル化したサンプル画像がHDMI出力できることも確認。

Zynqを用いたシステムを構築し、HDMI出力アプリケーションや画像処理IPの作成に挑戦しています。アプリケーションの検証に当たり、ビットマップファイルでのテスト画像をヘッダファイル化しておいたほうが何かと便利だったため、今回の記事では所望のビットマップ画像をヘッダファイルに変換するマクロをExcel VBA上で作成しました。

この記事の対象読者
  • ビットマップ画像の構造を理解したい読者
  • ビットマップ画像ファイルを入出力するExcel VBAマクロの例を知りたい方
  • ヘッダファイル化した画像をZynqに読み込ませて画像処理を行いたいエンジニア
ひがし
ひがし

それでは、興味のある方はぜひ最後までご覧ください!

目標と工程

本記事の目標

本記事の目標は次の通りです:

ビットマップ画像をヘッダファイルに変換するExcel VBAマクロの作成

ヘッダファイル化する理由

前回の記事で、静止画をHDMI出力するZynqのアプリケーションを作成しました。アプリケーションでは、メモリ内のフレームバッファから各画素値を読み出して、HDMI形式でディスプレイへ出力しています。フレームバッファには予め赤色一色となるように画素値を入れておきました。

ひがし
ひがし

ただ、赤色一色だけだと画像処理を行っても、結果がよくわかりません…。

そこで、静止画をヘッダファイル化し、フレームバッファに格納しておいて、それを読み出すようにしたいなと考えました。ヘッダファイルに変換するツールとして、本記事では比較的どのPCにもインストールされていて、扱いやすいExcel VBAを使用しています。

フレームバッファに予め所望の静止画を格納する方法は他にもあるかもしれませんが、今回はこのヘッダファイルを作成する方法をとっています。最後に、余談としてHDMI出力の様子も紹介しますので、ぜひご覧いただければと思います。

ひがし
ひがし

では、本記事ではビットマップ画像をヘッダファイルに変換するExcel VBAマクロを作成していきたいと思います!

Excel VBAマクロの紹介

コード例

まず、最初にVisual Basicのコード例を書いておきます。

以下のコードをVisual Basic Editorに貼り付けて、Main関数を実行すると、ファイル選択ダイアログが立ち上がります。ファイル選択ダイアログで所定のビットマップファイルを指定すれば、Excelシートを格納しているパス上にheader.hという名前のヘッダファイルが作成されます。

Private Type RGBTRIPLE
    rgbBlue As Byte
    rgbGreen As Byte
    rgbRed As Byte
End Type
Private Type RGBQUAD
    rgbBlue As Byte
    rgbGreen As Byte
    rgbRed As Byte
    rgbReserved As Byte
End Type
Private Type BITMAPFILEHEADER
    bfType As String * 2
    bfSize As Long
    bfReserved1 As Integer
    bfReserved2 As Integer
    bjOffBits As Long
End Type
Private Type BitmapInfoHeader
    biSize As Long
    biWidth As Long
    biHeight As Long
    biPlanes As Integer
    biBitCount As Integer
    biCompression As Long
    biSizeImaze As Long
    biXPixPerMeter As Long
    biYPixPerMeter As Long
    biClrUsed As Long
    biClrImporant As Long
End Type

'------------------------------------
Sub Main()
    'ファイル選択ダイアログでファイルを指定
    Dim vFilePath As Variant
    vFilePath = Application.GetOpenFilename
    If vFilePath = False Then
        End
    End If
 
    'ファイルサイズが0バイトの場合は処理終了
    Dim nFileLen As Long
    nFileLen = FileLen(vFilePath)
    If nFileLen = 0 Then
        End
    End If
        
    Dim bData() As Byte
    ReDim bData(0 To nFileLen - 1)
 
    '選択されたbmp画像をバイナリデータで取得
    Dim iFile As Integer
    iFile = FreeFile
    
    Open vFilePath For Binary As #iFile
    Get #iFile, , bData
    Close #iFile
  
    If Not (bData(0) = 66 And bData(1) = 77) Then
        MsgBox "bmp画像を選択してください。"
        Exit Sub
    End If
    
    Call CreateBMPtoHeader(bData)
 
End Sub

'------------------------------------
'BMPのバイナリ配列からヘッダファイルを作成
'------------------------------------
Sub CreateBMPtoHeader(ByRef bmpData() As Byte)
    'バイナリデータのヘッダ情報を格納
    Dim biHeader As BitmapInfoHeader
    
    With biHeader
        .biWidth = bmpData(18) + (bmpData(19) * 256) + (bmpData(20) * 256 * 256) + (bmpData(21) * 256 * 256 * 256)
        .biHeight = bmpData(22) + (bmpData(23) * 256) + (bmpData(24) * 256 * 256) + (bmpData(25) * 256 * 256 * 256)
    End With
    
    'バイナリデータのデータ部情報を格納
    Dim rgbData() As RGBTRIPLE
    ReDim rgbData(biHeader.biWidth * biHeader.biHeight)
 
    Dim i As Long
    For i = 1 To UBound(rgbData)
        With rgbData(i)
            .rgbRed = bmpData((i - 1) * 3 + 54 + 2)
            .rgbGreen = bmpData((i - 1) * 3 + 54 + 1)
            .rgbBlue = bmpData((i - 1) * 3 + 54)
        End With
    Next i
    
    'ヘッダーファイルへの書き込み
    Dim fso         As FileSystemObject     '// FileSystemObject
    Dim ts          As TextStream           '// TextStream
    Dim sFilePath   As String               '// ファイルパス
    Dim sLine       As String               '// ファイル行
    
    sFilePath = ThisWorkbook.Path & "\header.h"
    
    Set fso = New FileSystemObject ' インスタンス化
    Set ts = fso.CreateTextFile(sFilePath, Overwrite:=True, Unicode:=False)
    
    Call ts.writeline("//BMP to Header")
    Call ts.writeline("#define X_BMP_SIZE " + Str(biHeader.biWidth))
    Call ts.writeline("#define Y_BMP_SIZE " + Str(biHeader.biHeight))
    
    Dim k As Long
    Dim R As Integer
    Dim G As Integer
    Dim B As Integer
    Call ts.writeline("unsigned char bmptoheader[" + Str(biHeader.biWidth) + "][" + Str(biHeader.biHeight) + "][3] = {")
    
    For i = 1 To biHeader.biHeight
        sLine = "{"
        
        For j = 1 To biHeader.biWidth
            k = (biHeader.biHeight - i) * biHeader.biWidth + j

            With rgbData(k)
                R = Int(.rgbRed)
                G = Int(.rgbGreen)
                B = Int(.rgbBlue)
            End With
            
            sLine = sLine + "{" + Str(B) + "," + Str(G) + "," + Str(R) + "}"
            
            If j <> biHeader.biWidth Then
                sLine = sLine + ","
            End If
            
        Next j
        
        sLine = sLine + "}"
        
        If i <> biHeader.biHeight Then
            sLine = sLine + ","
        End If
        Call ts.writeline(sLine)
        
    Next i
    Call ts.writeline("};")
    
    ' 書き込み処理
    ts.Close ' ファイルを閉じる
    
    ' 後始末
    Set ts = Nothing
    Set fso = Nothing
    
End Sub

注意:もし、マクロの実行時に「エラー ユーザ定義型は定義されていません」というエラーがFileSystemObjectで発生した場合は、以下のようにすれば回避できると思います。

  1. Visual Basic Editorで「ツール」をクリック
  2. 「参照設定」をクリック
  3. ≪参照可能なライブラリファイル≫の一覧から[Microsoft Scripting Runtime]にチェックをつけ、「OK」をクリック

コードの説明

ビットマップ画像の構成

ビットマップ・ファイルの内部のバイナリ構成は図のようになっています。

前半14byteがファイル・ヘッダ、次の40byteが情報ヘッダと呼ばれ、残りの領域には画像データが格納されています。

種類byte数オフセット格納されている情報本記事で紹介するコードにおける構造体
ファイル・ヘッダ140ファイルタイプやファイルサイズなどのファイルに関する情報BITMAPFILEHEADER
情報ヘッダ4014画像の幅(オフセット18、byte数4)や高さ(オフセット22、byte数4)などのビットマップ画像に関する情報BitmapInfoHeader
画像データ任意54ビットマップ画像の画素値のデータRGBTRIPLE

本記事で作成したVBAマクロでは、ファイル・ヘッダから画像の幅と高さに関する情報、画像データから画素値に関する情報を読み出して、ヘッダファイルに書き出しています。

ファイル入出力関数

ビットマップ画像の読み込み

Main関数の中の以下のコードでは、まず指定したパスのビットマップ画像をバイナリデータで取得しています。bDataという配列内にバイトごとにデータを格納しました。

    Dim bData() As Byte
    ReDim bData(0 To nFileLen - 1)
 
    '選択されたbmp画像をバイナリデータで取得
    Dim iFile As Integer
    iFile = FreeFile
    
    Open vFilePath For Binary As #iFile
    Get #iFile, , bData
    Close #iFile
BMPのバイナリ配列から情報ヘッダ(biHeader)と画像データ(rgbData())をまとめた構造体を作成

bDataという配列から所望の画像の幅と高さに関する情報と画像データを取得します。

    'バイナリデータのヘッダ情報を格納
    Dim biHeader As BitmapInfoHeader
    
    With biHeader
        .biWidth = bmpData(18) + (bmpData(19) * 256) + (bmpData(20) * 256 * 256) + (bmpData(21) * 256 * 256 * 256)
        .biHeight = bmpData(22) + (bmpData(23) * 256) + (bmpData(24) * 256 * 256) + (bmpData(25) * 256 * 256 * 256)
    End With
    
    'バイナリデータのデータ部情報を格納
    Dim rgbData() As RGBTRIPLE
    ReDim rgbData(biHeader.biWidth * biHeader.biHeight)
 
    Dim i As Long
    For i = 1 To UBound(rgbData)
        With rgbData(i)
            .rgbRed = bmpData((i - 1) * 3 + 54 + 2)
            .rgbGreen = bmpData((i - 1) * 3 + 54 + 1)
            .rgbBlue = bmpData((i - 1) * 3 + 54)
        End With
    Next i

まず、biHeaderという構造体を定義します。この構造体には情報ヘッダ内の各バイナリデータを格納します。

例えば、画像の幅に関する情報は、ビットマップ画像ファイルにおいて、オフセット18~21(4byte)の領域にリトルエンディアン形式で保存されています。そこで、

$$ W = \mathrm{bData(18)} + (\mathrm{bData(19)} \times 256)+ (\mathrm{bData(20)} \times 256 ^ 2)+ (\mathrm{bData(21)} \times 256 ^3) $$

というふうに、画像の幅\(W \)を配列bDataの18~21番目の要素を用いて計算して取得しました。

リトルエンディアン形式

複数のバイトで構成されるデータを記録・伝送する際の並び順の一つで、最下位のバイトから上位に向けて順に取り扱う方式。例に示したように、18byte目が最下位のバイトで、1byte進むごとに8bit分上位にシフトしているような方式です。

次に、画像データに関しては、RGBTRIPLEというRGB1画素分を一まとまりにした構造体の配列rgbData(i)に保存していきます。

ビットマップ画像ファイルの54byte目からBGRの順に各画素値が格納されていますので、配列bDataから画素値を順次読み出して、配列rgbData(i)に格納しました。

ヘッダファイルの作成

テキスト形式のファイルの作成は、FileSystemObjectというVBAのオブジェクトのCreateTextFileというメソッドを使用しました。作成したテキストファイルはTextStreamというオブジェクトでインスタンス化され、writelineというメソッドで1行ずつテキストを書き込むことができます。

    'ヘッダーファイルへの書き込み
    Dim fso         As FileSystemObject     '// FileSystemObject
    Dim ts          As TextStream           '// TextStream
    Dim sFilePath   As String               '// ファイルパス
    Dim sLine       As String               '// ファイル行
    
    sFilePath = ThisWorkbook.Path & "\header.h"
    
    Set fso = New FileSystemObject ' インスタンス化
    Set ts = fso.CreateTextFile(sFilePath, Overwrite:=True, Unicode:=False)
    
    Call ts.writeline("//BMP to Header")
    Call ts.writeline("#define X_BMP_SIZE " + Str(biHeader.biWidth))
    Call ts.writeline("#define Y_BMP_SIZE " + Str(biHeader.biHeight))

RGB画像データを順番に1行ずつ、以下の形式でテキストファイルに保存していきます。

$$ \{\{\mathrm{B(0)}, \mathrm{G(0)}, \mathrm{R(0)} \}, \{\mathrm{B(1)}, \mathrm{G(1)}, \mathrm{R(1)}\}, \cdots, \{\mathrm{B(511)}, \mathrm{G(511)}, \mathrm{R(511)}\}\}$$

※サンプル画像のビットマップファイルデータの画像幅.biWidthが512でしたので、上式では512画素分を1行分としています

なお、bmp形式では、左から右へ、そして下から上へという順で画素値が指定されています。一方、ディスプレイでは下から上ではなく、上から下へという順で画素値が指定されます。この違いを修正するため、kの計算では上から下になるように(biHeader.biHeight – i)としました。

            k = (biHeader.biHeight - i) * biHeader.biWidth + j
            With rgbData(k)
                R = Int(.rgbRed)
                G = Int(.rgbGreen)
                B = Int(.rgbBlue)
            End With
            
            sLine = sLine + "{" + Str(B) + "," + Str(G) + "," + Str(R) + "}"
            
            If j <> biHeader.biWidth Then
                sLine = sLine + ","
            End If
            

Excel VBAマクロの実行結果

作成したヘッダファイル

VBAマクロを動作させて、次のサンプル画像(画素数640×480)をヘッダファイル化してみました。

ひがし
ひがし

筆者が近くの公園で撮影した画像を使用しています。

我ながらベストショットだと思っています。

得られたヘッダファイルは、以下のような形式で出力されます。
※長いので省略しました

//BMP to Header
#define X_BMP_SIZE  512
#define Y_BMP_SIZE  512
unsigned char bmptoheader[ 512][ 512][3] = {
{{ 57, 22, 82},{ 57, 22, 82},{ 62, 32, 96},{ 62, 28, 93},…
………略………
…,{ 90, 99, 200}}
};

応用例 Zynq HDMI出力デモに実装してみた

前回の記事で作成したHDMI出力のアプリケーション・プロジェクトに、作成したヘッダファイルを組み込んでみましょう。

ソースコードを以下のように変更して、HDMI形式でフレームバッファ内部の画像を出力してみました。

配列frameBufにヘッダファイルで定義した配列bmptoheaderの値を代入しています。メモリ容量が足りない場合は、配列bmptoheaderを直接読み出してもよいかと思いますが、今回はわかりやすさを優先し代入してみました。


/*            略                */
#include "header.h"
/*            略                */

int main(void)
{
/*------------------------------*/
/*            略                */
/*------------------------------*/

    /* frameBufへの書き込み */
    u32 iPixelAddr;
    u32 xcoi, ycoi;
    for(xcoi = 0; xcoi < VMODE_640x480.width; xcoi++)
    {
        iPixelAddr = xcoi * 4;
        for(ycoi = 0; ycoi < VMODE_640x480.height; ycoi++)
        {
            if(xcoi < X_BMP_SIZE && ycoi < Y_BMP_SIZE){
                frameBuf[iPixelAddr]       = bmptoheader[ycoi][xcoi][0];   //blue
                frameBuf[iPixelAddr + 1]   = bmptoheader[ycoi][xcoi][1];   //green
                frameBuf[iPixelAddr + 2]   = bmptoheader[ycoi][xcoi][2];   //red
            }

            iPixelAddr += DEMO_STRIDE;
        }
    }
    Xil_DCacheFlushRange((unsigned int) frameBuf, DEMO_MAX_FRAME);

    return 0;
}

HDMI出力結果

ひがし
ひがし

無事に写真の通り、ディスプレイに想定通りのサンプル画像を出力することができました!

ひがし
ひがし

少しずつですが、画像処理っぽい記事に近づいてきました。次回からは、本格的に画像処理をZynq上で行っていきたいと思います!

ここまで読んでいただき、ありがとうございました!

コメント