BMPファイルフォーマット(画像データ)

作成:

前回までBMP形式の様々なヘッダ情報について説明してきた、 今回は、実際の画像データがどのように格納されているのかを説明していこうと思う。 といっても、基本的には非圧縮の方式であり、非常にシンプルである。 少し複雑なのがランレングス圧縮による圧縮であるが、 これも圧縮方式としては非常にシンプルな方式であるため理解もしやすいと思う。

24bit RGB

まずは一番単純な 24bit RGB 値の画像データについて、 RGB 各色で 8bit 深度の値を格納するだけなので非常にシンプルである。

幾つか気をつけるべきポイントを上げると、 まず、格納順序がBGRと一般的な感覚とは逆になっている点である。 3Byte をまとめて読みだしてリトルエンディアンで解釈したとすると、 上位桁からRGBの順序になるのでバイナリ列よりもそちらの表現を優先したためかも知れない。

次が、BMP形式では画素情報の格納順序がボトムアップになっている。 すなわち、データの先頭から画像の下側のデータが格納されており、 最後に画像の上側のデータが格納されるという順序になっている。 さらに、高さの値が負の値の場合は、トップダウンとなり、上下を逆転させる必要がある。

最後に、一行のデータは4の倍数になっていなければならないという点である。 1 画素あたり 3Byte なので幅が4の倍数でない場合、パディング領域が必要となる。 ただし、よく使われる画像サイズではサイズが 4 の倍数に該当することが多いため、パディングが必要ない。 そのため、実装の際この制約を忘れていても、動作検証で発覚しにくいので注意が必要である。

簡単な実例を上げてみよう、幅3、高さ3、の画像で、各ピクセルの色が

#ff0000 #ff0066 #ff00cc
#ff6600 #ff6666 #ff66cc
#ffcc00 #ffcc66 #ffcccc

と、こんな感じになっていた場合、 実際のバイナリとしてどう見えるか、というと、以下のようになる。

     00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
    ------------------------------------------------
10 | 00 cc ff 66 cc ff cc cc ff 00 00 00 00 66 ff 66
20 | 66 ff cc 66 ff 00 00 00 00 00 ff 66 00 ff cc 00
30 | ff 00 00 00

1行分のデータを反転表示させている。 上下が反転しているので一番最初に入っているのは左下に当たる #ffcc00 が、 BGR の順で入っている。 次に #ffcc66、#ffcccc と入って 3Byte のパディングとして 0x00 が格納されている。 それ以降の行も同じように格納されていることがわかるだろう。

8 / 4 / 1 bit カラーパレット

カラーパレットを利用した方式で、画像情報として格納されているのがカラーパレットのインデックス値というものである。 8bit の場合はシンプルなのだが、 1byte 中に複数ピクセルの情報が詰まっている 4bit / 1bit の方式はほんの少し工夫が必要な方式である。

1Byteに複数の画素の情報を格納できる場合、そのデータをどの順序で格納する順序は、上位bitから順に詰めていくことになる。 この事自体は特に特殊なことではないのだが、 全体としてはリトルエンディアン、すなわち、下位 Byte を先頭に配置する形式だが、 1Byte 内のデータは上位 bit を先頭にする。 と、ぱっと聞くとちぐはぐな感じがして、この手のデータの扱い方に慣れていないと頭が混乱してしまうかもしれない。 この部分は頭のなかがきちんと整理してしっかりとイメージを捉えておいてほしい。 そうしないと、この読み込み書き出しの処理を実装するときに混乱してしまうだろう。

簡単な実例で説明する。 24bit RGBでの例と同じ、3×3の画像、ただし、カラーパレット形式になっており、 カラーパレット内でのインデックスがカッコで示した番号だとしよう。 最大値が8なので 4bit で表現可能だ。

#ff0000(0) #ff0066(1) #ff00cc(2)
#ff6600(3) #ff6666(4) #ff66cc(5)
#ffcc00(6) #ffcc66(7) #ffcccc(8)

