FlutterからRustでエッジ検出させようとしたら、あまり上手くいかなかった話
FlutterでRustを使ってみようと、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のコードを呼び出す
- 必要な全てのグルーコードをブリッジが生成する。
使ってみる
ここに書いているので、詳細はそちらを参照してください。
以下、簡単なまとめ。前提条件
- 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で処理します。
表示している画像の大きさは約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
言語 | 処理時間 |
---|---|
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
を付けていますか?ご指摘ありがとうございます!
flutter_rust_bridgeがrustのビルドをしてくれているので、どこで
--release
設定にすれば良いのかわからなかったのですが、Flutterをリリースビルドすれば出来ました。378msとめちゃ早になりました。ありがとうございます。