RustとPythonの画像のリサイズ速度
画像をリサイズする際、Rust使ったら速いんじゃないかと書いてみたら、Pythonのほうが速かった件。
画像をリサイズするツール作る際に思いつきでRustを使ってみたら思ったより速くなかったので比較してみました。
Rustはimageクレート v0.24.7で cargo build --releaseしたもの
PythonはPIL v8.3.2
言語というより、使用するライブラリとクレートの比較になりますが、画像をリサイズして保存する速度だけを比較してます。
自分は、Rust初心者なので参考程度に
リサイズに使用するアルゴリズムにもいくつか種類があり、今回はNearestとLanczosの二つを使用します。ざっくり特徴は
Nearestは、速度は速いが品質が低い
Lanczosは、速度は遅いが品質が高い
他にも使用できるアルゴリズムがあります。↓
リサイズに使う画像 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 それぞれのフォルダに保存する。
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"
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
コメントするなら自分でやれやという話なのですが気になったのでコメントさせていただきます
どうやらrustのコードではlanczosアルゴリズムを用いてresizeを用いているようですが
PILはデフォルトでnearestを用いるようです
このようにするとPILでもlanczosを用いるようにできるようです
実際にNearestとLanczosで、それぞれちゃんと指定してやり直してみたところ、
とそれぞれ全然違う結果が出ました。
恥ずかしながら、リサイズ時のアルゴリズムという視点が抜けており、誤った検証方法で検証していました。ご指摘を元に記事を直したいとともに、自分も速度に疑問を持っていたのでsepiさんのコメントでその理由に気づくことができました。コメントありがとうございます!
同じアルゴリズムでPythonが実際にはCで書かれたバイナリを用いていたとしてもここまで差が出るものなのですね...
やはり使用者が多いと最適化が進むということでしょうか
再検証ありがとうございました!