Pytorch:目标检测网络-概述,指标计算和使用预训练网络

Pytorch: 目标检测网络概述,指标计算和使用预训练网络

Copyright: Jingmin Wei, Pattern Recognition and Intelligent System, School of Artificial and Intelligence, Huazhong University of Science and Technology

Pytorch教程专栏链接


本教程不商用,仅供学习和参考交流使用,如需转载,请联系本人。

Reference

RCNN(Regions with CNN Features)

Fast RCNN

Faster RCNN

Mask RCNN

SSD(Single Shot MultiBox Detector)

YOLO v1(You Only Look Once)

华中科技大学 AIA 学院-计算机视觉课件

《深度学习之 Pytorch 物体检测实战》

注:物体检测的教程使用的数据集主要为 ImageNet,COCO,PASCAL VOC 这三个常用的目标检测数据集,相关数据集下载和使用方式请自行查阅资料。

import numpy as np 
import sys
from PIL import Image, ImageDraw, ImageFont
import matplotlib.pyplot as plt 
import os

import torchvision
import torch
import torchvision.transforms as transforms
物体检测技术

在计算机视觉众多的技术领域中,物体检测是一项非常基础的任务,图像分割、物体追踪、关键点检测等通常都要依赖于物体检测。此外,由于每张图像中物体的数量、大小及姿态各不相同,也就是非结构化的输出,这是与图像分类非常不同的一点, 并且物体时常会有遮挡截断,物体检测技术也极富挑战性,从诞生以来始终是研究学者最为关注的焦点领域之一。

物体检测技术,通常是指在一张图像中检测出物体出现的位置及对应的类别。 对于图中的人,我们要求检测器输出 5 5 5 个量:物体类别, x min ⁡ , y min ⁡ , x max ⁡ , x max ⁡ x_{\min}, y_{\min},x_{\max},x_{\max} xmin,ymin,xmax,xmax 当然,对于一个边框,检测器也可以输出中心点与宽高的形式,这两者是等价的。

在计算机视觉中,图像分类、物体检测与图像分割是最基础、也是目前发展最为迅速的 3 3 3 个领域。

图像分类:输入图像往往仅包含一个物体,目的是判断每张图像是什么物体,是图像级别的任务,相对简单,发展也最快。

物体检测:输入图像中往往有很多物体,目的是判断出物体出现的位置与类别,是计算机视觉中非常核心的-一个任务。

图像分割:输入与物体检测类似,但是要判断出每一个像素属于哪一个类别,属于像素级的分类。图像分割与物体检测任务之间有很多联系,模型也可以相互借鉴。

传统方式

在利用深度学习做物体检测之前传统算法对于物体的检测通常分为区域选取、特征提取与特征分类这 3 3 3 个阶段。

区 域 选 取 → 特 征 提 取 → 特 征 分 类 区域选取\rightarrow 特征提取\rightarrow 特征分类

  • 区域选取:首先选取图像中可能出现物体的位置,由于物体位置、大小都不固定,因此传统算法通常使用滑动窗口(Sliding Windows)算法,但这种算法会存在大量的冗余框,并且计算复杂度高。
  • 特征提取:在得到物体位置后,通常使用人工精心设计的提取器进行特征提取,如 SIFT 和 HOG 等。由于提取器包含的参数较少,并且人工设计的鲁棒性较低,因此特征提取的质量并不高。
  • 特征分类:最后,对上一步得到的特征进行分类,通常使用如SVM, AdaBoost的分类器。
目标检测网络

深度学习时代的物体检测发展过程如图所示。深度神经网络大量的参数可以提
取出鲁棒性和语义性更好的特征,并且分类器性能也更优越。 2014 2014 2014 年的 RCNN(Regions with CNN features) 算是使用深度学习实现物体检测的经典之作,从此拉开了深度学习做物体检测的序幕。

在这里插入图片描述

R-CNN

参考文章:Rich feature hierarchies for accurate object detection and semantic segmentation

其主要算法分为 4 4 4 个阶段:

  1. 候选区域生成:每张图像会采用 Selective Search 方法,生成 1000 − 2000 1000-2000 10002000 个候选区域。

  2. 特征提取:针对每个生成的候选区域,归一化为统一尺寸,使用深度卷积网络提取候选区域的特征。

  3. 类别判断:将 CNN 特征送入每一类 SVM 分类器,判别候选区域是否属于该类。

  4. 位置精修:使用回归器惊喜修正候选框位置。

在 RCNN 基础上, 2015 2015 2015 年的 Fast RCNN 实现了端到端的检测与卷积共享。

