1. 方案介绍

MNN 工作台上提供了一套基于 MNN、MNNCV、OpenCV 以及 Numpy 的机器学习/深度学习三端一体Playground 能力,能够实现一套 Python 代码在 Windows / Android / iOS 上多平台同时运行。基于这一套技术,可以实现机器学习/深度学习算法在 PC 上进行调试并实时展示算法效果,调试完成后直接智能推送到移动端,进行端侧验证。

本文将介绍一套从开源深度学习框架(如Tensorflow,PyTorch等)的模型推理工程转换为 MNN 三端一体 Playground 的方案。如图例 1.1 所示,该方案主要分三个部分,分别是模型转换,模型加载与推理以及三端一体效果测试。对于模型转换,MNN 工作台提供了模型转换工具,能够实现多个深度学习框架模型(如tf,onnx,caffe等)转换成 MNN 模型,能够通过 UI 交互实现模型转换;对于模型加载与推理,我们对与 MNN 操作相关的注入数据格式转换,模型加载以及模型推理进行了封装,用户只需要关心与算法以及业务相关的前后处理代码,便能够实现基于 MNN 的机器学习/深度学习算法前向推理。对于三端一体效果测试,工作台提供了基于 MNNCV 的 Playground UI Python API,该 API 能够实现一套代码在 PC / Android / iOS UI操作,避免进行多端界面开发,能够轻松在多个平台上进行算法验证。MNN 工作台还提供了一个三端一体模板工程,该工程将 MNN 以及 Debug UI 相关操作进行了封装,用户只需要关注与具体算法或者业务相关的逻辑代码实现,便可以实现 MNN 三端一体工程
image.png
图例 1. 开源框架算法转换为 MNN 三端一体流程示意图

2. 方案流程

2.1 模型转换

首先我们将开源框架的模型(如pb,onnx,caffe等)转换成MNN模型,工作台上提供了模型转换工具,能够将开源框架的模型转换为 MNN 模型,具体转换流程请参考《5.2 模型转换工具 Model Convertor》

2.2 创建MNN playground模板工程

  1. 打开工作台主界面,点击create a new project按钮,新建一个工程,如下图所示。

截屏2020-09-08下午7.19.46.png
图例 2. MNN工作台主界面

  1. 点击左侧栏的 Models 按钮,进入工作台开箱即用能力集页面,如下图所示。

截屏2020-11-26下午4.24.35.png
图例 3. MNN工作台开奖即用能力集界面

  1. 点击页面中的create Empty Playground按钮,新建一个三端一体 Playground 工程,如下图所示。输入工程名,如”Gan”。

截屏2020-11-26下午4.29.50.png
图例 4 新建一个 Playground 工程示意图

  1. 点击存储按钮,进入 Playground 界面,如下图所示。

截屏2020-11-26下午4.59.58.png
图例 5. MNN 工作台 Playground 模板界面

2.3 实现一个三端一体算法 Playground

2.3.1 模板工程目录介绍

下图展示了 Playground 模板工程的目录树,其中与工程相关的主要包括 base 文件夹,models 文件夹,截屏2020-11-26下午7.23.39.png
图例 6. MNN 工作台 Playground 模板工程目录树

my_project_forward.py 文件以及工程入口文件 main.py。 下表对每个文件的作用做介绍。

文件名 作用
app.mnnplayground 工作台playground工程文件
base/data_format.py 定义了所有cv相关图像输入数据格式,如RGB, BGR等
base/mnn_forward_base.py 定义了MNN模型前向推理的基类,主要包括网络定义,网络前处理,网络输入数据转换,网络推理以及模型后处理。该基类封装了与MNN相关的所有操作,用户可以不用关心诸如MNN网络定义,数据转换,推理等细节。
base/mnn_net.py 包含关于MNN网络定义,前向推理的类
models 用于存储与算法相关的模型文件,由用户自行放入
my_project_forward.py 定义了本工程算法的前向推理类,继承MNNModelForwardBase基类,用户只需要在
该文件实现与本算法和业务相关的前处理和后处理函数,可以实现算法基于MNN的前向推理流程
main.py 工程文件入口,通过MNNCV实现三端一体playground,能够同时在pc/ios/Android
上完成界面构建,网络推理,以及结果展示。
requirements.txt
requirements_inner.txt
系统自动配置的与MNN三端一体工程相关的依赖库

