"不积跬步,无以至千里; 不积小流, 无以成江海",darknet的高楼大厦(network)是根据我们的图纸(.cfg配置文件),用砖头(data)堆砌成一面面墙与柱(layer),之后才搭建起来的,因此想弄懂一个框架,了解其数据结构是第一步。我们要先从最基本的开始。
data
data在darknet框架里用于存储和交换数据,是darknet的最基本的存储单元,可以持有矩阵(matrix)、图片(image)、列表(list)、树(tree)。data就相当于其他的深度学习框架如caffe中的Blob、Tensorflow中的Tensor等。要想要理解data的源码,首先我们可以从data的使用开始感性的了解,当我们使用darknet加载数据时,我们的调用过程是这样的:
detector.c–>data.c/load_data()–>data.c/load_threads()–>data.c/load_data_in_thread()–>data.c/load_thread()
其中load_thread()根据数据参数中的type选择读什么类型的数据,读各种数据的函数也在data.c中。
由此可见data应该可以用来加载数据、保存数据、保存模型权值等等。
-
结构体声明
来到src文件夹下,找到data.h,我们可以看到data中将darknet.h include了进来,darknet.h中有对data的结构描述和一些与data有关的函数原型。
/*
** 图像的结构声明
** w: 图像的宽
** h: 图像的高
** c: 图像的通道数
** data指针:指向图像第一个像素位置的指针,可查看image.c中的get_pixel(image m, int x, int y, int c)函数,
** 这个函数的目的是找到第c个通道(x,y)位置的像素大小
*/
typedef struct {
int w;
int h;
int c;
float *data;
} image;
/*
** 框,包括坐标(x,y)和框的宽与高,这样就能推出整个框所有四个点的坐标
*/
typedef struct{
float x, y, w, h;
} box;
/*
** 检测,有框、分类、概率、掩模(这个应该属于图像处理的范畴)、后两个应该是用于region的
*/
typedef struct detection{
box bbox;
int classes;
float *prob;
float *mask;
float objectness;
int sort_class;
} detection;
/*
**矩阵数据
*/
typedef struct matrix{
int rows, cols;
float **vals;
} matrix;
/*
** 从data.c中的concat_data()函数中可以看出shallow设为1时只能进行数据的浅拷贝和浅释放
*/
typedef struct{
int w, h;
matrix X;
matrix y;
int shallow;
int *num_boxes;
box **boxes;
} data;
/*
** 设定数据的类型
*/
typedef enum {
CLASSIFICATION_DATA, DETECTION_DATA, CAPTCHA_DATA, REGION_DATA, IMAGE_DATA, COMPARE_DATA, WRITING_DATA, SWAG_DATA, TAG_DATA, OLD_CLASSIFICATION_DATA, STUDY_DATA, DET_DATA, SUPER_DATA, LETTERBOX_DATA, REGRESSION_DATA, SEGMENTATION_DATA, INSTANCE_DATA, ISEG_DATA
} data_type;
typedef struct load_args{
int threads;
char **paths;
char *path;
int n;
int m;
char **labels;
int h;
int w;
int out_w;
int out_h;
int nh;
int nw;
int num_boxes;
int min, max, size;
int classes;
int background;
int scale;
int center;
int coords;
float jitter;
float angle;
float aspect;
float saturation;
float exposure;
float hue;
data *d;
image *im;
image *resized;
data_type type;
tree *hierarchy;
} load_args;
/*
方框的标签,包括id和四个坐标位置
*/
typedef struct{
int id;
float x,y,w,h;
float left, right, top, bottom;
} box_label;
network *load_network(char *cfg, char *weights, int clear);
load_args get_base_args(network *net);
/*
**释放数据内存
*/
void free_data(data d);
typedef struct node{
void *val;
struct node *next;
struct node *prev;
} node;
typedef struct list{
int size;
node *front;
node *back;
} list;
/*开辟线程加载数据*/
pthread_t load_data(load_args args);
/*加载数据的配置,其实现在option_list.c中可得到数据的options的链表,包括类别个数,训练数据文件位置等*/
list *read_data_cfg(char *filename);
list *read_cfg(char *filename);
unsigned char *read_file(char *filename);
data resize_data(data orig, int w, int h);
data *tile_data(data orig, int divs, int size);
data select_data(data *orig, int *inds);
-
data是怎么炼成的
打开data.h发现其包含了了matrix、image、tree、list四种数据的头文件
直接来看matrix的实现matrix.c
#include "matrix.h"
#include "utils.h"
#include "blas.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <math.h>
/*
** 释放矩阵的内存
** free()函数是stdlib.h标准库中的,用来释放calloc为指针变量分配的内存空间
*/
void free_matrix(matrix m)
{
int i;
for(i = 0; i < m.rows; ++i) free(m.vals[i]);/*将每一行矩阵数据的指针内存都释放*/
free(m.vals);/*最后释放整个矩阵的指针,vals是指向指针的指针*/
}
/*
** 求topK准确率,就是你的道德预测结果中前K个结果有正确的分类即判断为正确
*/
float matrix_topk_accuracy(matrix truth, matrix guess, int k)
{
int *indexes = calloc(k, sizeof(int));
int n = truth.cols;
int i,j;
int correct = 0;
for(i = 0; i < truth.rows; ++i){
top_k(guess.vals[i], n, k, indexes);/*将预测概率矩阵中前k大的类别放到indexes指针所指的内存空间中,此函数在utils.c中*/
for(j = 0; j < k; ++j){
int class = indexes[j];
if(truth.vals[i][class]){
++correct;
break;
}
}
}
free(indexes);
return (float)correct/truth.rows;
}
/*
**将矩阵按比例缩放,不知道这个在哪里能用上
*/
void scale_matrix(matrix m, float scale)
{
int i,j;
for(i = 0; i < m.rows; ++i){
for(j = 0; j < m.cols; ++j){
m.vals[i][j] *= scale;
}
}
}
/*将给定的matrix的行数规整到size大小*/
matrix resize_matrix(matrix m, int size)
{
int i;
if (m.rows == size) return m;
/*
** 给定的矩阵的行数小于size时,对原先矩阵的行与列都分配够size大小的内存
*/
if (m.rows < size) {
m.vals = realloc(m.vals, size*sizeof(float*));/*realloc函数用于修改一个原先已经分配的内存块的大小,可以使一块内存的扩大或缩小*/
for (i = m.rows; i < size; ++i) {
m.vals[i] = calloc(m.cols, sizeof(float));
}
/*
** 当原先的行数要大于size时,首先将多出来的几行的内存都释放掉,之后重新分配内存区域大小
*/
} else if (m.rows > size) {
for (i = size; i < m.rows; ++i) {
free(m.vals[i]);
}
m.vals = realloc(m.vals, size*sizeof(float*));
}
m.rows = size;
return m;
}
/*
** 矩阵相加,首先断言保证两个矩阵的大小相等,只有相等才能相加
*/
void matrix_add_matrix(matrix from, matrix to)
{
assert(from.rows == to.rows && from.cols == to.cols);
int i,j;
for(i = 0; i < from.rows; ++i){
for(j = 0; j < from.cols; ++j){
to.vals[i][j] += from.vals[i][j];/*刚开始怀疑这块是不是地址相加了,其实不是,这里的下标起到了间接访问的结果*/
}
}
}
matrix copy_matrix(matrix m)
{
matrix c = {0};
c.rows = m.rows;
c.cols = m.cols;
c.vals = calloc(c.rows, sizeof(float *));
int i;
for(i = 0; i < c.rows; ++i){
c.vals[i] = calloc(c.cols, sizeof(float));
copy_cpu(c.cols, m.vals[i], 1, c.vals[i], 1);/*这个函数位于blas.c中,相当于第i行的所有列的数字给复制过去*/
}
return c;
}
/*
** 创造矩阵
** calloc()函数分配所需的内存空间,并返回指向这个空间的指针
*/
matrix make_matrix(int rows, int cols)
{
int i;
matrix m;
m.rows = rows;
m.cols = cols;
m.vals = calloc(m.rows, sizeof(float *));/*calloc分配rows个float指针大小的内存空间*/
for(i = 0; i < m.rows; ++i){
m.vals[i] = calloc(m.cols, sizeof(float));/*为每个行指针分配cols个float大小的内存空间*/
}
return m;
}
/*
**这个函数将给定的矩阵随机抽取n行放入新的矩阵中,同时也打乱了给定的矩阵
**这里输入的矩阵的地址,相当于传址调用,这样可以改变m矩阵本身的值
*/
matrix hold_out_matrix(matrix *m, int n)
{
int i;
matrix h;
h.rows = n;
h.cols = m->cols;
h.vals = calloc(h.rows, sizeof(float *));
for(i = 0; i < n; ++i){
int index = rand()%m->rows;
h.vals[i] = m->vals[index];
m->vals[index] = m->vals[--(m->rows)];
}
return h;
}
/*提取出给定矩阵中的c个列*/
float *pop_column(matrix *m, int c)
{
float *col = calloc(m->rows, sizeof(float));
int i, j;
for(i = 0; i < m->rows; ++i){
col[i] = m->vals[i][c];
for(j = c; j < m->cols-1; ++j){
m->vals[i][j] = m->vals[i][j+1];
}
}
--m->cols;
return col;
}
/*
** 将csv文件转为矩阵格式
*/
matrix csv_to_matrix(char *filename)
{
FILE *fp = fopen(filename, "r");
if(!fp) file_error(filename);
matrix m;
m.cols = -1;
char *line;
int n = 0;
int size = 1024;
m.vals = calloc(size, sizeof(float*));
while((line = fgetl(fp))){
if(m.cols == -1) m.cols = count_fields(line);
if(n == size){
size *= 2;
m.vals = realloc(m.vals, size*sizeof(float*));
}
m.vals[n] = parse_fields(line, m.cols);/*此函数用来解析csv格式的数据*/
free(line);
++n;
}
m.vals = realloc(m.vals, n*sizeof(float*));
m.rows = n;
return m;
}
void matrix_to_csv(matrix m)
{
int i, j;
for(i = 0; i < m.rows; ++i){
for(j = 0; j < m.cols; ++j){
if(j > 0) printf(",");
printf("%.17g", m.vals[i][j]);
}
printf("\n");
}
}
void print_matrix(matrix m)
{
int i, j;
printf("%d X %d Matrix:\n",m.rows, m.cols);
printf(" __");
for(j = 0; j < 16*m.cols-1; ++j) printf(" ");
printf("__ \n");
printf("| ");
for(j = 0; j < 16*m.cols-1; ++j) printf(" ");
printf(" |\n");
for(i = 0; i < m.rows; ++i){
printf("| ");
for(j = 0; j < m.cols; ++j){
printf("%15.7f ", m.vals[i][j]);
}
printf(" |\n");
}
printf("|__");
for(j = 0; j < 16*m.cols-1; ++j) printf(" ");
printf("__|\n");
}
再来看image的实现,image的源代码中主要是对图像的一些处理过程,感兴趣的小伙伴可以结合数字图像处理来看,说实话数字图像处理我还没怎么看过,mask等操作我也都没了解,等我研究一下以后再更新上,这里我就挑几个函数来分析一下。
tile_images
首先必须了解tile images是啥,打开百度问度娘得到如下解释
tile_images tiles multiple input image objects, which must contain the same number of channels, into a large image
也就是说这个函数可以将两张通道数相同的图片拼成一张大图,要分析这个函数首先要看另外两个函数
embed_image
/** 将输入source图片的像素值嵌入到目标图片dest中.
* @param source 源图片
* @param dest 目标图片(相当于该函数的输出)
* @param dx 列偏移(dx=(source.w-dest.w)/2,因为源图尺寸一般小于目标图片尺寸,所以要将源图嵌入到目标图中心,源图需要在目标图上偏移dx开始插入,如下图)
* @param dy 行偏移(dx=(source.h-dest.h)/2)
* @details 下图所示,外层为目标图(dest),内层为源图(source),源图尺寸不超过目标图,源图嵌入到目标图中心
* ###############
* # #
* #<-->###### #
* # dx # # #
* # ###### #
* # dy #
* ###############
* @details 此函数是将源图片的嵌入到目标图片中心,意味着源图片的尺寸(宽高)不大于目标图片的尺寸,
* 目标图片在输入函数之前已经有初始化值了,因此如果源图片的尺寸小于目标图片尺寸,
* 那么源图片会覆盖掉目标图片中新区域的像素值,而目标图片四周多出的像素值则保持为初始化值.
*/
void embed_image(image source, image dest, int dx, int dy)
{
int x,y,k;
for(k = 0; k < source.c; ++k){
for(y = 0; y < source.h; ++y){
for(x = 0; x < source.w; ++x){
/// 获取源图中k通道y行x列处的像素值
float val = get_pixel(source, x,y,k);
/// 设置目标图中k通道dy+y行dx+x列处的像素值为val
set_pixel(dest, dx+x, dy+y, k, val);
}
}
}
}
composite_image
/*
** 图像融合
** 将dest图像偏移后的位置用source图像的像素代替从而达到融合的作用
*/
void composite_image(image source, image dest, int dx, int dy)
{
int x,y,k;
for(k = 0; k < source.c; ++k){
for(y = 0; y < source.h; ++y){
for(x = 0; x < source.w; ++x){
float val = get_pixel(source, x, y, k);
float val2 = get_pixel_extend(dest, dx+x, dy+y, k);
set_pixel(dest, dx+x, dy+y, k, val * val2);
}
}
}
}
image tile_images(image a, image b, int dx)
{
if(a.w == 0) return copy_image(b);
/*创建一个c的图片结构,宽为a与b的和加上偏置项dx,图像的高和通道数与ab中较大的为准*/
image c = make_image(a.w + b.w + dx, (a.h > b.h) ? a.h : b.h, (a.c > b.c) ? a.c : b.c);
/*向c的data中填充1*/
fill_cpu(c.w*c.h*c.c, 1, c.data, 1);
embed_image(a, c, 0, 0);
composite_image(b, c, a.w + dx, 0);
return c;
}
draw_detections
/*
** 本函数用于绘制检测信息,同时打印检测结果
* @param im 输入图片,将在im上绘制检测结果,绘制内容包括定位用矩形框,由26个单字母(由alphabet)拼接而成的类别标签
* @param dets 检测信息,包括检测框坐标,概率,类别等
* @param num 整张图片所拥有的box(物体检测框)个数,输入图片经过检测之后,会划分成l.w*l.h的网格,每个网格中会提取l.n个box,
* 每个box对应一个包含所有类别所属的概率的数组,数组中元素最大的并且大于某一指定概率阈值对应的类别将作为这个box最终所检测出的物体类别
* @param thresh 概率阈值,每个box最大的所属类别概率必须大于这个阈值才接受这个检测结果,否则该box将作废(即不认为其包含物体)
* @param probs 这是一个二维数组,行为num,列为物体类别总数(就是该模型所能检测的物体类别的总数,或者说训练数据集中所含有的所有物体类别总数,
* 训练集中不包含的物体肯定无法检测~~),即每行对应一个box,每行包含该框所属所有物体类别的概率。每行最大的元素所对应的物体类别为
* 该检测框最有可能所属的物体类别,当然,最终判定是否属于该类别还需要看其是否大于某一指定概率阈值。
* @param names 可以理解成一个一维字符串数组(C中没有字符串的概念,用C字符数组实现字符串),每行存储一个物体的名称,共有classes行
* @param alphabet
* @param classes 物体类别总数
*/
void draw_detections(image im, detection *dets, int num, float thresh, char **names, image **alphabet, int classes)
{
int i,j;
/*遍历所有检测框,加入最开始将图片网格化为m*n个小网格,每个网格检测k个检测框,那么该图片的num就是m*n*k,这是根据网络设定来的*/
for(i = 0; i < num; ++i){
char labelstr[4096] = {0};
int class = -1;
/*遍历每个类别*/
for(j = 0; j < classes; ++j){
/*只有当检测框的当前分类的概率值大于某个阈值时,才认为当前检测框有检测到物体,之后才分析这个物体是什么类别*/
/*这部分和之前的版本有些不同,之前时得到最大的分类概率,判断最大概率是否大于阈值如果不大于,那么直接不考虑这个检测框*/
if (dets[i].prob[j] > thresh){
if (class < 0) {
strcat(labelstr, names[j]);
class = j;
} else {
strcat(labelstr, ", ");
strcat(labelstr, names[j]);
}
/*打印该框为j类物体的概率*/
printf("%s: %.0f%%\n", names[j], dets[i].prob[j]*100);
}
}
if(class >= 0){
int width = im.h * .006;
/*
if(0){
width = pow(prob, 1./2.)*10+1;
alphabet = 0;
}
*/
//printf("%d %s: %.0f%%\n", i, names[class], prob*100);
int offset = class*123457 % classes;
float red = get_color(2,offset,classes);
float green = get_color(1,offset,classes);
float blue = get_color(0,offset,classes);
float rgb[3];
//width = prob*20+2;
rgb[0] = red;
rgb[1] = green;
rgb[2] = blue;
box b = dets[i].bbox;
//printf("%f %f %f %f\n", b.x, b.y, b.w, b.h);
/* 计算矩形框在图片中的真实像素坐标(box中所含的都是比例坐标),left为矩形框左横坐标,right为矩形框右横坐标,
* top为矩形框上方纵坐标,bot为矩形框下方纵坐标(这里也是按照OpenCV的习惯,y轴往下,x轴往右,原点为左上角)*/0
int left = (b.x-b.w/2.)*im.w;
int right = (b.x+b.w/2.)*im.w;
int top = (b.y-b.h/2.)*im.h;
int bot = (b.y+b.h/2.)*im.h;
if(left < 0) left = 0;
if(right > im.w-1) right = im.w-1;
if(top < 0) top = 0;
if(bot > im.h-1) bot = im.h-1;
draw_box_width(im, left, top, right, bot, width, red, green, blue);
if (alphabet) {
image label = get_label(alphabet, labelstr, (im.h*.03));
draw_label(im, top + width, left, label, rgb);
free_image(label);
}
if (dets[i].mask){
image mask = float_to_image(14, 14, 1, dets[i].mask);
image resized_mask = resize_image(mask, b.w*im.w, b.h*im.h);
image tmask = threshold_image(resized_mask, .5);
embed_image(tmask, im, left, top);
free_image(mask);
free_image(resized_mask);
free_image(tmask);
}
}
}
}