バイナリ操作


ビットマップ(BMP)ファイルを扱う

最後におまけとして、VBAを使って画像ファイルを操作する方法を紹介します。

普通、画像や音声、動画ファイルなどを編集するためには専用のソフトを使いますが、それらのソフトが行っている内部的な処理はバイナリファイルの操作に他なりません。

VBAではファイルをバイナリモードで開くことができますから、理論上全てのファイルを編集することが可能です。

ここでは実際にVBAによるバイナリファイルの操作を体験するために、画像データ(BMPファイル)をシート上に展開するプログラムを作成し、簡単な解説を行います。


ビットマップ

ビットマップ(bitmap、bmp)ファイルとは、おおざっぱに言えば「色情報が連続して記録された形式」の画像ファイルです。

例えばサイズが100×100の24bitビットマップ画像は10000個の点(画素、ピクセル)で表され、この画像ファイルのデータ部には各画素の色情報(RGBの値)を示す24ビットデータが10000個、連続して並んでいます。24ビットは3バイトですから、どちらかと言えば30000バイトのデータが並んでいると言った方が分かりやすいかもしれません。

ビットマップ形式の詳細に興味があれば、こちらのサイト(BMP ファイルフォーマット)が参考になります。


画像(bmp)を読み込んでシート上に展開するサンプルプログラム

早速ですが、以下にサンプルプログラムを記載します。

このプログラムは、指定したビットマップファイルを読み込んで、それぞれの画素の位置に対応するセルの背景色を指定された色に変更するものです。

1画素に対して1セルを使用するため元の画像が大きいと困りますから、縮小倍率を指定することができるようにしてあります。縮小すると色情報を間引くため、モザイクをかけたような画像になります。

単純にセルのサイズを大きくすればエクセル上ではモザイク画像に見えるようになりますが、画面からはみ出してしまうことにでしょう。下のサンプルプログラムでは、管理人の環境で1セルが2×2ピクセルになるような値をセルの幅に指定しています。

※以下のサンプルコードを訂正します。


'画像ファイルをシート上に展開するプログラム
Option Explicit
Sub BitmapTest()
    Dim filename As String 'ファイル名
        filename = ThisWorkbook.Path + "\hiyoko.bmp" '読み込むファイル
    Dim ff As Long 'フリーファイル
        ff = FreeFile 'フリーファイル変数
    Dim databuf() As Byte 'バイト配列
    
    Open filename For Binary As #ff 'バイナリモードでファイルを開く
        ReDim databuf(LOF(ff)) 'バイト配列のサイズをセット
        Get #ff, 1, databuf 'バイト配列にデータを格納
    Close #ff
    
    Dim image_power As Long '画像の倍率
        image_power = 1 / 0.5 '倍率を指定
    Dim bmp_width As Long '画像の横サイズ
        bmp_width = HexToDec(databuf, 18, 21) '4バイト
    Dim bmp_height As Long '画像の縦サイズ
        bmp_height = HexToDec(databuf, 22, 25) '4バイト
    Dim bmp_bit As Integer '画像のビットサイズ
        bmp_bit = HexToDec(databuf, 28, 29) '2バイト
    Dim file_size As Long 'ファイルサイズ
        file_size = HexToDec(databuf, 2, 5) '4バイト
        
    Cells.Clear 'セルをクリア
    Range(Columns(1), Columns(bmp_width / image_power)).ColumnWidth = 0.16 'セルの横幅
    Range(Rows(1), Rows(bmp_height / image_power + 1)).RowHeight = 1.5 'セルの縦幅
    
    Dim pos As Long 'データオフセット
    Dim w_index As Long 'ビットマップの横位置
    Dim h_index As Long 'ビットマップの縦位置
        w_index = 1 '横の初期位置座標(左)をセット
        h_index = Fix(bmp_height / image_power) + 1 '縦の初期位置座標(下)をセット
    
    Dim height_count As Long '縦倍率変更カウンタ
    
    '以下3バイト(24bit)ずつループ
    For pos = 54 To file_size - 3 * image_power Step 3 * image_power
        Cells(h_index, w_index).Interior.Color = _
            RGB(HexToDec(databuf, pos + 2, pos + 2), _
            HexToDec(databuf, pos + 1, pos + 1), _
            HexToDec(databuf, pos, pos)) 'データに対応するセル背景色にRGBを指定
            
        w_index = w_index + 1 '横座標を右へ移動
        If w_index > Fix(bmp_width / image_power) Then  '右端到達
            pos = pos + (image_power - 1) * bmp_width * 3 '倍率に応じてポインタ移動
            w_index = 1 '横初期位置(左端)へ移動
            h_index = h_index - 1 '縦座標を上へ移動
        End If
        DoEvents
    Next
    MsgBox "画像の展開が完了しました" '処理完了