表1 MNN 工作台 Playground 模板工程文件及其作用列表

2.3.2 模型算法前向:补充算法前处理与后处理

MNN 工作台自动生成一个模型算法前向代码模板 my_project_forward.py,该 Python 文件定义了本工程相关算法的一个类模板,如下代码块所示。该类继承了 base 中的 MNNModelForwardBase 基类,用户只需要写与模型和业务相关的前处理和后处理函数,就可以实现基于 MNN 的前向推理。整个工程中与 MNN 相关的诸如 MNN 网络定义,输入数据格式转换以及网络前向推理都封装在 MNNModelFowardBase 中,用户可以不用关心。

  1. # -*- coding: UTF-8 -*-
  2. import cv2
  3. import numpy as np
  4. from base.mnn_forward_base import MNNModelForwardBase
  5. from base.data_format import DataFormat
  6. class GanForwardProcess(MNNModelForwardBase):
  7. def __init__(self,
  8. net_path,
  9. input_layer_names=list(),
  10. output_layer_names=list()):
  11. super(GanForwardProcess, self).__init__(net_path,
  12. input_layer_names,
  13. output_layer_names)
  14. pass
  15. def preprocess(self,
  16. input_data,
  17. data_format=DataFormat.RGBA,
  18. **kwargs):
  19. '''
  20. preprocess input data
  21. params:
  22. input_data: a numpy.ndarray, raw input data
  23. data_format: the data format of input data, RGBA, RGB, BGR etc.
  24. return:a dict its key is input tensor name and its value is input data
  25. '''
  26. input_data_dict = dict()
  27. return input_data_dict
  28. def postProcess(self, model_outputs_dict, **kwargs):
  29. '''
  30. do post process ops based on its algorithm
  31. params:
  32. model_outputs_dict:a dict which key is tensor name and value is tensor
  33. '''
  34. post_outputs = None
  35. return post_outputs

2.3.3 MNNCV 实现三端一体 Playground

在模型入口文件 main.py 我们基于 MNNCV 写了一个三端一体的 Playground 界面模板,如下代码块所示。MNNCV 提供了一套 PC / iOS / Android 通用的 Playground UI API,可以实现一套代码在三端中完成操作界面,网络推理,以及算法结果展示。更多关于MNNCV的介绍,可以参考 Playground API 使用手册

  1. #!/usr/bin/python
  2. # -*- coding: UTF-8 -*-
  3. import MNNCV
  4. import os
  5. from my_project_forward import GanForwardProcess
  6. #get env vars
  7. kit = MNNCV.Kit()
  8. vars = kit.getEnvVars()
  9. work_path = vars['workPath']
  10. #@TODO:mnn model name, end with .mnn
  11. MNN_MODEL_NAME = '.mnn'
  12. model_path = os.path.join(work_path, MNN_MODEL_NAME)
  13. #input and output tensor name list, default empty
  14. input_layer_names = list()
  15. output_layer_names = list()
  16. #create global net
  17. global_net = GanForwardProcess(model_path=model_path,
  18. input_layer_names=input_layer_names,
  19. output_layer_names=output_layer_names)
  20. class GanPlayground(object):
  21. @staticmethod
  22. def run():
  23. kit.open("image")
  24. kit.selectPhoto(GanPlayground.selectPhotoCallback)
  25. kit.setCloseCallback(GanPlayground.closeCallback)
  26. @staticmethod
  27. def selectPhotoCallback(data, format, imgw, imgh):
  28. #do model forward process
  29. outputs = global_net.process(data, format)
  30. #do debug ui
  31. pass
  32. @staticmethod
  33. def closeCallback(key=None):
  34. print "End of Callback"
  35. if __name__ == '__main__':
  36. GanPlayground.run()

main.py代码模板上已经完成了诸如工程环境配置8-10行,网络定义21-23行,以及运行时类定义25-42行。用户在此基础上需要将2.1中转换成功的mnn模型放入models文件夹中,然后在MNN_MODEL_NAME中修改对应的模型名称。

