先日、姉妹連載でマンデルブロ集合を描画するプログラムを作成したのですが、今回はこれを一歩進めてRustで大量の画像を生成して動画を作ってみましょう。大量の画像処理ならRustの出番です。Rustで高速にマンデルブロ集合の計算を行って動画を生成しましょう。

  • 大量の画像を生成してマンデルブロ集合の動画を作成しよう

    大量の画像を生成してマンデルブロ集合の動画を作成しよう

Rustでマンデルブロ集合を使って動画を作成しよう

繰り返し拡大しても次々とフラクタルな図形が描画される「マンデルブロ集合」はとても面白い題材です。マンデルブロ集合のある部分を少しずつ拡大することで次々と異なる美しい図形を描画できます。見た目が面白いだけでなく、画像処理のためのプログラムを作る練習にもなります。

今回は、ある座標を中心にして連続で拡大する画像を生成し、それを結合して次のような動画を作成してみましょう。

  • マンデルブロ集合を拡大して動画を作ろう

ちなみに、今回のプログラムを使って30秒の動画を3本作成し、それをつなげたのが、こちらの動画です。

マンデルブロ集合とは?

最初に、マンデルブロ集合について考察しておきましょう。マンデルブロ集合の画像とは、次のような複素数の漸化式を使って作るフラクタル図形です。この式のz0, z1, z2…とcは全て複素数です。

  • マンデルブロ集合を拡大して動画を作ろう

    マンデルブロ集合の漸化式

なお、漸化式とは、前の計算結果を使って次の値を求めるという繰り返しの計算です。それで、このcの値を変更した計算結果が無限に大きくなっていくなら、その数はマンデルブロ集合に含まれません。これを発散すると呼びます。一方、計算の繰り返しが一定の範囲内に収まる場合、その数はマンデルブロ集合に含まれ、これを収束すると呼びます。

マンデルブロ集合の画像を描く際には、収束する回数に基づいて色を決定するのです。この漸化式の一部を繰り返し拡大すると特徴的な美しい図形が現れます。

また、冒頭で紹介したように、JavaScriptの連載32回目でもマンデルブロ集合について詳しく紹介しています。今回のプログラムは、JavaScriptのプログラムと見比べながら読むとより楽しめるものにしました。

プロジェクトを作ろう

それでは、最初に簡単なマンデルブロ集合の画像を作成するプログラムを作ってみましょう。ターミナル(WindowsならPowerShell、macOSならターミナル.app)を起動して下記のコマンドを実行してプロジェクトを作成しましょう。

# プロジェクトフォルダを作成
mkdir mandelbrot && cd mandelbrot
# プロジェクトの初期化
cargo init
# クレートをインストール
cargo add image
cargo add colorsys

ここでインストールするクレートは、PNG画像を保存するためにimageクレート、色空間をHSLからRGBへ変換するために、colorsysクレートです。

上記のコマンドを実行すると、次のようなディレクトリ構成でファイルが生成されます。

.
├── Cargo.lock
├── Cargo.toml
└── src
    └── main.rs

基本的なマンデルブロ集合の図形を描画するプログラム

そして、src/main.rsを次のように書き換えましょう。以下のプログラムが、マンデルブロ集合を描画してPNGファイルに保存するプログラムです。

use image::{ImageBuffer, Rgb};
use colorsys::Hsl;

// マンデルブロ集合を描画して保存する --- (*1)
fn main() {
    let (width, height) = (800, 800);
    draw(width, height, -1.0, 0.0, 3.0, "manderbrot.png");
}
// マンデルブロ集合の計算 --- (*2)
fn mandelbrot(c: (f64, f64), max_iter: u32) -> u32 {
    let mut z = (0.0, 0.0);
    let mut n = 0; // 繰り返し回数
    while n < max_iter {
        if z.0 * z.0 + z.1 * z.1 > 4.0 { break; }
        z = (z.0 * z.0 - z.1 * z.1 + c.0, 2.0 * z.0 * z.1 + c.1);
        n += 1;
    }
    n
}
// imageにマンデルブロ集合を描画してファイルに保存する関数 --- (*3)
fn draw(imgx: u32, imgy: u32, cx: f64, cy: f64, zoom: f64, filename: &str) {
    let max_iter = 1000; // 最大繰り返し回数を指定 --- (*4)
    // 切り取る範囲を計算
    let (left, top) = (cx - zoom / 2.0, cy - zoom / 2.0);
    let (right, bottom) = (left + zoom, top + zoom);
    let scale_x = (right - left) / imgx as f64;
    let scale_y = (bottom - top) / imgy as f64;
    // 画像をメモリ上に作成して、画像を描画する --- (*5)
    let mut imgbuf = ImageBuffer::new(imgx, imgy);
    for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
        let cx = left + x as f64 * scale_x;
        let cy = top + y as f64 * scale_y;
        // マンデルブロ集合の計算 --- (*6)
        let i = mandelbrot((cx as f64, cy as f64), max_iter);
        // iの値に応じて色を指定する --- (*7)
        *pixel = if i == max_iter { Rgb([255, 255, 255]) } else {
            let cc = i as f64 / max_iter as f64;
            // 色を指定(HSLからRGBに変換)
            let rgb = colorsys::Rgb::from(&Hsl::new(cc * 360.0, 100.0, 50.0, None));
            Rgb([rgb.red() as u8, rgb.green() as u8, rgb.blue() as u8])
        };
    }
    // 画像を保存 --- (*7)
    imgbuf.save(filename).unwrap();
}

