libjpegによるC言語での画像入出力
C言語で画像を処理してみたいと思い、JPEG画像のエンコード/デコード実装として使われているライブラリ、libjpegを試してみました。
2019/3/7:内容を整理。
実行環境
インストール
$ sudo apt install libjpeg-dev
執筆時点(2018/9/23)での最新リリースバージョンは 9c of 14-Jan-2018のようです。
JPEGファイルの読み込み
フロー
JPEGファイルの読み込みは次の流れとなっています。
- 構造体確保
- ファイル指定
- ヘッダ読み込み
- データ読み込み
- 構造体破棄
1. 構造体確保
画像データを扱うためのオブジェクトjpeg_decompress_struct
を確保し、jpeg_create_decompress()
で初期化する。
入力の操作はこのオブジェクト(以降cinfo)に対して行う。
また、cinfo.errメンバにエラー情報jpeg_err_mgr
を、デフォルト値で設定しておく(jpeg_std_error()
)。
struct jpeg_decompress_struct cinfo; jpeg_create_decompress(&cinfo); struct jpeg_error_mgr jerr; cinfo.err = jpeg_std_error(jerr); // 既定値
2. ファイル指定
入力のJPEGファイルをバイナリ読み込みモードでオープンし、ファイルポインタとcinfoをjpeg_stdio_src()
に渡す。
FILE fp = fopen("./input.jpg", "rb"); jpeg_stdio_src(&cinfo, fp);
3. ヘッダ読み込み
jpeg_read_header()
で画像のヘッダ部から情報を取得する。
取得できる情報は次のようなもの。
image_width
:幅image_height
:高さnum_components
:チャネル数in_color_space
:色空間J_COLOR_SPACE
型で示す(JCS_RGB、JCS_GRAYSCALE など)。
jpeg_read_header(&cinfo); int width = cinfo.image_width; // 幅 int height = cinfo.image_height; // 高さ int ch = cinfo.num_components; // チャネル数
4. データ読み込み
jpeg_start_decompress()
で画像データを取得する。
jpeg_start_decompress(&cinfo);
取得したデータはJSAMPARRAY
型で、次のような階層構造になっている。
JSAMPARRAY
:画像全体。(画像高さ)分のJSAMPROW
配列JSAMPROW
:画像列。(チャネル数)*(画像幅)分のJSAMPLE
配列JSAMPLE
:画素。基本は符号なし8bit整数
jpeg_read_scanlines()
により、画像データを一列ずつ/一括で読み込める。
// 画像データ格納する配列を動的確保 JSAMPARRAY *data = (JSAMPARRAY*)malloc(sizeof(JSAMPLE) * width * height * ch); JSAMPROW *row = data; // 列単位で読み込む for (int y = 0; y < height; y++) { jpeg_read_scanlines(&cinfo, &row, 1); row += width * ch; }
5. 構造体の破棄
読み込みが終わったら、確保した構造体を破棄する。
jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo);
読み込みフローまとめ
一連の読み込み処理をまとめると次のようになる。
// 構造体確保 struct jpeg_decompress_struct cinfo; jpeg_create_decompress(&cinfo); struct jpeg_error_mgr jerr cinfo.err = jpeg_std_error(jerr); // 入力ファイル指定 FILE fp = fopen("./input.jpg", "rb"); jpeg_stdio_src(&cinfo, fp); // ヘッダ情報取得 jpeg_read_header(&cinfo); int width = cinfo.image_width; int height = cinfo.image_height; int ch = cinfo.num_components; // データ読み込み jpeg_start_decompress(&cinfo); // 画像データ格納する配列を動的確保 JSAMPARRAY *data = (JSAMPARRAY*)malloc(sizeof(JSAMPLE) * width * height * ch); JSAMPROW *row = data; // 列単位でデータを格納 for (int y = 0; y < height; y++) { jpeg_read_scanlines(&cinfo, &row, 1); row += width * ch; } // 構造体破棄 jpeg_finish_decompress(&cinfo); jpeg_destroy_decompress(&cinfo); fclose(fp); /************************************/ // 以降、dataに対して様々な処理 /************************************/
JPEGファイルの書き出し
出力処理についてもフローは同じで、入力時の伸長(decompress)に対して圧縮(compress)となる対のデータ構造・関数を用いて処理を行います(jpeg_create_compress
など)。
ヘッダ情報の指定
入力時とは逆にcinfoのメンバに値を設定し、jpeg_set_defaults()
でその他既定値を設定します。
また、jpeg_set_quality()
で出力の画質を設定できます(0~100で大きいほど高画質、設定しないときの既定値は75)。
// 幅width、高さheightのRGB画像とする cinfo.image_width = width; cinfo.image_height = height; cinfo.input_components = 3; cinfo.in_color_space = JSC_RGB; jpeg_set_defaults(&cinfo); // 画質設定 jpeg_set_quality(&cinfo, 75, TRUE);
データ書き出し
データの書き出しは、jpeg_write_scanlines()
により入力とほぼ同じ形で実行できます。
// 適当なデータとして、すべて赤(R=255, ,G=0, B=0)を設定 JSAMPARRAY *data = (JSAMPARRAY*)malloc(sizeof(JSAMPLE) * width * height * 3); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { data[y * width + x * 3 + 0] = 255; // R data[y * width + x * 3 + 1] = 0; // G data[y * width + x * 3 + 2] = 0; // B } } // 列単位での書き込み JSAMPROW *row = data; for (int y = 0; y < height; y++) { jpeg_write_scanlines(&cinfo, &row, 1); row += width * 3; }
書き出しフローまとめ
// 構造体確保 struct jpeg_compress_struct cinfo; jpeg_create_compress(&cinfo); cinfo.err = jpeg_std_error(jerr); // 出力ファイル指定(バイナリ書き込みモード) FILE fp = fopen("./output.jpg", "wb"); jpeg_stdio_dest(&cinfo, fp); // ヘッダ情報の指定 // 幅width、高さheightが決まっているRGB画像とする cinfo.image_width = width; cinfo.image_height = height; cinfo.input_components = 3; cinfo.in_color_space = JSC_RGB; jpeg_set_defaults(&cinfo); // 画質設定 jpeg_set_quality(&cinfo, 75, TRUE); jpeg_start_compress(&cinfo, TRUE); // データ書き込み // 赤単色画像 JSAMPARRAY *data = (JSAMPARRAY*)malloc(sizeof(JSAMPLE) * width * height * 3); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { data[y * width + x * 3 + 0] = 255; // R data[y * width + x * 3 + 1] = 0; // G data[y * width + x * 3 + 2] = 0; // B } } JSAMPROW *row = data; for (int y = 0; y < height; y++) { jpeg_write_scanlines(&cinfo, &row, 1); row += width * 3; } // 構造体破棄 jpeg_finish_compress(&cinfo); jpeg_destroy_compress(&cinfo); fclose(fp);
ソースコード
大まかな流れは上記コードと同じですが、次の点で処理を変えています。
JSAMPARRAY
の代わりにuint8_t
配列を使用している- 取得した画像データを、独自の構造体
JpegData
に格納している
libjpegによる読み込み処理をread_jpeg()
、書き出し処理をwrite_jpeg()
として関数にまとめています。
また、画像処理のテストとして、読み込んだ画素をビット反転した後に出力しています。
// reverse all bits int size = jpegData.width * jpegData.height * jpegData.ch; for (int i = 0; i < size; i++) { jpegData.data[i] = ~jpegData.data[i]; }
コンパイル時には、オプションとして-ljpeg
でライブラリを指定します。
$ gcc jpegtest.c -std=c11 -ljpeg
ビルド・実行
定番のLennaを読み込み・書き出してみると、次のような画像が出力されました。
出力の確認
読み込んだデータに何も操作せず書き出すと、入出力データはほぼ同じに見えます(左:入力 "lena.jpg"、右:出力 "out.jpg")。 しかし、ヘッダ情報は微妙に異なっていました。コメントや解像度の情報を読み取り・書き出ししていないのが原因と思われます。
$ file ./lena.jpg ./lena.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 73x73, segment length 16, comment: "Handmade Software, Inc. Image Alchemy v1.9", baseline, precision 8, 512x512, frames 3
$ file ./out.jpg ./out.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 512x512, frames 3
また、ファイルサイズも異なっています。
$ ls -l ./lena.jpg ./out.jpg -rw-r--r-- 1 mts mts 91814 9月 22 23:22 ./lena.jpg -rw-r--r-- 1 mts mts 38916 9月 23 15:18 ./out.jpg
おそらく、非可逆圧縮によりサイズが縮小しているのだと思われます(画質もやや劣化しているはず)。
簡易的な入出力は簡単にできますが、使いこなすには、JPEGフォーマットに対する理解が必要ですね。
リファレンス
libjpegにリファレンスはありませんが、主な関数やデータ構造については、下記リポジトリのソースコードに付属するテキストファイル(libjpeg.txt)に記載があります。
参考
SIMD命令により高速化されている実装として、 libjpeg-turboがあります。