様々な画像が明滅するので気を付けよう!
ここに僕がいます
これがこうなるツールを作成しました
% git clone https://github.com/windymelt/superify.git % cd superify % cargo build --release % ./target/release/superify windymelt.png % open windymelt.png.animated.gif
めでたいですね。
ゲーミングGIFを作りたい
自粛疲れなんて言葉がありますが僕も疲れています。せめて明るい話題が欲しいもの。そんな中僕はParty Parrotが好きで、何の説明も無しにゲーミングPCめいて発光するオウムを見るとなんか面白い気分になってしまいます。
僕も画像を虹色に光らせたい。仮にゲーミングGIFと呼ぶことにしましょう。Slackの絵文字に設定すると爆笑必至、社内の人気者です。
GIMPでチマチマ作業することで作ることもできるのですが、やることは同じなので自動化したい。欲しいときに一瞬でキマったGIFを手に入れたいので、プログラムを作ることにしました。
どうすればいいか
どうすればゲーミングGIFを作れるのか考えてみます。
- 画像を読み取る
- 元画像の色相環(HSVのHue)を数°回転させた画像を生成する
- 1周するまで繰り返す
- 回転した画像を1つのアニメーションGIFにする
こういう流れでいけそうです。
実装
画像の読み込みができてGIFが出力できる言語、というかライブラリがあれば何でもよさそうですが、ひとまず最近勉強しているRustでやってみようと思いました。なんか速そうだし。
色相回転はimage
ライブラリで可能です。色相を15°回転させた画像を24回生成し一度ディスクに保存します。次に、Engiffen
という画像をアニメーションGIFに変換するライブラリを使い、結合してもらいます。fpsは24に設定し、1秒で1回転するようになります。
一度保存したのは、Engiffen
のインターフェイスの都合です。
できたコードです。
use engiffen::*; use image::imageops::*; use std::fs::File; fn main() { let args: Vec<String> = std::env::args().collect(); if args.len() < 2 { exit_with_message("Please provide image file path"); } let filename = &args[1]; let outfilename = format!("{}.animated.gif", filename); let img = match image::open(filename) { Ok(img) => img, Err(_) => return exit_with_message("Failed to open file"), }; for i in 0..24 { let img2 = colorops::huerotate(&img, 15 * i); // 24frameで360になる match img2.save(format!("out_{}.png", i)) { Ok(_) => print!("."), Err(_) => exit_with_message("Failed to save image"), } } println!(); let paths = vec![ "out_0.png", "out_1.png", "out_2.png", "out_3.png", "out_4.png", "out_5.png", "out_6.png", "out_7.png", "out_8.png", "out_9.png", "out_10.png", "out_11.png", "out_12.png", "out_13.png", "out_14.png", "out_15.png", "out_16.png", "out_17.png", "out_18.png", "out_19.png", "out_20.png", "out_21.png", "out_22.png", "out_23.png", ]; let images = engiffen::load_images(&paths); let gif = match engiffen(&images, 24, Quantizer::NeuQuant(2)) { Ok(g) => g, Err(_) => return exit_with_message("making gif failed"), }; let mut outfile = match File::create(&outfilename) { Ok(f) => f, Err(_) => return exit_with_message(&format!("failed to create {}", &outfilename)), }; match gif.write(&mut outfile) { Ok(_) => (), Err(_) => return exit_with_message(&format!("failed to write {}", &outfilename)), }; for p in paths { match std::fs::remove_file(p) { Ok(_) => (), Err(_) => return exit_with_message(&format!("failed to remove temporary file {}", p)), }; print!("."); } println!("done!"); } fn exit_with_message(message: &str) { eprintln!("*** {}", message); std::process::exit(1); }
ファイル名をベタ書きしているところがダサいのですが、まあ動くので良いかと思いました。動作はかなり速く、いまのところストレスは感じません。
工夫したところ
Rustは成功するか失敗するかわからない関数の実行結果をResult
という型で返してきます。プロトタイプでは適当に.unwrap()
を呼んで動かしていました(scalaで言うとOption.get
みたいな危険なメソッドです)が、最終的にちゃんとmatch
で分岐して処理させるようにしました。このパターンで書くのがベストなのかはよくわかりません。if
で分岐するよりはちょっと綺麗かな?という感じです。モナドがあったらいいですね。
そういえばRustのborrowまわりはここが分かりやすかったと思います。
C++のunique_ptr
から辿って説明されているのですんなり理解できました。
よかったですね
cargo build --release
した結果のバイナリサイズは5.6MiBでした。strip
したら2.2MiBにまで減りました。ご活用ください。