BMP画像の読み込み(ヘッダ)
4回に渡ってBMP形式の詳細なフォーマットについて説明した。 今回はC言語でそれを読み出す方法について説明していく。 前回までの説明で色空間など画像や色を厳密扱う上で必要になるパラメータもあったと思うが ここでは、他の形式等の説明で利用しているようにARGBのビットマップ情報だけを取り扱うため、 多くのパラメータは無視しているので予めご了承頂きたい。
ソースコードは GitHub にて公開している。 BMPの入出力を記述しているのは bmp.c である。 ある程度割り切っているとはいえ、様々な形式に対応させる必要もあり、 全体では1000行を超えてしまっている。 そのため、全てのコードをこのページに載せることはせず、ポイントをピックアップして説明する。 全体像を見たい場合は上記リンクからGitHubのコードを見てほしい。 また、特に読み出し処理は様々な処理を行う必要があり、長大になってしまうため、2回に分割して説明する。
バイトストリーム
簡易的な読み出しではプログラム上の構造体メモリマップがファイルと一致していることを前提とした読み出しを行ったが、 ある程度移植性のあるコードを書くことを考えると、あまり良い方法ではない。 構造体内部のメモリ配置というコード上では分かりにくく、実装依存のものに配慮する必要がある。 また、エンディアンが異なる環境では当然使えない。
そこで、ここではこの手のデータを扱う上での常套手段の一つ、 データの入出力を行う簡易的なユーティリティ関数群、バイトストリームを作成して対応している。 XByteずつ取り出すことができ、取り出す際にはエンディアン依存のない形で変換される。 このユーティリティを介することで、エンディアン等を気にする必要なく、 データを順次取り出すという処理をシンプルに記述することができる。 もう少し柔軟かつ高機能な実装にしてライブラリ化しても良いのだが、 今回は必要最低限の機能のみを実装したシンプルなものを利用している。
このバイトストリームのデータ構造は以下、
typedef struct bs_t {
uint8_t *buffer; /**< バッファ */
size_t offset; /**< 現在のオフセット */
size_t size; /**< バッファのサイズ */
int error; /**< エラー */
} bs_t;
この構造体を使って以下のように読み出しを行う。
static uint32_t bs_read32(bs_t *bs) {
uint8_t *b = &bs->buffer[bs->offset];
if (bs->offset + 4 > bs->size) {
bs->error = ERROR;
return 0;
}
bs->offset += 4;
return b[0] | b[1] << 8 | b[2] << 16 | b[3] << 24;
}
この読み出しではシフト演算を利用しているため実行するホストのエンディアンには依存しない。 1Byte読み出す場合、2Byte読み出す場合と、それぞれの関数を作成している。 戻り値は符号なし整数であるが、ビット配列が負の値(2の補数)になっていて、 格納先とビット幅が同一であれば負の値として解釈される。 ビット幅が同一でない場合、かつ負の値として解釈したい場合は工夫が必要となるが、 今回はそのような用法は存在しない。
今回はファイル入力を使ってバッファに読みだしたものを、適宜取り出すための関数として作成しているが、 場合によってはファイル入力自体をこのような関数でラップする方法をとっても良いだろう。
BMPファイル読み込み
例によってファイル名を指定して読み込む関数と、オープン済みのファイルストリームから読み込む関数を分離している。 ファイル名を指定して、ファイルストリームをオープンするところはほぼテンプレートなので説明は割愛する。
ファイルストリームからの読み出し処理は以下になる。 ほとんどの具体的な処理は別関数として実装しているため、ここでは大雑把な流れのみが記述されていることになる。
image_t *read_bmp_stream(FILE *fp) {
image_t *img = NULL;
bmp_header_t header;
int height;
uint16_t color_type;
memset(&header, 0, sizeof(header));
if ((read_file_header(fp, &header) != SUCCESS) ||
(read_info_header(fp, &header) != SUCCESS)) {
return NULL;
}
if (header.info.biBitCount <= 8) {
color_type = COLOR_TYPE_INDEX;
} else if (header.cmasks[3].mask == 0) {
color_type = COLOR_TYPE_RGB;
} else {
color_type = COLOR_TYPE_RGBA;
}
height = abs(header.info.biHeight); // 高さは負の可能性がある
if ((img = allocate_image(header.info.biWidth, height, color_type)) == NULL) {
return NULL;
}
if (color_type == COLOR_TYPE_INDEX) {
if (read_palette(fp, &header, img) != SUCCESS) {
goto error;
}
}
if (fseek(fp, header.file.bfOffBits, SEEK_SET) != 0) {
goto error;
}
if (read_bitmap(fp, &header, img) != SUCCESS) {
goto error;
}
if (header.info.biHeight < 0) {
// 高さが負の値の場合、トップダウン方式なので上下を反転させる
int i;
for (i = 0; i < height / 2; i++) {
pixcel_t *tmp = img->map[i];
img->map[i] = img->map[height - 1 - i];
img->map[height - 1 - i] = tmp;
}
}
return img;
error:
free_image(img);
return NULL;
}
流れそのものは特に変わったことはやっておらず、ヘッダを読み込み、ヘッダ情報に応じて画像構造体の初期化、 画像データの読み込み、という流れになる。
ポイントというか、BMP形式のちょっと特殊なところとして、 データの格納が基本はボトムアップだが、トップダウンで格納されている可能性がある。 これについてはそれぞれの読み込み処理では全てボトムアップとして読み出しを行い、 その後、トップダウン形式だった場合は上下を逆転させるという方法をとっている。 なぜなら、ボトムアップとトップダウン両方を考慮して読み出しを行うと処理が複雑になってしまうからだ。 ビットマップ全体の上下反転はそれなりの大きさのメモリコピーの連続となるため比較的大きな負荷となるが、 行ポインタの配列として管理しているため、行ポインタ配列の反転だけで済むようになっている。 トップダウン形式にについてはレアケースなのでこの対応で十分だろう。
ヘッダの読み込み処理
これまで解説してきたようにBMPのヘッダは2つのパートにわかれている。 うちファイルヘッダについては全ての形式で共通、かつ格納されているデータは僅かなのでシンプルな処理となる。
static result_t read_file_header(FILE *fp, bmp_header_t *header) {
bs_t bs;
uint8_t buffer[FILE_HEADER_SIZE];
if (fread(buffer, FILE_HEADER_SIZE, 1, fp) != 1) {
return FAILURE;
}
bs_init(buffer, FILE_HEADER_SIZE, &bs);
header->file.bfType = bs_read16(&bs);
header->file.bfSize = bs_read32(&bs);
header->file.bfReserved1 = bs_read16(&bs);
header->file.bfReserved2 = bs_read16(&bs);
header->file.bfOffBits = bs_read32(&bs);
// ファイルタイプチェック
if (header->file.bfType != FILE_TYPE) {
return FAILURE;
}
if (header->file.bfOffBits > OFF_BITS_MAX) {
return FAILURE;
}
return SUCCESS;
}
簡易読み出しのように読み出せば完了というほどシンプルではないが、必要なサイズを読み出し、 バイトストリームの関数を利用して、必要な幅ごとに読み出しを行うという処理となっている。 最後に申し訳程度ではあるがデータの整合性チェックを行っている。
ここでファイルヘッダに該当する構造体は、簡易読み出しで利用したようなパッキング指定は行っていない。 一般的なコンパイラであれば、bfTypeとbfSizeの間に2Byteのパディングがあるはずだが、 バイトストリームを使って一変数ずつ読み出しを行っているため特に気にする必要はない。 ファイルにはファイルの事情、メモリにはメモリの事情がある。 入出力でわずかに処理が多くなるが、この方が移植性も高くメンテナンス性も良い。
次が情報ヘッダとなるが、この情報ヘッダには重要なデータが詰まっている上、 バリエーションが豊富なため少し大きな関数になってしまっている。 また、カラーマスクについては、ヘッダ内に情報があるものと、ヘッダ外にあるものの両方があるが、 ヘッダ外にあるものについてもここで読み出しを行うようにしている。
static result_t read_info_header(FILE *fp, bmp_header_t *header) {
bs_t bs;
uint8_t buffer[INFO_HEADER_SIZE_MAX];
int buf_size = 4;
// 先頭4byteを読み出す
if (fread(buffer, buf_size, 1, fp) != 1) {
return FAILURE;
}
bs_init(buffer, buf_size, &bs);
header->info.biSize = bs_read32(&bs);
if (header->info.biSize > INFO_HEADER_SIZE_MAX) {
return FAILURE;
}
buf_size = header->info.biSize - buf_size;
if (fread(buffer, buf_size, 1, fp) != 1) {
return FAILURE;
}
bs_init(buffer, buf_size, &bs);
if (header->info.biSize == CORE_HEADER_SIZE) {
// OS/2 ビットマップ、この場合のみカラーパレットが3byte
header->info.biWidth = bs_read16(&bs); // 16bit
header->info.biHeight = bs_read16(&bs); // 16bit
header->info.biPlanes = bs_read16(&bs);
header->info.biBitCount = bs_read16(&bs);
header->info.biCompression = 0;
header->info.biSizeImage = 0;
header->info.biXPelsPerMeter = 0;
header->info.biYPelsPerMeter = 0;
header->info.biClrUsed = 0;
header->info.biClrImportant = 0;
} else if (header->info.biSize == INFO_HEADER_SIZE
|| header->info.biSize == INFO2_HEADER_SIZE) {
// Windowsビットマップ
header->info.biWidth = bs_read32(&bs);
header->info.biHeight = bs_read32(&bs);
header->info.biPlanes = bs_read16(&bs);
header->info.biBitCount = bs_read16(&bs);
header->info.biCompression = bs_read32(&bs);
header->info.biSizeImage = bs_read32(&bs);
header->info.biXPelsPerMeter = bs_read32(&bs);
header->info.biYPelsPerMeter = bs_read32(&bs);
header->info.biClrUsed = bs_read32(&bs);
header->info.biClrImportant = bs_read32(&bs);
if (header->info.biCompression == BI_BITFIELDS) {
// パレット部分にビットフィールドが格納されている
uint32_t masks[4];
buf_size = 4 * 3; // RGBの3つのマスクを読み出す
if (header->file.bfOffBits - FILE_HEADER_SIZE - header->info.biSize
< buf_size) {
// 読み出せるビットフィールドがない
return FAILURE;
}
if (fread(buffer, buf_size, 1, fp) != 1) {
return FAILURE;
}
bs_init(buffer, buf_size, &bs);
masks[0] = bs_read32(&bs);
masks[1] = bs_read32(&bs);
masks[2] = bs_read32(&bs);
masks[3] = 0;
read_color_masks(masks, header->cmasks);
} else if (header->info.biCompression == BI_RGB) {
set_default_color_masks(header->info.biBitCount, header->cmasks);
}
} else if (header->info.biSize == V4_HEADER_SIZE
|| header->info.biSize == V5_HEADER_SIZE) {
// V4/V5ヘッダ、読み込みマスクのみ利用する
header->info.biWidth = bs_read32(&bs);
header->info.biHeight = bs_read32(&bs);
header->info.biPlanes = bs_read16(&bs);
header->info.biBitCount = bs_read16(&bs);
header->info.biCompression = bs_read32(&bs);
header->info.biSizeImage = bs_read32(&bs);
header->info.biXPelsPerMeter = bs_read32(&bs);
header->info.biYPelsPerMeter = bs_read32(&bs);
header->info.biClrUsed = bs_read32(&bs);
header->info.biClrImportant = bs_read32(&bs);
if (header->info.biCompression == BI_BITFIELDS) {
// V4/V5ヘッダはヘッダ内にマスクがある
uint32_t masks[4];
masks[0] = bs_read32(&bs);
masks[1] = bs_read32(&bs);
masks[2] = bs_read32(&bs);
masks[3] = bs_read32(&bs);
read_color_masks(masks, header->cmasks);
} else if (header->info.biCompression == BI_RGB) {
set_default_color_masks(header->info.biBitCount, header->cmasks);
}
} else {
// ヘッダサイズ異常
return FAILURE;
}
// ヘッダの整合性チェック
if (!(header->info.biBitCount == 1
|| header->info.biBitCount == 4
|| header->info.biBitCount == 8
|| header->info.biBitCount == 16
|| header->info.biBitCount == 24
|| header->info.biBitCount == 32)) {
// 有効なビット数は1,4,8,16,24,32のみ
return FAILURE;
}
if (!(header->info.biCompression == BI_RGB
|| (header->info.biBitCount == 4
&& header->info.biCompression == BI_RLE4)
|| (header->info.biBitCount == 8
&& header->info.biCompression == BI_RLE8)
|| (header->info.biBitCount == 16
&& header->info.biCompression == BI_BITFIELDS)
|| (header->info.biBitCount == 32
&& header->info.biCompression == BI_BITFIELDS))) {
// 有効な圧縮形式か判定、RGBは有効なビット数全てOK
// RLE4は4bitのみ、RLEは8bitのみ、BITFIELDSは16bitか32bitの時のみ有効
// JPEGやPNGは対応外とする。
return FAILURE;
}
if (header->info.biWidth <= 0
|| header->info.biHeight == 0
|| header->info.biHeight == INT32_MIN) {
// サイズ異常、widthは正の値である必要がある。
// heightは0でなければ正負どちらであっても良いが、
// INT32_MINの場合は符号反転不可能のため異常扱い
return FAILURE;
}
return SUCCESS;
}
ここで読み出すべきデータサイズについては先頭4Byteに記述されているため、はじめにその先頭 4Byte を読み出している。 ここに関してはバイトストリームを利用するより直接ビットシフト演算等を記述したほうが記述量は少なくなるが、 読み出し方法を統一する意味もあり、バイトストリームを利用している。 この 4Byte を読み出せばヘッダサイズが分かるためその分をまとめて読みだす。 ヘッダサイズはサイズが記述されている4Byteも含んでいるため、読みだすデータサイズはその分忘れないように引き算をしておく。
その次に具体的なヘッダ内の情報を読み出すことになる。 データ構造のところで説明したとおり、どのようなデータ構造になっているかはデータサイズで分岐を行う。 ただし、読み込み先は BITMAPINFOHEADER 構造体である。 この構造体のデータについても全てを使うわけではないが、 これ以上の情報を持つヘッダの場合はその他の多くの情報を読み捨てている。
最初の分岐は BITMAPCOREHEADER すなわちOS/2形式である。 この形式は保持している情報が少ないため、格納先構造体はゼロで埋めている。 ここで注意すべきなのが、この形式のみ縦横のサイズが16bitである点だ。 加えて、この次の処理に関係するが、カラーパレットを持つ形式であった場合は、 この形式のみ RGBTRIPLE であるため、ヘッダが BITMAPCOREHEADER であるという情報だけは保持する必要がある。 この場合、biSizeがヘッダ種別の識別に使用できるためこれを保持する形になる。
次の分岐が BITMAPINFOHEADER すなわち Windows ビットマップである。 格納先の構造体と同一であるため、そのままサイズ通りに読み出せば良い。 少しややこしいのがその次にあるカラーマスクの読み出しである。 biCompression が BI_BITFIELDS の場合、ヘッダに続いてカラーマスクが格納されている。 カラーマスクはRGB各色4Byteで計 12Byte ある。 読み出せるだけのデータサイズがあるかチェックした後この 12Byte を 32bit 整数4つの配列に読み込み、カラーマスクデータに変換する。 RGB なのに4つの配列としているのは、のちのアルファチャンネルも含んだカラーマスクと同じ処理で実装するためである。 ここではアルファチャンネルは無いためアルファチャンネルのマスクを0としている。 また、 BI_BITFIELDS ではなく BI_RGB の場合はカラーマスクは格納されていないが、 デフォルトのカラーマスクを利用する必要がある。 そのため BI_RGB の場合はデフォルトのカラーマスクを取得する処理を追加している。 カラーマスクの処理については後ほど説明する。
次の分岐は BITMAPV4HEADER と BITMAPV5HEADER である。 これらは付加情報を BITMAPINFOHEADER に追加したデータ構造であり、 今回は BITMAPINFOHEADER に含まれない情報は読みとばすため、 ほとんどの処理は BITMAPINFOHEADER と同じである。 唯一違いがあるのが、カラーマスクの部分で、このヘッダについてはヘッダ内にあるためデータサイズの確認が不要、 また、カラーマスクにアルファチャンネルが含まれているため、 BITMAPINFOHEADER では 0 を代入していたところに実際に読みだした値を設定するようにする。
ひと通りヘッダ情報の読み出しが終わると、読みだしたデータの整合性をチェックして終わりである。
カラーマスクの読み出し処理
正確には読み出し済みのデータの解釈であるが、カラーマスクを使ってデータを読み出す場合、 カラーマスクを使って必要なビットの抽出、ビットシフトを行い、正規化を行うという処理が必要となる。 必要となるパラメータは全てカラーマスクのビットパターンから求めることができるのだが、 各ピクセルごとの処理でこれらを一から求めていては無駄が多いので予め求めた値を保持しておく。
読みだしたデータについては、以下の構造体に格納することにする。 意味は変数名及びコメントのとおりである。 これをRGBAの4つ保持する。
typedef struct channel_mask {
uint32_t mask; /**< マスク */
uint32_t shift; /**< シフト量 */
uint32_t max; /**< 最大値 */
} channel_mask;
読みだしたカラーマスク情報から、上記情報を求める処理が以下になる。
static void read_color_masks(uint32_t *masks, channel_mask *cmasks) {
int i, b;
for (i = 0; i < 4; i++) {
cmasks[i].mask = masks[i];
if (cmasks[i].mask == 0) {
cmasks[i].shift = 0;
cmasks[i].max = 0xff;
continue;
} else {
for (b = 0; b < 32; b++) {
if (cmasks[i].mask & (1 << b)) {
cmasks[i].shift = b;
cmasks[i].max = cmasks[i].mask >> cmasks[i].shift;
break;
}
}
if (cmasks[i].max == 0) {
cmasks[i].max = 0xff;
}
}
}
}
可能性としては、ビットが連続していないなどの場合も有り得るのだが、そのような不正データの可能性は無視している。 そのため、意図的に作られた不正なデータを読み込ませると挙動がおかしくなるだろう。 本来であればチェックすべきだろうが、この値がおかしくなったところで、 この読み出しルーチンでは画像が正常に読み出せない以上の被害はないし、 チェックしたところで正常に読み出せるようになるわけではない。
まず、maskについてはマスクデータそのままだ。 次に必要となるシフト量については、下位ビットから順に調べて、初めてビットが立っている箇所までの距離で求まる。 0 から32まで、1をシフトし続け、ビットアンドをとった値が0でなくなるまでのループ回数で求める。 次に、そのカラーマスクで得られる値の最大値を求める。 これはマスクデータを先に求めたシフト量分右シフトすることで求めている。 カラーマスクが0、すなわち全くビットが立っていない場合はmaxは0となる。 しかし、この最大値は正規化計算を行う際、この数を割り算に使用する。 整数演算において、ゼロ除算はハングアップのもとになるため、マスクが0の場合はmaxの値として255を設定している。 読み出しの際、ビットマスクが0が否かをチェックすればよいだけではあるが、 予めそのような危険性が予見される場合は、必要以上に無駄なコストが発生しない範囲で、 安全な側に倒しておくことはバグの少ないコードを書く上で重要だ。
カラーマスクの処理ではこれ以外に、 BI_RGB の時にデフォルト値を読み込む処理もあるが、 本当にデフォルト値を設定しているだけなので解説やコードの紹介は割愛する。
カラーパレットの読み込み
次にインデックスカラー形式の場合はカラーパレットを読み込む。 以下がカラーパレットの読み込み処理だ。
static result_t read_palette(FILE *fp, bmp_header_t *header, image_t *img) {
int i;
bs_t bs;
uint8_t buffer[PALET_SIZE_MAX];
// OS/2形式ではRGBTRIPLE、それ以外はRGBQUAD
int color_size = (header->info.biSize == CORE_HEADER_SIZE ? 3 : 4);
int palette_size = header->file.bfOffBits - FILE_HEADER_SIZE
- header->info.biSize;
int palette_num = palette_size / color_size;
int palette_max = (1 << header->info.biBitCount);
if (palette_num < header->info.biClrUsed) {
// 色数よりパレットが小さいので異常
return FAILURE;
}
if (palette_num > palette_max) {
// ビット数から計算された最大値より大きい場合はその値を最大値とする。
palette_num = palette_max;
}
if (header->info.biClrUsed != 0 && header->info.biClrUsed < palette_num) {
// 色数がヘッダに記載されている場合はそちらを優先
palette_num = header->info.biClrUsed;
}
palette_size = palette_num * color_size;
if (fread(buffer, palette_size, 1, fp) != 1) {
return FAILURE;
}
bs_init(buffer, palette_size, &bs);
img->palette_num = palette_num;
for (i = 0; i < palette_num; i++) {
img->palette[i].b = bs_read8(&bs);
img->palette[i].g = bs_read8(&bs);
img->palette[i].r = bs_read8(&bs);
img->palette[i].a = 0xff;
if (color_size != 3) {
bs_read8(&bs); // Reserve読み飛ばし
}
}
return SUCCESS;
}
カラーパレットのデータはシンプルであるが、ちょっとしたルールがあるのでその辺の整合を取るための処理がはじめに入っている。 まず、 BITMAPCOREHEADER の場合とそれ以外でサイズが異なるため、その判定を行っている。 次に、パレットサイズで、ヘッダと画像データまでに収まっているが、ここが何色分に該当するかを計算し、 ビットカウントから求められる最大数を超えていないかをチェック。 実際に使用されている色数は biClrUsed に格納されているのだが、0の場合は最大値を利用するというルールになっている。 また保険として、 biClrUsed に記載された数値が最大値よりも小さい場合にのみ採用するようにしている。 あとは、パレットサイズ分をまとめて読み込み、バイトストリームから順次読み出しを行い、カラーマスクを格納していく。 RGBQUAD は 4Byte あるがRGBの3色分のみで、もう 1Byte は Reserve のため読み飛ばす処理が必要となる。 RGBTRIPLE と RGBQUAD の分岐がループ内に入っており、あまり良くない処理になっているが、 カラーパレットは多くともたかだか 256個なので、処理の効率よりも記述の容易さを優先している。
オフセット調整
今回読み込むBMP形式のヘッダ情報は以上になるが、次に実際の画像データ部分の読み込みに入る。 しかし、画像データが格納されている位置は、通常はヘッダの次から隙間なく続いているが、 何らかのパディングが行われている可能性もあるし、今後の拡張で何らかの情報が追加されている可能性もある。 そのため正しく画像の開始位置を求めるには、ファイルヘッダにある bfOffBits の値を参照する必要がある。
その処理が以下である。
if (fseek(fp, header.file.bfOffBits, SEEK_SET) != 0) {
goto error;
}
単にfseek
関数を呼び出しているだけだが、
ファイル入出力を扱い始めた初級者にはあまり馴染みのない関数だろう。
上記記述で、ファイルの先頭から bfOffBits Byteの位置にファイルの読み出し位置を移動させるという意味なる。
以前に紹介した簡易版の読み出しでは、ファイルヘッダは固定でその他の情報は含まれないという前提で、
ヘッダを読みだした次の位置から画像情報を読み込むようにしていたが、ファイルヘッダの意味を汲むとこのような処理になる。