yoloV3 darknet GPU手把手从编译到训练再到C++调用API

我要先声明一下,系统是Ubuntu18.04,我的机器已经装好了opencv4.1.1 和 cuda10.0 的,过程可参考我另一篇博客,这部分不再说明
IDE使用qtcreator

1.下载Darknet

Github链接
官网链接

这个不用多讲,直接git clone,网不好不管

git clone https://github.com/pjreddie/darknet

还要下载预训练权重darknet53.conv.74
这个先放一边,一会拿它组成训练文件夹
下载完成后进入darknet文件夹

2.配置Makefile

为了方便修改代码,使用VSCode打开darknet所在的文件夹

找到Makefile文件,打开,前5行是配置的参数,OPENCV、GPU、CUDNN全部打开

GPU=0
CUDNN=0
OPENCV=0
OPENMP=0
DEBUG=0

修改完就是这样的

GPU=1
CUDNN=1
OPENCV=1
OPENMP=0
DEBUG=0

关于后面的显卡计算能力配置这个参考别的博客,我不修改没有遇到问题

ARCH= -gencode arch=compute_30,code=sm_30 \
      -gencode arch=compute_35,code=sm_35 \
      -gencode arch=compute_50,code=[sm_50,compute_50] \
      -gencode arch=compute_52,code=[sm_52,compute_52]
#      -gencode arch=compute_20,code=[sm_20,sm_21] \ This one is deprecated?

# This is what I use, uncomment if you know your arch and want to specify
# ARCH= -gencode arch=compute_52,code=compute_52

找到第42行,修改pkg-congif的包名称,opencv4的名称就是opencv4,两处都改

ifeq ($(OPENCV), 1) 
COMMON+= -DOPENCV
CFLAGS+= -DOPENCV
LDFLAGS+= `pkg-config --libs opencv` -lstdc++
COMMON+= `pkg-config --cflags opencv` 
endif

这样,其他的不用动

ifeq ($(OPENCV), 1) 
COMMON+= -DOPENCV
CFLAGS+= -DOPENCV
LDFLAGS+= `pkg-config --libs opencv4` -lstdc++
COMMON+= `pkg-config --cflags opencv4` 
endif

下面紧接着是cuda的库,检查一下是否和自己的路径相符,我的默认就好

ifeq ($(GPU), 1) 
COMMON+= -DGPU -I/usr/local/cuda/include/
CFLAGS+= -DGPU
LDFLAGS+= -L/usr/local/cuda/lib64 -lcuda -lcudart -lcublas -lcurand
endif

到此Makefile配置完成

3.编译,并修改一些错误