これが、実際のバイナリとしてどう見えるか、というと、以下のようになる。

     00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
    ------------------------------------------------
10 | 67 80 00 00 34 50 00 00 01 20 00 00

1行分のデータを反転表示させている。 左下から右方向にカラーインデックスで 6 7 8 のデータが入っており、 1Byte 内のデータは上位ビットから詰めて格納するため、 0x67 0x80 となる、 2Byte 目の下位4bitについてはデータが無いため、 0 を格納する。 また、1行のデータは 4 の倍数 Byte である必要が有るため、パディングとして 2Byte 分の 0x00 が格納される。

なお、この画像を8bitカラーで格納すると以下のようになり、データ量の差がパディング分に収まるためサイズは同一になる。

     00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
    ------------------------------------------------
10 | 06 07 08 00 03 04 05 00 00 01 02 00

1bit カラーの場合は、上位 bit から 1bit ずつ、 1Byte に8画素分のデータを詰め込むということ以外は同じである。 1画素あたり 1bit と非常に小さいが、この方式でも 1 行のデータサイズが 4Byte 単位という制約は変わらない。

16/32bit BITFIELD

biBitCount が 16 もしくは 32 の場合は、 biComplession が BI_RGB と BI_BITFIELD の2パターンがあり、 BI_RGB ではカラーマスクがデータとして格納されていない。 しかし、この2つはカラーマスクがデータ上にあるか、予め決められたデフォルトのカラーマスクを利用するというだけで、 どちらも読み書きの方法は基本的に同じだ。 そのため、どちらも BITFIELD を利用した形式としてまとめて説明する。 また BITMAPV4HEADER および BITMAPV5HEADER ではビットフィールドにアルファチャンネルが追加されているが、 これについても考え方は同様である。

なお、 32bit BITFIELD の場合は、一色あたり 8bit 以外を割り当てるのはレアケース (アルファチャンネル 1bit などはありうるが)であり、 全てを BITFIELD として扱うと、無駄な処理が発生するのだが、柔軟に扱うため後に紹介するサンプルコードでは 全て BITFIELD として扱っている。

この形式の読み出し方についてはヘッダの説明のカラーマスクの項で説明しているので、 そちらを参照して欲しいが、ここでも軽くおさらいしておく。

32bit BITFIELD で、32bit を読み出し、リトルエンディアンで解釈したデータが 0x01223344 だったとする。 そして、アルファチャンネルのカラーマスクが 0x01000000 だったとする。 この時のアルファチャンネルの値は以下のようにして求める。

  1. ビットANDを行い、他のチャンネルのデータを除外する。
    0x01223344 & 0x01000000 ⇒ 0x01000000
  2. ビットシフトを行い、数値として読みだす。
    0x01000000 >> 24 ⇒ 0x1
  3. 読みだした値を、最大値255で正規化する。
    0x1 * 0xff / 0x1 ⇒ 0xff

各段階で使用しているパラメータはカラーマスクから取得できる値だ。 1段階目で利用しているマスク値はカラーマスクそのもの、 2段階目で利用しているシフト値はカラーマスクのうち下位ビットの0の数、もしくは初めて1が現れるビットまでの数、 3段階目で利用している正規化の比率は、カラーマスクの値を2段階目で使用したシフト量分右シフトした値、 となる。 実際に読み出し処理などを実装する上でこれら値はヘッダを読みだした時点で計算しておくことになる。

RLE8 / RLE4

ランレングス圧縮(Run Length Encoding = RLE)という方式を使って圧縮した形式。 圧縮と言っても RLE は非常に原始的でシンプルな方式で、実装難易度も低い。 とは言え、読み出し書き出しには圧縮展開が必要なため、非圧縮の方式にくられべれば多少複雑な方式である。

ランレングス圧縮とは

そもそも「ランレングス圧縮とはなにか」について軽く説明する。 日本語で表現した場合は連長圧縮と呼ばれる。 その語感からなんとなくどういう方式か想像がつくと思う。 例えば以下の様なデータがあったとする。

     00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
    ------------------------------------------------
