LoginSignup
6
2

More than 3 years have passed since last update.

Rust でマンデルブロ集合のズーム・アニメーション 再び

Last updated at Posted at 2020-07-19

はじめに

三年前に以下の記事を書いた。内容はタイトルそのまんま。
Rust でマンデルブロ集合のズーム・アニメーション - Qiita

今回の記事は,そのコードをちょこっと修正しただけ。元記事を修正しようかと思ったが,ややこしいので新たな記事を起こした。

成果物

こんな APNG アニメーションができました。

mandelbrot.png

(2020-07-20 追記)これ,APNG 画像を貼り付けのにアニメーションとして表示されません。下の小節の追記参照。

新しい記事を書いたきっかけ

きっかけは,久々に件の記事を見たところ,記事に貼り込んだ APNG アニメーション画像が動かなかったこと。どうやらいつの間にか APNG がただの PNG になっていたっぽい。
原因は分からないが,Qiita さんが PNG を最適化でもして,その際に APNG の最初のフレーム以外が消えてしまったのかも?と想像している。濡れ衣だったらごめんなさい。

(投稿直後の追記)
上の画像はやっぱり静止画として表示される。うーむ。投稿後最初の閲覧ではちゃんと動画になってたのになあ。
調べたら WebP(静止画像フォーマットの一つ)に変換されていた。なんでやねん!
とりあえず以下の URL にアクセスしてもらうと,ちゃんと APNG 動画として表示されるぽい。でもこの URL が恒久的に生きるのかどうかはよく分からない。
https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48101/10b24932-5761-5bda-7b79-0a305c15654b.png

変更点

大きな変更点としては,元記事で「よく分かんなかった」と書いた並列化に再挑戦し,実現したこと。

小さな点としては,画像サイズを一箇所で定義し,そこだけ変えればよいようにしたことなど。
ちなみに,当初 800×800 px で 200 コマの APNG を作ったらファイルサイズが大きくて Qiita に投稿できなかったので,以下のコードでは 400×400 px にしている。

コード

Cargo.toml(依存関係部分)
image = "0.23.7"
num = "0.3.0"
rayon = "1.3.1"
main.rs
use num::complex::Complex;
use num::Float;
use rayon::prelude::*;
use std::f64::consts::PI;

fn n2color(n: usize) -> image::Rgb<u8> {
    const A0: [f64; 3] = [255.0,  32.0,  64.0];
    const A1: [f64; 3] = [210.0,  10.0,  30.0];
    const B0: [f64; 3] = [255.0, 255.0, 255.0];
    const B1: [f64; 3] = [ 60.0,  20.0, 255.0];
    let v = (n as f64 * 0.02).atan() / PI * 2.0;
    let v_1 = 1.0 - v;
    let (a, b) = if n % 2 == 0 { (A0, B0) } else { (A1, B1) };
    image::Rgb([
        (v_1 * a[0] + v * b[0]) as u8,
        (v_1 * a[1] + v * b[1]) as u8,
        (v_1 * a[2] + v * b[2]) as u8,
    ])
}

fn m(c: Complex<f64>, iter_max: usize) -> usize {
    let mut z: Complex<f64> = Complex::new(0.0, 0.0);
    for i in 0..iter_max {
        z = z * z + c;
        if z.norm_sqr() > 4.0 {
            return i;
        }
    }
    iter_max
}

fn write_image(
    size: u32,
    center: Complex<f64>,
    iter_max: usize,
    palette: &Vec<image::Rgb<u8>>,
    span: f64,
    filename: &str,
) {
    let black = image::Rgb([0, 0, 0]);
    let mut imgbuf = image::ImageBuffer::new(size, size);
    let unit: f64 = span / size as f64;
    for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
        let c = center
            + Complex::new(
                (x as f64 - (size as f64 - 1.0) * 0.5) * unit,
                ((size as f64 + 1.0) * 0.5 - y as f64) * unit,
            );
        let n = m(c, iter_max);
        *pixel = if n == iter_max { black } else { palette[n] };
    }
    imgbuf.save(filename).unwrap();
}

fn main() {
    const ITER_MAX: usize = 3600;

    let center = Complex::new(-0.0938884202514072, 0.959822827920789);

    let palette: Vec<image::Rgb<u8>> = (0..ITER_MAX).map(|i| n2color(i)).collect();
    (0..200).into_par_iter().for_each(|i| {
        let s = format!("png/foo-{:04}.png", i);
        write_image(
            400,
            center,
            ITER_MAX,
            &palette,
            4.0 * 0.87.powf(i as f64),
            &s,
        )
    })
}

これだけ。
事前に png というディレクトリーを作成しておく必要がある。

rayon による並列化

このプログラムは計 200 コマの PNG を書き出すのだが,各コマの計算と画像出力は完全に独立している。ならば並列に実行すれば実行時間が減るのではあるまいか。

貧弱なマシンで CPU のコアが二つしかないが,半分とは言わないまでも,たとえば 2/3 くらいの時間で終了すればうれしいかも。

これには rayon というクレートを使った。
冒頭で

use rayon::prelude::*;

としておき,画像の計算・出力部分を

(0..200).into_par_iter().for_each(|i| {
  # なんとかかんとか
})

と書いたら動いた(ここに至るまでだいぶ試行錯誤した)。
画像が書き出されるとき,確かにファイル名の順にならなかったりするので,並列化できているぽい。

macOS の time コマンドで時間を測ったところ,並列化した場合は,ただの for でループしたときと比べて,CPU のユーザー時間は 1.5 倍くらいになったが,実時間は半分以下になった(そのときどきで時間は変動)。

え? 半分以下? コアが 2 個しかないので倍速以上にはなりえない気がするんだが???
なんかファイル I/O 関係の事情があるのかな? ←意味がよく分からずテキトーに言ってる

APNG 化

apngasm -o mandelbrot.png png/*.png -l 1 -d 1:20

とやって APNG 画像を作成している。

apngasm は macOS なら

brew install apngasm

でインストールできる。Windows とか Linux でも使える。

-o オプションは出力ファイルの名前。-l 1 は 1 回だけアニメーション表示の意。もし -l 0 とすると,0 回じゃなくて無限に繰り返す。

-d はフレームレート(というか各コマの表示時間)の指定なのだが,指定方法に二通りある。-d 1:20 のように書くと,各コマの表示時間が $\frac1{20}$ 秒になる。つまり,毎秒 20 コマだ。-d 2:3 だと $\frac23$ 秒。またコロンを使わずに -d 200 と書くと 200 ミリ秒の意になる。

感想

Ruby ばかり使っている私は,「速い計算」とか「並列処理」に憧れがある。
一方,何十年も前だが,不注意でメモリーを無茶苦茶にするプログラムを書いてしまって以来,「コンパイラこわいよコンパイラ」とトラウマになっている。
Rust は自分の足を撃つのがやりにくくなっているので,「もしや初心者にやさしい?」と思って遊んでみているが,Ruby と全然違う言語ということもあって興味深い。

並列化については,スーパー素人の私でも,「並列化に伴うオーバーヘッドがあるから CPU が 2 個あっても倍速にはならないんだぞ」ということは何となく分かっている。
なので,あんまり期待していなかったのだが,今回のコードについては面白いように高速化したので,ニンマリしている。

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