Anchor策略:


yolov3延续了yolov2的anchor策略,基本没有变化。


边框的表示方式通过框的中心坐标bx,by,和框的宽bw,高bh这4个变量来表示。实际预测的值为tx,ty,tw,th。


由tx,ty,tw,th得到bx,by,bw,bh的详细公式如上图,其中,


cx,cy为框的中心坐标所在的grid cell 距离左上角第一个grid cell的cell个数。


tx,ty为预测的边框的中心点坐标。


σ()函数为logistic函数,将坐标归一化到0-1之间。最终得到的bx,by为归一化后的相对于grid cell的值


tw,th为预测的边框的宽,高。


pw,ph为anchor的宽,高。实际在使用中,作者为了将bw,bh也归一化到0-1,实际程序中的 pw,ph为anchor的宽,高和featuremap的宽,高的比值。最终得到的bw,bh为归一化后相对于feature map的比值


σ(t0)表示预测的边框的置信度,为预测的边框的概率和预测的边框与ground truth的IOU值的乘积。


 


这里有别于faster系列,yolov3只为ground truth 匹配一个最优的边界框。


分类损失函数:


yolov3中将yolov2中多分类损失函数softmax cross-entropy loss 换为2分类损失函数binary cross-entropy loss 。因为当图片中存在物体相互遮挡的情形时,一个box可能属于好几个物体,而不是单单的属于这个不属于那个,这时使用2分类的损失函数就更有优势。


多尺度预测:


Yolov3采用了类似SSD的mul-scales策略,使用3个scale(13_13,26_26,52_52)的feature map进行预测。


有别于yolov2,这里作者将每个grid cell预测的边框数从yolov2的5个减为yolov3的3个。最终输出的tensor维度为N × N × [3 ∗ (4 + 1 + 80)] 。其中N为feature map的长宽,3表示3个预测的边框,4表示边框的tx,ty,tw,th,1表示预测的边框的置信度,80表示分类的类别数。


和yolov2一样,anchor的大小作者还是使用kmeans聚类得出。在coco数据集上的9个anchor大小分别为:(10× 13); (16× 30); (33× 23); (30× 61); (62× 45); (59×119); (116 × 90); (156 × 198); (373 × 326)


其中在yolov3中,最终有3个分支输出做预测,输出的特征图大小分别为13_13,26_26,52_52,每个特征图使用3个anchor,


13_13的特征图使用(116 × 90); (156 × 198); (373 × 326);这3个anchor


26_26的特征图使用(30× 61); (62× 45); (59×119);这3个anchor


52_52的特征图使用(10× 13); (16× 30); (33× 23);这3个anchor


而在yolov3-tiny中,一共有6个anchor,(10,14), ( 23,27),  (37,58),  (81,82),  (135,169),  (344,319),


yolov3-tiny最终有2给分支输出作预测,特征图大小分别为13_13,26_26。每个特征图使用3个anchor做预测。


13_13的特征图使用(81,82),  (135,169),  (344,319)这3个anchor


26_26的特征图使用( 23,27),  (37,58),  (81,82)这3个anchor


 


plus:


faster rcnn:3个scale(128_128,256_256,512_512),3个aspect ratio(1:1,1:2,2:1)共9个anchor


ssd:5个aspect ratio(1:1,1:2,1:3,2:1,3:1),再加一个中间的default box,一共6个anchor


yolov3:一共9个anchor


tiny-yolov3:一共6个anchor


FPN:5个scale(32_32; 64_64; 128_128; 256_256; 512_512),3个aspect ratio(1:1,1:2,2:1),共15个anchor


ctpn:anchor宽度固定为16,高度为11-283之间的10个数,每次处以0.7得到,最终得到[11, 16, 23, 33, 48, 68, 97, 139, 198, 283]共10个anchor


 

plus:


假设都是用voc数据集


faster rcnn:最后的输出层分类部分的全连接层输出的个数是21。虽然faster已经先经过前面的RPN的2分类,过滤掉了大部分背景类别,但是后续仍然有可能存在背景类别。


SSD:分类的类别为21类,因为,使用softmax loss,肯定会有一个值最大,所以必须得加背景类别。


yolov3:20类,因为使用的是多个sigmoid来代替softmax,本质上每一个sigmoid都是前景,背景分类问题。


使用多尺度融合的策略,使得yolov3的召回率和准确性都有大的提升。