Fast R-CNN

参考文章:Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks

Fast R-CNN 是两阶段方法的奠基性工作,提出的 RPN 网络取代 Selecctive Search 算法使得检测任务可以由神经网络端到端地完成。

其具体操作方法是将 RPN 放在最后一个卷积层之后,RPN直接训练得到候选区域。RPN 网络的特点在于通过滑动窗口的方式实现候选框的提取,在特征映射上滑动窗口,每个滑动窗口位置生成 9 9 9 个不同尺度、不同宽高的候选窗口,提取对应 9 9 9 个候选窗口的特征,用于目标分类和边框回归。

目标分类只需要区分候选框内特征为前景或者北京,与 Fast R-CNN 类似,边框回归确定更精确的目标位置。

之后,Faster RCNN 提出锚框(Anchor)这一划时代的思想, 将物体检测推向了第一个高峰。在 2016 2016 2016 年,YOLO v1 实现了无锚框(Anchor-Free)的一阶检测,SSD 实现了多特征图的一阶检测,这两种算法对随后的物体检测也产生了深远的影响,在后续教程中将分别用一章的篇幅详细介绍。

YOLO

参考文章:You Only Look Once: Unified, Real-Time Object Detection

YOLO(You Only Look Once) 是经典的单目标检测算法,将目标区域预测和目标类别预测整合于单个神经网络模型中,实现在准确率较高的情况下快速检测与识别目标。YOLO的主要优点是检测速度快、全局处理使得背景错误相对较少、泛化性能好。但是YOLO由于其设计思想的局限,所以会在小目标检测时有些困难。

算法流程如下:

首先将图像划分为 S × S S\times S S×S 个网格,然后在每个网格上通过深度卷积网络给出其物体所述的类别判断(图像使用不同的颜色表示),并在网格基础上生成 B 个边框(box),每个边框预测 5 5 5 个回归值,其中前 4 4 4 个值表示边框位置,第五个值表征这个边框含有物体的概率和位置的准确程度。最后经过 NMS 非极大值抑制过滤得到最后的预测框。

2017 2017 2017 年,FPN 利用特征金字塔实现了更优秀的特征提取网络,Mask RCNN 则在实现了实例分割的同时,也提升了物体检测的性能。进入 2018 2018 2018 年后,物体检测的算法更为多样,如使用角点做检测的 CornerNet ,使用多个感受野分支的 TidentNet ,使用中心点做检测的 CenterNet 等。