10 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

このデータを人が説明するとき、00 00 00... と説明する人は少ないだろう。 00 が 16 個続いているデータと表現する人のほうが多いはずだ。 同じデータが続いている場合は、その繰り返しをそのまま表現するより、何が何個と表現した方が短くてすむ。 これを、データ上で表現したものがランレングス圧縮という方式だ。

実際のデータでは日本語の語順とは逆に、個数+データで表現する。 上記を RLE8 で圧縮したとすると、以下のようになる。

     00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
    ------------------------------------------------
10 | 10 00

16Byte 必要だったデータが 2Byte で表現できるようになった。 これがランレングス圧縮である。 しかし、この方法で圧縮の効果が得られる状況は限られている。 例えば、以下のように同じデータが全く続いていない場合

     00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
    ------------------------------------------------
10 | 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f

これに同様の方式を適用させたとすれば、以下のようになる。

     00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
    ------------------------------------------------
10 | 01 00 01 01 01 02 01 03 01 04 01 05 01 06 01 07
20 | 01 08 01 09 01 0a 01 0b 01 0c 01 0d 01 0e 01 0f

圧縮したはずがデータサイズは2倍になってしまった。 実際はこのようにならないように、圧縮効果が得られない部分については、 非圧縮のデータをそのまま配置する、というモードも設けられている。 しかし、それでも元のデータよりサイズが大きくなることに変わりはない。 ランレングス圧縮で圧縮効果があげられるのは、 全く同じ値が連続しやすいデータにおいてのみである。

一般的な画像は似たような色が隣り合うという特性を持っていることは確かだが、 この方法で圧縮できるのはあくまで同じ値である必要がある。 そのため、いわゆるフルカラーの画像では、この方式で圧縮するのは難しい。 しかし、これが色数を256色だとか、16色とかに減色した画像の場合は、 同じ色が連続しやすくなり、圧縮の効果が得られるようになる。 ただ、減色の際ディザリングが行われると圧縮の効果が低くなるが。

圧縮効果が得られるパターンは限定的で汎用的なデータには向かないが、 圧縮展開に際して、追加で必要となるメモリも小さく、処理負荷も小さい。 そのため、古くから広く利用されている圧縮方式である。

RLE8 / RLE4 のデータ構造

ランレングス圧縮がどういうものかを説明したところで、 実際にデータ構造としてどうなっているのかを説明していく。 まず、 RLE では1行のデータを 4 の倍数 Byte にしなければならないというルールはない。 その代わり、データは必ず 2Byte 単位で処理できるような配置にされている。 また、 RLE はボトムアップ形式でのみ定義されており、 RLE のトップダウン形式は定義されていない。

単にランレングス圧縮したデータだけを配置するのであれば非常にシンプルなのだが、 前述のとおり、エンコードすると返ってデータサイズが大きくなってしまう場合があり、 データの配置方法についていくつかのモードが設けられている。

エンコードモード

RLE によって圧縮が行われているデータの配置方法である。 はじめの 1Byte が連続数、次の 1Byte が連続しているデータで表現される。 展開の仕方は説明するまでもないが、 1Byte 目の個数、 2Byte 目のデータを配置させれば良い。 1Byte で数を表現ししているため表現できる最大の連続数は 255 だ。 これ以上連続している場合は、分割して表現するしか無い。 また、このデータ配置の特性上、1Byte 目が0になることは無い、データの連続数は最小で1であるからだ。 1Byte目が 0 である場合は他のモードと判断する。

イメージをつかみやすいように、以下に RLE8 のエンコードデータとその展開の例を示す。

01 12 ⇒ 12
02 12 ⇒ 12 12
03 12 ⇒ 12 12 12
04 12 ⇒ 12 12 12 12