Backbone骨架:


和yolov2的19层的骨架(Darknet-19 )不同,yolov3中,作者提出了53层的骨架(Darknet-53 ),并且借鉴了ResNet的shortcut结构。



 


上图为论文中的网络结构,但是卷积层只有52层,和作者实际的程序还是有点出入。为此,自己根据作者的程序撸了一个,主干网络还是52层。


一个需要注意的地方,yolov3-tiny 有max pooling,而yolov3使用stride=2的卷积代替pooling操作


yolov3-tiny:


yolov3:


精度vs速度:


Yolov3的精度和速度都达到的空前的高快。


在分类任务中,以darknet-53的骨架网络,速度是ResNet-152的2倍,精度也基本相当。


在检测任务中,当IOU标准定为0.5时,只比RetinaNet低3.2%个点。在IOU标准定为0.75时,比RetinaNet低9.7%个点。其实这个问题也是yolo一直存在的一个问题,在相对比较小的检测物体上,会存在检测框不是很准的想象。速度方面比RetinaNet快出3倍多。


RUN:(测试显卡为P40)


  1. cd darknet
  2. Make -j32
  3. ./darknet detect cfg/yolov3.cfg yolov3.weights data/dog.jpg



./darknet detect cfg/yolov3-tiny.cfg yolov3-tiny.weights data/dog.jpg


训练自己数据:


这里假定我要实现一个简单的3个类别检测(3个类别)。


(1)首先就是数据集的准备,这里建议使用python+QT开发的抠图小工具,labelImg。保存的时候可以选择保存为voc格式,也可以保存为yolo格式。建议保存为VOC格式,因为格式更加标准通用。


(2)模仿VOC的格式建立相应的文件夹,执行,


  1. cd darknet
  2. mkdir VOCdevkit
  3. cd VOCdevkit
  4. mkdir VOC2019
  5. mkdir Annotations ImageSets JPEGImages labels
  6. cd ImageSets/
  7. mkdir Main


tree -d


目录结构显示如下,



其中,VOC2019为我自己的数据集起的名字,你也可以起别的名字,Annotations存放XML文件,Main中存放,train.txt,val.txt,test.txt,txt中只写图片的名字,一行一个。JPEGImages中存放图片。labels中存放由XML生成的txt文件。


(3)修改scripts下面的voc_label.py,将数据集的目录修改为自己的目录,


  1. 开始几行
  2. sets=[(‘2019’, ‘train’), (‘2019’, ‘val’),(‘2019’, ‘test’)]
  3. classes = [“apple”, “banana”, “orange”]
  4. 最后2
  5. os.system(“cat 2019_train.txt > train.txt”)
  6. os.system(“cat 2019_train.txt > train.all.txt”)


然后执行


Python3  scripts/voc_label.py


就会生成labels文件夹,以及文件夹下面的txt标记,以及train.txt 和train.all.txt


其中,train.txt中存储路径+图片名,一行一个


  1. /data/darknet/VOCdevkit/VOC2019/JPEGImages/55000087.jpg
  2. /data/darknet/VOCdevkit/VOC2019/JPEGImages/43000097.jpg
  3. /data/darknet/VOCdevkit/VOC2019/JPEGImages/14000107.jpg


Labels文件夹下每个图片对应一个txt文件,里面存储类别 框坐标的归一化值


  1. 2 0.368896484375 0.14908854166666666 0.03076171875 0.03515625
  2. 2 0.328125 0.18359375 0.0283203125 0.03515625
  3. 0 0.190185546875 0.6207682291666666 0.03173828125 0.026692708333333332
  4. 1 0.40625 0.21028645833333331 0.193359375 0.16666666666666666


(4)修改,cfg/voc.data


class为训练的类别数


train为训练集train.txt


valid为验证集val.txt


names为voc.names,里面为自己训练的目标名称


backup为weights的存储位置


  1. classes= 3
  2. train = /DATA/darknet/<span class=”hljs-title class_“>VOCdevkit/2019_train.txt
  3. valid = /DATA/darknet/<span class=”hljs-title class_“>VOCdevkit/2019_test.txt
  4. names = /DATA/darknet/data/voc.names
  5. backup = /DATA/darknet/weights


(5)修改cfg/yolov3.cfg


修改每个classes=3(610,696,783共3处修改)


修改最后一个卷基层,filters和最后一个region的classes,num参数是因为yolov3有3给分支,每个分支3个anchor。


