首战Flutter开发tflite详细记录

首战Flutter开发tflite详细记录

2023-04-23

0. 简介

之前自学了flutter,也写过几个项目,稍微有点开发经验。这次有个同学找到我,让帮忙写一个展示他们模型的平台出来。模型简单来说就是压缩图片(input: shape[1, 1, 256, 256]; output: shape[1, 2, 128, 128]),需求:

  1. 能从手机上选择图片
  2. 能成功运行模型(两个,encoder和decoder)
  3. 能将输出存储为字节流到文件
  4. 能从文件中读取字节流并恢复为图片

感觉上都是比较简单的(除了第一次接触flutter使用tensorflow-lite),所以就直接开始做了。

仓库链接:JSYRD/krccsnet (github.com)

1. Plugin-List

除开tflite相关部分,根据需求可知大概需要以下插件:

image_picker | Flutter Package (pub.dev) 用于选择图片

file_picker | Flutter Package (pub.dev) 用于读取文件

permission_handler | Flutter Package (pub.dev) 用于管理权限

image | Dart Package (pub.dev) 用于处理图像(不是dart:ui中的image)

由于可能会与dart:ui中的image冲突,所以我们可以:

import 'package:image/image.dart' as image_utils;

关于tflite模型的使用,pub.dev上搜索"tflite"第一个结果并不是官方的插件,而是自制并且貌似适用范围比较窄。继续搜索后发现

tflite_flutter | Flutter Package (pub.dev)

是tensorflow官方开发的插件,并且刚刚发布17天。官方插件有如下优点:

Overview

TensorFlow Lite Flutter plugin provides a flexible and fast solution for accessing TensorFlow Lite interpreter and performing inference. The API is similar to the TFLite Java and Swift APIs. It directly binds to TFLite C API making it efficient (low-latency). Offers acceleration support using NNAPI, GPU delegates on Android, Metal and CoreML delegates on iOS, and XNNPack delegate on Desktop platforms.

Key Features

  • Multi-platform Support for Android, iOS, Windows, Mac, Linux.
  • Flexibility to use any TFLite Model.
  • Acceleration using multi-threading and delegate support.
  • Similar structure as TensorFlow Lite Java API.
  • Inference speeds close to native Android Apps built using the Java API.
  • You can choose to use any TensorFlow version by building binaries locally.
  • Run inference in different isolates to prevent jank in UI thread.

比较符合需求

p.s.: 截止2023-04-20,插件依然是v0.9.1,非正式版,在插件介绍中也有声明:

Announcement

Update: 2 April, 2023

The TensorFlow team has taken official ownership of this plugin and will begin migrating to the TensorFlow org with a team managed repo. You can follow the package on pub.dev for updates as we make progress. Our goal is to have this moved over by mid-May, then we’ll start work on updates and accepting community contributions.

Feel free to reach out to me with questions until then.

Thanks!

  • ptruiz@google.com

所以对某些模型和设备可能会存在bug。

需要注意的点:

  • 在Dart中,传给模型的Tensor事实上是List<dynamic>
  • 模型运行需要两个参数:input ,output,会自动将结果填入output中,所以需要提前准备好一个空的outputTensor

2. 准备和思路

由于对美观基本没要求,基本上Scaffold搭个页面,能显示图片,放几个按钮再输出下状态就行,最终效果大致如下:

请添加图片描述

具体实现细节就不再赘述,可以自行翻代码,这里重点讲几个按钮的功能的实现。

准备工作

准备模型(版权原因,模型未放在仓库中),放在assets/下,并在pubspec.yaml中添加:

  assets:
  - assets/rena.png
  - assets/tflite_0.5encoder.tflite
  - assets/tflite_0.5decoder.tflite