End Sub

'連続したバイト配列の値を10進数に変換する関数
Function HexToDec(ByRef databuf, ByVal first, ByVal last) As Long
    Dim i As Long 'ループカウンタ
    Dim temp As String '16進数を格納する文字列配列
        temp = ""
    For i = last To first Step -1 '後ろから処理
        temp = temp + Right("00" & Hex(databuf(i)), 2) '10進数を16進数に変換
    Next
    HexToDec = Val("&H" & temp) '16進数を10進数に変換
End Function


Option Explicit
'画像ファイルをシート上に展開するプログラム
Sub test()
    Dim filename As String 'ファイル名
        filename = ThisWorkbook.Path + "\hiyoko.bmp" '読み込むファイル
    Dim ff As Long 'フリーファイル
        ff = FreeFile 'フリーファイル変数
    Dim databuf() As Byte 'バイト配列
    
    Open filename For Binary As #ff 'バイナリモードでファイルを開く
        ReDim databuf(LOF(ff)) 'バイト配列のサイズをセット
        Get #ff, 1, databuf 'バイト配列にデータを格納
    Close #ff
    
    '=====================================================================================
    Dim image_power As Long '画像の倍率
    '=====================================================================================
        image_power = 2    '倍率を1/image_powerで指定
    '=====================================================================================
    Dim bmp_width As Long '画像の横サイズ
        bmp_width = HexToDec(databuf, 18, 21) '4バイト
    Dim bmp_height As Long '画像の縦サイズ
        bmp_height = HexToDec(databuf, 22, 25) '4バイト
    Dim bmp_bit As Integer '画像のビットサイズ
        bmp_bit = HexToDec(databuf, 28, 29) '2バイト
    Dim file_size As Long 'ファイルサイズ
        file_size = HexToDec(databuf, 2, 5) '4バイト
        
    Cells.Clear 'セルをクリア
        
    Range(Columns(1), Columns(bmp_width / image_power)).ColumnWidth = 0.31 'セルの横幅
    Range(Rows(1), Rows(bmp_height / image_power)).RowHeight = 3 'セルの縦幅
        
    Dim width_size As Long 'セル上の横サイズ
        width_size = Fix(bmp_width / image_power)
    Dim height_size As Long 'セル上の縦サイズ
        height_size = Fix(bmp_height / image_power)
        If height_size = 0 Then
            MsgBox "指定した倍率が小さすぎます", vbExclamation
            Exit Sub '高さが0になる場合、終了
        End If
        
    '=====================================================================================
    '倍率を考慮して補正するバイト数を調整
    '=====================================================================================
    Dim widthcount As Long '横のデータ数
    Dim addpos As Integer '埋めるバイト数
    widthcount = Fix(bmp_width * 3)
    If widthcount Mod 4 > 0 Then '※widthが4の倍数に満たない場合、横の実データ数を求める
        widthcount = Fix(widthcount / 4 + 1) * 4
        '例
        'widthcount = 192 * 3 = 576 → widthcount Mod 4 = 0バイト埋める → 1列 = 576バイト
        'widthcount = 191 * 3 = 573 → widthcount Mod 4 = 3バイト埋める → 1列 = 576バイト
        'widthcount = 190 * 3 = 570 → widthcount Mod 4 = 2バイト埋める → 1列 = 572バイト
        'widthcount = 189 * 3 = 567 → widthcount Mod 4 = 1バイト埋める → 1列 = 568バイト
    End If
    addpos = widthcount - width_size * image_power * 3 '倍率に応じた不足分を調整
    '=====================================================================================
    
    Dim pos As Long 'データオフセット
    Dim w_index As Long 'ビットマップの横位置
    Dim h_index As Long 'ビットマップの縦位置
        w_index = 1 '横の初期位置座標(左)をセット
        h_index = height_size '縦の初期位置座標(下)をセット
    
    pos = 54 '初期値(データ先頭位置)
    For h_index = h_index To 1 Step -1 '高さのループ
        For w_index = 1 To width_size '幅のループ
            Cells(h_index, w_index).Interior.Color = _
                RGB(HexToDec(databuf, pos + 2, pos + 2), _
                HexToDec(databuf, pos + 1, pos + 1), _
                HexToDec(databuf, pos, pos)) 'データに対応するセル背景色にRGBを指定
            pos = pos + 3 * image_power '横移動
        Next
        pos = pos + addpos '4バイト区切りの不足分を加算(posを調整)
        pos = pos + widthcount * (image_power - 1) '列×倍率分飛ばす(縦移動)
    Next
    MsgBox "画像の展開が完了しました" '処理完了