其中,filters=num×(classes + coords + 1)=3_(3+4+1)=24,这里我有3个类别。(603,689,776行,共3处修改)


(6)执行下面的语句进行训练


./darknet detector train ./cfg/voc.data ./cfg/yolov3.cfg  ./ yolov3.weights -clear


-clear参数可以加载作者的预训练模型,重新进行微调训练。


训练完毕就可以生成weights文件,


 


(7)测试,执行下面语句,


./darknet detect  ./cfg/yolov3.cfg  weights/yolov3_final.weights  1.jpg


  (8)anchor修改,根据自己的数据集重新kmeans设置anchor,自己撸的程序


  1. import matplotlib.pyplot as plt
  2. from sklearn.datasets.samples_generator import make_blobs
  3. from sklearn.cluster import KMeans
  4. from sklearn import metrics
  5. import xml.etree.ElementTree as ET
  6. import os
  7. def parse_xml(xmlpath,train_input_width,train_input_height):
  8. tree = ET.parse(xmlpath)
  9. root = tree.getroot()
  10. for size in root.iter(‘size’):
  11. width_text = int(size.find(‘width’).text)
  12. height_text = int(size.find(‘height’).text)
  13. width_list=[]
  14. height_list=[]
  15. for box in root.iter(‘bndbox’):
  16. x1 = int(box.find(‘xmin’).text)
  17. y1 = int(box.find(‘ymin’).text)
  18. x2 = int(box.find(‘xmax’).text)
  19. y2 = int(box.find(‘ymax’).text)
  20. width=(x2-x1)/width_text_train_input_width#经过resize后的长宽
  21. height=(y2-y1)/height_text_train_input_height#经过resize后的长宽
  22. width_list.append(width)
  23. height_list.append(height)
  24. return width_list,height_list
  25. xml_path_lists=[“./VOC2012/Annotations/“,“./VOC2017/Annotations/“]#xml位置
  26. kmeans_num=6#聚类类别数
  27. train_input_width=320#训练网络输入图片宽度
  28. train_input_height=320#训练网络输入图片高度
  29. width_list_all=[]
  30. height_list_all=[]
  31. for xml_path in xml_path_lists:
  32. for xml in os.listdir(xml_path):
  33. width_list,height_list=parse_xml(xml_path+xml,train_input_width,train_input_height)
  34. width_list_all.extend(width_list)
  35. height_list_all.extend(height_list)
  36. plt.scatter(width_list_all, height_list_all, marker=‘o’) # 假设暂不知道y类别,不设置c=y,使用kmeans聚类
  37. plt.show()
  38. kmeans = KMeans(n_clusters=kmeans_num, random_state=9).fit(list(zip(width_list_all,height_list_all)))
  39. y_pred = KMeans(n_clusters=kmeans_num, random_state=9).fit_predict(list(zip(width_list_all,height_list_all)))
  40. plt.scatter(width_list_all, height_list_all, c=y_pred)
  41. plt.show()
  42. print (kmeans.cluster_centers_)
  43. print(metrics.calinski_harabaz_score(list(zip(width_list_all,height_list_all)), y_pred))



最终输出结果,




