ホーム 主筆 その他ソフト その他情報 Syuhitu.org English

Windows関連

スクリーンセーバー作成法

半透明ウインドウの性能

bootfont.bin

キャビネット形式

ウインドウスタイルをいじる

Java製ソフトをServiceに登録する

イベントログにメッセージを出力する

コントロールパネルにアイコンを追加する

スクリプトによる拡張1

スクリプトによる拡張2

ガジェットの作成

大容量メモリ

メモリ搭載量の下限に挑む

スパースファイルにする

表示されるアイコンの種類を調べてみた

メモリマップIOとエラー処理

ファイルを作る順番と速度の関係

Cryptography API: Next Generationを使う

Windows 10のアクセントカラー

iSCSIディスクにバックアップを取る

サーバプロセスを分離して実装する

サーバプロセスを分離して実装する - F#

レジストリに大量に書き込む

Solaris関連

OpenGL

Solaris設定

ディレクトリの読み込み

主筆プラグイン開発

マルチスレッドでの開発

door

音を出す

Blade100の正しい虐め方

パッケージの作成

画像入出力

BMPファイル

ICOファイル

ANIファイル

JPEGファイル

減色アルゴリズム

減色アルゴリズムの並列化

その他アルゴリズムなど

自由軸回転

Base64

文字列操作

CPU利用率の取得

正規表現ライブラリ

メタボールを作る

メタボールを作る2

正規表現とNFA・DFA

C言語の構文解析

液晶ディスプレイを解体してみた

iSCSIの理論と実装

単一フォルダにファイルを沢山作る

USB-HUBのカスケード接続

SafeIntの性能

VHDファイルのフォーマット

USBメモリに書き込み続けてみた

JPEGファイルの入出力

2006年1月5日公開

今まで、BMPファイルICOファイルANIファイルのファイルフォーマットと入出力について解説してきたから、今度はJPEGファイルについて考えてみたいと思う

ところがJPEGファイルというのはその仕様が半端ではなく難しい。圧縮されている上に、その圧縮方法というのが離散コサイン変換とか言う数学を駆使した代物だという。

これでは俺の手には負えそうにはないので、今回は他人が作ったライブラリの助けを借りることにする。

そう言うことで今回使うのは、IJGとか言うところが作ってるjpeglibとか言うライブラリである。これをダウンロードしてきて使うのもいいが、Solarisの場合はデフォルトでこのライブラリがインストールされてるから、それを使っても問題ない。

まぁ、作ったソフトを配布(あるいは納品)する場合、このライブラリに関する著作権とか何とかいろいろと嫌な問題がつきまとうから、Solarisに入っているのを当てにした方が良いかも知れない。

俺の英語の読解が正しければ、再配布にはこういう制約がある、と書いてあるように思う。
  • ライブラリやその一部をソースコードの形で配布する場合は、中に含まれているREADMEファイルを一緒に配布すること。修正を加えたなら、どこをどう変更したのかを明示すること。
  • バイナリのみで配布するのなら、どこかに"this software is based in part on the work of the Independent JPEG Group"という記述を加えること。
  • このライブラリの作者は、一切の責任を負わない。ライブラリを使う奴が全ての責任を負え。

他にもあるかも知れない。とりあえずREADMEは読んでおけ。

このライブラリの使い方については、中に含まれてるlibjpeg.docというファイルに書いてあるのだが、これが例のごとく全部英語である。とりあえずこのページでは、このlibjpeg.docファイルの中身に沿って、(日本語で)jpeglibの使い方について解説しようと思う。

取り扱うイメージ

JPEGファイルがサポートしているイメージは、フルカラーとグレースケールのみである。インデックス化されたカラー画像はサポートしていない。

しかしこのライブラリでは、フルカラーのJPEG画像を読み込み、インデックスカラーに変換する機能を持っている。でも、インデックスカラーの画像を自動的にフルカラーのJPEGに変換する機能は持っていない。それぐらいは自分でやれと言うことなのだろう。

ヘッダファイル

jpeglib.hというファイルをインクルードする。

Solaris10では/usr/includeにそのファイルが存在するため、何も考えずに「#include <jpeglib.h>」とすればインクルード出来るはずである。

ライブラリ・コンパイル

Solarisにデフォルトで入っているのを使用する場合は、/usr/lib/libjpeg.soというライブラリとリンクする必要がある。そのためコンパイルするときには「-ljpeg」という指定を加える。

データ型

 入出力するデータ型と型名などについて。

 このライブラリでは、画像データを以下のような形式で入出力するようになっている。

JSAMPLEというのは、(基本的には)unsigned char型で各ピクセル内の一色を表す。つまり、RGBカラーであればJSAMPLE3つで1ピクセルを表すことになる。グレースケールであればJSAMPLE1つで1ピクセルを表すことになる。

