9. 画像ファイル --- 独自のフォーマットを作ってみよう

それでは、バイナリデータの世界を覗いてみましょう。 テキストデータの特徴が簡便性と汎用性だとしたら、 バイナリデータの特徴は高効率と自由だと言えます。

つまり、バイナリデータは 0〜255 までの数をフルに使うので、 同じサイズのデータでも情報量が多くなり、また、0〜255 までの数を 「文字」にとらわれずに自由に解釈するができます。

一方で、この自由さこそが原因となって、バイナリファイルは中を覗いても なんのこっちゃ、ということになります。実際、現実に広く使われている Windows ビットマップファイル (BMP) や JPEG 画像ファイルや MS-Word 文書などのバイナリファイルは、その中身を覗いてやるには あまりにも複雑で、この講座では手に負えません。 そこで、簡単な独自の画像ファイルの形式 (フォーマット) である kubota1 フォーマットを作ってやることにします。

まず、画像のサイズは 100ドット×100ドット固定としましょう。 あとで、サイズ可変になるように拡張します。 それから、色数ですが、これは 256 色固定とします。

そうすると、256 色のうちのどれかの色を持つ点 (ドット) が 100×100=10000 ドットあるわけです。

まず、ひとつの点の色を表現するにはどうすればいいかを考えます。 ひとつの点の色は、256 種類の色のうちのひとつなのですから、1 バイト あればそれを表現することができます。次に、0〜255 までの数と 256 種類の色とをどのように対応させるかという問題があります。 つまり、たとえば 0 という数は青を表すのか・赤を表すのか、ということを 決めてやる必要があります。 しかしここでは、かなり強引ですが、256 種類の色には番号がつけられていて、 すべてのコンピュータがそれを知っていると仮定します。そうすると、 数と色との対応づけをどうするかについて考える必要がなくなります。

次に、画像は 100×100 の 2 次元の情報ですが、コンピュータで扱えるデータは 0〜255 の数が一列に並んだもの、すなわち 1 次元なので、2 次元から 1 次元への 変換が必要になります。

そのために、画像に座標をつけて、左上を (x,y)=(0,0)、右上を (x,y)=(99,0)、 左下を (x,y)=(0,99)、右下を (x,y)=(99,99) としましょう。そして、 (0,0)、(1,0)、(2,0)、(3,0)、...、(98,0)、(99,0)、(0,1)、(1,1)、(2,1)、 ...、(99,1)、(0,2)、...、(99,2)、(0,3)、...、(98,99)、(99,99) という 順番、つまり左から右へ、上から下へ順番に並べることにすれば、 一列に並べることができます。そして、ひとつの点は 1 バイトで表現する ことになっていたので、点のデータをこの順番に並べると、 全体で 10000 バイトのサイズがあることになります。(図)

 x=0                        x=99
+----------------------------------+
| (0,0)―――[1]―――――→(99,0) | y=0
| ――――――[2]―――――――→  |
| ――――――[3]―――――――→  |
|              :                   |
|              :                   |
|              :                   |
| ――――――[99]―――――――→ |
| (0,99)―――[100]―――→(99,99) | y=99
+----------------------------------+
   [1][2]の順に点のデータを並べる

これで、最も簡単な画像フォーマット kubota1 が完成しました。 このプログラムは、kubota1 フォーマットの画像ファイルを読み込んで 画面に表示させる C のプログラムです。ここで、display_dot(x,y,c) という関数は、画面の (x,y) という位置の色を c にするという機能を もつと仮定します。C が分からない人は、読み飛ばしてもらえばいいです。 とりあえず、こんなに短いプログラムでいいんだ、ということが 分かってもらえればいいです。

#include <stdio.h>		/* 基本的な機能を使うという宣言 */
main(int argc,char *argv[]){	/* ここからプログラムが始まる */
    FILE *fp;			/* fp という変数を使うという宣言 */
    int x,y,c;			/* これも変数の宣言 */
    fp=fopen(argv[1],"rb");	/* ファイルをオープンする */
    for(y=0; y<100; y++){	/* 縦軸を 0 から 99 まで繰り返す */
        for(x=0; x<100; x++){	/* 同じく横軸 */
            c=fgetc(fp);	/* ファイルから 1 バイト読む */
            display_dot(x,y,c);	/* 画面に表示する */
        }			/* 横軸の繰り返し終わり */
    }				/* 縦軸の繰り返し終わり */
}				/* プログラム終わり */

