基于Paddle-Lite的实时目标检测程序(Flutter & YOLO v3)

有任何疑问或建议欢迎在下方评论以及通过关于界面的联系方式找到我。

很早之前接触到了PaddlePaddle框架以及PaddleDetection工具,被他们的简单易用所吸引,可以看出百度在DL领域的发力,同时,这些工具极大降低了训练模型的门槛并减少了所需时间,非常适合新手入门。

因为最近自己在学习跨平台开发的一大趋势Flutter,所以让我们来探索一下怎么在移动设备上实现实时的目标检测。

这次我们使用的是Paddle-Lite,这款引擎允许我们在手机等场景上面实现轻量化的高效预测,跑一次预测的耗时较短,并且不需要太多的计算资源,比较适合我们的应用场景。

那么让我们开始,所有用到的源码和资源会在参考链接里面给出。项目的GitHub地址:KernelErr/realtime-object-detector

约定:

  • Flutter端:Flutter项目主目录。
  • Android端:项目的Android子目录,原生安卓。

开发环境

如果你的开发环境和我不同,可能会造成一些错误,所以我们在这里列出:

  • Flutter version 1.12.13+hotfix.8
  • Dart version 2.7.0
  • Android Studio (version 3.6)
  • Android toolchain - develop for Android devices (Android SDK version 29.0.3)

因为Flutter加入了越来越多新的特性,网上很多老版本的实现方法其实已经不太实用,我会在最后说明这些问题。

准备模型

Paddle-Lite需要通过opt工具生成其支持的轻量化模型,如果你手上已经有Paddle Detection训练出来的模型,那么你需要先在Paddle Detection导出模型,然后通过opt工具进行转换。

1
2
3
4
5
./opt --model-file=./yolov3_mobilenet_v1/__model__ \
--param_file=./yolov3_mobilenet_v1/__params__ \
--valid_targets=arm \
--optimize_out_type=naive_buffer \
--optimize_out=yolov3_mobilenet_v1_opt

如果你有其他框架训练出来的模型,如caffe、tensorflow、onnx等,可以利用X2Paddle来转换。

具体的操作请查阅参考链接中的官方文档,我们这里主要实现手机App的编写。

假设我们已经得到了两个文件:

  • model.nb - 基于Yolov3 Tiny训练且已经通过opt优化好的模型
  • label - 模型预测一一对应的标签

在Flutter中支持Paddle-Lite

我们只需要通过Android Studio创建一个新的Flutter项目,这里我们假设名字是realtime_od

准备Paddle-Lite的预测库和模型文件

由于我们使用的是安卓原生代码,所以我们需要在Android端放置文件,而不是Flutter端。我们在Paddle-Lite提供的预编译预测库里面下载需要的预编译库,并放到指定的目录:

  • PaddlePredictor.jar放到realtime_od/android/app/libs
  • libpaddle_lite_jni.so根据armv7和armv8的不同分别放到realtime_od/android/app/src/main/jniLibs/[arm64-v8a|armeabi-v7a]
  • 复制官方demo的其他so库文件到相应目录。

我们继续在android文件夹内放置我们的模型文件,在realtime_od/android/app/src/main/下面新建assets文件夹,并分别把模型和标签放到models和labels子文件夹内。这时候你的目录结果应该是这样:

我们使用口罩模型作为样例,模型位置是:models/mask/model.nb,标签位置是:labels/mask_label_list。

因此你需要在MainActivity里面赋值:

1
2
protected String modelPath = "models/mask"; //会自动补充model.nb
protected String labelPath = "labels/mask_label_list";

禁用压缩

因为不希望模型在打包的时候被压缩导致问题,所以在Android端的build.gradle (Module:app)做如下修改:

1
2
3
4
5
android {
aaptOptions {
noCompress "assets"
}
...

提供原生安卓支持

如果为了Flutter的支持,给Paddle-Lite专门写一套Dart调用代码是工作巨大的,所以我们不妨直接基于官方的Demo进行修改。

在Android端,你可以看到我们直接使用了官方的部分文件,并在MainActivity内注册了Channel。

由于Flutter中加入了进程安全机制,你可以看到我们使用了一个MethodResultWrapper保证在主进程里面返回result。新版Flutter中你需要使用configureFlutterEngine而不是onCreate来注册组件。

使用实时影像

让我们来给Flutter提供来自摄像头的实时影像!

添加一下Flutter的camera插件:

1
2
3
4
5
6
7
8
dependencies:
flutter:
sdk: flutter

# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
camera: ^0.5.7

同时确保项目的最低Android SDK版本在21以上。

官方提供的Demo中,使用的是Bitmap图片,但是我们从插件得到的格式是android.graphics.ImageFormat.YUV_420_888,所以需要转换,你可以在Predictor类的最下面找到相应的转换代码,转换代码的使用已经联系原作者获得授权。我们在其中使用了RenderScript进行高效计算,避免延迟过高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public Bitmap getBitmap(HashMap image){
Bitmap bitmap = Bitmap.createScaledBitmap(yuv420toBitMap(image), 416, 416, true);
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Matrix matrix = new Matrix();
matrix.postRotate((Integer)image.get("rotation"));
return Bitmap.createBitmap(bitmap , 0, 0, width, height, matrix, true);
}

@SuppressWarnings("unchecked")
public Bitmap yuv420toBitMap(final HashMap image) {
int w = (int) image.get("width");
int h = (int) image.get("height");
ArrayList<Map> planes = (ArrayList) image.get("planes");

byte[] data = yuv420toNV21(w, h, planes);

Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs));