JSAMPROW型というのは、JSAMPLEの配列の先頭アドレスを示すポインタの型である。つまりJSAMPLE*である。

JSAMPARRAY型というのは、JSAMPROWの配列の先頭アドレスを示すポインタの型である。つまりJSAMPROW*である。

JSAMPLEの配列一つが、高さ1ピクセル分のイメージを表す。配列の先頭側がイメージの左側に相当する。

JSAMPROWの配列がイメージ全体を表す。配列の先頭側がイメージの上側に相当する。

256×256のフルカラー画像を保持するバッファは、次に様に確保される。

JSAMPARRAY img;
int i, j;

img = (JSAMPARRAY)malloc( sizeof( JSAMPROW ) * 256 );
for ( i = 0; i < 256; i++ ) {
  img[i] = (JSAMPROW)malloc( sizeof( JSAMPLE ) * 3 * 256 );
  for ( j = 0; j < 256; j++ ) {
    img[i][ j * 3 + 0 ] = i;
    img[i][ j * 3 + 1 ] = 0;
    img[i][ j * 3 + 2 ] = j;
  }
}

なお、上記のコードではこのように初期化している。

なお、jpeglibでは、JSAMPLEの配列の事をスキャンラインと呼んでいるらしい。

上の例ではイメージ全体を保持するバッファをメモリ中に確保しているが、別にこれはそうしなければならないわけではない。必要とあれば1スキャンラインずつ処理するようにすることも出来る。その場合、バッファは1行分だけで済む。

圧縮

jpeglib.docでは、JPEGファイルを出力することをCompressionと書いてある。どうやら、そういう哲学であるらしい。だからこのページでもそれに合わせて、JPEGファイルの出力を「圧縮」、JPEGファイルの読み込みを「伸張」と書くが、これは適当に読み替えてもらって問題ないと思う。

JPEGオブジェクトを確保

JPEGでの圧縮を行うにはJPEGオブジェクトという物が必要になるらしい。だからそれを確保してやる。

struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;

cinfoがJPEGオブジェクトというものである。jerrというのは、エラー処理を行うためのものである。とりあえず必要。

JPEGオブジェクトの初期化

使う前には初期化する必要がある。

初期化には初期化用関数jpeg_create_compressを用いる。だた、これは内部でメモリ領域の確保を行うため、エラーが発生する恐れがある。だからその前に、エラー処理の為の初期化を行う必要がある。

cinfo.err = jpeg_std_error( &jerr );
jpeg_create_compress( &cinfo );

デフォルトのエラーハンドラを使用する場合は、この記述は決まり文句であるらしい。なお、デフォルトのエラーハンドラは、エラーが発生した時には標準エラー出力にメッセージを表示してプロセスを停止させてしまうらしい。しかしこれは変更することも出来る

ファイルを開く

デフォルトでは、JPEGライブラリはstdioのストリームに結果を出力する。これは変更することも出来るらしいが、当面はこのまま解説する。

出力先がstdioのストリームということなので何でも良いのだが、とりあえず、一番判りやすいファイルに出力させることにする。

出力先を指定するには、jpeg_stdio_destという関数を使用する。

outfile = fopen( "ファイル名", "wb" );
jpeg_stdio_dest( &cinfo, outfile );

まぁ、ふつーfopenの戻り値ぐらいは確認するんだろうが、ここでは省略する。あと当然だが、ファイルはバイナリモードで開く必要がある。

パラメータを設定する

どういう風に出力したいのか、パラメータを設定する必要がある。

パラメータは、JPEGオブジェクトのメンバに値を設定することで指定する。

よく使うパラメータは、以下の通り。

cinfo.image_width = 画像の幅;
cinfo.image_height = 画像の高さ;
cinfo.input_components = 1ピクセル当たりの色数;
cinfo.in_color_space = カラースペース;

input_componentsには、1ピクセル当たりの色数を指定する。RGBカラーであれば3、グレースケールであれば1を指定する。

in_color_spaceには、カラースペースを示すJ_COLOR_SPACE型の定数を指定する。RGBカラーであればJCS_RGBを、グレースケールであればJCS_GRAYSCALEを指定する。

それ以外のデフォルト値は、jpeg_set_defaults関数で設定させることが出来る。ただ、このデフォルト値というのが、カラースペースの設定によって値が変わるため、jpeg_set_defaults関数を呼ぶのはカラースペースを指定した後である必要があるらしい。

だから、256×256のRGBカラーであれば、こういう風になるらしい。

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

画質を設定するには、jpeg_set_quality関数を使用する。