在物体检测算法中,物体边框从无到有,边框变化的过程在一定程度上体现了检测是一阶的还是两阶的。

  • 两阶:两阶的算法通常在第一阶段专注于找出物体出现的位置,得到建议框,保证足够的准召率(Recall),然后在第二个阶段专注于对建议框进行分类,导找更精确的位置)典型算法如 RCNN, Faster RCNN 。两阶的算法通常精度准更高,但速度较慢。当然,还存在例如 Cascade RCNN 这样更多阶的算法。
  • 一阶:一阶的算法将二阶算法的两个阶段合二为一,在一个阶段里完成寻找物体出现位置与类别的预测,方法通常更为简单,依赖于特征融合、(Focal Loss 等优秀的网络经验,速度一般比两阶网络更快,但精度会有所损失,典型算法如 SSD, YOLO, RetinaNet 等。

Anchor 是一个划时代的思想,最早出现在 Faster RCNN 中,其本质上是一系列大小宽高不等的先验框,均匀地分布在特征图上,利用特征去预测这些 Anchors 的类别,以及与真实物体边框存在的偏移。Anchor 相当于给物体检测提供了一个梯子,使得检测器不至于直接从无到有地预测物体,精度往往较高,常见算法有 Faster RCNN, SSD, YOLO v2 等。

当然,还有一部分无锚框的算法,思路更为多样,有直接通过特征预测边框位置的方法,如 YOLO v1 等。最近也出现了众多依靠关键点来检测物体的算法,如 CornerNet, CenterNet 等。

技术应用领域

由于检测性能的迅速提升,物体检测也是深度学习在工业界取得大规模应用的领域之以下列举了 5 5 5 个广泛应用的领域。

  • 安防:受深度学习的影响,安防领域近年来取得了快速的发展与落地。例如广为人知的人脸识别技术,在交通卡口、车站等已有了成熟的应用。此外,在智慧城市的安防中,行人与车辆的检测也是尤为重要的一环。 在安防领域中,有很大的趋势是将检测技术融入到摄像头中,形成智能摄像头,以海康威视、地平线等多家公司最为知名。
  • 自动驾驶:自动驾驶的感知任务中,行人、车辆等障碍物的检测尤为重要。由于涉及驾驶的安全性,自动驾驶对于检测器的性能要求极高,尤其是召回率这个指标,自动驾驶也堪称人工智能应用的"珠穆朗玛峰"。此外,由于车辆需要获取障碍物相对于其自身的三维位置,因此通常在检测器后还需要增加很多的后处理感知模块。
  • 机器人:工业机器人自动分拣中,系统需要识别出要分拣的各种部件,这是极为典型的机器人应用领域。此外,移动智能机器人需要时刻检测出环境中的各种障碍物,以实现安全的避障与导航。从广泛意义来看,自动驾驶车辆也可以看做是机器人的一种形式。
  • 搜索推荐:在互联网公司的各大应用平台中,物体检测无处不在。例如,对于包含特定物体的图像过滤、筛选、推荐和水印处理等,在人脸、行人检测的基础上增加更加丰富的应用,如抖音等产品。
  • 医疗诊断:基于人工智能与大数据,医疗诊断也迎来了新的春天,利用物体检测技术,我们可以更准确、迅速地对 CT, MR 等医疗图像中特定的关节和病症进行诊断。
评价指标

对于一个检测器,我们需要制定一 定的规则来评价其好坏,从而选择需要的检测器。对于图像分类任务来讲,由于其输出是很简单的图像类别,因此很容易通过判断分类正确的图像数量来进行衡量。

Intersection of Union(IoU)

物体检测模型的输出是非结构化的, 事先并无法得知输出物体的数量、位置、大小等,因此物体检测的评价算法就稍微复杂一些。 对于具体的某个物体来讲,我们可以从预测框与真实框的贴合程度来判断检测的质量,通常使用 IoU(Intersection of Union) 来量化贴合程度。

IoU 的计算方式如图所示,使用两个边框的的交集集与并集的比值,就可以得到 IoU, 公式如下所示。显而易见,loU 的取值区间是 [ 0 , 1 ] [0,1] [0,1] , IoU 值越大,表明两个框重合越好。

在这里插入图片描述

I o U A , B = S A ∩ S B S A ∪ S B IoU_{A,B}=\frac{S_A\cap S_B}{S_A\cup S_B} IoUA,B=SASBSASB

利用代码可以很方便地实现 IoU 的计算:

def IoU(boxA, boxB):
    # 计算重合部分的上下左右4个边的值
    left_max = max(boxA[0], boxB[0]) # x_left中更大的x坐标
    top_max = max(boxA[1], boxB[1]) # y_top中更大的y坐标
    right_min = min(boxA[2], boxB[2]) # x_right中更小的x坐标
    bottom_min = min(boxA[3], boxB[3]) # y_bottom中更小的y坐标
    # 计算重合的面积
    inter = max(0, right_min-left_max) * max(0, bottom_min-top_max)
    # 计算两个框的面积
    SA = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
    SB = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
    # 计算所有区域的面积
    union = SA + SB - inter
    iou = inter / union
    return iou

对于 IoU 而言,我们通常会选取一个闽值,如 0.5 0.5 0.5 ,来确定预测框是正确的还是错误的。当两个框的 IoU 大于 0.5 0.5 0.5 时,我们认为是一个有效的检测,否则属于无效的匹配。

4 种样本(TP, FP, FN, TN)

在这里插入图片描述

如图中有两个杯子的标签,模型产生了两个预测框。

由于图像中存在背景与物体两种标签,预测框也分为正确与错误,因此在评测时会产生以下 4 4 4 种样本。

  • 正确检测框 TP(True Positive):预测框正确地与标签框匹配了,两者间的 IoU 大于 0.5 0.5 0.5 ,如图中右下方的检测框。
  • 误检框 FP(False Positive):将背景预测成了物体,如图中左下方的检测框,通常这种框与图中所有标签的 IoU 都不会超过 0.5 0.5 0.5
  • 漏检框 FN(False Negative):本来需要模型检测出的物体,模型没有检测出,如图中左上方的杯子。
  • 正确背景 TN(True Negative):本身是背景,模型也没有检测出来,这种情况在物体检测中通常不需要考虑。

在这里插入图片描述

小技巧:

T/F: 模型是否检测正确

P/N:模型有没有检测到

检测正确又检测到了,目标,TP;检测错误又检测到,把背景当成物体,FP;需要检测又没检测到,漏检,FN;检测正确且本身就不需要检测,背景,TN

有了上述基础知识,我们就可以开始进行检测模型的评测。

Recall, Precision, mean Average Precision(mAP)

对于一个检测器,通常使用 mAP(mean Average Precision) 这一指标来评价一个模型的好坏,这里的 AP 指的是一个类别的检测精度,mAP 则是多个类别的平均精度。评测需要每张图片的预测值与标签值,对于某一个实例,二者包含的内容分别如下:

  • 预测值(Dets) :物体类别、边框位置的 4 4 4 个预测值、该物体的得分。
  • 标签值(GTs) :物体类别、边框位置的 4 4 4 个真值(ground truth)。

在预测值与标签值的基础上,AP 的具体计算过程如图所示。我们首先将所有的预测框按照得分从高到低进行排序( 因为得分越高的边框其对于真实物体的概率往往越大),然后从高到低遍历预测框。

在这里插入图片描述

对于遍历中的某一个预测框, 计算其与该图中同一类别的所有标签框 GTs 的 IoU,并选取拥有最大 IoU 的 GT 作为当前预测框的匹配对象。如果该 loU 小于阈值,则将当前的预测框标记为误检框 FP 。

如果该 IoU 大于阈值,还要看对应的标签框 GP 是否被访问过。如果前面已经有限分更高的预测框与该标签框对应了,即使现在的 IoU 大于阙值,也会被标记为 FP 。如果没有被访问过,则将当前预测框 Det 标记为正确检测框 TP ,并将该 GT 标记为访问过,以防止后面还有预测框与其对应。

在遍历完所有的预测框后,我们会得到每一个预测框的属性,即 TP 或 FP 。在遍历的过程中,我们可以通过当前TP的数量来计算模型的召回率(Recall, R),即当前一共检测出的标签框与所有标签框的比值,如下式所示,(正确检测 / 正确检测 + 漏检)

R = T P l e n ( G T s ) = T P T P + F N R=\frac{TP}{len(GTs)}=\frac{TP}{TP+FN} R=len(GTs)TP=TP+FNTP

除了召回率,还有一个重要指标是准确率(Precision, P),即当前遍历过的预测框中,属于正确预测边框的比值,如下式所示,(正确检测 / 正确检测 + 误检)

P = T P T P + F P P=\frac{TP}{TP+FP} P=TP+FPTP

在这里插入图片描述

遍历到每一个预测框时, 都可以生成一个对应的 P 与 R ,这两个值可以组成一个点 ( R , P ) (R,P) (R,P) ,将所有的点绘制成曲线,即形成了 P-R 曲线,如图所示。

在这里插入图片描述

然而,即使有了 P-R 曲线,评价模型仍然不直观,如果直接取曲线上的点,在哪里选取都不合适,因为召回率高的时候准确率会很低,准确率高的时候往往召回率很低。这时,AP 就派上用场了,计算公式如式所示。

A P = ∫ 0 1 P d R AP=\int_0^1P\mathrm{d}R AP=01PdR

从公式中可以看出,AP 代表了曲线的面积,综合考量了不同召回率下的准确率,不会对 P 与 R 有任何偏好。每个类别的 AP 是相互独立的,将每个类别的 AP 进行平均,即可得到 mAP 。严格意义上讲,还需要对曲线进行定的修正, 再进行 AP 计算。除了求面积的方式,还可以使用 11 11 11 个不同召回率对应的准确率求平均的方式求 AP 。

代码实现 mAP

下面从代码层面详细讲述 AP 求解过程。

文件夹 data/detections 只存放了 1 1 1 张图片的检测信息(真实情况有 n 张图)。图片名为 1.jpg 对应检测信息为 1.txt。

Class,Left, Top, Right, Bottom, Score

文件内容:

class1 12 58 53 96 0.87
class1 51 88 152 191 0.98
class2 345 898 431 945 0.67
class2 597 346 674 415 0.45
class1 243 546 298 583 0.83
class2 99 345 150 426 0.96

文件夹 data/groundtruths 存放其真值信息 1.txt 。

Class, Left, Top, Right, Bottom

文件内容:

class1 14 56 50 100
class1 50 90 150 189
class2 345 894 432 940
class1 458 657 580 742
class2 590 354 675 420

假设经过标签数据与预测数据的加载,需要得到了下面 3 3 3 个变量:

  • det_boxes:包含全部图像中所有类别的预测框,其中一个边框包含了 [Ieft, top, right, bottom, score, NameofImage] 。
  • gt_boxes:包含了全部图像中所有类别的标签,其中一个标签的内容为 [left, top, right, bottom, 0]。最后一位 0 0 0 代表该标签有没有被匹配过,如果匹配过则会置为 1 1 1 ,其他预测框再去匹配则为误检框。
  • num_pos:包含了全部图像中所有类别的预测个数。

下述代码可以生成两个满足上述图像信息要求的字典数据类型:

def getDetBoxes(DetFolder='./data/detections'):

    files = os.listdir(DetFolder)
    files.sort()

    det_boxes = {}
    for f in files:
        nameOfImage = f.replace(".txt", "")
        fh1 = open(os.path.join(DetFolder, f), "r")

        for line in fh1:
            line = line.replace("\n", "")
            if line.replace(' ', '') == '':
                continue
            splitLine = line.split(" ")

            # 类别
            cls = (splitLine[0])
            # 坐标
            left = float(splitLine[1])
            top = float(splitLine[2])
            right = float(splitLine[3])
            bottom = float(splitLine[4])
            # 置信度
            score = float(splitLine[5])
            # nameOfImage为图片名,这里只有一张图,名字为1
            one_box = [left, top, right, bottom, score, nameOfImage]

            if cls not in det_boxes:
                det_boxes[cls]=[]
            det_boxes[cls].append(one_box)

        fh1.close()
    return det_boxes
def getGTBoxes(GTFolder='./data/groundtruths'):
    files = os.listdir(GTFolder)
    files.sort()

    classes = []
    num_pos = {}
    gt_boxes = {}
    for f in files:
        nameOfImage = f.replace(".txt", "")
        fh1 = open(os.path.join(GTFolder, f), "r")
        
        for line in fh1:
            line = line.replace("\n", "")
            if line.replace(' ', '') == '':
                continue
            splitLine = line.split(" ")

            # 类别
            cls = (splitLine[0])
            left = float(splitLine[1])
            # 坐标
            top = float(splitLine[2])
            right = float(splitLine[3])
            bottom = float(splitLine[4])      
            # 0表示未被访问过
            one_box = [left, top, right, bottom, 0]
            
            # 类别名列表
            if cls not in classes:
                classes.append(cls)
                gt_boxes[cls] = {}
                num_pos[cls] = 0

            num_pos[cls] += 1

            if nameOfImage not in gt_boxes[cls]:
                gt_boxes[cls][nameOfImage] = []
            gt_boxes[cls][nameOfImage].append(one_box)  
            
        fh1.close()
    return gt_boxes, classes, num_pos
gt_boxes, classes_name, num_pos = getGTBoxes('./data/groundtruths')
det_boxes = getDetBoxes('./data/detections')
# ground truth
gt_boxes
{'class1': {'1': [[14.0, 56.0, 50.0, 100.0, 0],
   [50.0, 90.0, 150.0, 189.0, 0],
   [458.0, 657.0, 580.0, 742.0, 0]]},
 'class2': {'1': [[345.0, 894.0, 432.0, 940.0, 0],
   [590.0, 354.0, 675.0, 420.0, 0]]}}
# detection boxing
det_boxes
{'class1': [[12.0, 58.0, 53.0, 96.0, 0.87, '1'],
  [51.0, 88.0, 152.0, 191.0, 0.98, '1'],
  [243.0, 546.0, 298.0, 583.0, 0.83, '1']],
 'class2': [[345.0, 898.0, 431.0, 945.0, 0.67, '1'],
  [597.0, 346.0, 674.0, 415.0, 0.45, '1'],
  [99.0, 345.0, 150.0, 426.0, 0.96, '1']]}
classes_name
['class1', 'class2']
num_pos
{'class1': 3, 'class2': 2}
cfg = {'iouThreshold': 0.5} # 配置文件

在这里插入图片描述

按照上述算法调用 IoU 函数,并循环标记 TP 和 FP:

# AP计算函数
def AP_caculate(cfg, classes_name, det_boxes, gt_boxes, num_pos):
    # 配置参数,所有类别的名字,全部预测框,全部标签框,全部预测框的长度
    ret = []
    for class_name in classes_name:
        # 通过类别作为关键字,得到每个类别的预测、标签及总标签数
        dets = det_boxes[class_name]
        gt_class = gt_boxes[class_name]
        npos = num_pos[class_name]
        # 利用得分,即dets的第4个元素作为关键字,对预测框按得分高低排序
        dets = sorted(dets, key=lambda conf: conf[4], reverse=True)
        # 设置两个与预测边框长度相同的列表,标记为TP or FP
        TP = np.zeros(len(dets))
        FP = np.zeros(len(dets))
        # 对某一个类别的所有预测框进行遍历
        for d in range(len(dets)):
            # 将IoU默认置为最低
            IoUMax = sys.float_info.min
            # 遍历与预测框同一图像中的同一类别的标签,计算IoU
            if dets[d][-1] in gt_class:
                for j in range(len(gt_class[dets[d][-1]])):
                    iou = IoU(dets[d][: 4], gt_class[dets[d][-1]][j][:4])
                    if iou > IoUMax:
                        IoUMax = iou
                        jmax = j # 记录与预测有最大IoU的标签
                # 如果最大IoU大于阈值,且没有被匹配过,则赋TP
                if IoUMax >= cfg['iouThreshold']:
                    if gt_class[dets[d][-1]][jmax][4] == 0:
                        TP[d] = 1
                        gt_class[dets[d][-1]][jmax][4] = 1 # 标记为匹配过
                    # 如果被匹配过,则赋FP
                    else:
                        FP[d] = 1
                # 如果最大IoU没超过阈值,则赋FP
                else:
                    FP[d] = 1
            # 如果对应的图像中没有该类别的标签,则赋FP
            else:
                FP[d] = 1
        # 计算累积的FP和TP
        acc_FP = np.cumsum(FP)
        acc_TP = np.cumsum(TP)
        # 得到每个点的Recall,即 TP / len(GTs)
        rec = acc_TP / npos
        # 得到每个点的Precision,即 TP / TP + FP
        prec = np.divide(acc_TP, (acc_FP + acc_TP))
        # 通过Recall和Precision计算AP
        [ap, m_pre, m_rec, ii] = CalculateAveragePrecision(rec, prec)
        r = {
                'class': class_name,
                'precision': prec,
                'recall': rec,
                'AP': ap,
                'interpolated precision': m_pre,
                'interpolated recall': m_rec,
                'total positives': npos,
                'total TP': np.sum(TP),
                'total FP': np.sum(FP),
            }
        ret.append(r)
    return ret, classes_name        

得到每个点的 Precision 和 Recall 后,对每个离散点进行插值计算,最后采用离散积分的方式计算 AP:

# 得到每个点的P和R后,采用离散积分的方式计算AP
def CalculateAveragePrecision(rec, prec):
        m_rec = []
        m_rec.append(0)
        [m_rec.append(e) for e in rec] # 列表生成式,添加召回率
        m_rec.append(1)

        m_pre = []
        m_pre.append(0)
        [m_pre.append(e) for e in prec] # 列表生成式,添加精度
        m_pre.append(0)

        for i in range(len(m_pre) - 1, 0, -1):
            # 插值,两点间取更大的precision
            m_pre[i - 1] = max(m_pre[i - 1], m_pre[i])

        ii = []
        for i in range(len(m_rec) - 1):
            if m_rec[i + 1] != m_rec[i]:
                # 插值,只取两点间recall不等的
                ii.append(i + 1)

        ap = 0
        for i in ii:
            # 离散积分
            ap = ap + np.sum((m_rec[i] - m_rec[i - 1]) * m_pre[i])

        return [ap, m_pre[0:len(m_pre) - 1], m_rec[0:len(m_pre) - 1], ii]
ret, class_name = AP_caculate(cfg, classes_name, det_boxes, gt_boxes, num_pos)
ret
[{'class': 'class1',
  'precision': array([1.        , 1.        , 0.66666667]),
  'recall': array([0.33333333, 0.66666667, 0.66666667]),
  'AP': 0.6666666666666666,
  'interpolated precision': [1.0, 1.0, 1.0, 0.6666666666666666],
  'interpolated recall': [0,
   0.3333333333333333,
   0.6666666666666666,
   0.6666666666666666],
  'total positives': 3,
  'total TP': 2.0,
  'total FP': 1.0},
 {'class': 'class2',
  'precision': array([0.        , 0.5       , 0.66666667]),
  'recall': array([0. , 0.5, 1. ]),
  'AP': 0.6666666666666666,
  'interpolated precision': [0.6666666666666666,
   0.6666666666666666,
   0.6666666666666666,
   0.6666666666666666],
  'interpolated recall': [0, 0.0, 0.5, 1.0],
  'total positives': 2,
  'total TP': 2.0,
  'total FP': 1.0}]
# class1的AP
ret[0]['AP']
0.6666666666666666
# class2的插值后每个点的Recall
ret[1]['interpolated recall']
[0, 0.0, 0.5, 1.0]
使用预训练的目标检测网络

R-CNN系列的预训练的目标检测网络有:

detection.fasterrcnn_resnet50_fpn

detection.maskrcnn_resnet50_fpn

detection.keypointrcnn_resnet50_fpn

# 模型加载选择GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)
print(torch.cuda.device_count())
print(torch.cuda.get_device_name(0))
cuda
1
GeForce MX250
图像目标检测

使用预训练好的具有 ResNet-50-FPN 结构的 Fast R-CNN 模型,使用 COCO 数据集进行训练

(COCO 数据集下载地址:https://cocodataset.org)

# 导入预训练好的ResNet50 Faster R-CNN模型
model = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained = True)
model = model.to(device)
model.eval()
FasterRCNN(
  (transform): GeneralizedRCNNTransform(
      Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
      Resize(min_size=(800,), max_size=1333, mode='bilinear')
  )
  (backbone): BackboneWithFPN(
    (body): IntermediateLayerGetter(
      (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
      (bn1): FrozenBatchNorm2d(64, eps=0.0)
      (relu): ReLU(inplace=True)
      (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
      (layer1): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
            (1): FrozenBatchNorm2d(256, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(64, eps=0.0)
          (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(64, eps=0.0)
          (conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(256, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer2): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(512, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (3): Bottleneck(
          (conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(128, eps=0.0)
          (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(128, eps=0.0)
          (conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(512, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer3): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(1024, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (3): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (4): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (5): Bottleneck(
          (conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(256, eps=0.0)
          (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(256, eps=0.0)
          (conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(1024, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
      (layer4): Sequential(
        (0): Bottleneck(
          (conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
          (downsample): Sequential(
            (0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)
            (1): FrozenBatchNorm2d(2048, eps=0.0)
          )
        )
        (1): Bottleneck(
          (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
        )
        (2): Bottleneck(
          (conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn1): FrozenBatchNorm2d(512, eps=0.0)
          (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
          (bn2): FrozenBatchNorm2d(512, eps=0.0)
          (conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
          (bn3): FrozenBatchNorm2d(2048, eps=0.0)
          (relu): ReLU(inplace=True)
        )
      )
    )
    (fpn): FeaturePyramidNetwork(
      (inner_blocks): ModuleList(
        (0): Conv2d(256, 256, kernel_size=(1, 1), stride=(1, 1))
        (1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1))
        (2): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1))
        (3): Conv2d(2048, 256, kernel_size=(1, 1), stride=(1, 1))
      )
      (layer_blocks): ModuleList(
        (0): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
        (3): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      )
      (extra_blocks): LastLevelMaxPool()
    )
  )
  (rpn): RegionProposalNetwork(
    (anchor_generator): AnchorGenerator()
    (head): RPNHead(
      (conv): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
      (cls_logits): Conv2d(256, 3, kernel_size=(1, 1), stride=(1, 1))
      (bbox_pred): Conv2d(256, 12, kernel_size=(1, 1), stride=(1, 1))
    )
  )
  (roi_heads): RoIHeads(
    (box_roi_pool): MultiScaleRoIAlign(featmap_names=['0', '1', '2', '3'], output_size=(7, 7), sampling_ratio=2)
    (box_head): TwoMLPHead(
      (fc6): Linear(in_features=12544, out_features=1024, bias=True)
      (fc7): Linear(in_features=1024, out_features=1024, bias=True)
    )
    (box_predictor): FastRCNNPredictor(
      (cls_score): Linear(in_features=1024, out_features=91, bias=True)
      (bbox_pred): Linear(in_features=1024, out_features=364, bias=True)
    )
  )
)
# 准备需要检测的图像
image = Image.open('./data/objdetect/2012_004308.jpg')
transform_d = transforms.Compose([transforms.ToTensor()])
image_t = transform_d(image).to(device) # 图像变换
pred = model([image_t]) # 输出预测
pred
[{'boxes': tensor([[139.8201,  35.2344, 306.0309, 211.2748],
          [ 78.5456, 117.7256, 294.9999, 274.1726],
          [176.4146,  45.9989, 293.7729, 167.6908],
          [446.5353, 298.2009, 482.5389, 332.6683],
          [144.3929,  59.9620, 242.3081, 232.6723],
          [264.5503, 289.4034, 348.2632, 330.4233],
          [ 81.9035,  99.5320, 306.7264, 279.0831],
          [304.1234,  68.3819, 500.0000, 314.6510],
          [246.3921,  79.3525, 495.8307, 323.0642],
          [264.6102, 288.0742, 348.0310, 330.5592]], device='cuda:0',
         grad_fn=<StackBackward>),
  'labels': tensor([ 1,  2,  1,  1,  1, 15,  4,  5,  2,  8], device='cuda:0'),
  'scores': tensor([0.9954, 0.9430, 0.8601, 0.8108, 0.4989, 0.3326, 0.3135, 0.1794, 0.1665,
          0.1197], device='cuda:0', grad_fn=<IndexBackward>)}]

boxes 为边界框。

labels 为目标所属的类别。

scores 为属于相应类别的得分(即置信度 objectness)。

检测内容可视化

定义每个类别对应的标签:

COCO_INSTANCE_CATEGORY_NAMES = [
    '__background__', 'person', 'bicycle', 'car', 'motorcycle',
    'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light',
    'fire hydrant', 'N/A', 'stop sign', 'parking meter', 'bench',
    'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 
    'bear', 'zebra', 'giraffe', 'N/A', 'backpack', 'umbrella', 'N/A',
    'N/A', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard',
    'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard',
    'surfboard', 'tennis racket', 'bottle', 'N/A', 'wine glass',
    'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple',
    'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza',
    'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'N/A',
    'dining table', 'N/A', 'N/A', 'toilet', 'N/A', 'tv', 'laptop',
    'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven',
    'toaster', 'sink', 'refrigerator', 'N/A', 'book', 'clock',
    'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'
]

可视化前,需要分别将有效的预测目标数据解读出来,提取的信息有每个目标的位置、类别和得分,然后将得分大于 0 , 5 0,5 0,5 的目标作为检测到的有效目标,并将检测到的目标在图像上显示。

# 检测出的目标类别和得分
pred_class = [COCO_INSTANCE_CATEGORY_NAMES[ii] for ii in list(pred[0]['labels'].cpu().numpy())]
pred_score = list(pred[0]['scores'].detach().cpu().numpy())
# 检测出目标的边界框
pred_boxes = [[ii[0], ii[1], ii[2], ii[3]] for ii in list(pred[0]['boxes'].detach().cpu().numpy())]
# 只保留识别概率大于0.5的
pred_index = [pred_score.index(x) for x in pred_score if x > 0.5]
# 设置图像显示的字体
fontsize = np.int16(image.size[1] / 30)
font1 = ImageFont.truetype('C:/windows/Fonts/STXIHEI.TTF', fontsize) # 华文细黑
# 可视化图像
draw = ImageDraw.Draw(image)
for index in pred_index:
    box = pred_boxes[index]
    draw.rectangle(box, outline = 'red')
    texts = pred_class[index] + ':' + str(np.round(pred_score[index], 2))
    draw.text((box[0], box[1]), texts, fill = 'red', font = font1)
image


在这里插入图片描述

下面将上述目标检测过程定义为一个函数,方便对任意图像进行检测:

def Object_Detect(model, image_path, COCO_INSTANCE_CATEGORY_NAMES, threshold = 0.5):
    image = Image.open(image_path)
    transform_d = transforms.Compose([transforms.ToTensor()])
    image_t = transform_d(image).to(device) # 图像变换
    pred = model([image_t]) # 输出预测
    # 检测出目标的类别和得分
    pred_class = [COCO_INSTANCE_CATEGORY_NAMES[ii] for ii in list(pred[0]['labels'].cpu().numpy())]
    pred_score = list(pred[0]['scores'].detach().cpu().numpy())
    # 检测出目标的边界框
    pred_boxes = [[ii[0], ii[1], ii[2], ii[3]] for ii in list(pred[0]['boxes'].detach().cpu().numpy())]
    # 只保留识别概率大于threshold的结果
    pred_index = [pred_score.index(x) for x in pred_score if x > threshold]
    # 设置图像显示的字体
    fontsize = np.int16(image.size[1] / 30)
    font1 = ImageFont.truetype('C:/windows/Fonts/STXIHEI.TTF', fontsize) # 华文细黑
    # 可视化图像和检测结果
    draw = ImageDraw.Draw(image)
    for index in pred_index:
        box = pred_boxes[index]
        draw.rectangle(box, outline = 'red')
        texts = pred_class[index] + ':' + str(np.round(pred_score[index], 2))
        draw.text((box[0], box[1]), texts, fill = 'red', font = font1)
    return image
# 调用上面的函数
image_path = './data/objdetect/2012_003924.jpg'
Object_Detect(model, image_path, COCO_INSTANCE_CATEGORY_NAMES, 0.7)


在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值