nms和P,R,map原理及在Yolov5代码中的解析

         将非极大值抑制(nms)和map放在一块进行讲解分析,因为其都是通过IOU和置信度(score)来计算,但两者方式不一样,容易产生干扰,NMS通过IOU来过滤掉候选框,而map通过IOU来筛选正负样本。

目录

nms

所有类别nms

不同类别nms

准确率,召回率

F1和map

F1:

 Ap:       

Yolov5代码中P, R和Map解析


nms

       目标检测推理过程会产生许多目标检测框,这些检测框宽高都不一致,且每个检测框都赋有一个置信度阈值,需要对这些目标框进行过滤,筛选出最优的目标框。首先,通过事先设定好的置信度阈值可以过滤掉部分检测框(即置信度小于该阈值的检测框被过滤),置信度有两种形式,一种是前景的概率(即包含有物体的概率),另一种是前景概率与类别概率的乘积。对于剩余检测框通过NMS进行过滤,最终仅保留一个与目标最匹配的检测框。

        NMS有两种思路:

所有类别nms

伪代码算法简易步骤:

all_box = all_box.sort()  ## 将所有检测出的box从大到小进行排序

for i in len(all_box):       ## 根据置信度从大到小遍历所有的box

        for  j in len(all_box) :  ## 将置信度小于某个box的其他所有box与此box对比,计算IOU

                if j > i :

                        判断all_box[i]和all_box[j]的IOU面积是否大于阈值,如果大于阈值则删除此box,否则保留此box,直到所有box被保存,即为整张图片被检测到的所有目标框。

其原理图示如下,图片源于网络 Tom Hardy博客

 对所有类别进行nms,代码以python示例:


def NMS(boxes,scores, thresholds):
    x1 = boxes[:,0]
    y1 = boxes[:,1]
    x2 = boxes[:,2]
    y2 = boxes[:,3]
    areas = (x2-x1)*(y2-y1)
 
    _,order = scores.sort(0,descending=True)
    keep = []
    while order.numel() > 0:
        i = order[0]
        keep.append(i)
        if order.numel() == 1:
            break
        xx1 = x1[order[1:]].clamp(min=x1[i])
        yy1 = y1[order[1:]].clamp(min=y1[i])
        xx2 = x2[order[1:]].clamp(max=x2[i])
        yy2 = y2[order[1:]].clamp(max=y2[i])
 
        w = (xx2-xx1).clamp(min=0)
        h = (yy2-yy1).clamp(min=0)
        inter = w*h
 
        ovr = inter/(areas[i] + areas[order[1:]] - inter)
        ids = (ovr<=thresholds).nonzero().squeeze()
        if ids.numel() == 0:
            break
        order = order[ids+1]
    return torch.LongTensor(keep)

代码以nanodet的推理c++为例(仅部分):

