ncnn填坑记录八:将自己训练的模型打包为APK并部署到安卓端运行

上一篇:ncnn填坑记录七:examples/squeezenet.cpp代码阅读

做一个分类任务,模型选取的mobilenetv3,训练好模型,并按前文依次转换为onnx、ncnn后,参考官方https://github.com/nihui/ncnn-android-squeezenet进行修改。

一. 在PC端运行

ncnn/examples文件夹下:squeezenet.cpp -->mobilenetv3.cpp
修改如下:

#include "net.h"

#include <algorithm>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <stdio.h>
#include <vector>

static int detect_mobilenetv3(const cv::Mat& bgr, std::vector<float>& cls_scores)
{
	// 申明Net对象
    ncnn::Net mobilenetv3;  

	// 使用GPU
    mobilenetv3.opt.use_vulkan_compute = true;  

    // the ncnn model https://github.com/nihui/ncnn-assets/tree/master/models
    // 加载模型网络结构
    mobilenetv3.load_param("mobilenetv3.param");
    // 加载模型权重参数
    mobilenetv3.load_model("mobilenetv3.bin");

	// 将图片转换为ncnn的格式,并resize到227*227,后面4个参数为原图w,h,缩放后的w,h
	// 还可以实现BGR的转换,如PIXEL_RGB2GRAY、RGB2BGR,若模型输入为BGR,则用PIXEL_BGR;若模型输入为RGB,则用PIXEL_BGR2RGB
    ncnn::Mat in = ncnn::Mat::from_pixels_resize(bgr.data, ncnn::Mat::PIXEL_RGB, bgr.cols, bgr.rows, 224, 224);

    const float mean_vals[3] = {123.68f, 116.28f, 103.53f};
    const float std_vals[3] = {0.0171f, 0.0175f, 0.0174f};
    
    // 对图片进行归一化
    in.substract_mean_normalize(mean_vals, std_vals);
    
	// 实例化Extractor
    ncnn::Extractor ex = mobilenetv3.create_extractor();
    
    // 可以设置线程个数,加快计算
    // ex.set_num_threads(4);
    // 设置执行器是否使用轻量模式
    // ex.set_light_mode(true);    

	// 输入,将图片放入网络中,进行前向推理,"data"换为自己网络的输入名
    ex.input("input.1", in);

    ncnn::Mat out;
    
    // 提取输出,"prob"换为自己网络的输出名
    ex.extract("653", out);
	// printf(out);

	// 将out中的值转化为cls_scores,返回不同类别的得分
    cls_scores.resize(out.w);
    for (int j = 0; j < out.w; j++)
    {
        cls_scores[j] = out[j];
    }

    return 0;
}

static int print_topk(const std::vector<float>& cls_scores, int topk)
{
    // partial sort topk with index
    int size = cls_scores.size();
    
    // 声明一个pair容器
    std::vector<std::pair<float, int> > vec;
    vec.resize(size);
    for (int i = 0; i < size; i++)
    {
        vec[i] = std::make_pair(cls_scores[i], i);
    }

	// 部分排序算法,只要topk的,返回一个当前vector容器中起始元素的迭代器
	// greator,降序排列,应该是使用第一个float
    std::partial_sort(vec.begin(), vec.begin() + topk, vec.end(),
                      std::greater<std::pair<float, int> >());

    // print topk and score
    for (int i = 0; i < topk; i++)
    {
        float score = vec[i].first;
        int index = vec[i].second;
        fprintf(stderr, "%d = %f\n", index, score);
    }

    return 0;
}


// argv有2个参数,argv[0]指向程序中的可执行文件的文件名,arg[1]表示imagepath
int main(int argc, char** argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s [imagepath]\n", argv[0]);
        return -1;
    }

    const char* imagepath = argv[1];

    cv::Mat m = cv::imread(imagepath, 1);
    
    // cv读出的图片为空
    if (m.empty())
    {
        fprintf(stderr, "cv::imread %s failed\n", imagepath);
        return -1;
    }

	// 定义用来存储最终各类别的得分
    std::vector<float> cls_scores;
    
    // 调用推理函数
    detect_mobilenetv3(m, cls_scores);
    
	// 打印top3
    print_topk(cls_scores, 1);

    return 0;
}

需要注意的是,模型的输入输出用netron打开后查找

对图片进行归一化的mena_vals和std_vals按实际修改

由于我是用PIL读取读片进行训练的,故此处改为ncnn::Mat::PIXEL_RGB(可能有点问题?)

打印top1即可。

同文件夹下,CMakeLists.txt添加:ncnn_add_example(mobilenetv3)

编译后运行,分类的结果能对上

(base) lgy@lgy:~/tools/ncnn/build/examples$ ./mobilenetv3 ./2_95.jpg 
[0 GeForce GTX 1060 6GB]  queueC=2[8]  queueG=0[16]  queueT=1[2]
[0 GeForce GTX 1060 6GB]  bugsbn1=0  bugbilz=0  bugcopc=0  bugihfa=0
[0 GeForce GTX 1060 6GB]  fp16-p/s/a=1/1/0  int8-p/s/a=1/1/1
[0 GeForce GTX 1060 6GB]  subgroup=32  basic=1  vote=1  ballot=1  shuffle=1
1 = 2.361328
(base) lgy@lgy:~/tools/ncnn/build/examples$ ./mobilenetv3 ./1_114.jpg 
[0 GeForce GTX 1060 6GB]  queueC=2[8]  queueG=0[16]  queueT=1[2]
[0 GeForce GTX 1060 6GB]  bugsbn1=0  bugbilz=0  bugcopc=0  bugihfa=0
[0 GeForce GTX 1060 6GB]  fp16-p/s/a=1/1/0  int8-p/s/a=1/1/1
[0 GeForce GTX 1060 6GB]  subgroup=32  basic=1  vote=1  ballot=1  shuffle=1
0 = 3.609375

二. 在安卓端运行

app/src/main/jni文件夹内
在这里插入图片描述

1.解压 ncnn-20210525-android-vulkan 至该文件夹下

2.CMakeLists.txt

squeezencnn_jni.cpp 改为 mobilenetv3ncnn_jni.cpp

project(squeezencnn)

cmake_minimum_required(VERSION 3.10)

set(ncnn_DIR ${CMAKE_SOURCE_DIR}/ncnn-20210525-android-vulkan/${ANDROID_ABI}/lib/cmake/ncnn)
find_package(ncnn REQUIRED)

add_library(squeezencnn SHARED mobilenetv3ncnn_jni.cpp)

target_link_libraries(squeezencnn ncnn)

3.squeezenet_v1.1.id.h 是去除可见字符串得到的

(base) lgy@lgy:~/tools/ncnn/build/tools$ ./ncnn2mem mobilenetv3.param mobilenetv3.bin mobilenetv3.id.h mobilenetv3.mem.h

mobilenetv3.id.h 移至该文件夹下

mobilenetv3.binmobilenetv3.param.bin 移至 my_ncnn-android-mobilenetv3/app/src/main/assets 文件夹下
在这里插入图片描述

app/src/main/assets/synset_words.txt修改为自己的类别

A
B

4.mobilenetv3ncnn_jni.cpp修改:

①.头文件

#include "mobilenetv3.id.h"

②.权重导入

这是加密后的权重导入,明文的换另一种导入方式

int ret = squeezenet.load_param_bin(mgr, "mobilenetv3.param.bin");
int ret = squeezenet.load_model(mgr, "mobilenetv3.bin");

③.图片resize

    if (width != 227 || height != 227)
        return NULL;
    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
        return NULL;

    // ncnn from bitmap
    ncnn::Mat in = ncnn::Mat::from_android_bitmap(env, bitmap, ncnn::Mat::PIXEL_BGR);

可以看到,mobilenetv3ncnn_jni.cpp只有判断是否为227的代码,并无resize的。

src/main/java/com/tencent/squeezencnn/MainActivity.java

// resize to 227x227
yourSelectedImage = Bitmap.createScaledBitmap(rgba, 227, 227, false);

227都改为224

rgba为读入图片的格式,上面ncnn::Mat::PIXEL_BGR为格式转换,
由于我是用PIL读取读片进行训练的,故此处改为ncnn::Mat::PIXEL_RGB(可能有点问题?)

④.图片归一化

        const float mean_vals[3] = {123.68f, 116.28f, 103.53f};
        const float std_vals[3] = {0.0171f, 0.0175f, 0.0174f};
        in.substract_mean_normalize(mean_vals, std_vals);

⑤.输入输出

		ex.input(mobilenetv3_param_id::BLOB_input_1, in);

        ncnn::Mat out;
        ex.extract(mobilenetv3_param_id::BLOB_653, out);

⑥.输出标签

    std::string result_str = std::string(word.c_str()) + " = " + tmp;

原来为

    std::string result_str = std::string(word.c_str() + 10) + " = " + tmp;
    // +10 to skip leading n03179701

是因为squeezenet的synset_words.txt为:

n01440764 tench, Tinca tinca
n01443537 goldfish, Carassius auratus
......

前面刚好有9位数加1个空格。

5.app改名

/app/src/main/AndroidManifest.xml

 <application android:label="my_mobilenetv3ncnn" > // 这是安装app时屏幕上显示的名字
        <activity android:name="MainActivity"
                  android:label="****分类">   // 这是安装后app的名字和界面左上角显示的名字

/app/build.gradle

        applicationId "com.tencent.mobilenetv3ncnn"  //  这是生成的安装包的名字

6.同一个项目生成多个apk安装到同一手机上

由于我使用mobilenetv3训练了2个不同的分类数据集,项目是同一个,只改了相关数据集,在生成第二个项目的apk安装到手机上时,虽然已经改了名,但还是提示已经安装同版本的apk。
参考https://blog.csdn.net/xiaoluer/article/details/78430191,包名是应用程序的唯一标识
解决方法为:在build.gradle里, 把applicationId改成不同的名称。

三.补充

界面显示的是类别名称等于一个概率,而我使用时,显示的概率经常是大于了1,后来研究mobilenetv3ncnn_jni.cpp代码,发现原代码在进行后处理时,和我的模型有一点区别。

cls_scores.resize(out.w);
for (int j=0; j<out.w; j++)
{
    cls_scores[j] = out[j];
}

这一步是把模型的输出降维,shape由[1,n]变为[n],n为类别数

// return top class
    int top_class = 0;
    float max_score = 0.f;
    for (size_t i=0; i<cls_scores.size(); i++)
    {
        float s = cls_scores[i];
//         __android_log_print(ANDROID_LOG_DEBUG, "SqueezeNcnn", "%d %f", i, s);
        if (s > max_score)
        {
            top_class = i;
            max_score = s;
        }

这一步求的是cls_scores中最大值的索引,此处是直接用排序算法进行比较的,cls_scores中的值为每个类别的置信度,而我的模型输出后,是经过torch.argmax得到最大值索引的,故导致界面显示出来的值存在大于1的情况。

可以直接修改为只显示类别,不显示概率。

不过,类别一旦多了,总感觉不好,查询c++的argmax实现,修改代码
新增一个求argmax的函数

#include <algorithm>
#include <array>

template<class ForwardIterator>
inline size_t argmax(ForwardIterator first, ForwardIterator last)
{
    return std::distance(first, std::max_element(first, last));
}

//return top class及之后部分改为:

// return top class
    /*
    int top_class = 0;
    float max_score = 0.f;
    for (size_t i=0; i<cls_scores.size(); i++)
    {
        float s = cls_scores[i];
//         __android_log_print(ANDROID_LOG_DEBUG, "SqueezeNcnn", "%d %f", i, s);
        if (s > max_score)
        {
            top_class = i;
            max_score = s;
        }
    }
    */
    size_t top_class = argmax(cls_scores.begin(), cls_scores.end());

    const std::string& word = squeezenet_words[top_class];
    //char tmp[32];
    //sprintf(tmp, "%.3f", max_score);
    //std::string result_str = std::string(word.c_str()) + " = " + tmp;
    std::string result_str = std::string(word.c_str());

使用的时候,模型遇到不认识的物体,还是会识别为预先设定的某类,看实际项目是否每次检测的图,都在设定的类别范围之内,不然还是按每类打分,取大于阈值的最大值更好。

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值