JPEG画像の入出力

作成:

今回も前回に引き続き、オープンソースのライブラリを利用した入出力方法について説明する。

今回説明するのは JPEG 画像の入出力である。 JPEG はそれを策定した団体である Joint Photographic Experts Group の頭字語(アクロニム)になっている。 JPEG といえば画像フォーマットを指す言葉だと思われているが、厳密には標準のファイルフォーマットは決められておらず、 JFIF ( JPEG File Interchange Format ) が事実上のJPEGの標準のフォーマットとなっている。 また、デジタルカメラなどでは撮影時の各種情報を格納できるように、 JFIF を拡張した EXIF ( EXchangeable Image File Format ) が使用される。 JFIF と EXIF は画像の付帯情報の格納方法に違いがあるだけであり、純粋に画像データとして見た場合は同じだと考えて良い。

自然画像の特徴を利用し、不可逆圧縮を行うことで、 可逆圧縮では実現できない高い圧縮率と、画質を維持できる画像フォーマットである。 おそらく最も利用されいてる画像フォーマットだろう。

画像情報は 8 x 8 のマクロブロックに分割され、それを離散コサイン変換をかけることで周波数情報に変換する。 画像データを周波数成分に分離すると、低周波成分に多くの情報が集まる性質を利用し、 高周波情報の量子化レベルを下げることで不可逆にデータ量を減らしている。 ただし、この圧縮方法は万能ではなく、苦手な画像や圧縮率を高くした場合に、 ブロックノイズやモスキートノイズと言われるノイズが現れることがある。

また、色空間としては主に YCbCr (もしくは YUV )、すなわち、輝度色差方式が用いられる。 輝度式差方式とは、色の明るさの成分と、それ以外の色成分に分離した表現方式である。 通常コンピュータの世界で利用される RGB と表現の幅は同じだが、 人間の視覚は輝度の変化の方が、色の変化よりも敏感であることを利用し、 色差信号を間引くというデータ削減手法も利用できる。 規格上は色空間としては YCbCr 以外に RGB や CMYK も利用可能である。

非常にざっくりとした説明だが、全部自前で実装をするのが大変そうだということは理解していただけるだろう。 今回はこれら複雑な処理を実装している libjpeg を利用して読み書きを行う。

libjpeg の利用

libjpeg は Independent JPEG Group が開発しているオープンソースのライブラリである。 libjpeg のライセンスもオープンソースとしてはゆるい部類のライセンスで公開されており、様々な場所で利用されている。

libjpeg のライセンスはライブラリに同梱されている README を参照。 前回に引き続き、私自身法律の専門家ではないため、当サイトではライセンスについては厳密には解説しない。 当該ライセンスを参照の上、自己の責任の上で利用してほしい。

JPEG は離散コサイン変換という演算能力と精度が求められる計算を行うが、 libjpeg では高精度だが遅い整数演算、低精度だが速い整数演算、 浮動小数点演算から演算アルゴリズムを選択することもできる。 デフォルトでは高精度整数演算が選択される。

特有の癖があり、その癖を理解したうえで利用する必要があるが、 多くの機能はライブラリ自体に手を入れなくてもカスタマイズ可能であり、 必要があればライブラリのソースコードも入手可能なのでそちらをカスタマイズいて利用することもできる。

外部のライブラリを利用するためそのライブラリは何らかの方法で入手する必要がある。 Ubuntu にて libjpeg の開発環境をインストールするには以下のようにする。

$ sudo apt-get install libjpeg-dev

JPEG画像入出力

JPEG 画像の読み書きの方法を紹介する。 これまでと同様、ソースコード全体は GitHub にて公開している。 JPEG の入出力を記述しているのは jpeg.c である。

libjpeg の関数群を使用するためには、 jpeglib.h を include する必要がある。

#include <jpeglib.h>

このヘッダ及び、このヘッダから include されているヘッダによって、 libjpeg の関数群のプロトタイプ宣言、及び、使用する構造体等の定義が行われている。

実行バイナリを作成する際は当然 libjpeg とのリンクが必要になる。 例えば、gccでコンパイルの際は、以下のように-ljpegオプションが必要となる。

$ gcc hoge.c -o hoge -ljpeg

JPEG画像の読み込み

以下にJPEG画像を読み込むコードを示す。 全体については jpeg.c を参照