void nanodet::nms(std::vector<BoxInfo>& input_boxes, float NMS_THRESH)
{
    std::sort(input_boxes.begin(), input_boxes.end(), [](BoxInfo a, BoxInfo b) { return a.score > b.score; }); // 对检测的box根据置信度排序
    std::vector<float> vArea(input_boxes.size());
    for (int i = 0; i < int(input_boxes.size()); ++i)
    {
        vArea[i] = (input_boxes.at(i).x2 - input_boxes.at(i).x1 + 1)
            * (input_boxes.at(i).y2 - input_boxes.at(i).y1 + 1);
    }  // 获取所有检测出的box的面积
    for (int i = 0; i < int(input_boxes.size()); ++i)
    {
        for (int j = i + 1; j < int(input_boxes.size());)
        {
            float xx1 = (std::max)(input_boxes[i].x1, input_boxes[j].x1);
            float yy1 = (std::max)(input_boxes[i].y1, input_boxes[j].y1);
            float xx2 = (std::min)(input_boxes[i].x2, input_boxes[j].x2);
            float yy2 = (std::min)(input_boxes[i].y2, input_boxes[j].y2);
            float w = (std::max)(float(0), xx2 - xx1 + 1);
            float h = (std::max)(float(0), yy2 - yy1 + 1);
            float inter = w * h;
            float ovr = inter / (vArea[i] + vArea[j] - inter); // IOU
            if (ovr >= NMS_THRESH)  // 从vector begin开始,置信度最大的box与另一box对比,如果IOU大于阈值则删除此box,进而和下一个box对比,直到所有box都对比完
            {
                input_boxes.erase(input_boxes.begin() + j);
                vArea.erase(vArea.begin() + j);
            }
            else
            {
                j++;
            }
        }
    }

        通过手动设置IOU阈值,容易产生两个主要问题,一是:当IOU阈值设置较大时,会有很多冗余的检测框不会被有效过滤;当IOU阈值设置较小,虽可有效过滤更多的检测框。但当有两个不同类别的物体相距很近时,另一个置信度较低的物体容易被过滤,从而无法被检测到;为了弥补这种缺陷往往会采用另一种方式。

不同类别nms

         通过不同类别进行NMS,伪代码算法步骤为:

for label in all_labels: 

        a. 获取此类别(label)下所有box信息   ## 坐标位置,置信度,类别概率

        b. 根据box置信度从高至低排序,保存且记录当前置信度最大box

        c. 遍历b中置信度最大box以外的其他所有box,对比其他所有box与置信度最大box的IOU,删除IOU大于阈值的其他box

        d. 对剩下box,重复循环b,c步骤

这种方法的缺陷为,当两个相同类别的物体相隔很近时,另一个被检测到置信度较低些的物体容易被过滤掉,结果仅保留此类别下的一种物体。图示如下,其中红框的犬只可以被有效检测到,但蓝框虚线犬只会被过滤,因为其IOU大于阈值。当然图示相隔还有一定距离,如果相隔更近,IOU就更大了,更加难以去除。

对于yolov5代码直接调用函数torchvision.ops.nms:

i = torchvision.ops.nms(boxes, scores, iou_thres)

对于多类别NMS的实现,通过对每个候选框坐标添加一个偏移量来实现,偏移量可以通过不同类别的索引来实现。源码如下:

max_coordinate = boxes.max()
offsets = idxs.to(boxes) * (max_coordinate + torch.tensor(1).to(boxes))
boxes_for_nms = boxes + offsets[:, None]
keep = nms(boxes_for_nms, scores, iou_threshold)
return keep

通过torchvision.ops.boxes.batched_nms(boxes, scores, classes, nms_thresh) 调用。

在yolov5中,实现代码:

        # Batched NMS
        c = x[:, 5:6] * (0 if agnostic else max_wh)  # classes 类别
        boxes, scores = x[:, :4] + c, x[:, 4]  # boxes (offset by class), scores

        ## 采用nms将框box数量过滤,IOU设置越小,框越少; i为经过nms后剩余框的索引
        i = torchvision.ops.nms(boxes, scores, iou_thres)  # NMS 将所有的框box,依据置信度scores得分进行过滤

 通过设置agnostic来判定是否使用多类别NMS,当agnostic为True时,即对所有类别进行NMS,当其为False时,对每个类别分开单独进行NMS。max_wh为检测框的最大宽高(像素),yolov5中指定为4096。

这两种方式的缺陷:①IOU阈值设置过大,,单个目标物会出现多个检测框,IOU阈值设置过小,则相邻的同类物体会被过滤掉;②低于IOU阈值的,置信度设置为0,不够合理;③NMS只能在CPU上运行,影响性能。

现方法中除了nms外还有soft nms可以从原理上有效解决多个同类别物体相隔很近时的检测问题。此外对于IOU的演化,还有GIOU,CIOU,DIOU以及最新的SIOU,其将两个不同box之间的距离,重叠率,尺度,横纵比等多维度进行考量。这些方法的改进思路和方法很简单,这里就不再赘述。

准确率,召回率

对准确性和召回率,通过TP,FP,FN三者的关系对准确性和召回率进行计算,对TP,FP,FN的解析如下:

TP:  与真实框的IOU大于设定阈值的检测框,被视为模型正确识别的正样本;

FP:与真实框的IOU小于设定阈值的检测框,被视为模型错误识别的正样本;

FN:没有被模型识别为正样本的目标(即模型没有检测到)

准确率和召回率计算公式为:

准确率(Precision):

Precision=TP/(TP+FP)

表示模型预测的所有检测框中,预测正确的检测框(正样本数)所占的比例

召回率(Recall):

Recall=TP/(TP+FN)

表示模型预测的所有检测框中,预测正确的检测框与实际真实框的比例

计算过程为:首先模型对所有验证集图片进行检测,通过NMS后保留下所有验证集图片的目标检测框。再基于设定的置信度阈值,对大于此阈值的检测框进行统计分析。

以如下图示和表为例:红色框为GT框,蓝色框,黑色框和黄色虚线框为检测框,

① 假定置信度阈值为0.3,三个检测框都大于设定的置信度阈值。另假定IOU阈值为0.6,其中蓝色框,黑色框与真实框IOU大于设定阈值,黄色虚线框与真实框IOU小于设定阈值,则TP=2(即被模型识别正确的检测框——蓝色框和黑色框),FP=1(被模型识别错误的检测框——黄色虚线框),FN=2(漏检的,左图红框中的犬只与右图下面犬只未被检测到),故准确率:Precision=2/(2+1)=0.67,召回率:Recall=2/(2+2)=0.5.

②假定置信度阈值为0.7,则置信度小于0.7的检测框不纳入统计范畴,IOU依旧阈值为0.6,则蓝色框和黄色虚线框作为检测框,TP=1(蓝色框),FP=1(黄色虚线框),FN=3(四个GT框,仅蓝色框对应的GT框被检测到,其余三个为被检测到),则准确率:Precision=1/(1+1)=0.5,Recall=1/(1+3)=0.25.

③假定置信度阈值为0.7,IOU阈值为0.4,基于置信度阈值,ID为1,2的两个框被检测到并作为统计,则:TP=2(蓝色框和黄色虚线框),FP=0,FN=2,Precision=2/(2+0)=1,Recall=2/(2+2)=0.5.

基于置信度从高至低排序:

目标ID| 检测框    |  置信度   | IOU
----------------------------
1     | 蓝色框    |  0.96    | 0.95
----------------------------
2     | 黄色虚线框|  0.75    | 0.42
----------------------------
3     |  黑色框   |  0.62    | 0.8

 总结:

        对准确率,召回率的计算,首先基于置信度阈值,选取置信度大于阈值的目标检测框,然后基于检测框和GT框的IOU,对IOU大于阈值的为预测正确的(TP),小于IOU阈值的为预测错误的(FP),GT框中没有检测到的为漏检的(FN)。但基于准确率和召回率来衡量模型的性能效果,存在一定问题:通过手动设置置信度和IOU会存在人为因素偏差,针对不同的目标有不同的效果。故需要综合权衡准确率,召回率以及IOU的设置,一方面通过F1指标来权衡准确率和召回率,另一方面通过map来衡量。

F1和map

F1

F1的计算很简单,公式如下:

 Ap     

        Ap是衡量某一个类别检测效果的好坏。其根据不同的置信度和IOU阈值,对应有不同的准确率和召回率,进而通过计算准确率和召回率构成的二维曲线图面积即为Ap值。以官网图示为例:

在每个”峰值点”往左画一条直线,和上一个”峰值点”的垂直线相交,这样和坐标轴的面积就是AP值。 

 通过不同类别的Ap求取平均值即为mAP,其为衡量多个类别的检测效果

         基于COCO的评价指标map,其IOU选择为0.5~0.95,间隔0.05,共10个IOU阈值,置信度固定为0.1或0.01,Yolov5源码中固定置信度阈值为0.1的一个线性插值,后面对Yolov5 map代码做讲解时分析。此外,其对Recall从0~1间隔0.1,分为101份小间隔,对这101个Recall对应的Precision值采用线性插值计算,最后通过计算所有这101个Recall和Precision值构成的小矩形面积,计算出Ap值。

Yolov5代码中P, R和Map解析

        以Yolov5源代码中对准确率,召回率,F1,Map的计算做展开讲解(每一行都有相对细节的文字解析):

def ap_per_class(tp, conf, pred_cls, target_cls, plot=False, save_dir='precision-recall_curve.png', names=[]):
    # Sort by objectness
    i = np.argsort(-conf)  ## i 为基于所有验证集预测框的置信度的升序排序(因为添加了负号), 获取升序后置信度对应的索引(将模型检测的所有验证集图片的box汇总一起)

    tp, conf, pred_cls = tp[i], conf[i], pred_cls[i]  ## tp为模型预测的每个框在10个IOU阈值下是否为正确的,其与GT框的IOU大于阈值则为True,否则为False,eg.tp ([ True  True  True  True  True  True  True  True  True False]);pred_cls为对应预测的类别(eg, pred_cls: [0,1,0,0],即预测的类别分别为0,1,0,0)

    # Find unique classes 
    unique_classes = np.unique(target_cls)  # target_cls 真实的类别(eg,[0,1,0,1]);unique_classes(唯一的类别顺序,即对所有GT框对应的类别从低到高排序且去重,例如:5个GT box对于类别[0,1,1,2,0],则unique_classes为[0,1,2])

    # Create Precision-Recall curve and compute AP for each class
    px, py = np.linspace(0, 1, 1000), []  # for plotting
    pr_score = 0.1  # score to evaluate P and R 指定固定置信度阈值https://github.com/ultralytics/yolov3/issues/898
    s = [unique_classes.shape[0], tp.shape[1]]  # number class, number iou thresholds (i.e. 10 for mAP0.5...0.95)
    ap, p, r = np.zeros(s), np.zeros(s), np.zeros(s) # p.shape:[类别数,10],每一行表示每个类别,每一列代表每个IOU下的准确性(IOU:0.5~0.95)

    for ci, c in enumerate(unique_classes):   ## unique_classes 类别序号,如:0,1,2,... 0代表:猫,1代表狗,2代表鸟,...

        i = pred_cls == c  # i为基于预测的box列表中类别为c的索引处,当其IOU大于阈值为True,否则为False
        n_l = (target_cls == c).sum()  # number of labels, GT box列表中为类别c的数量加和,即类别c的GT框数量
        n_p = i.sum()  # number of predictions;预测的box列表中为类别c的数量加和,即预测类别c的预测框数量
        if n_p == 0 or n_l == 0:
            continue
        else:
            # Accumulate FPs and TPs  ;cumsum(0),实现0轴(横轴)上的元素进行累加
            fpc = (1 - tp[i]).cumsum(0)  ## tp[i]的数组形状为[box数量,10],每行为一个预测box,每列对应一个IOU下此box与GT box的布尔值(若大于IOU阈值为True,否则为False),通过1 - tp[i]获取预测box与GT box小于阈值的框,进而对所有预测box的True 或者False作累加。
            tpc = tp[i].cumsum(0)  ## 计算所有预测box与GT box的True或者False的累加值(IOU大于阈值为True) ,每一行为上一行到此行所有预测box的准确数或错误数累加和
            # Recall  r[ci] 
            recall = tpc / (n_l + 1e-16)  # recall curve
            # recall[:, 0] 为iou为0.5在所有预测框累加的召回率
            r[ci] = np.interp(-pr_score, -conf[i], recall[:, 0])  # r at pr_score, negative x, xp because xp decreases  基于各预测框置信度和召回率的对应(横轴,纵轴)关系,计算指定置信度阈值(0.1)下,采用线性插值法的召回率

            # Precision
            precision = tpc / (tpc + fpc)  # precision curve  -conf[i] [。。。]每个索引对应的置信度
            print('precision[:, 0]', precision[:, 0])
            p[ci] = np.interp(-pr_score, -conf[i], precision[:, 0])  # p at pr_score 计算方法和召回率一致

            # AP from recall-precision curve
            for j in range(tp.shape[1]):
                ap[ci, j], mpre, mrec = compute_ap(recall[:, j], precision[:, j])  ap的计算见下面分析
                if plot and (j == 0):
                    py.append(np.interp(px, mrec, mpre))  # precision at mAP@0.5
    # Compute F1 score (harmonic mean of precision and recall)
    f1 = 2 * p * r / (p + r + 1e-16)
    return p, r, ap, f1, unique_classes.astype('int32')

      对Yolov5代码整体过程简单分析:假设有5个预测box,6个gt box,置信度阈值和Yolov5一致设为0.1,IOU设置为0.5,对代码块

r[ci] = np.interp(-pr_score, -conf[i], recall[:, 0])

p[ci] = np.interp(-pr_score, -conf[i], precision[:, 0])

整体情况简析如下表:以第1,2行为例,预测box与gt box的IOU大于0.5为True,Recall=1/6=0.16, Precision=1/1=1,当rank为2,Recall=2/6=0.33, Precision=2/2=1,表格自上至下为Recall和Precision的累加形式。

Rank | box  |  -conf |GT(>0.5)| Recall | Precision
-----------------------------------------------
1    | Box1 |  -0.95|  True  | 0.16   |  1
-----------------------------------------------
2    | Box2 |  -0.90|  True  | 0.33   |  1
-----------------------------------------------
3    | Box3 |  -0.82|  False | 0.33   |  0.66
-----------------------------------------------
4    | Box4 |  -0.61|  True  | 0.50   |  0.75
-----------------------------------------------
5    | Box5 |  -0.05|  True  | 0.50   |  0.75
-----------------------------------------------

绘制-conf 和Recall,Precision曲线图,如下图所示,通过设置置信度阈值为0.1(和Yolov5源码设置的置信度一致)(则-conf为-0.1),计算其在-conf—Recall和-conf—Precision的线性插值,获取对应的准确率和召回率。

(注:上图横坐标应为-conf,保持与代码 -pr_score 一致)

 其对应的Recall-Precision曲线图如下所示,图形与横轴围成的面积即为Ap值,同理当IOU为0.5~0.95中的任意一个时,其计算方式同IOU=0.5相同。此例子中Recall(iou=0.5)=0.50,Precision(IOU=0.5)=0.75.

        Yolov5中对Map的计算代码如下,其通过对纵坐标(Recall)分为101份,采用线性插值法计算每个Recall对应的Precision值,对所有101份Recall值和Precision围成的矩形计算面积加和,即为Ap:

def compute_ap(recall, precision):
    """ Compute the average precision, given the recall and precision curves
    # Arguments
        recall:    The recall curve (list)
        precision: The precision curve (list)
    # Returns
        Average precision, precision curve, recall curve
    """

    # Append sentinel values to beginning and end
    mrec = np.concatenate(([0.], recall, [recall[-1] + 0.01]))
    mpre = np.concatenate(([1.], precision, [0.]))

    # Compute the precision envelope
    mpre = np.flip(np.maximum.accumulate(np.flip(mpre)))

    # Integrate area under curve
    method = 'interp'  # methods: 'continuous', 'interp'
    if method == 'interp':
        x = np.linspace(0, 1, 101)  # 101-point interp (COCO) 设置将Recall分为101份
        ap = np.trapz(np.interp(x, mrec, mpre), x)  # integrate  # np.trapz计算x与mpre围成的面积之和; 采用np.interp(x, mrec, mpre)线性插值将基于横坐标(recall)的101个点基于线性插值,得出纵坐标(pre)的值
    else:  # 'continuous'
        i = np.where(mrec[1:] != mrec[:-1])[0]  # points where x axis (recall) changes
        ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1])  # area under curve

    return ap, mpre, mrec

 Yolov5 test.py代码中(下述代码),p[:, 0], r[:, 0], ap[:, 0],为IOU=0.5时的准确率,召回率和ap,纵列为10列IOU从0.5~0.95对应的值,若p[:,1]为IOU=0.55时的准确率,p[:,10]为IOU=0.95时的准确率。因为有多个类别,需要对其采用.mean()求取平均。

        p, r, ap50, ap = p[:, 0], r[:, 0], ap[:, 0], ap.mean(1)  # [P, R, AP@0.5, AP@0.5:0.95]
        ## 计算了IOU从0.5到0.95时准确性和召回率分别为多少,并进行了平均值的计算
        mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()
        nt = np.bincount(stats[3].astype(np.int64), minlength=nc)  # number of targets per class

  • 4
    点赞
  • 82
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 在Yolov5,Soft NMS是通过在后处理阶段应用一种软化的非极大值抑制算法来实现的。这种算法通过在每个检测框周围的区域内计算重叠度来确定哪些框应该被保留,而不是直接删除重叠的框。这种方法可以减少重叠框的数量,从而提高检测的准确性。在Yolov5,Soft NMS是通过在detect.py文件的postprocess函数实现的。 ### 回答2: Soft NMS(soft non-maximum suppression)是一种补充了传统非最大值抑制方法的算法,用于在目标检测去除那些具有重叠率很高的重复框(bounding box)。传统的NMS算法认为,当两个框之间的重叠率超过一定的阈值时,较小的框将被舍弃。但这种方法可能会导致某些目标被忽略,因为被舍弃的框可能包含着独特的信息。 因此,Soft NMS算法采用了一种更加平滑的方式,通过减少重叠框的得分,来达到减少重复框的目的。Soft NMS算法仍会计算重叠框之间的IoU值,但是在舍弃较小框的时候,不是直接把它得分设为0,而是将得分进行调整。这种调整基于一个权重函数,权重函数使得IoU值高的框得分减少的程度更低,从而消除舍弃较小框带来的信息丢失问题。 在Yolov5的Soft NMS算法实现,当预测得分低于一定阈值时,将会被直接丢弃。在框去重过程,将计算目标框与其他所有框的IoU值,并根据计算出的IoU值进行权重调整。最后,根据预设的阈值重新进行得分排序,得到消除重复框后的框。 总之,Soft NMS是一种有效的框去重算法,可以使得目标检测的精度更高,尤其是在目标之间高度重叠的情况下效果更佳。在Yolov5,Soft NMS算法的实现进一步提升了模型的检测性能。 ### 回答3: 在 YOLOv5 ,SoftNMS 是在后处理过程使用的一种技术,主要用于减小检测框之间的重叠区域,以提高检测的准确率。 SoftNMS(Soft Non-Maximum Suppression)是在传统的 NMS(Non-Maximum Suppression)基础上进行改进的,解决了 NMS 产生的一些缺陷。NMS 通过设置阈值来筛选边界框,但它会忽略掉置信度较低的边界框,因此可能会产生漏检的情况。 SoftNMS 的实现可以分为以下几个步骤: 1. 首先,将检测结果按照置信度从高到低排序。 2. 对于置信度最高的检测框,将其它重叠度大于一定阈值的框的置信度进行一定程度的惩罚,惩罚方式是将置信度乘以一个衰减系数,使其置信度过低的框会逐渐被淘汰。 3. 剩下的框继续按照置信度从高到低排序,然后重复步骤 2 直到所有的框都被遍历完。 4. 最后,再次按照置信度从高到低排序,将经过惩罚后的框输出作为最终的检测结果。 在 YOLOv5 ,SoftNMS 的实现与其它优化技术一样,被封装在 detect.py 脚本的 postprocess 方法。首先,使用 torch.topk 对置信度从大到小进行排序,然后对每个框执行惩罚操作,重复进行这个过程,直到所有的边界框都被遍历完。最后,再次使用 torch.topk 对置信度进行排序,输出最终的结果。 总之,SoftNMS 是一种改进的非极大值抑制方法,可以在一定程度上提高检测结果的准确性。在 YOLOv5 ,SoftNMS 的实现主要是通过对置信度进行惩罚,实现对重叠较大的边界框的压缩,从而得到更加准确的检测结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值