Bootstrap

ncnn模型部署——onnx模型转ncnn模型并推理可执行程序(resnet18例子)

欢迎学习ncnn系列相关文章,从训练、模型转换、精度分析,评估到部署Android端,推荐好资源:

一、YoloV5训练自己数据集并测试
二、ncnn编译和安装
三、onnx模型转ncnn模型并推理可执行程序(resnet18例子)
四、yolov5-6.0Pyotorch模型转onxx模型再转ncnn模型部署
五、训练自己YOLOv5模型转ncnn模型并部署到Android手机端


在这里插入图片描述

ncnn为用户提供了一系列的模型转换工具,能够轻松地将caffe、onnx等格式的模型转换为ncnn可以识别的格式。在源代码编译完成后,这些工具会被存放在ncnn/build/tools目录下。在本次的示例中,使用了onnx2ncnn工具,将在pytorch中导出的resnet18的onnx模型转换为ncnn模型,并进行了推理。

一、源码包准备

本教程配套源码包获取方法为文章末扫码到公众号「视觉研坊」中回复关键字:模型部署onnx转ncnn。获取下载链接。

源码包下载解压后的样子如下:

在这里插入图片描述

二、环境准备

resnet18使用的是Pytorch模型,所以需要先搭好torch环境,下面是我自己的环境版本号,仅供参考,其它版本也行。

在这里插入图片描述

三、Pytorch模型转ONNX模型

3.1 注意事项

在onnx模型转ncnn模型前,需要先将Pytorch模型转为onnx模型,不可以Pytorch模型直接转onnx模型,因为这两种模型的格式和结构差异很大。

3.2 代码

具体转换脚本在源码在目录onnx_to_ncnn/python/export_res18.py

import torch
import torchvision.models as models
import torch.onnx as onnx

# 加载预训练的ResNet-18模型
resnet = models.resnet18(pretrained=True)  # 使用Pytorch官方的模型加载预训练权重

# 将模型设置为评估模式
resnet.eval()

# 创建一个示例输入张量
dummy_input = torch.randn(1, 3, 224, 224)

# 使用torch.onnx.export函数导出模型为ONNX格式
onnx_file_path = "./model_param/resnet18_test.onnx"        # 导出路径         model_param
onnx.export(resnet, dummy_input, onnx_file_path)

print("ResNet-18模型已成功导出为ONNX格式:", onnx_file_path)

3.3 参数修改

在这里插入图片描述

3.4 代码运行结果

在这里插入图片描述

在这里插入图片描述

四、onnx转ncnn

4.1 添加可执行权限

如果电脑上第一次转换,先确保 onnx2ncnn 这个文件有可执行权限,使用 chmod 命令来添加可执行权限:

sudo chmod +x bin/onnx2ncnn

4.2 转换过程

在终端使用下面命令转换:

bin/onnx2ncnn model_param/resnet18_test.onnx model_param/resnet18_test.param model_param/resnet18_test.bin

上面命令中,使用 onnx2ncnn 工具,将 resnet18_test.onnx 转换为 resnet18_test.param 和 resnet18_test.bin。在这里resnet18_test.param 存储了模型的参数信息,它记录了计算图的结构。而 resnet18_test.bin 则存放了模型的所有具体参数。就可以使用 ncnn 框架来加载和运行这个模型了。

4.3 转换结果

下面是具体的转换过程及结果:

在这里插入图片描述

4.3.1 .bin文件

bin文件中存放数据,如卷积数据,算子加载的权重等都放在二进制bin文件中。

4.3.2 .param文件

在这里插入图片描述

下面是.param文件中具体参数解析,7767517 是一个magic数,表示ncnn格式的;58是layer数,及算子的数量;66是blob数,blob中间数据存储的数量,中间数和计算结果都是放在blob里的。

在这里插入图片描述

在这里插入图片描述

4.3.3 可视化网络结构

netron官网:entron

在netron中打开.param文件查看计算图结构,方形框中的内容就是layer(算子),各个方形框之间的连接线就是blob。如下:

在这里插入图片描述

通过上面的一系列操作就得到了ncnn可以识别的两个文件:.param和.bin。

五、构建和编译

5.1 推理脚本

5.1.2 参数修改

推理脚本为resnet18.cpp,使用是需要修改的地方见下:

在这里插入图片描述
在这里插入图片描述

5.1.3 代码

