気が向いたら書くやつ

気が向いたら何か書きます

libjpegによるC言語での画像入出力

C言語で画像を処理してみたいと思い、JPEG画像のエンコード/デコード実装として使われているライブラリ、libjpegを試してみました。

2019/3/7:内容を整理。

実行環境

  • Ubuntu 18.04 LTS 64bit
  • gcc 7.3.0
  • libjpeg version 8c of 16-Jan-2011

インストール

$ sudo apt install libjpeg-dev 

執筆時点(2018/9/23)での最新リリースバージョンは 9c of 14-Jan-2018のようです。

Independent JPEG Group

JPEGファイルの読み込み

フロー

JPEGファイルの読み込みは次の流れとなっています。

  1. 構造体確保
  2. ファイル指定
  3. ヘッダ読み込み
  4. データ読み込み
  5. 構造体破棄

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を読み込み・書き出してみると、次のような画像が出力されました。

f:id:soratobi96:20180923014145j:plain

出力の確認

読み込んだデータに何も操作せず書き出すと、入出力データはほぼ同じに見えます(左:入力 "lena.jpg"、右:出力 "out.jpg")。 しかし、ヘッダ情報は微妙に異なっていました。コメントや解像度の情報を読み取り・書き出ししていないのが原因と思われます。

f:id:soratobi96:20180923152253p:plain

$ 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  922 23:22 ./lena.jpg
-rw-r--r-- 1 mts mts 38916  923 15:18 ./out.jpg

おそらく、非可逆圧縮によりサイズが縮小しているのだと思われます(画質もやや劣化しているはず)。

簡易的な入出力は簡単にできますが、使いこなすには、JPEGフォーマットに対する理解が必要ですね。

リファレンス

libjpegにリファレンスはありませんが、主な関数やデータ構造については、下記リポジトリソースコードに付属するテキストファイル(libjpeg.txt)に記載があります。

github.com

参考

d.hatena.ne.jp

daeudaeu.com

SIMD命令により高速化されている実装として、 libjpeg-turboがあります。

libjpeg-turbo | Main / libjpeg-turbo