複天一流:どんな手を使ってでも問題解決を図るブログ

宮本武蔵の五輪書の教えに従い、どんな手を使ってでも問題解決を図るブログです(特に、科学、数学、工学の問題についてですが)

東大数学2024問題6 (part 9):gnuplotで見つけた格子点の確認

前回のあらすじ

東大入試問題の数学2024問題6の後半にとりかかった。まずは二次方程式の解が2つとも正となる条件について調べることにした。条件を満たす領域がわかり、数値的にいくつかの格子点を(手で)抜き出してみたら、次のような感じとなた。

赤点が条件を満たす格子点

これが果たして、素数を生み出すかどうか検算してみる。

判別式を計算してみる

ところで判別式は英語でDiscriminantというそうだ。だから$D=b^2-4ac$とDで表すのであろう。

さて、計算結果をじっと睨んで、手で抜き出した格子点をデータファイルにしてみた。これをpythonのプログラムに読み込ませて、判別式を計算し、ちゃんと素数が出てくるか調べてみたい。ということで、プログラム名はdiscriminant.pyとする。

import math

def discr(a,b):
    return a**2 - 4*(b-1)

while True:
    try:
        ab_data = input().split()
        a = int(ab_data[0])
        b = int(ab_data[1])
        d = discr(a,b)
        sqd = math.sqrt(d)
        if sqd.is_integer() == True:
            sol0 = (-a + sqd)/2.
            sol1 = (-a - sqd)/2.
            print(a,b,d,sqd.is_integer(),sol0,sol1)
    except EOFError:
        break
print("Programme completed...")

真ん中あたりにあるif文は、判別式が「平方数」になっているかを確認している。つまり、うまい具合に平方根が開けて、整数となれば、解が素数になる可能性が出てくるのである。もし平方根が開けなければ、解は「実数」になってしまって題意を満たすことはできない。その判定にis_integer()という関数を利用したというわけである。

さて、格子点の中でうまい具合に平方数になるものは意外にたくさんあって、全部で12個見つかった。

% python3 discriminant.py < ab-data.dat|cat -n
     1  -5 1 25 True 5.0 0.0
     2  -4 1 16 True 4.0 0.0
     3  -3 1 9 True 3.0 0.0
     4  -2 1 4 True 2.0 0.0
     5  -1 1 1 True 1.0 0.0
     6  0 1 0 True 0.0 0.0
     7  -2 2 0 True 1.0 1.0
     8  -3 3 1 True 2.0 1.0
     9  -4 4 4 True 3.0 1.0
    10  -4 5 0 True 2.0 2.0
    11  -5 5 9 True 4.0 1.0
    12  -5 7 1 True 3.0 2.0
    13  Programme completed...

右端の2つの整数が2次方程式の解であるが、これが素数となっているのは12番目のケースのみである。0や1は素数の定義により除外されるからである。つまり、我々の探索範囲で条件を満たしたのは、$(a,b)=(-5,7)$のときに発生する、$f(2)=2, f(3)=3$という2つの素数の解であった。

素数の解は4つあるだろうか?

さて、(a,b)=(-5,7)の場合、ケース3.により2つの解が見つかったことになる。果たして、この(a,b)に対し、ケース1.およびケース2.そしてケース4.は解を2つ提供できるだろうか?まずは似たような状況としてケース4.が2つの素数解を提供できるか確認してみよう。

この場合、$x=-p<0$に対して, $k(p)=p^2+5p+7=-1$とならねばならない。つまり、 \begin{equation} p^2+5p+8 =0 \end{equation} の解が素数になっていなくてはならないのだが、解の公式で解いてみると \begin{equation} p = \frac{-5\pm\sqrt{25-32}}{2} \end{equation} となってしまい、これは「複素数」である。実数ですらないので素数には到底なり得ないのは明らかである。ということで、あえなく唯一の望みは消えてしまったのである。

ケース4.がだめなら、ケース1.とケース2.を満たす素数が2つ見つかればよい。やってみると \begin{equation} k(1)=1-5+7 =3, \quad k(-1) = 1+5+7 =13 \end{equation} となった。$k(1)=3$はよいのだが、2次方程式の解の一つとしてすでに見つかっているため、「だぶり」である。もうひとつのケースについては$k(-1)=13$となってダメである。これはケース2.なので$k(-1)<0$とならなければならないからである。13は素数であるから、かなりいい線まで肉薄できたのだが、正負の関係で失敗である。ということで、今回候補となった(a,b)については、問題6の前半で見つけたような素数の解が3つあるケースではなく、素数の解が2つある場合に過ぎないことが確認された。

東大数学2024問題6 (part 8): gnuplotで条件に合致する領域を塗ってみる

前回のあらすじ

しばらく前になるが、問題6の前半部分を解析的、および数値的に解いた。後半部分は、前半部分の応用であるからあまりおもしろくない、と思っていたのだが、gnuplotの練習の観点から少し面白そうなネタありそうだったので、取り上げてみることにする。

後半部分

後半部分は、考える関数が「若干」一般化され、整数$a,b$を用いて \begin{equation}g(x)=x^3+ax^2+bx=x(x^2+ax+b)\end{equation}となる。問題の前半は$a=10, b=20$という特別な場合を考えていたのであった。後半でも、$x\rightarrow n\in \mathbb{Z}$としたときに、$g(n)$が素数になる場合について考えさせる問題である。$g(n)$が素数になるケースは、問題前半で検討したように、4つの場合が存在する。ただし、以下では因数分解したときの二次関数の部分を$k(x)=x^2+ax+b$と表すことにする。

  1. $x=1$, かつ$k(1)>0$が素数になる場合
  2. $x=-1$, かつ$k(-1)<$が負の符号がついた素数($-p$)になる場合
  3. $p$を素数とする。$x=p>0$に対して$k(p)=p^2+ap+b =1$となる場合
  4. $x=-p <0$に対して$k(-p)=p^2-ap+b=-1$となる場合。

この4つの場合の組み合わせを丁寧に追いかければ証明はできるはずである。とはいえ、いろいろな組み合わせが発生するので、取りこぼしがないように気をつけねばならない。

最初のケース

まずは3.のケースを考えたい。$k(p)=1$となるような素数$p$が見つかるかどうかである。これは$p$についての2次方程式を解くことになるので、一般解は解の公式を用いて \begin{equation} p = \frac{-a\pm\sqrt{a^2-4(b-1)}}{2} \end{equation} と「形式的」にかける。もちろん、このままでは複素数だったり、無理数を一般的に許すことになるので、制限を課さねばならない。

絶対にまずいのが、複素数になってしまう場合である。これを避けるために判別式(つまり平方根の中身)が正値あるいは零値をもつことを課さねばならない。すなわち最初の条件式は \begin{equation} a^2-4(b-1) \ge 0 \rightarrow b \le \frac{a^2}{4}+1 \end{equation} である。この条件式を満たす$(a,b)$ならば, 素数$p$が複素数になってしまうことはない(笑)。

