0 introduction
NCNN是腾讯优图实验室开源的一个为手机端极致优化的高性能神经网络前向计算框架。ncnn从设计之初深刻考虑手机端的部属和使用。无第三方依赖,跨平台,手机端cpu的速度快于目前所有已知的开源框架。基于ncnn,开发者能够将深度学习算法轻松移植到手机端高效执行,开发人工智能APP。
我们可以将很多深度学习训练的框架转成.onnx格式,用来实现c++落地。
FAQ
Q ncnn的起源
A 深度学习算法要在手机上落地,caffe依赖太多,手机上也没有cuda,需要个又快又小的前向网络实现
Q ncnn名字的来历
A cnn就是卷积神经网络的缩写,开头的n算是一语n关。比如new/next(全新的实现),naive(ncnn是naive实现),neon(ncnn最初为手机优化),up主名字(←_←)
Q 支持哪些平台
A 跨平台,主要支持 android,次要支持 ios / linux / windows
Q 计算精度如何
A armv7 neon float 不遵照 ieee754 标准,有些采用快速实现(如exp sin等),速度快但确保精度足够高
Q pc 上的速度很慢
A pc都是x86架构的,基本没做什么优化,主要用来核对结果,毕竟up主精力是有限的
1 install NCNN
参见:Github
Start → Programs → Visual Studio 2017 → Visual Studio Tools → x64 Native Tools Command Prompt for VS 2017
使用vs2017的 Native Tools Command Prompt进行编译,而不是cmake
2 pt2onnx
def pt2onnx(net, size): #size[0], size[1]为输入图像的宽和高
net.load_state_dict(torch.load('savedModel/yolov3.pth'))
dummy_input = torch.randn(1, 1, size[0], size[1], device='cuda') #定义输入的数据类型(B,C,H,W)
torch.onnx.export(net, dummy_input, 'yolov3.onnx', verbose=True)
#调用方法:
pt2onnx(eye_model,(192,384))
3 simplify onnx
主要是为了简化模型,也就是量化,模型复杂度虽然降低了,但是貌似精度会下降参考这里
pip3 install onnx-simplifier ,then
python3 -m onnxsim input_onnx_model output_onnx_model
4 onnx2parm_bin
- onnx转换ncnn模型文件
onnx2ncnn.exe output_model.onnx output_model.param output_model.bin
param文件可以看到网络的结构,为了安全,可以使用 ncnn2mem 工具将onnx转换为二进制描述文件和内存模型,生成output_model.bin一个二进制文件和两个静态数组的代码文件。具体使用方法详见examples
- ncnn模型文件转换为cpp头文件
ncnn2mem.exe output_model.param output_model.bin id_model_param.h mem_model_bin.h
5 NCNN源码详解
参考:https://www.cnblogs.com/buddho/p/9718626.html
ncnn 源码学习-Mat.h Mat.c
纯小白记录下腾讯的ncnn框架源码的学习。纯粹写给自己看的,不保证正确性。
Mat 类似于 caffe中的blob,是一个张量的存储结构体。
一、数据成员:
1、void * data
多维数据按一位数组来存储。并且需要是16字节对齐的。
2、int * refcount *refcount
表示这个Mat被引用的个数。类似于智能指针?×refcount == 0 自动释放。
3、size_t elemsize
每个元素的大小。sizeof(type)
4、dims
维度。如果是1维就是向量只有w,2维就是平面w * h, 3维就是立方体 w * h * c。
5、w h c
三个维度
6、cstep : channel step
每个channel 有多少个元素。注意是元素个数,而不是字节数。
total() = cstep * c
整个Mat有多少个元素。
二、方法成员:
void create(int _w, size_t _elemsize)
;
一维数组初始化。只有一个w。二维平面就是w h。
三维有些特殊:
cstep
不是单纯的 w * h, 而是要求每个channel的起始地址都是16字节对齐的。
cstep
是 whelemsize在16的基础上,对齐得到的。
也就是说,cstep * elemsize
必须是16的倍数。
所以,三维mat的大小不是简单的elemsize* align(w * h * c,16)
。而是elemsize* c* align(w*h, 16)
.
Mat Mat::channel(int c)
返回某一个channel的一维新Mat。
在channel内部,数据是连续的。channel之间可能有空隙。为了保证每个channel的起始地址是16对齐。
float * Mat::row(int y)
返回某一个Mat的第一个channel的第y个行的首地址。
template <typename T>
inline Mat::operator T*()
{
return (T*)data;
}
这个符号重载,神奇。把Mat转换成任意T *类型的指针。
返回的其内存地址。
inline float& Mat::operator[](int i)
{
return ((float*)data)[i];
}
重载了取下标操作,直接取其地址的第i个。
因为channel之间不是连续的,所以这么取对第1个以后的channel来说,可能会出问题。应该是对dim < 3的方法。
6 topK
- 计划任务TODO
7 cal_iou & nms
typedef struct Object
{
cv::Rect_<float> rect;
float confidence;
int index;
};
static float get_iou_value(cv::Rect rect1, cv::Rect rect2)
{
int xx1, yy1, xx2, yy2;
xx1 = std::max(rect1.x, rect2.x);
yy1 = std::max(rect1.y, rect2.y);
xx2 = std::min(rect1.x + rect1.width, rect2.x + rect2.width - 1);
yy2 = std::min(rect1.y + rect1.height, rect2.y + rect2.height - 1);
int insection_width, insection_height;
insection_width = std::max(0, xx2 - xx1 + 1);
insection_height = std::max(0, yy2 - yy1 + 1);
float insection_area, union_area, iou;
insection_area = float(insection_width) * insection_height;
if (insection_area <= 0)
{
return 0;
}
union_area = float(rect1.width*rect1.height + rect2.width*rect2.height - insection_area);
iou = insection_area / union_area;
return iou;
}
bool comp(const Object &lsh, const Object &rsh) {
if (lsh.confidence > rsh.confidence)
return true;
else
return false;
}
void nms_boxes(const std::vector<Object>& objects, const float confThreshold, const float nmsThreshold, std::vector<Object> &results)
{
std::sort(objects.begin(), objects.end(), comp);
std::vector<bool> del(objects.size(), false);
for (size_t i = 0; i < objects.size(); i++)
{
if (!del[i])
{
for (size_t j = i + 1; j < objects.size(); j++)
{
if (!del[j] && get_iou_value(objects[i].rect, objects[j].rect) > nmsThreshold)
{
del[j] = true;//IOU大于阈值,扔掉
}
}
}
}
for (int i = 0; i < objects.size(); i++)
{
if (!del[i] && objects[i].confidence > confThreshold) results.push_back(objects[i]);
}
return;
}
8 inference
推理的流程:读取模型文件→对输入的图像预处理(缩放、填充等到指定尺寸)→ Extract提取模型,得到张量Ma→看Mat是否是chw排列的三维张量,否则reshape到chw顺序→对该张量提取前十置信度的一组数据,nms极大值抑制→画出最终的lable、confidence、rectangle等数据→over!
以下为推理的核心代码
static int detect_yolov3(const cv::Mat& bgr, std::vector<Object>& objects)
{
ncnn::Net yolov3;
#if NCNN_VULKAN
yolov3.opt.use_vulkan_compute = true;
#endif // NCNN_VULKAN
yolov3.load_param("face_eye-v1.0_2.param");
yolov3.load_model("face_eye-v1.0_2.bin");
//图像数据排列顺序重排
cv::Mat image;
cv::Mat resizeImage;
cvtColor(bgr, image, CV_BGR2GRAY);
cv::resize(image, resizeImage, cv::Size(image.size().width / 8, image.size().height / 8));
cv::copyMakeBorder(resizeImage, image, (192 - resizeImage.rows) / 2, (192 - resizeImage.rows) / 2,
(384 - resizeImage.cols) / 2, (384 - resizeImage.cols) / 2, cv::BORDER_CONSTANT, cv::Scalar(0));
ncnn::Mat in = ncnn::Mat::from_pixels(image.data, ncnn::Mat::PIXEL_GRAY, image.cols, image.rows);
const float mean_vals[3] = {
0.0f, 0.0f, 0.0f };
const float norm_vals[3] = {
1.0 / 255.f, 1.0 / 255.f, 1.0 / 255.f };
in.substract_mean_normalize(mean_vals, norm_vals);
ncnn::Extractor ex = yolov3.create_extractor();
ex.input("0", in);
ncnn::Mat outL, outM, outH;
ncnn::Mat finL, finM, finH;
ncnn::Mat tmp, tmp1;
ex.extract("302", outL);
std::cout << "叁零贰:" << outL.w << ":" << outL.h << ":" << outL.c << std::endl;
ex.extract("303", outM);
std::cout << "叁零叁:" << outM.w << ":" << outM.h << ":" << outM.c << std::endl;
ex.extract("304", outH);
std::cout << "叁零四:" << outH.w << ":" << outH.h << ":" << outH.c << std::endl;
//chw转NCNN需要的whc格式
finL = outL;// .reshape(outL.c, outL.h, outL.w);
finM = outM;//.reshape(outM.c, outM.h, outM.w);
finH = outH;// .reshape(outH.c, outH.h, outH.w);
int img_w = image.cols;
int img_h = image.rows;
objects.clear();
std::vector<float> confidence;
//L层
for (size_t k = 0; k < 10; k++) //置信度前10
{
float maxconfidence = finL.channel(0).row(0