之前在一个公司实习的时候有个需求,说要把Darknet的模型隐藏起来。就是说提供给用户的只有dll,而cfg和weights文件不能直接给客户,不然就暴露商业机密了嘛。所以就研究了一下如何隐藏模型,主要参考这篇文章。具体原理咱也不是很懂,反正只要能隐藏就行了。下面介绍一下Darknet隐藏模型的方法。
一、准备
首先准备好VS,然后配置opencv和cuda/cudnn,这两个好像不是必须的,但本文都是配好的。在Darknet官网下载最新版的源码。
二、封装模型
打开工程yolo_cpp_dll.sln(无显卡为yolo_cpp_dll_no_gpu.sln),准备好要封装的模型(cfg)和权重(weights)。右键工程->添加->资源->导入,选择模型文件(这里用yolov4-tiny -> 详情)并命名为cfg(随意),同理把权重也添加上去。
这时系统会自动生成resource.h(在build/darknet下)
说明资源添加成功。
三、调用资源
Darknet中供外部调用的接口是yolo_v2_class.hpp,先把名字改成xxx.h(随意,但总不能暴露你用的yolo吧,高大上一点),对应的.c也改了,然后重新添加到工程。在xxx.c中添加调用资源代码
//load cfg
char* LoadCfg(int &len)
{
HMODULE phexmodule = LoadLibrary("xxx.dll");
HRSRC hRsrc = FindResource(phexmodule, MAKEINTRESOURCE(101), TEXT("cfg"));
DWORD dwSize = SizeofResource(phexmodule, hRsrc);
len = dwSize;
HGLOBAL hGlobal = LoadResource(phexmodule, hRsrc);
LPVOID pBuffer = LockResource(hGlobal);
char *s = (char *)pBuffer;
return s;
}
//load weights
float* LoadWeights(int &len)
{
HMODULE phexmodule = LoadLibrary("xxx.dll");
HRSRC hRsrc = FindResource(phexmodule, MAKEINTRESOURCE(102), TEXT("weights"));
DWORD dwSize = SizeofResource(phexmodule, hRsrc);
len = dwSize;
HGLOBAL hGlobal = LoadResource(phexmodule, hRsrc);
LPVOID pBuffer = LockResource(hGlobal);
float *s = (float *)pBuffer;
return s;
}
注意LoadLibrary里的参数应与生成的dll文件名相同(默认则是工程名),FindResource中的资源编号与resource.h中生成的相同,名字与第二步中你给这个资源的命名相同。
把Detector类的构造函数前两个参数删掉,只保留gpuid这个参数。因为这两个参数要从内存中(也就是dll中)读,就不需要给客户留接口了。在Detector类的构造函数中将读模型和权重的代码替换为
int cfg_len;
char *cfg = LoadCfg(cfg_len);
int weights_len;
float *weights = LoadWeights(weights_len);
net = parse_network_cfg_custom_from_memory(cfg, cfg_len);
load_weights_from_memory(&net, weights, weights_len);
并删除其他依赖文件的相关代码。
四、改写代码
源代码中的cfg和weights文件分别是从函数parse_network_cfg_custom和load_weights_upto读进来的,下面来实现从内存中读取。
(1)模型
在parser.h中添加
network parse_network_cfg_custom_from_memory(char *cfg, int cfg_len);
复制一份parser.c中的parse_network_cfg_custom的实现并把函数改为上述形式,然后将第一行改为
list *sections = read_cfg_from_memory(cfg, cfg_len);
read_cfg_from_memory的实现如下
list *read_cfg_from_memory(char *cfg, int cfg_len)
{
int i, j, linenums = 0;
list *sections = make_list();
section *current = 0;
for (i = 0; i < cfg_len; i++)
{
char *line = (char *)malloc(256 * sizeof(char));
for (j = 0; cfg[i] != '\n'; i++, j++)
{
line[j] = cfg[i];
}
line[j] = '\0';
++linenums;//行数
strip(line);//去掉特殊字符
switch (line[0])
{
case '[':
current = (section*)xmalloc(sizeof(section));
list_insert(sections, current);
current->options = make_list();
current->type = line;
break;
case '\0':
case '#':
case ';':
case ' ':
case '\t':
free(line);
break;
default:
if (!read_option(line, current->options)) {
fprintf(stderr, "Config file error line %d, could parse: %s\n", linenums, line);
free(line);
}
break;
}
}
return sections;
}
其实现原理基本和函数read_cfg相同,区别是从以*cfg为头指针,长度为cfg_len的内存区域读取模型。
(2)权重
权重同理,在parser.h中添加
void load_weights_from_memory(network *net, float *weights, int weights_len);
其实现为
void load_weights_from_memory(network *net, float *weights, int weights_len)
{
#ifdef GPU
if(net->gpu_index >= 0){
cuda_set_device(net->gpu_index);
}
#endif
fprintf(stderr, "Loading weights from memory");
fflush(stdout);
int i, start = 5;
int *index = &start;
for(i = 0; i < net->n ; ++i)
{
layer l = net->layers[i];
if (l.dontload) continue;
if(l.type == CONVOLUTIONAL && l.share_layer == NULL){
load_convolutional_weights_from_memory(l, weights, index);
}
if (l.type == SHORTCUT && l.nweights > 0) {
load_shortcut_weights_from_memory(l, weights, index);
}
if(l.type == BATCHNORM){
load_batchnorm_weights_from_memory(l, weights, index);
}
……
这个函数很长,后面还有没放出来的。但因为我封装的是yolov4-tiny,用不到connect、rcnn那些层,所以后面全删了。这里要看你封装的是什么模型,需要什么层,再去写那些层的load_xxx_weights_from_memory。以卷积层为例,其load函数的实现为
void load_convolutional_weights_from_memory(layer l, float *weights, int *index)
{
int num = l.nweights;
int read_bytes;
myLoad(l.biases, weights, l.n, index);
if (l.batch_normalize && (!l.dontloadscales))
{
myLoad(l.scales, weights, l.n, index);
myLoad(l.rolling_mean, weights, l.n, index);
myLoad(l.rolling_variance, weights, l.n, index);
if (0)
{
int i;
for (i = 0; i < l.n; ++i) {
printf("%g, ", l.rolling_mean[i]);
}
printf("\n");
for (i = 0; i < l.n; ++i) {
printf("%g, ", l.rolling_variance[i]);
}
printf("\n");
}
if (0)
{
fill_cpu(l.n, 0, l.rolling_mean, 1);
fill_cpu(l.n, 0, l.rolling_variance, 1);
}
}
myLoad(l.weights, weights, num, index);
if (l.flipped) {
transpose_matrix(l.weights, (l.c / l.groups)*l.size*l.size, l.n);
}
#ifdef GPU
if (gpu_index >= 0) {
push_convolutional_layer(l);
}
#endif
}
*weights为权重文件指针,index为索引,从5开始是因为前5个int字符都有别的含义,不是权重本身(详见load_weights_upto函数)。而myLoad函数就是读内存了
void myLoad(float *destination, float *source, int num, int *index)
{
for (int i = 0; i < num; i++)
{
destination[i] = source[*index];
(*index)++;
}
}
其他load_xxx_weights_from_memory函数也同理,只要从load_xxx_weights拷贝一份代码,并把所有的
fread(xxx, sizeof(float), length, fp);
变成
myLoad(xxx, weights, length, index);
即可。至此核心代码完成,生成dll。如果有其他报错(我被window.h坑过==)百度搜索即可。
五、测试
新建工程,添加刚才的xxx.h、resource.h及lib、dll文件,创建main.cpp,添加测试代码
#include "xxx.hpp"
#include "resource.h"
#include<iostream>
#include<cstring>
#include<string.h>
#include<stdlib.h>
using namespace std;
int main()
{
string img = "test.jpg";
Detector detector;
vector<bbox_t> result_vec = detector.detect(img);
for (auto &i : result_vec)
{
cout << "obj_id = " << i.obj_id << ", x = " << i.x << ", y = " << i.y << ", w = " << i.w
<< ", h = " << i.h << ", prob = " << i.prob << endl;
}
return 0;
}
能成功运行并输出检测结果(类别id和坐标,具体id对应哪一类需要查看names文件)就表示封装成功了。如果没结果,也不代表封装失败,可能是没检测到东西,降低阈值再试试。输出结果中默认打印了很多网络信息,如果不想输出这些信息,可以在parse_convolutional和make_convolutional_layer(以卷积层打印的信息为例,其它层同理)中注释掉相关打印代码即可。当然,如果你想加入一些其他的输出信息也可自行添加。最终我的输出结果为
代码不传git了,有需要私聊。如果哪位大佬有更好的隐藏模型的方法也欢迎赐教。