次に、kubota1 フォーマットを改良して、kubota2 フォーマットを 作ります。サイズが固定ではあんまりなので、サイズを変えれるようにしましょう。 そのためには、画像のサイズを記録しなければなりませんが、そのために どれだけのバイトを割り当てるかが問題になります。1 バイトで表現できる 数は 0〜255 で、大きさ 0 ドットというのはありえないから 0〜255 を 1〜256 に読み換えることにしましょう。そして、横方向と 縦方向のサイズが必要になります。そこで、データの先頭バイトは縦方向の サイズ、次のバイトは横方向のサイズ、その次から kubota1 フォーマットと 同様な方法で色のデータを並べるようにしましょう。まとめると、

+----------------+
| 縦方向ドット数 |
+----------------+
| 横方向ドット数 |
+----------------+    --
| (0,0)の点の色  |    ↑
+----------------+    |
| (1,0)の点の色  |    |
+----------------+    |(縦方向ドット数)
        :             |  ×(横方向ドット数) バイト
        :             |
+----------------+    |最大 256×256=65536 バイト
|  最後の点の色  |    ↓
+----------------+    --

という構造をしていることになります。ところで、画像のサイズに関する情報を 先頭に持ってきたのはなぜかというと、データを読むときにはふつう先頭から 順番に読んでいくので、まずサイズが分かったら、以降の点の色のデータの部分で 何回繰り返せばいいかが分かるからです。プログラムを書いてみると、

#include <stdio.h>
main(int argc,char *argv[]){
    FILE *fp;
    int x,y,c,x_size,y_size;	   /* 変数宣言が増えてます */
    fp=fopen(argv[1],"rb");
    y_size=fgetc(fp)+1;		   /* ファイルから 1 バイト読む */
    x_size=fgetc(fp)+1;
    for(y=0; y<y_size; y++){	   /* 繰り返す範囲が変わっています */
        for(x=0; x<x_size; x++){
            c=fgetc(fp);
            display_dot(x,y,c);
        }
    }
}

となります。kubota1 のプログラムとほとんど同じですね。

ところで、kubota1 形式の画像ファイルと kubota2 形式の画像ファイルがあるとき、 ファイルを一つ指定すれば、そのファイルがどちらの形式であるかを自動判別する ことはできるでしょうか。

たとえば、ファイルのサイズが 10000 バイトなら kubota1、違えば kubota2 だと判断するようにプログラムすればうまくいくでしょうか。 これでは、kubota2 形式でもサイズが 10000 バイトになる可能性があるので、 完全とは言えません。では、ファイルの先頭 2 バイトを、kubota2 形式を 仮定して読み込み、それから計算されるサイズと実際のファイルのサイズが 一致すれば kubota2 という判断基準はどうでしょうか。これでも、 kubota1 形式が誤って kubota2 形式だと判断されてしまう可能性があります。

それに、このような複雑な判断法を使っていると、将来 kubota3、kubota4、kubota5 と数多くの形式が生まれたとき、行き詰まってしまうかも知れません。

そこでよく使われるテクニックが、データの形式を表す記号 (識別子) をファイルの先頭に埋め込んでおく、というものです。

たとえば、kubota1 形式のファイルは必ず 1 という数で始まるようにする (上の kubota1 形式の先頭に「1」という内容の 1 バイトを加える) という ふうにし、kubota2 形式のファイルは必ず 2 という数で始まるというふうに kubota1 形式と kubota2 形式を改良してやれば、先頭の 1 バイトを見ただけで すぐに区別がつきます。

識別子の長さは 1 バイトでなければならないということはありません。 実際には、2〜8バイト程度の長さをとり、しかもこの部分はアスキーコードを 用いて文字として解釈したときに人間が読んで意味が分かるような 数値を選ぶのが賢明です。実際、現実に広く用いられているデータ形式の 識別子を見てやると...

   データ形式                    識別子
   BMP (Windows ビットマップ)    「BM」の 2 バイト
   GIF 画像                      「GIF87a」「GIF89a」などの 6 バイト
   ZIP (圧縮ファイル)            「PK」の 2 バイト

というぐあいです。もちろん、このような識別子を持たない (必要としない) 形式もたくさんありますが。

次の kubota3 形式では、もっと多くの色が使えるようにしてみましょう。 現在、広く用いられているパソコンのなかで、最も表示色数が多いものは、 16777216 (2 の 24 乗) 色を表示できます。そこで、16777216 色を表現できる ような形式を考えましょう。

