BMPファイルフォーマット(簡易版)

作成:

PNM(PPM/PGM/PBM)画像に引き続いて説明するのは、 BMP(Microsoft Windows BitMaP Image)画像形式だ。 単にビットマップとも呼ばれる形式で、主にWindowsで利用される形式だ。 これも以前説明したことがある形式だが、改めて説明する。

BMP形式をビットマップと呼んだり、ビットマップ画像といえばBMP形式を指すことが多いのは事実だが、 ビットマップとは本来画像の表現形式で、 ピクセルの集合によって表現される画像をビットマップ画像という。 例えば、PBM形式はPortable BitMapから来ている。 この意味では混同を避ける目的も有りラスター画像という用語もよく使われる。

BMP形式と呼ばれ、拡張子も.bmpが一般的だが、 DIB(Device Independent Bitmap)形式と呼ばれることもある。 DIBの名前から分かるように、デバイスに依存しない画像形式を意識して作られている。 画像のデータ構造で説明しているが、 一般的なディスプレイやプリンタなどのデバイスは、画像を左上から右方向に走査しながら下に向かって処理を行うため。 画像データも左上を原点としてx軸は右方向、y軸は下方向を正とする座標系を取ることが多い。 しかし、この形式は数学的なx軸y軸の方向を採用していて、 左下が原点で、x軸は右、y軸は上を正の方向とする座標系を採用している。 つまり、画像内のピクセル情報は下から上に向かって格納されるところが特徴である。

ただし、マルチバイトの解釈はリトルエンディアンになっている。 そのためリトルエンディアン以外の環境では読み書きの際エンディアンの変換が必要となる。

このファイルフォーマットはIBMとMicrosoftがOS/2を共同で開発した時代に作られており、 その後何度かのバージョンアップが行われ、いくつかのバリエーションが生まれている。 初期のものはOS/2ビットマップ、現在多く使われているものはWindowsビットマップなどと呼び分けられている。 さらに、Windowsビットマップにも新しいバージョンがある。 だだし、新しいバージョンには対応しているツールは少なく、Windowsの標準ツールですら対応していなかったりする。

非圧縮画像の代表例のように思われがちであるが、カラーパレット形式や、その圧縮形式も定義されている。 しかし、同様の特徴を持つ他の方式に比べ効率が悪く、圧縮形式には対応していないツールも多い。 実利用上BMPファイルはフルカラーの非圧縮形式となっている場合がほとんどである。

このように、単にBMP形式と言っても様々なバリエーションがあり複雑だ。 しかし、BMPで多く使われるフォーマットは非圧縮のフルカラーであり、 OS/2ビットマップが使われることは現代ではほぼないといって良いだろう。 つまり、Windowsビットマップのフルカラーに限定してしまっても、手元で使うツールぐらいであれば十分な対応レベルになるだろう。 そこで、まずはWindowsビットマップのフルカラーに絞って説明していこうと思う。 その後で、より詳細に各バリエーションについて説明していく。

詳細版の説明でも、簡易版で説明済みの内容も含めて説明しており、 そこから読んでも問題ないようにしているので、 中途半端な簡易版の説明は不要という場合はこのページはスキップしてもらって問題ない。

ファイルヘッダ

早速Windowsビットマップについて説明していくのだが、 このファイルヘッダに関してだけはどのバリエーションでも共通になっている。 MicrosoftのMSDNドキュメント BITMAPFILEHEADER structure に解説が書かれているが、以下のようになっている。