按快捷键Ctrl+shift+P打开VSCode的命令,搜索C/C++,打开编辑配置(UI),不用UI的随便

  1. 找到包含路径,添加opencv的包含路径和cuda的包含路径,为了后面修改代码方便
    查看opencv的包含地址用pkg-config就行了,例如我的写/usr/local/include/opencv4/**让它自己找就行了
    cuda的找不到,一般在/usr/local/cuda/include里面,我的添加完如下
${workspaceFolder}/**
/usr/local/cuda/include/
/usr/local/include/opencv4/**
  1. 添加全局,就在下面的格里,配置参数打开啥就填上啥,我的是这样
OPENCV
CUDNN
GPU
do{
    make;
    if(编译出错){
    		修改;
    		continue;
    }
    else break;
}while(1); 	//这就是要干的

其实很简单按Ctrl+shift+`打开VSCode的终端,执行make
然后报错

./src/image_opencv.cpp:12:1: error: ‘IplImage’ does not name a type; did you mean ‘image’?
 IplImage *image_to_ipl(image im)
 ^~~~~~~~

这是因为他原来写的是opencv3的结构和命名,4的有些变动
按住Ctrl,鼠标点击./src/image_opencv.cpp:12:1直接转到这个位置,这是我用VSCode的一大原因
在最上面添加一个头文件

#include "opencv2/imgproc/imgproc_c.h"

这个错误就解决了
然后是

./src/image_opencv.cpp:76:20: error: ‘CV_CAP_PROP_FRAME_WIDTH’ was not declared in this scope
    if(w) cap->set(CV_CAP_PROP_FRAME_WIDTH, w);

这个就是命名变化的,此类的把前面的CV_去掉就行,这个错误都在image_opencv.cpp里面,这个文件以后有用,请记住它,改完错误就能完成编译。

4.制作自己的数据集

先建一个文件夹,名字任意,不要带中文,你懂的然后以下都是在这里面操作
darknet YoloV3的数据集格式是文本文档,结构简单
一个JPEGImages文件夹,里面放图片,都是JPG格式的,不是的话可以用opencv自己写程序自动转换
另一个labels文件夹,存放标记文件也是txt格式是文本文档
标记文件具体的格式是,文件名和对应的JPG文件名保证相同

2 0.341624 0.582733 0.00729167 0.0175926
1 0.433048 0.624979 0.0286458 0.062963
4 0.265383 0.666638 0.0776042 0.125926
#ID 空格 box中心点X坐标 空格 box中心点Y坐标 空格 目标宽度W 空格 目标高度H
#坐标都是归一化的0-1,相对于画面的
#一个图多个目标直接换行再写就行

然后两个文件夹一定放在一起,darknet似乎是直接自动找相对于图片文件的../labels/xxxxxx.txt来找标记文件的
再建一个backup文件夹,用来存放训练权重文件的备份
weigits文件夹,用来存放训练完成要保留的权重文件,这个自愿
ImageSets文件夹,用来存图片集在里面再建两个文本文档,就是图片集了
一个名叫train.txt就是用来训练的图片集,里面存JPG图片的路径,推荐绝对路径,一行一个路径
另一个val.txt用来验证格式相同
出ImageSets文件夹,
新建一个voc.names文本文档里面写你的目标的标签,例如

car
people
bicycle
bird
boat

一行一个我已5个目标为例
新建一个voc.data文本文档里面有5行

classes= 你的目标数量
train  = /...../你的训练集文件夹/ImageSets/train.txt
valid  = /...../你的训练集文件夹/ImageSets/val.txt
names  = /...../你的训练集文件夹/voc.names
backup = /...../你的训练集文件夹/backup

还有一个最重要的cfg文件,从cfg/yolov3-voc.cfg里面复制一个,到你的文件夹里,可以重命名,后缀保留(其实应该没关系)然后修改
前几行[net]下面

[net]
# Testing
 batch=1						#batch是多少张图片迭代一次训练,调大能快速收敛,但是这个超级吃显存
 subdivisions=1 		#subdivisions是分多少步去计算一个batch的图片
 										#最终同时在显存里的就是batch/subdivisions,可以达到既能收敛又能省显存的目的
# Training
# batch=64
# subdivisions=16
width=416					#width和height还有channels是网络入口的大小,通道一般不变,宽高在速度和精度之间衡量
height=416
channels=3
momentum=0.9
decay=0.0005
angle=0
saturation = 1.5
exposure = 1.5
hue=.1

learning_rate=0.001		#学习率,不变就好
burn_in=1000
max_batches = 50200	#最大迭代次数
policy=steps						#下面三行都是学习率衰减策略,如果这项不动的话就
steps=40000,45000			#是在迭代40000次时学习率乘0.1,45000次时再乘0.1
scales=.1,.1						#要根据你的最大迭代次数改,可以逗号分隔再往后填

然后在下面找[yolo]项,一共有3个,在608行,692行,776行
拿一个为例,其他三个一样

[convolutional]
size=1
stride=1
pad=1
filters=10							#这个一定要改,和目标数量有关,就是3*(目标数量+5),否则在构建网络的时候报错
activation=linear

[yolo]
mask = 0,1,2
anchors = 10,13,  16,30,  33,23,  30,61,  62,45,  59,119,  116,90,  156,198,  373,326
classes=20						#类的数量
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1						#多尺度训练,能让大图片准,小图片快,代价就是显存
											#如果输入的图片大小固定应该没啥用,可以关掉

再加上一开始下载的预训练权重,这样就配置完成了
最终的文件夹结构

你的训练集文件夹
	╞backup
	╞ImageSets
	║	└train.txt  val.txt
	╞JPEGImages
	║	└你的图片
	╟labels
	║	└你的标签
	╟weigits
	╟voc.names  
	╟voc.data 
	╟yolov3-voc.cfg 
	└darknet53.conv.74

关于训练集标签和图片集的生成,我想大家都是撸代码的,偷懒办法不可能想不出来吧

4.1来一点自动化工具

本来能直接开始训练了,但是本人比较懒,嫌命令太长,然后就有了这个

DARKNET_PATH=你的darknet路径

LOCAT=${PWD}		#如果是放在你的训练集文件夹下面就能自动获取当前目录

classes=5
TRAIN_PATH=${LOCAT}/ImageSets/train.txt
VALID_PATH=${LOCAT}/ImageSets/val.txt
NAMES_PATH=${LOCAT}/voc.names
BACKUP_PATH=${LOCAT}/backup

DATA_PATH=${LOCAT}/voc.data
CFG_PATH=${LOCAT}/yolov3-voc.cfg
MODEL_PATH=${LOCAT}/darknet53.conv.74

cat>${DATA_PATH}<<EOF
classes= ${classes}
train  = ${TRAIN_PATH}
valid  = ${VALID_PATH}
names  = ${NAMES_PATH}
backup = ${BACKUP_PATH}
EOF

cd ${DARKNET_PATH}

if [ "$1" = "train" ];then
    if [ "$2" = "" ];then
        ./darknet detector train ${DATA_PATH} ${CFG_PATH} darknet53.conv.74 -gpus 0
    else
        if [ "$2" = "goon" ];then
            WEIGHT=${BACKUP_PATH}/yolov3-voc.backup
            ./darknet detector train ${DATA_PATH} ${CFG_PATH} ${WEIGHT} -gpus 0
        else
            ./darknet detector train ${DATA_PATH} ${CFG_PATH} $2 -gpus 0
        fi
    fi
fi
if [ "$1" = "test" ];then
    WEIGHT=${BACKUP_PATH}/yolov3-voc.backup
    if [ "$3" = "" ];then
        ./darknet detector test ${DATA_PATH} ${CFG_PATH} ${WEIGHT} -gpus 0 $2
    else
        ./darknet detector test ${DATA_PATH} ${CFG_PATH} $3 $2 -gpus 0
    fi
fi

前面的都简单,使用方法就是
./xxxx.sh train 是训练(默认使用darknet53.conv.74训练)
./xxxx.sh train goon是继续上一次权重训练(默认使用backup/yolov3-voc.backup训练)
./xxxx.sh train 另一个权重文件的路径 就是在另一个权重文件基础上训练
./xxxx.sh test JPG文件路径就是使用那个图片测试
./xxxx.sh test JPG文件路径 权重文件的路径就是使用那个图片和权重文件测试
把这个脚本也放进你的训练集文件夹里就行了

5.开始训练

配置好上一步的前几行变量就可以./xxxx.sh train
想要预览效果可以./xxxx.sh test JPG文件路径

6.C++调用

首先将darknet目录下include/darknet.h和libdarknet.so复制到其他的安全的地方(比如在/usr/local下面新建一个文件夹)然后把那个共享库加到系统里

cd /etc/ld.so.conf.d/
sudo vim darknet.conf  #里面写入你的共享库所在文件夹的路径
sudo ldconfig					#刷新共享库路径

新建一个qtcreator的项目(拿qtcreator举例,操作都可以在其他平台上复现)
在pro文件里添加opencv(这个就不详细说了)
添加darknet.h的引用目录,添加cuda的引用目录,前面提到了一般在/usr/local/cuda/include里面
添加darknet和cuda的库路径,添加OPENCV那几个在VSCode里面添加过的宏
完事就是这样的

#darknet依赖cuda,需要添加cuda头文件路径和库
INCLUDEPATH += /usr/local/darknet /usr/local/cuda/include
LIBS += -L/usr/local/darknet/lib -L/usr/local/cuda/lib64 -lcuda -lcudart -lcublas -lcurand -lcudnn -ldarknet
DEFINES += GPU OPENCV CUDNN     #darknet内部需要的全局宏,否则功能打不开

回到VSCode里,读一下darknet的源码
找到examples/darknet.c,里面包含main函数,可以顺着找到函数,调用肯定是和测试差不多的,找到识别detector命令的地方

    } else if (0 == strcmp(argv[1], "detector")){
        run_detector(argc, argv);

就是这个函数,到定义

    if(0==strcmp(argv[2], "test")) test_detector(datacfg, cfg, weights, filename, thresh, hier_thresh, outfile, fullscreen);
    else if(0==strcmp(argv[2], "train")) train_detector(datacfg, cfg, weights, gpus, ngpus, clear);

能找到test命令和train命令,到定义test_detector

void test_detector(char *datacfg, char *cfgfile, char *weightfile, char *filename, float thresh, float hier_thresh, char *outfile, int fullscreen)
{
/*
    printf("进入test_detector\n"
            "datacfg = %s\n"    //路径配置文件
            "cfgfile = %s\n"    //网络配置文件
            "weightfile = %s\n" //权重文件
            "filename = %s\n"   //测试图片
            "thresh = %f\n"     //最低可能性阈值,默认0.5
            "hier_thresh = %f\n"//? = 0.5
            "outfile = %s\n"    //输出图像路径
            "fullscreen = %d\n" //全屏显示选项
            , datacfg, cfgfile, weightfile, filename, thresh, hier_thresh, outfile, fullscreen);*/
    list *options = read_data_cfg(datacfg);																			//读取.data路径文件$
    char *name_list = option_find_str(options, "names", "data/names.list");	//获取标签名列表文件路径$
    /*printf("name_list = %s\n", name_list);*/
    char **names = get_labels(name_list);									//解析签名列表文件到字符串数组$

    image **alphabet = load_alphabet();									//加载标签图片
    network *net = load_network(cfgfile, weightfile, 0);		//从配置文件和权重加载神经网络$
    set_batch_network(net, 1);														//启动网络(CUDNN支持)$
    srand(2222222);
    double time;
    char buff[256];
    char *input = buff;
    float nms=.45;
    while(1){
        if(filename){													//如果测试图片文件名非空,拷贝文件名作为输入
            strncpy(input, filename, 256);			
        } else {																//否则询问测试图片文件路径
            printf("Enter Image Path: ");
            fflush(stdout);
            input = fgets(input, 256, stdin);
            if(!input) return;
            strtok(input, "\n");
        }
        image im = load_image_color(input,0,0);						//加载测试图片(load_image_cv函数加检查)
        image sized = letterbox_image(im, net->w, net->h);	//缩放图像至网络入口大小$
        //image sized = resize_image(im, net->w, net->h);
        //image sized2 = resize_max(im, net->w);
        //image sized = crop_image(sized2, -((net->w - sized2.w)/2), -((net->h - sized2.h)/2), net->w, net->h);
        //resize_network(net, sized.w, sized.h);
        layer l = net->layers[net->n-1];


        float *X = sized.data;							//获取图像数组指针
        time=what_time_is_it_now();		//获取当前时间
        network_predict(net, X);					//网络推理$
        printf("%s: Predicted in %f seconds.\n", input, what_time_is_it_now()-time);
        int nboxes = 0;
        //获得bandbox列表$
        detection *dets = get_network_boxes(net, im.w, im.h, thresh, hier_thresh, 0, 1, &nboxes); 		
        //printf("%d\n", nboxes);
        //if (nms) do_nms_obj(boxes, probs, l.w*l.h*l.n, l.classes, nms);
        if (nms) do_nms_sort(dets, nboxes, l.classes, nms);
        draw_detections(im, dets, nboxes, thresh, names, alphabet, l.classes);		//画bandbox
        free_detections(dets, nboxes);																							//释放bandbox列表
        if(outfile){
            save_image(im, outfile);			//如果outfile非空,按outfile路径保存输出图像
        }
        else{
            save_image(im, "predictions");
#ifdef OPENCV
            make_window("predictions", 512, 512, 0);
            show_image(im, "predictions", 0);
#endif
        }

        free_image(im);
        free_image(sized);
        if (filename) break;
    }
}