16777216 は 2 の 24 乗なので、16777216 個のものの中から 1 個のものを 指定するには、24 ビットの記憶容量があればいいことになります。 24 ビット= 3 バイトですので、1 ドットにつき 3 バイトが必要になります。 また、16777216 色というのは、赤・青・緑をそれぞれ 256 段階に変化させた ものを混ぜ合わせた色になっています (256×256×256=16777216)。 したがって、3 個のバイトをそれぞれ赤・青・緑の強度とすると、 16777216 種類の色と 16777216 種類のバイトの状態とを対応づけることができます。

また、画像サイズが最大 256×256 まででは小さいので、縦横のサイズを記憶するのに 1 バイトではなく 2 バイトを割り当てるようにしましょう。2 バイト、つまり 16 ビットの記憶容量があると、2 の 16 乗=65536 通りの情報を表すことができ、 画像サイズの上限は 65536×65536 になります。

ここで、エンディアンという問題があります。連続する 2 バイトを 単一の 16 ビットの数と解釈するとき、第一のバイトと第二のバイトのどちらを 上位 (桁が大きい方) にし、どちらが下位 (桁が小さい方) にするか、 という問題です。CPU も、連続する 2 バイトや 4 バイトを単一の 16 ビットや 32 ビットの数として解釈する命令があり、そのとき、インテル系の CPU (8080、 8088、Z80、8086、i486、Pentium など) では下位バイトが先にくる (リトルエンディアン) ことになっていますが、モトローラ系の CPU (6809、68000、68040、たぶん PowerPC など) ではその逆に 上位バイトが先にくる (ビッグエンディアン) ことになっています。 それを示したのが、次の図です。

  -------------------------------------------------------
  2 バイトのデータ列          123, 234
  (16 進数)                   0x7b, 0xea
  -------------------------------------------------------
  リトルエンディアンで解釈  123 + 234 × 256 = 60027
  (16 進数)                 0x7b + 0xea × 0x100 = 0xea7b
  -------------------------------------------------------
  ビッグエンディアンで解釈  123 × 256 + 234 = 31722
  (16 進数)                 0x7b × 0x100 + 0xea = 0x7bea
  -------------------------------------------------------

    なお、0x123abc は 16 進数で 123abc を意味します。

そのため、この問題をコンピュータに任せてしまうと、インテル系の CPU を 使ったコンピュータとモトローラ系の CPU を使ったコンピュータで データの互換性がないという困ったことになってしまいます。 そこで、どちらかに決める必要があります。今回作っている kubota3 形式は 実用性を考えないので、簡単なビッグエンディアンとしておきます。

このようにしてできた kubota3 形式をまとめると、次の図のようになります。


+----------------+
|   'k' (0x6b)   |  以下、識別子
+----------------+
|   'u' (0x75)   |
+----------------+
|   'b' (0x62)   |
+----------------+
|   'o' (0x6f)   |
+----------------+
|   't' (0x74)   |
+----------------+
|   'a' (0x61)   |
+----------------+
|   '3' (0x32)   |
+----------------+
| 縦方向ドット数 | 上位バイト (a)
+----------------+                実際のドット数 = (a)×256+(b)
| 縦方向ドット数 | 下位バイト (b)
+----------------+
| 横方向ドット数 | 上位バイト (c)
+----------------+                実際のドット数 = (c)×256+(d)
| 横方向ドット数 | 下位バイト (d)
+----------------+    --
| (0,0)の点の赤  |    ↑
+----------------+    |
| (0,0)の点の青  |    |
+----------------+    |
| (0,0)の点の緑  |    |
+----------------+    |
| (1,0)の点の赤  |    |
+----------------+    |(縦方向ドット数)
        :             |  ×(横方向ドット数) × 3 バイト
        :             |
+----------------+    |最大 65536×65536×3=12884901888 バイト
|  最後の点の緑  |    ↓
+----------------+    --

実際に広く用いられている画像フォーマットは、もっと高度なテクニックを 豊富に使っています。たとえば圧縮とか、透明色、パレット、など。 しかし、ここの話が分かった人は、それらも簡単に分かるはずです (初心者向けというふうに謳いながら、今回はものすごく難しい話に なってしまいました)。今回の話が分からければ次回は分からない、 というふうにはしないつもりです。


10. 圧縮 --- 現代の魔術か?

「データの形式っていったい何なの?」に戻る

久保田智広ホームページに戻る