前言
该文链接至:YOLO-V3-SPP详细解析
本文主要讲解在推理阶段YoloV3-SPP的处理,分以下几点:
- 预处理数据
- 推理的NMS处理
关于map的计算,ultralytic版本的源码调用了pycoco库函数来计算map,因此这里不讲map的计算,感兴趣可以去看看我朋友关于对YOLOV5之MAP计算的博客:【YOLOV5-5.x 源码解读】val.py
源码
Yolo-V3-SPP版本是ultralytics版本,需要较详细的请去github下载
NMS源码
validation.py调用
pred = model(imgs)[0] # only get inference resultpred = non_max_suppression(pred, conf_thres=0.01, iou_thres=0.6, multi_label=False)
predict_test.py调用
# 网络进行正向传播,t为时间差,pred为返回结果t1 = torch_utils.time_synchronized()pred = model(img)[0] # only get inference resultt2 = torch_utils.time_synchronized()print(t2 - t1)# 非极大值抑制处理pred = utils.non_max_suppression(pred, conf_thres=0.1, iou_thres=0.6, multi_label=True)[0]
这里pred为model的返回值,返回值的处理参照model.py的处理,这里给出关键部分的代码:
else: # inference 如果是验证或者推理阶段# io的shpae(batch_size,anchor_num,grid_cell,grid_cell,xywh+obj_confidence+classes_num)io = p.clone() # inference output# clone返回一个张量的副本,其与原张量的尺寸和数据类型相同。# 与copy_()不同,这个函数记录在计算图中。传递到克隆张量的梯度将传播到原始张量# grid的shape=[batch_size, na, grid_h, grid_w, wh],和io最后一维取前两个xy后的shape一致,进行加法io[..., :2] = torch.sigmoid(io[..., :2]) + self.grid# xy 计算在feature map上的xy坐标,对应论文的sigmoid(tx)+cx# anchor_wh的shape:[batch_size, na, grid_h, grid_w, wh]与io最后一维取第3,4个,即wh后的shape一致,进行乘法io[..., 2:4] = torch.exp(io[..., 2:4]) * self.anchor_wh # wh yolo method 计算在feature map上的whio[..., :4] *= self.stride # xywh换算映射回原图尺度# obj和类别预测经过sigmoidtorch.sigmoid_(io[..., 4:])return io.view(bs, -1, self.no), p # view [1, 3, 13, 13, 85] as [1, 507, 85],3X13X13=507# io的shape(batch_size,...,xywh+obj_confidence+classes_num)# p的shape是(batch_size,anchor_num,grid_cell,grid_cell,xywh+obj_confidence+classes_num)
NMS源码
def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.6,multi_label=True, classes=None, agnostic=False, max_num=100):"""Performs Non-Maximum Suppression on inference resultsparam: prediction[batch, num_anchors X (gird_x X gird_y), (xywh+obj_conf+cls_num)]Returns detections with shape:nx6 (x1, y1, x2, y2, conf, cls)"""# Settingsmerge = False # merge for best mAPmin_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and heighttime_limit = 10.0 # seconds to quit aftert = time.time()nc = prediction[0].shape[1] - 5 # number of classesmulti_label &= nc > 1 # multiple labels per boxoutput = [None] * prediction.shape[0]for xi, x in enumerate(prediction): # image index, image inference 遍历每张图片# Apply constraintsx = x[x[:, 4] > conf_thres] # confidence 根据obj confidence虑除背景目标x = x[((x[:, 2:4] > min_wh) & (x[:, 2:4] < max_wh)).all(1)] # width-height 虑除小目标# If none remain process next imageif not x.shape[0]:continue# Compute confx[..., 5:] *= x[..., 4:5] # conf = obj_conf * cls_conf# Box (center x, center y, width, height) to (x1, y1, x2, y2)box = xywh2xyxy(x[:, :4])# Detections matrix nx6 (xyxy, conf, cls)if multi_label: # 针对每个类别执行非极大值抑制i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).t()x = torch.cat((box[i], x[i, j + 5].unsqueeze(1), j.float().unsqueeze(1)), 1)else: # best class only 直接针对每个类别中概率最大的类别进行非极大值抑制处理conf, j = x[:, 5:].max(1)x = torch.cat((box, conf.unsqueeze(1), j.float().unsqueeze(1)), 1)[conf > conf_thres]# Filter by classif classes:x = x[(j.view(-1, 1) == torch.tensor(classes, device=j.device)).any(1)]# Apply finite constraint# if not torch.isfinite(x).all():# x = x[torch.isfinite(x).all(1)]# If none remain process next imagen = x.shape[0] # number of boxesif not n:continue# Sort by confidence# x = x[x[:, 4].argsort(descending=True)]# Batched NMSc = x[:, 5] * 0 if agnostic else x[:, 5] # classesboxes, scores = x[:, :4].clone() + c.view(-1, 1) * max_wh, x[:, 4] # boxes (offset by class), scoresi = torchvision.ops.nms(boxes, scores, iou_thres)i = i[:max_num] # 最多只保留前max_num个目标信息if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)try: # update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)iou = box_iou(boxes[i], boxes) > iou_thres # iou matrixweights = iou * scores[None] # box weightsx[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes# i = i[iou.sum(1) > 1] # require redundancyexcept: # possible CUDA error https://github.com/ultralytics/yolov3/issues/1139print(x, i, x.shape, i.shape)passoutput[xi] = x[i]if (time.time() - t) > time_limit:break # time limit exceededreturn output
NMS源码解析
回顾NMS
Soft-NMS算法
这里的iou(M,bi)iou(M,b_i)iou(M,bi?)采用的是GiouGiouGiou
解析
def non_max_suppression(prediction, conf_thres=0.1, iou_thres=0.6,multi_label=True, classes=None, agnostic=False, max_num=100):
传参:
- prediciton:shape为(batch_size,anchor×grid_x×grid_y,xywh+obj_conf+cls_num)(batch\_size,anchor\times grid\_x\times grid\_y,xywh+obj\_conf+cls\_num)(batch_size,anchor×grid_x×grid_y,xywh+obj_conf+cls_num)
- conf_thres:置信度和类别阈值
- Iou_thres:iou阈值
- multi_label:多类执行NMS标志位,True表示对每个类别执行NMS,False表示只对类别最大的执行NMS(注:这里不是指多分类和单分类,而仅仅是NMS的处理方式)
- classes:默认为None,作用是控制筛选输出的类别为指定的classes,作为一个特殊的功能扩展,对预测输出时可指定输出指定的类,默认不使用
- agnostic:
- max_num:NMS后最多只保留前max_num个目标信息
# Settingsmerge = False # merge for best mAPmin_wh, max_wh = 2, 4096 # (pixels) minimum and maximum box width and heighttime_limit = 10.0 # seconds to quit after
Merge后面是用来对经过NMS之后的预测框进行一个权重平衡,这里简单提一下,后面会讲到.
min_wh和max_wh作用:
- 筛选掉大小预测框
- 在nms时,max_wh会对不同类别的预测框进行区分,具体操作后面会详细说明
time_limit限制循环的运行时间不能超过10s
t = time.time()nc = prediction[0].shape[1] - 5 # number of classesmulti_label &= nc > 1 # multiple labels per box
t = time.time():返回当前时间的时间戳(1970纪元后经过的浮点秒数)。
nc = prediction[0].shape[1] - 5
这里prediction的shape为[batch, num_anchors X (gird_x X gird_y), (xywh+obj_conf+cls_num)]
上述prediction[0].shape[1]即取prediciton最后一维的长度,由于前5个均是(xywh+obj),减去前面的维度即可以得到剩下cls_num的长度,nc表示类数目。
multi_label &= nc > 1
这是与运算&两个式子分别为:multi_label和nc>1,两个布尔值取与&。
output = [None] * prediction.shape[0]
prediction.shape[0]指batch_size个数,output为list,list个数为当前传入的predict的batch_size,如果在预测阶段传入一张图片,那么batch_size=1,那么这时的output为1个list,总之output的list个数为batch_size。
for循环代码的解释
for xi, x in enumerate(prediction): # image index, image inference 遍历每张图片
对于单张图片的nms处理,该循环只会执行一次,而对于验证集处理是一个batch进行nms处理,循环次数为batch_size。
prediciton的shape[batch, num_anchors X (gird_x X gird_y), (xywh+obj_conf+cls_num)]
x的shape为[num_anchors X (gird_x X gird_y), (xywh+obj_conf+cls_num)]
# Apply constraintsx = x[x[:, 4] > conf_thres] # confidence 根据obj confidence虑除背景目标x = x[((x[:, 2:4] > min_wh) & (x[:, 2:4] < max_wh)).all(1)] # width-height 滤除小目标
x = x[x[:, 4] > conf_thres]
筛选出Obj_conf > conf_thres的预测框
x = x[((x[:, 2:4] > min_wh) & (x[:, 2:4] < max_wh)).all(1)]
筛选出预测框宽高在[min_wh,max_wh]之间的预测框信息
# If none remain process next imageif not x.shape[0]:continue
x.shape[0]是当前图片筛选出来的预测框数量,如果当前图片经过conf_thres和滤除小目标后得到的预测框为0,那么这张图片不需要nms,continue下一张图片进行nms处理
# Compute confx[..., 5:] *= x[..., 4:5] # conf = obj_conf * cls_conf
x[…, 5:]表示cls_conf维度,x[…, 4:5]表示obj_conf
这里回顾下YOLO-V1的论文
每个grid cell预测cls_num个类的条件概率Pr(Classi∣Object)Pr(Class_i\mid Object)Pr(Classi?∣Object),我们要得到实际类别概率需要
Pr(Classi)=Pr(Classi∣Object)?Pr(Object)Pr(Class_i)=Pr(Class_i\mid Object)\ast Pr(Object)Pr(Classi?)=Pr(Classi?∣Object)?Pr(Object)
实际等价于
Pr(Classi)=cls_conf?obj_confPr(Class_i)=cls\_conf\ast obj\_confPr(Classi?)=cls_conf?obj_conf
经过上述代码之后,x原处cls_conf的位置的内容从Pr(Classi∣Object)Pr(Class_i\mid Object)Pr(Classi?∣Object)变为Pr(Classi)Pr(Class_i)Pr(Classi?)
# Box (center x, center y, width, height) to (x1, y1, x2, y2)box = xywh2xyxy(x[:, :4])
xywh2xyxy方法如下
def xywh2xyxy(x):# Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-righty = torch.zeros_like(x) if isinstance(x, torch.Tensor) else np.zeros_like(x)y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left xy[:, 1] = x[:, 1] - x[:, 3] / 2 # top left yy[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right xy[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right yreturn y
将yolo格式的xywh转化为xyxy,赋给box,但不改变x对应为位置的内容
多类NMS前的处理和单类NMS前的处理
多类NMS
# Detections matrix nx6 (xyxy, conf, cls)if multi_label: # 针对每个类别执行非极大值抑制i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).t()x = torch.cat((box[i], x[i, j + 5].unsqueeze(1), j.float().unsqueeze(1)), 1)else: # best class only 直接针对每个类别中概率最大的类别进行非极大值抑制处理conf, j = x[:, 5:].max(1)x = torch.cat((box, conf.unsqueeze(1), j.float().unsqueeze(1)), 1)[conf > conf_thres]
multi_label :多类NMS为true,单类NMS为false。
i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).t()
x[:, 5:] > conf_thres表示将所有预测框的cls_conf和conf_thres进行匹配,如果大于这个阈值,则将该cls_conf设为true,否则设为false,debug详情如下:
这里Tensor:(1779,2)表示我当前预测图片的预测框有1779个,所需分类为2个
以上是x[:, 5:] > conf_thres的状态
nonzero(as_tuple=False).t()将上述变量的非零值,即True值的矩阵位置内容(预测框id,类别)保存下来,赋给i,j变量(该(i,j)坐标可在x[:, 5:]变量寻址获得大于conf_thres的类别置信度cls_conf,形象地说,寻址的tensor为(预测框id个数,类别),其中i表示预测框id,j表示类别)
注意:i表示的预测框id个数中可能预测框来自同一个id,但是类别不同,表示该预测框的两个类别置信度都>conf_thres
x = torch.cat((box[i], x[i, j + 5].unsqueeze(1), j.float().unsqueeze(1)), 1)
box[i]表示预测框i,状态如下:
x[i, j + 5].unsqueeze(1)表示对x在第2个维度进行扩展一个维度,原来的shape是[预测框数量,xywh+obj+cls_num]
x[i, j + 5]的状态如下:
经过unsqueeze(1)后,状态如下:
j.float().unsqueeze(1)将类别转化为浮点类型,并且在位置的维度升维,状态如下:
将上述三个变量在第2个维度上拼接,得到的x的状态如下:
其中选取x[0]=[3.02696e+02, 8.21888e+01, 3.32809e+02, 1.57338e+02, 1.08854e-01, 0.00000e+00]
有6个数值,前四个参数表示(xleft?top,yleft?top,xright?top,yright?top)(x_{left-top},y_{left-top},x_{right-top},y_{right-top})(xleft?top?,yleft?top?,xright?top?,yright?top?),后两个参数表示(cls_conf,j.float())(cls\_conf,j.float())(cls_conf,j.float())
单类NMS
else: # best class only 直接针对每个类别中概率最大的类别进行非极大值抑制处理conf, j = x[:, 5:].max(1)x = torch.cat((box, conf.unsqueeze(1), j.float().unsqueeze(1)), 1)[conf > conf_thres]
max(1)表示取x[:, 5:]的类别维度的最大值,返回其中最大的cls_conf及所表示的类别j
torch.cat同多类NMS处理相同
多类NMS和单类NMS的区别
上面两个部分都是在NMS前进行筛选的操作,为了形象说明,我做的实验是分两类,那么我将定义筛选前变量为(box_id,cls_conf1,cls_conf2)(box\_id,cls\_conf1,cls\_conf2)(box_id,cls_conf1,cls_conf2)
多类NMS:只要该预测框预测的预测类别cls_conf1cls\_conf1cls_conf1和cls_conf2cls\_conf2cls_conf2置信度大于conf_thres,那么该预测类别将会保存下来,最后筛选得到(box_id,cls_conf1)(box\_id,cls\_conf1)(box_id,cls_conf1)和(box_id,cls_conf2)(box\_id,cls\_conf2)(box_id,cls_conf2)
这里注意,对于筛选到的预测框中,含有不同的cls_conf1cls\_conf1cls_conf1和cls_conf2cls\_conf2cls_conf2的预测框可能是同一个box_idbox\_idbox_id,它们都可能会送进NMS处理.由于这个预测框是同一个预测框,只是预测了cls_conf1cls\_conf1cls_conf1和cls_conf2cls\_conf2cls_conf2,在进行NMS时,表示同一个预测框,预测不同类的两个target的IOU是完全重叠的。从这同一个框的两个target来分析哪个框会得到保留,对于NMS原理可知,假设是soft-NMS原理,那么至少会有一个框得到保留,最极端的情况就是当这同一预测框的两个target具有相同的cls_confcls\_confcls_conf,那么这两框可能都会被保留。
单类NMS:预测框只筛选出其中一个类别的target,筛选条件为max(cls_conf1,cls_conf2)max(cls\_conf1,cls\_conf2)max(cls_conf1,cls_conf2),注意单类NMS不需要经过conf_thres筛选,只取最大类别置信度的预测框,那么所有的预测框都会被筛选上,并且一个预测框对应一个target。
classes参数的作用
# Filter by classif classes:x = x[(j.view(-1, 1) == torch.tensor(classes, device=j.device)).any(1)]
我的实验classes设为None,意味着上述代码并没有执行,其中的意思:
classes参数说明讲得很清楚了
j的状态是:
j.view(-1,1)的状态是:
classes是一个list或者nparrary,定义为指定的类别list,控制NMS指对指定的类别进行NMS,抛弃其他类别,输出指定的类别预测或验证,估计预测时使用较多,作为一个功能扩展,默认不使用.
# If none remain process next imagen = x.shape[0] # number of boxesif not n:continue
经过上述代码的类阈值筛选之后,判断x是否还能继续NMS
agnostic参数的作用
# Batched NMSc = x[:, 5] * 0 if agnostic else x[:, 5] # classes
agnostic默认使用false,c的参数为tensor(预测框id,)
x的shape为(xleft?top,yleft?top,xright?top,yright?top,cls_conf,j.float())(x_{left-top},y_{left-top},x_{right-top},y_{right-top},cls\_conf,j.float())(xleft?top?,yleft?top?,xright?top?,yright?top?,cls_conf,j.float())
x[:, 5]表示第6个参数的信息j.float(),即表示类别
c的状态为:
agnostic为True会使获得的类别变量c全为第一类,若为false则c获取的是所有预测框id对应的类
该变量的具体作用未知,由于未用到,还不清楚具体的作用。
boxes, scores = x[:, :4].clone() + c.view(-1, 1) * max_wh, x[:, 4]
x[:, :4]表示(xleft?top,yleft?top,xright?top,yright?top)(x_{left-top},y_{left-top},x_{right-top},y_{right-top})(xleft?top?,yleft?top?,xright?top?,yright?top?)信息
c.view(-1, 1)的状态为:
max_wh为4096
对于agnostic为fasle时,max_wh能将c中非0类的坐标信息(xleft?top,yleft?top,xright?top,yright?top)(x_{left-top},y_{left-top},x_{right-top},y_{right-top})(xleft?top?,yleft?top?,xright?top?,yright?top?)乘以max_wh。
在进行nms时,会区分boxes坐标
当agnostic为True时,c的值将会全为0,boxes信息将不会区分不同类的坐标信息,对所有类进行NMS操作.
以上agnostic参数的具体实现在于对boxes不同类nms的处理
boxes不同类区分坐标信息,分类进行NMS操作
boxes = x[:, :4].clone() + c.view(-1, 1) * max_wh,这里将不同类别的坐标以max_wh倍数进行区分,具体作用请看如下debug,我的0类的boxes信息debug如下:
经过区分,不同类的boxes信息的iou是0,那么nms只会对同类预测框操作。
scores为x的第5个参数,即cls_conf,类置信度
i = torchvision.ops.nms(boxes, scores, iou_thres)
i = i[:max_num] # 最多只保留前max_num个目标信息
调用torchvision自带的nms库函数,采用giou来进行NMS
merge参数的作用
if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean)try: # update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)iou = box_iou(boxes[i], boxes) > iou_thres # iou matrixweights = iou * scores[None] # box weightsx[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes# i = i[iou.sum(1) > 1] # require redundancyexcept: # possible CUDA error https://github.com/ultralytics/yolov3/issues/1139print(x, i, x.shape, i.shape)pass
merge参数默认为false:
如果为true,将会给筛选的boundingbox以一定权重分配宽高,权重分配以当前筛选的box信息和总的box信息进行iou计算,将iou>Iou_thres的box保存下来,将当前最佳框的宽高以如下公式更新:
boxi=0[x1,y1,x2,y2]=∑i=1targetclsi[x1_i,y1_i,x2_i,y2_i]∑i=1targetclsibox_{i=0}[x_1,y_1,x_2,y_2]=\frac{\sum_{i=1}^{target}cls_i[x_{1\_i},y_{1\_i},x_{2\_i},y_{2\_i}]}{\sum_{i=1}^{target}cls_i}boxi=0?[x1?,y1?,x2?,y2?]=∑i=1target?clsi?∑i=1target?clsi?[x1_i?,y1_i?,x2_i?,y2_i?]?
其中,i=0i=0i=0表示cls_confcls\_confcls_conf最高的一个box,i=1i=1i=1到targettargettarget个boxes均是与boxmax_confbox_{max\_conf}boxmax_conf?的iou>iou_thres筛选得到的box
这里的代码如果看不懂,可以看这个版本的代码:
原文:nms源码解读
elif method == 'merge': # weighted mixture boxwhile len(dc): # dc是按置信度排好序的box信息if len(dc) == 1:det_max.append(dc)breaki = bbox_iou(dc[0], dc) > nms_thres # i = True/False的集合weights = dc[i, 4:5] # 根据i,保留所有Truedc[0, :4] = (weights * dc[i, :4]).sum(0) / weights.sum() # 重叠框位置信息求解平均值det_max.append(dc[:1])dc = dc[i == 0]
关于NMS中的具体操作是比较简单的,主要复杂的是NMS前的一些处理.以上代码的NMS是调包使用,具体源码实现我不太清楚,这里给出几个版本的NMS实现,hard-nms,hard-nms-and,soft-nms,diou-nms,原文:nms源码解读
# 推理时间:0.0030selif method == 'soft_nms': # soft-NMS https://arxiv.org/abs/1704.04503sigma = 0.5 # soft-nms sigma parameterwhile len(dc):# if len(dc) == 1: 这是U版的源码 我做了个小改动# det_max.append(dc)# break# det_max.append(dc[:1])det_max.append(dc[:1]) # append dc的第一行 即targetif len(dc) == 1:breakiou = bbox_iou(dc[0], dc[1:]) # 计算target与其他框的iou# 这里和上面的直接置0不同,置0不需要管维度dc = dc[1:] # dc=target往后的所有预测框# dc必须不包括target及其前的预测框,因为还要和值相乘, 维度上必须相同dc[:, 4] *= torch.exp(-iou ** 2 / sigma) # 得分衰减dc = dc[dc[:, 4] > conf_thres]# 推理时间:0.00299elif method == 'diou_nms': # DIoU NMS https://arxiv.org/pdf/1911.08287.pdfwhile dc.shape[0]: # dc.shape[0]: 当前class的预测框数量det_max.append(dc[:1]) # 让score最大的一个预测框(排序后的第一个)为targetif len(dc) == 1: # 出口 dc中只剩下一个框时,breakbreak# dc[0] :target dc[1:] :其他预测框diou = bbox_iou(dc[0], dc[1:], DIoU=True) # 计算 dioudc = dc[1:][diou < nms_thres] # remove dious > threshold 保留True 删去False