image_t *read_jpeg_stream(FILE *fp) {
  result_t result = FAILURE;
  uint32_t x, y;
  struct jpeg_decompress_struct jpegd;
  my_error_mgr myerr;
  image_t *img = NULL;
  JSAMPROW buffer = NULL;
  JSAMPROW row;
  int stride;
  jpegd.err = jpeg_std_error(&myerr.jerr);
  myerr.jerr.error_exit = error_exit;
  if (setjmp(myerr.jmpbuf)) {
    goto error;
  }
  jpeg_create_decompress(&jpegd);
  jpeg_stdio_src(&jpegd, fp);
  if (jpeg_read_header(&jpegd, TRUE) != JPEG_HEADER_OK) {
    goto error;
  }
  jpeg_start_decompress(&jpegd);
  if (jpegd.out_color_space != JCS_RGB) {
    goto error;
  }
  stride = sizeof(JSAMPLE) * jpegd.output_width * jpegd.output_components;
  if ((buffer = calloc(stride, 1)) == NULL) {
    goto error;
  }
  if ((img = allocate_image(jpegd.output_width, jpegd.output_height,
                            COLOR_TYPE_RGB)) == NULL) {
    goto error;
  }
  for (y = 0; y < jpegd.output_height; y++) {
    jpeg_read_scanlines(&jpegd, &buffer, 1);
    row = buffer;
    for (x = 0; x < jpegd.output_width; x++) {
      img->map[y][x].c.r = *row++;
      img->map[y][x].c.g = *row++;
      img->map[y][x].c.b = *row++;
      img->map[y][x].c.a = 0xff;
    }
  }
  jpeg_finish_decompress(&jpegd);
  result = SUCCESS;
  error:
  jpeg_destroy_decompress(&jpegd);
  free(buffer);
  if (result != SUCCESS) {
    free_image(img);
    img = NULL;
  }
  return img;
}

エラー処理のオーバーライド

本筋の画像読み込み処理から外れてしまうが、 libjpeg のエラー処理をオーバーライドする手続きを最初に行っている。 オーバーライドを行わず、デフォルトのエラー処理を利用する場合は以下のように記述する。

  struct jpeg_decompress_struct jpegd;
  struct jpeg_error_mgr jerr;
  jpegd.err = jpeg_std_error(&jerr);

純粋に libjpeg を使うというだけならこれでも十分なのだが、 libjpeg のデフォルトでは、致命的エラーが発生した場合の処理は以下のような実装になっている。 (METHODDEF(noreturn_t)は define や typedef をたどるとstatic voidという意味になる)

METHODDEF(noreturn_t)
error_exit (j_common_ptr cinfo)
{
  /* Always display the message */
  (*cinfo->err->output_message) (cinfo);

  /* Let the memory manager delete any temp files before we die */
  jpeg_destroy(cinfo);

  exit(EXIT_FAILURE);
}

最終的にコールされるのはexit(EXIT_FAILURE); すなわち、実行プログラムの強制終了である。 ほとんどの場合においてこのエラー処理ではまずい。 そのため、 libjpeg をきちんと使いこなすためにはエラー処理のオーバーライドは不可欠である。

デフォルトのエラー処理がexit()である、ということは、 エラーを順当にコールスタックに伝搬させてコール元に戻る、 というC言語的なエラー処理は実装されていないということである。 また、できることは、上記error_exit関数を自前の関数に置き換えることだけである。 この前提で、エラー発生時にコール元に戻るためsetjmplongjmpを使用している。

まず、以下のように、本来のエラー処理情報を登録する構造体であるstruct jpeg_error_mgrを拡張し、 setjmpで保存する実行コンテキストを保持できる構造体を用意する。

typedef struct my_error_mgr {
  struct jpeg_error_mgr jerr;
  jmp_buf jmpbuf;
} my_error_mgr;

初期化処理では以下のように、 libjpeg の関数にはmy_error_mgr内のstruct jpeg_error_mgrのポインタを渡し、 デフォルトの初期化を行う。 その後、error_exitをローカルで定義した関数で上書きする。 また、jmpbufに実行コンテキストの保存を行う。

  struct jpeg_decompress_struct jpegd;
  my_error_mgr myerr;
  jpegd.err = jpeg_std_error(&myerr.jerr);
  myerr.jerr.error_exit = error_exit;
  if (setjmp(myerr.jmpbuf)) {
    goto error;
  }

そして、自前のエラー処理関数では以下のようにexit()の代わりに、 longjmp()をコールするようにする。 longjmp()を行うための実行コンテキストは引数のcinfoからたどるようにする。

static void error_exit(j_common_ptr cinfo) {
  my_error_mgr *err = (my_error_mgr *) cinfo->err;
  (*cinfo->err->output_message)(cinfo);
  longjmp(err->jmpbuf, 1);
}

この関数の引数j_common_ptrだが、 これは libjpeg で 読み込みで利用されるjpeg_decompress_structと、 書き込みで利用されるjpeg_compress_structの先頭部分は共通のメンバ変数が配置されており、 それらに共通してアクセスできるポインタ型になっている。 jpeg_error_mgr型のポインタであるerrもここからアクセスできる。 また、errに登録したのはmy_error_mgrjerrへのポインタであるが、 構造体の先頭メンバーのポインタ値と、その構造体のポインタ値は同一なので、 my_error_mgr型のポインタにキャストすれば、jmpbufにアクセスできるという寸法である。