Type.Builder yuvType = new Type.Builder(rs, Element.U8(rs)).setX(data.length);
Allocation in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT);
in.copyFrom(data);

Type.Builder rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(w).setY(h);
Allocation out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT);

yuvToRgbIntrinsic.setInput(in);
yuvToRgbIntrinsic.forEach(out);

out.copyTo(bitmap);
return bitmap;
}

public byte[] yuv420toNV21(int width,int height, ArrayList<Map> planes){
byte[] yBytes = (byte[]) planes.get(0).get("bytes"),
uBytes= (byte[]) planes.get(1).get("bytes"),
vBytes= (byte[]) planes.get(2).get("bytes");
final int color_pixel_stride =(int) planes.get(1).get("bytesPerPixel");

ByteArrayOutputStream outputbytes = new ByteArrayOutputStream();
try {
outputbytes.write(yBytes);
outputbytes.write(vBytes);
outputbytes.write(uBytes);
} catch (IOException e) {
e.printStackTrace();
}

byte[] data = outputbytes.toByteArray();
final int y_size = yBytes.length;
final int u_size = uBytes.length;
final int data_offset = width * height;
for (int i = 0; i < y_size; i++) {
data[i] = (byte) (yBytes[i] & 255);
}
for (int i = 0; i < u_size / color_pixel_stride; i++) {
data[data_offset + 2 * i] = vBytes[i * color_pixel_stride];
data[data_offset + 2 * i + 1] = uBytes[i * color_pixel_stride];
}
return data;
}

显示实时图像并标注

大量的工作都花在了Android端上面,下面让我们来Flutter端做些工作。

在main.dart和object_detector.dart里面你可以发现我们调用Android端提供的方法,即loadModel以及detectObject。同时在DrawObjects类里面提供了标注目标的功能,代码比较简单,就不详细解释了。

Let’s run it!

这里使用的是群友提供的口罩模型,label文件里面只有两行,分别是戴口罩和未带口罩。我们在Android 9设备上面用PaddlePaddle官方示例图片测试一下。

从日志上面可以看出Paddle-Lite预测的时间是接近700 ms。

1
2
3
4
I/Predictor( 4651): Time used 694.0 ms.
I/flutter ( 4651): Run model.
I/GRALLOC ( 4651): LockFlexLayout: baseFormat: 11, yStride: 640, ySize: 307200, uOffset: 307200, uStride: 640
I/flutter ( 4651): [[0, 戴口罩, 68.51083374023438, 91.56356811523438, 210.63702392578125, 210.0908203125, 0.947]]

更改模型和优化方案

如何使用其他模型

我们是参考群友的解决方案(参考链接里面给出)适配的YOLO v3,主要的修改在Predictor内的模型输入以及MainActivity的初始化。因为官方使用的是其他模型,输入的Shape和我们不一样,我们的是320。

如果你需要使用其他模型,请同步修改输入处的:

1
2
3
protected long[] inputShape = new long[]{1, 3, 320, 320};
protected float[] inputMean = new float[]{0.485f, 0.456f, 0.406f};
protected float[] inputStd = new float[]{0.229f, 0.224f, 0.225f};

以及输出处的:

1
2
3
4
float rawLeft = outputTensor.getFloatData()[i + 2]/320;
float rawTop = outputTensor.getFloatData()[i + 3]/320;
float rawRight = outputTensor.getFloatData()[i + 4]/320;
float rawBottom = outputTensor.getFloatData()[i + 5]/320;

标注函数可能坐标有偏移,修改main.dart:

1
2
var ratioW = sizeRed.width / 320;
var ratioH = sizeRed.height / 320;

怎么更快

实际上我们的模型还不够快,如果你使用SSD模型的话,可以把预测时间缩短到更短。具体还是看自己的需要,Paddle-Lite支持的模型大家都可以选择。

Trouble Shooting

记录的问题包括Flutter开发过程中遇到的和Paddle-Lite使用中遇到的。

Methods marked with @UiThread must be executed on the main thread.

这是因为Flutter引入了进程安全,你不能直接在子进程里面返回result,需要在主进程里面返回,网上现在有很多解决办法,我们的也是来自GitHub。

错误: 不兼容的类型: MainActivity无法转换为FlutterEngine

很可能你看的教程是旧版本,请直接参考官方文档写原生安卓。

Paddle-Lite出现库错误

一开始以为是官方的问题,但是自己手动编译一次库就能解决。我已经内置了arm64的无问题的库。

其他问题

官方文档 -> GitHub -> 搜索引擎 -> 询问他人,一般都能解决。:)

参考链接

  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载需要标明作者,并以超链接的方式指向原文。
  • Copyrights © 2020 Kevin Li
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~