Flutter&Rust#02 | 图片灰度 - 性能提升!

1,915 阅读4分钟

Flutter&rust1.png

Flutter&Rust#01系列文章:


上一篇我们介绍了如何在 Flutter 中集成 Rust 代码进行开发。本篇将通过一个具体的案例体验一下 Rust 所带来的巨大收益。如下所示:

读取一个图片转化为灰度图片并存储到当前目录

image.png


1. 交互效果

rust 代码将使用 image 库进行处理,另外 dart 版的 image 库也可以做到该功能,所以可以对比两者执行该功能的耗时,来对比 Rust 给 Flutter 开发带来的真实收益。这个测试案例的交互非常简单:

用户可以选择一张设备中的图片;
上下两个按钮分别通过 dartrust 代码处理灰度化图片的逻辑。
右上角展示操作消耗的时间。
选择文件过后,用户可以通过 x 来取消图片选择,再选择新图片。

无标题项目.gif

  • Rust 方法处理 1 MB 左右的图片耗时,约 35~45ms
  • Dart 方法处理 1 MB 左右的图片耗时,约 740~800ms

特别是对于处理能力不足的移动端,我试了一张 12 MB 的图片, rust 约 1.1 秒完成,而 dart 需要 23 秒。性能的提升体验,在感知上还是非常明显的。所以图片处理这类复杂计算的工作,交给 Rust 是一个明智之举。

image.png

: 上面是在 release 模式下的对比结果。不同设备、不同时刻性能表现可能有所差异。
请勿在 Debug 模式对比、讨论任何性能问题!
请勿在 Debug 模式对比、讨论任何性能问题!
请勿在 Debug 模式对比、讨论任何性能问题!

另,由于 Dart 最后需要将图像压缩为 PNG , Rust 存储时直接存为 PNG
消耗时长对比,会受到参数的影响。不同的图片也会有所差异,所以具体的倍数很难定量确定。
即便 Dart 代码中压缩比设为最低,Rust 也比 Dart 快 8 倍左右。


2. dart 逻辑处理

如下所示,处理流程如下所示:

  • 读取 src 图片文件,通过 img.decodeImage 解码图片
  • 遍历图片的像素点,获取rgb值,计算灰度值。
  • 将grayImage 对应的像素点rgb设为灰度值
  • 解码出 grayImage 的字节数组,写入到 dist 文件中。
import 'dart:io';
import 'dart:typed_data';
import 'package:image/image.dart' as img;

void luma({required String src, required String dist}) {
  File imageFile = File(src);
  img.Image image = img.decodeImage(imageFile.readAsBytesSync())!;
  img.Image grayImage = img.Image(width: image.width, height: image.height, numChannels: 1);
  for (int y = 0; y < image.height; y++) {
    for (int x = 0; x < image.width; x++) {
      img.Pixel pixel = image.getPixel(x, y);
      num r = pixel.r;
      num g = pixel.g;
      num b = pixel.b;
      int luma = (0.299 * r + 0.587 * g + 0.114 * b).round();
      grayImage.setPixel(x, y, img.ColorInt8.rgba(luma, luma, luma, 1));
    }
  }
  File(dist).writeAsBytesSync(img.encodePng(grayImage));
}

3. rust 逻辑处理

如下所示,处理流程如下所示,其中算法和 dart 的保持一致:

  • 读取 src 图片文件,通过 image::open 解码图片
  • 遍历图片的像素点,获取rgb值,计算灰度值。
  • 将 gray_image 对应的像素点rgb设为灰度值
  • 将 gray_image 写入到 dist 文件中。
---->[rust/src/api/image_handler.rs]----
use image::{DynamicImage, GenericImageView, ImageBuffer, Rgba};

#[flutter_rust_bridge::frb(sync)]
pub fn luma_image(src: &str, dist: &str) {
    let img = image::open(src).expect("Failed to open image");
    let (width, height) = img.dimensions();
    let mut gray_image = ImageBuffer::new(width, height);

    for (x, y, pixel) in img.pixels() {
        let Rgba([r, g, b, _]) = pixel;  
        let luma = (0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32) as u8;
        gray_image.put_pixel(x, y, image::Luma([luma]));
    }
    gray_image.save(dist).expect("Failed to save image");
}

通过 flutter_rust_bridge_codegen generate 可以自动生成 Dart 和 Rust 的桥接代码:

image.png


4.界面构建

界面构建中,核心在于处理选择图片的框和处理文件选择。通过 file_picker 可以在全平台中选择文件:

image.png

界面中主要有三个需要状态变化的数据。_path 表示选择的图片,_dist 表示生成的图片,_cost 表示生成的时间。如下所示,_onTapSelect 方法触发选择文件,选中后为 _path 赋值并重新构建:

String? _path;
String? _cost;
String? _dist;

void _onTapSelect() async {
  FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.image);
  if (result != null && result.files.isNotEmpty) {
    String? path = result.files.first.path;
    if (path != null) {
      setState(() {
        _path = path;
      });
    }
  }
}

三个状态决定如下三个区域的视图展示,在交互过程中,只需要在不同的时机,改变状态数据即可:

image.png

如下所示,点击橙色按钮,执行 rust 的 lumaImage 方法,为 _cost_dist 赋值,并重新构建。Dart 的处理也是类似:

String dist = p.join(File(src).parent.path,basename+"_rust.png");
File(dist).deleteSync();
int start = DateTime.now().microsecondsSinceEpoch;
lumaImage(src: src, dist: dist);
int end = DateTime.now().microsecondsSinceEpoch;

_cost = '$info: ${(end - start) / 1000}ms');
_dist = dist;
setState(() { });

通过这个小例子,可以切实的体会到 Rust 在图片操作中的优越性。Rust 接入 Flutter ,将会为我的 匠心千刃 带来更高的上限。后期会基于 匠心千刃 的打造过程,对比和分享更多 Flutter 和 Rust 的知识,敬请期待 ~