typedef struct tagBITMAPFILEHEADER {
  WORD  bfType;
  DWORD bfSize;
  WORD  bfReserved1;
  WORD  bfReserved2;
  DWORD bfOffBits;
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;

WindowsSDKなどに不慣れな人はWORDDWORDってなんだ?と思うことだろう。 これも、MSDNの Windows Data Types などに説明がある。Windowsに関わるプログラミングを行う場合、ひと通り見ておいたほうがいいだろう。 要するに、ビット長を固定にしたり、符号の有無を含めてtypedef定義された型である。 今回必要な物、関連しそうなものをピックアップすると以下の4種類だろう。

BYTE
符号なし8bit整数
WORD
符号なし16bit整数
LONG
符号あり32bit整数
DWORD
符号なし32bit整数

公式のドキュメントを読むための前知識としてWindows特有の型名で説明したが、 以降はstdint.hで定義された型を利用し、自前で定義する構造体を使って説明していく。 構造体名やメンバー名の命名規則は他の部分と相容れない形になってしまっているが、 ここを下手に合わせてしまうと、元の構造体からかけ離れてしまうため、あえてそのままとしている。 BITMAPFILEHEADERを書き直したのが以下だ。

typedef struct BITMAPFILEHEADER {
  uint16_t bfType;      /**< ファイルタイプ、必ず"BM" */
  uint32_t bfSize;      /**< ファイルサイズ */
  uint16_t bfReserved1; /**< リザーブ */
  uint16_t bfReserved2; /**< リサーブ */
  uint32_t bfOffBits;   /**< 先頭から画像情報までのオフセット */
} BITMAPFILEHEADER;

それぞれの意味はすでにコメントで書いてしまっているが、構造体のメンバーを順に説明する。

bfType
BMP形式を識別するマジックナンバー。 必ず"BM"の文字コードが入ることになっている。 2文字まとめて、2byte整数として表現されている。 B=0x42、M=0x4Dであるため、 リトルエンディアンの環境であれば0x4D42が格納される。
bfSize
ファイル全体のサイズ
bfReserved1 / bfReserved2
将来の拡張のための予約領域で、 出力する際は0を入れ、入力の際は無視することになっている。
bfOffBits
画像情報までの(ファイルの先頭からの)オフセットバイト数を格納する。 メンバー名はBitsだが、バイト数であることに注意。 このファイルヘッダ構造体14byteと、続く情報ヘッダ、(あれば)カラーパレットのサイズを合わせた大きさが格納される。 今回説明するWindowsビットマップの非圧縮形式であれば54が格納されているはずである。

以上がファイルヘッダのメンバーの説明だが、ファイルヘッダにはほぼ画像としての情報はないに等しい。 この次に画像としての情報が入ったヘッダがあるのだが、 bfOffBitsというメンバがあることから推測できると思うが、 その画像情報ヘッダの大きさを拡張できるような構成になっている。

ファイル構造と構造体

少し本題からずれるが、ここではあくまでファイル構造の説明をしていて、C言語上でどのように表現するかという話はしていない。 そんな中、ファイル構造の説明に構造体が出てくることに違和感を感じるかもしれない。 実はこの構造体のメモリ上のデータをそのままファイルに書きだしたものがファイルのフォーマットになっている。 構造体がメモリ上でどのような構造になっているのかわかっていないとこのイメージはつきにくいと思うが、 C言語のコード上では、書き出すときはこの構造体を作成し、その構造体をbyte配列とみなして書き出しする。 読み込むときは、読み込んだbyte配列をこの構造体にキャストすれば、そのまま読めるというイメージである。 言葉で説明してもイメージはつきにくいだろうから、現時点でピンと来ない場合は、 次回以降に説明する、実際の書き出し、読み込み処理を見ていただければ良いだろう。

構造体をダンプする際に気をつけなければいけないことがいくつかある。ひとつはエンディアンである。 Windowsビットマップはリトルエンディアンであるため、Intelアーキテクチャであれば何も細工はいらない。 逆に移植性を考慮し、ビッグエンディアンの環境もサポートするためには、 変数ごとにエンディアン依存なく入出力する仕組みが必要になる。

次に考慮しなければならないのは構造体のアライメントによるパッキングである。 以下の様な構造体があったとする。

struct a {
  uint16_t a;
  uint32_t b;
};

sizeof(uint16_t)は2、sizeof(uint32_t)は4である。 さて、sizeof(struct a)は幾つになるだろうか? 2の変数と4の変数を格納しているのだから6だろうか? おそらく多くの環境では8となるはずである。 これは非常に簡単に確認できるので、自分の環境で構造体の中身を変えたりしてsizeofの結果が どのようになるか確認してみるのもいいだろう。

簡単に説明すると、2byte型は2の倍数のアドレス、4byte型は4の倍数のアドレス、 と言った具合に、キリの良いアドレスに配置されていないとメモリアクセスの効率が悪くなるため、 コンパイラはできるだけそのようなアドレスにメンバーを配置できるように、メンバー間に詰め物をしてしまう。 上記の例ではaとbの間に使われない2byteの領域を作成してしまうのだ。

なぜこのような説明をしたかといえば、 BITMAPFILEHEADERを構成する各変数のサイズを見ると2,4,2,2,4となっている。 つまり、多くの環境ではbfTypebfSizeの間に見えない2byteの詰め物が入ってしまう。 そのため、実際に使用する際には細工が必要になる。

情報ヘッダ

情報ヘッダとして、画像としての情報が格納されたヘッダがある。 ここの構造体がOS/2ビットマップとWindowsビットマップ、またそれ以降の拡張ビットマップ形式で違いがある部分だ。 Windowsビットマップでは以下の様な構造になっている。MSDNでは BITMAPINFOHEADER structure に説明がある。

typedef struct BITMAPINFOHEADER {
  uint32_t biSize;         /**< この構造体のサイズ */
  int32_t biWidth;         /**< 画像の幅 */
  int32_t biHeight;        /**< 画像の高さ */
  uint16_t biPlanes;       /**< 画像の枚数、通常1 */
  uint16_t biBitCount;     /**< 一色のビット数 */
  uint32_t biCompression;  /**< 圧縮形式 */
  uint32_t biSizeImage;    /**< 画像領域のサイズ */
  int32_t biXPelsPerMeter; /**< 画像の横方向解像度情報 */
  int32_t biYPelsPerMeter; /**< 画像の縦方向解像度情報*/
  uint32_t biClrUsed;      /**< カラーパレットのうち実際に使っている色の個数 */
  uint32_t biClrImportant; /**< カラーパレットのうち重要な色の数 */
} BITMAPINFOHEADER;

例によってコメントでほぼ書いてしまっているが順に説明していく。

biSize
このヘッダのサイズ、この構造体が採用されているのであれば40が格納される。
biWidth
画像の幅、符号付き整数だが、負の値は不正な値である
biHeight
画像の高さ、符号付き整数で、負の値の場合はトップダウン形式といって、画素の格納順が上から下であることを示す。 ただし、この方式は互換性の観点から非推奨とされる。
biPlanes
画像の枚数で通常は1が格納される。
biBitCount
1画素あたりのビット数、今回の非圧縮方式であれば24が格納される。
biCompression
圧縮形式、というか画素情報の格納方式を表す。今回扱う非圧縮方式であればRGB値を表す0が格納される。
biSizeImage
画像情報のサイズ
biXPelsPerMeter
x軸方向の解像度、1mあたりのピクセル数で表現する。解像度を扱わない場合は0でも問題ない。
biYPelsPerMeter
y軸方向の解像度、1mあたりのピクセル数で表現する。解像度を扱わない場合は0でも問題ない。
biClrUsed
カラーパレットの数を表す。今回の方式では0である。
biClrImportant
カラーパレットのうち重要な色の数を表す。0の場合はbiClrUsedと同じ値であることを示す。

パラメータの数は多いが、今回説明する範囲に限定した場合、固定値でよいパラメータが殆どになる。 書き出す場合は固定値を何も考えずに入れればいいし、 読み込む際は値のチェックを行い、ここで示した値以外であれば対応外のフォーマットであるのでエラーにする必要がある。

画像情報

画像情報についても24bitフルカラーに限定すればシンプルなのだが、いくつか癖がある。

ひとつは、最初から何度か説明しているが、画素の格納順が下から上になっている点。 ファイルを逆方向から走査するのは効率が悪いので、順次読み込みつつ、 格納先の下から格納していくことになるだろう。

もう一つは、各画素のRGB値の格納順がBGRの順序になっている点。 これだけ聞くと、通常と逆の順序になっているのだが、 BGRと読み込んでリトルエンディアンの一つの整数に代入したとすれば上位桁からRGBとなる。 (例えば、B=0xaa,G=0xbb,R=0xccであれば、リトルエンディアンで解釈すると0xccbbaaになり、上位桁からRGBの順序になる)

もう一つ、一行のデータサイズを4の整数倍に整える必要がある点。 1画素が3byteなのでデータサイズは3の整数倍になり、横のピクセル数が4の倍数でなければ必ず端が出てくる。 画素情報だけで4の倍数にならない場合は4の倍数になる数だけ詰め物(パディング)が必要となる。

以上のルールにしたがって読み書きを行う必要がある。