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

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

画像処理にチャレンジ(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);

...(省略)

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