这就是最好的例程了,其中$结尾的是必须的步骤

如果想要使用opencv读取图像,找到src/image_opencv.cpp
里面的mat_to_image是将opencv的Mat转换为Yolo接受的格式是函数,ipl_to_image和rgbgr_image就在旁边,需要啥就复制出来

image mat_to_image(Mat m)
{
	IplImage ipl = m;
	image im = ipl_to_image(&ipl);
	rgbgr_image(im);
	return im;
}

关于detection结构体说明,有些我也没看懂,但是这些够用

typedef struct
{
	//中心点坐标和bandbox宽高(归一化的)
	float x, y, w, h;
} box;

typedef struct detection
{
	boxbbox;           //bandbox
	int classes;
	float *prob;        	//数组,各个目标的可能性
	float *mask;
	float objectness;
	int sort_class;
} detection;

最后送上例程一份

#include
#include
#include 
#include

using namespace std;
using namespace cv;

#define datacfg     "/home/yao/Documents/yoloV3/darknet/VOCdevkit/VOC2007/voc.data"
#define cfgfile     "/home/yao/Documents/yoloV3/darknet/VOCdevkit/VOC2007/yolov3-voc.cfg"
#define weightfile  "/home/yao/Documents/yoloV3/darknet/VOCdevkit/VOC2007/weigits/640_480_3K_5.23.weights"
#define videosrc    "/home/yao/Videos/0.mp4"
#define SavePAYH    "/home/yao/Videos/640_480_3K_5.23.avi"
#define PicturePATH "/home/yao/Downloads/qq-files/1790931015/file_recv/BL}A~RL7L3Y0({BI0[}JGRT.png"
#define SaveVideo 1
#define USE_Picture 0