RLE8 についてはだいたい予想通りだと思う。 では RLE4 はどうかというと、 1Byte 目は連続数、 2Byte 目が連続しているデータを表している。 ここまでは RLE8 と同じで、2Byte 目には2画素分のデータを配置できる点が異なる。

ちょっとややこしいが、これについては実例を見ればすぐに理解できると思う。 RLE8 と同様に以下に RLE4 のエンコードデータとその展開の例を示す。

01 12 ⇒ 10
02 12 ⇒ 12
03 12 ⇒ 12 10
04 12 ⇒ 12 12

RLE4 では同一色の画素の並びというよりは2画素をまとめた1Byteの連続を圧縮する形になる。ただし、連続数は画素の数だ。 まとめられた2画素は同一色である必要はなく、2色でもそのパターンが連続していれば、圧縮可能である。 便宜上、連続数が奇数の場合の下位 4bit を0で表現しているが、 0という値が入るわけではなく、次のエンコードデータの展開結果が詰めて格納される。 以下のように、この値が入る手前で上位 4bit に既にデータが詰まっていた場合は全体が 4bit ずれることになる。

03 12 03 34 ⇒ 12 13 43

絶対モード

RLE によって圧縮が行われていないデータを配置するときに利用されるモードである。 1Byte 目が 0 で、 2Byte 目の値は絶対モードに格納されているデータの数が記述されている。 当然、絶対モードのデータ数は 255 が最大値となる。 また、最小の値は 3 であり、 2 以下の数の場合は他のモードの識別に使用される。 それ以降にここで示された個数分のデータが格納される。 ただし、絶対モードのデータ幅は必ず 2 の倍数 Byte 必要となる。 例えば、 RLE8 で連続数が 3 の場合、絶対モードのデータは 3Byte となるが、パディングを加えて 4Byte にする必要がある。

2Byte 目が 3 以上という制約があるが、 RLE8 で連続数 4 、 RLE4 で連続数 7 以上でないとエンコードモードより小さくはならないため、 データ数 2 以下の絶対モードをわざわざ使う必要はない。

以下が、 RLE8 での絶対モードのデータとその展開例である。

00 03 12 34 57 00 ⇒ 12 34 57

以下が、 RLE4 での絶対モードのデータとその展開例である。

00 05 12 34 50 00 ⇒ 12 34 50

通常 RLE のデータはエンコードモードと、絶対モードの2種類の組み合わせだけで表現可能だが、 以下の制御モードが定義されている。 読み出し時はこれを制御トリガとして利用して読みだすことになる。

行の終端

1Byte 目が 0 、 2Byte 目も 0 の時、行の終端を表す。 y 座標を 1 進め、 x 座標を 0 に戻すことになる。 通常は1行分のデータが全て配置された後、終端符号として配置される。 仮に現在の行のデータが埋まっていない場合、残りのデータは未定義となる。

イメージの終端

1Byte 目が 0 、 2Byte 目が 1 の時、イメージの終端を表す。 このデータが現れた時点でイメージのデコードを終了させる。 通常は全画像データの末尾に配置される。 また、最後の行の終端はこの符号となり、行の終端は配置されない。 仮に画像サイズ分のデータが埋まっていない場合、残りのデータは未定義となる。

上記制御モードはデータの意味的にはなくてもデコードできそうではあるが、出力する際は必ず配置する。 また、モードとしてもう一つ以下の制御モードが定義されている。 しかし、これについては実際に利用されているものを見たことがない。

位置移動モード

1Byte 目が 0 、2Byte 目が 2 の時、位置移動モードとなり、 3Byte 目が x 座標、4Byte 目が y 座標の移動量を表す。 前述のとおり、現在の座標から移動先までの画素データについては未定義となる。 また、移動量は符号なし整数として解釈するので y 座標はもとより、 x 座標も現在の場所から後ろに移動させることができない。

これらの座標移動を伴うデータは、制御に直接反映されるため、 実データなどを見るよりも読み込みの処理を見たほうが理解しやすいと思う。 次回以降、実コードの解説をするのでそちらを参照してほしい。