jpeg_set_quality( &cinfo, 10, TRUE );

二番目の引数は画質を表す整数である。0から100まで指定できる。0の方が画質が低い。

三番目の引数はベースラインを強制するか否かを指定する。よく判らんがTRUEにしておけばいいらしい。

圧縮を開始する

パラメータを設定したら、圧縮を始めることが出来る。

圧縮はjpeg_start_compress関数を呼ぶことで開始される。

jpeg_start_compress( &cinfo, TRUE );

この関数の二つ目のパラメータは、完全なJPEG圧縮データストリームの出力を保証することを示すものであるらしい。とりあえずTRUEにしておけばいいらしい。

この関数を呼び出すことで、内部で作業用のメモリが確保されて、JPEGファイルのヘッダが出力される。

圧縮する

ここでJPEGライブラリにイメージデータを食わせてやる必要がある。

イメージデータはjpeg_write_scanlines関数で渡す。

jpeg_write_scanlines( &cinfo, img, 256 );

二つ目の引数は、JSAMPARRAY型の値である。三つ目の引数はスキャンラインの数である。

上記「データ型」で示した例のように、メモリ中にイメージ全体のデータを保持している場合は、この一行の呼び出しで、イメージ全体を圧縮することが出来る。

imgに一行分のデータしかなければ、次のようになる。

for ( i = 0; i < 行数; i++ ) {
  jpeg_write_scanlines( &cinfo, img, 1 );
}

なお、jpeg_write_scanlines関数の戻り値は、出力したスキャンラインの行数である。

また、JPEGオブジェクト(ここではcinfo)のnext_scanlineフィールドには、今まで出力したスキャンラインの行数が格納されている。

もしここで、最初に指定したイメージの高さを超えてデータを出力した場合(つまり、データを渡しすぎた場合)、ライブラリは余分なデータは無視するらしい。

終了する

必要なデータを全部渡してしまったら、jpeg_finish_compress関数を呼び出す。

jpeg_finish_compress( &cinfo );

これにより、バッファに残っているデータが完全に出力され、作業用のメモリが解放される。

なお、必要なイメージデータを全て渡しきっていない状態でこの関数を呼び出すと、エラーが発生する。途中で処理を中断したい場合には、jpeg_abort関数を使用する必要がある。

JPEGオブジェクトを破棄する

使い終わったら片付ける。

jpeg_destroy_compress( &cinfo );

これにより、不要になったメモリ領域が開放される。

ファイルを閉じる

最後にファイルを閉じる必要がある。

アボートする

もし途中で処理を中断したくなったら、jpeg_destroy_compressかjpeg_abort_compressを使用する。

処理を中断して、かつ、JPEGオブジェクトを再利用する必要がないのであれば、jpeg_destroy_compressを呼べばいい。jpeg_create_compressを呼び出した後には、いつ何時jpeg_destroy_compressを呼んでも問題ない。

処理を中断しつつ、JPEGオブジェクトを再利用する必要がある場合には、jjpeg_abort_compressを呼ぶ。

jpeg_abort_compress( &cinfo );

プログラム例

以上の事を元に、「データ型」で示した例のイメージを出力するプログラムを示す。

とりあえず、画像を全部メモリ中に保持しておくパターン(jpeg_prog01.c)と、一行ずつ処理するパターン(jpeg_prog02.c)の二つを公開しておく。

また、同じイメージを画質を0にした場合と、100にした場合の例も示す。


伸張

つまり、JPEGファイルの読み込みのことである。

JPEGオブジェクトの確保と初期化

圧縮の時と同様である。

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

ファイルを開く

圧縮の時と同様、JPEGライブラリではデフォルトのソースとしてstdioのストリームを使用する。ここではファイルから読み込むものとして解説を進める。

infile = fopen( "入力ファイル", "rb" );
jpeg_stdio_src( &cinfo, infile );

ヘッダの読み込み

JPEGファイルのヘッダを読み込んで、イメージに関する情報を取得する。

jpeg_read_header( &cinfo, TRUE );

この関数を呼び出すことで、JPEGファイルのヘッダが読み込まれ、イメージのオリジナルの幅や高さ、カラースペースなどの情報を取得することが出来るようになる。

パラメータの設定

展開に関するパラメータを設定する。

しかし、jpeg_read_header関数がデフォルト値を設定してくれるため、デフォルトで問題ない場合は、ここでは特に何もすることはない。

展開の開始

展開出来るようになったら、jped_start_decompress関数を呼び出して、圧縮を開始する。

jpeg_start_decompress( &cinfo );

この関数を呼び出すと、JPEGオブジェクトのメンバに、スケーリングなどを行った後の最終的なイメージサイズが設定される。