次は無理数にならない条件などを考えたいところだが、ちょっと面倒なので、まずは素数が正数であるという定義から攻めることにしよう。ここで考えるのは、2次方程式の2つの解が両方とも正値をとるための条件である。片方だけ正値ということはあるかもしれないが、まずは両方ともに正値になる場合について考えてみる。 \begin{equation} -a\pm\sqrt{a^2-4(b-1)} > 0 \rightarrow -a > \sqrt{a^2-4(b-1)} > a \end{equation} $a>0$の場合は成立しないので、$a=-a'<0$の場合だけを考える。すなわち \begin{equation} a' > \sqrt{a'^2-4(b-1)} > -a' \end{equation} 右側の不等式は自明な関係式なので、条件式としては利用できない。左側の不等式を自乗して整理すると \begin{equation} b > 1 \end{equation} という条件式が手に入る。

gnuplotの出番

ここで、gnuplotの出番である。3.のケースが2つの素数を許す「必要条件」として次の3つが浮かび上がった。 \begin{align} a &<0 \\ b & > 1\\ b &\le \frac{a^2}{4}+1 \end{align} 我々が興味あるのは、この領域を図示するにはどうすればよいかというgnuplotの問題である。まずはそれぞれの領域を塗りつぶし、重ね塗りすることで対応する領域を見つけ出したい。

いろいろ検索すると、弘前大学の相対論グループが発表しているgnuplotマニュアルが役立ちそうである。

まずは上の条件式のなかに含まれる放物線$b=a^2/4+1$を描いてみよう。今回は、座標軸も描き入れてみよう。

unset key
unset arrow

set size square
set grid

f(x)=x**2/4.+1

xmax=5; xmin=-5
ymax=8; ymin=-2
set arrow from xmin,0 to xmax,0 lw 2
set arrow from 0,ymin to 0,ymax lw 2

plot [xmin:xmax][ymin:ymax] f(x) lw 2

この放物線の下側の領域が3.の場合の2つの解が実数となる領域に対応する。塗りつぶすには次のようにする。

plot [xmin:xmax][ymin:ymax] f(x) w filledcurves y=ymin fc "#114422"

この「塗り絵」はちょっと重苦しいので、透明度を上げて印象を軽くしよう。

set style fill transparent solid 0.2
...
plot [xmin:xmax][ymin:ymax] f(x) w filledcurves y=ymin fc "#114422", f(x) w l lw 3

を付け足すとよい。 境界線の放物線グラフを際立たせるために「2度描き」していることに注意。

次は2次方程式の解が2つとも正になるための2つ目の条件$b>1$を取り込もう。$1<b<\text{ymax}$の部分を塗りつぶせば良いので、

plot [xmin:xmax][ymin:ymax] \
     f(x) w filledc y=ymin fc "#114422",\
     1 w filledc y=ymax fc "#114422",\
     f(x) lw 2 lt 2, \
     1 lw 2 lt 2

となる。重ね塗りされた部分が正しい領域になる。

最後に$a<0$を取り込む。描画領域を負の領域と正の領域に分ける必要があり、このためにはサンプリングのテクニックを使う。サンプリングはplot sampleで実現できる。具体的には次のようにする。

plot [xmin:xmax][ymin:ymax] sample \
     [xmin:xmax] f(x) w filledc y=ymin fc "#114422",\
     [xmin:xmax] 1 w filledc y=ymax fc "#114422",\
     [xmin:00] ymax w filledc y=ymin fc "#114422",\
     [xmin:xmax] f(x) lw 2 lt 2, \
     [xmin:xmax] 1 lw 2 lt 2

4行目の定義域が「部分的」、つまり負の領域だけが指定されている。つまりsampleによって定義域を分割(sampling)することができるのである。表示結果は次のような感じになる。流石に3重塗りだとよくわからなくなってくる。

2つの関数グラフに挟み込間れた部分だけを塗る

gnuplotは「挟み込んだ領域」を塗りつぶす命令をもっているのでそれを利用する。上の場合は、挟み込む領域が放物線と$y=ymin$だったので、放物線と$y=1$に変えればよいだけである。

plot [xmin:xmax][ymin:ymax]  f(x) w filledc y=1 fc "#114422",\
     f(x) lw 2, 1 lw 2 lt 2

最後にsampleのテクニックを盛り込んで完了である。随分簡単に描ける。

plot [xmin:xmax][ymin:ymax] sample \
     [xmin:0] f(x) w filledc y=1 fc "#114422", f(x) lw 2, 1 lw 2 lt 2

ちょっとだけ数学の問題に戻ってみる

さて、問題を解くという観点に少しだけ戻ってみよう。

塗りつぶされた領域のどこでも良いわけではなく、整数の格子点だけが許されることに注意しなくてはならない。このグラフに含まれている部分だけを抜き出せば、(a,b)=(-5,0),(-4, 0), (-3,0), (-2, 0), (-1,0), (0,0); (-5,1),(-4,1),(-3,1),(-2,1); (-5, 2), (-4,2),(-3,2),(-2,2); (-5,3),(-4,3), (-3,3); (-5,4),(-4,4), (-5,5),(-5,4),(-5,6),(-5,7)が候補となる。これらの値を$k(n)$に代入したときに、素数となるようケースが見つかるだろうか?(つづく)

画像処理にチャレンジ(part 4): jpeg(JFIF)のヘッダー情報

前回はjpegのヘッダー情報を読み込むための関数を書いた。そして、JPEGを表す「魔法数」、つまりSOIを表す整数である「0xff 0xd8」を読み取ることに成功した。今回は、この関数を用いて更なるヘッダー情報を読み込んでみる。

次のヘッダーを読み込む(APP0)

最初の2バイトが魔法数になっているのは、JPEGBMPも同じだった。たぶん、pngやmp3なんかも同様の魔法数があるのであろう(ちょっと検索してみたらどうも2バイトとは限らないようである)。JPEGの場合、魔法数の次から始まるのが種々の「セグメント(情報の塊)」で、それぞれのセグメントは「マーカー」という2バイトの整数値によって区切られている。したがって、最初の読み込みの後は再度2バイトを読み込んで、最初のマーカーを識別することになる。

多くのJPEG形式の場合、この最初のマーカーはFF Enという値になってようだが、これはAPPnと呼ばれるマーカーである。JPEG画像を生成したアプリケーションが付加したヘッダー情報が続くことを示すマーカーである。nは0-15つまり16進数の一桁分に相当する整数である。したがってAPPnセグメントは16種類ある。

もっとも頻発するのがn=0の場合、つまりAPP0である。マーカー値はFF E0になっていて、その値に続いてJPEGの拡張版であるJFIF形式の画像データが始まる。

APP1セグメントはある場合とない場合がある

ときどきAPP1が付随しているJPEG画像がネットなどで見つかるかもしれない。APP1とはExifという形式で記録された画像のヘッダー情報である。たとえば、どんなデジカメを使って撮影したか、どこで撮影したか、などといった情報が記録される。よく犯罪で利用されるのはこの部分である。個人情報などがここに記録される場合があるので、インターネットに画像をアップロードして不特定多数に公開するときは、この部分を削ってからアップしたほうがセキュリティが高まる。「削る」といっても、バイナリファイルをエディタで編集するのは難しいので、自作のプログラムを使って削除するのが手っ取り早いだろう(ここでの研究が役立つはずである)。

ファイルを開けておけば、次々と読み続けることができる。

JPEGのセグメントはマーカー値の次に、2バイトのセグメント長が来る。この情報はどれだけファイルを読み進めるか判断するときに役立つ。C言語のファイルポインタは、現在の読み取り位置がファイルのどの位置まできているか記録してくれるので、ファイルを開けっぱなしにしておけば(つまりfclose()を実行しなければ)、順番にデータを読み続けていくことが可能である。

ohtani.jpgの場合はちょっと古いver.1.01のJFIFであった

ohtani.jpgの場合は、JPEGの魔法数ff d8が来た後、ff e0というマーカーが置かれていた。つまり、APP0であり、JFIF形式であることが宣言されていた。JFIFのバージョンを調べてみると1.01であった。

コード

それではAPP0までを丸ごと読み取って、そのバージョンを表示させるコードを書いてみる。

#include <stdio.h>

int *readJpegHeader(FILE *fp, int byteSize, int *header_p){
    int i;
    for(i = 0; i < byteSize; ++i){
        header_p[i] = fgetc(fp);
    }
    return header_p;
}

int main(){
    int i, byteSize;
    int header_info[256];
    int *header_p;
    FILE *fp;
    if( (fp = fopen("ohtani.jpg","rb")) == NULL){
        printf("File not found.\n");
        return 1;
    }
    printf("File found.\n");
    header_p = readJpegHeader(fp, byteSize = 2, header_info);
    if( header_p[0] == 0xff && header_p[1] == 0xd8){
        printf("JPEG file found: success.\n");
        header_p = readJpegHeader(fp, byteSize = 2, header_info);
    }else{
        printf("Not a JPEG: error.\n");
        return 1;
    }

    if( header_p[0] == 0xff && header_p[1] == 0xe0){
        printf("Marker APP0 found.\n");
        header_p = readJpegHeader(fp, byteSize = 2, header_info);
        byteSize = header_p[1] * 256 + header_p[0];
    }else{
        printf("Marker APP0 not found. Maybe elsewhere\n");
        return 1;
    }

    header_p = readJpegHeader(fp, byteSize, header_info);
    for(i = 0; i < 5; ++i)
        printf("%c ",header_p[i]);
    printf("\n");
    printf("Version: %3.2f\n",header_p[5] + header_p[6]/100.);

    printf("-- next marker: %x %x\n",header_p[byteSize - 2], header_p[byteSize -1]);

    return 0;
}

マーカーの次に置かれているセグメント長は、マーカーを除いた部分のバイト長を意味する。とすると、最後から7行目にある、自作関数を最後に呼び出したところで、読み出しサイズを(バイト長を除いた)byteSize - 2ではなく、単にbyteSizeとしているのを不自然に感じるかもしれない。これはあえてそうすること次のマーカーを読み取っているのである。最後のprintf文でそれを確認しており、マーカーがff dbつまり、APP0の次にDQTセグメントが続くことが確認できる。

このプログラムをコンパイルしてから実行すると、次のようになるはずである。

% a.out    

File found.
JPEG file found: success.
Marker APP0 found.
Size of APP0: 16
J F I F  
Version: 1.01
-- next marker: ff db

さらにその先は?

このような感じで、次々とマーカーを識別しつつ、対応するセグメント長を計算しながらヘッダー情報を先へ先へと読み進めていけば良い。最後に、マーカーがff daとなったところが最後のセグメントである。大抵は12バイト長であるが、それを読み切った先が、JPEG画像データの本体となるのである。

ohtani.jpgの場合、画像データの本体の最初の値はf9 a1である。これはマーカーではない。画像データそのものである。

画像処理にチャレンジ(part 3): jpegのヘッダー情報を読み込む関数をつくる

前回のあらすじ

前回はJPEGのヘッダー情報を読み取ってみた。最初の「魔法数」に相当するSOIがff d8であること、つづくff e0がAPP0に相当し、JFIF形式の画像(JPEGの拡張版)となっていることなどが読み込めた。

しかし、1バイトずつfgetc()関数で読み取るのは、なんだか素人っぽくて恥ずかしい感じもしないでもない。そこでC言語らしく「自作の関数」でも書いて「どうだ!」とやってみたい。ということで、今回はそれをつかってJPEG(JFIF形式)のヘッダー情報を読み取ってみたい。

自作の関数Ver.0

今回も読み取るのは、LA Dodgersの大谷選手の画像である(MLBのHPより拝借したもの)。この画像は"ohtani.jpg"という名前で保存した。まずは基本部分のコードを書き込もう(main.cと名付ける)。

#include <stdio.h>

int main(){
    FILE *fp;
    if ( (fp = fopen("ohtani.jpg","rb")) == NULL) {
        printf("File not found.\n");
        return 1;
    }
    printf("File found.\n");
    fclose(fp);
    return 0;
}

この状態ですでにコンパイルすることができるが、当然ながら実行しても何も起きない(ohtani.jpgがあるかないかは判断してくれる)。

これまでは、main()のなかで直接fgetc()を利用していたのだが、関数を呼び出してそこで読み出すようにしてみる。関数名はreadJpegHeader()としよう。 読み出したヘッダー情報は「整数」なので整数型の関数となる。また、ファイルの中身にアクセスするためには、ファイルポインタ*fpを引き取る必要もあるだろう。 すなわち、次のような形になることが予想される。

int readJpegHeader(FILE *fp){
    int headerInfo;
    headerInfo = fgetc(fp);
    return headerInfo;
}

この部分をmain.cに書き込んでもいいし、readJpegHeader.cという別ファイルに書き込んで後でまとめてコンパイルしてもよいし、いろいろあるだろう。まだ関数が短いので、まずはmain.cの先頭に書き加えてみる。

#include <stdio.h>

int readJpegHeader(FILE *fp){
    int headerInfo;
    headerInfo = fgetc(fp);
    return headerInfo;
}

int main(){
    FILE *fp;
    if ( (fp = fopen("ohtani.jpg","rb")) == NULL) {
        printf("File not found.\n");
        return 1;
    }
    printf("File found.\n");
    header_info = readJpegHeader(fp);
    printf("%x\n",header_info);
    fclose(fp);
    return 0;
}

コンパイルは通る。実行すると、ohtani.jpgの最初の1バイトの情報が出力される。それは当然ながら0xffである。

jpegか、それともbmpかどうかを判定するには、最初の2バイトを読み出す必要がある。そこで任意の長さのデータの読み出しを可能にするために、関数の引数にbyteSizeという整数値を加えることにする。そうすると自作関数は次のように書き変わる。が、すぐに問題にぶち当たる。まずはコードを書いてみよう。

int readJpegHeader(FILE *fp, int byteSize){
    int i, headerInfo;
    for( i=0; i < byteSize; ++i){
        headerInfo = fgetc(fp);
    }
    return headerInfo;
}

このままコンパイルしてもエラーは出ないが、出力は2バイト目の0xd8だけとなる。もちろん、先ほどの実行結果から最初の2バイトが0xff 0xd8となったことは推測できる。しかし、1回の実行で2バイト分の情報を一括して出力させたい。1文字ずつ関数を呼ぶという手も考えられるが、それではfgetcを1文字ずつ呼び出すのと大差ないから、全くの無駄な解決法である。コンピュータというのは「繰り返し」をやらせるための機械であるから、これができないと手作業と大差ないことになってしまう。こういう場合に利用すべきものが配列である。そこで、headerInfoを整数型の配列に拡張してみる。

int *readJpegHeader(FILE *fp, int byteSize){
    int i, headerInfo[byteSize];
    for( i=0; i < byteSize; ++i){
        headerInfo[i] = fgetc(fp);
    }
    return headerInfo;
}

さて、このコードはコンパイルできるだろうか? まず、配列自体の受け渡しがC言語ではできないので、ポインタだけを自作関数からmain()関数に渡さねばならない。そのためには関数の型はint *つまり整数のポインタを返す関数として定義しなくてはならない。またmain()でこのポインタを受け取るためには、main()でポインタを用意しておく必要がある。ということで、全体として次のような形になる。

#include <stdio.h>

int *readJpegHeader(FILE *fp, int byteSize){
  int i;
  int headerInfo[byteSize];
  for( i=0; i < byteSize; ++i){
    headerInfo[i] = fgetc(fp);
  }
  return headerInfo;
}

int main(){
  int i;
  int *header_info, byteSize;
  FILE *fp;
  if( (fp = fopen("ohtani.jpg","rb")) == NULL){
    printf("File not found.\n");
    return 1;
  }
  printf("File found.\n");
  header_info = readJpegHeader(fp, byteSize=2);
  for( i=0; i < byteSize; ++i){
    printf("%d %x\n",i, header_info[i]);
  }
  fclose(fp);
  return 0;
}

コンパイルするとwarningが出るが、一応実行形式a.outは生成された。しかし実行してみると予想外の結果となってしまった。

%a.out 

File found.
0 ff
1 1

最後の行が"1 1"となっている。ここはJPEGのSOIとなるように"1 d8"となってほしいところである。自作関数で読み取りに失敗が起きているのであろうか?関数内で配列の中身を表示させてみる。

int *readJpegHeader(FILE *fp, int byteSize){
  int i;
  int headerInfo[byteSize];
  for( i=0; i < byteSize; ++i){
    headerInfo[i] = fgetc(fp);
  }
  for (i = 0; i < byteSize; ++i){
    printf("%d %x\n",i, headerInfo[i]);
  }
  return headerInfo;
}

コンパイルするとwarningは出るが、先ほど同じようにa.outは生成されるので、実行してみた。

File found.
0 ff
1 d8
0 ff
1 1

2、3行目が関数内で配列の中身を印字させた結果であり、正しくJPEGのSOIが表示されている。しかし、その配列(のポインタ)をmain()で参照した途端に問題が派生している。問題と言っても最初のffは正しいのだが、2つめのd8が壊れて1に変わってしまったという「部分的な問題」である。

あきらかにWarningを無視しているのが良くない点である。仕方ないので、よく読んで対処することにする。まずはwarningの内容を見てみよう。

warning: address of stack memory associated with local variable 'headerInfo' returned [-Wreturn-stack-address]
  return headerInfo;
         ^~~~~~~~~~
1 warning generated.

局所変数に関するスタックメモリのアドレスが返り値になっているから警告ね」と言っているように思える。だめなんですか?としか言いようがないが、ダメなのである。

局所変数はスタックと呼ばれる「簡易メモリ領域」に一時保存されるが、その内容は保証されないらしい。最初の要素は運良く保存してくれたが、2つ目はダメでした、ということなんであろう。スタックを利用せず、局所変数の値を保存するにはどうすればよいのだろうか?

というか、局所変数の内容を利用しているから警告なのである。とすれば、mainでheaderInfoを配列として宣言し、そのポインタを渡してやったらどうだろう。これなら、スタックを経由しないのではないだろうか?

ということで、次のように書き換えてみた。

#include <stdio.h>

#define MAX_HEADER 14

int *readJpegHeader(FILE *fp, int byteSize, int *headerInfo){
  int i;
  for(i = 0; i < byteSize; ++i){
    headerInfo[i] = fgetc(fp);
  }
  for(i = 0; i < byteSize; ++i){
    printf("%d %x\n",i, headerInfo[i]);
  }
  return headerInfo;
}

int main(){
  int i;
  int header_info[MAX_HEADER], byteSize;
  int *header_p;
  FILE *fp;
  if( (fp = fopen("ohtani.jpg","rb")) == NULL){
    printf("File not found.\n");
    return 1;
  }
  printf("File found.\n");
  printf("%p \n",header_info);
  header_p = readJpegHeader(fp, byteSize=2, header_info);
  printf("%p \n",header_p);
  for( i=0; i < byteSize; ++i){
    printf("%d %x\n",i, header_p[i]);
  }
  fclose(fp);
  return 0;
}

配列header_info[]をmain()の中で定義し、その先頭要素をポインタによって自作関数とやりとりする形式である。これなら局所変数を呼び出さないのでwarningは出ないはずである。コンパイルしてみると、警告は全て消えa.outが生成された!成功である。実行してみると予想通りの結果となり成功である。

File found.
0x16f6db6b0 
0 ff
1 d8
0x16f6db6b0 
0 ff
1 d8

最初にmain()で定義された配列header_infoの先頭アドレスが表示され、それが0x16f6db6b0であることがわかる。自作関数内で正しいJPEGヘッダーデータが読み込まれていることが次の2行"0 ff, 1 d8"の部分でわかる。次の長い16進数はmain()の中で表示させたポインタheader_pの中身である。配列header_infoの先頭アドレスと同じになっていることが確認できた。そして最後の2行である。0 ff, 1 d8となり、JPEG(JFIF)のSOI識別コードであるff d8が表示できた!

ちなみに、main()の中でheader_pの代わりにheader_infoを表示させてみたらどうなるだろうか?つまり、最後の部分であるfclose(fp);の文の上の箇所でheader_pの代わりにheader_infoに変えたらどうなるか?

...(省略)

for( i=0; i < byteSize; ++i){
    printf("%d %x\n",i, header_info[i]);
  }
fclose(fp);

...(省略)

コンパイルは通ってしまって、実行形式を走らせても同じ結果を得ることが確認できた。つまり、目的だった「自作関数からの複数データの受け渡し」に成功したことになる(やった!)。

画像処理にチャレンジ(part 2): jpegのヘッダー情報解読

前回のあらすじ

前回はBMP画像ファイルの読み込みに挑戦し、まずはヘッダーファイルという画像情報の記録に挑戦し、少しだけその「正体」を理解することができた。バイナリファイルの読み書きについての練習もできたし、実りの多い試みであった。

引き続いてBMPファイルの画像データそのものにアクセスし、加工してみようかと思ったのだが、ひとつアイデアが出てきてしまったので、最初にそちらを片付けてから本筋に戻ることにした。そのアイデアとはJPEGのヘッダー情報の読み取りの挑戦である!

もともと、デジカメのデータはjpegで記録することが多いので、画像処理したい対象はjpegpngであった。しかし「圧縮」という複雑な処理が施されているので、画像処理の素人には敷居の高い代物に見えた。しかし、よくよく考えるとヘッダー情報なら「圧縮」されているわけはないので、単なるバイナリファイルの読み取りで事済むはずじゃないだろうか?と思いたったというわけである。また、これまでに書き下したBMPヘッダーファイル読み取りプログラムは、少し書き換えればバイナリファイル一般に応用可能である。つまり、jpegに踏み込める上に、コードは再利用できるということで「一石二鳥」というわけである。

最初のマジックナンバー

jpegファイルはバイナリファイルなので、ファイルサイズ以内の読み取りサイズ(私のコード中でbyteSizeと定義されているもの)を指定してファイルをひらけば、なんらかのデータは読み取れるはずである。BMPの場合は最初14バイトがBITMAPFILEHEADERという「構造体」になっていたが、JPEGはどうなんだろうか?14バイト以上の画像データをどこかで拾ってきて、最初の14バイトを兎にも角にも読み取ってみよう。

拾ってきたのは、LA Dodgersの大谷選手の紹介HPに利用されていた画像である。今回のサンプルプログラムで読み取りたいのは、この画像のデータである。

MLBのLAドジャースのHPより拝借...
まずは何も考えずに最初の14バイトを(先日bmp用に書いたコードを使って)読み取ってみた。結果は次の通りである。

File found.
255 ff 
216 d8 
255 ff 
224 e0 
0 0 
16 10 
74 4a 
70 46 
73 49 
70 46 
0 0 
1 1 
1 1 
0 0 

読めた!読めたぞ!

最初の2バイトに"ff d8"というのがあって、これがBMPファイルのマジックナンバー"B M (0x42 0x4d)"に似た感じがある。続く"ff e0"もなんらかの記号のような印象を受ける。この数字を使って検索をかけてみると案の定であった。ただし、JPEGの場合には「魔法数」とは呼ばず、SOI(Start of Image)と呼ぶそうである。

JPEGのファイルであることを示す最初のマジックナンバー(魔法数)は、先頭2バイトの"ff d8"であった。つまり、最初の2バイトを読み取ると、BMPか、JPEGか識別できるというわけだ。これは後で画像処理アプリを開発するときに利用できそうな情報である。

JPEGには終わりのマークもある(EOI)

検索結果を読み進めると、BMPと違って、JPEGにはファイルの終わりを示すマークもあることがわかった。それはEOI(End of Image)と呼ばれる"0xff 0xd9"であるようだ。1バイトずつ読み取っていけば、最後にEOIが出てきて、そこでJPEGファイルが終わるということなんだろうか?この実験は後でやってみることにしよう。

ff e0の意味

JPEGの「魔法数」に相当するSOI、つまり0xff 0xd8の数字の次に記録されていたのが、0xff 0xe0であった。検索結果のなかにこの意味の解説も書かれていた。

これはAPPnと呼ばれるデータセグメントの先頭で、JPEGを作成したアプリケーションが付与したヘッダー情報になっているそうである。nの値は0から15まで(つまり16進数)の範囲をもっている。今回の大谷の画像で見つかったのはe0だからAPPnの最初のカテゴリーであるという意味であろう。

ff e0に続く次の2バイトはApp0セグメントの長さを(バイト単位で)示す情報で、大谷の画像では0x00 0x10つまり、16byteがApp0に割かれていることがわかる。次の5バイトは10 4a 46 49 46 00となっているが、これはASCIIコードになっているそうである。文字形式で印字してみると、JFIF\0と印字された。これは厳密な意味での”JPEG”画像データではなく、拡張されたJPEGという意味らしい。とはいえ、その拡張子はjpg, jpegを取ることが許されていて、緩い意味でのJPEG画像ファイルとして認識されているという。(これは知らなかったので勉強になった。)

JFIF\0の宣言文字に続くのは0と1の値であるが、これはなんらかの画像情報のデータであろう。

最初の試みとしてはこんなところであろう

ということで、jpegファイルもヘッダー部分は単なるバイナリデータであることが確認でき、私の未熟な技能でも読み取りが可能であることがわかった。手も足も出なかったJPEGファイルに一太刀浴びせることができたのは嬉しい限りである。JPEGのヘッダーファイルはBMPのそれよりも長く複雑であるそうだが、各種のマーク記号によって区切られており、セグメントごとに読み取ればなんとか処理できそうである。次はそちら方面をやってみようかと思っている。たとえば、JPEGのファイルサイズとか、撮影日時なんかのヘッダー情報が手に入れば、それはかなり実用面に近づくと思うのである。

おまけ(ファイルの終端)

JPEGの終端がff d9で終わっているか、まずは手っ取り早くxddコマンドを使って調べてみた。結果は次のとおりで、予想通りであった。

彗星観測やっと成功

19世紀の初めに発見された70年周期の楕円軌道の彗星、それが今回北半球の空に浮かんでいるPons-Brooks彗星である。 これまで2回挑戦したが、いずれも彗星の位置を確認することができなかった。最初は雲がかかり、2回目は彗星の位置がわからなかった。

しかし、牡羊座と三角座のだいたいの位置がわかってきたこともあり、今回は成功の可能性がだいぶ高まったと感じていた。なにより、雨上がりの青空が終日広がり観測の好機であった。とはいえ、地平線に近い彗星は日に日に夜の空から消えつつある。しかも、この時期は日没がどんどん遅くなっている。もはや6時半でも夕空になってしまっている。これは冬が終わり春が近づいているという意味だから、普通の人には嬉しい変化である。しかし、日没の地平線に浮かぶ彗星を狙うアマチュア天文家には、恨めしい状況なのである。今日失敗すれば「徳俵状態」になるだろう。

木星から牡羊座、そして三角座の先端はあっという間に見つかった。あとは少しずつずらしながら彗星を探すだけである。広角レンズでなんとなくそれらしいものが写ったが、はっきりしない。はっきりさせようと思って、露出時間を長くすると昼間のような青空の写真になってしまう。夕方の地平線間際の天体観測は、残光がとても邪魔である。

しかたないので、しばらく待ってから、今度は望遠レンズで狙ってみることにした。視野が狭くなるので天体の位置がはっきりわかっていないとなかなか天体をレンズの中に入れることができないのだが、局所的に光をたくさん集めたほうが淡い天体は補足しやすいと考え、望遠レンズ使用に踏み切った。

何度か試し撮影をした後、ついにその美しい姿がカメラに映ったのであった!やはり5等星ならばデジタル写真には綺麗に映る。

画像処理にチャレンジ(part 1):彗星観測の失敗と無謀な挑戦

Pons-Brook彗星の観測失敗

今年は大きめの彗星が2つほどやってくると噂に聞いた。実は一つ目はすでに観測可能であり、ネットを検索すると綺麗な緑青色のコマをもった彗星の画像が溢れている。

www.theguardian.com

(Arundel城の頭上に光るこの写真はおそらく合成された偽写真だと思うが)こんな感じで見えたら最高である!ということで、遅まきながら私も観測に参加することにした。ちなみに、この彗星は19世紀の初めに発見されたが、最初がフランス、その70年後が英国の天文学者による観測だったという。つまり70年ほどの周期で太陽の周りを周回する楕円軌道の彗星である。

彗星の観測といえば、吉田氏のHPである。光度グラフをみると5月1日あたりが最大光度のようだが、関東周辺の場合、現在は夕方になると地平線の上方にわずかに見える位置にある。おそらく4月に入ると夜間は地平線に沈み、昼間の空に彗星の位置が移動するだろうから、(北半球からは)観測できなくなるであろう。ということで、「今しかない」のである。

さっそく観測してみたが、現在5等星(最大光度の場合でも4.5等星)なので肉眼でみるのはきついが、観測写真なら十分捕捉可能な明るさである。しかし、現在の彗星存在地点が馴染みの薄い場所であり探しにくいという問題点が(私の場合は)ある。

ということで、広角レンズで、少し広めに夜空を切り取ってみた。 今、もっとも目立つのが木星である(写真の中央からやや下めに位置する明るい点)。その少し上にプレアデス星団(すばる)が見える。すばるの左斜め上にあるV字型の星座が牡牛座でアルデバランの赤い点が比較的よく目立つ。昨年は、アルデバランのすぐ横にZTF2023E3彗星が位置した時期があり、撮影に成功した。アルデバランの赤(朱色)と彗星の緑青のコントラストが見事であった。そのさらに右上にずーっと視線を動かすとオリオン座が右下を覗き込むように傾いて光っている。

しかし、今回のPons-Brooks彗星は「逆側」の領域にある。逆という意味は、木星とスバルをむすぶ垂直線に対して右側という意味である。右側に目を写し、上方を仰ぎ見ると比較的明るめの恒星カペラがみえる(ぎょしゃ座)。写真の中央右上にぎょしゃ座の5角形は丸ごと写っている。カペラの下にあるのがペルセウス座で、星がぐちゃぐちゃっと写っている領域である。その手にメデューサの首ならぬアルゴル変光星が握られている。カペラとアルゴルを結ぶ直線の先に2つの星が縦に並んでいるのが見える。これが牡羊座のハマル(上)とシェラタン(下)である。写真中央部の地平線近くにのっぽの針葉樹が見えるが、そのすぐ左上にある、縦に並んだ2つの恒星である。この星の右上に、鋭角三角形が下向きに位置している。三角座である。彗星は、牡羊座と三角座の中間に位置しているはずである。...が、残念ながらこの写真には写っていない。

拡大写真

彗星は動き回る天体なので、毎回毎回その位置ぎめが大変な作業である。星座によく習熟しておかないと、さっぱりの場合もある(今回がそれである)。いつも思うのが、星のパターンを自動認識してくれるソフトウェアがないかな、という願望である。それよりなにより、この写真に写っている星はいったい幾つあるのだろうか?明るい星はどのくらいで、暗い星は何個あるだろうか?そういう分析こそ、画像処理が得意ではないのか?

....と思ったのがきっかけである。デジタル画像処理について興味が湧いたので、さっそく自分でできる範囲までやってみようと思い立ったのである(無謀?)

画像処理の教科書を納屋から引っ張り出す

遥か以前、画像処理をやってみようと思い立った時期があっった。あの頃は、変光星に興味があり、定期的に観測して、その光度がどのように時間変化するのか興味があったのだ。フリーウェアがいくつか見つかったのだが、そのほとんどがWindows向けのものであり、LinuxからmacOSに軸足を移しつつあった私は、その試みを諦めたのである。吉田氏の彗星光度分析にあるような画像解析をやりたかったのだった。

そこで、自分でなんとかならないかと考え、いろいろな書籍を購入してみたのだが、そのほとんどが(やはり)windows向けの書籍であり、内容を読んでもさっぱりわからず、再び挫折してしまったのである。

この時購入したいくつかの書籍が埃をかぶって納屋に眠っているのを思い出し、もしかすると今なら多少はわかるようになっているかも、と根拠のない楽観に基づき、再再度挑戦することにしたのである。

doomとnanosaursの経験が生きているかも

この間チャレンジしたdoomnanosaursというゲームのコンパイルをやっている最中に、アプリケーションのコードがどのように書かれているのか、多少見聞きする機会があった。特にlinuxxdoomでは、C言語のプログラムのバグを手で修正したので、その構造や書き方について(少しではあるが)その慣習を知ることができた。

昔買った教科書のひとつが「画像処理プログラミング」というものである。以前はこのプログラムの意味がまったくわからず、コンパイルも通らず途方に暮れたのだが、今見ると、どうしてうまくいかなかったのかすぐにわかった。関数のコードリストはそのままコンパイルできないこともあるし、includeファイルの設定や、main()の記述などもどこかでやらねばならないのである。

doomの場合には、mainはほとんど形式的な利用になっていて、コードを見ると、すぐにD_DoomMain()を呼び出して実行的な処理はそちらでやるようになっていた(main関数はi_main.cにある)。

int main( int argc,  char **argv ) 
{ 
    myargc = argc; 
    myargv = argv; 
 
    D_DoomMain (); 

    return 0;
} 

さて、教科書に掲載されたプログラムであるが、まずはmain関数が載っているプログラムを探さないといけない。ずっとページを追いかけていくと、第1章の終わりに近いところにあった。ただ、ヘッダーファイルのリストが教科書のどこにも載っていないことがわかった。これではコンパイルが通らないわけである。

よくよく調べると、この教科書のコードはすべてソフトバンクのサーバーで公開されているそうで、まずはそちらをダウンロードしてみることにした。すると、そこにヘッダーファイルが含まれていたのであった。しかし、これだけではまだ不十分である。ダウンロードパッケージにはmakefileが含まれていなかったので、自分で書く必要があった。ファイルの依存関係を見出すのがなかなか骨折りであったが、mainを含むコードを読んで、必要な関数が含まれているコードを探し、それを個別にオブジェクトファイルにコンパイルできるかどうかチェックし、最後にリンクしてみたら、うまくいった!

できあがったコードを走らせると、小さなBMPファイルが生成されていた。教科書で解説していた市松模様の画像であった!

灰色の四角形に見えるが、拡大すると市松模様になっているBMP画像ファイル

これで、やっとこの教科書の読み方がわかった。画像処理の技術はひとつひとつが関数として分割して書かれており、見栄えが悪いヘッダーファイルは教科書では省略されてしまっている。コードの書き方だけを学んでね、という書き方である。

実際にコンパイルしてコンピュータで動かすには、ヘッダーファイルを書き足し、必要なメイン関数を書いて、関数ファイルとリンクさせる必要がある。主な作業はダウンロードパッケージに含まれたファイルを借用すれば省けるが、ファイルの依存性だけは自分でやらないとだめである。コードの中身を読み、教科書を読みながら、自分で分析してmakefileを書く。これらを突破し最後までは辿り着けば、実行ファイルが生成されて、BMP画像が作られるのである。

教科書をまねる

どっちみち自分でコードの構造を理解し、分析しないと動かないのであるから、思い切って同じ内容のプログラムを自分で書き直してみることにした。

まずは、画像データの先頭に組み込まれているデータ情報の読み込みをやってみることにした。画像ファイルをプログラムで読み込めなければ、なにも始まらない。

この教科書はC言語で画像処理を実装している。画像形式としては圧縮しないタイプのBMPが選ばれている。jpegとかpngとかをやりたいところであるが、これら圧縮された画像データの処理はかなり複雑であり、ライブラリを介してやらないと大変骨が折れる(ライブラリを使っても大変だと思うが)。画像処理プログラミングの最初の一歩としては(現在はあまり利用されなくなったとはいえ)BMPが適しているのかもしれない。BMP利用が減少した最大の理由がその大きなファイルサイズであったが、近年の技術革新により、BMPサイズの大きさがあまり気にならなくなってきたというのもある。Windows3.1やOS/2が利用されていた「大昔」においては4MBのフロッピーディスクに画像を何枚しまえるかどうかが問題だったが、今では4TBの外付けHDDや256GBのSDカードが1万円から2万円程度で買えるようになった。もはや画像はBMPのままでも問題ないのかもしれない(ネット利用でなければjpegは必要ないかも)。

BMPファイルを用意する

まずはBMPファイルを用意する必要がある。教科書のプログラムを使って生成した(上の)市松模様のファイルがその一つである。もう一つは、macOS Montereyのバージョンを示す画像をpngスクリーンショットした後、imagemagickをつかってBMPに変換したものである。

まずは、imagemagickをつかって、用意した画像のプロパティを確認しておこう。

% identify out.bmp

out.bmp BMP3 100x100 100x100+0+0 8-bit sRGB 30054B 0.010u 0:00.000

いろいろな情報が表示されたが、わかるのは「形式がBMP3、大きさが100x100ピクセル、ファイルサイズは30054byte」ということであろう。

pngからbmpに変換したものについても同じようにやってみると、

%identify monterey.bmp

monterey.bmp BMP 654x400 654x400+0+0 8-bit sRGB 789826B 0.010u 0:00.003

となった。ファイル形式は(BMP3ではなく)BMPとある。いろいろなバージョンがBMPにもあるのであろう。「大きさは654x400ピクセル、そしてファイルサイズが789826byte」であった。

その他の情報については、BMPについて詳しくなれば次第にわかるようになるであろう。

ファイルを読み込むコード

BMPはバイナリファイルであるから、まずはC言語でバイナリファイルを開いてから読み込むことになる。

K&Rを久しぶりに読み直してみると、「ファイルはポインタで定義される」とある。バッファの位置や、その現在の文字位置、加えて読み書きどちらなのかといった情報、ファイルの終端(EOF)が来たのか来てないのか、エラーのあるなしなど、ファイルに関する様々なプロパティが構造体の形でまとめられているのだという。そしてその情報の先頭が置かれているアドレスがファイルポインタである。したがって、まずはファイルポインタを宣言し、そのインスタンスを生成する。ちなみに、このファイル構造体の定義は、stdio.hに書かれているという(見たことはないが)。

ということで、C言語でファイルの読み出しを行う第一歩は次のようになる。

#include <stdio.h>

int main(){
    FILE *fp;
    return 0;
}

この状態ですでにコンパイルでき、実行できる(なにも起きないが)。

% gcc openFile.c

次に、BMPデータファイルの情報を、このファイルポインタ構造体に結びつける。このために必要なのが、stdioライブラリに収められているfopenである。ファイル名とファイルアクセスモード(読み込みか書き込みかなど)を指定して実行する。

FILE *fp;
fp = fopen("out.bmp","rb");

rは読み込み(read)、bはバイナリ(binary)形式、を意味する。BMPはバイナリファイルなのでbが必要である。

「ファイルオープン」つまりファイルとファイルポインタの結びつきが失敗すると、ポインタfpにはNULLが結びつく。それ以外の場合は成功と見做される。K&Rでは、ファイルオープンの成功失敗の検査も込みした例文が書き下されているので、それを採用してみた。

#include <stdio.h>

int main(){
    FILE *fp;
    if( (fp = fopen("out.dat","rb")) == NULL){
        printf("File not found. Failsure.\n");
        return 1;
    }else{
        printf("File found. Success.\n");
   }
    return 0;
}

最後にファイルの中身にアクセスしてみる。といっても最初2バイトのみとする。これは、BMPデータファイルの最初の2バイトには、識別のための"B"と"M"が必ず書き込まれているからである(そういう仕様になっていて「マジックナンバー(魔法数)」と呼ばれる)。

ASCII文字コード

C言語は米国で開発されたプログラミング言語であるため、扱う文字は米国系のアルファベットと関連する記号である。ドイツ語だったり、北欧系、東欧系の諸国で利用されている文字や記号は採用されてない。したがって、128通りの区別があれば、米国系の英文に限ってならコンピュータで表現できる(らしい)。ということで、文字を表す表現としては27=128の情報をもつASCIIコードが採用された。たとえば、Aは整数65、Bは整数66....によって識別する。ちなみに、「1」は49, 「2」は50、...といった具合に「数字」についても「文字」として認識され、ASCIIコードが割り振られている。

BMPの最初の2バイトに書き込まれているBMという2つの文字は、ASCIIでは66と77に相当する(16進数では0x42と0x4dである)。

現代のコンピューターアーキテクチャーでは、1byte=8bitであるから、28=256文字を処理することができる。ASCIIコードの最大値は127=27-1、つまり7ビットであるから、大幅に「余り」が出てしまう。この余の部分に各国の言語特有の記号を埋め込んだ新しいASCIIコードというものが制定されてはいるようだが、今考えるべきはオリジナルのASCIIコード7ビット分、つまり0.5バイトの情報量である。しかし、0.5バイトというのは中途半端な数字である。ファイルに文字を書き込む時、実際はどのようにやっているのか疑問に思うのである。

putc()関数

ファイルポインタfpで定義されたファイルの中へ一文字つまり1バイトのASCIIデータを書き込む関数はputc()である。

似たような関数にfputc()があるため、どちらを使ってよいか初心者は非常に困惑する。色々読んで調べてみたが、結局はどちらでもよいということになった(少なくとも初心者目線では)。教科書ではfputcが採用されているが、あえてputcを使って書き換えてみることにした。どちらでも同じ、というならば、ちゃんと動くだろう。

次のようなコードを書いて、ファイルに1文字書き込んでみた。果たして0.5byteになるのか、それとも1byteになるのか?

#include <stdio.h>

int main(){
  int c;
  FILE *fp;
  fp = fopen("sample.dat","wb");
  c = putc('B',fp);
  fclose(fp);
  return 0;
}

wbというのはバイナリ形式での(新規ファイルへの)書き込みモード、という意味である。書き込む先のファイルは「sample.dat」と名付けた。1文字を表すときは一重引用符で囲む。つまり'B'とする。二重引用符で囲む、つまり"B"とすると、C言語では文字列扱いになってしまうので、終端記号'\0'が自動的に付加されて2バイトになってしまう。putcが書き込めるのは、あくまで「一文字」だけであり、文字列は別の関数を使わねばならない。

さて出来上がったファイルを見ると、

% ls -l sample.dat

-rw-r--r--  1 user  group  1  3 26 12:50 sample.dat

% xxd sample.dat

00000000: 42                                       B

となっていて、1バイト、文字Bひとつだけが書き込まれたファイルになっている。では2文字書き込んだらどうなるか?

#include <stdio.h>

int main(){
  int c;
  FILE *fp;
  fp = fopen("sample.dat","wb");
  c = putc('B',fp);
  c = putc('M',fp);
  fclose(fp);
  return 0;
}

結果は、

% xxd sample.dat

00000000: 424d                                     BM

% ls -l sample.dat

-rw-r--r--  1 user  group  2  3 26 12:53 sample.dat

たしかに2バイトである。

この実験により明らかになったのは、ASCII文字コードは7bit仕様(128文字)であるが、現代のコンピュータアーキテクチャでは、8bit(256文字)=1byteとして扱っているということである。かなり無駄をしているように見えるが、無駄が無駄にならないくらい技術が進歩したのであろう。

ちなみに、「無駄」な情報といえば、生体アミノ酸を思い出す。生体アミノ酸は20種類あるそうだが、塩基配列RNAの遺伝子情報)では4進数表現(G,C,A,U)の3ビット(例えばGCCはアラニンを表す)で表現されているそうである。43=64なので、20種類のアミノ酸を表すには「多すぎる」のだが、この冗長さが進化論的に有利に働いたのであろう。これは、コロナウイルスが変異しやすい理由などと若干関係しているのかもしれないが、よくしらないのでこれ以上の深入はやめておこう.....

