注:文末附最小可运行的代码,不需要积分即可下载
ncnn数据加载,主要有两个函数,分别为Net::load_model(),Net::load_param(),这两个函数在net.cpp里面定义了,对于数据读取,最终是调用了datareader.cpp
中实现的方法,因为在加载模型数据时,是先通过.param文件来获取layer,再通过.bin来获取layer的参数,所以下面我们先来解开Net::load_param(),看看里面到底有些啥。
关于Net::load_model()的解析,在另外一篇文章ncnn模型数据的读取load_model 进行细述
首先看param的加载,主要涉及以下2个cpp文件:
datareader.cpp
datareader.h
net.cpp
net.h
这里涉及几个类DataReader,DataReaderFromStdio,DataReaderFromStdioPrivate
1、定义DataReader,只定义虚函数,不实现
2、DataReaderFromStdio继承DataReader,并实现相应方法,定义了删除,读取,扫描,等操作
3、DataReaderFromStdio构造函数传入了DataReaderFromStdioPrivate,获取了数据流
4、DataReaderFromStdioPrivate对FILE* _fp进行封装,最终的数据来源于FILE* 的读取
1、首先定义了一个父类,只有虚函数,没做任何实现
class NCNN_EXPORT DataReader{
// parse plain param text
// return 1 if scan success
virtual int scan(const char* format, void* p) const; // 用来读取parma
// read binary param and model data
// return bytes read
virtual size_t read(void* buf, size_t size) const; // 用来读取bin
// get model data reference
// return bytes referenced
virtual size_t reference(size_t size, const void** buf) const;
}
DataReaderFromMemory
与DataReaderFromStdio
均继承自DataReader
,这里提供了两种读取方式,我们这里只对DataReaderFromStdio
进行说明。
定义DataReader
的好处就是在后面定义了int Net::load_param(const DataReader& dr)
,传入参数为父类DataReader
,那么DataReaderFromMemory
与DataReaderFromStdio
均可直接传入,方便统一处理,只不过是数据来源不一样而已。
DataReaderFromStdio
定义了对数据的读取,读取方式有两种,即scan(const char* format, void* p)
与read(void* buf, size_t size)
,scan是用来读取parma的,而read是用来读取bin的。
class NCNN_EXPORT DataReaderFromStdio : public DataReader // 继承了DataReader,并做相应实现
{
public:
explicit DataReaderFromStdio(FILE* fp); // 定义构造函数,并禁止隐式类型转换
virtual ~DataReaderFromStdio(); // 里面定义了对d的删除
virtual int scan(const char* format, void* p) const;// 用来读取parma,通过私有类d来操作
virtual size_t read(void* buf, size_t size) const;// 用来读取bin,通过私有类d来操作
private:
DataReaderFromStdio(const DataReaderFromStdio&);
DataReaderFromStdio& operator=(const DataReaderFromStdio&);
private:
DataReaderFromStdioPrivate* const d; //对FILE* fp进行无操作封装
};
上面还定义了DataReaderFromStdioPrivate,来看看下这个是做了什么
可以看到,什么也没做,就是把FILE* fp封装了一下而已
class DataReaderFromStdioPrivate
{
public:
DataReaderFromStdioPrivate(FILE* _fp) //构造函数,接受一个 FILE 指针 _fp,并将其赋值给 fp 成员变量。
: fp(_fp) // fp(_fp) 的作用是将传入的 _fp 值赋给 fp 成员变量。这种方式是在对象构造时直接初始化成员变量,而不是在构造函数体中使用赋值语句
{
}
FILE* fp; //公有成员变量 fp,它是一个指向 FILE 结构的指针,用于表示输入流。
};
至此可以看出来DataReaderFromStdioPrivate
只是对FILE* fp
进行无操作封装后传递给DataReaderFromStdio
的私有变量DataReaderFromStdioPrivate* const d;
然后DataReaderFromStdio
里的scan
,read
,就对d->fp
,即FILE* fp
进行操作
这里再啰嗦几句:
以yolov7为例
1、上面实际上就是通过FILE* 获取到yolov7.parma文件的句柄,然后自定义函数来实现一些方法,最终实现对数据流的操作,比如删除,读取,扫描,等操作。
2、主要流程为
1)将文件句柄封装到DataReaderFromStdioPrivate
2)把DataReaderFromStdioPrivate传递给DataReaderFromStdio
3)DataReaderFromStdio继承自DataReader,而DataReader只定义了虚函数,无方法实现
4)最终,DataReaderFromStdio里面定义了对文件流操作的方法
总之,就是将文件流封装为一个方便逐一读取数据的类,以供下面的load_param进行读取
net
就是通过读取上面封装好的DataReaderFromStdio
类里面的数据流,提取出网络层的超参,创建layer对象,并将超参赋值给对应的layer对象里面,最终layer会保存在NetPrivate* const d;
的const std::vector<Layer*>& layers() const;
里
这里封了多层,传入字符串,接着使用fopen打开,然后传入DataReaderFromStdio
,
最终调用了Net::load_param(const DataReader& dr)
,也是在这里将参数通过
SCAN_VALUE
解开的。
假如load_param
的传入参数为"yolov7.param"
,即net.load_param("yolov7.param");
那么调用下面的函数
int Net::load_param(const char* protopath)
{
FILE* fp = fopen(protopath, "rb");
if (!fp)
{
NCNN_LOGE("fopen %s failed", protopath);
return -1;
}
int ret = load_param(fp);
fclose(fp);
return ret;
}
获取到FILE* fp
后,调用下面的函数,将FILE* fp
封装到DataReaderFromStdio
int Net::load_param(FILE* fp)
{
DataReaderFromStdio dr(fp);
return load_param(dr);
}
获取到DataReaderFromStdio
后,最终调用下面的int Net::load_param(const DataReader& dr)
函数,这个函数就是将yolov7.param
里面的内容一一解出来。
这里给出最简单的版本,已经去掉layer,本来解出来的参数是要赋值给对应layer的
因为这里读取的其实是layer的超参,为了能够更加清楚网络是怎么读取的,这里就简化写
关键点就是通过SCAN_VALUE
这个函数来逐一读取参数
int Net::load_param(const DataReader& dr)
{
// 定义SCAN_VALUE的宏定义函数
#define SCAN_VALUE(fmt, v) \
if (dr.scan(fmt, &v) != 1) \
{ \
printf("parse " #v " failed"); \
return -1; \
}
int magic = 0;
SCAN_VALUE("%d", magic) // 读取参数文件第一行
if (magic != 7767517)
{
printf("param is too old, please regenerate");
return -1;
}
// parse
int layer_count = 0;
int blob_count = 0;
SCAN_VALUE("%d", layer_count) // 读取参数文件第二行,第一列
SCAN_VALUE("%d", blob_count) // 读取参数文件第二行,第二列
printf("layer_count: %d\n",layer_count);
printf("blob_count: %d\n",blob_count);
if (layer_count <= 0 || blob_count <= 0)
{
printf("invalid layer_count or blob_count");
return -1;
}
int blob_index = 0;
for (int i = 0; i < layer_count; i++)// 逐行读取参数文件
{
char layer_type[256];
char layer_name[256];
int bottom_count = 0;
int top_count = 0;
SCAN_VALUE("%255s", layer_type) // 读取网络类型,比如输入层为Input
SCAN_VALUE("%255s", layer_name)// 读取网络名字,比如输入层的名字为images
SCAN_VALUE("%d", bottom_count) // 读取网络的输入blob 比如输入层的输入为0层
SCAN_VALUE("%d", top_count) // 读取网络的输出blob 比如输入层的输出为1层
printf("%s\n",layer_type);
printf("%s\n",layer_name);
printf("%d\n",bottom_count);
printf("%d\n",top_count);
// Input images 0 1 images
for (int j = 0; j < bottom_count; j++) // 遍历输入blob的名称,对于Input层为空
{
char bottom_name[256];
SCAN_VALUE("%255s", bottom_name)
// printf("%s ",bottom_name);
}
for (int j = 0; j < top_count; j++) // 遍输出blob的名称,对于Input层为images
{
char blob_name[256];
SCAN_VALUE("%255s", blob_name)
// printf("%s ",blob_name);
}
int pdlr = pd.load_param(dr); // 读取网络的超参,比如0=32 1=3 11=3 2=1后面的32 3 3 1
d->layers[i] = layer; //将创建的网络层保存到NetPrivate* const d;
}
#undef SCAN_VALUE
return 0;
}
上面的代码最后面通过int pdlr = pd.load_param(dr);
来获取网络的超参,简写版如下:
int ParamDic_load_param(const DataReader& dr)
{
int id = 0;
printf("vstr: ");
while (dr.scan("%d=", &id) == 1) // 逐一读取,例如对0=32 1=3 11=3 2=1进行遍历,遍历四次
{
bool is_array = id <= -23300;
if (is_array)
{
id = -id - 23300;
}
if (id >= NCNN_MAX_PARAM_COUNT)
{
printf("id < NCNN_MAX_PARAM_COUNT failed (id=%d, NCNN_MAX_PARAM_COUNT=%d)", id, NCNN_MAX_PARAM_COUNT);
return -1;
}
char vstr[16];
int nscan = dr.scan("%15s", vstr); // 进行读取,就是将0=32的32取出来
printf("%s ",vstr);
if (nscan != 1)
{
printf("ParamDict read value failed");
return -1;
}
}
printf("\n");
return 0;
}
注:简化版均去掉了layer的参数填入部分,这部分会关系到ncnn::Mat
以及layer目录文件下的所有.cpp文件等,为了代码简单所以都去掉
以下是可直接运行的版本:
运行方法
cd build
cmake ..
make
./demo1
可以看到输出结果