系列文章目录
前言
先验框(与锚)是一个原理,他们主要应用在多目标识别当中,自YOLOV-2系列引入,可以很大程度增加目标检测的性能。直到现在还在广泛应用着。
一. 先验框是什么
简单来说,所谓先验框就是在图像上提前设定好不同大小,不同长宽比的框框。
如上图所示。先验框的概念也是自YOLOV2之后提出的,因为它大大增强了目标增强的性能,所以后来就被习惯引用了。
我们知道目标识别中,需要学习该目标的类别特征,位置,该目标的大小。而在早期的yolov1中,直接通过全连接层进行边框检测这类操作很难适应于不同物体的形状,这使得多目标识别成为了难题。使用anchor boxes之后,YOLOV2的召回率大大提升,所以在Yolo之后的版本中,均保留了先验框这个方法。
每个先验框内部有若干个参数,分别对应x_offset,y_offset,h&w,置信度,分类结果。解码后,先验框可以辅助处理这些信息。通过这些处理提升目标检测的效果。
二. 先验框讲解
1. 获取先验框
获取先验框一般有三个方法:
- 手动获取
- k-means聚类算法或者改良k-means++
- 作为超参数学习
第一种在当代无疑是非常愚蠢的,手动标注,在大工作量面前几乎可以直接放弃了。
第二种是是目前流行的方法,k-means作为机器学习最为简单基础的方法之一,它的应用从早期到现在也非常的广。当我们选择交互比iou作为之指标之后,它对先验框的计算表现出还较好的效果。如果想要了解k-means该算法详细原理及python实现,请移步我的博客:
传送门
第三种暂且不谈。
k-means前文已经已经有博客讲解,前文也提过,对于先验框的设计方法其实就是换了个指标的k-means,这个指标就是iou(交并比)
交并比:交集与并集的比值。可形象的表示:如下
红色为交集
蓝色为并集
交并比就是红色除以蓝色。。。
k-means指标计算如下:
这里的d可以视为普通k-means中的欧氏距离指标,那么很明显交并比越大,则距离越近。
另外我们要注意,在选择先验框的时候,k越多,则平均iou越大,这个推到过程很简单,就不叙述了。同时一般情况下,目标检测效果也会越好,但是相应的计算代价会上升。
三. 代码
'''
锚先验参数计算。
'''
import glob
import xml.etree.ElementTree as ET
import numpy as np
# 计算交并比
def cas_iou(box, cluster):
x = np.minimum(cluster[:, 0], box[0])
y = np.minimum(cluster[:, 1], box[1])
intersection = x * y
area1 = box[0] * box[1]
area2 = cluster[:, 0] * cluster[:, 1]
iou = intersection / (area1 + area2 - intersection)
return iou
def avg_iou(box, cluster):
return np.mean([np.max(cas_iou(box[i], cluster)) for i in range(box.shape[0])])
def kmeans(box, k):
# 取出一共有多少框
row = box.shape[0]
# 每个框各个点的位置
distance = np.empty((row, k))
# 最后的聚类位置
last_clu = np.zeros((row,))
np.random.seed()
# 随机选5个当聚类中心
cluster = box[np.random.choice(row, k, replace=False)]
# cluster = random.sample(row, k)
while True:
# 计算每一行距离五个点的iou情况。
for i in range(row):
distance[i] = 1 - cas_iou(box[i], cluster)
# 取出最小点
near = np.argmin(distance, axis=1)
if (last_clu == near).all():
break
# 求每一个类的中位点
for j in range(k):
cluster[j] = np.median(
box[near == j], axis=0)
last_clu = near
return cluster
def load_data(path):
data = []
# 对于每一个xml都寻找box
for xml_file in glob.glob('{}/*xml'.format(path)):
tree = ET.parse(xml_file)
height = int(tree.findtext('./size/height'))
width = int(tree.findtext('./size/width'))
if height <= 0 or width <= 0:
continue
# 对于每一个目标都获得它的宽高
for obj in tree.iter('object'):
xmin = int(float(obj.findtext('bndbox/xmin'))) / width
ymin = int(float(obj.findtext('bndbox/ymin'))) / height
xmax = int(float(obj.findtext('bndbox/xmax'))) / width
ymax = int(float(obj.findtext('bndbox/ymax'))) / height
xmin = np.float64(xmin)
ymin = np.float64(ymin)
xmax = np.float64(xmax)
ymax = np.float64(ymax)
# 得到宽高
data.append([xmax - xmin, ymax - ymin])
return np.array(data)
if __name__ == '__main__':
# 运行该程序会计算'./VOCdevkit/VOC2007/Annotations'的xml
# 会生成yolo_anchors.txt
SIZE = 416
anchors_num = 9
# 载入数据集,可以使用VOC的xml
path = './data/label'
# 载入所有的xml
# 存储格式为转化为比例后的width,height
data = load_data(path)
# 使用k聚类算法
out = kmeans(data, anchors_num)
out = out[np.argsort(out[:, 0])]
print('acc:{:.2f}%'.format(avg_iou(data, out) * 100))
print(out * SIZE)
data = out * SIZE
f = open("yolo_anchors.txt", 'w')
row = np.shape(data)[0]
for i in range(row):
if i == 0:
x_y = "%d,%d" % (data[i][0], data[i][1])
else:
x_y = ", %d,%d" % (data[i][0], data[i][1])
f.write(x_y)
f.close()