关于网络输入张量名称以及网络输出节点名称两个列表,在默认为空的情况下,工程会使用mnn模型中定义的输入输出节点名称。如果用户需要定制网络输入输出,如对网络进行中间层截断,可以添加到output_layer_names 列表中。对于查询模型节点名称,工作台提供了mnn模型可视化工具Model Visualizer,可以对 MNN 模型进行可视化,也可以查询对应的节点名称。关于如何使用该可视化工具,可参考《5.1 模型可视化工具 Model Visualizer》在完成网络定义后,需要做的就是利用 MNNCV 算法结果可视化。MNNCV提供了诸如关键点渲染,图片渲染,文本渲染等三端一体的操作。需要注意的是,模板代码默认使用图片作为输入数据源,其对应代码为kit.open('image'),用户可以根据具体的算法对输入源做相应的改变。

2.3.4 算法结果显示与端上推送

在完成上述步骤之后,我们便可以在pc上展示算法结果并进行效果调试,完成调试后,智能推送到端测(iOS/Android)进行端测验证。
先点击当前界面右上角的截屏2020-11-27上午10.36.04.png按钮,选择当前工程的入口文件,如下图所示,模板工程默认入口文件为main.py
截屏2020-11-27上午10.33.32.png
图例 7. Playground 工程选择入口文件示例图
入口文件选择完成后,我们点击 Preview 按钮截屏2020-11-27上午10.37.40.png,工作台会从运行入口文件,如main.py,如果出现代码问题,会显示代码的错误信息,如下图所示。
截屏2020-11-27上午10.39.40.png
图例 8. Playground 代码preview错误展示示例图

如果代码不存在问题,则会进入到 PC 端的 Playground 界面,如下图所示。
截屏2020-11-27上午10.48.42.png
图例 9. Playground 代码 Preview 正常示例图
完成调试后,点击左上角截屏2020-11-27上午11.40.33.png,页面出现用于连接移动端设备(如手机)和工作台的二维码,如下图所示。
截屏2020-11-27上午11.49.45.png
图例 10. Playground 用于连接端测与工作台的二维码

用 Playground App 扫码,将手机与工作台连接。连接成功后,页面左上角会显示连接的手机名称,本例手机名称为”DeviceAIPhone”,如下图所示。
截屏2020-11-27上午11.50.52.png图例 11. Playground 连接手机设备示例图

点击推动按钮截屏2020-11-27下午2.21.31.png,将 Playground 推动到端侧,如手机,则端上会出现以下应用界面。

tmp_result.JPG

图例 12. 端侧 Playground UI 界面示例图

该示例中提供了两种数据输入方式,一种是通过从相册获取图片数据,一种是通过拍照获得图片数据。选择其中一种获得图片后,点击推理,就能完成模型的前向推理流程并展示对应的算法结果。

3. 开源工程到 MNN 三端一体 Playground 转换示例

本章节以一个简单的风格迁移 Tensorflow 前向推理工程为示例,展示如何将开源框架前向工程转换为 MNN 工作台三端一体工程。关于 Tensorflow 工程所有的文件,可以在附件中下载。该工程是实现了将任意的图片转换为漫画风格,效果如下图所示。
image.png
图例 13. 漫画风格迁移效果图(左图为原图,右图为风格迁移后图片)

该 Tensorflow 工程目录树下如下图所示。
截屏2020-11-30下午4.05.25.png
图例 14. Tensorflow 开源工程目录树

其中网络模型文件为 gan.pb,模型前向推理代码为 tf_model_forward.py。以下将分步骤介绍如何将该工程转换为 MNN 工作台三端一体工程。

  1. 首先,我们利用2.1中介绍的工作台模型转换工具《5.2 模型转换工具 Model Convertor》将 gan.pb 文件转换为 MNN 模型文件 gan.mnn。

  2. 我们利用《5.1 模型可视化工具 Model Visualizer》打开 gan.mnn 模型,如下图所示,找到模型输入输出节点的名称。出现该图,说明模型已经转换成功,我们通过可视化模型结构图可以找到模型输入输出模型节点名称。在本例中,输入节点名称为“input_1”,输出节点为“StatefulPartitionedCall/StatefulPartitionedCall/Generator/activation/Tanh”。

截屏2020-11-30下午4.18.24.png
图例 15. 风格迁移MNN模型可视化图

  1. 根据开源工程完成算法前处理和后处理函数