官方程序:


  1. ‘’’
  2. Created on Feb 20, 2017
  3. @author: jumabek
  4. ‘’’
  5. from os import listdir
  6. from os.path import isfile, join
  7. import argparse
  8. #import cv2
  9. import numpy as np
  10. import sys
  11. import os
  12. import shutil
  13. import random
  14. import math
  15. width_in_cfg_file = 416.
  16. height_in_cfg_file = 416.
  17. def <span class=”hljs-title function_“>IOU(x,centroids):
  18. similarities = []
  19. k = len(centroids)
  20. for centroid in centroids:
  21. c_w,c_h = centroid
  22. w,h = x
  23. if c_w>=w and c_h>=h:
  24. similarity = w_h/(c_w_c_h)
  25. elif c_w>=w and c_h<=h:
  26. similarity = w_c_h/(w_h + (c_w-w)_c_h)
  27. elif c_w<=w and c_h>=h:
  28. similarity = c_w_h/(w_h + c_w_(c_h-h))
  29. else: #means both w,h are bigger than c_w and c_h respectively
  30. similarity = (c_w_c_h)/(w_h)
  31. similarities.append(similarity) # will become (k,) shape
  32. return np.array(similarities)
  33. def <span class=”hljs-title function_“>avg_IOU(X,centroids):
  34. n,d = X.shape
  35. sum = 0.
  36. for i in range(X.shape[0]):
  37. #note IOU() will return array which contains IoU for each centroid and X[i] // slightly ineffective, but I am too lazy
  38. sum+= max(IOU(X[i],centroids))
  39. return sum/n
  40. def <span class=”hljs-title function_“>write_anchors_to_file(centroids,X,anchor_file):
  41. f = open(anchor_file,‘w’)
  42. anchors = centroids.copy()
  43. print(anchors.shape)
  44. for i in range(anchors.shape[0]):
  45. anchors[i][0]_=width_in_cfg_file/32.
  46. anchors[i][1]_=height_in_cfg_file/32.
  47. widths = anchors[:,0]
  48. sorted_indices = np.argsort(widths)
  49. print(‘Anchors = ‘, anchors[sorted_indices])
  50. for i in sorted_indices[:-1]:
  51. f.write(‘%0.2f,%0.2f, ‘%(anchors[i,0],anchors[i,1]))
  52. #there should not be comma after last anchor, that’s why
  53. f.write(‘%0.2f,%0.2f\n’%(anchors[sorted_indices[-1:],0],anchors[sorted_indices[-1:],1]))
  54. f.write(‘%f\n’%(avg_IOU(X,centroids)))
  55. print()
  56. def <span class=”hljs-title function_“>kmeans(X,centroids,eps,anchor_file):
  57. N = X.shape[0]
  58. iterations = 0
  59. k,dim = centroids.shape
  60. prev_assignments = np.ones(N)*(-1)
  61. iter = 0
  62. old_D = np.zeros((N,k))
  63. while True:
  64. D = []
  65. iter+=1
  66. for i in range(N):
  67. d = 1 - IOU(X[i],centroids)
  68. D.append(d)
  69. D = np.array(D) # D.shape = (N,k)
  70. print(“iter {}: dists = {}”.format(iter,np.sum(np.abs(old_D-D))))
  71. #assign samples to centroids
  72. assignments = np.argmin(D,axis=1)
  73. if (assignments == prev_assignments).all() :
  74. print(“Centroids = “,centroids)
  75. write_anchors_to_file(centroids,X,anchor_file)
  76. return
  77. #calculate new centroids
  78. centroid_sums=np.zeros((k,dim),np.float)
  79. for i in range(N):
  80. centroid_sums[assignments[i]]+=X[i]
  81. for j in range(k):
  82. centroids[j] = centroid_sums[j]/(np.sum(assignments==j))
  83. prev_assignments = assignments.copy()
  84. old_D = D.copy()
  85. def <span class=”hljs-title function_“>main(argv):
  86. parser = argparse.ArgumentParser()
  87. parser.add_argument(‘-filelist’, default = ‘\path\to\voc\filelist\train.txt’,
  88. help=‘path to filelist\n’ )
  89. parser.add_argument(‘-output_dir’, default = ‘generated_anchors/anchors’, type = str,
  90. help=‘Output anchor directory\n’ )
  91. parser.add_argument(‘-num_clusters’, default = 0, type = int,
  92. help=‘number of clusters\n’ )
  93. args = parser.parse_args()
  94. if not os.path.exists(args.output_dir):
  95. os.mkdir(args.output_dir)
  96. f = open(args.filelist)
  97. lines = [line.rstrip(‘\n’) for line in f.readlines()]
  98. annotation_dims = []
  99. size = np.zeros((1,1,3))
  100. for line in lines:
  101. line = line.replace(‘JPEGImages’,‘labels’)
  102. line = line.replace(‘.jpg’,‘.txt’)
  103. line = line.replace(‘.png’,‘.txt’)
  104. print(line)
  105. f2 = open(line)
  106. for line in f2.readlines():
  107. line = line.rstrip(‘\n’)
  108. w,h = line.split(‘ ‘)[3:]
  109. #print(w,h)
  110. annotation_dims.append(tuple(map(float,(w,h))))
  111. annotation_dims = np.array(annotation_dims)
  112. eps = 0.005
  113. if args.num_clusters == 0:
  114. for num_clusters in range(1,11): #we make 1 through 10 clusters
  115. anchor_file = join( args.output_dir,‘anchors%d.txt’%(num_clusters))
  116. indices = [ random.randrange(annotation_dims.shape[0]) for i in range(num_clusters)]
  117. centroids = annotation_dims[indices]
  118. kmeans(annotation_dims,centroids,eps,anchor_file)
  119. print(‘centroids.shape’, centroids.shape)
  120. else:
  121. anchor_file = join( args.output_dir,‘anchors%d.txt’%(args.num_clusters))
  122. indices = [ random.randrange(annotation_dims.shape[0]) for i in range(args.num_clusters)]
  123. centroids = annotation_dims[indices]
  124. kmeans(annotation_dims,centroids,eps,anchor_file)
  125. print(‘centroids.shape’, centroids.shape)
  126. if __name__==“__main__“:
  127. main(sys.argv)



