kaeruspoon

35歳定年説

誕生日をむかえて、驚くべきことにまたひとつ年をとった今日この頃なのです。プログラマの世界では「35歳定年説」というものがあって、それによるとぼくは来年にはプログラマをやめなきゃいけなくなるわけですが、とんとそんな気はなく老害を巻散らかす所存であります。

デザインにまた飽きてきた

新しい白いデザインはシンプルすぎてもう飽きてきました。なんでもすぐに飽きる男なのです。

仕事が忙しくて最近日記も書けません。仕事が忙しいというのは恥ずかしいことなので、恥ずかしくて日記も書けないのです。今書いているけど。実は忙しいというのも嘘で、家に帰ってきてからご飯を食べたり寝たりする時間が5時間もあったりします。まったくけしからん。

二十代の頃のぼくはまったくテレビを見ていなかったのですが、結婚してからちょくちょく見るようになりました。頭が悪くなってきた証左でしょう。ぼくが見ているのは、ほとんどNHKとMXTVだけです。

村上春樹さんのエルサレム賞受賞のスピーチが話題になったりしていますが、新聞が報道している、エルサレムを批判している、というものとはニュアンスが違っています。そういう具体的で局所的な話なんかしていないですよね。彼の言っている「卵」とはガザの人はもちろんですが、エルサレムの人のことでもあるのだと思います。
ちなみにぼくは村上春樹さんの本を一冊も読んだことはないし、これからも読むことはないでしょう。みんなが評価しているものは読みたくなくなるアマノジャクなのです。

職場について

雑誌などを見ていると、デザイナさんの会社はみんなおしゃれで楽しそうで素敵な職場ばかりが載っています。最近はソフトウェアの会社もがんばっているところがあるけど、まだまだデザイン系の会社には勝てないといった感じがします。

そんなぼくの職場は雑音やらなにやらでいろいろと大変なので、集中するために音楽を聴きながら仕事をしています。音楽を聴いていると本当はあまり集中できないんだけど。
最近、ループしていつも同じ曲ばかりだったので、大昔のCDを引っ張り出してきて今携帯に転送しているところです。SUPER EURO BEATとか出てきてビビりました。高校生の頃は阿呆みたいに聴いていたので…。チナミに最近ぼくが聴いているのはロックとクラシックばかりです。たまにコードギアスやガンダムが入ってきたりします。

なんだか思うようにコードが書けなくて、もっとうまい方法がないものかと考えたりしています。サンデープログラミングの手法を仕事でやっているのはけっこう怖いです。やっぱりテストを書きたいお年頃。18歳(誰が?)。同じ業界でも、きちんとXP回しているところとそうでないところの差が激しい気がします。

調子のよくない日もある

今日はなんだかやる気がでなくて一日中ごろごろしていたような気がします(マリナ・イスマイールと子供たちではない)。jpeglibを使ったりいろいろやろうとしていたのだけど中途半端中であります。~であります、というのはもともと山口県の方言なのですが明治維新後に長州藩出身の軍人が多かったせいで、それが軍人言葉になったそうです。

日本人の知らない日本語
  • 日本人の知らない日本語
  • 作者/アーティスト: 蛇蔵&海野凪子
  • 出版社/メーカー: メディアファクトリー
  • メディア: 単行本(ソフトカバー)
  • 発売日: 2009-02-18

「日本人の知らない日本語」に書いてありました。話は変わりませんが、この本はなかなかおもしろいです。そんな今日は初代のフェアディZが走っているのを見たりして、その車はぼくの父も乗っていたのですが、二人乗りの車なので荷台に載せられて幼稚園に送り迎えされたりしていて、弟と並んで寝ころんで電熱線ごしに見ていた青空を思い出したりしていました。

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

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]);
}

サムネイル画像高速作成ツールspeedpetalの開発 - その2 ニアレストネイバー法を試してみた

speedpetalにニアレストネイバー法を適用してみました。ニアレストネイバー法は、単純に変換後の位置に該当する変換前画像のピクセルを持ってくる、という方法なので実装は簡単です。

// ニアレストネイバー法によるサムネイルの作成
void nearest_neighbor(JSAMPARRAY in_buffer, JSAMPARRAY out_buffer, int origin_width, int origin_height, int width, int height, int components) {
    double width_factor = (double)origin_width / width;
    double height_factor = (double)origin_height / height;
    int width_pos[width];

    for(int x = 0; x < width; x++) {
        width_pos[x] = (int)(x * width_factor) * components;
    }

    for(int y = 0; y < height; y++) {
        int height_pos = (int)(y * height_factor);
        for(int x = 0; x < width; x++) {
            memcpy(&out_buffer[y][x * components], &in_buffer[height_pos][width_pos[x]], components);
        }
    }
}

こんな感じ。
できあがるサムネイル画像は200x200くらいになると荒さが目立ちます。速い代わりに汚くなるのがニアレストネイバー法なのです。といっても100x100くらいになれば、荒さも目立たなくなるので全然有用だと思います。