End Sub

'連続したバイト配列の値を10進数に変換する関数
Function HexToDec(ByRef databuf, first, last) As Long
    Dim i As Long 'ループカウンタ
    Dim temp As String '16進数を格納する文字列配列
        temp = ""
    For i = last To first Step -1 '後ろから処理
        temp = temp + Right("00" & Hex(databuf(i)), 2) '10進数を16進数に変換
    Next
    HexToDec = Val("&H" & temp) '16進数を10進数に変換
End Function

実行すると以下のようになります。

元の画像

エクセルに展開した画像

※ここで使用した画像はGATAG|フリー画像・写真素材集 4.0よりダウンロードしたもので、著作者はElmar Krenkel氏です。
クリエイティブ・コモンズ・ジャパン


サンプルプログラムの解説

以下は興味のある方向けの簡単な解説です。

上記ヒヨコのBMPファイルはサンプルプログラム用にリサイズしてありますので、プログラムを試してみる場合はエクセルと同じフォルダに配置して実行してください。

「filename = ThisWorkbook.Path + "\hiyoko.bmp"」でエクセルファイルと同フォルダに置かれた「hiyoko.bmp」のフルパスをセットしています。

バイナリデータ

ここで使用したBMPファイルの内容な以下のようになっています。画像はバイナリエディタでファイルを開いたところです。

BMPファイルをバイナリエディタで開いた画像

画像データといえども、中身はこのように単なるビットデータの羅列です。

それぞれがどのような意味を持つかさえ分かりさえすれば、解析したり編集したりすることが可能です。

例えばこのBMPファイルでは0x0000からの2バイトがファイルタイプ、0x0002からの4バイトがファイルサイズ、0x000Aからの4バイトがファイル先頭から画像データまでのオフセット、0x0012からの4バイトが画像の横サイズ(ピクセル)、0x0016からの4バイトが画像の縦サイズ(ピクセル)、0x001Cからの2バイトが画素のデータサイズ(bit)を表しています。

つまり、上のバイナリデータを見ればこれは種類がBMPで、ファイルサイズが90054バイト、画像サイズが200×150ピクセル、ビットデータサイズ24bitの画像ファイルで、データ部の開始位置が先頭から54バイト目だということが分かります。

HexToDec(ByRef databuf, ByVal first, ByVal last)について

この関数は、複数バイトの16進数を10進数に変換するために用意した関数です。

VBAには10進数を16進数に変換するHex関数が用意されていますから、これを利用しています。

指定した範囲内を1バイト単位で16進数に変換して桁数を調整して一つの16進数表記にしたのち、Val("&H" & temp)で10進数に変換しています。

