jpeglibを使ってサムネイル画像を高速に作る-その1 jpeglibの使い方編

Tsukasa OISHI

Rubyを使っていてサムネイル画像を作りたいとき、RMagickを使うのが一般的だと思いますが、RMagickは遅い上にメモリを消費するし、メモリリークもやってくれるという超高性能野郎です。コマンドとしてimagemagickをコールしたほうが全然いいくらいだし、重い処理をするときはそうしたりしています(カッコ悪いけど)。
実際、imagemagickほどの高機能が必要なことというのはweb開発ではあまりなくて、結局みんなサムネイル画像が欲しいだけなのです。
なので、サムネイル画像だけを取得するためのシンプル軽量高速なツールを作ってみたくなりました。

高速に動かすとなると、CかC++で実装ということになると思います。jpeglibというライブラリがあって、こいつを使うのはC++が都合がよさそうです。C++は大昔に挫折して以来触っていないのですが、まあ、C言語っぽく書いても問題ないでしょう。ゆくゆくはきれいなC++のコードにすればいいというスタンスでいきます。

というわけで、これからヒマを見つけては高速軽量シンプルなサムネイル画像取得ツールspeedpetalをつくっていきます。仕事が忙しいときほどこういうことしたくなるのはどうしてなのでしょうか。

jpegは圧縮されているので、サムネイル画像を作るためにはまず展開をして、サムネイルを作ったらそれを圧縮しないといけません。
jpeglibで展開処理を行うには、jpeg_decompress_struct構造体を使います。

    struct jpeg_decompress_struct in_info;
    struct jpeg_error_mgr jpeg_error;
    in_info.err = jpeg_std_error(&jpeg_error);

jpeg_std_error関数でエラー処理の設定をします。この状態だと標準エラーに出力して処理が終了してしまうので、ちゃんとしたツールにするときは自前のエラー処理を作る必要があります。今回はとりあえずこのまま。

    jpeg_create_decompress(&in_info);
    jpeg_stdio_src(&in_info, infile);
    jpeg_read_header(&in_info, TRUE);
    jpeg_start_decompress(&in_info);

jpeg_create_decompress()関数で展開処理のための初期処理を行います。これと対になるのがjpeg_finish_decompress()関数で、最後に呼んであげないといけません。
jpeg_stdio_src()関数で、処理を行う画像のファイルを指定します。infileはオープン済みの画像のFILEポインタです。当然ですが、画像はバイナリで開きます。
jpeg_read_header()関数で画像のヘッダ情報を読み込みます。jpeg_start_decompress()関数で展開処理を開始します。

それから展開したデータを読み込むためのメモリを確保します。

    JSAMPARRAY buffer = (JSAMPARRAY)malloc(sizeof(JSAMPROW)*in_info.output_height);
    for(int i = 0; i < in_info.output_height; ++i){
        buffer[i] = (JSAMPROW)calloc(sizeof(JSAMPLE), in_info.output_width * in_info.output_components);
    }

jpeglibでは、画像のひとつのポイントにRGBそれぞれの画像データをもっています。そのひとつひとつがJSAMPLE型のデータです。画像の一列のラインを、JSAMPLE型の配列で表します。3ピクセルの画像なら、RGBRGBRGBと、JSAMPLE型の配列の大きさは9になります。
この一列の配列のポインタがJSAMPROW型で表されます。JSAMPROW型の配列が、画像全体を表すことになります。このJSAMPROW型の配列のポインタがJSAMPARRAY型です。
output_componentsはカラーなら3、グレースケールなら1になります。

そして確保したメモリに展開したデータを読み込みます。

    while(in_info.output_scanline < in_info.output_height){
        jpeg_read_scanlines(&in_info, buffer + in_info.output_scanline, in_info.output_height - in_info.output_scanline);
    }

jpeg_read_scanlines()関数は、数ラインのデータを読み込みます。いくつ読み込まれるかはわかりません。output_scanlineで読み込んだライン数がわかるので、画像の高さ分を読み込むまでループで回します。jpeg_read_scanlines()関数の第二引数はコピー先のポインタ、第三引数は、残っているライン数を示します。

展開は以上です。圧縮も似たような流れになります。

    struct jpeg_compress_struct out_info;
    out_info.err = jpeg_std_error(&jpeg_error);
    jpeg_create_compress(&out_info);
    jpeg_stdio_dest(&out_info, outfile);

圧縮に使うのはjpeg_compress_struct構造体です。jpeg_stdio_dest()関数で出力先のファイルを指定します。

次に画像の情報をセットします。

    out_info.image_width = image_width;
    out_info.image_height = image_height;
    out_info.input_components = 3;
    out_info.in_color_space = JCS_RGB;
    jpeg_set_defaults(&out_info);

input_componentsはRGBなら3、グレースケールなら1を指定します。in_color_spaceはJ_COLOR_SPACE型の値で、RGBならJCS_RGB、グレースケールならJCS_GRAYSCALEを指定します。
jpeg_set_defaults()関数で他の情報をセットします。

    jpeg_start_compress(&out_info, TRUE);
    jpeg_write_scanlines(&out_info, img, image_height);

圧縮の開始とメモリからの書き込みです。読み込みに比べるとワンラインで済むので楽チンです。第三引数は書き込むライン数を指定します。

というわけ簡単なサムネイルを生成するツールを作ってみました。ベンチマークの結果は以下のとおり。

             user     system      total        real