输出结果,



2个结果有一些差异,但是相差不是很大。


 


Python接口:


Python/darknet.py


注意这里Python检测输出的结果为中心坐标和宽高。


 


原始的接口读取图片为作者自己的结构体IMAGE方式,这里增加numpy转IMAGE的接口。


1、在darkenet.py中自定义一个函数,大概48行


  1. def nparray_to_image(img):
  2. data = img.ctypes.data_as(POINTER(c_ubyte))
  3. image = ndarray_image(data, img.ctypes.shape, img.ctypes.strides)
  4. return image


2、在darknet.py中增加如下行代码,大概127行


  1. ndarray_image = lib.ndarray_to_image
  2. ndarray_image.argtypes = [POINTER(c_ubyte), POINTER(c_long), POINTER(c_long)]
  3. ndarray_image.restype = IMAGE


3、在src/image.c中增加如下代码段,增加位置大概550行,


  1. #ifdef NUMPY
  2. image ndarray_to_image(unsigned char_ src, long_ shape, long_ strides)
  3. {
  4. int h = shape[0];
  5. int w = shape[1];
  6. int c = shape[2];
  7. int step_h = strides[0];
  8. int step_w = strides[1];
  9. int step_c = strides[2];
  10. image im = make_image(w, h, c);
  11. int i, j, k;
  12. int index1, index2 = 0;
  13. for(i = 0; i < h; ++i){
  14. for(k= 0; k < c; ++k){
  15. for(j = 0; j < w; ++j){
  16. index1 = k_w_h + i_w + j;
  17. index2 = step_h_i + step_w_j + step_c_k;
  18. //fprintf(stderr, “w=%d h=%d c=%d step_w=%d step_h=%d step_c=%d \n”, w, h, c, step_w, step_h, step_c);
  19. //fprintf(stderr, “im.data[%d]=%u data[%d]=%f \n”, index1, src[index2], index2, src[index2]/255.);
  20. im.data[index1] = src[index2]/255.;
  21. }
  22. }
  23. }
  24. rgbgr_image(im);
  25. return im;
  26. }
  27. #endif



4、在src/image.h的23行后面加如下代码


  1. #ifdef NUMPY
  2. image ndarray_to_image(unsigned char_ src, long_ shape, long_ strides);
  3. #endif


5、在makefile的47行后面中加如下代码


  1. ifeq ($(NUMPY), 1)
  2. COMMON+= -DNUMPY -I/usr/include/python3.6/ -I /usr/lib/python3/dist-packages/numpy/core/include/numpy/
  3. CFLAGS+= -DNUMPY
  4. Endif


在makefile的第1行后面中加如下代码NUMPY =1


  1. CUDNN=1
  2. OPENCV=1
  3. OPENMP=0
  4. NUMPY=1
  5. DEBUG=0


6、重新编译make clean + make


7、修改darknet.py的后续处理


  1. if __name__ == “__main__“:
  2. net = load_net(b“cfg/yolov3.cfg”, b“yolov3.weights”, 0)
  3. meta = load_meta(b“cfg/coco.data”)
  4. image = cv2.imread(b‘data/dog.jpg’)
  5. im=nparray_to_image(image)
  6. r = detect(net, meta, im)
  7. print(r)
  8. for newbox in r:
  9. p1 = (int(newbox[0]), int(newbox[1]))
  10. p2 = (int(newbox[2]), int(newbox[3]))
  11. cv2.rectangle(image, p1, p2, (255,0,0))
  12. cv2.imshow(‘tracking’, image)
  13. cv2.waitKey()


 


References:


https://pjreddie.com/darknet/yolo/


https://github.com/pjreddie/darknet