MoTLab -Mobility Technologies Engineering Blog-MoTLab -Mobility Technologies Engineering Blog-

RustのドローイングライブラリPlottersの紹介

AIRust
August 04, 2020

はじめまして、AI技術開発部の加藤(@_tkato_)です。

私たちはエッジ x クラウドの機械学習システムのプロダクション開発を行う上で、Rustを開発言語の一つとして利用しています。今後はこのブログを使って、私たちがRustで開発している際に得た知見を共有していきたいと思います。

本内容は7/27に行われたRust LT Online #1で発表した内容です。興味のある方は以下のスライドも合わせてご覧ください。

An image from Notion

Introduction of Plotters - Google スライド

今回はPlottersの紹介です。PlottersはRustで簡単に図形やグラフを描画するために便利なドローイングライブラリです。

同様にRustで可視化ができるcrateとして、opencv-rustPlotly.rsなどがありますが、特にPlottersはpure Rustで簡単にインストールして組み込める点や、WebAssembly含めて複数のBackendに拡張していける点などが優れています。それぞれ長所短所があるので用途に応じて使い分けるのが良いと考えています。

私たちは、Rust側の計算結果をjsonやprotobufs等にシリアライズし、Pythonなど他の言語で可視化したこともありましたが、このやり方ではCIパイプライン等での自動化が複雑になりやすいです。そのため、Plottersのようなcrateを利用してRustの cargo test の実行だけでテストから可視化までを自動化できるのであれば、それが単純で望ましいと考えています。

Plottersとは

38/plotters: A rust drawing library for high quality data plotting for both WASM and native, statically and realtimely 🦀 📈🚀

Plottersは、グラフなどを描画するためのcrate(Rustのライブラリのこと)です。

下図のような、様々な種類のグラフやアルゴリズムの結果の可視化ができます。

また、複数のBackend(描画先)に対応しており、画像ファイル(png, gif, svgなど)だけでなくGTKやターミナルへの描画や、WebAssemblyにビルドしてHTMLのCanvasに対して描画することもできます。

An image from Notion

PlottersのExample: https://github.com/38/plotters より引用

基本的な使い方

簡単な例でPlottersのAPIを理解してみましょう。本記事で利用するPlottersのバージョンは0.2.15です。

ここでは、以下のグラフを描画するためのコード例を示しています。

An image from Notion

ChartContextのAPIで描いたy=x^2のグラフ

#[test]
fn chart_context() {
   // 描画先をBackendとして指定。ここでは画像に出力するためBitMapBackend
   let root = BitMapBackend::new("chart.png", (640, 480)).into_drawing_area();
   root.fill(&WHITE).unwrap();

   // グラフの軸の設定など
   let mut chart = ChartBuilder::on(&root)
       .caption("y=x^2", ("sans-serif", 50).into_font())
       .margin(10)
       .x_label_area_size(30)
       .y_label_area_size(30)
       .build_ranged(-1f32..1f32, -0.1f32..1f32).unwrap();

   chart.configure_mesh().draw().unwrap();

   // データの描画。(x, y)のイテレータとしてデータ点を渡す
   chart.draw_series(LineSeries::new(
       (-50..50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)),
       &RED,
   )).unwrap();
}

基本的にはこれだけです。

下図のようにPlottersのAPIは3層に分かれています。上記の例では、ChartContextのAPIでハイレベルにグラフを描画しました。

ユーザーはどの層のAPIでも直接描画でき、組み合わせて利用することもできます。例えば、ChartContext APIでグラフを描画し、その上にDrawingArea APIで図形を重ねる、といったこともできます。

An image from Notion

Plottersの主要なAPIの説明

中間のDrawingArea APIでは、レイアウトやElementと呼ぶ抽象化された図形オブジェクトの描画ができます。

An image from Notion

DrawingAreaのAPIで描いた図形

