BMP画像の読み込み(画像データ)
今回は前回に引き続き、BMP画像のC言語での読み込み処理の実装について説明していく。
ソースコードは GitHub にて公開している。 BMPの入出力を記述しているのは bmp.c である。
画像データの読み込みに関しては、一段以下のような関数を挟んで実装を分割している。
static result_t read_bitmap(FILE *fp, bmp_header_t *header, image_t *img) {
int stride = (header->info.biWidth * header->info.biBitCount + 31) / 32 * 4;
switch (header->info.biBitCount) {
case 32:
return read_bitmap_32(fp, header, stride, img);
case 24:
return read_bitmap_24(fp, header, stride, img);
case 16:
return read_bitmap_16(fp, header, stride, img);
case 8:
case 4:
case 1:
if (header->info.biCompression == BI_RGB) {
return read_bitmap_index(fp, header, stride, img);
}
return read_bitmap_rle(fp, header, stride, img);
}
return FAILURE;
}
BMP形式には様々なフォーマットが存在するが、バリエーションはほぼビット数に依存しているため、ビット数による分岐を行っている。 24bit は BI_RGB のみ、32bit/16bitは BI_RGB と BI_BITFIELD の2種類があるが、 読み出し処理についてはこの2つは同じ処理を行うことになる。 32bitと16bitでは一度に読み出すデータサイズが異なるのみなので、 ひとつの関数にまとめたかったのだが、スマートな方法が思いつかなかったので分割している。 8bit/4bit/1bit については BI_RGB と BI_RLE8/BI_RLE4 の可能性がある。 この3つの間ではビット数による違いよりも、非圧縮と圧縮という大きな違いで分岐させたほうがまとめやすかったので、 このような形にしている。
この処理でポイントとなるのは、stride
という変数の準備だろう。
これまで何度も説明しているが、BMP形式のRLE以外のフォーマットでは、
一行分のデータサイズが4の倍数バイトとなるようにパディングが必要だ。
そこで、先に一行分のデータサイズを求めておき、読み出しはこのサイズごとに行う。
そして、読み出し処理は必要なデータサイズ分の処理を行い、あとのデータは無視する。
このようにすることでパディングに関する処理がシンプルになる。
RLEではこの値は使用しないのだが、他の関数を引数を合わせるために渡すようにしている。
24bitカラーの読み出し
24bitカラーについては読み出しにおいて特段の加工は不要で、 順に読み出し、適切な変数に代入していけば良いだけの最もシンプルな実装となっている。
static result_t read_bitmap_24(
FILE *fp, bmp_header_t *header, int stride, image_t *img) {
int x, y;
bs_t bs;
uint8_t *buffer;
int width = header->info.biWidth;
int height = abs(header->info.biHeight); // 高さは負の可能性がある
if ((buffer = malloc(stride)) == NULL) {
return FAILURE;
}
for (y = height - 1; y >= 0; y--) {
if (fread(buffer, stride, 1, fp) != 1) {
free(buffer);
return FAILURE;
}
bs_init(buffer, stride, &bs);
for (x = 0; x < width; x++) {
img->map[y][x].c.b = bs_read8(&bs);
img->map[y][x].c.g = bs_read8(&bs);
img->map[y][x].c.r = bs_read8(&bs);
img->map[y][x].c.a = 0xff;
}
}
free(buffer);
return SUCCESS;
}
どの処理も特に変わったことはやっていない。 あえて工夫点を上げれば、画像形式がボトムアップとなっているため、 y軸方向のループカウンタをデクリメントするように実装し、配列の添字としては自然な記述ができるようにしている。 また、前述のとおり、stride分ずつ行を読み出すことで、パディングの読み飛ばし処理を特段実装しなくて良いようにしている。
32bit/16bitカラーの読み出し
32bit/16bitは BI_RGB と BI_BITFIELD の2種類があるが、 BI_RGB はデフォルトのカラーマスクを使うというだけで、読み出し処理はどちらもおなじになる。
この処理を実装しているのが以下の関数だ。
この関数は32bit用だが、16bitとの違いは、
読み出しがbs_read32
を使っているか、bs_read16
を使っているかの違いでしか無い。
static result_t read_bitmap_32(
FILE *fp, bmp_header_t *header, int stride, image_t *img) {
int x, y;
bs_t bs;
uint8_t *buffer;
int width = header->info.biWidth;
int height = abs(header->info.biHeight); // 高さは負の可能性がある
if ((buffer = malloc(stride)) == NULL) {
return FAILURE;
}
for (y = height - 1; y >= 0; y--) {
if (fread(buffer, stride, 1, fp) != 1) {
free(buffer);
return FAILURE;
}
bs_init(buffer, stride, &bs);
for (x = 0; x < width; x++) {
uint32_t tmp = bs_read32(&bs);
img->map[y][x].c.r = CMASK(tmp, header->cmasks[0]);
img->map[y][x].c.g = CMASK(tmp, header->cmasks[1]);
img->map[y][x].c.b = CMASK(tmp, header->cmasks[2]);
if (header->cmasks[3].mask == 0) {
img->map[y][x].c.a = 0xff;
} else {
img->map[y][x].c.a = CMASK(tmp, header->cmasks[3]);
}
}
}
free(buffer);
return SUCCESS;
}
ここでも特別な処理は行っておらず、24bitでは1Byteずつ読みだしていたものを32bit分をまとめて読みだして、 その値からRGBそれぞれの成分をカラーマスクの情報を使って計算するという処理になっている。 マスク処理はマクロで定義し、それを利用している。定義は以下になる。
#define CMASK(d, m) \
(((((d) & (m).mask) >> (m).shift) * 255 + (m).max / 2) / (m).max)
読みだしたデータに対してカラーマスクの情報を元にデータの抽出から正規化までをまとめて行うマクロである。 既に何度か説明しているが、整数演算で正規化を行うところが少し技巧的な記述になっている。
インデックスカラー形式の読み出し
ここで用意している画像形式はインデックスカラー形式に対応しているため、 読み出しにおいて加工は不要で、インデックス値を読みだして順に格納するだけで良い。 ただし、インデックス値のビット幅によっては1Byte内のデータを切り出すための処理が必要になる。 この部分は実装するにあたって、注意して実装しないとミスをし易い部分である。
static result_t read_bitmap_index(
FILE *fp, bmp_header_t *header, int stride, image_t *img) {
int x, y;
bs_t bs;
uint8_t *buffer;
uint8_t tmp;
int bc = header->info.biBitCount;
uint32_t mask = (1 << bc) - 1;
int width = header->info.biWidth;
int height = abs(header->info.biHeight); // 高さは負の可能性がある
if ((buffer = malloc(stride)) == NULL) {
return FAILURE;
}
for (y = height - 1; y >= 0; y--) {
int shift = 8;
if (fread(buffer, stride, 1, fp) != 1) {
free(buffer);
return FAILURE;
}
bs_init(buffer, stride, &bs);
tmp = bs_read8(&bs);
for (x = 0; x < width; x++) {
shift -= bc;
img->map[y][x].i = (tmp >> shift) & mask;
if (shift == 0) {
shift = 8;
tmp = bs_read8(&bs);
}
}
}
free(buffer);
return SUCCESS;
}
上記関数内では、1画素毎にシフト量をずらし、0になったら次の1Byteを読み出すという処理にしている。 これで8bitから1bitまで同じ処理で実現できる。
ランレングス圧縮形式
最後に説明するのが、やはりこの中では一番複雑な処理となる、ランレングス圧縮形式である。
static result_t read_bitmap_rle(
FILE *fp, bmp_header_t *header, int stride, image_t *img) {
int x, y, i;
bs_t bs;
uint8_t buffer[256];
uint8_t tmp;
int bc = header->info.biBitCount;
int mask = (1 << bc) - 1;
int width = header->info.biWidth;
y = abs(header->info.biHeight) - 1;
x = 0;
bs_init(buffer, sizeof(buffer), &bs);
while (y >= 0 && x <= width) {
if (fread(buffer, 2, 1, fp) != 1) {
return FAILURE;
}
if (buffer[0] != 0) { // エンコードデータ
for (i = 0; i < buffer[0] && x < width;) {
int shift = 8 - bc;
for (; shift >= 0 && i < buffer[0] && x < width; shift -= bc) {
img->map[y][x].i = (buffer[1] >> shift) & mask;
x++;
i++;
}
}
} else if (buffer[1] > 2) { // 絶対モード
int shift = 8;
int n = buffer[1];
int c = (n * bc + 15) / 16 * 2; // 2byte単位揃え
if (fread(buffer, c, 1, fp) != 1) {
return FAILURE;
}
bs_set_offset(&bs, 0);
tmp = bs_read8(&bs);
for (i = 0; i < n && x < width; i++) {
shift -= bc;
img->map[y][x].i = (tmp >> shift) & mask;
x++;
if (shift == 0) {
shift = 8;
tmp = bs_read8(&bs);
}
}
} else if (buffer[1] == 2) { // 移動
if (fread(buffer, 2, 1, fp) != 1) {
return FAILURE;
}
x += buffer[0];
y -= buffer[1];
} else if (buffer[1] == 1) { // 終了
break;
} else { // 行終了、次の行へ
x = 0;
y--;
}
}
return SUCCESS;
}
RLEはまかりになりにも圧縮形式であるため、読み出したデータ量と解凍後のデータ量は当然異なる。 一番外側のループはx座標y座標をループ条件としているが、処理の内部でxやyの値は変更されてしまうため、 バグが混入しないように慎重に処理を記述していく必要がある。
RLE ではデータが 2Byte 単位になっているため、まず 2Byte を読み出し、モード判定を行う。 先頭が0でなければエンコードデータとなり、 2Byte 目の値が 1Byte 目の個数連続することになる。 ただし、 BI_RLE8 と BI_RLE4 で個数のカウントが異なるので、ビットシフトと組み合わせて繰り返し処理の中で処理している。 ループ条件では、 RLE ではエンコードデータが行をまたぐことはないため、xの値が width を超えないような条件も加えている。
次に 1Byte 目が 0 の場合、 2Byte目でモードを判定する。
2Byte 目が 2 より大きい場合、絶対モードとなる。 2Byte 目の値を length と解釈し、この length 分非圧縮データが格納されているため、そのまま読み込みを行う。 ただし、length は奇数の可能性があるが、絶対モードのデータエリアは偶数と決まっているため、 length 以上の最小の偶数を求め、その長さだけ読み込みを行う。 あとは順次読みこむだけだが、 BI_RLE8 と BI_RLE4 で読み込むデータ幅が異なる部分については、 BI_RGB の場合と同じように、シフト量を一種のループ変数として利用して処理を分岐させている。
2Byte 目が 2 の場合、読み込みの座標移動となる。
次の 2Byte を読み出し、1Byte 目をxに加算、 2Byte 目をyから減算する。
y が減算となっているのは、処理の中ではBMP的概念の下から上に向かう座標系ではなく、上から下に向かう座標系を利用しているためだ。
座標移動が発生すると、そこまでのピクセルの内容が未定義になってしまうが、仕様上も未定義なので特段の処理は入れていない。
実際は、画像を格納する構造体の確保時にcalloc
を利用しているため、0で埋められることになる。
2Byte 目が 1 の場合、画像の終了を意味する。 正常な画像であれば全てのデータが揃った後にこのデータが配置され、ここで処理の終了となる。 また、仮に画像の最後までデータが揃っていなかったとしても、ここで終了である。 その場合に読み込まれていないエリアについてはやはり未定義となる。
2Byte 目が 0 の場合、行の末尾を意味する。 y をデクリメントし、x を0に戻して次の処理に移る。 このデータが無ければ次の行に処理を進めることができない。 また、仮に1行分のデータが揃う前にこのデータが来てしまうと残り部分については未定義となる。
以上で、BMP画像の読み込みについては終わりである。 結構なボリュームがあるが、フォーマットに様々なバリエーションがあるからで、 一つ一つの処理についてはそれほど複雑ではなかったかと思う。 現在はほぼ使われなくなったOS/2形式や、最新のV5形式まで考慮しているので、 おそらく、世の中に出回っているBMP形式の画像ファイルで読み込めないものは殆ど無いと言っていいと思う。 市販の画像編集ソフトでもここまで対応しているものは少ないだろう。