🖼️

RustとPythonの画像のリサイズ速度

2023/10/21に公開
3

画像をリサイズする際、Rust使ったら速いんじゃないかと書いてみたら、Pythonのほうが速かった件。

画像をリサイズするツール作る際に思いつきでRustを使ってみたら思ったより速くなかったので比較してみました。
Rustはimageクレート v0.24.7で cargo build --releaseしたもの
PythonはPIL v8.3.2
言語というより、使用するライブラリとクレートの比較になりますが、画像をリサイズして保存する速度だけを比較してます。
自分は、Rust初心者なので参考程度に

リサイズに使用するアルゴリズムにもいくつか種類があり、今回はNearestとLanczosの二つを使用します。ざっくり特徴は
Nearestは、速度は速いが品質が低い
Lanczosは、速度は遅いが品質が高い
他にも使用できるアルゴリズムがあります。↓
https://pillow.readthedocs.io/en/stable/handbook/concepts.html#filters-comparison-table
https://docs.rs/image/latest/image/imageops/enum.FilterType.html

リサイズに使う画像 600kb程度 2729x1535の写真 10枚と50枚
Nearest

結果

rust(10枚)
1.40, 0.74, 1.08, 0.59, 0.59, 0.58, 0.88, 0.59, 0.67, 1.40
python(10枚)
0.35, 0.29, 0.32, 0.33, 0.34, 0.29, 0.26, 0.29, 0.28, 0.28
rust(50枚)
3.72, 3.75, 3.49, 3.57, 3.49, 7.41, 3.68, 3.83, 3.53, 7.41
python(50枚)
2.02, 1.52, 1.60, 1.64, 1.76, 1.57, 1.53, 1.48, 1.45, 1.57

言語 平均 最大値 最小値
Rust(10枚) 0.85秒 1.40秒 0.58秒
Python(10枚) 0.303秒 0.35秒 0.26秒
Rust(50枚) 4.38秒 7.41秒 3.49秒
Python(50枚) 1.61秒 2.02秒 1.45秒

Lanczos Lnaczos3

結果

rust(10枚)
1.54, 1.73, 3.32, 1.45, 1.58, 3.50, 2.25, 2.27, 2.05, 2.02
python(10枚)
0.68, 0.71, 0.71, 0.75, 0.78, 0.69, 0.71, 0.68, 0.71, 0.77
rust(50枚)
20.96, 13.27, 13.24, 20.23, 13.91, 16.68, 13.87, 12.81, 13.80, 12.90
python(50枚)
5.68, 4.31, 4.17, 4.33, 4.71, 4.59, 4.38, 4.50, 4.17, 5.06

言語 平均 最大値 最小値
Rust(10枚) 2.17秒 3.50秒 1.54秒
Python(10枚) 0.71秒 0.78秒 0.68秒
Rust(50枚) 15.16秒 20.96秒 12.81秒
Python(50枚) 4.59秒 5.68秒 4.17秒

PythonのライブラリはCなどで書かれてるらしく、速度でますね。
気になるのはrustの結果で突然遅くなる時があること。キャッシュなどが関係してるのでしょうか?
Rustは思いつきで書いただけなので、ちゃんと書けばもっと速くなると思いますが、ちょっとしたツールをちゃちゃっと作ろうとしたら、Pythonで良さそう。

code

リサイズする画像のあるフォルダ内にrustとpythonという名前のフォルダを準備し、
実行後リサイズする画像のあるフォルダの絶対パスを渡すと、そのフォルダ内の画像を幅400にリサイズして、rustとpython それぞれのフォルダに保存する。

main.rs
use std::io;
use image;
use image::imageops::FilterType;
use std::path::PathBuf;
use std::fs::{self};
use std::time;

fn main() {
    println!("ディレクトリの場所入力");
    let mut inp = String::new();
    io::stdin().read_line(&mut inp).expect("失敗"); //入力

    let path = PathBuf::from(inp.trim().to_string());
    let save = path.to_str().to_owned().unwrap().to_string() + "\\rust\\";

    let mut list: Vec<PathBuf> = Vec::new(); //リサイズする画像のリスト

    if let Ok(files) = fs::read_dir(fs::canonicalize(&path).unwrap()) { //入力したディレクトリの内容
        for file in files {
            if let Ok(file) = file {
                //PathBufにする
                let path_buf = file.path();
                //ディレクトリじゃなかったら追加
                if !path_buf.is_dir() {
                    list.push(path_buf.clone());
                }
            }
        }
        let now = time::Instant::now(); //測定開始
        for pic in list {

            let img = image::open(pic.clone()).unwrap();

            let nwidth: u32 = 400; //横幅400
            let nheight: u32 = img.height();
	    let resized_img = img.resize(nwidth, nheight, FilterType::Nearest); //nearest
            //let resized_img = img.resize(nwidth, nheight, FilterType::Lanczos3);//lanczos3
            let save_path = save.to_string() + pic.file_name().unwrap().to_str().unwrap();

            resized_img.save(save_path).unwrap();
        }
        println!("実行時間: {:?}", now.elapsed());
    }

    io::stdin().read_line(&mut inp).expect("失敗"); //コンソール閉じない用
}
//Cargo.toml
//~
//[dependencies]
//image="0.24.7"
main.py
from pathlib import Path
import time
from PIL import Image

print("ディレクトリの場所入力")
inp = input()

dirpath = Path(inp)
save = str(dirpath) + "\\python\\"

list = []

for file in dirpath.glob("*"): #入力したディレクトリの中身
    
    if not file.is_dir():
        list.append(file)

time_str = time.time() #測定開始
for i in list:
    new_img = Image.open(i)
    ratio = round(400 / new_img.width, 2)
    new_img = new_img.resize((400, int(new_img.height * ratio)), Image.NEAREST) #nearest
    # new_img = new_img.resize((400, int(new_img.height * ratio)), Image.LANCZOS) #lanczos
    new_img_path = save + str(i.name)

    new_img.save(new_img_path)

time_end = time.time()
print("実行時間" + str(round(time_end - time_str, 4)) + "秒")

Discussion

sepisepi

コメントするなら自分でやれやという話なのですが気になったのでコメントさせていただきます
どうやらrustのコードではlanczosアルゴリズムを用いてresizeを用いているようですが
PILはデフォルトでnearestを用いるようです

new_img = new_img.resize((400, int(new_img.height * ratio)), Image.LANCZOS)

このようにするとPILでもlanczosを用いるようにできるようです

だよだよ

実際にNearestとLanczosで、それぞれちゃんと指定してやり直してみたところ、

50枚リサイズ平均 Nearest Lanczos
PIL(python) 1.6秒 4.74秒
Image(rust) 3.74秒 11.90秒
python
new_img = new_img.resize((400, int(new_img.height * ratio)), Image.NEAREST) #nearest
new_img = new_img.resize((400, int(new_img.height * ratio)), Image.LANCZOS) #lanczos
rust
let resized_img = img.resize(nwidth, nheight, FilterType::Nearest); //nearest
let resized_img = img.resize(nwidth, nheight, FilterType::Lanczos3); //lanczos3

とそれぞれ全然違う結果が出ました。
恥ずかしながら、リサイズ時のアルゴリズムという視点が抜けており、誤った検証方法で検証していました。ご指摘を元に記事を直したいとともに、自分も速度に疑問を持っていたのでsepiさんのコメントでその理由に気づくことができました。コメントありがとうございます!

sepisepi

同じアルゴリズムでPythonが実際にはCで書かれたバイナリを用いていたとしてもここまで差が出るものなのですね...
やはり使用者が多いと最適化が進むということでしょうか
再検証ありがとうございました!