cinfo.output_width : 幅
cinfo.output_height : 高さ
cinfo.out_color_components : 1ピクセル当たりの色数(RGBカラーなら3、グレースケールなら1)

また、色のインデックス化を要求していた場合には、カラーマップも設定される。

cinfo.output_components : 1ピクセル当たりのデータ長(インデックスカラーの場合は1、それ以外の場合はout_color_componentsと同じ)
cinfo.colormap : カラーマップ

展開する

ここまで来てようやくデータを読み込むことが可能になる。

データを取得するにはjpeg_read_scanlines関数を使用する。

while( cinfo.output_scanline < cinfo.output_height ) {
  jpeg_read_scanlines( &cinfo,
      img + cinfo.output_scanline,
      cinfo.output_height - cinfo.output_scanline
  );
}

jpeg_read_scanlines関数の三番目の引数には、バッファに用意されているスキャンラインの数を指定する。しかし、一回の呼び出しで、バッファが完全に満たされるとは限らない。(現在の実装では、一回の呼び出しで数行分のデータしか返さないらしい。)

ライブラリは、JPEGオブジェクトのoutput_scanlineに今まで読み込んだスキャンラインの行数を記録するから、この値を使用して、全ての行が読み込まれるまでjpeg_read_scanlinesを複数回呼び出さなければならない。

展開を終了する

展開が終わったらjpeg_finish_decompressを呼び出す。

jpeg_finish_decompress( &cinfo );

JPEGオブジェクトを破棄する

jpeg_destroy_decompress関数をしようして、JPEGオブジェクトを破棄する。

jpeg_destroy_decompress( &cinfo );

ファイルを閉じる

当然、事が終わったらファイルを閉じなければならない。

アボートする

途中で処理を中断したくなった場合は、jpeg_destroy_decompressかjpeg_abort_decompress関数を呼び出す。

処理中断し、かつ、JPEGオブジェクトを再利用しない場合はjpeg_destroy_decompressを呼び出す。JPEGオブジェクトを再利用したい場合には、jpeg_abort_decompressを使用する。

jpeg_abort_decompress( &cinfo );

プログラム例

エラー処理を変更する

「圧縮」のはじめの方でも書いたが、デフォルトのエラー処理では、エラー発生時には標準エラー出力にメッセージを出力して、プロセスを停止させてしまう。

実用的なアプリケーションに組み込む場合、これでは使い物にならないため、エラー処理の挙動を変更してやらなければならない。

エラーハンドラは、「圧縮」と「展開」の「JPEGオブジェクトの確保」のところで記述した、struct jpeg_error_mgr型のjerrに、関数ポインタの形で保持されている。だから、jpeg_std_error関数を呼び出した後に、必要なメンバの関数ポインタを置き換えてしまえばいい。

その中でも特に問題になるのは、error_exitとoutput_messageである。

error_exit

関数プロトタイプは

void error_exit( j_common_ptr cinfo );

である。

この関数は、ライブラリ内で致命的なエラーが発生したときに呼び出される。デフォルトの処理では、後述するoutput_messageを呼び出して、exitするようになっている。

引数のj_common_ptrというのは、jpeg_compress_structjpeg_decompress_structのどちらかのオブジェクトをポイントするポインタである。圧縮か展開かの判断は、cinfoのメンバis_decompressorを調べることで行う。

この関数からは、呼び出し側に制御を返してはいけない。基本的にはexitやlongjmpを呼び出すこと。

output_message

関数プロトタイプは

void my_output_message( j_common_ptr cinfo );

である。

この関数はerror_exit関数から呼ばれる。それ以外から呼ばれることはない。

デフォルトの処理では、エラーメッセージ生成用の関数(cinfo->err->format_message)を呼び出して、エラーメッセージを生成し、それを標準エラー出力に出力する。

追加の情報

大概の場合、エラーハンドラerror_exitの中で追加の情報が必要になる。例えばlongjmp関数のjmp_buf型の引数などである。

そのため、JPEGオブジェクトにはvoid*型のclient_dataというフィールドが用意されており、ユーザが自由に使用することが出来るようになっている。これを用いることで、エラーハンドラに必要な情報を渡すことが出来る。

プログラム例

以上の事を元に、エラー処理を実装したプログラム(jpeg_prog04.c)を示す。これは、jpeg_prog02.cを元に下記の点を変更したものである。

  • my_output_message関数を追加
  • my_error_exit関数を追加
  • jpeg_std_error関数の呼び出しと、jpeg_create_compressの呼び出しの間に、エラーハンドラを設定する処理を追加

 


連絡先 - サイトマップ - 更新履歴
Copyright (C)  2000 - 2016 nabiki_t All Rights Reserved.