🦀

FlutterからRustでエッジ検出させようとしたら、あまり上手くいかなかった話

2024/01/22に公開
2

FlutterでRustを使ってみようと、flutter_rust_bridgeというライブラリを試した記録です。
https://github.com/fzyzcjy/flutter_rust_bridge
当方、RustもFlutterも初心者なので、間違っていることがあればご指摘ください。

簡単なまとめ

目的は、RustのコードをFlutterから呼び出すこと。
せっかくRustを使うのなら重め処理をさせようと思い、エッジ検出をさせてみた。
残念ながら、Flutter(Dart)よりRustの方が遅くなってしまった。
コメントでご指摘いただきました。リリースビルドで計測するべきでした。

このライブラリについて

ドキュメントに書いてあったWhat's this?を引用します。

  • Just write down some normal Rust code (even with arbitrary types, closure, &mut, async, etc)
  • And call it from Flutter, as if Rust code is normal Flutter code
  • The bridge will generate all needed glues in between
  • 普通のRustコードを書く(任意の型、クロージャ、&mut、asyncなどを含む)
  • FlutterからFlutterのコードを普通に呼び出すように、Rustのコードを呼び出す
  • 必要な全てのグルーコードをブリッジが生成する。

使ってみる

ここに書いているので、詳細はそちらを参照してください。
https://cjycode.com/flutter_rust_bridge/quickstart
以下、簡単なまとめ。

前提条件

  • Flutterの環境が整っていること
  • Rustの環境が整っていること

インストール

cargo install 'flutter_rust_bridge_codegen@^2.0.0-dev.0'

プロジェクトの作成

flutter_rust_bridge_codegen create my_app

動かす

flutter run

画像処理をさせてみる

サンプルのままではつまらないので、エッジ検出の処理の速度を計測してみます。

Flutterのコード

画像をクリックで、エッジ検出をするようにします。
また、画面左がFlutterで処理、右がRustで処理します。
Compare Flutter and Rust Image
表示している画像の大きさは約1MBです。
画像はここから→ https://freetestdata.com/

main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image/image.dart' as img;
import 'package:rust_flutter_sample/src/rust/api/simple.dart';
import 'package:rust_flutter_sample/src/rust/frb_generated.dart';

void main() async {
  await RustLib.init();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) => const MaterialApp(
        home: ImageProcessingPage(),
      );
}

class ImageProcessingPage extends StatefulWidget {
  const ImageProcessingPage({super.key});

  
  createState() => _ImageProcessingPageState();
}

class _ImageProcessingPageState extends State<ImageProcessingPage> {
  Uint8List? _defaultImage, _flutterProcessedImage, _rustProcessedImage;
  String _processingTime = '';
  String _rustProcessingTime = '';

  
  void initState() {
    super.initState();
    _loadDefaultImage();
  }

  void _loadDefaultImage() async {
    final bytes = await rootBundle.load('assets/test.png');
    setState(() => _defaultImage = bytes.buffer.asUint8List());
  }