この辺はポインタと型解釈の考え方をしっかり把握していないとうまく理解できないと思うが、 あまり詳細に説明し始めるときりがないので、ここでの説明は以上としておく。

ここで説明したエラー処理のオーバーライド方法については、 libjpeg の example.c で使用、説明されている方法なので、そちらも参照すると良いだろう。

構造体の初期化

ここからが画像の読み込みの本来の処理になる。

  jpeg_create_decompress(&jpegd);
  jpeg_stdio_src(&jpegd, fp);

ここで、読み込み用構造体を初期化し、読み込みに使用するファイルストリームの設定を行う。 jpeg_create_decompressでは構造体の初期化が行われるが errメンバ変数については、保持されるので先に設定しおいてよい。

ヘッダの読み込みと復号開始

次の処理でヘッダの読み込みと復号の開始を行う。

  if (jpeg_read_header(&jpegd, TRUE) != JPEG_HEADER_OK) {
    goto error;
  }
  jpeg_start_decompress(&jpegd);

jpeg_read_headerをコールした後であれば、画像の基本的な情報である幅や高さが読み出せそうな気がするが、 それらの情報はjpeg_start_decompressをコールした後でなければ正常に読み出せないので注意。

画像情報の読み出し

画像データの読み出しの準備を行う。

  if (jpegd.out_color_space != JCS_RGB) {
    goto error;
  }

まず、pegd.out_color_spaceの値をチェックしている。 この値は復号後の画像データの色表現の種別を表している。 JPEG 画像の色空間が、YCbCrおよびRGBの場合、復号結果はJSC_RGBに変換される。 あまり一般的ではないが、色空間としては他に CMYK の場合があり、この場合、CNYKの値が戻される。 またグレースケールの場合もある。 グレースケールについては対応してもよいと思うが、 CMYK の場合は安直な変換はできないこともあり、エラーとした。

  stride = sizeof(JSAMPLE) * jpegd.output_width * jpegd.output_components;
  if ((buffer = calloc(stride, 1)) == NULL) {
    goto error;
  }

次に行っているのが、作業バッファの確保である。 libjpeg では画像データの読み出しを最低1行単位で行うため、 作業領域として1行分のデータが入るバッファが必要となる。 幅のピクセルサイズと、1色あたりのデータサイズで1行分のデータとなる。 また、この時点で画像サイズがわかっているので、格納先の構造体を用意しておく。

画像データの読み出し

画像データの読み出しは以下のように行う。

  for (y = 0; y < jpegd.output_height; y++) {
    jpeg_read_scanlines(&jpegd, &buffer, 1);
    row = buffer;
    for (x = 0; x < jpegd.output_width; x++) {
      img->map[y][x].c.r = *row++;
      img->map[y][x].c.g = *row++;
      img->map[y][x].c.b = *row++;
      img->map[y][x].c.a = 0xff;
    }
  }

jpeg_read_scanlinesは複数行のデータを纏めて読みだすことができる関数で、 第三引数にバッファの残行数を設定すると、内部処理でまとめて書き出すことができる行数分を書き出してくれる。 そのため第二引数は行ポインタ配列になっている。 また、その場合は、jpegd.output_scanlineの値で、どこまで出力されたかを確認しながら処理をすすめる。

ここでは複数行の出力は行わず、1行ずつ読み出し、読み出し先に詰め込むという処理にしている。

終了処理

最後に後始末を行う。

  jpeg_finish_decompress(&jpegd);
  jpeg_destroy_decompress(&jpegd);
  free(buffer);

復号処理を完了させ、復号構造体を破棄、また、作業領域として確保したバッファを開放する。

以上で読み込み処理は完了となる。 エラー処理のオーバーライドがやや面倒だが、 CMYK の場合を無視したり、画像構造体間の変換が無いため、前回の PNG よりも更に簡素になっていると思う。

JPEG画像の書き出し

以下にJPEG画像を書き出すコードを示す。 全体については jpeg.c を参照

