JPEG ライブラリを試す

[2003/12/31]

これまで JPEG 画像を扱う場合には djpeg で pnm 形式へ展開しそれを他の形 式へ変換するか,Tcl/Tk での PIFE ライブラリを利用してきましたが, 効率などの問題と単なる興味から直接 JPEG 画像を扱ってみたくなったのでラ イブラリを試してみました.

ソース・ライブラリの入手

使用している FreeBSD 環境には既に JPEG ライブラリはインストールされて いますが,内容を眺めるため IJG のサイトから JPEG ソースファイルアーカイブ jpegsrc.v6b.tar.gz を入手しました.これを展開し ./configure, make を行なうと, 通常のコマンドである cjpeg, djpeg などが生成されます.

今回は展開を試すため djpeg.c を参考にします.このファイルでは多種形式へ の出力のため抽象化されている箇所があり読み取りにくいのですが, なんとか PPM 形式の処理だけを抜き出して,処理の流れを単純化していきま した.この単純化がまっとうかは不明ですがだいぶわかりやすくなりました.

サンプルファイル

作成したサンプルファイルを次に示します. これはJPEG 画像を読み込み横幅 80 ピクセルの画像へ縮小して出力します. 出力形式は PostScriptとなっています(溜りまくったカシオ エクシリムの画像群のサムネイルを生成する野望用). コンパイルはシステムにインストールされているライブラリを使って 行なうように指定します. コマンドライン引数にファイル名を与えて起動すると,次のようなサムネイル 画像が PostScript 形式で生成されます.

なお,オリジナルの djpeg.c はこちら

内容解説

インクルードファイルは JPEG 処理用には jpeglib.h と jinclude.h となり ます.
#include <stdio.h>
#include <stdlib.h>
#include <libgen.h>

/* JPEG 用インクルード */
#include <jpeglib.h>
#include <jinclude.h>
今回の出力は PostScript 形式で行ないますが,画像データは 16 進表現され, これに適宜改行を補って出力します.その際の出力数をカウントする変数を大 域変数として準備しておきます.
/* PostScript 時の一行あたりの出力データ数のカウント用 */
long total_put_cnt=0;
画像の展開データは処理系に負荷をかけすぎないよう1行ずつ出力されるそう で,その処理を行なう関数を作成します.PostScript では1行中のデータを 16 進文字列で「赤のみ,緑のみ,青のみ(一行終わり)」として表現するら しいので,そのように出力していきます.

この処理を行なう関数 put_pixel_rows() は,1行の画像データバッファ *iobuffer とそのデータ数(画像の横幅) width の引数を取ります.データ は RGB 毎に3回ループを回して出力しました(もっと賢いやり方ができそう ですが…).上で定義した出力数をカウントする total_put_cnt 変数を使っ て32データ毎(2桁の16進数なので64文字)に改行を入れています.