さて、putc()はint型なので、返値は整数で与えられる。前回確認したようにC言語の整数表現は4byte、つまり32ビット表現がデフォルトである。putcが返す整数値は、書き込んだASCIIコードになっているらしいことが実験をしてみるとわかる。しかし、ASCIIコードは0.5byteで十分だから、4byteの整数表現では「ゆるゆる」ではないだろうか? 

まずは、文字がASCIIコード(つまり0-127の整数)と等価であることを確認しておこう。putc('B',fp)の代わりに、BのASCIIコードである66を使っても同じ結果になる。つまり、

c = putc(66, fp);

c = putc('B',fp);

は同じ結果となるのである。K&Rにも

int putc( int, FILE *)

と書いてある。返値cを印字させると66と出力される。予想通り、書き込んだASCIIコードが返値になっているのである。

ここで実験である。ASCIIコードをはるかに超える大きな数、たとえば578を使ってファイルに書き込んでみたらどうなるだろうか?つまり、

c = putc(578, fp);

である。コンパイルはうまく通るので、これは間違いではないらしい。書き込んだファイルを覗いてみると、Bが書き込まれており、さらにcを出力すると66となっているではないか。つまり578は66なのである(?)どういうことかというと、現代のマシンではASCIIコードが8ビット(つまり256種)に拡張されており、256を超える大きな数は「周期的に」再定義されているらしいのである。つまり、

