Copyright: Jingmin Wei, Pattern Recognition and Intelligent System, School of Artificial and Intelligence, Huazhong University of Science and Technology
Pytorch教程专栏链接
本教程不商用,仅供学习和参考交流使用,如需转载,请联系本人。
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 特征分类 区域选取→特征提取→特征分类
深度学习时代的物体检测发展过程如图所示。深度神经网络大量的参数可以提
取出鲁棒性和语义性更好的特征,并且分类器性能也更优越。 2014 2014 2014 年的 RCNN(Regions with CNN features) 算是使用深度学习实现物体检测的经典之作,从此拉开了深度学习做物体检测的序幕。
参考文章:Rich feature hierarchies for accurate object detection and semantic segmentation
其主要算法分为 4 4 4 个阶段:
候选区域生成:每张图像会采用 Selective Search 方法,生成 1000 − 2000 1000-2000 1000−2000 个候选区域。
特征提取:针对每个生成的候选区域,归一化为统一尺寸,使用深度卷积网络提取候选区域的特征。
类别判断:将 CNN 特征送入每一类 SVM 分类器,判别候选区域是否属于该类。
位置精修:使用回归器惊喜修正候选框位置。
在 RCNN 基础上, 2015 2015 2015 年的 Fast RCNN 实现了端到端的检测与卷积共享。
参考文章: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 实现了多特征图的一阶检测,这两种算法对随后的物体检测也产生了深远的影响,在后续教程中将分别用一章的篇幅详细介绍。
参考文章: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 等。
在物体检测算法中,物体边框从无到有,边框变化的过程在一定程度上体现了检测是一阶的还是两阶的。
Anchor 是一个划时代的思想,最早出现在 Faster RCNN 中,其本质上是一系列大小宽高不等的先验框,均匀地分布在特征图上,利用特征去预测这些 Anchors 的类别,以及与真实物体边框存在的偏移。Anchor 相当于给物体检测提供了一个梯子,使得检测器不至于直接从无到有地预测物体,精度往往较高,常见算法有 Faster RCNN, SSD, YOLO v2 等。
当然,还有一部分无锚框的算法,思路更为多样,有直接通过特征预测边框位置的方法,如 YOLO v1 等。最近也出现了众多依靠关键点来检测物体的算法,如 CornerNet, CenterNet 等。
由于检测性能的迅速提升,物体检测也是深度学习在工业界取得大规模应用的领域之以下列举了 5 5 5 个广泛应用的领域。
对于一个检测器,我们需要制定一 定的规则来评价其好坏,从而选择需要的检测器。对于图像分类任务来讲,由于其输出是很简单的图像类别,因此很容易通过判断分类正确的图像数量来进行衡量。
物体检测模型的输出是非结构化的, 事先并无法得知输出物体的数量、位置、大小等,因此物体检测的评价算法就稍微复杂一些。 对于具体的某个物体来讲,我们可以从预测框与真实框的贴合程度来判断检测的质量,通常使用 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=SA∪SBSA∩SB
利用代码可以很方便地实现 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 4 4 种样本。
小技巧:
T/F: 模型是否检测正确
P/N:模型有没有检测到
检测正确又检测到了,目标,TP;检测错误又检测到,把背景当成物体,FP;需要检测又没检测到,漏检,FN;检测正确且本身就不需要检测,背景,TN。
有了上述基础知识,我们就可以开始进行检测模型的评测。
对于一个检测器,通常使用 mAP(mean Average Precision) 这一指标来评价一个模型的好坏,这里的 AP 指的是一个类别的检测精度,mAP 则是多个类别的平均精度。评测需要每张图片的预测值与标签值,对于某一个实例,二者包含的内容分别如下:
在预测值与标签值的基础上,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 。
下面从代码层面详细讲述 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 个变量:
下述代码可以生成两个满足上述图像信息要求的字典数据类型:
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=),
'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=)}]
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)