由于模型的使用需要初始化其Interpreter,所以我们需要在一开始时加载Interpreter,以保证后续使用。这里选择在HomePage.initState()中调用加载模型的方法:

  late Interpreter encoderInterpreter;
  late Interpreter decoderInterpreter;  
  
  void initState() {
    loadModel();
    super.initState();
    // selectedImage = Image.asset('assets/rena.png');
  }
  void loadModel() async {
    var interpreteroptions = InterpreterOptions();
    interpreteroptions.useNnApiForAndroid = true;
    encoderInterpreter = await Interpreter.fromAsset('tflite_0.5encoder.tflite',
        options: interpreteroptions);

    decoderInterpreter = await Interpreter.fromAsset('tflite_0.5decoder.tflite',
        options: interpreteroptions);
  }

选择照片的实现

给选择照片按钮绑定方法openGallery。方法实现思路如下:

  • 使用image_picker插件提供的方法获取一个图片;
  • 渲染并处理图片,而后将处理图片后得到的tensor交由模型处理;
  • 将模型给出的输出(字节)返回。

需要注意的是需求是选择完图片后自动开始编码,所以集成到了该方法中,事实上可以再拆分出一个按钮来控制开始编码。

考虑到渲染并处理图片,以及模型的运行需要时间。如果采用同步或者异步单线程的方法会导致主线程阻塞,所以我们使用多线程,把耗时操作放在副线程运行,避免阻塞:

  void openGallery() async {
    final ImagePicker _picker = ImagePicker();
    XFile? pickedFile = await _picker.pickImage(source: ImageSource.gallery); // 选择一个图片
    if (pickedFile == null) {
    } else {
      setState(() {
        selectedImage = Image.file(File(pickedFile.path));
        byteStreamReady = false;
      });
      ReceivePort receivePort = ReceivePort();
      await Isolate.spawn(KrccsnetEncoder.encode, [
        receivePort.sendPort,
        pickedFile.path,
        encoderInterpreter.address,
      ]);//编码,运行模型
      setState(() {
        benchmarkText = "encoding";
      });
      receivePort.listen((message) {//得到输出的tensor
        setState(() {
          benchmarkText = "Done!\nelapsed time: ${message[0] as int} ms";
          outputTensor = message[1] as List<dynamic>;
          byteStreamReady = true;
        });
      });
    }
  }

这里我将处理图片和运行模型的部分抽象成了方法encode,并且将几个类似方法放在一起抽象成了类KrccsnetEncoder,具体细节后文解读。

需要注意的是,dart的多线程并没有内存共享,唯一的通讯方式是通过Port。

在使用Isolate.spawn创建新线程时,有一个参数List<Object> message,代表传给要运行的方法的参数。但需要注意的是,无论是通过Port还是通过message传递参数时,都要求内容必须可序列化。这就导致了我们没有办法直接将Interpreter传给KrccsnetEncoder.encode,但好在插件想到了这个问题,所以提供了Interpreter.address和配套的Interpreter.fromAddress()方法,便于在线程之间通信使用。

保存字节流的实现

给保存字节流按钮绑定方法saveByteStream。 上一步我们通过模型运行后得到了一个输出List<dynamic> outputTensor,事实上是一个shape[1, 2, 128, 128], fp32,我们需要将其转化为字节流并存储。由于存储文件涉及到路径和文件名,所以我又写了一个页面用于选择路径和填写文件名,然后使用showDialog()方法就可以达到弹窗的效果:

  void saveByteStream() async {
    if (byteStreamReady) {
      await showDialog(
          context: context,
          builder: (BuildContext context) {
            return SaveFilePage(
              outputTensor: outputTensor,
            );
          });
    } else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
          content: Text("字节流未生成"),
          showCloseIcon: true,
          closeIconColor: Colors.lightBlue));
    }
  }

这里将outputTensor作为参数传给了SaveFilePage,所以需要稍微修改一下常规的StatefulWidget Snipple:

class SaveFilePage extends StatefulWidget {
  const SaveFilePage({super.key, required this.outputTensor});
  final List<dynamic> outputTensor;
  
