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 cv2
import numpy as np
from base.mnn_forward_base import MNNModelForwardBase
from base.data_format import DataFormat
class 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)
pass
def preprocess(self,
input_data,
data_format=DataFormat.RGBA,
**kwargs):
'''
preprocess input data
params:
input_data: a numpy.ndarray, raw input data
data_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_dict
def postProcess(self, model_outputs_dict, **kwargs):
'''
do post process ops based on its algorithm
params:
model_outputs_dict:a dict which key is tensor name and value is tensor
'''
post_outputs = None
return 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 MNNCV
import os
from my_project_forward import GanForwardProcess
#get env vars
kit = MNNCV.Kit()
vars = kit.getEnvVars()
work_path = vars['workPath']
#@TODO:mnn model name, end with .mnn
MNN_MODEL_NAME = '.mnn'
model_path = os.path.join(work_path, MNN_MODEL_NAME)
#input and output tensor name list, default empty
input_layer_names = list()
output_layer_names = list()
#create global net
global_net = GanForwardProcess(model_path=model_path,
input_layer_names=input_layer_names,
output_layer_names=output_layer_names)
class GanPlayground(object):
@staticmethod
def run():
kit.open("image")
kit.selectPhoto(GanPlayground.selectPhotoCallback)
kit.setCloseCallback(GanPlayground.closeCallback)
@staticmethod
def selectPhotoCallback(data, format, imgw, imgh):
#do model forward process
outputs = global_net.process(data, format)
#do debug ui
pass
@staticmethod
def 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 data
if 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_data
self.shape = input_data.shape
image_height, image_width, _ = image_data.shape
if 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 Tensors
output_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 inference
model_outputs = self.sess.run(
[output_tensor],
feed_dict={input_tensor: image_data})
#postprocess: transfer float to uint8 fromat
out = ((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 size
self.max_size_len = 480
self.shape = list()
def preprocess(self,
input_data,
data_format=0,
**kwargs):
'''
preprocess input data
params:
input_data: a numpy.ndarray, raw input data
data_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.shape
input_data_dict = dict()
if input_data is not None:
#convert input image in RGB format
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_data
image_height, image_width, _ = image_data.shape
if 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
input_data_dict['input_1'] = image_data
return input_data_dict
def postProcess(self,
model_outputs,
**kwargs):
'''
do post process ops based on its algorithm
params:
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 MNNCV
import os
from my_project_forward import GanForwardProcess
#get env vars
kit = MNNCV.Kit()
vars = kit.getEnvVars()
work_path = vars['workPath']
#@TODO:mnn model name, end with .mnn
MNN_MODEL_NAME = 'models/gan.mnn'
model_path = os.path.join(work_path, MNN_MODEL_NAME)
#input and output tensor name list, default empty
input_layer_names = list(['input_1'])
output_layer_names = list(['StatefulPartitionedCall/StatefulPartitionedCall/Generator/activation/Tanh'])
#create global net
global_net = GanForwardProcess(model_path=model_path,
input_layer_names=input_layer_names,
output_layer_names=output_layer_names)
class GanPlayground(object):
@staticmethod
def run():
kit.open("image")
kit.selectPhoto(GanPlayground.selectPhotoCallback)
kit.setCloseCallback(GanPlayground.closeCallback)
@staticmethod
def selectPhotoCallback(data, format, imgw, imgh):
#do model forward process
outputs = global_net.process(data, format)
#do debug ui
kit.drawImage(outputs)
@staticmethod
def 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