目标检测Tensorflow:Yolo v3代码详解 (1)
一、标注数据
1、标注工具
这里给一个labelimg软件的传送门https://pan.baidu.com/s/1tuIQmuyedRHP1WeGVVSx_Q 提取码: ejgx 。
使用的工具是LabelImg,由于YOLOV3的label标注的一行五个数分别代表类别:编号,BoundingBox 中心 X 坐标,中心 Y 坐标,宽,高。这些坐标都是 0~1 的相对坐标。由于该标注工具默认生成.xml文件属性,这时我们可以通过LabelImg修改生成文件属性。点击如下红框可直接生成yolov3算法需求的文件(.txt)
2、数据集编号
import os
def rename(file_path):
"""
遍历文件并且进行重命名
:param file_path:文件的路径(绝对)
:return:
"""
filelist = os.listdir(file_path)
total_num = len(filelist)
i = 1
for item in filelist:
# if item.endswith('.jpg'):
if item.endswith('.txt'):
src = os.path.join(os.path.abspath(file_path), item)
str1 = str(i)
# dst = os.path.join(os.path.abspath(file_path), str1.zfill(6) + '.jpg') # str1.zfill(x),x为一共几位数,用0补齐,如001000
dst = os.path.join(os.path.abspath(file_path), str1.zfill(6) + '.txt') # str1.zfill(x),x为一共几位数,用0补齐,如001000
try:
os.rename(src, dst)
print('converting %s to %s ...' % (src, dst))
i = i + 1
except:
continue
# file_path = 'D:\\ImageLabel\\TargetData\\output\\' # .jpg图片的存储路径
file_path = 'D:\\ImageLabel\\SaveTxt' # .txt图片的存储路径
rename(file_path)
为了规划自己的数据,减少出错的可能性,最好自己先给自己的图片编一个合理的序号,比如0001~0999。
3、标注数据
利用软件把自己的数据标注好。每一个图片名对应的有一个相应名字的label.xml。
xml中的数据如下所示
4、利用voc制作自己的数据集
在目录下新建VOC2007,并在VOC2007下新建Annotations,ImageSets和JPEGImages三个文件夹。在ImageSets下新建Main文件夹。文件目录如下所示:
将自己的数据集图片拷贝到JPEGImages目录下。将数据集label文件拷贝到Annotations目录下。在VOC2007下新建test.py文件夹,将下面代码拷贝进去运行,准备训练、验证、测试数据,生成四个文件:train.txt,val.txt,test.txt和trainval.txt。
import os
import random
from datetime import datetime
import xml.etree.ElementTree as ET
import core.utils as utils
from config import cfg
class Prepare(object):
def __init__(self):
# 图像路径
self.image_path = cfg.COMMON.IMAGE_PATH
# 图像的后缀名
self.image_extension = cfg.COMMON.IMAGE_EXTENSION
# xml 路径
self.annotation_path = cfg.COMMON.ANNOTATION_PATH
# 获取 c 类 字典型
self.classes_dir = utils.read_class_names(cfg.COMMON.CLASS_FILE_PATH)
self.classes_len = len(self.classes_dir)
# 获取 c 类 list 型
self.classes_list = [self.classes_dir[key] for key in range(self.classes_len)]
# 数据的百分比
self.test_percent = cfg.COMMON.TEST_PERCENT
self.val_percent = cfg.COMMON.VAL_PERCENT
# 各成分数据保存路径
self.train_data_path = cfg.TRAIN.TRAIN_DATA_PATH
self.val_data_path = cfg.TRAIN.VAL_DATA_PATH
self.test_data_path = cfg.TEST.TEST_DATA_PATH
pass
def do_prepare(self):
xml_file_list = os.listdir(self.annotation_path)
xml_len = len(xml_file_list)
# 根据百分比得到各成分 数据量
n_test = int(xml_len * self.test_percent)
n_val = int(xml_len * self.val_percent)
n_train = xml_len - n_test - n_val
if os.path.exists(self.train_data_path):
os.remove(self.train_data_path)
pass
if os.path.exists(self.val_data_path):
os.remove(self.val_data_path)
pass
if os.path.exists(self.test_data_path):
os.remove(self.test_data_path)
pass
# 随机划分数据
n_train_val = n_train + n_val
train_val_list = random.sample(xml_file_list, n_train_val)
train_list = random.sample(train_val_list, n_train)
train_file = open(self.train_data_path, "w")
val_file = open(self.val_data_path, "w")
test_file = open(self.test_data_path, "w")
for xml_name in xml_file_list:
# 名字信息
name_info = xml_name[: -4]
# 图像名
image_name = name_info + self.image_extension
# 如果文件名在 训练 和 验证 文件划分中
if xml_name in train_val_list:
# 如果文件名在 训练数据划分中
if xml_name in train_list:
self.convert_annotation(xml_name, image_name, train_file)
train_file.write('\n')
pass
# 否则文件在 验证 文件
else:
self.convert_annotation(xml_name, image_name, val_file)
val_file.write('\n')
pass
pass
# 否则文件名在 测试 文件
else:
self.convert_annotation(xml_name, image_name, test_file)
test_file.write('\n')
pass
pass
def convert_annotation(self, xml_name, image_name, file):
xml_path = os.path.join(self.annotation_path, xml_name)
image_path = os.path.join(self.image_path, image_name)
file.write(image_path)
# 打开 xml 文件
xml_file = open(xml_path)
# 将 xml 文件 转为树状结构
tree = ET.parse(xml_file)
root = tree.getroot()
for obj in root.iter("object"):
diff = obj.find("difficult").text
cls = obj.find("name").text
if cls not in self.classes_list or int(diff) == 1:
continue
cls_id = self.classes_list.index(cls)
xml_box = obj.find("bndbox")
b = (int(xml_box.find('xmin').text), int(xml_box.find('ymin').text),
int(xml_box.find('xmax').text), int(xml_box.find('ymax').text))
file.write(" " + ",".join([str(a) for a in b]) + ',' + str(cls_id))
pass
pass
if __name__ == "__main__":
# 代码开始时间
start_time = datetime.now()
print("开始时间: {}".format(start_time))
demo = Prepare()
demo.do_prepare()
# 代码结束时间
end_time = datetime.now()
print("结束时间: {}, 训练模型耗时: {}".format(end_time, end_time - start_time))
pass
5、先验框设计
即使用 k-means 算法对训练集上的 boudnding box 尺度做聚类。此外,考虑到训练集上的图片尺寸不一,因此对此过程进行归一化处理。
k-means 聚类算法有个坑爹的地方在于,类别的个数需要事先指定。这就带来一个问题,先验框 anchor 的数目等于多少最合适?一般来说,anchor 的类别越多,那么 YOLO 算法就越能在不同尺度下与真实框进行回归,但是这样就导致模型的复杂度更高,网络的参数量更庞大。
在上面这张图里,作者发现 k=5 时就能较好的实现高召回率和模型复杂度之间的平衡。由于在 YOLOV3 算法里一种有 3 个尺度预测,因此只能是 3 的倍数,所以最终选择了 9 个先验框。这里还有个问题需要解决,k-means 度量距离的选取很关键。距离度量如果使用标准的欧氏距离,大框框就会比小框产生更多的错误。在目标检测领域,我们度量两个边界框之间的相似度往往以 IOU 大小作为标准。因此,这里的度量距离也和 IOU 有关。需要特别注意的是,这里的IOU计算只用到了 boudnding box 的长和宽。在作者的代码里,是认为两个先验框的左上角位置是相重合的。(其实在这里偏移至哪都无所谓,因为聚类的时候是不考虑 anchor 框的位置信息的。)
如果两个边界框之间的IOU值越大,那么它们之间的距离就会越小。
def kmeans(boxes, k, dist=np.median,seed=1):
"""
Calculates k-means clustering with the Intersection over Union (IoU) metric.
:param boxes: numpy array of shape (r, 2), where r is the number of rows
:param k: number of clusters
:param dist: distance function
:return: numpy array of shape (k, 2)
"""
rows = boxes.shape[0]
distances = np.empty((rows, k)) ## N row x N cluster
last_clusters = np.zeros((rows,))
np.random.seed(seed)
# initialize the cluster centers to be k items
clusters = boxes[np.random.choice(rows, k, replace=False)]
while True:
# 为每个点指定聚类的类别(如果这个点距离某类别最近,那么就指定它是这个类别)
for icluster in range(k): # I made change to lars76's code here to make the code faster
distances[:,icluster] = 1 - iou(clusters[icluster], boxes)
nearest_clusters = np.argmin(distances, axis=1)
# 如果聚类簇的中心位置基本不变了,那么迭代终止。
if (last_clusters == nearest_clusters).all():
break
# 重新计算每个聚类簇的平均中心位置,并它作为聚类中心点
for cluster in range(k):
clusters[cluster] = dist(boxes[nearest_clusters == cluster], axis=0)
last_clusters = nearest_clusters
return clusters,nearest_clusters,distances
二、构建yolov3模型
1、YOLO-V3模型设计思想
YOLO V3算法的基本思想可以分成两部分:
1、按一定规则在图片上产生一系列的候选区域,然后根据这些候选区域与图片上物体真实框之间的位置关系对候选区域进行标注。跟真实框足够接近的那些候选区域会被标注为正样本,同时将真实框的位置作为正样本的位置目标。偏离真实框较大的那些候选区域则会被标注为负样本,负样本不需要预测位置或者类别。
2、使用卷积神经网络提取图片特征并对候选区域的位置和类别进行预测。这样每个预测框就可以看成是一个样本,根据真实框相对它的位置和类别进行了标注而获得标签值,通过网络模型预测其位置和类别,将网络预测值和标签值进行比较,就可以建立起损失函数。
2、YOLO-V3网络
1、如上图所示:在训练过程中对于每幅输入图像,yolov3会预测三个不同大小的3D tensor,对应着三个不同的scale,设计这三个scale的目的是为了能检测出不同大小的物体。
2、这里以13 * 13的tensor为例,对于这个scale,原始输入图像会被分割成13 × 13的grid cell,每个grid cell对应着3D tensor中的1x1x255这样一个长条形voxel。255是由3x(4+1+80)而来,由上图可知,公式NxNx[3x(4+1+80)]中NxN表示的是scale size例如上面提到的13x13。3表示的是each grid predict 3 boxes。4表示的是坐标值即(tx,ty,tw,th)。1表示的是置信度,80表示的是类别数目。
3、如果训练集中某一个ground truth对应的bounding box中心恰好落在了输入图像的摸一个grid cell中,那么这个grid cell就负责预测此物体的bounding box,于是这个grid cell所对应的置信度为1,其他grid cell的置信度为0.每个grid cell都会被赋予3个不同大小的prior box,学习过程中,这个grid cell会学会如何选择哪个大小的prior box。作者定义的是只选择与ground truth bounding box的IOU重合度最高的prior box。
4、上面说到的三个预设的不同大小的prior box,这三个大小是如何计算的,首先在训练前,将coco数据集中的所有bbox使用k-means clustering分成9个类别,每3个类别对应一个scale,故总共3个scale,这种关于box大小的先验信息帮助网络准确预测每个box的offset和coordinate。从直观上,大小合适的box会使得网络更精准的学习。
产生候选区域
如何产生候选区域,是检测模型的核心设计方案。目前大多数基于卷积神经网络的模型所采用的方式大体如下:
1、按一定的规则在图片上生成一系列位置固定的锚框,将这些锚框看作是可能的候选区域
2、对锚框是否包含目标物体进行预测,如果包含目标物体,还需要预测所包含物体的类别,以及预测框相对于锚框位置需要调整的幅度。
生成锚框
让我们思考下面一个例子,其中输入图像大小是 416×416,网络的步幅是 32。如之前所述,特征图的维度会是 13×13。随后,我们将输入图像分为 13×13 个网格。
生成预测框
下面的公式描述了网络输出是如何转换,以获得边界框预测结果的
其中,Cx,Cy是feature map中grid cell的左上角坐标,在yolov3中每个grid cell在feature map中的宽和高均为1。如下图1的情形时,这个bbox边界框的中心属于第二行第二列的grid cell,它的左上角坐标为(1,1),故Cx=1,Cy=1.公式中的Pw、Ph是预设的anchor box映射到feature map中的宽和高(anchor box原本设定是相对于416416坐标系下的坐标,在yolov3.cfg文件中写明了,代码中是把cfg中读取的坐标除以stride如32映射到feature map坐标系中)。
最终得到的边框坐标值是bx,by,bw,bh即边界框bbox相对于feature map的位置和大小,是我们需要的预测输出坐标。但我们网络实际上的学习目标是tx,ty,tw,th这4个offsets,其中tx,ty是预测的坐标偏移值,tw,th是尺度缩放,有了这4个offsets,自然可以根据之前的公式去求得真正需要的bx,by,bw,bh4个坐标。
那么4个坐标tx,ty,tw,th是怎么求出来的呢?对于训练样本,在大多数文章里需要用到ground truth的真实框来求这4个坐标:
上面这个公式是faster-rcnn系列文章用到的公式,Px,Py在faster-rcnn系列文章是预设的anchor box在feature map上的中心点坐标。 Pw、Ph是预设的anchor box的在feature map上的宽和高。至于Gx、Gy、Gw、Gh自然就是ground truth在这个feature map的4个坐标了(其实上面已经描述了这个过程,要根据原图坐标系先根据原图纵横比不变映射为416416坐标下的一个子区域如416312,取 min(w/img_w, h/img_h)这个比例来缩放成416312,再填充为416416,坐标变换上只需要让ground truth在416312下的y1,y2(即左上角和右下角纵坐标)加上图2灰色部分的一半,y1=y1+(416-416/768576)/2=y1+(416-312)/2,y2同样的操作,把x1,x2,y1,y2的坐标系的换算从针对实际红框的坐标系(416312)变为416*416下了,这样保证bbox不会扭曲,然后除以stride得到相对于feature map的坐标)。
但是在yolov3中与faster-rcnn系列文章用到的公式在前两行是不同的,yolov3里Px和Py就换为了feature map上的grid cell左上角坐标Cx,Cy了,即在yolov3里是Gx,Gy减去grid cell左上角坐标Cx,Cy。x,y坐标并没有针对anchon box求偏移量,所以并不需要除以Pw,Ph。
也就是说是tx = Gx - Cx
ty = Gy - Cy
这样就可以直接求bbox中心距离grid cell左上角的坐标的偏移量。
tw和th的公式yolov3和faster-rcnn系列是一样的,是物体所在边框的长宽和anchor box长宽之间的比率,不管Faster-RCNN还是YOLO,都不是直接回归bounding box的长宽而是尺度缩放到对数空间,是怕训练会带来不稳定的梯度。因为如果不做变换,直接预测相对形变tw和th,那么要求tw,th>0,因为你的框的宽高不可能是负数。这样,是在做一个有不等式条件约束的优化问题,没法直接用SGD来做。所以先取一个对数变换,将其不等式约束去掉,就可以了。
这里就有个重要的疑问了,一个尺度的feature map有三个anchors,那么对于某个ground truth框,究竟是哪个anchor负责匹配它呢?和YOLOv1一样,对于训练图片中的ground truth,若其中心点落在某个cell内,那么该cell内的3个anchor box负责预测它,具体是哪个anchor box预测它,需要在训练中确定,即由那个与ground truth的IOU最大的anchor box预测它,而剩余的2个anchor box不与该ground truth匹配。YOLOv3需要假定每个cell至多含有一个grounth truth,而在实际上基本不会出现多于1个的情况。与ground truth匹配的anchor box计算坐标误差、置信度误差(此时target为1)以及分类误差,而其它的anchor box只计算置信度误差(此时target为0)。
边框回归为何只能微调?当输入的 Proposal 与 Ground Truth 相差较小时,,即IOU很大时(RCNN 设置的是 IoU>0.6), 可以认为这种变换是一种线性变换, 那么我们就可以用线性回归(线性回归就是给定输入的特征向量 X, 学习一组参数 W, 使得经过线性回归后的值跟真实值 Y(Ground Truth)非常接近. 即Y≈WX )来建模对窗口进行微调, 否则会导致训练的回归模型不work(当 Proposal跟 GT 离得较远,就是复杂的非线性问题了,此时用线性回归建模显然就不合理了)
那么训练时用的ground truth的4个坐标去做差值和比值得到tx,ty,tw,th,测试时就用预测的bbox就好了,公式修改就简单了,把Gx和Gy改为预测的x,y,Gw、Gh改为预测的w,h即可。
1、中心坐标
注意:我们使用 sigmoid 函数进行中心坐标预测。这使得输出值在 0 和 1 之间。原因如下:
正常情况下,YOLO 不会预测边界框中心的确切坐标。它预测:
与预测目标的网格单元左上角相关的偏移;
使用特征图单元的维度(1)进行归一化的偏移。
以我们的图像为例。如果中心的预测是 (0.4, 0.7),则中心在 13 x 13 特征图上的坐标是 (6.4, 6.7)(红色单元的左上角坐标是 (6,6))。
但是,如果预测到的 x,y 坐标大于 1,比如 (1.2, 0.7)。那么中心坐标是 (7.2, 6.7)。注意该中心在红色单元右侧的单元中,或第 7 行的第 8 个单元。这打破了 YOLO 背后的理论,因为如果我们假设红色框负责预测目标狗,那么狗的中心必须在红色单元中,不应该在它旁边的网格单元中。
因此,为了解决这个问题,我们对输出执行 sigmoid 函数,将输出压缩到区间 0 到 1 之间,有效确保中心处于执行预测的网格单元中。
边界框的维度
2、边界框的维度
我们对输出执行对数空间变换,然后乘锚点,来预测边界框的维度。
得出的预测 bw 和 bh 使用图像的高和宽进行归一化。即,如果包含目标(狗)的框的预测 bx 和 by 是 (0.3, 0.8),那么 13 x 13 特征图的实际宽和高是 (13 x 0.3, 13 x 0.8)。
3、锚框优劣的评定
如果该目标的真实边界框已知,这里的“较好”该如何量化呢?一种直观的方法是衡量锚框和真实边界框之间的相似度。我们知道,Jaccard系数(Jaccard index)可以衡量两个集合的相似度。给定集合 A 和 B,它们的Jaccard系数即二者交集大小除以二者并集大小:
实际上,我们可以把边界框内的像素区域看成是像素的集合。可以用两个边界框的像素集合的Jaccard系数衡量这两个边界框的相似度。
通常将Jaccard系数称为交并比(intersection over union,IoU),即两个边界框相交面积与相并面积之比,如图所示。交并比的取值范围在0和1之间:0表示两个边界框无重合像素,1表示两个边界框相等。
这里使用 GIoU 来衡量检测框和 GT bbox 之间的差距
4、怎么样训练锚框
在训练集中,我们将每个锚框视为一个训练样本。需要为每个锚框标注两类标签:一是锚框所含目标的类别,简称类别;二是真实边界框相对锚框的偏移量,简称偏移量(offset)。
大致思路:在目标检测时,
- 我们首先生成多个锚框
- 为每个锚框预测类别以及偏移量
- 根据预测的偏移量调整锚框位置从而得到预测边界框
- 筛选需要输出的预测边界框
Objectness 分数
Object 分数表示目标在边界框内的概率。红色网格和相邻网格的 Object 分数应该接近 1,而角落处的网格的 Object 分数可能接近 0。
objectness 分数的计算也使用 sigmoid 函数,因此它可以被理解为概率
类别置信度
类别置信度表示检测到的对象属于某个类别的概率(如狗、猫、香蕉、汽车等)。在 v3 之前,YOLO 需要对类别分数执行 softmax 函数操作。
但是,YOLO v3 舍弃了这种设计,作者选择使用 sigmoid 函数。因为对类别分数执行 softmax 操作的前提是类别是互斥的。简言之,如果对象属于一个类别,那么必须确保其不属于另一个类别。这在我们设置检测器的 COCO 数据集上是正确的。但是,当出现类别「女性」(Women)和「人」(Person)时,该假设不可行。这就是作者选择不使用 Softmax 激活函数的原因。
置信度是每个bounding box输出的其中一个重要参数,作者对他的作用定义有两重:一重是代表当前box是否有对象的概率,注意,是对象,不是某个类别的对象,也就是说它用来说明当前box内只是个背景(backgroud)还是有某个物体(对象);另一重表示当前的box有对象时,它自己预测的box与物体真实的box可能的的值,注意,这里所说的物体真实的box实际是不存在的,这只是模型表达自己框出了物体的自信程度。以上所述,也就不难理解作者为什么将其称之为置信度了,因为不管哪重含义,都表示一种自信程度:框出的box内确实有物体的自信程度和框出的box将整个物体的所有特征都包括进来的自信程度。经过以上的解释,其实我们也就可以用数学形式表示置信度的定义了
在不同尺度上的预测
YOLO v3 在 3 个不同尺度上进行预测。检测层用于在三个不同大小的特征图上执行预测,特征图步幅分别是 32、16、8。这意味着,当输入图像大小是 416 x 416 时,我们在尺度 13 x 13、26 x 26 和 52 x 52 上执行检测。
该网络在第一个检测层之前对输入图像执行下采样,检测层使用步幅为 32 的层的特征图执行检测。随后在执行因子为 2 的上采样后,并与前一个层的特征图(特征图大小相同)拼接。另一个检测在步幅为 16 的层中执行。重复同样的上采样步骤,最后一个检测在步幅为 8 的层中执行。
在每个尺度上,每个单元使用 3 个锚点预测 3 个边界框,锚点的总数为 9(不同尺度的锚点不同)。
作者称这帮助 YOLO v3 在检测较小目标时取得更好的性能,而这正是 YOLO 之前版本经常被抱怨的地方。上采样可以帮助该网络学习细粒度特征,帮助检测较小目标。
1、小目标检测
2、中等尺度目标检测
3、大尺度目标检测
输出处理
对于大小为 416 x 416 的图像,YOLO 预测 ((52 x 52) + (26 x 26) + 13 x 13)) x 3 = 10647 个边界框。但是,我们的示例中只有一个对象——一只狗。那么我们怎么才能将检测次数从 10647 减少到 1 呢?
目标置信度阈值:首先,我们根据它们的 objectness 分数过滤边界框。通常,分数低于阈值的边界框会被忽略。
非极大值抑制:非极大值抑制(NMS)可解决对同一个图像的多次检测的问题。例如,红色网格单元的 3 个边界框可以检测一个框,或者临近网格可检测相同对象
通俗的说:每个框都有一个所属类别的概率值,根据概率值先确定一个框的类别,然后把与这个框相交、且概率值大于阈值的框全都扔掉。最后剩下的框就是没有重复的预测边界框了
3、构建 yolo-v3 网络模型了 yolov3.py
(1)其中的build_network()函数,建立网络模型
import numpy as np
import tensorflow as tf
import core.utils as utils
import core.common as common
import core.backbone as backbone
from config import cfg
class YoloV3(object):
def __init__(self, input_data, training_flag):
# 超参
self.alpha = cfg.COMMON.ALPHA
self.gamma = cfg.COMMON.GAMMA
# 是否是在训练模式下返回输出 True 为返回
self.training_flag = training_flag
# 获取 classes
self.classes = utils.read_class_names(cfg.COMMON.CLASS_FILE_PATH)
# 获取 classes 的种类数
self.num_class = len(self.classes)
# 获取 YOLOV3 的 3 个尺度
self.strides = np.array(cfg.YOLO.STRIDES)
# 获取 anchors
self.anchors = utils.get_anchors(cfg.COMMON.ANCHOR_FILE_PATH)
# 每个尺度有 3 个 anchors
self.anchor_per_scale = cfg.YOLO.ANCHOR_PER_SCALE
# iou 损失的 阈值
self.iou_loss_thresh = cfg.COMMON.IOU_LOSS_THRESH
# 上采样的方式
self.up_sample_method = cfg.YOLO.UP_SAMPLE_METHOD
try:
# 获取 yolov3 网络 小、中、大 三个尺度的 feature maps
self.conv_lbbox, self.conv_mbbox, self.conv_sbbox = self.structure_network(input_data)
pass
except:
raise NotImplementedError("Can not structure yolov3 network!")
pass
# anchors list [[116., 90.], [156., 198.], [373., 326.]],
# 小尺度的 feature maps 使用 大尺度的 anchors 值,用于检测大尺度目标
with tf.variable_scope('pred_lbbox'):
self.pred_lbbox = self.pred_conv_bbox(self.conv_lbbox, self.anchors[2], self.strides[2])
pass
# anchors list [[30., 61.], [62., 45.], [59., 119.]], 用于检测中等尺度目标
with tf.variable_scope('pred_mbbox'):
self.pred_mbbox = self.pred_conv_bbox(self.conv_mbbox, self.anchors[1], self.strides[1])
pass
# anchors list [[10., 13.], [16., 30.], [33., 23.]],
# 大尺度的 feature maps 使用 相对小点尺度的 anchors 值,用于检测小尺度目标
with tf.variable_scope('pred_sbbox'):
self.pred_sbbox = self.pred_conv_bbox(self.conv_sbbox, self.anchors[0], self.strides[0])
pass