0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

Rust の Image を使って JPEG 画像のサムネイル化1を試してみました。

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

サムネイル化

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

Cargo.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 に限定せず、汎用的に画像をサムネイル化する場合は次のような処理で実現できます。

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

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

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

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

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

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

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

src/thumb.rs
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/21/41/8 のサイズに縮小できるという特性があります。

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

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

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

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

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
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

  1. 画像の縦横比を保ったまま縮小化

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

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

  4. 処理する画像サイズが小さくなる

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?