BMP画像の読み込み(簡易版)
今回は、前回説明したWindowsビットマップ24bitフルカラーに限定した、 簡易的なBMP画像の読み込み方法について説明する。 フォーマットを限定しているため、当然読み込めないBMPファイルも出てくるだろうが、 多くの場合はこの方式で事足りるだろう。
ソースコードは GitHub にて公開している。 今回説明する、BMP形式の簡易入出力を記述しているのは bmp_simple.c である。 簡易版でない正式版は bmp.c であり、様々な形式に対応するため、コード量が約5倍になっている。 説明についても長くなっているが、簡易版で説明済みの内容も含めて説明しており、 そこから読んでも問題ないようにしているので、 中途半端な簡易版の説明は不要という場合はこのページはスキップしてもらって問題ない。
定義
#define FILE_TYPE 0x4D42 /**< "BM"をリトルエンディアンで解釈した値 */
#define FILE_HEADER_SIZE 14 /**< BMPファイルヘッダサイズ */
#define INFO_HEADER_SIZE 40 /**< Windowsヘッダサイズ */
#define DEFAULT_HEADER_SIZE (FILE_HEADER_SIZE + INFO_HEADER_SIZE)
/**< 標準のヘッダサイズ */
マジックナンバーとヘッダサイズは定数なのでここで定義する。
次が前回説明したヘッダの構造体である。
#pragma pack(2)
typedef struct BITMAPFILEHEADER {
uint16_t bfType; /**< ファイルタイプ、必ず"BM" */
uint32_t bfSize; /**< ファイルサイズ */
uint16_t bfReserved1; /**< リザーブ */
uint16_t bfReserved2; /**< リサーブ */
uint32_t bfOffBits; /**< 先頭から画像情報までのオフセット */
} BITMAPFILEHEADER;
#pragma pack()
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;
中身については前回説明したとおりだが、
一点初級者、ひょっとしたら中級者でも見慣れないだろう記述が追加されている。
#pragma pack()
の記述だ。
#pragma
はコンパイラへの命令を伝えるプリプロセッサで、
pack()
は構造体のアライメントを変更させる命令になっている。
前回簡単に説明したが、通常構造体内のメンバーがbfSize
が4byte境界に整列させられ、
bfType
とbfSize
の間に見えない2byteの詰め物が入ってしまう。
メモリ上でこの構造体を経由して使う場合には問題にならないが、
メモリマップを使う場合には問題となる。
そこで、#pragma pack(2)
という命令によって、
構造体のアライメント境界を2byteにしている。
これで余計な詰め物のない構造体定義ができる。
また、#pragma pack()
と引数のない指定を行うことで、アライメント設定をデフォルト値に戻している。
画像の読み込み
例によってファイル名を指定して読み込む関数と、オープン済みのファイルストリームから読み込む関数を分離している。 ファイル名を指定して、ファイルストリームをオープンするところはほぼテンプレートだ。
image_t *read_bmp_simple_file(const char *filename) {
FILE *fp = fopen(filename, "rb");
if (fp == NULL) {
perror(filename);
return NULL;
}
image_t *img = read_bmp_simple_stream(fp);
fclose(fp);
return img;
}
次が、オープンしたファイルストリームから実際にBMPファイルを読み込む処理だ。
image_t *read_bmp_simple_stream(FILE *fp) {
uint8_t header_buffer[DEFAULT_HEADER_SIZE];
BITMAPFILEHEADER *file = (BITMAPFILEHEADER*)header_buffer;
BITMAPINFOHEADER *info = (BITMAPINFOHEADER*)(header_buffer + FILE_HEADER_SIZE);
uint8_t *row, *buffer;
image_t *img = NULL;
int x, y;
int width;
int height;
int stride;
if (fread(header_buffer, DEFAULT_HEADER_SIZE, 1, fp) != 1) {
return NULL;
}
if (file->bfOffBits != DEFAULT_HEADER_SIZE ||
info->biBitCount != 24 ||
info->biHeight <= 0) {
return NULL;
}
width = info->biWidth;
height = info->biHeight;
stride = (width * 3 + 3) / 4 * 4;
if ((buffer = malloc(stride)) == NULL) {
return NULL;
}
if ((img = allocate_image(width, height, COLOR_TYPE_RGB)) == NULL) {
goto error;
}
for (y = height - 1; y >= 0; y--) {
if (fread(buffer, stride, 1, fp) != 1) {
goto error;
}
row = buffer;
for (x = 0; x < width; x++) {
img->map[y][x].c.b = *row++;
img->map[y][x].c.g = *row++;
img->map[y][x].c.r = *row++;
img->map[y][x].c.a = 0xff;
}
}
free(buffer);
return img;
error:
free(buffer);
free_image(img);
return NULL;
}
これが、BMPファイルの簡易読み込み処理の実質的な処理の全てだ。 フォーマットを限定したことで非常にシンプルな処理になっている。
エラー処理のためにgoto
を使っているが、
goto
の是非についてはここでは議論するつもりはないのでスルーしてほしい。
以下がヘッダの読み込み処理だ。
uint8_t header_buffer[DEFAULT_HEADER_SIZE];
BITMAPFILEHEADER *file = (BITMAPFILEHEADER*)header_buffer;
BITMAPINFOHEADER *info = (BITMAPINFOHEADER*)(header_buffer + FILE_HEADER_SIZE);
...
if (fread(header_buffer, DEFAULT_HEADER_SIZE, 1, fp) != 1) {
return NULL;
}
この部分はポインタの処理に慣れていないと、何をやっているのかピンと来ない部分だろう。
まず最初に2つのヘッダ構造体を合わせたサイズの配列を用意している。
その先頭ポインタを、BITMAPFILEHEADER型のポインタにキャスト、
先頭からBITMAPFILEHEADERのサイズ分だけ加算したポインタを
BITMAPINFOHEADER型のポインタにキャストしている。
こうすることで、header_buffer
で用意されたメモリ領域へ
それぞれの構造体を経由してアクセスできるようになる。
次にファイルの先頭からheader_buffer
へ、そのサイズ分ファイルから読み出しを行う。
すると、ファイルの先頭部分をこれら構造体にマップしたのと同じ効果を得られるという寸法である。
構造体がひとつなら、構造体変数に対して直接書き込みを行ってしまう方がシンプルだと思うが、 読み込み処理を一度に行うためにこういう方法をとってみた。
前述のとおり、構造体のアライメントへの対応は行っているが、エンディアンについては シンプル化のため、リトルエンディアン環境で実行される前提で組んでいる。 当然のことながらビッグエンディアン環境には移植できない。
次が、フォーマットを限定するための判別処理である。
if (file->bfOffBits != DEFAULT_HEADER_SIZE ||
info->biBitCount != 24 ||
info->biHeight <= 0) {
return NULL;
}
BMPファイルとしては正しくても、今回は対応しない形式のためエラーとして扱っている。
次が1行あたりのアライメントへの対処である。
width = info->biWidth;
height = info->biHeight;
stride = (width * 3 + 3) / 4 * 4;
if ((buffer = malloc(stride)) == NULL) {
return NULL;
}
先に1行分のサイズ(stride)を計算しておく。
4の倍数きっかりならそのサイズ、端がある場合は次の4の倍数になる数。
つまり端を繰り上げる必要がある。
通常の整数演算で割り算を使うと切り捨てになる性質を利用するのだが、繰り上げ計算はほぼ常套手段が決まっている。
基数より1小さい数を加算してから基数で割って、基数をかける。(x + base - 1) / base * base
または、1減算してから、基数で割って、基数をかけた後1を加算する。(x - 1) / base * base + 1
という方法が取られる。どちらでも好きな方法を取ればよいだろう。
次が画素情報の読み込み処理。
for (y = height - 1; y >= 0; y--) {
if (fread(buffer, stride, 1, fp) != 1) {
goto error;
}
row = buffer;
for (x = 0; x < width; x++) {
img->map[y][x].c.b = *row++;
img->map[y][x].c.g = *row++;
img->map[y][x].c.r = *row++;
img->map[y][x].c.a = 0xff;
}
}
1行分のデータを一気に読み込み、それをBGRの順に格納する。 4の倍数のパディングについては、1行分を読み込んだ時点でそのパディングも含めて読みだしていて、 格納の際は必要なデータだけを利用し、パディングを無視している。 つまり、特段の処理は記述されていないが、パディングの読み飛ばしもきちんと処理されている。
以上、かなり条件を限定した簡易的な読み出し処理だが、シンプルで分かりやすい処理だと思う。 たったこれだけのコードで、一般的に出回っている画像形式が読み込めるようになるということで、 初級者がステップアップのために取り組むには良いボリュームだと思う。 次回の書き出し処理と合わせて、是非チャレンジしてほしい。