  State<SaveFilePage> createState() => _SaveFilePageState();
}
class _SaveFilePageState extends State<SaveFilePage> {
  final TextEditingController _getFileName = TextEditingController();
  late final List<dynamic> outputTensor;
  String? saveDirectory;
  
  void initState() {
    super.initState();
    outputTensor = widget.outputTensor;
    loadDefaultDirectory();
  }

直接在_SaveFilePageState中调用widget.outputTensor就可以获取到SaveFilePageoutputTensor了。

请添加图片描述

同样的,限于篇幅,页面搭建细节不再赘述,可以自行翻代码。这里还是重点讲解功能的实现。

首先存储路径的部分是一个TextButton,点击文字部分就会自动跳出资源管理器,让用户自行选择存储路径,这样较为符合逻辑。

由于实现逻辑较为简单,就直接采用匿名函数的方式写在页面逻辑中了:

                    onPressed: () async {
                      if (await Permission.storage.request().isGranted) {
                        String? selectedDirectory =
                            await FilePicker.platform.getDirectoryPath();
                        if (selectedDirectory != null) {
                          setState(() {
                            saveDirectory = selectedDirectory;
                          });
                        }
                      }
                    },

需要注意的是这里可能需要获取存储权限,所以需要利用Permission.storage.request().isGranted获取权限。然后用file_picker提供的方法即可。

填写文件名的部分使用了一个TextField,内容用一个Controller _getFileName来控制。

最后给确定按钮实现一下逻辑。点击确定后需要获取刚才得到的路径和文件名,拼接成一个完整文件路径。然后需要将传进来的outputTensor进行处理后存储。

      actions: [
        ElevatedButton(
            onPressed: () {
              SaveReadUtils.saveOutputTensor(
                  "$saveDirectory/${_getFileName.text}.krc", outputTensor);
              Navigator.pop(context);
            },
            child: const Text("确定"))
      ],

同样的,这里我将处理Tensor和保存字节流的部分封装了起来,后文统一详细解读。

读取字节流的实现

类似与存储字节流,思路同样也是获取权限后选择文件,然后将从选择的文件中读取的字节流转化成Tensor,再将Tensor转化为Image,最终显示出来。

  void readByteStream() async {
    if (await Permission.storage.request().isGranted) {
      FilePickerResult? result =
          await FilePicker.platform.pickFiles(type: FileType.any);
      if (result != null) {//选择一个文件
          
        var input = await SaveReadUtils.readTensor(result.files.first.path!);//将文件读取为Tensor
          
        ReceivePort receivePort = ReceivePort();
        await Isolate.spawn(KrccsnetEncoder.decode,
            [receivePort.sendPort, input, decoderInterpreter.address]);		//将Tensor交由模型解码

        setState(() {
          benchmarkText = "decoding";
        });
        receivePort.listen((message) {	//得到解码后的字节流
          // message: Uint8List
          var rawImage = image_utils.Image.fromBytes(
              width: 256, height: 256, bytes: message);

          setState(() {
            selectedImage = Image.memory(image_utils.encodePng(rawImage));//转为图片
            benchmarkText = "Done!";
          });
        });
      }
    }
  }

3. 数据处理

本项目用到的就只有图片,由于需要存储,所以一共包含两对四种数据处理方式:

  1. 从图片到张量
  2. 从张量到字节流
  3. 从字节流到张量
  4. 从张量到图片

编解码输入输出张量

这个插件运行模型时的输入输出都是将Dart中的List<dynamic>看作张量。所以我们只需要将我们需要构造成张量的数据构造成List<dynamic>即可。注意shape要与本身预计的输入和输出一致。并且interpreter.run(input, output)需要传入一个空的output参数,以便将运行结果填入。

从图片到张量

png图片本质上是一个一个像素,像素又可以简单看成由RGB(A)组成的。本模型的输入为shape[1, 1, 256, 256],值为每个点对应的归一化的亮度。所以我们只需要获取每个点的亮度,填入到List中即可:

  static List<dynamic> getEncodeInputTensor(image_utils.Image rawImage) {
    var input = List<double>.filled(IMAGE_HEIGHT * IMAGE_WIDTH, 0.0)
        .reshape([1, 1, IMAGE_HEIGHT, IMAGE_WIDTH]);
    // fill with lumianceNormalized, shape: n c h w
    for (int ch = 0; ch < IMAGE_HEIGHT; ++ch) {
      for (int cw = 0; cw < IMAGE_WIDTH; ++cw) {
        input[0][0][ch][cw] = rawImage.getPixel(cw, ch).luminanceNormalized;
      }
    }
    return input;
  }

我们前面只获得了图片文件的路径,还需要将图片文件先转为image_utils.Image:

  static void encode(List<Object> options) async {
    SendPort sendPort = options[0] as SendPort;
    Interpreter encoderInterpreter = Interpreter.fromAddress(options[2] as int);
      
    File(options[1] as String).readAsBytes().then((imageBytes) async {
      image_utils.Decoder? rawImageDecoder =
          image_utils.findDecoderForData(imageBytes);

      if (rawImageDecoder == null) {
        throw Exception("Format not supported.");
      }

      var rawImage = rawImageDecoder.decode(imageBytes)!;//转为image_utils.Image 注意这里不是Krccsnet.decode,注意区分

      var input = getEncodeInputTensor(rawImage);//转为tensor
      var output = getEncodeOutputTensor();		//获得空Tensor,shape[1, 2, 128, 128]

      int krccsnetTime = await _encode(encoderInterpreter, input, output, 1);
      sendPort.send([krccsnetTime, output]);
    });
  }

getEncodeOutputTensor()定义如下:

  static List<dynamic> getEncodeOutputTensor() {
    // fill output , shape 1, 2, 128, 128
    var output = List<double>.filled(IMAGE_HEIGHT * IMAGE_WIDTH ~/ 2, 0.0)
        .reshape([1, 2, IMAGE_HEIGHT ~/ 2, IMAGE_WIDTH ~/ 2]);
    return output;
  }

_encode方法只是简单将模型运行过程封装,具体实现如下:

  static Future<int> _encode(Interpreter encoderInterpreter,
      List<dynamic> input, List<dynamic> output, int times) async {
    if (times <= 0) {
      throw Exception("times can't be $times, must > 0");
    }
    var start = DateTime.now().millisecondsSinceEpoch;
    for (int roll = 0; roll < times; ++roll) {
      encoderInterpreter.run(input, output);
    }
    var end = DateTime.now().millisecondsSinceEpoch;
    int krccsnetTime = (end - start) ~/ times;
    return krccsnetTime;
  }

为了方便测试性能,定义了参数times,以连续运行多轮取平均值。

从张量到字节流

上文提到存储时需要将张量转化为字节流。我们已经从图片得到了张量outputTensor,事实上是一个List<dynamic>,所以我们只需要考虑如何存储一个List即可。由于本模型使用FP32,所以可以保证outputTensor中存放的都是FP32数据。也就是说事实上只需要将Tensor拆分成一个个List<double>,再进行顺序存储即可。具体实现如下:

  static Future<void> saveOutputTensor(
      String saveFullDirectory, List<dynamic> outputTensor) async {
    // save output , shape 1, 2, 128, 128
    File(saveFullDirectory).createSync();

    for (int j = 0; j < 2; ++j) {
      for (int i = 0; i < IMAGE_HEIGHT ~/ 2; ++i) {
        await File(saveFullDirectory).writeAsBytes(
            Float32List.fromList(outputTensor[0][j][i] as List<double>)
                .buffer
                .asUint8List(),
            mode: FileMode.append);
      }
    }
  }

注意请添加图片描述
由于存储只能以字节形式存储,所以我们需要使用.asUint8List()方法将一个FP32拆分成4个Uint8作为字节存储,所以后续读取的时候同样需要将4个字节还原成一个FP32。

从字节流到张量

首先需要从文件读出所有字节,然后按照我们编码的方式,将每4个字节编码为一个FP32。幸运的是有内置方法可以使用:

  static Future<List<dynamic>> readTensor(String readFullDirectory) async {
    var input = KrccsnetEncoder.getDecodeInputTensor();
    Uint8List rawBytes =
        await File(readFullDirectory).readAsBytes(); // 2 * 128 * 128
    for (int i = 0; i < 2; ++i) {
      var reader = rawBytes.buffer.asFloat32List(
          i * IMAGE_HEIGHT * IMAGE_WIDTH, IMAGE_HEIGHT * IMAGE_WIDTH ~/ 4);
      for (int ch = 0; ch < IMAGE_HEIGHT ~/ 2; ++ch) {
        for (int cw = 0; cw < IMAGE_WIDTH ~/ 2; ++cw) {
          input[0][i][ch][cw] = reader[ch * IMAGE_HEIGHT ~/ 2 + cw];
        }
      }
    }
    return input;
  }

从张量到图片

在读取字节流时,readTensor方法读出张量后,会创建新线程交由decode方法运行模型获得输出output

  static void decode(List<Object> options) async {
    SendPort sendPort = options[0] as SendPort;
    List<dynamic> input = options[1] as List<dynamic>;
    Interpreter decoderInterpreter = Interpreter.fromAddress(options[2] as int);

    var output = <int, Object>{};
    output[0] = getDecodeOutputTensor();
    output[1] = getDecodeOutputTensor();
    await _decode(decoderInterpreter, input, output, 1);

    var rawImage = image_utils.Image(width: 256, height: 256);
    for (int ch = 0; ch < IMAGE_HEIGHT; ++ch) {
      for (int cw = 0; cw < IMAGE_WIDTH; ++cw) {
        double c = (output[1]! as List<dynamic>)[0][0][ch][cw] * 255.0;
        rawImage.setPixelRgb(cw, ch, c, c, c);
      }
    }

    sendPort.send(rawImage.buffer);
  }

既然已经得到张量,我们就可以根据我们需要的方式构造一个image_utils.Image出来,例如在本项目中需要将输出作为亮度(也就是输出一个灰度图),只需要将归一化的值恢复到0-255,再根据灰度图的定义,将该值作为RGB值构造一个图片即可。最后将该图片以字节流形式发送回正在等待的主线程。然后直接调用Image.fromBytes()恢复该图片即可。