  void _processImage(Uint8List bytes, {required bool useRust}) async {
    final startTime = DateTime.now();
    if (useRust) {
      _rustProcessedImage = processImageWithRust(input: bytes);
    } else {
      var image = img.decodeImage(bytes)!;
      img.grayscale(image);
      img.sobel(image);
      _flutterProcessedImage = Uint8List.fromList(img.encodePng(image));
    }
    final endTime = DateTime.now();
    final elapsed = endTime.difference(startTime);
    setState(() {
      if (useRust) {
        _rustProcessingTime = 'Rust Processing time: ${elapsed.inMilliseconds} ms';
      } else {
        _processingTime = 'Flutter Processing time: ${elapsed.inMilliseconds} ms';
      }
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Compare Flutter and Rust Image Processing'),
      ),
      body: Column(
        children: [
          Expanded(
            child: Row(
              children: [
                _buildImageColumn(useRust: false),
                _buildImageColumn(useRust: true),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildImageColumn({required bool useRust}) {
    return Expanded(
      child: GestureDetector(
        onTap: () async {
          final bytes = await rootBundle.load('assets/test.png');
          _processImage(bytes.buffer.asUint8List(), useRust: useRust);
        },
        child: Center(
          child: Column(children: [
            _defaultImage != null
                ? Image.memory(useRust
                    ? _rustProcessedImage ?? _defaultImage!
                    : _flutterProcessedImage ?? _defaultImage!)
                : const Text('Tap to process image')
                ,
                useRust ? Text(_rustProcessingTime) : Text(_processingTime),
          ]),
        ),
      ),
    );
  }
}

Rustのコード

サンプルプログラムのままなら、src/rust/api/simple.rsに処理を書きます。
画像を扱うために、imageライブラリ、並列処理のためにrayonライブラリを使います。

simple.rs

#[flutter_rust_bridge::frb(sync)] // Synchronous mode for simplicity of the demo
pub fn process_image_with_rust(input: Vec<u8>) -> Vec<u8> {
    let img = load_from_memory(&input).expect("Failed to load image");
    let processed_img = edge_detection_parallel(&img);
    // let processed_img = &img;

    let mut output = Vec::new();
    processed_img.write_to(&mut output, ImageOutputFormat::Png).unwrap();
    output
}

pub fn edge_detection_parallel(input: &DynamicImage) -> DynamicImage {
    let gray_image = grayscale(input);
    let (width, height) = gray_image.dimensions();

    let rows: Vec<_> = (1..height - 1)
        .into_par_iter()
        .map(|y| {
            let mut row = Vec::with_capacity((width - 2) as usize);
            for x in 1..width - 1 {
                let gx = sobel_operator(&gray_image, x, y, true);
                let gy = sobel_operator(&gray_image, x, y, false);

                let intensity = (gx * gx + gy * gy).sqrt() as u8;
                row.push((x, y, Luma([intensity])));
            }
            row
        })
        .collect();

    let mut output = ImageBuffer::new(width, height);
    for row in rows {
        for (x, y, pixel) in row {
            output.put_pixel(x, y, pixel);
        }
    }

    DynamicImage::ImageLuma8(output)
}

fn sobel_operator(img: &GrayImage, x: u32, y: u32, horizontal: bool) -> f32 {
    let kernel = if horizontal {
        [
            [-1.0, 0.0, 1.0],
            [-2.0, 0.0, 2.0],
            [-1.0, 0.0, 1.0],
        ]
    } else {
        [
            [-1.0, -2.0, -1.0],
            [0.0, 0.0, 0.0],
            [1.0, 2.0, 1.0],
        ]
    };

    let mut sum = 0.0;

    for ky in 0..3 {
        for kx in 0..3 {
            let px = img.get_pixel(x + kx - 1, y + ky - 1).0[0] as f32;
            sum += px * kernel[ky as usize][kx as usize];
        }
    }

    sum
}

グルーコードを生成する

flutter_rust_bridge_codegen generate

結果

flutter run

Compare Flutter and Rust Image Processing

言語 処理時間
Dart(Flutter) 6364ms
Rust(並列化) 10968ms
Rust(並列化しない) 19302ms

ちゃんとリリースビルドで計測しましょう!!

言語 処理時間
Dart(Flutter) 17289ms
Rust(並列化) 378ms
Rust(並列化しない) 382ms

Flutterの方はリリースビルドで遅くなってしまいましたが、Rustの方は劇的に速くなりました。
並列化の効果は無くなってしまいましたが、明らかにRustを使った方が早いですね。


以降、Debugビルドで計測してたことが原因による蛇足です。

少しだけ調べてみる

本来の目的(FlutterからRustを呼び出す)は達成できましたが、少し調べてみます。原因はわかりませんでしたが...

元の関数 (10958ms)

pub fn process_image_with_rust(input: Vec<u8>) -> Vec<u8> {
    let img = load_from_memory(&input).expect("Failed to load image");
    let processed_img = edge_detection_parallel(&img);

    let mut output = Vec::new();
    processed_img.write_to(&mut output, ImageOutputFormat::Png).unwrap();
    output
}

エッジ検出しない関数 (11261ms)

なんで遅くなるんだ...

pub fn process_image_with_rust(input: Vec<u8>) -> Vec<u8> {
    let img = load_from_memory(&input).expect("Failed to load image");
    let processed_img = &img;

    let mut output = Vec::new();
    processed_img.write_to(&mut output, ImageOutputFormat::Png).unwrap();
    output
}

load_from_memoryだけ (3768ms)

pub fn process_image_with_rust(input: Vec<u8>) -> Vec<u8> {
    let img = load_from_memory(&input).expect("Failed to load image");
    input
}

画像をもらって返すだけの関数 (101ms)

pub fn process_image_with_rust(input: Vec<u8>) -> Vec<u8> {
    input
}

まとめ

どうやら、load_from_memoryとwrite_toが遅いようです。
有識者の方は、もしRustでちゃんと画像処理をさせる方法があれば優しく教えて下さい。
目的は、FlutterからRustのコードを呼び出すことだったのでひとまずヨシ。

Discussion

白山風露白山風露

画像処理のような反復回数の多いコードで重いというのは、単純にデバッグビルドとリリースビルドの違いではないでしょうか。cargoのコマンドラインオプションに --release を付けていますか?

kaiseikaisei

ご指摘ありがとうございます!
flutter_rust_bridgeがrustのビルドをしてくれているので、どこで--release設定にすれば良いのかわからなかったのですが、Flutterをリリースビルドすれば出来ました。
378msとめちゃ早になりました。ありがとうございます。