void drawBox(Mat& Src, const Scalar& color, box Box, const char* name, float prob);
Scalar get_Scalar(int index, int classes);
image cvmat_to_image(cv::Mat m);

int main()
{
    list *options = read_data_cfg(datacfg);                          //读取路径文件
    char *name_list = option_find_str(options, "names", NULL);          //获取标签名列表文件路径
    char **names = get_labels(name_list);                               //解析签名列表文件到字符串数组
    int classes = option_find_int(options, "classes", 2);               //获得总标签数量
    network *net = load_network(cfgfile, weightfile, 0);             //从配置文件和权重加载神经网络
    set_batch_network(net, 1);                                          //启动网络(CUDNN支持)

#if (USE_Picture == 0)
    VideoCapture CAP(videosrc);
//用摄像头就设置一下
//    CAP.open("/dev/video0", CAP_V4L2);
//    CAP.set(CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M', 'J', 'P', 'G'));
//    CAP.set(CAP_PROP_FRAME_HEIGHT, 480);
//    CAP.set(CAP_PROP_FRAME_WIDTH, 640);
//    CAP.set(CAP_PROP_AUTO_WB, 1);
//    CAP.set(CAP_PROP_AUTO_EXPOSURE, 0);
//    CAP.set(CAP_PROP_EXPOSURE, 121);
//    CAP.set(CAP_PROP_FPS, 120);
#endif

#if(SaveVideo == 1)
    VideoWriter Writer(SavePAYH, VideoWriter::fourcc('M', 'J', 'P', 'G'), 30, Size(1920, 1080));
    if(!Writer.isOpened())
    {
        cout << "Writer Error" << endl;
        exit(-1);
    }
#endif

    Mat Src, Dst;
    int nboxes = 0;
    namedWindow("Video", WINDOW_NORMAL);
    resizeWindow("Video", 1280, 720);
#if (USE_Picture == 0)
    double time1 = 0;
    double time2 = 0;
    char buff[100] = { 0 };
    if(CAP.isOpened())
    {
        while(CAP.read(Src))
        {
            time1 = (double)getCPUTickCount();
#else
    Src = imread(PicturePATH);
#endif
            resize(Src, Dst, Size(net->w,net->h));                                        //缩放至网络入口大小
            image im = cvmat_to_image(Dst);                                             //将图像转换为Yolo格式
            network_predict(net, im.data);                                                  //使用图像数据网络推理
            detection *dets = get_network_boxes(net, im.w, im.h, 0.5, 0.5, 0, 1, &nboxes);  //获得bandbox列表
            cout << nboxes << endl;
            for(int i = 0; i < nboxes; ++i)
            {
                //cout << i << ": objectness=" << dets[i].objectness << endl;
                float probmax = dets[i].prob[0];
                int index = 0;
                for(int j = 0; j < dets->classes; ++j)
                {
                    if(probmax < dets[i].prob[j])       //循环找出可能性最大的序号
                    {
                        probmax = dets[i].prob[j];
                        index = j;
                    }
                    //cout << "names=" << names[j] << " prob=" << dets[i].prob[j] << endl;
                }
                if(probmax < 0.5) //可能性小于一定值时抛弃
                {
                    cout << "throw" << endl;
                    continue;
                }


                drawBox(Src, get_Scalar(index, classes), dets[i].bbox, names[index], probmax);
            }

#if (USE_Picture == 0)
#if(SaveVideo == 1)
            time2 = (double)getCPUTickCount();
            double usetime = (time2 - time1) / getTickFrequency();

            sprintf(buff, "Time = %4.3lfms  FPS = %2.2lf BoxConut = %d", usetime * 1000.0, 1.0 / usetime, nboxes);
            cout << buff << endl;

            putText(Src, buff, Point(25, 100), FONT_ITALIC, 1, Scalar(0, 255, 0), 2);
            putText(Src, weightfile, Point(25, 50), FONT_ITALIC, 1, Scalar(0, 255, 0), 2);

            imshow("Video", Src);
            Writer.write(Src);
#endif
            waitKey(1);
#else
            imshow("Video", Src);
            waitKey(100000);
#endif

            free_image(im);                   //free!!!!!!!纯C编写的darknet不会自动释放内存
            free_detections(dets, nboxes);    //free!!!!!!!纯C编写的darknet不会自动释放内存
#if (USE_Picture == 0)
        }
    }
    else
    {
        cout << "CAP Error" << endl;
    }
#endif
    free_network(net);                  //free!!!!!!!纯C编写的darknet不会自动释放内存
    free_ptrs((void**)names, classes);  //free!!!!!!!纯C编写的darknet不会自动释放内存
    free_list(options);                 //free!!!!!!!纯C编写的darknet不会自动释放内存
    cout << "Done!" << endl;
}

