目标检测 Object Detection
文章目录
前言
在YOLOv1(2016)提出之前,R-CNN系列算法在目标检测领域独占鳌头。R-CNN系列检测精度高,但是由于其网络结构是双阶段
(two-stage)的特点,使得它的检测速度不能满足实时性
双阶段
(Two-stage)目标检测方法通常包括两个主要步骤:
- 区域提议(Region Proposal):在第一阶段,模型会生成一系列可能包含目标的区域。这些区域被称为“候选框”或“区域提议”。常用的算法有Selective Search等。
- 分类和回归(Classification and Regression):在第二阶段,模型会对生成的区域提议进行分类,判断其中是否有目标,并对目标的边界框进行精确调整(回归)。这一步通常会使用深度学习模型进行处理,比如卷积神经网络(CNN)。
YOLOv1思想
YOLO 的核心思想就是把目标检测转变成一个回归问题,利用整张图作为网络的输入,仅仅经过一个神经网络,得到bounding box(边界框) 的位置及其所属的类别
检测策略
YOLOv1采用的是“分而治之”的策略,将一张图片平均分成7×7个网格,每个网格分别负责预测中心点落在该网格内的目标。
实现过程
- 将一幅图像分成 S×S个网格(grid cell),如果某个 object 的中心落在这个网格中,则这个网格就负责预测这个object。
- 每个网格要预测 B 个bounding box,每个 bounding box 要预测 (x, y, w, h) 和 confidence 共5个值。
- 每个网格还要预测一个类别信息,记为 C 个类。
- 总的来说,S×S 个网格,每个网格要预测 B个bounding box ,还要预测 C 个类。网络输出就是一个 S × S × (5×B+C) 的张量。
YOLOv1把一张图片划分为了7×7个网格,并且每个网格预测2个Box(Box1和Box2),20个类别。所以实际上,S=7,B=2,C=20。那么网络输出的shape也就是:7×7×30。
- (x, y):bounding box的中心坐标,通常相对于网格的左上角进行归一化。
- (w, h):bounding box的宽度和高度,同样是相对于整个图像进行归一化。
- confidence:表示该bounding box内包含物体的置信度,通常计算为检测到物体的概率乘以预测的IoU(Intersection over Union)。
YOLOv1的损失函数公式
在YOLOv1中,具体的三种损失的公式如下:
-
定位损失(Localization Loss):
- 对于每个bounding box,定位损失使用均方误差(MSE)来计算:
Localization Loss = ∑ i = 0 B [ 1 obj i ( ( x i − x ^ i ) 2 + ( y i − y ^ i ) 2 + ( w i − w ^ i ) 2 + ( h i − h ^ i ) 2 ) ] \text{Localization Loss} = \sum_{i=0}^{B} \left[ \text{1}_{\text{obj}}^i \left( (x_i - \hat{x}_i)^2 + (y_i - \hat{y}_i)^2 + (w_i - \hat{w}_i)^2 + (h_i - \hat{h}_i)^2 \right) \right] Localization Loss=i=0∑B[1obji((xi−x^i)2+(yi−y^i)2+(wi−w^i)2+(hi−h^i)2)]
其中, 1 obj i \text{1}_{\text{obj}}^i 1obji 是指示变量,当第 i i i 个bounding box包含目标时为1,否则为0。
- 对于每个bounding box,定位损失使用均方误差(MSE)来计算:
-
置信度损失(Confidence Loss):
- 置信度损失包括两部分:
Confidence Loss = ∑ i = 0 B [ 1 obj i ( C i − C ^ i ) 2 + λ noobj ⋅ 1 noobj i ( C i − C ^ i ) 2 ] \text{Confidence Loss} = \sum_{i=0}^{B} \left[ \text{1}_{\text{obj}}^i (C_i - \hat{C}_i)^2 + \lambda_{\text{noobj}} \cdot \text{1}_{\text{noobj}}^i (C_i - \hat{C}_i)^2 \right] Confidence Loss=i=0∑B[1obji(Ci−C^i)2+λnoobj⋅1noobji(Ci−C^i)2]
其中, C i C_i Ci 是真实的置信度, C ^ i \hat{C}_i C^i 是预测的置信度, 1 noobj i \text{1}_{\text{noobj}}^i 1noobji 是指示变量,当第 i i i 个bounding box不包含目标时为1。
- 置信度损失包括两部分:
-
分类损失(Classification Loss):
- 分类损失使用交叉熵来衡量:
Classification Loss = ∑ j = 0 C ∑ i = 0 B 1 obj i ( − y j log ( y ^ j ) ) \text{Classification Loss} = \sum_{j=0}^{C} \sum_{i=0}^{B} \text{1}_{\text{obj}}^i \left( -y_j \log(\hat{y}_j) \right) Classification Loss=j=0∑Ci=0∑B1obji(−yjlog(y^j))
其中, y j y_j yj 是真实类别的one-hot编码, y ^ j \hat{y}_j y^j 是预测的类别概率。
- 分类损失使用交叉熵来衡量:
YOLOv1的优缺点
优点:
- YOLO检测速度非常快。标准版本的YOLO可以每秒处理 45 张图像;YOLO的极速版本每秒可以处理150帧图像。这就意味着 YOLO 可以以小于 25 毫秒延迟,实时地处理视频。对于欠实时系统,在准确率保证的情况下,YOLO速度快于其他方法。
- YOLO 实时检测的平均精度是其他实时监测系统的两倍。
- 迁移能力强,能运用到其他的新的领域(比如艺术品目标检测)。
局限:
- YOLO对相互靠近的物体,以及很小的群体检测效果不好,这是因为一个网格只预测了2个框,并且都只属于同一类。
- 由于损失函数的问题,定位误差是影响检测效果的主要原因,尤其是大小物体的处理上,还有待加强。(因为对于小的bounding boxes,small error影响更大)
-
- YOLO对不常见的角度的目标泛化性能偏弱。
核心代码
结构
import torch
import torch.nn as nn
from prepare_data import GL_CLASSES, GL_NUMBBOX, GL_NUMGRID#类数量,BBox数量,GRID数量
# YOLOv1 模型
class YOLOv1(nn.Module):
def __init__(self):
super(YOLOv1, self).__init__()
self.conv_layers = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64),
nn.LeakyReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.BatchNorm2d(192),
nn.LeakyReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(192, 128, kernel_size=1),
nn.BatchNorm2d(128),
nn.LeakyReLU(),
nn.Conv2d(128, 256, kernel_size=3, padding=1),
nn.BatchNorm2d(256),
nn.LeakyReLU(),
nn.Conv2d(256, 256, kernel_size=1),
nn.BatchNorm2d(256),
nn.LeakyReLU(),
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.LeakyReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(512, 256, kernel_size=1),
nn.BatchNorm2d(256),
nn.LeakyReLU(),
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.LeakyReLU(),
nn.Conv2d(512, 256, kernel_size=1),
nn.BatchNorm2d(256),
nn.LeakyReLU(),
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.LeakyReLU(),
nn.Conv2d(512, 256, kernel_size=1),
nn.BatchNorm2d(256),
nn.LeakyReLU(),
nn.Conv2d(256, 512, kernel_size=3, padding=1),
nn.BatchNorm2d(512),
nn.LeakyReLU(),
nn.Conv2d(512, 1024, kernel_size=3, padding=1),
nn.BatchNorm2d(1024),
nn.LeakyReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(1024, 512, kernel_size=1),
nn.BatchNorm2d(512),
nn.LeakyReLU(),
nn.Conv2d(512, 1024, kernel_size=3, padding=1),
nn.BatchNorm2d(1024),
nn.LeakyReLU(),
nn.Conv2d(1024, 512, kernel_size=1),
nn.BatchNorm2d(512),
nn.LeakyReLU(),
nn.Conv2d(512, 1024, kernel_size=3, padding=1),
nn.BatchNorm2d(1024),
nn.LeakyReLU(),
nn.Conv2d(1024, 1024, kernel_size=3, padding=1),
nn.BatchNorm2d(1024),
nn.LeakyReLU(),
nn.Conv2d(1024, 1024, kernel_size=3, padding=1),
nn.BatchNorm2d(1024),
nn.LeakyReLU(),
)
self.fc_layers = nn.Sequential(
nn.Linear(1024 * 7 * 7, 4096),
nn.LeakyReLU(),
nn.Linear(4096, GL_NUMGRID * GL_NUMGRID * (5*GL_NUMBBOX+len(GL_CLASSES))), # 7x7x30 (2 bbox + 1 confidence + 20 classes)
nn.Sigmoid() # 增加sigmoid函数是为了将输出全部映射到(0,1)之间,因为如果出现负数或太大的数,后续计算loss会很麻烦
)
def forward(self, x):
x = self.conv_layers(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
self.pred = x.reshape(-1, (5 * GL_NUMBBOX + len(GL_CLASSES)), GL_NUMGRID, GL_NUMGRID) # 记住最后要reshape一下输出数据
return self.pred
损失函数
def calculate_loss(self, labels):
"""
根据labels和self.outputs计算训练loss
labels: (bs, n), 对应训练数据的样本标签
return: loss数值
"""
self.pred = self.pred.double()
labels = labels.double()
num_gridx, num_gridy = GL_NUMGRID, GL_NUMGRID # 划分网格数量
noobj_confi_loss = 0. # 不含目标的网格损失(只有置信度损失)
coor_loss = 0. # 含有目标的bbox的坐标损失
obj_confi_loss = 0. # 含有目标的bbox的置信度损失
class_loss = 0. # 含有目标的网格的类别损失
n_batch = labels.size()[0] # batchsize的大小
# 可以考虑用矩阵运算进行优化,提高速度,为了准确起见,这里还是用循环
for i in range(n_batch): # batchsize循环
for n in range(num_gridx): # x方向网格循环
for m in range(num_gridy): # y方向网格循环
if labels[i, 4, m, n] == 1: # 如果包含物体
# 将数据(px,py,w,h)转换为(x1,y1,x2,y2)
# 先将px,py转换为cx,cy,即相对网格的位置转换为标准化后实际的bbox中心位置cx,xy
# 然后再利用(cx-w/2,cy-h/2,cx+w/2,cy+h/2)转换为xyxy形式,用于计算iou
bbox1_pred_xyxy = ((self.pred[i, 0, m, n] + n) / num_gridx - self.pred[i, 2, m, n] / 2,
(self.pred[i, 1, m, n] + m) / num_gridy - self.pred[i, 3, m, n] / 2,
(self.pred[i, 0, m, n] + n) / num_gridx + self.pred[i, 2, m, n] / 2,
(self.pred[i, 1, m, n] + m) / num_gridy + self.pred[i, 3, m, n] / 2)
bbox2_pred_xyxy = ((self.pred[i, 5, m, n] + n) / num_gridx - self.pred[i, 7, m, n] / 2,
(self.pred[i, 6, m, n] + m) / num_gridy - self.pred[i, 8, m, n] / 2,
(self.pred[i, 5, m, n] + n) / num_gridx + self.pred[i, 7, m, n] / 2,
(self.pred[i, 6, m, n] + m) / num_gridy + self.pred[i, 8, m, n] / 2)
bbox_gt_xyxy = ((labels[i, 0, m, n] + n) / num_gridx - labels[i, 2, m, n] / 2,
(labels[i, 1, m, n] + m) / num_gridy - labels[i, 3, m, n] / 2,
(labels[i, 0, m, n] + n) / num_gridx + labels[i, 2, m, n] / 2,
(labels[i, 1, m, n] + m) / num_gridy + labels[i, 3, m, n] / 2)
iou1 = calculate_iou(bbox1_pred_xyxy, bbox_gt_xyxy)
iou2 = calculate_iou(bbox2_pred_xyxy, bbox_gt_xyxy)
# 选择iou大的bbox作为负责物体
if iou1 >= iou2:
coor_loss = coor_loss + 5 * (torch.sum((self.pred[i, 0:2, m, n] - labels[i, 0:2, m, n]) ** 2) \
+ torch.sum((self.pred[i, 2:4, m, n].sqrt() - labels[i, 2:4, m, n].sqrt()) ** 2))
obj_confi_loss = obj_confi_loss + (self.pred[i, 4, m, n] - iou1) ** 2
# iou比较小的bbox不负责预测物体,因此confidence loss算在noobj中,注意,对于标签的置信度应该是iou2
noobj_confi_loss = noobj_confi_loss + 0.5 * ((self.pred[i, 9, m, n] - iou2) ** 2)
else:
coor_loss = coor_loss + 5 * (torch.sum((self.pred[i, 5:7, m, n] - labels[i, 5:7, m, n]) ** 2) \
+ torch.sum((self.pred[i, 7:9, m, n].sqrt() - labels[i, 7:9, m, n].sqrt()) ** 2))
obj_confi_loss = obj_confi_loss + (self.pred[i, 9, m, n] - iou2) ** 2
# iou比较小的bbox不负责预测物体,因此confidence loss算在noobj中,注意,对于标签的置信度应该是iou1
noobj_confi_loss = noobj_confi_loss + 0.5 * ((self.pred[i, 4, m, n] - iou1) ** 2)
class_loss = class_loss + torch.sum((self.pred[i, 10:, m, n] - labels[i, 10:, m, n]) ** 2)
else: # 如果不包含物体
noobj_confi_loss = noobj_confi_loss + 0.5 * torch.sum(self.pred[i, [4, 9], m, n] ** 2)
loss = coor_loss + obj_confi_loss + noobj_confi_loss + class_loss
# 此处可以写代码验证一下loss的大致计算是否正确,这个要验证起来比较麻烦,比较简洁的办法是,将输入的pred置为全1矩阵,再进行误差检查,会直观很多。
return loss / n_batch
计算iou
def calculate_iou(bbox1, bbox2):
"""计算bbox1=(x1,y1,x2,y2)和bbox2=(x3,y3,x4,y4)两个bbox的iou"""
if bbox1[2]<=bbox1[0] or bbox1[3]<=bbox1[1] or bbox2[2]<=bbox2[0] or bbox2[3]<=bbox2[1]:
return 0 # 如果bbox1或bbox2没有面积,或者输入错误,直接返回0
intersect_bbox = [0., 0., 0., 0.] # bbox1和bbox2的重合区域的(x1,y1,x2,y2)
intersect_bbox[0] = max(bbox1[0],bbox2[0])
intersect_bbox[1] = max(bbox1[1],bbox2[1])
intersect_bbox[2] = min(bbox1[2],bbox2[2])
intersect_bbox[3] = min(bbox1[3],bbox2[3])
w = max(intersect_bbox[2] - intersect_bbox[0], 0)
h = max(intersect_bbox[3] - intersect_bbox[1], 0)
area1 = (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]) # bbox1面积
area2 = (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1]) # bbox2面积
area_intersect = w * h # 交集面积
iou = area_intersect / (area1 + area2 - area_intersect + 1e-6) # 防止除0
# print(bbox1,bbox2)
# print(intersect_bbox)
# input()
return iou
计算NMS
NMS(Non-Maximum Suppression)是一种后处理技术,用于目标检测任务中,以减少重复的边界框。它的基本步骤如下:
- 置信度排序:根据每个边界框的置信度得分,对所有检测到的框进行排序。
- 选择最高分框:选择得分最高的边界框,将其添加到最终结果中。
- 计算IoU:计算该框与其他所有框的 IoU。
- 抑制低于阈值的框:如果 IoU 大于设定的阈值,则将这些重复的框移除。
- 重复以上步骤:对剩下的框重复上述步骤,直到所有框都被处理。
def labels2bbox(matrix):
"""
将网络输出的7*7*(5*2bbox+20个类=30)的数据转换为bbox的(98,25)的格式,然后再将NMS处理后的结果返回
:param matrix: 注意,输入的数据中,bbox坐标的格式是(px,py,w,h),需要转换为(x1,y1,x2,y2)的格式再输入NMS
:return: 返回NMS处理后的结果,bboxes.shape = (-1, 6), 0:4是(x1,y1,x2,y2), 4是conf, 5是cls
"""
if matrix.size()[0:2]!=(7,7):
raise ValueError("Error: Wrong labels size: ", matrix.size(), " != (7,7)")
matrix = matrix.numpy()
bboxes = np.zeros((98, 6))
# 先把7*7*30的数据转变为bbox的(98,25)的格式,其中,bbox信息格式从(px,py,w,h)转换为(x1,y1,x2,y2),方便计算iou
matrix = matrix.reshape(49,-1)
bbox = matrix[:, :10].reshape(98, 5)#前十个记录(px,py,w,h,conf),之后为cls
r_grid = np.array(list(range(7)))
r_grid = np.repeat(r_grid, repeats=14, axis=0) # [0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 ...]
c_grid = np.array(list(range(7)))
c_grid = np.repeat(c_grid, repeats=2, axis=0)[np.newaxis, :]
c_grid = np.repeat(c_grid, repeats=7, axis=0).reshape(-1) # [0 0 1 1 2 2 3 3 4 4 5 5 6 6 0 0 1 1 2 2 3 3 4 4 5 5 6 6...]
bboxes[:, 0] = np.maximum((bbox[:, 0] + c_grid) / 7.0 - bbox[:, 2] / 2.0, 0)
bboxes[:, 1] = np.maximum((bbox[:, 1] + r_grid) / 7.0 - bbox[:, 3] / 2.0, 0)
bboxes[:, 2] = np.minimum((bbox[:, 0] + c_grid) / 7.0 + bbox[:, 2] / 2.0, 1)
bboxes[:, 3] = np.minimum((bbox[:, 1] + r_grid) / 7.0 + bbox[:, 3] / 2.0, 1)
bboxes[:, 4] = bbox[:, 4]
cls = np.argmax(matrix[:, 10:], axis=1)
cls = np.repeat(cls, repeats=2, axis=0)
bboxes[:, 5] = cls
# 对所有98个bbox执行NMS算法,清理cls-specific confidence score较低以及iou重合度过高的bbox
keepid = nms_multi_cls(bboxes, thresh=0.1, n_cls=20)
ids = []
for x in keepid:
ids = ids + list(x)
ids = sorted(ids)
return bboxes[ids, :]
def nms_multi_cls(dets, thresh, n_cls):
"""
多类别的NMS算法
:dets:ndarray,nx6,dets[i,0:4]是bbox坐标;dets[i,4]是置信度score;dets[i,5]是类别序号;
:thresh: NMS算法的阈值;
:n_cls: 类别总数
"""
# 储存结果的列表,keeps_index[i]表示第i类保留下来的bbox下标list
keeps_index = []
for i in range(n_cls):
order_i = np.where(dets[:,5]==i)[0]
det = dets[dets[:, 5] == i, 0:5]
if det.shape[0] == 0:
keeps_index.append([])
continue
keep = nms_1cls(det, thresh)
keeps_index.append(order_i[keep])
return keeps_index
def nms_1cls(dets, thresh):
"""
单类别NMS
dets: ndarray,nx5,dets[i,0:4]分别是bbox坐标;dets[i,4]是置信度score
thresh: NMS算法设置的iou阈值
"""
# 从检测结果dets中获得x1,y1,x2,y2和scores的值
x1 = dets[:, 0]
y1 = dets[:, 1]
x2 = dets[:, 2]
y2 = dets[:, 3]
scores = dets[:, 4]
# 计算每个检测框的面积
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
# 按照置信度score的值降序排序的下标序列
order = scores.argsort()[::-1]
# keep用来保存最后保留的检测框的下标
keep = []
while order.size > 0:
# 当前置信度最高bbox的index
i = order[0]
# 添加当前剩余检测框中得分最高的index到keep中
keep.append(i)
# 得到此bbox和剩余其他bbox的相交区域,左上角和右下角
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
# 计算相交的面积,不重叠时面积为0
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
# 计算IoU:重叠面积/(面积1+面积2-重叠面积)
iou = inter / (areas[i] + areas[order[1:]] - inter)
# 保留IoU小于阈值的bbox
inds = np.where(iou <= thresh)[0]
order = order[inds+1]
return keep
标签:loss,YOLOv1,nn,检测,self,目标,bbox,pred,size
From: https://blog.csdn.net/qq_43322273/article/details/143138668