Rust で JPEG 画像をサムネイル化

Rust の [Image](https://github.com/image-rs/image) を使って JPEG 画像のサムネイル化[^1]を試してみました。

[^1]: 画像の縦横比を保ったまま縮小化

##### Cargo.toml 例
```toml
[dependencies]
image = "0.24"
```

# サムネイル化

JPEG に限定しない画像のサムネイル化（```thumb```）と JPEG に限定したもの（```thumb_jpeg```）をそれぞれ実装し、処理時間を比較してみます。

##### Cargo.toml
```toml
[package]
name = "thumbnail_sample"
version = "0.1.0"
edition = "2021"

[dependencies]
image = "0.24"

[[bin]]
name = "thumb"
path = "src/thumb.rs"

[[bin]]
name = "thumb_jpeg"
path = "src/thumb_jpeg.rs"
```

## (a) 汎用版 - thumb

JPEG に限定せず、汎用的に画像をサムネイル化する場合は次のような処理で実現できます。

```rust
// 読み込み（デコード）
let img = image::open(input_file)?;

// サムネイル化
let img_n = img.thumbnail(width, height);

// 書き込み（エンコード）
img_n.save(output_file)?;
```

出力画像のフォーマットは ```save``` で指定したファイルの拡張子によって決定されます。

```thumbnail``` では幅と高さを指定するようになっていますが、任意のサイズになるわけではなく、
元の画像の比率を保ったまま、より小さくなるように調整されるようです。[^2]

[^2]: 画像サイズが小さくなるように幅か高さのどちらかが使われ、もう一方の値は調整されます

JPEG 出力時の quality はデフォルトで 75 となっているようです。[^3]

[^3]: https://github.com/image-rs/image/blob/master/src/image.rs 参照

単純な処理時間の出力を加えて、次のようにしてみました。

##### src/thumb.rs
```rust
use image::ImageResult;

use std::env;
use std::time::Instant;

fn to_u32(v: String) -> Option<u32> {
    v.parse().ok()
}

fn main() -> ImageResult<()> {
    let mut args = env::args().skip(1);

    let i_file = args.next().unwrap();
    let w = args.next().and_then(to_u32).unwrap();
    let h = args.next().and_then(to_u32).unwrap();
    let o_file = args.next().unwrap();

    let t = Instant::now();

    let img = image::open(i_file)?;

    let p1 = t.elapsed().as_millis();
    println!("read & decode: {} ms", p1);
    // サムネイル化
    let img_n = img.thumbnail(w, h);

    let p2 = t.elapsed().as_millis();
    println!("thumbnail: {} ms", p2 - p1);

    img_n.save(o_file)?;

    let p3 = t.elapsed().as_millis();
    println!("encode & write: {} ms", p3 - p2);
    println!("total: {} ms", p3);

    Ok(())
}
```

## (b) JPEG 固定版 - thumb_jpeg

JPEG は 8x8 サイズのブロック単位でエンコードしており、デコード時に ```1/2```・```1/4```・```1/8``` のサイズに縮小できるという特性があります。

scaling decode と呼ぶみたいですが、ImageMagick で JPEG サイズヒント（```-define jpeg:size```）を付けると処理時間が短縮したりするのがこれです。

そのため、JPEG を ```1/2``` スケール以下にサムネイル化する場合、次のようにする方が効率的[^4]です。

* (1) 1/2 か 1/4 か 1/8 の適切なスケールへ縮小してデコード
* (2) サムネイル（縮小）化
* (3) エンコード

[^4]: 処理する画像サイズが小さくなる

```JpegDecoder::scale``` を使うと (1) を実施してくれるようなので、scaling decode を適用するには次のようにすれば良さそうです。

```rust
let reader = BufReader::new(File::open(input_file)?);
let mut dec = JpegDecoder::new(reader)?;

// スケールの設定
dec.scale(width as u16, height as u16)?;

// (1) デコード
let img = DynamicImage::from_decoder(dec)?;

// (2) サムネイル化
let img_n = img.thumbnail(width, height);

// (3) 書き込み（エンコード）
img_n.save(output_file)?;
```

このようにデコード後の処理は (a) と同じでもよかったのですが、下記コードでは出力も JPEG 固定にしてみました。

```JpegEncoder::new``` の代わりに ```JpegEncoder::new_with_quality``` を使うと、JPEG 出力時の quality を変更できます。

##### src/thumb_jpeg.rs
```rust
use image::{ DynamicImage, ImageResult };
use image::codecs::jpeg::{ JpegDecoder, JpegEncoder };

use std::env;
use std::fs::File;
use std::io::{ BufReader, BufWriter };
use std::time::Instant;

...省略

fn main() -> ImageResult<()> {
    let mut args = env::args().skip(1);

    let i_file = args.next().unwrap();
    let w = args.next().and_then(to_u32).unwrap();
    let h = args.next().and_then(to_u32).unwrap();
    let o_file = args.next().unwrap();

    let t = Instant::now();

    let reader = BufReader::new(File::open(i_file)?);
    let mut dec = JpegDecoder::new(reader)?;

    dec.scale(w as u16, h as u16)?;

    let img = DynamicImage::from_decoder(dec)?;

    let p1 = t.elapsed().as_millis();
    println!("read & decode: {} ms", p1);

    let img_n = img.thumbnail(w, h);

    let p2 = t.elapsed().as_millis();
    println!("thumbnail: {} ms", p2 - p1);

    let writer = BufWriter::new(File::create(o_file)?);
    let mut enc = JpegEncoder::new(writer);

    // エンコード（ファイルへの書き込み）
    enc.encode(img_n.as_bytes(), img_n.width(), img_n.height(), img_n.color())?;

    let p3 = t.elapsed().as_millis();
    println!("encode & write: {} ms", p3 - p2);
    println!("total: {} ms", p3);

    Ok(())
}
```

## 処理時間の比較

(a) と (b) でどの程度の差が出るのか 2種類の JPEG 画像で試してみました。

|ファイル名|画像サイズ|ファイルサイズ|
|---|---|---|
|01.jpg|1200x800|865 KB|
|02.jpg|5180x3663|2.4 MB|

release ビルドしておきます。

##### ビルド例
```
% cargo build --release
```

### 01.jpg サムネイル化 - 360x240

まずは 01.jpg を 360x240 サイズにサムネイル化してみます。

##### (a) thumb
```
% target/release/thumb 01.jpg 360 240 01_360_a.jpg
read & decode: 32 ms
thumbnail: 2 ms
encode & write: 2 ms
total: 36 ms
```

##### (b) thumb_jpeg
```
% target/release/thumb_jpeg 01.jpg 360 240 01_360_b.jpg
read & decode: 27 ms
thumbnail: 1 ms
encode & write: 2 ms
total: 30 ms
```

あまり大きな差は見られませんが、(b) の方が多少速くなっています。
何度か試してみましたが、概ね同じ結果でした。

### 02.jpg サムネイル化 - 600x424

次に 02.jpg を 600x424 サイズにサムネイル化してみます。

##### (a) thumb
```
% target/release/thumb 02.jpg 600 424 02_600_a.jpg
read & decode: 230 ms
thumbnail: 21 ms
encode & write: 3 ms
total: 254 ms
```

##### (b) thumb_jpeg
```
% target/release/thumb_jpeg 02.jpg 600 424 02_600_b.jpg
read & decode: 183 ms
thumbnail: 1 ms
encode & write: 3 ms
total: 187 ms
```

(b) の方が確実に速くなりました。
thumbnail の処理時間に明確な違いが出ており、scaling decode の効果が出ていると考えられます。

### 結果

結果をまとめると次のようになりました。

|実行ファイル|出力ファイル名|画像サイズ|ファイルサイズ|read & decode|thumbnail|encode & write|total|
|---|---|---|---|---|---|---|---|
|(a) thumb|01_360_a.jpg|360x240|19,957 B|32 ms|2 ms|2 ms|36 ms|
|(b) thumb_jpeg|01_360_b.jpg|360x240|20,441 B|27 ms|1 ms|2 ms|30 ms|
|(a) thumb|02_600_a.jpg|600x424|37,286 B|230 ms|21 ms|3 ms|254 ms|
|(b) thumb_jpeg|02_600_b.jpg|600x424|36,542 B|183 ms|1 ms|3 ms|187 ms|

ソースコードは https://github.com/fits/try_samples/tree/master/blog/20230123/rust_img_thumb