#include "net.h"
#include <algorithm>
#if defined(USE_NCNN_SIMPLEOCV)
#include "simpleocv.h"
#else
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#endif
#include <stdio.h>
#include <vector>


static int detect_resnet18(const cv::Mat& bgr, std::vector<float>& cls_scores)
{
    ncnn::Net resnet18;      // 定义了网络结构

    resnet18.opt.use_vulkan_compute = true;  // 使用vulkan加速

    //分别加载模型的参数和数据
    if (resnet18.load_param("model_param/resnet18_test.param"))
        exit(-1);
    if (resnet18.load_model("model_param/resnet18_test.bin"))
        exit(-1);
	//opencv读取图片是BGR格式,我们需要转换为RGB格式
    ncnn::Mat in = ncnn::Mat::from_pixels_resize(bgr.data, ncnn::Mat::PIXEL_BGR2RGB, bgr.cols, bgr.rows, 224, 224);
    
    //图像归一标准化,以R通道为例(x/225-0.485)/0.229,化简后可以得到下面的式子
    //需要注意的式substract_mean_normalize里的方差其实是方差的倒数,这样在算的时候就可以将除法转换为乘法计算
    //所以norm_vals里用的是1除
    const float mean_vals[3] = {0.485f*255.f, 0.456f*255.f, 0.406f*255.f};
    const float norm_vals[3] = {1/0.229f/255.f, 1/0.224f/255.f, 1/0.225f/255.f};
    in.substract_mean_normalize(mean_vals, norm_vals);

    /*imagenet图片三通道的均值和标准差分别是mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]。以R通道为例,原始图片的像素值是从0到255,
    所以像素值归一化即像x/255,减去均值再除以标准差就是(x/255-0.485)/0.229,把255乘下去也就是(x-0.485×255)/255×0.229。如果把归一化和标准
    化一起处理的话,等价均值就是0.485×255,等价标准差就是255×0.299。但由于substract_mean_normalize里的标准差实际是标准差的倒数,这样可以把除法
    转换为乘法来计算加快效率,所以这里norm_vals用的是标准差的倒数。*/

    ncnn::Extractor ex = resnet18.create_extractor();
	
    //把图像数据放入input.1这个blob里
    ex.input("input.1", in);  // 输入放入input.1

    ncnn::Mat out;
    //提取出推理结果,推理结果存放在191这个blob里
    ex.extract("191", out);   // 输出放入191

    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();
    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);
    }

    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;
}

int main(int argc, char** argv)
{
    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s [imagepath]\n", argv[0]);
        return -1;
    }

    const char* imagepath = argv[1];
	
    //使用opencv读取图片
    cv::Mat m = cv::imread(imagepath, 1);
    if (m.empty())
    {
        fprintf(stderr, "cv::imread %s failed\n", imagepath);
        return -1;
    }

    std::vector<float> cls_scores;
    detect_resnet18(m, cls_scores);         // 调用模型
	
    //打印得分前三的类别
    print_topk(cls_scores, 3);

    return 0;
}

5.2 CMakeLists.txt修改

下面说到的源码路径,指的是安装源码ncnn时的路径,关于安装源码ncnn见我上一篇博文:ncnn编译和安装

在这里插入图片描述
在这里插入图片描述

5.3 生成构建文件

先在根目录中生成一个build文件夹,然后执行命令构建如下:

cmake ..

在这里插入图片描述

5.3.1 cmake …解析

cmake … 是一个在当前目录下执行 CMake 的命令,它会查找上一级目录(由 … 表示)中的 CMakeLists.txt 文件,并生成构建文件。这些构建文件可以被 make、ninja 或其他构建工具使用,来编译和链接你的项目。

5.3.2 构建结果

通过上面命令执行后会在build文件夹中生成相关文件,如下:

在这里插入图片描述

5.4 编译

在上面构建文件基础上执行编译命令:

make

bin目录下的resnet18就是编译生成的可执行程序。

在这里插入图片描述

六、推理可执行程序

通过cd命令到根目录下,执行推理命令:

bin/resnet18 image/dog.jpg

下面是可执行程序运行后的输出结果,包含了设备信息,每层的推理时间,以及最终的推理结果。

在这里插入图片描述

七、总结

以上就是onnx模型转ncnn模型并推理可执行程序的详细过程,其它模型类型转换并推理。

总结不易,多多支持,谢谢!

感谢您阅读到最后!关注公众号「视觉研坊」,获取干货教程、实战案例、技术解答、行业资讯!

;