Rustのwebpクレートでwebp動画を生成する

Rustでwebp形式の画像を生成するメモ。アニメーションでも生成可能。

すること

  • 静止画のwebpファイルを作成してみる
  • アニメーションするwebpファイルを作成してみる

画像は正弦波を使うことにした。動いたときに分かりやすいかなと思った。

結果としてはどちらもうまくいった。ここではその記録を残す。

webpの仕様はこちら:

WebP Container Specification  |  Google for Developers

使うもの

webpクレートを使って、webpアニメーションを作るのが目標。webpクレートについてはこちらを参照:

https://crates.io/crates/webp

それぞれのフレーム・静止画は、imageクレートを使って用意することにした:

https://crates.io/crates/image

まずは静止画

解像度依存をなくすため座標を-1から+1の範囲のf32に変換する関数を使う。また、変換した座標に対するRGB値を関数で決めている。それぞれの関数の中身は後述。

webp生成はEncoderの関数名にencodeを含む関数を呼び出せばいい。encode_simple()を使うとロスレスか否かと品質(0-100)の2つを指定できる。

動画の場合もそうだが、最終的なバイナリはWebPMemoryという構造体に保持されているので、それを使ってfsを通してファイルに書き込む。

// 静止画描画の例。useは省略
fn main() {
    println!("hello");
    let (width, height) = (800, 800);
    let mut buf = image::ImageBuffer::new(width, height);
    for (x, y, p) in buf.enumerate_pixels_mut() {
        // これらの関数は後述
        let (uvx, uvy) = xy_to_uv(x, y, width, height);
        *p = uv_to_rgba(uvx, uvy, 1.57);
    }
    // imageクレートだけ使ってpngファイルを作成するならこれで十分
    // buf.save("z://sin.png").unwrap();

    let binding = buf.into_vec();
    let encoder = Encoder::from_rgba(&binding, width, height);
    let res = encoder.encode_lossless();
    // この関数も後述
    write_webp_file(res, "z://sin.webp").unwrap();
}

上のコードでは正弦波の位相を1.57radだけずらしているが、残っていた静止画は位相をずらしていないものだったので、これをwebp静止画の出力例として載せておく:

sin波を表示するwebp静止画

次に動画

アニメーションにはwebpクレートのAnimEncoderAnimFrameが必要だ。 フレームの画像を保持しつつ、タイムスタンプを適切に指定して、エンコードする。

タイムスタンプについては、webpの仕様を見る限りはDurationを指定することになっているが、webpエンコードの実装のlibwebpではタイムスタンプを利用してDurationを算出している。 そして、webpクレートは内部でlibwebpをFFIで使っているようなので、タイムスタンプで指定するようになっている。libwebpの該当実装箇所はこのあたり:

libwebp/src/mux/anim_encode.c at main · webmproject/libwebp
Mirror only. Please do not send pull requests. See - webmproject/libwebp

よって、あるフレームのタイムスタンプはその手前のフレームのタイムスタンプとの差の分だけの間表示されているということになる。1つ前のフレームのタイムスタンプとの差だけでDurationを決定するので、1つ目のフレームのタイムスタンプは0でなくても問題はないと思う(未実験)。

その辺だけ注意して実装すれば問題はないと思う。間違えてすべてのフレームのタイムスタンプを同じ値にしても、表示ソフトは最小Durationを設定している(webpの仕様書に書いてあった)ので狙ったタイミングではなくなるが、アニメーションは行われる。

webp動画エンコードのコード例は以下:

use std::{f32::consts::PI, fs::File, io::Write, path::Path};

use image::Rgba;
use webp::{AnimEncoder, AnimFrame, WebPConfig, WebPMemory};

type IOResult = std::io::Result<()>;

/// 1ループの合計描画枚数
const NUMBER_OF_FRAMES: usize = 120;
/// 1フレームあたりの表示時間(ミリ秒)
const DURATION_OF_A_FRAME: i32 = 16;
// 上2つの積がアニメーションの1ループの時間になる

fn main() {
    example_anim();
}

fn xy_to_uv(x: u32, y: u32, w: u32, h: u32) -> (f32, f32) {
    let uvx = (x as f32 * 2.0 - w as f32) / w as f32;
    let uvy = (y as f32 * 2.0 - h as f32) / h as f32;
    (uvx, uvy)
}

fn uv_to_rgba(uvx: f32, uvy: f32, t: f32) -> Rgba<u8> {
    if (uvy - (uvx + t).sin()).abs() < 0.0061 {
        let color_v = ((uvx + t).sin() + 1.0) / 2.0;
        let color_v = (color_v * 255.0) as u8;
        return Rgba([color_v, color_v / 2, color_v / 3, 255]);
    } else {
        return Rgba([0, 0, 0, 255]);
    }
}

fn write_webp_file<S: AsRef<Path>>(webpm: WebPMemory, filepath: S) -> IOResult {
    let mut file = File::create(filepath)?;
    file.write_all(&webpm)?;
    Ok(())
}

fn example_anim() {
    let (width, height) = (800, 800);
    let mut config = WebPConfig::new().unwrap();
    // 各フレームはこのconfigを変更して品質やロスレスフラグなどを変えられる
    config.quality = 90.0;
    let mut animator = Animator::new(width, height, &config);
    let mut imgs = vec![];
    // 1秒分のフレームを作る
    let length = NUMBER_OF_FRAMES;
    for i in 0..length {
        let t = 2.0 * PI * i as f32 / NUMBER_OF_FRAMES as f32;
        let mut buf = image::ImageBuffer::new(width, height);
        // 位相をtだけずらしたsinカーブの画像を生成
        for (x, y, p) in buf.enumerate_pixels_mut() {
            let (uvx, uvy) = xy_to_uv(x, y, width, height);
            *p = uv_to_rgba(uvx, uvy, t);
        }
        let v = buf.into_vec();
        imgs.push(v);
    }
    let mut elapsed_time_ms = 0;
    // animatorに追加
    for i in 0..length {
        animator.add_frame(&imgs[i as usize], elapsed_time_ms, width, height);
        // 次に使うタイムスタンプを設定。描画時間分だけ増加させるのがいい
        elapsed_time_ms += DURATION_OF_A_FRAME;
    }
    // animatorからwebpを生成し、保存する
    let webpm = animator.webp();
    write_webp_file(webpm, "z://example_sin_anim.webp").unwrap();
}

// エンコーダのラッパ
struct Animator<'a> {
    encoder: AnimEncoder<'a>,
}

impl<'a> Animator<'a> {
    fn new(width: u32, height: u32, config: &'a WebPConfig) -> Self {
        let encoder = AnimEncoder::new(width, height, config);
        Self { encoder }
    }
    fn add_frame(&mut self, img: &'a Vec<u8>, timestamp: i32, width: u32, height: u32) {
        let frame = AnimFrame::from_rgba(&img, width, height, timestamp);
        self.encoder.add_frame(frame);
    }
    fn webp(&self) -> WebPMemory {
        self.encoder.encode()
    }
}

2秒間で60fpsのwebp動画を作成する。

出力されるwebp動画はこちらのようになる:

sin波を表示するwebp動画

おわり

今回はwebpに格納するためのメモとして書いた。誰かの役に立つかもしくは自分があとから見てわかることを願う。

以上です。

タイトルとURLをコピーしました