        receivePort.listen((message) {
          // message: Uint8List
          var rawImage = image_utils.Image.fromBytes(
              width: 256, height: 256, bytes: message);

          setState(() {
            selectedImage = Image.memory(image_utils.encodePng(rawImage));
            benchmarkText = "Done!";
          });
        });

至此完整的图片->张量->字节流->张量->图片 转化流程已经全部实现。

4. 编译tflite

在运行时报错:

请添加图片描述

E/tflite ( 6951): Didn't find op for builtin opcode 'TRANSPOSE' versior '6'. An older version of this builtin might be supported. Are you using an old TFLite binary with a newer model?
E/tflite ( 6951): Registration failed.
E/flutter( 6951): [ERROR.flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception:Invalid angument(s): Unable to create interpreter.

简单搜索后得知是插件使用的tflite版本过低:

请添加图片描述

事实上,在插件介绍页,有提到使用插件的Initial setup:

(Important) Initial setup : Add dynamic libraries to your app

Android

  1. Place the script install.sh (Linux/Mac) or install.bat (Windows) at the root of your project.

  2. Execute sh install.sh (Linux) / install.bat (Windows) at the root of your project to automatically download and place binaries at appropriate folders.

    Note: The binaries installed will not include support for GpuDelegateV2 and NnApiDelegate however InterpreterOptions().useNnApiForAndroid can still be used.

  3. Use sh install.sh -d (Linux) or install.bat -d (Windows) instead if you wish to use these GpuDelegateV2 and NnApiDelegate.

These scripts install pre-built binaries based on latest stable tensorflow release. For info about using other tensorflow versions follow instructions in wiki.

查看install.sh/install.bat内容,发现事实上是从https://github.com/am15h/tflite_flutter_plugin/releases 下载动态链接库文件:

请添加图片描述

可以看到确实很久没有更新了,但是结合插件特性所言以及给定的提示:

  • You can choose to use any TensorFlow version by building binaries locally.
  • These scripts install pre-built binaries based on latest stable tensorflow release. For info about using other tensorflow versions follow instructions in wiki.

我们需要编译一版新的tflite出来才能正常运行。

上面的instructions in wiki里面给的链接挂了,我们可以直接去tf官网找到指南:

Build TensorFlow Lite for ARM boards

首先尝试构建针对ARM的版本,编译完成后将得到的二进制文件替换掉/android/app/src/main/jniLibs/*/libtensorflowlite_c.so

请添加图片描述

但是运行时报错,说明还是要构建针对Android的版本。

直接按照 Build TensorFlow Lite for Android 不使用Docker设置构建环境开始。

环境搭建

首先Bazel有版本要求,需要5.3.0版本。

在 Ubuntu 上安装 Bazel 可以根据这个文档安装。

接着需要克隆tf源代码:

$ git clone https://github.com/tensorflow/tensorflow.git tensorflow_src

然后需要准备NDK和Android SDK。这里需要注意NDK和Android SDK都是有版本要求的。例如我一开始下载了最新的NDK r25c,配置时会提示:

WARNING: The NDK version in /home/subwoy/temp/tfliteCompile/android-ndk-r25c is 25, which is not supported by Bazel (officially supported versions: [19, 20, 21]). Please use another version. Compiling Android targets may result in confusing errors.

所以我们需要下载NDK[19-21]版本,同时对应的也要下载对应支持的SDK(例如30)。

NDK下载

Unsupported Downloads · android/ndk Wiki (github.com)

从这里可以下载到NDK21旧版本。这里我下载了支持的最新的r21e。下载后解压。

SDK下载

我们使用官方的sdkmanager工具来下载SDK:

Download Android Studio & App Tools - Android Developers 去官网向下拉,找到Command line tools only:

请添加图片描述

下载后解压,在/bin下就可以找到sdkmanager了。由于我们是临时使用,并没有配置环境变量,所以在使用sdkmanager时需要带上--sdk_root=xxx来指定你要把sdk下载到哪个路径。例如我放在~/temp/tfliteCompile/android-sdk

首先查看可以下载哪些版本:

$ ./sdkmanager --list --sdk_root=~/temp/tfliteCompile/android-sdk

同样这里我直接下载最新支持的SDK30和build-tools:

$ ./sdkmanager "build-tools;30.0.3" "platforms;android-30"
配置bazel编译环境

进入到tf源码路径,执行./configure进行配置:

subwoy@DESKTOP-ENVI0R2:~/temp/tfliteCompile/tensorflow_src$ ./configure
You have bazel 5.3.0 installed.
Please specify the location of python. [Default is /usr/bin/python3]:


Found possible Python library paths:
  /usr/lib/python3/dist-packages
  /usr/local/lib/python3.8/dist-packages
Please input the desired Python library path to use.  Default is [/usr/lib/python3/dist-packages]

Do you wish to build TensorFlow with ROCm support? [y/N]: N
No ROCm support will be enabled for TensorFlow.

Do you wish to build TensorFlow with CUDA support? [y/N]: N
No CUDA support will be enabled for TensorFlow.

Do you wish to download a fresh release of clang? (Experimental) [y/N]: N
Clang will not be downloaded.

Please specify optimization flags to use during compilation when bazel option "--config=opt" is specified [Default is -Wno-sign-compare]: --cxxopt=--std=c++11 --config=android_arm


Would you like to interactively configure ./WORKSPACE for Android builds? [y/N]: y
Searching for NDK and SDK installations.

Please specify the home path of the Android NDK to use. [Default is /home/subwoy/Android/Sdk/ndk-bundle]: /home/subwoy/temp/tfliteCompile/android-sdk


The path /home/subwoy/temp/tfliteCompile/android-sdk or its child file "source.properties" does not exist.
Please specify the home path of the Android NDK to use. [Default is /home/subwoy/Android/Sdk/ndk-bundle]: /home/subwoy/temp/tfliteCompile/android-ndk


The path /home/subwoy/temp/tfliteCompile/android-ndk or its child file "source.properties" does not exist.
Please specify the home path of the Android NDK to use. [Default is /home/subwoy/Android/Sdk/ndk-bundle]: /home/subwoy/temp/tfliteCompile/android-ndk-r21e


Please specify the (min) Android NDK API level to use. [Available levels: ['16', '17', '18', '19', '21', '22', '23', '24', '26', '27', '28', '29', '30']] [Default is 26]: 30


Please specify the home path of the Android SDK to use. [Default is /home/subwoy/Android/Sdk]: /home/subwoy/temp/tfliteCompile/android-sdk


Please specify the Android SDK API level to use. [Available levels: ['30']] [Default is 30]:


Please specify an Android build tools version to use. [Available versions: ['30.0.3']] [Default is 30.0.3]:


Preconfigured Bazel build configs. You can use any of the below by adding "--config=<>" to your build command. See .bazelrc for more details.
        --config=mkl            # Build with MKL support.
        --config=mkl_aarch64    # Build with oneDNN and Compute Library for the Arm Architecture (ACL).
        --config=monolithic     # Config for mostly static monolithic build.
        --config=numa           # Build with NUMA support.
        --config=dynamic_kernels        # (Experimental) Build kernels into separate shared objects.
        --config=v1             # Build with TensorFlow 1 API instead of TF 2 API.
Preconfigured Bazel build configs to DISABLE default on features:
        --config=nogcp          # Disable GCP support.
        --config=nonccl         # Disable NVIDIA NCCL support.
Configuration finished

配置完成后就可以尝试开始编译了。有几种方式,例如可以根据插件仓库wiki中的方式直接:

$ bazel build -c opt --cxxopt=--std=c++11 --config=android_arm64 //tensorflow/lite/c:tensorflowlite_c

来仅编译arm64版本的二进制文件。也可以按照tf教程编译一个aar出来:

$ bazel build -c opt --fat_apk_cpu=x86,x86_64,arm64-v8a,armeabi-v7a \
  --host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
  //tensorflow/lite/java:tensorflow-lite

然后将aar解压,将里面得到的动态链接库改名替换放在你的项目中就可以了。

5. 后记

这个插件的作者退网一年了,联系不上,所以仓库很久没有更新。最近tf团队注意到了此事,并且正在尝试转移仓库。以后应该会持续更新并且修复bug。

请添加图片描述请添加图片描述请添加图片描述

By JSYRD

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Flutter开发文档是针对Flutter框架的官方文档,提供了全面的开发指南和参考资料。它包括了Flutter框架的概述,安装和配置指南,以及各种用于构建高性能跨平台应用的工具和资源。 首先,Flutter开发文档介绍了Flutter框架的基本概念和原理,包括如何使用Widget构建用户界面以及如何处理用户输入和交互。开发者可以通过这些指南来了解Flutter的整体架构和核心特性,从而更好地理解和利用Flutter进行应用开发。 其次,Flutter开发文档提供了丰富的API参考和使用示例,使开发者能够快速上手和深入了解各种Flutter组件和功能。开发者可以通过文档中的代码示例和详细说明来学习如何使用不同的Widget和布局进行界面设计,以及如何调用Flutter提供的各种API实现更复杂的业务逻辑。 此外,Flutter开发文档还详细介绍了Flutter开发工具和调试技巧,包括如何使用命令行工具进行项目管理和发布,如何使用集成开发环境(IDE)进行开发和调试,以及如何使用Flutter的调试工具来定位和解决应用中的问题。这些指南和建议可以帮助开发者提高开发效率,减少错误和调试时间。 总之,Flutter开发文档是学习和使用Flutter进行应用开发的重要参考资料。它提供了全面的开发指南和参考资料,帮助开发者快速上手和精通Flutter开发,实现高性能、跨平台的应用程序。对于初学者来说,阅读Flutter开发文档是入门的首要步骤,对于有经验的开发者来说,它是进一步提升技能和应用开发水平的宝贵资源。 ### 回答2: Flutter是Google推出的一种用于构建跨平台移动应用的开源框架。它具有快速开发、高效性能和灵活可扩展等特点。Flutter开发文档非常详细全面,为开发者提供了丰富的资源和指导。 首先,Flutter开发文档提供了完整的安装指南,让开发者能够快速搭建开发环境,并且支持多个操作系统,包括Windows、MacOS和Linux等。开发者只需按照文档中的步骤进行配置,即可开始使用Flutter进行开发。 其次,Flutter开发文档提供了丰富的基础知识和概念介绍,帮助开发者了解Flutter的整体架构和工作原理。文档中详细介绍了Flutter的Widget系统,以及如何使用Widget来构建用户界面。此外,文档还提供了许多关于布局、动画和主题等方面的教程和示例代码,帮助开发者快速上手。 另外,Flutter开发文档还包含了大量的API文档,涵盖了Flutter框架的核心功能和各种组件的使用方法。开发者可以根据需要查阅相应的API文档,了解每个组件的属性、方法和事件等详情。同时,文档还提供了一系列示例代码,让开发者能够更好地理解和运用这些组件。 最后,Flutter开发文档还提供了丰富的实用资源和工具,包括开发工具的介绍和配置、Flutter包管理和依赖管理等。开发者可以通过文档中提供的链接和教程,快速了解和使用这些工具,提高开发效率。 总的来说,Flutter开发文档对于开发者来说是非常宝贵的资源,通过详细的介绍和实用的示例,帮助开发者快速入门和深入理解Flutter框架,从而能够更高效地开发出跨平台的移动应用。 ### 回答3: Flutter开发文档是Flutter官方提供的一份详尽的技术文档,用于指导Flutter开发者学习和使用Flutter框架。文档详细地介绍了Flutter的各个方面,包括安装配置、基本概念、核心组件、布局系统、用户界面、动画、手势、插件等内容。 首先,文档提供了Flutter的安装配置指南,指导开发者如何搭建开发环境,并使用Flutter SDK进行应用开发。同时,文档还介绍了Flutter的基本概念和工作原理,帮助开发者理解Flutter的核心特性和优势。 其次,文档详细介绍了Flutter的核心组件,包括Widget、State、BuildContext等,开发者可以了解到如何使用这些组件构建复杂的用户界面。同时,文档还深入介绍了Flutter的布局系统,包括Row、Column、Stack等布局组件的用法,并提供了示例代码和案例说明。 此外,文档还介绍了Flutter的用户界面设计,包括样式、字体、图片等的使用方法,以及界面的响应事件和动画效果的实现。开发者可以通过文档了解到如何构建出令人满意的用户界面和用户交互效果。 最后,文档还提供了Flutter插件的开发方法和使用指南,通过插件可以扩展Flutter的功能和接入原生平台的特性,帮助开发者实现更复杂的应用需求。 总而言之,Flutter开发文档是一份权威且详细的技术指南,对于Flutter开发者来说是不可或缺的参考材料。通过学习和使用文档中的知识,开发者可以更加高效地进行Flutter应用的开发和调试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值