在Android端实现多个ncnn模型部署并实现并行推理
本次基于个人开发的盲图AI——辅助盲人阅读的智能APP项目的需求来实现,一开始遇到了非常多的技术瓶颈,例如模型部署,识别,绘制等,遇到了各种各样的技术壁垒,都是从零基础开始学起。以下介绍这次开发中所学到的关键技术点。
(1)部署yolo ncnn与mediapipe hand ncnn模型并实现同时推理
相信大家在做人工智能方向的app的时候都会遇到一个模型无法满足需求的时候吧?今天我来聊聊如何在一个app里面,整合两个ncnn模型并让他们实现同时运作并且互不干扰。
首先你得学会如何在移动端部署ncnn模型,具体看我另外一篇文章:关于ncnn模型在移动端的部署
其实部署两个Ncnn模型,无非就是两个Ncnn:Net,也就是说你需要在cpp层里面创建两个Net对象,后续就是持有这两个对象去做事情。首先创建两个Net对象,后创建2个集合 , 分别接受各自的返回结果 , 写2个布尔值变量分别弄2个锁 get和set都有共用各自的锁 , 各自管理自己得布尔值变量 , 2个yolo分别管理各自的布尔值 , 1个用于你第1个yolo 当返回了结果这个布尔值变量就为true 返回结果的时候判断一下这个布尔值变量是否为true , true就不重新返回结果, 另外一个用于你第2个yolo返回同理 , 开第三个线程 , 实时轮训这两个布尔值变量 , 两个都是true 就用这两个结果开始做你要做的事情 , 做完了 用第三个线程将这2个布尔值变量set成false , 就可以2个yolo各自推理自己得互不干扰 , 第三个线程也做自己得事情不干扰。(第三个线程是否创建看自己项目的需求)以下以yolo模型和MediepipeHand模型为例.
我直接把官方yolov8-ncnn的demo例子拿来改了,所以直接在yolo原来的c++文件上面融合代码。下面是JNI目录里面的代码文件
首先直接在主逻辑类里面更改:yolov8ncnn.cpp
第一步,创建两个Net对象和两个锁。(g_yolo原来就有的,所以只需要再创建一个g_hand就行)
//创建2个对象
static Yolo* g_yolo = 0;
static ncnn::Mutex lock;
static Hand* g_hand = 0;
static ncnn::Mutex lock2;
第二步,在 MyNdkCamera::on_image_render(cv::Mat& rgb) 方法里面增加用g_hand对象调用draw函数和detect函数的方法,如下:
//hand对象
{
ncnn::MutexLockGuard g(lock2);
if (g_hand)
{
std::vector<PalmObject> objects;
g_hand->detect(rgb, objects);
g_hand->draw(rgb, objects);
}
else
{
draw_unsupported(rgb);
}
}
第三步,在 JNI_OnUnload 方法里面增加对g_hand对象释放资源的方法
{
ncnn::MutexLockGuard g(lock2);
delete g_hand;
g_hand = 0;
}
第四步,在JAVA层增加一个loadModel函数,yolo有自己的loadmodel函数,因此hand也需要
第五步,接下来就是在C++层去实现这个loadmodel2函数了,其实大部分代码都跟yolo一样,但是要注意JNI函数的命名,要改为当前项目名
JNIEXPORT jboolean JNICALL Java_com_tencent_yolov8ncnn_Yolov8Ncnn_loadModel2(JNIEnv* env, jobject thiz, jobject assetManager, jint modelid, jint cpugpu)
{
if (modelid < 0 || modelid > 6 || cpugpu < 0 || cpugpu > 1)
{
return JNI_FALSE;
}
AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
__android_log_print(ANDROID_LOG_DEBUG, "ncnn", "loadModel %p", mgr);
const char* modeltypes[] =
{
//选择模型
"palm-lite"
};
const int target_sizes[] =
{
192,
192
};
const float mean_vals[][3] =
{
{0.f,0.f,0.f},
{0.f,0.f,0.f},
};
const float norm_vals[][3] =
{
{1 / 255.f, 1 / 255.f, 1 / 255.f},
{1 / 255.f, 1 / 255.f, 1 / 255.f},
};
const char* modeltype = modeltypes[(int)modelid];
int target_size = target_sizes[(int)modelid];
bool use_gpu = (int)cpugpu == 1;
// reload
{
ncnn::MutexLockGuard g(lock2);
if (use_gpu && ncnn::get_gpu_count() == 0)
{
// no gpu
delete g_hand;
g_hand = 0;
}
else
{
//调用load函数
if (!g_hand)
g_hand = new Hand;
g_hand->load(mgr, modeltype, target_size, mean_vals[(int)modelid], norm_vals[(int)modelid], use_gpu);
}
}
return JNI_TRUE;
}
第六步,到了这里其实主类修改已经完成了,但是我们的g_hand对象还要用到别的类,因此要在yolov8ncnn类的头文件引入hand.cpp
并且除了hand.h,其余的cpp文件也要引入到本项目的JNI目录下,即hand.cpp、hand.h、landmark.cpp、landmark.h,而在原本hand项目里面的主类则不需要引入,因为你已经把主类的逻辑都写在yolov8ncnn.cpp里面了不是吗?
第七步,引入模型文件到assets文件夹里面
最后一步,就是在安卓里面调用loadmodel函数了
private void reload()
{
boolean ret_init = yolov8ncnn.loadModel(getAssets(), current_model, current_cpugpu);
boolean ret_init2 = yolov8ncnn.loadModel2(getAssets(), current_model, current_cpugpu);
if (!ret_init)
{
Log.e("MainActivity", "yolov8ncnn loadModel failed");
}
if (!ret_init2)
{
Log.e("MainActivity", "yolov8ncnn loadModel2 failed");
}
}
至此,就已经实现了两个ncnn模型的部署,其实从设计规范来说,将他们封装成一个类来进行操作是更加方便的,具体就看自己的需求啦。部署三个,四个模型也是一样的道理,但是前提得你手机cpu吃的消噢,下一篇我会讲讲如何优化多个ncnn模型部署时候的运行速度,让你的视频流不会太卡。