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

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

画像処理にチャレンジ(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である。これはマーカーではない。画像データそのものである。