按照2.2节新建一个名称为”Gan”的模板工程,然后我们根据开源工程完成算法前处理和后处理函数。本
例中TensorFlow的前向代码在 tf_model_forward.py中,模型推理代码如下所示。

  1. def process(self,
  2. input_data,
  3. data_format):
  4. detection_results = list()
  5. outputs = None
  6. #preprocess input data
  7. if input_data is not None:
  8. if data_format == DataFormat.RGBA:
  9. image_data = cv2.cvtColor(input_data, cv2.COLOR_RGBA2RGB)
  10. elif data_format == DataFormat.BGR:
  11. image_data = cv2.cvtColor(input_data, cv2.COLOR_BGR2RGB)
  12. elif data_format == DataFormat.GRAY:
  13. image_data = cv2.cvtColor(input_data, cv2.COLOR_GRAY2RGB)
  14. elif data_format == DataFormat.BGRA:
  15. image_data = cv2.cvtColor(input_data, cv2.COLOR_BGRA2RGB)
  16. elif data_format == DataFormat.YUV_NV21:
  17. image_data = cv2.cvtColor(input_data, cv2.COLOR_YUV2RGB_NV21)
  18. elif data_format == DataFormat.YUV_NV12:
  19. image_data = cv2.cvtColor(input_data, cv2.COLOR_YUV2RGB_NV12)
  20. else:
  21. image_data = input_data
  22. self.shape = input_data.shape
  23. image_height, image_width, _ = image_data.shape
  24. if image_height > self.max_size_len or image_width > self.max_size_len:
  25. image_data = self._resize_process(image_data)
  26. image_data = np.expand_dims(image_data, 0).astype(np.float32) / 127.5 - 1.0
  27. # Definite input and output Tensors
  28. output_tensor_name = 'StatefulPartitionedCall/StatefulPartitionedCall/Generator/activation/Tanh:0'
  29. input_tensor_name = 'input_1:0'
  30. input_tensor = self.detection_graph.get_tensor_by_name(input_tensor_name)
  31. output_tensor = self.detection_graph.get_tensor_by_name(output_tensor_name)
  32. #do inference
  33. model_outputs = self.sess.run(
  34. [output_tensor],
  35. feed_dict={input_tensor: image_data})
  36. #postprocess: transfer float to uint8 fromat
  37. out = ((np.squeeze(model_outputs[0]) + 1) * 127.5).astype(np.uint8)
  38. out = cv2.cvtColor(out, cv2.COLOR_RGB2BGR)
  39. outputs = cv2.resize(out, (self.shape[1], self.shape[0]))
  40. return outputs

我们可以发现,该模型前向代码主要包括算法前处理8-26行,获得模型输入输出节点28-32,模型前向推理34-37以及模型后处理39-42。根据该代码我们在新建的模板工程中 my_project_forward.py 中完成算法前处理和后处理,如下代码所示。需要注意的是,前处理函数的返回值是一个字典,其中键为模型输入节点名称,值为输入该模型节点的数据。在后处理函数中,模型推理结果也是存在一个以模型节点名称为键,模型输出节点数据为值的字典,可以通过模型输出节点名称获得对应的模型推理结果,然后进行后处理。

  1. class GanForwardProcess(MNNModelForwardBase):
  2. '''create gan net and do image transfer process'''
  3. def __init__(self,
  4. model_path,
  5. input_layer_names=list(),
  6. output_layer_names=list()):
  7. super(GanForwardProcess, self).__init__(model_path,
  8. input_layer_names,
  9. output_layer_names)
  10. #set max net input size
  11. self.max_size_len = 480
  12. self.shape = list()
  13. def preprocess(self,
  14. input_data,
  15. data_format=0,
  16. **kwargs):
  17. '''
  18. preprocess input data
  19. params:
  20. input_data: a numpy.ndarray, raw input data
  21. data_format: the data format of input data, RGBA, RGB, BGR etc.
  22. return:a dict its key is input tensor name and its value is input data
  23. '''
  24. self.shape = input_data.shape
  25. input_data_dict = dict()
  26. if input_data is not None:
  27. #convert input image in RGB format
  28. if data_format == DataFormat.RGBA:
  29. image_data = cv2.cvtColor(input_data, cv2.COLOR_RGBA2RGB)
  30. elif data_format == DataFormat.BGR:
  31. image_data = cv2.cvtColor(input_data, cv2.COLOR_BGR2RGB)
  32. elif data_format == DataFormat.GRAY:
  33. image_data = cv2.cvtColor(input_data, cv2.COLOR_GRAY2RGB)
  34. elif data_format == DataFormat.BGRA:
  35. image_data = cv2.cvtColor(input_data, cv2.COLOR_BGRA2RGB)
  36. elif data_format == DataFormat.YUV_NV21:
  37. image_data = cv2.cvtColor(input_data, cv2.COLOR_YUV2RGB_NV21)
  38. elif data_format == DataFormat.YUV_NV12:
  39. image_data = cv2.cvtColor(input_data, cv2.COLOR_YUV2RGB_NV12)
  40. else:
  41. image_data = input_data
  42. image_height, image_width, _ = image_data.shape
  43. if image_height > self.max_size_len or image_width > self.max_size_len:
  44. image_data = self._resize_process(image_data)
  45. image_data = np.expand_dims(image_data, 0).astype(np.float32) / 127.5 - 1.0
  46. input_data_dict['input_1'] = image_data
  47. return input_data_dict
  48. def postProcess(self,
  49. model_outputs,
  50. **kwargs):
  51. '''
  52. do post process ops based on its algorithm
  53. params:
  54. model_outputs_dict:a dict which key is tensor name and value is tensor
  55. '''
  56. output_tensor_name = 'StatefulPartitionedCall/StatefulPartitionedCall/Generator/activation/Tanh'
  57. out = ((np.squeeze(model_outputs[output_tensor_name]) + 1) * 127.5).astype(np.uint8)
  58. out = cv2.cvtColor(out, cv2.COLOR_RGB2BGR)
  59. concat_image_1 = cv2.resize(out, (self.shape[1], self.shape[0]))
  60. return concat_image_1
  1. 用 MNNCV 实现三端一体 UI 界面