578 % 256 = 66

として元に戻ってくるように見えるのだ。ちなみに、putc(834,fp)もputc(21314,fp)も、Bを書き込む命令putc(66,fp)と等価であることを確認した。どれも余りが66となる整数である。

BMPの最初の2byteのデータを読み込む

ということで、bmp画像ファイルの最初の2バイトに書き込んであるという「魔法数」を読み取り、それがBMになっていることを確認してみよう。プログラムする前に、先ほど使ったxxdでBMPファイルをダンプしてみた。

% xxd out.bmp

00000000: 424d 6675 0000 0000 0000 3600 0000 2800  BMfu......6...(.
00000010: 0000 6400 0000 6400 0000 0100 1800 0000  ..d...d.........
00000020: 0000 0000 0000 2c01 0000 2c01 0000 0000  ......,...,.....
00000030: 0000 0000 0000 0000 00ff ffff 0000 00ff  ................
00000040: ffff 0000 00ff ffff 0000 00ff ffff 0000  ................
00000050: 00ff ffff 0000 00ff ffff 0000 00ff ffff  ................
00000060: 0000 00ff ffff 0000 00ff ffff 0000 00ff  ................
00000070: ffff 0000 00ff ffff 0000 00ff ffff 0000  ................
00000080: 00ff ffff 0000 00ff ffff 0000 00ff ffff  ................
00000090: 0000 00ff ffff 0000 00ff ffff 0000 00ff  ................
000000a0: ffff 0000 00ff ffff 0000 00ff ffff 0000  ................
000000b0: 00ff ffff 0000 00ff ffff 0000 00ff ffff  ................
000000c0: 0000 00ff ffff 0000 00ff ffff 0000 00ff  ................
000000d0: ffff 0000 00ff ffff 0000 00ff ffff 0000  ................
000000e0: 00ff ffff 0000 00ff ffff 0000 00ff ffff  ................
000000f0: 0000 00ff ffff 0000 00ff ffff 0000 00ff  ................
00000100: ffff 0000 00ff ffff 0000 00ff ffff 0000  ................
(以下省略)

確かに最初の2byteがBMになっている。しばらくはヘッダー情報が続くが、やがて0000と00ffの繰り返しになっていて、これが市松模様に対応していることがわかる。

では読み取ってみよう。

#include <stdio.h>

int main(){
  int c;
  FILE *fp;
  if( (fp = fopen("out.bmp","rb")) == NULL) {
     printf("File not found.\n");
     return 1;
  }
  printf("File found.\n");
  c = getc(fp);
  printf("%c %d %x\n", c, c, c);
  c = getc(fp);
  printf("%c %d %x\n", c, c, c);
  fclose(fp);
  return 0;
}

コンパイルして実行すると、次の結果を得る。

% a.out 

File found.
B 66 42
M 77 4d

out.bmpBMP画像データであることが確認できた!

BMPファイルの初めの14byteには、このような識別データがいくつか書き込まれているとBMPの仕様書には書いてある。2つ目のデータは4byteで構成されるファイルサイズのデータである。3つ目と4つ目は2byteずつ確保されているそうだが、零値が充てあられる規約となっているという。5つ目が4byteの情報で、画像データがどこから始まるか書いてあるそうである。

これらのデータは、BMという2バイトの識別データを読み込んだ要領と同様に読み取っていけばよい。

まずは、画像ファイルのヘッダー部分の読み取りができるようになった。この要領で、画像データ本体も読み取ってみたいと思うが、それは次回のテーマである。