我们读yolov3论文时都知道边框预测的公式,然而难以准确理解为何作者要这么做,这里我就献丑来总结解释一下个人的见解,总结串联一下学习时容易遇到的疑惑,期待对大家有所帮助,理解错误的地方还请大家批评指正,我只是个小白哦,发出来也是为了与大家多多交流,看看理解的对不对。
论文中边框预测公式如下:
其中,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原本设定是相对于416*416坐标系下的坐标,在yolov3.cfg文件中写明了,代码中是把cfg中读取的坐标除以stride如32映射到feature map坐标系中)。
图1
最终得到的边框坐标值是bx,by,bw,bh即边界框bbox相对于feature map的位置和大小,是我们需要的预测输出坐标。但我们网络实际上的学习目标是tx,ty,tw,th这4个offsets,其中tx,ty是预测的坐标偏移值,tw,th是尺度缩放,有了这4个offsets,自然可以根据之前的公式去求得真正需要的bx,by,bw,bh4个坐标。至于为何不直接学习bx,by,bw,bh呢?因为YOLO 的输出是一个卷积特征图,包含沿特征图深度的边界框属性。边界框属性由彼此堆叠的单元格预测得出。因此,如果你需要在 (5,6) 处访问该单元格的第二个边框bbox,那么你需要通过 map[5,6, (5 C): 2*(5 C)] 将其编入索引。这种格式对于输出处理过程(例如通过目标置信度进行阈值处理、添加对中心的网格偏移、应用锚点等)很不方便,因此我们求偏移量即可。那么这样就只需要求偏移量,也就可以用上面的公式求出bx,by,bw,bh,反正是等价的。另外,通过学习偏移量,就可以通过网络原始给定的anchor box坐标经过线性回归微调(平移加尺度缩放)去逐渐靠近groundtruth.为何微调可看做线性回归看后文。
图2
剩下的灰色区域用(128,128,128)填充即可构造为416*416。不管训练还是测试时都需要这样操作原图。pytorch代码中比较好理解这一点。下面这个函数实现了对原图的变换。
代码语言:javascript复制def letterbox_image(img, inp_dim):
"""
lteerbox_image()将图片按照纵横比进行缩放,将空白部分用(128,128,128)填充,调整图像尺寸
具体而言,此时某个边正好可以等于目标长度,另一边小于等于目标长度
将缩放后的数据拷贝到画布中心,返回完成缩放
"""
img_w, img_h = img.shape[1], img.shape[0]
w, h = inp_dim#inp_dim是需要resize的尺寸(如416*416)
# 取min(w/img_w, h/img_h)这个比例来缩放,缩放后的尺寸为new_w, new_h,即保证较长的边缩放后正好等于目标长度(需要的尺寸),另一边的尺寸缩放后还没有填充满.
new_w = int(img_w * min(w/img_w, h/img_h))
new_h = int(img_h * min(w/img_w, h/img_h))
resized_image = cv2.resize(img, (new_w,new_h), interpolation = cv2.INTER_CUBIC) #将图片按照纵横比不变来缩放为new_w x new_h,768 x 576的图片缩放成416x312.,用了双三次插值
# 创建一个画布, 将resized_image数据拷贝到画布中心。
canvas = np.full((inp_dim[1], inp_dim[0], 3), 128)#生成一个我们最终需要的图片尺寸hxwx3的array,这里生成416x416x3的array,每个元素值为128
# 将wxhx3的array中对应new_wxnew_hx3的部分(这两个部分的中心应该对齐)赋值为刚刚由原图缩放得到的数组,得到最终缩放后图片
canvas[(h-new_h)//2:(h-new_h)//2 new_h,(w-new_w)//2:(w-new_w)//2 new_w, :] = resized_image
return canvas
而且我们注意yolov3需要的训练数据的label是根据原图尺寸归一化了的,这样做是因为怕大的边框的影响比小的边框影响大,因此做了归一化的操作,这样大的和小的边框都会被同等看待了,而且训练也容易收敛。既然label是根据原图的尺寸归一化了的,自己制作数据集时也需要归一化才行,如何转为yolov3需要的label网上有一大堆教程,也可参考我的文章:将实例分割数据集转为目标检测数据集(https://zhuanlan.zhihu.com/p/49979730),这里不再赘述。
img
这里就有个重要的疑问了,一个尺度的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至多含有一个ground truth,而在实际上基本不会出现多于1个的情况。与ground truth匹配的anchor box计算坐标误差、置信度误差(此时target为1)以及分类误差,而其它的anchor box只计算置信度误差(此时target为0)。
图3
img
img
这个公式tx,ty为何要sigmoid一下啊?
img
代码语言:javascript复制box get_yolo_box(float *x, float *biases, int n, int index, int i, int j, int lw, int lh, int w, int h, int stride)
{
box b;
b.x = (i x[index 0*stride]) / lw;
// 此处相当于知道了X的index,要找Y的index,向后偏移l.w*l.h个索引
b.y = (j x[index 1*stride]) / lh;
b.w = exp(x[index 2*stride]) * biases[2*n] / w;
b.h = exp(x[index 3*stride]) * biases[2*n 1] / h;
return b;
}
float delta_yolo_box(box truth, float *x, float *biases, int n, int index, int i, int j, int lw, int lh, int w, int h, float *delta, float scale, int stride)
{
box pred = get_yolo_box(x, biases, n, index, i, j, lw, lh, w, h, stride);
float iou = box_iou(pred, truth);
float tx = (truth.x*lw - i);
float ty = (truth.y*lh - j);
float tw = log(truth.w*w / biases[2*n]);
float th = log(truth.h*h / biases[2*n 1]);
//scale = 2 - groundtruth.w * groundtruth.h
delta[index 0*stride] = scale * (tx - x[index 0*stride]);
delta[index 1*stride] = scale * (ty - x[index 1*stride]);
delta[index 2*stride] = scale * (tw - x[index 2*stride]);
delta[index 3*stride] = scale * (th - x[index 3*stride]);
return iou;
}
我们还可以注意到代码中有个注释scale = 2 - groundtruth.w * groundtruth.h,这是什么含义?
实际上,我们知道yolov1里 作者在loss里对宽高都做了开根号处理,是为了使得大小差别比较大的边框差别减小。 因为对不同大小的bbox预测中,想比于大的bbox预测偏差,小bbox预测偏差相同的尺寸对IOU影响更大,而均方误差对同样的偏差loss一样,为此取根号。例如,同样将一个 100x100 的目标与一个 10x10 的目标都预测大了 10 个像素,预测框为 110 x 110 与 20 x 20。显然第一种情况我们还可以接受,但第二种情况相当于把边界框预测大了 1 倍,但如果不使用根号函数,那么损失相同,但把宽高都增加根号时:
img
代码语言:javascript复制#scaling_factor*img_w和scaling_factor*img_h是图片按照纵横比不变进行缩放后的图片,即原图是768x576按照纵横比长边不变缩放到了416*372。
#经坐标换算,得到的坐标还是在输入网络的图片(416x416)坐标系下的绝对坐标,但是此时已经是相对于416*372这个区域的坐标了,而不再相对于(0,0)原点。
output[:,[1,3]] -= (inp_dim - scaling_factor*im_dim_list[:,0].view(-1,1))/2#x1=x1−(416−scaling_factor*img_w)/2,x2=x2-(416−scaling_factor*img_w)/2
output[:,[2,4]] -= (inp_dim - scaling_factor*im_dim_list[:,1].view(-1,1))/2#y1=y1-(416−scaling_factor*img_h)/2,y2=y2-(416−scaling_factor*img_h)/2
代码语言:javascript复制void correct_yolo_boxes(detection *dets, int n, int w, int h, int netw, int neth, int relative)
{
int i;
// 此处new_w表示输入图片经压缩后在网络输入大小的letter_box中的width,new_h表示在letter_box中的height,
// 以1280*720的输入图片为例,在进行letter_box的过程中,原图经resize后的width为416, 那么resize后的对应height为720*416/1280,
//所以height为234,而超过234的上下空余部分在作为网络输入之前填充了128,new_h=234
int new_w=0;
int new_h=0;
// 如果w>h说明resize的时候是以width/图像的width为resize比例的,先得到中间图的width,再根据比例得到height
if (((float)netw/w) < ((float)neth/h)) {
new_w = netw;
new_h = (h * netw)/w;
} else {
new_h = neth;
new_w = (w * neth)/h;
}
for (i = 0; i < n; i){
box b = dets[i].bbox;
// 此处的公式很不好理解还是接着上面的例子,现有new_w=416,new_h=234,因为resize是以w为长边压缩的
// 所以x相对于width的比例不变,而b.y表示y相对于图像高度的比例,在进行这一步的转化之前,b.y表示
// 的是预测框的y坐标相对于网络height的比值,要转化到相对于letter_box中图像的height的比值时,需要先
// 计算出y在letter_box中的相对坐标,即(b.y - (neth - new_h)/2./neth),再除以比例
b.x = (b.x - (netw - new_w)/2./netw) / ((float)new_w/netw);
b.y = (b.y - (neth - new_h)/2./neth) / ((float)new_h/neth);
b.w *= (float)netw/new_w;
b.h *= (float)neth/new_h;
if(!relative){
b.x *= w;
b.w *= w;
b.y *= h;
b.h *= h;
}
dets[i].bbox = b;
}
}
既然得到了这个坐标,就可以除以scaling_factor 缩放至真正的测试图片原图大小尺寸下的bbox实际坐标了,大功告成了!!!
!!!!!至此总结一下,我们得以知道,原来网络中通过feature map学习到的位置信息是偏移量tx,ty,tw,th,就是在Yolo检测层中,也就是最后的feture map,维度为(batch_size, num_anchors*bbox_attrs, grid_size, grid_size),对于每张图就是(num_anchors*bbox_attrs, grid_size, grid_size)对于coco的80类,bbox_attrs就是80 5,5表示网络中学习到的参数tx,ty,tw,th,以及是否有目标的score。也就是对于3层预测层,最深层是255*13*13,255是channel,物理意义表征bbox_attrs×3,3是anchor个数。为了计算loss,输出特征图需要变换为(batch_size, grid_size*grid_size*num_anchors, 5 类别数量)的tensor,这里的5就已经是通过之前详细阐述的边框预测公式转换完的结果,即bx,by,bw,bh.对于尺寸为416*416的图像,通过三个检测层检测后,有[(52*52) (26*26) (13*13)]*3=10647个预测框,也就是维度为(batchsize,10647,85).然后可以转为x1,y1,x2,y2来算iou,通过score滤去和执行nms去掉绝大多数多余的框,计算loss等操作了。
最后的小插曲:解释一下confidence是什么,
如果某个grid cell无object则Pr(Object) =0,否则Pr(Object) =1,则此时的confidence=IOU,即预测的bbox和ground truth的IOU值作为置信度。因此这个confidence不仅反映了该grid cell是否含有物体,还预测这个bbox坐标预测的有多准。在预测阶段,类别的概率为类别条件概率和confidence相乘:
Pr(Classi|Object) ∗ Pr(Object) ∗ IOU(pred,ground truth) = Pr(Classi) ∗ IOU(pred,ground truth)
这样每个bbox具体类别的score就有了,乘积既包含了bbox中预测的class的概率又反映了bbox是否包含目标和bbox坐标的准确度。