※本記事の背景にある『DRIVE CHART』は、2025年8月1日付けで会社分割に伴い新会社GOドライブ株式会社に移管されました。現在は、GOドライブ社のテックブログにて継続的に技術情報を発信していますので、そちらもご参照ください。
はじめまして、AI技術開発部の加藤(@tkato)です。
私たちはエッジ x クラウドの機械学習システムのプロダクション開発を行う上で、Rustを開発言語の一つとして利用しています。今後はこのブログを使って、私たちがRustで開発している際に得た知見を共有していきたいと思います。
本内容は7/27に行われたRust LT Online #1で発表した内容です。興味のある方は以下のスライドも合わせてご覧ください。

Introduction of Plotters - Google スライド
今回はPlottersの紹介です。PlottersはRustで簡単に図形やグラフを描画するために便利なドローイングライブラリです。
同様にRustで可視化ができるcrateとして、opencv-rustやPlotly.rsなどがありますが、特にPlottersはpure Rustで簡単にインストールして組み込める点や、WebAssembly含めて複数のBackendに拡張していける点などが優れています。それぞれ長所短所があるので用途に応じて使い分けるのが良いと考えています。
私たちは、Rust側の計算結果をjsonやprotobufs等にシリアライズし、Pythonなど他の言語で可視化したこともありましたが、このやり方ではCIパイプライン等での自動化が複雑になりやすいです。そのため、Plottersのようなcrateを利用してRustの cargo test の実行だけでテストから可視化までを自動化できるのであれば、それが単純で望ましいと考えています。
Plottersとは
Plottersは、グラフなどを描画するためのcrate(Rustのライブラリのこと)です。
下図のような、様々な種類のグラフやアルゴリズムの結果の可視化ができます。
また、複数のBackend(描画先)に対応しており、画像ファイル(png, gif, svgなど)だけでなくGTKやターミナルへの描画や、WebAssemblyにビルドしてHTMLのCanvasに対して描画することもできます。

PlottersのExample: https://github.com/38/plotters より引用
基本的な使い方
簡単な例でPlottersのAPIを理解してみましょう。本記事で利用するPlottersのバージョンは0.2.15です。
ここでは、以下のグラフを描画するためのコード例を示しています。

ChartContextのAPIで描いたy=x2のグラフ
#[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で図形を重ねる、といったこともできます。

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

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では、ピクセルレベルや単純な図形の描画をサポートしています。

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で書いてみました。

物体検出の結果を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を組み合わせています。

Bounding Box 描画用のElement
まとめ
本記事では、Plottersの簡単な紹介を行いました。
Plottersはハイレベル・ローレベルなドローイングができ、Rustで簡単に図形やグラフを描画するために便利です。興味のある方はぜひ使ってみてください。
今後も機械学習のプロダクションをRustで開発して得た便利crateの知見、チームとしてどのように開発をおこなっているか、エッジデバイスでの機械学習システム特有のソフトウェアアーキテクチャの考え方などを投稿して行きたいと思います。
最後まで読んでいただきありがとうございました。