#[test]
fn drawing_area() {
   let (w, h) = (640, 480);
   let root = BitMapBackend::new("drawing-area.png", (w, h)).into_drawing_area();

   let (top, bottom) = root.split_vertically(h / 2);
   let (bottom_left, bottom_right) = bottom.split_horizontally(w / 2);

   top.fill(&MAGENTA).unwrap();
   top.draw(&Circle::new((500, 100), 80, ShapeStyle::from(&WHITE).filled())).unwrap();

   bottom_left.fill(&BLUE).unwrap();
   bottom_left.draw(&Rectangle::new([(100, 100), (250, 200)],
                                    ShapeStyle::from(&WHITE).filled())).unwrap();

   bottom_right.fill(&YELLOW).unwrap();
   let data = vec![(50, 50), (250, 50), (150, 250)];
   bottom_right.draw(&Polygon::new(data, ShapeStyle::from(&WHITE).filled())).unwrap();
}

最下層のDrawingBackend APIでは、ピクセルレベルや単純な図形の描画をサポートしています。

An image from Notion

DrawingBackendのAPIで描いた図形

#[test]
fn backend() {
   let mut backend = BitMapBackend::new("backend.png", (640, 480));

   backend.draw_circle((100, 100), 100, &BLUE, true).unwrap();
   backend.draw_line((300, 50), (400, 400), &YELLOW).unwrap();
   backend.draw_rect((250, 250), (400, 400), &MAGENTA, true).unwrap();
   backend.draw_pixel((400, 400), &RED.mix(1.0)).unwrap();
}

応用例

最後に、機械学習やコンピュータビジョンの分野で利用する例を示します。

私たちは、機械学習システムのテストとして、「入力画像から期待通り物体を検出できること」などをテストケースとして自動テストに組み込んでいます。このとき、検出結果の座標値などに対するassertionだけでなく、検出結果を可視化した結果も見たいという要求があります。

ここでは、以下の様に物体検出した結果を描画する例をPlottersで書いてみました。

An image from Notion

物体検出の結果をPlottersで可視化した例

#[test]
fn visualize() {
   let (width, height) = (600, 400);
   // 画像ファイルに出力する
   let root = BitMapBackend::new("visualize.png", (width, height)).into_drawing_area();
   root.fill(&WHITE).unwrap();

   // Vec<u8> (RGB)として画像を用意する
   let mut img = image::open("dog.png").unwrap()
                     .resize_exact(width, height, FilterType::Nearest)
                     .to_rgb()
                     .to_vec();

   // ビルトインのBitMapElementは、画像データを描画できる
   root.draw(&BitMapElement::with_mut((0, 0), (width, height), &mut img).unwrap()).unwrap();

   // dummy result: (left, top, width, height, category)
   // 物体検出部分は省略。以下のように検出結果が得られたとする
   let result = vec![
       (10, 230, 200, 140, "cat"), (190, 130, 150, 230, "dog"),
       (310, 100, 170, 260, "dog"), (450, 230, 130, 140, "cat"),
   ];

   // Elementを組み合わせて、検出結果を表示するElementを作成
   let bbox_element = |(left, top, width, height, category)| {
       let color = if category == "dog" { &MAGENTA } else { &CYAN };
       EmptyElement::at((left, top))
           + Rectangle::new([(0, -25), (100, 0)], ShapeStyle::from(color).filled())
           + Rectangle::new([(0, 0), (width, height)], color)
           + Text::new(category, (10, -20), ("sans-serif", 20.0).into_font())
   };

   // 検出結果のデータを1つずつ描画
   for r in result {
       root.draw(&bbox_element(r)).unwrap();
   }
}

Elementの組み合わせは、以下のように行っています。EmptyElementを原点としてその相対座標としてTextとRectangleを組み合わせています。

An image from Notion

Bounding Box描画用のElement

まとめ

本記事では、Plottersの簡単な紹介を行いました。

Plottersはハイレベル・ローレベルなドローイングができ、Rustで簡単に図形やグラフを描画するために便利です。興味のある方はぜひ使ってみてください。

今後も機械学習のプロダクションをRustで開発して得た便利crateの知見、チームとしてどのように開発をおこなっているか、エッジデバイスでの機械学習システム特有のソフトウェアアーキテクチャの考え方などを投稿して行きたいと思います。

Mobility Technologiesでは私たちのチームメンバを募集しています。興味の在る方はぜひご連絡ください。

エッジAIエンジニア | 株式会社Mobility Technologies

最後まで読んでいただきありがとうございました。