/* 一行の画像データを出力 */
void put_pixel_rows(char *iobuffer, size_t width)
{
  unsigned char *buf;
  long cnt;

  /* PostScript では一行毎に rrr..ggg..bbb... と色別の出力を行なう */
  buf = iobuffer;
  for (cnt=0; cnt<width; cnt+=3) {
      printf("%02x", *buf++); buf++; buf++;
      if ((++total_put_cnt%32)==0) printf("\n");
  }
  buf = iobuffer+1;
  for (cnt=0; cnt<width; cnt+=3) {
      printf("%02x", *buf++); buf++; buf++;
      if ((++total_put_cnt%32)==0) printf("\n");
  }
  buf = iobuffer+2;
  for (cnt=0; cnt<width; cnt+=3) {
      printf("%02x", *buf++); buf++; buf++;
      if ((++total_put_cnt%32)==0) printf("\n");
  }
}
それでは次に main() 関数に入っていきます.
int main(int argc, char **argv)
{
JPEG ライブラリで使用される jpeg_decompress_struct 構造体 cinfo とエラー 処理の割り当てを行なう jpeg_error_mgr 構造体 jerrを用意します. また,サムネイル作成元の画像ファイルを扱うためのファイルストリーム変数 とファイル名用のポインタ変数 (fname が argv[1] に割り付けられる) を用 意します.
  struct jpeg_decompress_struct cinfo;
  struct jpeg_error_mgr jerr;
  FILE * fd;
  char *fname;
次に画像データに関わる変数です.展開用バッファ buf_w と出力データが入 るバッファの iobuffer を用意します.残りの2つは iobuffer を示すポイン タ JSAMPROW pixrow とさらにそのポインタである JSAMPARRAY samp_buf です. 出力画像の幅と高さは width, height 変数に保存します.
  size_t buf_w;
  char *iobuffer;		/* fwrite's I/O buffer */
  JSAMPROW pixrow;
  JSAMPARRAY samp_buf;
  int width, height;
読み込むファイルを開きます.コマンド引数を展開ファイル名にし,それがな い場合は標準入力から JPEG データを読み込むことにします.
  /* 読み込みファイルの指定 */
  fd = stdin;
  if (argc>1) {
      fname=argv[1];
      if ((fd = fopen(fname, "rb"))==NULL) {
	  fprintf(stderr, "%s: No such file '%s'.\n", basename(argv[0]), fname);
	  exit(1);
      }
  }
ここから JPEG 処理の割り当てを行なっていきます.まず,エラーメッセージ 出力用の設定.jerr を STDERR に割り当て,それを JPEG 処理用の構造体 cinfo に登録します.
  /* エラー時の割り当て */
  cinfo.err = jpeg_std_error(&jerr);
次に展開処理を生成します.jpeg_createcompress() で生成し, jpeg_stdio_src() で先に開いたファイルを指定します.そのご jpeg_read_header() でファイルの JPEG ヘッダの読み込みを行ないます.
  /* JPEG 展開用の初期化: cinfo 構造体に各種情報が入る */
  jpeg_create_decompress(&cinfo);
  jpeg_stdio_src(&cinfo, fd);
  jpeg_read_header(&cinfo, TRUE);
ここで,サムネイル用の縮小設定と展開設定を低品質にすることにより処理を高速 化します.

サムネイル画像幅は現在のところ 80 で固定とし,印刷時に縮小するよう解像 度を考慮して展開サイズ 160 としました.これを元に画像サイズから展開サ イズを逆算します.展開サイズは cinfo 構造体中のメンバの scale_num/scale_denom の分数で表されます.この分数に指定できる値は djpeg コマンドのマニュアルページによると「1/1, 1/2, 1/4 または 1/8」で なければならないようですが,JPEG ライブラリ中での制限なのかは未確認で す.また,サムネイルであればそれほどの品質は不要と思われますので, djpeg コマンドの -fast オプションに対応する設定(2パス展開の抑制など) を行なって,処理の高速化を期待します.

  /* サムネイル生成用に縮小設定 (num/denom となる) */
#define THUMNAIL_BASE_WIDTH (80)
  cinfo.scale_num=1; cinfo.scale_denom=
			 (int)(cinfo.image_width/(THUMNAIL_BASE_WIDTH*2));
#if 1
  /* 展開処理を高速化: djpeg コマンドの -fast オプションに対応 */
  cinfo.two_pass_quantize = FALSE;
  cinfo.dither_mode = JDITHER_ORDERED;
  if (! cinfo.quantize_colors) /* don't override an earlier -colors */
      cinfo.desired_number_of_colors = 216;
  cinfo.dct_method = JDCT_FASTEST;
  cinfo.do_fancy_upsampling = FALSE;
#endif
これらの設定データを元に jpeg_calc_output_dimensions() 関数を使って展 開サイズなどの各種パラメータを求めます.これらの値は cinfo の output_width, output_height 変数に保存されます.

さらにこれらの値から展開用のバッファなどを用意します.割り当ては cinfo.mem->alloc_small に割り当てられている関数を呼び出して行ない, buf_w と iobuffer の2つを用意します.そして iobuffer を pixrow とさ らに上位の samp_buf に割り当てておきます.

  /* 出力設定に基づいて各種サイズを算出 */
  jpeg_calc_output_dimensions(&cinfo);

  /* 出力画像サイズ */
  width=cinfo.output_width;
  height=cinfo.output_height;

  /* 展開用バッファの設定: cinfo の alloc_small 関数を使う */
  buf_w = sizeof(char)*width*cinfo.out_color_components;
  iobuffer = (char *)
      (*cinfo.mem->alloc_small)((j_common_ptr)&cinfo, JPOOL_IMAGE, buf_w);
  pixrow=(JSAMPROW) iobuffer;
  samp_buf = & pixrow;
ここから出力処理です.まずは PostScript ヘッダから.先に少し書きました が,出力解像度を上げるため画像の印刷上の幅は cinfo に保存されているも のより半分にします.BoundingBox もそれに対応しています.サムネイルのファ イル名を付加するので高さはそれを見込んだ値にします. プリアンブル中では画像データの読み込み関数の定義 readstring と読み込み用のバッファ文字列 rstr, gstr, bstr の割り当ても 行なっておきます.続くgsave では後ろに続く grestore と対に使って画像表 現をひと括りにします.
  /* PostScript ヘッダの出力: 紙面上の見かけ幅はさらに半分で解像度を上げる */
  printf("%%!PS-Adobe-2.0 EPSF-2.0\n");
  printf("%%%%BoundingBox: 0 0 %d %d\n", width/2, height/2+8);
  printf("%%%%EndComments\n");
  printf("/readstring {  currentfile exch readhexstring pop } bind def\n");
  printf("/rstr %d string def\n", width);
  printf("/gstr %d string def\n", width);
  printf("/bstr %d string def\n", width);
  printf("%%%%EndProlog\n");
  printf("gsave\n");
サムネイルの上部にはそのファイル名を表示することにしました.フォントは /Courier-Bold 6 を指定しています.それに続けて画像データ処理の設定です. ここの処理は後ろのPostScript 画像貼り込みの箇所を見て下さい.
  /* 出力画像の上部にファイル名を付加: BB の height の +8 はこれ用 */
  printf("/Courier-Bold findfont 6 scalefont setfont\n");
  printf("%d %d moveto (%s) dup stringwidth pop neg 2 div 0 rmoveto show\n",
	 width/4, height/2+2, fname);
  /* 画像の出力 */
  printf("%d %d scale\n", width/2, height/2);
  printf("%d %d 8\n", width, height);
  printf("[%d 0 0 -%d 0 %d]\n", width, height, height);
  printf("{rstr readstring}\n");
  printf("{gstr readstring}\n");
  printf("{bstr readstring}\n");
  printf("true 3 colorimage\n");
JPEG 展開を開始し,1行ずつのデータを出力していきます.展開された行の 画像データは先に作成しておいた put_pixel_rows() 関数に渡されます. jpeg_read_scanlines() 関数の読み込みは行数の引数があり,これに複数行の 指定もできるのでしょうが,1行にとどめておくのが行儀がいいのでしょうね.
  /* 展開開始 */
  jpeg_start_decompress(&cinfo);
  /* 展開出力(一行毎) */
  while (cinfo.output_scanline < cinfo.output_height) {
    jpeg_read_scanlines(&cinfo, samp_buf, 1);
    put_pixel_rows(iobuffer, buf_w);
  }
最後に jpeg_finish_decompress(), jpeg_destroy_decompress() 関数を呼び 出して処理を閉じ,PostScript フッタを出力して終了です.
  /* 展開終了 */
  jpeg_finish_decompress(&cinfo);
  jpeg_destroy_decompress(&cinfo);

  /* PostScript フッタの出力 */
  printf("\ngrestore\nshowpage\n");

  return 0;
}