例えば上のファイルでサイズを表す0x0002からの4バイト、「C6 5F 01 00」はそれぞれ10進数で「198 95 1 0」になります(VBA上で自動的に変換されて表示されます)が、これでは何の事だかわかりません。

このデータは連続したバイトデータが下位バイトから並べられるリトルエンディアン形式であるため、先頭バイトが00、最終バイトがC6となり、16進数表記「00015fC6」をHex関数で10進数に変換すると「90054」という10進数のファイルサイズが得られます。

3バイト(24bit)ずつループ

まず最初に画像をバイナリモードで開き、バイナリデータを解析して必要な画像情報を取得しておきます。

あとは54バイト目(データ部の開始位置)からデータが終了するまで、3バイトずつ画素データを取得して、対応するセルにコピーしていきます。3バイトというのは、このBMPファイルが24bitビットマップで、1ピクセルの情報が3バイトで表現されているからです。

ビットマップファイルでは基本的に左から右、下から上へデータが並べられているため、右端に到達したら一つ上の左端のセルに移動し、処理を繰り返します。データの並び方が逆になっているファイルでは、画像の高さを示す値が負数になっています。

訂正について(2015/4/16)
ビットマップファイルの仕様では横方向のデータ数を4の倍数とすることになっています。
例えば横サイズが191ピクセルの24ビットBMPであれば、画素を示すデータ数は191×3=573バイトですが、これは4の倍数(576バイト)に満たないため、横のデータ行ごとにこの3バイトが0で埋められています。
訂正前のサンプルではこのようなサイズのBMPファイルには対応しておらず、かつ倍率の指定方法(小数で指定した倍率の逆数を整数型に格納)や倍率を変更した際のポジション(pos)調整等も不十分だったため、諸々合わせて訂正させていただきました。
なお、訂正前はループカウンタとしてポジションを使用していましたが、流れをよりわかりやすくするために対象セルの数を使用するように変更しています。
訂正前のサンプルを参考にして悩まれた方がいらっしゃいましたら申し訳ありません。
メールをくださった方、親切なご指摘ありがとうございました。

その他

その他コメントを見れば何をしているか分からないということはないと思いますが、サンプルプログラムには多少不適切と思われる部分が見つかるかもしれません。

スポンサード リンク

例えば3バイトループのため24bitのビットマップしか処理できない、データの最終位置をファイルサイズを使って判定している、倍率変更時に常に左側のデータが優先される、など。

余裕があれば、サンプルを参考にカスタマイズしてみるのも練習になると思います。

また、ここでは倍率を変更した画像をエクセルに展開していますが、元のファイル自体を編集したり、編集後の画像ファイルを新規作成することもできますから、そちらも試してみると面白いでしょう。

ただし、元のファイルを編集してしまうと復元ができないので注意してください。コピーを作成してから試してみるとよいでしょう。


最後に

実際のところ、上記のようなプログラムを使ってシート上に画像を表示するメリットは、セルの性質上「画像がモザイクっぽくなって面白い」という効果くらいしかないかもしれません。プログラムで画像を編集するにしても、様々なプログラミング言語で様々なファイル形式をを扱うためのクラス等が予め用意されており、効率面からいえばVBAでしかもバイナリデータを直接1バイトずつ追ってやる必然性はありませんし、実際にVBAで作業している人は少ないでしょう。

しかしながら、結局のところ内部的になにが行われているかと言えば、プログラムがバイナリデータを解析して処理しているに過ぎません。そこで行われている生の処理を学ぶという意味では、VBAという身近なツールを使って上記のバイナリファイルを解析するのも有意義だと考えられます。

さてここまでくれば、VBAを使って色々なプログラムを作ることができるようになっているはずですし、分からないことを調べたとき、その説明を理解することが容易になっているでしょう。

ここまでお読みいただき、ありがとうございました。

このサイトが、VBAを勉強したいという方の道標の一つとなることを願っています。

以上