speedpetal  0.240000   2.110000  91.750000 ( 91.749929)
avg      0.000024   0.000211   0.009175 (  0.009175)

10000万回処理をした結果です。なかなかいい値。ちなみにRMagickで同じ処理をしてみると(時間がかかるのでこちらは100回にしました)、

             user     system      total        real
rmagick  0.020000   0.420000  22.430000 ( 22.449986)
avg      0.000200   0.004200   0.224300 (  0.224500)

24倍くらい遅いです。全然違いますね。

ちなみに今回のツールのソースは以下になります。まだサムネイルの作成は左上から切り取っているだけなので、右下まわりの情報が失われてしまいます。次はニアレストネイバー法とバイキュービック法を試してみようと思います。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <jpeglib.h>

// リサイズの比率を取得
// target_size : 目標のサイズ
// width , height : 縦横のサイズ
double get_scale_factor(int target_size, int width, int height) {
    if (width > height) {
        return (double)target_size / width;
    } else {
        return (double)target_size / height;
    }
}

// サムネイル作成
// request_size : 目標のサイズ
// in_file_name, out_file_name 入出力ファイル名
int convert(int request_size, const char *in_file_name, const char *out_file_name) {
    //
    // 展開処理開始
    //
    FILE *infile;
    if((infile = fopen(in_file_name, "rb")) == NULL){
        fprintf(stderr, "ファイルが開けません: %s\n", in_file_name);
        return -1;
    }

    struct jpeg_decompress_struct in_info;
    struct jpeg_error_mgr jpeg_error;
    in_info.err = jpeg_std_error(&jpeg_error);

    jpeg_create_decompress(&in_info);
    jpeg_stdio_src(&in_info, infile);
    jpeg_read_header(&in_info, TRUE);

    // 縮小サイズで高速に展開するための準備
    jpeg_calc_output_dimensions(&in_info);
    in_info.scale_denom = (unsigned int)(1 / get_scale_factor(request_size, in_info.output_width, in_info.output_height));
    in_info.two_pass_quantize = FALSE;
    in_info.dither_mode = JDITHER_ORDERED;
    if (! in_info.quantize_colors) {
        in_info.desired_number_of_colors = 216;
    }
    in_info.dct_method = JDCT_FASTEST;
    in_info.do_fancy_upsampling = FALSE;

    int components = in_info.output_components;
    J_COLOR_SPACE color_space = components == 3 ? JCS_RGB : JCS_GRAYSCALE;

    // サムネイルのサイズを計算
    int image_width = 0;
    int image_height = 0;
    double scale_factor = get_scale_factor(request_size, in_info.output_width, in_info.output_height);
    if (in_info.output_width > in_info.output_height) {
        image_width = request_size;
        image_height = (int)(in_info.output_height * scale_factor);
    } else {
        image_height = request_size;
        image_width = (int)(in_info.output_width * scale_factor);
    }

    jpeg_start_decompress(&in_info);

    // 展開用メモリの確保
    JSAMPARRAY buffer = (JSAMPARRAY)malloc(sizeof(JSAMPROW)*in_info.output_height);
    for(int i = 0; i < in_info.output_height; ++i){
        buffer[i] = (JSAMPROW)calloc(sizeof(JSAMPLE), in_info.output_width * in_info.output_components);
    }

    // 画像のスキャン
    while(in_info.output_scanline < in_info.output_height){
        jpeg_read_scanlines(&in_info, buffer + in_info.output_scanline, in_info.output_height - in_info.output_scanline);
    }

    // サムネイル画像用メモリの確保と展開した画像のコピー
    // ニアレストネイバーとバイキュービックをあとで試す
    JSAMPARRAY img = (JSAMPARRAY)malloc(sizeof(JSAMPROW)*image_height);
    for(int i = 0; i < image_height; ++i){
        img[i] = (JSAMPROW)calloc(sizeof(JSAMPLE), image_width * in_info.output_components);
        memcpy(img[i], buffer[i], image_width * in_info.output_components);
    }

    jpeg_finish_decompress(&in_info);
    jpeg_destroy_decompress(&in_info);
    fclose(infile);

    //
    // 圧縮処理開始
    //
    FILE *outfile;
    if((outfile = fopen(out_file_name, "wb")) == NULL){
        fprintf(stderr, "ファイルが開けません: %s\n", out_file_name);
        return -1;
    }

    struct jpeg_compress_struct out_info;
    out_info.err = jpeg_std_error(&jpeg_error);
    jpeg_create_compress(&out_info);

    jpeg_stdio_dest(&out_info, outfile);

    // サムネイル画像の設定値セット
    out_info.image_width = image_width;
    out_info.image_height = image_height;
    out_info.input_components = components;
    out_info.in_color_space = color_space;
    jpeg_set_defaults(&out_info);

    jpeg_start_compress(&out_info, TRUE);

    jpeg_write_scanlines(&out_info, img, image_height);

    jpeg_finish_compress(&out_info);
    jpeg_destroy_compress(&out_info);
    fclose(outfile);

    // メモリの解放
    for(int i = 0; i < in_info.output_height; ++i){
        free(buffer[i]);
    }
    free(buffer);

    for(int i = 0; i < image_height; ++i){
        free(img[i]);
    }
    free(img);

    return 0;
}

// メイン
int main(int argc, char **argv)
{
    convert(atoi(argv[1]), argv[2], argv[3]);
}