//画出bandbox及其标签和可能性
void drawBox(Mat& Src, const Scalar& color, box Box, const char* name, float prob)
{
    int left  = (Box.x-Box.w/2.)*Src.cols;
    int right = (Box.x+Box.w/2.)*Src.cols;
    int top   = (Box.y-Box.h/2.)*Src.rows;
    int bot   = (Box.y+Box.h/2.)*Src.rows;

    if(left < 0) left = 0;
    if(right > Src.cols-1) right = Src.cols-1;
    if(top < 0) top = 0;
    if(bot > Src.rows-1) bot = Src.rows-1;

    Point p1(left, top);
    Point p2(right, bot);

    putText(Src, name + to_string(prob * 100) + "%", p1 - Point(0, 3), FONT_HERSHEY_PLAIN, 1, color);
    rectangle(Src, p1, p2, color, 2);
}

float get_color(int c, int x, int max)
{
    float colors[6][3] = { {1,0,1}, {0,0,1},{0,1,1},{0,1,0},{1,1,0},{1,0,0} };
    float ratio = ((float)x / max) * 5;
    int i = floor(ratio);
    int j = ceil(ratio);
    ratio -= i;
    float r = (1-ratio) * colors[i][c] + ratio*colors[j][c];
    return r;
}

Scalar get_Scalar(int index, int classes)
{
    int offset = index * 123457 % classes;
    int red = get_color(0, offset, classes) * 255;
    int green = get_color(1, offset, classes) * 255;
    int blue = get_color(2, offset, classes) * 255;
    return Scalar(blue, green, red);
}

image cvipl_to_image(IplImage *src)
{
    int h = src->height;
    int w = src->width;
    int c = src->nChannels;
    image im = make_image(w, h, c);
    unsigned char *data = (unsigned char *)src->imageData;
    int step = src->widthStep;
    int i, j, k;

    for (i = 0; i < h; ++i)
    {
        for (k = 0; k < c; ++k)
        {
            for (j = 0; j < w; ++j)
            {
                im.data[k * w * h + i * w + j] = data[i * step + j * c + k] / 255.;
            }
        }
    }
    return im;
}

image cvmat_to_image(cv::Mat m)
{
    IplImage ipl = m;
    image im = cvipl_to_image(&ipl);
    rgbgr_image(im);
    return im;
}

都到这份上了别再想着白嫖我了

你可能感兴趣的:(#,YoloV3,darknet)