result_t write_jpeg_stream(FILE *fp, image_t *img) {
  result_t result = FAILURE;
  int x, y;
  struct jpeg_compress_struct jpegc;
  my_error_mgr myerr;
  image_t *to_free = NULL;
  JSAMPROW buffer = NULL;
  JSAMPROW row;
  if (img == NULL) {
    return FAILURE;
  }
  if ((buffer = malloc(sizeof(JSAMPLE) * 3 * img->width)) == NULL) {
    return FAILURE;
  }
  if (img->color_type != COLOR_TYPE_RGB) {
    // 画像形式がRGBでない場合はRGBに変換して出力
    to_free = clone_image(img);
    img = image_to_rgb(to_free);
  }
  jpegc.err = jpeg_std_error(&myerr.jerr);
  myerr.jerr.error_exit = error_exit;
  if (setjmp(myerr.jmpbuf)) {
    goto error;
  }
  jpeg_create_compress(&jpegc);
  jpeg_stdio_dest(&jpegc, fp);
  jpegc.image_width = img->width;
  jpegc.image_height = img->height;
  jpegc.input_components = 3;
  jpegc.in_color_space = JCS_RGB;
  jpeg_set_defaults(&jpegc);
  jpeg_set_quality(&jpegc, 75, TRUE);
  jpeg_start_compress(&jpegc, TRUE);
  for (y = 0; y < img->height; y++) {
    row = buffer;
    for (x = 0; x < img->width; x++) {
      *row++ = img->map[y][x].c.r;
      *row++ = img->map[y][x].c.g;
      *row++ = img->map[y][x].c.b;
    }
    jpeg_write_scanlines(&jpegc, &buffer, 1);
  }
  jpeg_finish_compress(&jpegc);
  result = SUCCESS;
  error:
  jpeg_destroy_compress(&jpegc);
  free(buffer);
  free_image(to_free);
  return result;
}

初期化処理

エラー処理のオーバーライドについては全く同じなので割愛。 書き出し処理では予めサイズがわかっているので作業バッファの確保も一番最初に行っている。 また、JPEGで出力する場合は基本的にフルカラーかつアルファチャンネルなしである必要があるので、 元の画像が RGB でない場合は RGB に変換してから出力処理を行っている。

  jpeg_create_compress(&jpegc);
  jpeg_stdio_dest(&jpegc, fp);

読み出し処理とほぼ同じだが、 書き出し構造体の初期化と、書き出し先のファイルストリームの登録を行う。

画像情報の設定

書き出す画像の情報を構造体に代入する。

  jpegc.image_width = img->width;
  jpegc.image_height = img->height;
  jpegc.input_components = 3;
  jpegc.in_color_space = JCS_RGB;

input_componentsは1色を表現するバイト数。 in_color_spaceは色空間を指定する。

  jpeg_set_defaults(&jpegc);
  jpeg_set_quality(&jpegc, 75, TRUE);

libjpeg では、圧縮に際し、様々なパラメータを設定できるが、 通常はあまり変更する必要のないパラメータなどもあるため jpeg_set_defaultsで、デフォルト値に初期化したうえで、 変更したいパラメータを個別で変更するようにする。 デフォルト値を設定する上で、入力のカラースペースが必要な箇所があるため、 この関数をコールする前に入力のカラースペースは設定しておく必要がある。

jpeg_set_qualityでは圧縮のクオリティを設定する。 値は 0 ~ 100の範囲で指定する。 jpeg_set_defaultsで、クォリティは 75 に初期化されるため、 75 を指定する場合は特にコールする必要はないが、 このパラメータは変更する可能性が高いためあえてコールするようにしている。

画像データの出力

ライブラリへ画像データを入力し、ファイルへ出力する。

  jpeg_start_compress(&jpegc, TRUE);
  for (y = 0; y < img->height; y++) {
    row = buffer;
    for (x = 0; x < img->width; x++) {
      *row++ = img->map[y][x].c.r;
      *row++ = img->map[y][x].c.g;
      *row++ = img->map[y][x].c.b;
    }
    jpeg_write_scanlines(&jpegc, &buffer, 1);
  }

処理手順は読み込みと逆で、jpeg_start_compressをコールした後、 jpeg_write_scanlinesで入力データを渡していく、 読み込みと同様に、jpeg_write_scanlinesでは複数行を渡すことができるが、 ここでは1行ずつ処理を行っている。

終了処理

最後に後始末を行う。

  jpeg_finish_compress(&jpegc);
  jpeg_destroy_compress(&jpegc);
  free(buffer);
  free_image(to_free);

圧縮処理を完了させ、圧縮構造体を破棄、また、作業領域として確保したバッファを開放する。 また、元画像が RGB でなかった場合に変換をかけた画像データがあればそれも開放しておく。

以上で、JPEG 画像の出力処理は完了である。

libjpeg を利用した JPEG 画像の入出力の説明は以上となる。 PNG と同様に比較的簡単な記述で JPEG 画像の入出力が実現できる。

オープンソースのライブラリは、広く利用されていることでよくデバッグされており、 様々なシーンでの利用ノウハウも共有されている。 何よりソースコードそのものを自由に見ることができるため勉強にはうってつけだろう。 プロプライエタリな開発では特に、ライセンスに気をつける必要はあるが、 そこさえクリアすれば非常に便利なので積極的に利用したいものである。