官方文档
有疑问一定要先查这里!
MNN是一个轻量级的深度神经网络推理引擎,在端侧加载深度神经网络模型进行推理预测。目前,MNN已经在阿里巴巴的手机淘宝、手机天猫、优酷等20多个App中使用,覆盖直播、短视频、搜索推荐、商品图像搜索、互动营销、权益发放、安全风控等场景。此外,IoT等场景下也有若干应用。
MNN可以分为Converter和Interpreter两部分。
Converter由Frontends和Graph Optimize构成。前者负责支持不同的训练框架,MNN当前支持Tensorflow(Lite)、Caffe和ONNX(PyTorch/MXNet的模型可先转为ONNX模型再转到MNN);后者通过算子融合、算子替代、布局调整等方式优化图。
Interpreter由Engine和Backends构成。前者负责模型的加载、计算图的调度;后者包含各计算设备下的内存分配、Op实现。在Engine和Backends中,MNN应用了多种优化方案,包括在卷积和反卷积中应用Winograd算法、在矩阵乘法中应用Strassen算法、低精度计算、Neon优化、手写汇编、多线程优化、内存复用、异构计算等。
MNN作为开开源时间并不长的框架,社区的建设相对于NCNN来讲毕竟还是有所欠缺,好在官方已经在努力不断建设社区的内容,建立了很多钉钉群,相关的问题可以先在github里提交issue之后再去钉钉群中解答。具体的链接在MNN的Github的主页里可以找到,请点击这里。一些常见的使用问题可以在这里找到相关的解答。
物体分类样例
车道检测样例
直线检测
人脸识别
中文字OCR
其他案例汇总
由于有了之前我们部署NCNN的经验,大致上的端侧部署的原理都是相通的,NCNN是以模型推理器(Extractor)的形式来处理数据,加载模型,而到了MNN则是以解释器(Interpreter)的形式来加载数据和模型,处理数据的。
使用MNN推理时,有两个层级的抽象,分别是解释器Interpreter和会话Session。Interpreter是模型数据的持有者;Session通过Interpreter创建,是推理数据的持有者。多个推理可以共用同一个模型,即,多个Session可以共用一个Interpreter。
一个标准的运行流程包括以下几部分:
创建会话->输入数据->运行会话->获取输出
MNN输入数据是按照tensor的方式输入的,这块包括两大部分,数据的输入和预处理。
Tensor
Interpreter上提供了两个用于获取输入Tensor的方法:getSessionInput用于获取单个输入tensor,
getSessionInputAll用于获取输入tensor映射。
下面是官方给的图像处理的标准处理:
auto input = net->getSessionInput(session, NULL);
auto output = net->getSessionOutput(session, NULL);
auto dims = input->shape();
int bpp = dims[1];
int size_h = dims[2];
int size_w = dims[3];
auto inputPatch = argv[2];
FREE_IMAGE_FORMAT f = FreeImage_GetFileType(inputPatch);
FIBITMAP* bitmap = FreeImage_Load(f, inputPatch);
auto newBitmap = FreeImage_ConvertTo32Bits(bitmap);
auto width = FreeImage_GetWidth(newBitmap);
auto height = FreeImage_GetHeight(newBitmap);
FreeImage_Unload(bitmap);
Matrix trans;
//Dst -> [0, 1]
trans.postScale(1.0/size_w, 1.0/size_h);
//Flip Y (因为 FreeImage 解出来的图像排列是Y方向相反的)
trans.postScale(1.0,-1.0, 0.0, 0.5);
//[0, 1] -> Src
trans.postScale(width, height);
ImageProcess::Config config;
config.filterType = NEAREST;
float mean[3] = {103.94f, 116.78f, 123.68f};
float normals[3] = {0.017f,0.017f,0.017f};
::memcpy(config.mean, mean, sizeof(mean));
::memcpy(config.normal, normals, sizeof(normals));
config.sourceFormat = RGBA;
config.destFormat = BGR;
std::shared_ptr<ImageProcess> pretreat(ImageProcess::create(config));
pretreat->setMatrix(trans);
pretreat->convert((uint8_t*)FreeImage_GetScanLine(newBitmap, 0), width, height, 0, input);
net->runSession(session);
从代码可以看到,用到了Freeimage的库,鉴于我们大部分使用的还是opencv,这里也给一个替换的教程阿里MNN移动端部署框架,将FreeImage更换为opencv的实现。
这一块比较简单,一般来讲直接调用即可。
runSession(Session* session) const;
输出这块也是用的tensor
auto outputTensor = interpreter->getSessionOutput(session, NULL);
auto nchwTensor = new Tensor(outputTensor, Tensor::CAFFE);
outputTensor->copyToHostTensor(nchwTensor);
auto score = nchwTensor->host<float>()[0];
auto index = nchwTensor->host<float>()[1];
// ...
delete nchwTensor;
然后把相关的指针赋值给输出的数组或者mat即可。
接下来为流程的汇总调用。
#include
#include
#include
#include
#include
#include
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "MNNExample", __VA_ARGS__)
static MNN::Interpreter *interpreter = nullptr;
static MNN::Session *session = nullptr;
static MNN::Tensor *inputTensor = nullptr;
static MNN::Tensor *outputTensor = nullptr;
extern "C" {
JNIEXPORT jint JNICALL
Java_com_example_mnnexample_MNNRunner_init(JNIEnv *env, jobject instance, jstring modelPath_) {
// 获取模型路径
const char *modelPath = env->GetStringUTFChars(modelPath_, 0);
// 创建解释器
interpreter = MNN::Interpreter::createFromFile(modelPath);
MNN::ScheduleConfig config;
config.numThread = 1; // 设置线程数
session = interpreter->createSession(config);
// 获取输入输出张量
std::vector<MNN::Tensor *> inputs = interpreter->getSessionInputAll(session);
std::vector<MNN::Tensor *> outputs = interpreter->getSessionOutputAll(session);
inputTensor = inputs[0];
outputTensor = outputs[0];
// 释放字符串
env->ReleaseStringUTFChars(modelPath_, modelPath);
return 0;
}
JNIEXPORT jfloatArray JNICALL
Java_com_example_mnnexample_MNNRunner_predict(JNIEnv *env, jobject instance,
jfloatArray data_, jint width, jint height,
jint channel) {
// 获取传入的数据
jfloat *data = env->GetFloatArrayElements(data_, NULL);
if (data == NULL) {
return NULL;
}
// 输入张量的形状为 NHWC
inputTensor->resize({1, height, width, channel});
inputTensor->copyFromHostFloat(data);
// 运行模型
interpreter->runSession(session);
// 获取输出张量数据
std::vector<int> dims = outputTensor->shape();
jfloatArray output = env->NewFloatArray(dims[1]);
outputTensor->copyToHostFloat(output);
// 释放内存
env->ReleaseFloatArrayElements(data_, data, 0);
return output;
}
JNIEXPORT void JNICALL
Java_com_example_mnnexample_MNNRunner_release(JNIEnv *env, jobject instance) {
// 释放资源
inputTensor->release();
outputTensor->release();
interpreter->releaseSession(session);
delete interpreter;
}
}
其中,Java_com_example_mnnexample_MNNRunner_init
函数用于初始化模型解释器,Java_com_example_mnnexample_MNNRunner_predict
函数用于运行模型,并返回输出结果,Java_com_example_mnnexample_MNNRunner_release
函数用于释放资源。
需要注意的是,安卓JNI中访问Java类成员变量或者调用Java类的函数,需要使用env->GetObjectClass(instance)
获取Java类对象。
相对来讲,目前我们过了两个端侧部署框架NCNN和MNN,
NCNN突出的特点在于:
MNN的突出特点在于: