上一篇: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.bin
与 mobilenetv3.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());
使用的时候,模型遇到不认识的物体,还是会识别为预先设定的某类,看实际项目是否每次检测的图,都在设定的类别范围之内,不然还是按每类打分,取大于阈值的最大值更好。