プログラムを確認してみましょう。(*1)はmain関数です。Rustでは最初にこの関数が実行されます。ここでは、(*3)で定義しているdraw関数を呼び出し、描画結果をファイル「manderbrot.png」へ保存します。

(*2)ではマンデルブロ集合の計算を行います。二次元の画像に対して描画を行うため、変数cを(f64, f64)型のタプルで指定します。また漸化式が発散するかどうかを確かめるために、最大繰り返し回数(max_iter)も指定します。変数nで繰り返し回数を指定します。

(*3)の関数drawでは、引数に画像サイズ(imgx, umgy)と、描画中心点(cx, cy)と、拡大率zoom、保存ファイル名filenameを指定します。(*4)では発散するかどうかを確かめる最大繰り返し回数max_iterを指定します。なお、この値を小さくすると計算回数が減るので描画速度が速くなりますが生成される色の変化が荒くなります。

(*5)以降の部分では画像をメモリに作成し、座標(x, y)に対してどの色を描画するかを繰り返し計算します。(*6)でマンデルブロ集合の計算をして、収束回数を取得して、どの色で描画するのかを決定します。ここでは、一般的な光の三原色であるRGBではなく、HSL色空間(色相/彩度/明度)を指定して描画する色を決定します。そして、(*7)では描画済みの画像をファイルに保存します。

プログラムを実行しよう

作成したプログラムを実行するには、ターミナルで次のコマンドを実行します。すると、マンデルブロ集合の画像が保存されます。

cargo run

次のような画像が生成されることでしょう。

  • マンデルブロ集合を描画したところ

    マンデルブロ集合を描画したところ

ここで、関数drawの引数に注目してみてください。このパラメータで、描画位置を指定しています。例えば、プログラム(*1)にあるdraw関数を次のように書き変えてみると、次のような画像が描画されます。

let (width, height) = (800, 800);
draw(width, height, 0.0578099, 0.6565466, 0.001, "zoom1.png");

同じく「cargo run」コマンドを実行すると「zoom1.png」という画像が作成されます。

  • パラメータを変えることで描画される画像が変化する

    パラメータを変えることで描画される画像が変化する

動画を生成しよう

それでは、動画を生成してみましょう。上記のプログラムのdraw関数の引数zoomを少しずつ変更することで、大量の画像を生成するようにするのです。main関数を下記のように書き換えて実行することで大量の画像を生成できます。

let fps = 24; // フレームレートを指定
let image_count = fps * 30; // 30秒の画像を作るために必要な枚数を計算
let mut zoom = 4.0; // zoomの初期値を指定
for i in 0..image_count { // 繰り返し画像を生成
    println!("{:04}/{} - m{:04}.png - zoom*{:.8}", i, image_count, i, zoom);
    let fname = format!("m{:04}.png", i);
    draw(sx, sy, cx, cy, zoom, &fname);
    zoom *= 0.95; // ズームの値を小さくする
}

なお、せっかく動画にするので、正方形の画像ではなく、HD画像(解像度1280×720)の画像を生成するように修正してみました。修正したプログラムをこちらにアップロードしました。実行すると、続々と画像が書き出されます。

  • 大量の画像を書き出したところ

    大量の画像を書き出したところ

なお、特に最適化もしていないこともあり、筆者のPC(Macbook M1)では30秒の動画を描き出すのに36分掛かりました。もし、もっと高速に動作させたい場合には、プログラム中の関数drawの冒頭で設定している「let max_iter = 1000;」を「let max_iter = 100;」などのように小さな値にすると速くなります。

動画の書き出しを行おう

Rustのプログラムによって大量の画像の書き出しが完了したら、これを動画に変換しましょう。動画への変換はFFmpegを利用すると簡単です。FFmpegのインストール方法は、こちらで詳しく解説しています。

これを使って動画を作成するには、ターミナルで下記のコマンドを実行します。m001.png, m002.png, m003.png…という連番画像のあるディレクトリに移動します。すると、out.mp4という動画ファイルを生成します。

この記事は
Members+会員の方のみ御覧いただけます

ログイン/無料会員登録

会員サービスの詳細はこちら