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 三端一体工程。
图例 1. 开源框架算法转换为 MNN 三端一体流程示意图
2. 方案流程
2.1 模型转换
首先我们将开源框架的模型(如pb,onnx,caffe等)转换成MNN模型,工作台上提供了模型转换工具,能够将开源框架的模型转换为 MNN 模型,具体转换流程请参考《5.2 模型转换工具 Model Convertor》
2.2 创建MNN playground模板工程
- 打开工作台主界面,点击
create a new project按钮,新建一个工程,如下图所示。

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

图例 3. MNN工作台开奖即用能力集界面
- 点击页面中的
create Empty Playground按钮,新建一个三端一体 Playground 工程,如下图所示。输入工程名,如”Gan”。

图例 4 新建一个 Playground 工程示意图
- 点击
存储按钮,进入 Playground 界面,如下图所示。

图例 5. MNN 工作台 Playground 模板界面
2.3 实现一个三端一体算法 Playground
2.3.1 模板工程目录介绍
下图展示了 Playground 模板工程的目录树,其中与工程相关的主要包括 base 文件夹,models 文件夹,
图例 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 中,用户可以不用关心。
# -*- coding: UTF-8 -*-import cv2import numpy as npfrom base.mnn_forward_base import MNNModelForwardBasefrom base.data_format import DataFormatclass GanForwardProcess(MNNModelForwardBase):def __init__(self,net_path,input_layer_names=list(),output_layer_names=list()):super(GanForwardProcess, self).__init__(net_path,input_layer_names,output_layer_names)passdef preprocess(self,input_data,data_format=DataFormat.RGBA,**kwargs):'''preprocess input dataparams:input_data: a numpy.ndarray, raw input datadata_format: the data format of input data, RGBA, RGB, BGR etc.return:a dict its key is input tensor name and its value is input data'''input_data_dict = dict()return input_data_dictdef postProcess(self, model_outputs_dict, **kwargs):'''do post process ops based on its algorithmparams:model_outputs_dict:a dict which key is tensor name and value is tensor'''post_outputs = Nonereturn post_outputs
2.3.3 MNNCV 实现三端一体 Playground
在模型入口文件 main.py 我们基于 MNNCV 写了一个三端一体的 Playground 界面模板,如下代码块所示。MNNCV 提供了一套 PC / iOS / Android 通用的 Playground UI API,可以实现一套代码在三端中完成操作界面,网络推理,以及算法结果展示。更多关于MNNCV的介绍,可以参考 Playground API 使用手册
#!/usr/bin/python# -*- coding: UTF-8 -*-import MNNCVimport osfrom my_project_forward import GanForwardProcess#get env varskit = MNNCV.Kit()vars = kit.getEnvVars()work_path = vars['workPath']#@TODO:mnn model name, end with .mnnMNN_MODEL_NAME = '.mnn'model_path = os.path.join(work_path, MNN_MODEL_NAME)#input and output tensor name list, default emptyinput_layer_names = list()output_layer_names = list()#create global netglobal_net = GanForwardProcess(model_path=model_path,input_layer_names=input_layer_names,output_layer_names=output_layer_names)class GanPlayground(object):@staticmethoddef run():kit.open("image")kit.selectPhoto(GanPlayground.selectPhotoCallback)kit.setCloseCallback(GanPlayground.closeCallback)@staticmethoddef selectPhotoCallback(data, format, imgw, imgh):#do model forward processoutputs = global_net.process(data, format)#do debug uipass@staticmethoddef closeCallback(key=None):print "End of Callback"if __name__ == '__main__':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)进行端测验证。
先点击当前界面右上角的
按钮,选择当前工程的入口文件,如下图所示,模板工程默认入口文件为main.py。
图例 7. Playground 工程选择入口文件示例图
入口文件选择完成后,我们点击 Preview 按钮
,工作台会从运行入口文件,如main.py,如果出现代码问题,会显示代码的错误信息,如下图所示。
图例 8. Playground 代码preview错误展示示例图
如果代码不存在问题,则会进入到 PC 端的 Playground 界面,如下图所示。
图例 9. Playground 代码 Preview 正常示例图
完成调试后,点击左上角
,页面出现用于连接移动端设备(如手机)和工作台的二维码,如下图所示。
图例 10. Playground 用于连接端测与工作台的二维码
用 Playground App 扫码,将手机与工作台连接。连接成功后,页面左上角会显示连接的手机名称,本例手机名称为”DeviceAIPhone”,如下图所示。
图例 11. Playground 连接手机设备示例图
点击推动按钮
,将 Playground 推动到端侧,如手机,则端上会出现以下应用界面。
图例 12. 端侧 Playground UI 界面示例图
该示例中提供了两种数据输入方式,一种是通过从相册获取图片数据,一种是通过拍照获得图片数据。选择其中一种获得图片后,点击推理,就能完成模型的前向推理流程并展示对应的算法结果。
3. 开源工程到 MNN 三端一体 Playground 转换示例
本章节以一个简单的风格迁移 Tensorflow 前向推理工程为示例,展示如何将开源框架前向工程转换为 MNN 工作台三端一体工程。关于 Tensorflow 工程所有的文件,可以在附件中下载。该工程是实现了将任意的图片转换为漫画风格,效果如下图所示。
图例 13. 漫画风格迁移效果图(左图为原图,右图为风格迁移后图片)
该 Tensorflow 工程目录树下如下图所示。
图例 14. Tensorflow 开源工程目录树
其中网络模型文件为 gan.pb,模型前向推理代码为 tf_model_forward.py。以下将分步骤介绍如何将该工程转换为 MNN 工作台三端一体工程。
首先,我们利用2.1中介绍的工作台模型转换工具《5.2 模型转换工具 Model Convertor》将 gan.pb 文件转换为 MNN 模型文件 gan.mnn。
我们利用《5.1 模型可视化工具 Model Visualizer》打开 gan.mnn 模型,如下图所示,找到模型输入输出节点的名称。出现该图,说明模型已经转换成功,我们通过可视化模型结构图可以找到模型输入输出模型节点名称。在本例中,输入节点名称为“input_1”,输出节点为“StatefulPartitionedCall/StatefulPartitionedCall/Generator/activation/Tanh”。