おまけ:PostScript 中での画像表現

読み込み処理の設定とそれに続く画像データで表現します.
/readstring {                       % 色データの1行読み用の定義
  currentfile exch readhexstring pop
} bind def                          % currentfile で後ろのデータを指定
/rstr _width_ string def            % データ用一時文字列を用意(赤)
/gstr _width_ string def            %                         (緑)
/bstr _width_ string def            %                         (青)
gsave
_width_ _height_ scale              % ←任意のサイズに拡大
_width_ _height_ 8                  % ←画像のパラメータ
[_width_ 0 0 -_height_ 0 _height_]  % ←画像を 1x1 に変換
{rstr readstring}                   % ←読み込み処理(赤)
{gstr readstring}                   %               (緑)
{bstr readstring}                   %               (青)
true 3 colorimage
...RRRRRRRRR...GGGGGGG...BBBBBB.... % 以下1行毎に RGB データを
.....RRRRRRRRR...GGGGGGG...BBBBBB.. % 個別に16進で並べていく.
........RRRRRRRRR...GGGGGGG...BBBBB % しきたりで32個毎ぐらいに改行
   ...
grestore
showpage
上記のように生成された画像データをさらに並進させて配置します.この場合 内包される画像 PS は %%BoundingBox でサイズを指定し EPS ファイルにして おきます(ソース参照).
gsave
300 300 translate
  ...画像 PS...
grestore

参考


おまけ2:エクシリム画像のサムネイル生成

カシオ エクシリムで撮り溜めた画像群のサムネイルを作成します.印刷して 眺められるよう PostScript 形式とします.

画像ファイル名の変更

エクシリムの標準のユーティリティでは画像ファイルは 「日付/CIMG(一時連番).JPG」の形で保存されます.連番はディレクトリ毎で 重複もあるためそこから抜き出して同じ箇所で作業する場合などには不便です. パソコンへ保存されたときもファイルの日付は撮影時のものと一致している (たぶん)ので,これをもとにファイル名を「日付時刻.jpg」に変更します. ファイル名自体を変更したほうが使い勝手はよさそうですが,標準ユーティリ ティとの連係も一応残しておいて,元のファイルへのシンボリックリンクで対 応します.

サムネイル群の生成とまとめ

変更したいファイル名群を以下の date_name.tcl へ標準入力から与えるとそ のファイルの日付情報をもとにシンボリックリンクを生成します.
例:find ~/画像ディレクトリ -maxdepth 2 -name \*.JPG | ./date_name.tcl

そして,1ページにサムネイル画像をずらずらと並べます. 個別サムネイルの生成は上記の djpeg_sample を内部呼び出しして使って,そ れをページ中に配置していきます.配置は下の img_wrapper.tcl を使います. (両者を統合したほうがいいですがとりあえず…). 処理ファイルの指定はコマンドラインではなく,いまのところ Tcl スクリプ ト中の glob コマンドで行なっています. いちおう改ページにも対応しています.gsave や grestore でいろいろ括って しまいました.希望の出力とはなっていますが,正しい処理なのかはわかりま せん m(__)m.

サムネイルの生成例はこちら (PDF変換版 375KB) です (元PSファイル, gz圧縮で5MB).

#それにしても,ソリダコ (20030719130648.jpg) ってなんなのでしょうねぇ...(→Google検索


||Doc||PostScript 入門||

oshiro@mibai.tec.u-ryukyu.ac.jp