当完成模型前后处理后,我们可以通过 MNNCV 写一套三端一体 UI 界面,主要在 main.py 文件中,如下面代码所示。

  1. #!/usr/bin/python
  2. # -*- coding: UTF-8 -*-
  3. import MNNCV
  4. import os
  5. from my_project_forward import GanForwardProcess
  6. #get env vars
  7. kit = MNNCV.Kit()
  8. vars = kit.getEnvVars()
  9. work_path = vars['workPath']
  10. #@TODO:mnn model name, end with .mnn
  11. MNN_MODEL_NAME = 'models/gan.mnn'
  12. model_path = os.path.join(work_path, MNN_MODEL_NAME)
  13. #input and output tensor name list, default empty
  14. input_layer_names = list(['input_1'])
  15. output_layer_names = list(['StatefulPartitionedCall/StatefulPartitionedCall/Generator/activation/Tanh'])
  16. #create global net
  17. global_net = GanForwardProcess(model_path=model_path,
  18. input_layer_names=input_layer_names,
  19. output_layer_names=output_layer_names)
  20. class GanPlayground(object):
  21. @staticmethod
  22. def run():
  23. kit.open("image")
  24. kit.selectPhoto(GanPlayground.selectPhotoCallback)
  25. kit.setCloseCallback(GanPlayground.closeCallback)
  26. @staticmethod
  27. def selectPhotoCallback(data, format, imgw, imgh):
  28. #do model forward process
  29. outputs = global_net.process(data, format)
  30. #do debug ui
  31. kit.drawImage(outputs)
  32. @staticmethod
  33. def closeCallback(key=None):
  34. print "End of Callback"
  35. if __name__ == '__main__':
  36. GanPlayground.run()

首先,将步骤1中模型转换得到的 MNN 文件放入到工程中的 models 文件夹中,并将 main.py中的MNN_MODEL_NAME 进行对应的修改,如代码中12行所示。同时,将模型的输入输出节点名称改为2中得到的模型输入输出节点名称。此时,整个模型前向推理相关代码已经完成,我们最后需要做的就是结果显示。由于模型输出结果为图片,我们使用 MNNCV 中的 drawImage 函数,将图片展示出来,如代码中第38行所示。点击 Preview 按钮,选择图片,我们可以看到在右侧 Playground 中有以下输出结果。
截屏2020-11-30下午5.27.23.png
图例 17. 漫画风格迁移 Playground 结果展示图

按照2.4节中的步骤将手机连上工作台,将该工程推送到手机端,我们选择图片,点击推理按钮,得到以下结果。
orin.JPGtransfered.JPG
图例 18. 端侧 Playground 漫画风格迁移结果图

到此,我们完成风格迁移 Tensorflow 工程到 MNN 工作台三端一体 Playground 工程转换。

4. 附件

  • 风格迁移TensorFlow前向推理工程-tf_gan.zip