图例 15. 风格迁移MNN模型可视化图
- 根据开源工程完成算法前处理和后处理函数
按照2.2节新建一个名称为”Gan”的模板工程,然后我们根据开源工程完成算法前处理和后处理函数。本
例中TensorFlow的前向代码在 tf_model_forward.py中,模型推理代码如下所示。
def process(self,input_data,data_format):detection_results = list()outputs = None#preprocess input dataif input_data is not None:if data_format == DataFormat.RGBA:image_data = cv2.cvtColor(input_data, cv2.COLOR_RGBA2RGB)elif data_format == DataFormat.BGR:image_data = cv2.cvtColor(input_data, cv2.COLOR_BGR2RGB)elif data_format == DataFormat.GRAY:image_data = cv2.cvtColor(input_data, cv2.COLOR_GRAY2RGB)elif data_format == DataFormat.BGRA:image_data = cv2.cvtColor(input_data, cv2.COLOR_BGRA2RGB)elif data_format == DataFormat.YUV_NV21:image_data = cv2.cvtColor(input_data, cv2.COLOR_YUV2RGB_NV21)elif data_format == DataFormat.YUV_NV12:image_data = cv2.cvtColor(input_data, cv2.COLOR_YUV2RGB_NV12)else:image_data = input_dataself.shape = input_data.shapeimage_height, image_width, _ = image_data.shapeif image_height > self.max_size_len or image_width > self.max_size_len:image_data = self._resize_process(image_data)image_data = np.expand_dims(image_data, 0).astype(np.float32) / 127.5 - 1.0# Definite input and output Tensorsoutput_tensor_name = 'StatefulPartitionedCall/StatefulPartitionedCall/Generator/activation/Tanh:0'input_tensor_name = 'input_1:0'input_tensor = self.detection_graph.get_tensor_by_name(input_tensor_name)output_tensor = self.detection_graph.get_tensor_by_name(output_tensor_name)#do inferencemodel_outputs = self.sess.run([output_tensor],feed_dict={input_tensor: image_data})#postprocess: transfer float to uint8 fromatout = ((np.squeeze(model_outputs[0]) + 1) * 127.5).astype(np.uint8)out = cv2.cvtColor(out, cv2.COLOR_RGB2BGR)outputs = cv2.resize(out, (self.shape[1], self.shape[0]))return outputs
我们可以发现,该模型前向代码主要包括算法前处理8-26行,获得模型输入输出节点28-32,模型前向推理34-37以及模型后处理39-42。根据该代码我们在新建的模板工程中 my_project_forward.py 中完成算法前处理和后处理,如下代码所示。需要注意的是,前处理函数的返回值是一个字典,其中键为模型输入节点名称,值为输入该模型节点的数据。在后处理函数中,模型推理结果也是存在一个以模型节点名称为键,模型输出节点数据为值的字典,可以通过模型输出节点名称获得对应的模型推理结果,然后进行后处理。
class GanForwardProcess(MNNModelForwardBase):'''create gan net and do image transfer process'''def __init__(self,model_path,input_layer_names=list(),output_layer_names=list()):super(GanForwardProcess, self).__init__(model_path,input_layer_names,output_layer_names)#set max net input sizeself.max_size_len = 480self.shape = list()def preprocess(self,input_data,data_format=0,**kwargs):'''preprocess input dataparams:input_data: a numpy.ndarray, raw input datadata_format: the data format of input data, RGBA, RGB, BGR etc.return:a dict its key is input tensor name and its value is input data'''self.shape = input_data.shapeinput_data_dict = dict()if input_data is not None:#convert input image in RGB formatif data_format == DataFormat.RGBA:image_data = cv2.cvtColor(input_data, cv2.COLOR_RGBA2RGB)elif data_format == DataFormat.BGR:image_data = cv2.cvtColor(input_data, cv2.COLOR_BGR2RGB)elif data_format == DataFormat.GRAY:image_data = cv2.cvtColor(input_data, cv2.COLOR_GRAY2RGB)elif data_format == DataFormat.BGRA:image_data = cv2.cvtColor(input_data, cv2.COLOR_BGRA2RGB)elif data_format == DataFormat.YUV_NV21:image_data = cv2.cvtColor(input_data, cv2.COLOR_YUV2RGB_NV21)elif data_format == DataFormat.YUV_NV12:image_data = cv2.cvtColor(input_data, cv2.COLOR_YUV2RGB_NV12)else:image_data = input_dataimage_height, image_width, _ = image_data.shapeif image_height > self.max_size_len or image_width > self.max_size_len:image_data = self._resize_process(image_data)image_data = np.expand_dims(image_data, 0).astype(np.float32) / 127.5 - 1.0input_data_dict['input_1'] = image_datareturn input_data_dictdef postProcess(self,model_outputs,**kwargs):'''do post process ops based on its algorithmparams:model_outputs_dict:a dict which key is tensor name and value is tensor'''output_tensor_name = 'StatefulPartitionedCall/StatefulPartitionedCall/Generator/activation/Tanh'out = ((np.squeeze(model_outputs[output_tensor_name]) + 1) * 127.5).astype(np.uint8)out = cv2.cvtColor(out, cv2.COLOR_RGB2BGR)concat_image_1 = cv2.resize(out, (self.shape[1], self.shape[0]))return concat_image_1
- 用 MNNCV 实现三端一体 UI 界面
当完成模型前后处理后,我们可以通过 MNNCV 写一套三端一体 UI 界面,主要在 main.py 文件中,如下面代码所示。
#!/usr/bin/python# -*- coding: UTF-8 -*-import MNNCVimport osfrom my_project_forward import GanForwardProcess#get env varskit = MNNCV.Kit()vars = kit.getEnvVars()work_path = vars['workPath']#@TODO:mnn model name, end with .mnnMNN_MODEL_NAME = 'models/gan.mnn'model_path = os.path.join(work_path, MNN_MODEL_NAME)#input and output tensor name list, default emptyinput_layer_names = list(['input_1'])output_layer_names = list(['StatefulPartitionedCall/StatefulPartitionedCall/Generator/activation/Tanh'])#create global netglobal_net = GanForwardProcess(model_path=model_path,input_layer_names=input_layer_names,output_layer_names=output_layer_names)class GanPlayground(object):@staticmethoddef run():kit.open("image")kit.selectPhoto(GanPlayground.selectPhotoCallback)kit.setCloseCallback(GanPlayground.closeCallback)@staticmethoddef selectPhotoCallback(data, format, imgw, imgh):#do model forward processoutputs = global_net.process(data, format)#do debug uikit.drawImage(outputs)@staticmethoddef closeCallback(key=None):print "End of Callback"if __name__ == '__main__':GanPlayground.run()
首先,将步骤1中模型转换得到的 MNN 文件放入到工程中的 models 文件夹中,并将 main.py中的MNN_MODEL_NAME 进行对应的修改,如代码中12行所示。同时,将模型的输入输出节点名称改为2中得到的模型输入输出节点名称。此时,整个模型前向推理相关代码已经完成,我们最后需要做的就是结果显示。由于模型输出结果为图片,我们使用 MNNCV 中的 drawImage 函数,将图片展示出来,如代码中第38行所示。点击 Preview 按钮,选择图片,我们可以看到在右侧 Playground 中有以下输出结果。
图例 17. 漫画风格迁移 Playground 结果展示图
按照2.4节中的步骤将手机连上工作台,将该工程推送到手机端,我们选择图片,点击推理按钮,得到以下结果。


图例 18. 端侧 Playground 漫画风格迁移结果图
到此,我们完成风格迁移 Tensorflow 工程到 MNN 工作台三端一体 Playground 工程转换。
4. 附件
- 风格迁移TensorFlow前向推理工程-tf_gan.zip

