目标检测是对图像中存在的目标进行定位和分类的过程。识别出的物体在图像中显示为边界框。一般的目标检测有两种方法:基于区域提议的和基于回归/分类的。在本章中,我们将使用一个名为YOLO的基于回归/分类的方法。YOLO-v3是该系列的其中一个版本,在精度方面比以前的(YOLOV1、YOLOV2)版本表现更好。因此,本章将重点介绍使用PyTorch开发的Yolo-v3。
在本章中,我们将学习如何实现YOLO-v3算法,并使用PyTorch训练和部署它。
我们将介绍以下内容:
使用COCO数据集训练YOLOV3模型。COCO是一个大规模的目标检测、分割和看图说话数据集,包含80个类别。
在本教程中,您将学习如何创建自定义数据集、执行数据变换和定义数据加载器。
现在我们已经下载了COCO数据集,我们将使用PyTorch的dataset和Dataloader类创建训练和验证数据集和数据加载器。
在本节中,我们将定义CocoDataset类,并展示来自训练和验证数据集的一些示例图像。
#1. 首先定义一个自己数据集类
form torch.utils.data import Dataset
from PIL import Image
import torchvision.transforms.functional as TF
import os
import numpy as np
import torch
device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 定义CocoDataset类
class CocoDataset(Dataset):
def __init__(self, path2listFile, transform=None, trans_params=None):
with open(path2listFile, "r") as file:
self.path2imgs = file.readlines()
self.path2labels=[path.replace("images", "labels").replace(".png", ".txt").replace(".jpg", ".txt") for path in self.path2imgs]
self.trans_params = trans_parms
self.transform = transform
def __len__(self):
return len(self.path2imgs)
def __getitem__(self, index):
path2img = self.path2imgs[index%len(self.path2imgs)].rstrip()
labels = None
if os.path.exists(path2label):
labels=np.loadtxt(path2label).reshape(-1, 5)
if self.transform:
img, labels = self.transform(img, labels, self.trans_params)
return img, labels, path2img
#2. 下一步,为训练集创建一个CocoDataset类的实例
root_data = "./data/coco"
path2trainList=os.path.join(root_data, "trainvalno5k.txt")
coco_train = CocoDataset(path2trainList)
print(len(coco_train))
#117264
# 获取coco_train中的一个样本数据
img, labels, path2img = coco_train[1]
print("image size:", img.size, type(img))
print("labels shape:", labels.shape, type(labels))
print("labels \n", labels)
# image size: (640, 426)
# labels shape: (2, 5)
# labels
# [[23. 0.77 0.49 0.34 0.70]
# [23. 0.19 0.90 0.21 0.13]]
#3. 为验证集创建一个CocoDataset类的实例
path2valList=os.path.join(root_data, "5k.txt")
coco_val = CocoDataset(path2valList, transform=None, trans_params=None)
print(len(coco_val))
# 5000
# 获取coco_val的一个样本
img, labels, path2img = coco_val[7]
print("image size:", img.size, type(img))
print("labels shape:", labels.shape, type(labels))
print("labels \n", labels)
# image size: (640, 426)
# labels shape: (3, 5)
# labels
# [[20. 0.57 0.59 0.74 0.90]
# [20. 0.49 0.40 0.61 0.63]
# [20. 0.89 0.40 0.21 0.93]]
#4. 显示来自于coco_train和coco_val中的一些样本
import matplotlib.pylab as plt
import numpy as np
from PIL import Image, ImageDraw, ImageFont
from torchvision.transforms.functional import to_pil_image
import random
# 获取COCO类别名称列表
path2cocoNames="./data/coco.names"
fp=open(path2cocoNames,"r")
coco_names=fp.read().split("\n")[:-1]
print("number of classes:", len(coco_names))
print(coco_names)
# number of classese: 80
# ['person', 'bicycle', 'car', 'motorbike', ...]
# 定义一个rescale_bbox函数将缩放归一化边界框转换到原始图像大小
def rescale_bbox(bb, W, H):
x,y,w,h=bb
return [x*w, y*H, w*W, h*H]
# 定义show_img_bbox函数显示带有标注框的图像
COLORS=np.random.randint(0,255,size=(80,3),dtype="uint8")
fnt=ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf",16)
def show_img_bbox(img, targets):
if torch.is_tensor(img):
img=pil_to_image(img)
if torch.is_tensor(targets):
targets=targets.numpy()[:,1:]
W,H=img.size
draw = ImageDraw.Draw(img)
for tg in targets:
id_=int(tg[0])
bbox=tg[1:]
bbox=rescale_bbox(bbox,W,H)
xc,yc,w,h=bbox
color=[int(c) for c in COLORS[id_]]
name=coco_names[id_]
draw.rectangle(((xc-w/2,yc-h/2), (xc+w/2,yc+h/2)),outline=tuple(color),width=3)
draw.text((xc-w/2,yc-h/2),name,font=fnt,fill=(255,255,255,0))
plt.imshow(np.array(img))
# 在前面的代码片段中,如果传递给ImageFont.truetype的字体在您的计算机上不可用,您可以从draw.text中删除
# font=fnt。或者,您可以使用更常见的字体,如以下所示
# fnt=ImageFont.truetype("arial.ttf",16)
#5. 调用show_img_bbox函数显示coco_train中的样例
np.random.seed(2)
rnd_ind = np.random.randint(len(coco_train))
img,labels,path2img=coco_train[rnd_ind]
print(img.size, labels.shape)
plt.rcParams["figure.figsize"]=(20, 10)
show_img_bbox(img,labels)
# (640,428) (2,5)
#6. 调用show_img_bbox函数显示coco_val中的样例
np.random.seed(0)
rnd_ind = np.random.randint(len(coco_val))
img,labels,path2img=coco_val[rnd_ind]
print(img.size, labels.shape)
plt.rcParams["figure.figsize"]=(20, 10)
show_img_bbox(img,labels)
# (640,428) (3,5)
在本节中,我们将定义一个转换函数和要传递给CocoDataset类的参数。
#1. 首先定义pad_to_square函数
def pad_to_square(img, boxes, pad_value=0, normalized_labels=True):
w,h=img.size
w_factor,h_factor=(w,h) if normalized_labels else (1,1)
dim_diff=np.abs(h-w)
pad1 = dim_diff//2
pad2=dim_diff-pad1
if h<w:
left,top,right,bottom=0,pad1,0,pad2
else:
left,top,right,bottom=pad1,0,pad2,0
padding=(left,top,right,bottom)
img_padded = TF.pad(img, padding=padding, fill=pad_value)
w_padded, h_padded = img_padded.size
x1=w_factor * (boxes[:,1] - boxes[:,3]/2)
y1=h_factor * (boxes[:,2] - boxes[:,4]/2)
x2=w_factor * (boxes[:,1] + boxes[:,3]/2)
y2=h_factor * (boxes[:,2] + boxes[:,4]/2)
x1 += padding[0] # left
y1 += padding[1] # top
x2 += padding[2] # right
y2 += padding[3] # bottom
boxes[:,1]=((x1+x2)/2)/w_padded
boxes[:,2]=((y1+y2)/2)/h_padded
boxes[:,3] *=w_factor/w_padded
boxes[:,4] *=w_factor/h_padded
return img_padded, boxes
#2. 使用hflip函数实现图片水平翻转
def hflip(image, labels):
image = TF.hflip(image)
labels[:,1]=1.0-labels[:,1]
return image, labels
#3. 定义transformer函数
def transformer(image, labels, params):
if params["pad2square"] is True:
image, labels=pad_to_square(image, labels)
image=TF.resize(image, params["target_size"])
if random.random()<params["p_hflip"]:
image, labels=hflip(image, labels)
image=TF.to_tensor(image)
targets=torch.zeros((len(labels),6))
targets[:,1:]=torch.from_numpy(labels)
return image, targets
#4. 现在,为训练数据创建CocoDataset对象并传递transformer函数
trans_params_train={
"target_size":(416,416),
"pad2square":True,
"p_hflip":1.0,
"normalized_labels":True,
}
coco_train = CocoDataset(path2trainList,transform=transformer,trans_params=trans_params_train)
# 显示coco_train的一个样本
np.random.seed(2)
rnd_ind=np.random.randint(len(coco_train))
img,targets,path2img=coco_train[rnd_ind]
print("image shape:", img.shape)
print("labels shape:", targets.shape)
plt.rcParams["figure.figsize"]=(20, 10)
COLORS=np.random.randint(0,255,size=(80,3),dtype="uint8")
show_img_bbox(img,targets)
# image shape: torch.Size([3, 416, 416])
# labels shape: torch.Size([2, 6])
#5. 为验证数据创建CocoDataset对象并传递transformer函数
trans_params_val={
"target_size":(416,416),
"pad2square":True,
"p_hflip":0.0,
"normalized_labels":True,
}
coco_val= CocoDataset(path2valList,transform=transformer,trans_params=trans_params_val)
# 显示coco_val的一个样本
np.random.seed(0)
rnd_ind=np.random.randint(len(coco_val))
img,targets,path2img=coco_val[rnd_ind]
print("image shape:", img.shape)
print("labels shape:", targets.shape)
plt.rcParams["figure.figsize"]=(20, 10)
COLORS=np.random.randint(0,255,size=(80,3),dtype="uint8")
show_img_bbox(img,targets)
# image shape: torch.Size([3, 416, 416])
# labels shape: torch.Size([3, 6])
在本节中,我们将定义训练和验证数据加载器,这样我们就可以从coco_train和coco_val获得小批量的数据。
#1. 为训练集定义Dataloader对象
from torch.utils.data import DataLoader
batch_size=8
train_dl=DataLoader(coco_train, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True,collate_fn=collate_fn)
# collate_fn定义如下:
def collate_fn(batch):
imgs,targets,paths = list(zip(*batch))
# 删除空的框
targets = [boxes for boxes in targets if boxes is not None]
for b_i, boxes in enumerate(targets):
boxes[:,0]=b_i
targets = torch.cat(targets,0)
imgs = torch.stack([img for img in imgs])
return imgs, targets, paths
# 显示train_dl中的一个mini-batch
torch.manual_seed(0)
for imgs_batch,tg_batch,path_batch in train_dl:
break
print(imgs_batch.shape)
print(tg_batch.shape, tg_batch.dtype)
# torch.Size([8, 3, 416, 416])
# torch.Size([32, 6]) torch.float32
#2. 为验证集定义Dataloader对象
val_dl=DataLoader(coco_val,batch_size=batch_size,shuffle=False,num_workers=0,pin_memory=True,collate_fn=collate_fn)
# 显示val_dl中的一个mini-batch
torch.manual_seed(0)
for imgs_batch,tg_batch,path_batch in val_dl:
break
print(imgs_batch.shape)
print(tg_batch.shape, tg_batch.dtype)
# torch.Size([8, 3, 416, 416])
# torch.Size([83, 6]) torch.float32
代码解析:
在“创建自定义COCO数据集”小节中,我们为COCO数据集创建了一个PyTorch数据集类。首先,导入需要的包,然后,定义CocoDataset类。这个类首先定义了带有三个输入的__init__函数:
在步骤2中,我们使用“trainvalno5k.txt”中的训练数据列表创建了CocoDataset类的对象。我们没有传递一个变换函数。如我们所见,coco_train包含用于训练的117264张图像。然后,我们从coco_train获得一个示例图像,并打印图像的大小、类型和标签。如预期的那样,返回了PIL图像和numpy数组。检查打印的标签。每一行的第一个数字是对象ID,而接下来的四个数字是规范化的边界框坐标。
在步骤3中,与步骤2类似,我们使用“5k.txt”中的验证数据列表创建了CocoDataset类的对象。正如我们所看到的,coco_val中有5000张图片。然后,我们从coco_val中获得一个样本示例,并打印图像和标签的类型和大小。您可以尝试通过更改索引来获取不同的图像。正如我们所见,没有任何转换,图像有不同的尺寸大小。
在第4步中,我们显示了来自coco_train和coco_val数据集的示例图像和对象边界框。首先,我们导入所需的包。然后,我们从cocoa .names文件中将COCO对象名称加载到一个列表中。正如预期的那样,该文件中有80个对象类别。我们将使用这些名称在边界框上显示对象名称。
接下来,我们定义了rescale_bbox辅助函数来将标准化的边界框缩放到原始图像大小。同样,为了以不同的颜色显示边界框,我们定义了COLORS,这是一个随机元组数组。
然后,我们定义了有两个输入的show_img_bbox辅助函数:
在辅助函数的其余部分中,我们循环了边界框并将它们添加到图像中。注意,我们分别从COLORS和names(基于对象索引)中获得了边界框的颜色和名称。名称作为一段文本放在边框的左上角。
接下来,我们调用show_img_bbox函数来显示来自coco_train和coco_val示例图像及其边界框。您可以注释掉np.random.seed行,以便在每次重新运行时看到不同的图像。
在"数据变换"小节中,我们定义了数据变换所需的函数。例如调整图像大小、数据增强或将数据转换为PyTorch张量。
我们首先定义了pad_to_square函数,它有四个输入:
然后,我们计算图像每边的填充大小。如果高度小于宽度,我们只需要在图像的顶部和底部添加像素。否则,我们将在图像的左边和右边添加像素。
在填充图像之后,我们根据填充大小调整边界框坐标。为此,我们提取了填充前的左上角的x1, y1和右下角的x2, y2坐标。
方框数组的格式为[id, xc, yc, w, h],其中id为对象标识符,xc, yc为边界框的中心坐标,w, h为边界框的宽度和高度。
然后,我们通过添加填充大小来调整x1, y1, x2, y2。接下来,我们使用调整后的x1、y1、x2、y2值计算边界框。注意,我们再次将标签规范化为[0,1]的范围。这个函数返回一个方形的PIL图像和一个numpy数组的标签。
在步骤2中,我们定义了hflip函数来水平翻转图像和标签。
在步骤3中,我们定义了带有三个输入的transformer函数:
该函数输入PIL图像及其标签,并将变换后的图像和标签作为PyTorch张量返回。在函数中,我们检查pad2square标志,如果它是True,我们调用pad_to_square函数。然后,图像的大小调整为416416,这是一个yolov3网络的标准输入大小。接下来,我们调用hflip函数随机翻转图像以增强数据。最后,我们使用torchvision的to_tensor函数将PIL图像转换为PyTorch张量。标签也被转换为大小为n6的PyTorch张量。在小批量处理中,额外的维度将用于索引图像。
在第4步中,我们重新定义了coco_train;但是,这一次,我们将transformer和trans_params_train传递给CocoDataset类。为了强制水平翻转,我们将p_hflip概率设置为1.0。在实践中,我们通常将概率设为0.5。您可以在示例图像上看到转换的效果。图像从顶部和底部被填充为零,大小调整为416*416,并水平翻转。
类似地,在步骤5中,我们重新定义了coco_val。我们不需要对验证数据进行数据扩充,所以我们将p_hflip的概率设置为0.0。看看变换后的样本尺寸大小。图像从顶部和底部被填充为零,大小调整为416*416,但没有翻转。
在定义Dataloader小节中,我们分别为训练和验证数据集定义了Dataloader类的两个对象,train_dl和val_dl。我们还定义了collate_fn函数来处理小批量数据并返回PyTorch张量。该函数作为Dataloader类的参数给出,以便进程动态进行。在这个函数中,我们使用zip(*iterateble)对小批处理中的图像、目标和路径进行分组。然后,我们删除目标中的任何空边界框。接下来,我们在小批量处理中设置样本索引。最后,我们将图像和目标连接为PyTorch张量。为了了解它是如何工作的,我们从train_dl和val_dl中提取了一个小批处理,并打印了返回张量的形状。注意,在train_dl中批大小被设置为16,而在val_dl中被设置为32。此外,在train_dl的小批量处理中有87个边界框,但在val_中有250个边界框。
YOLO-v3网络由stride=2、跳过连接和上采样层的卷积层构成。没有池层。网络接收一个大小为416416的图像作为输入,提供三个YOLO输出,如下图所示:
该网络对输入图像下采样32倍,得到尺寸为1313的feature map,得到yolo-out1。为了提高检测性能,1313 feature map被向上采样到2626和52*52,分别得到yolo-out2和yolo-out3。feature map中的一个单元格预测了三个预定义锚点边界框。因此,网络总计预测13x13x3+26x26x3+52x52x3=10647个边界框。
一个边界框用85个数字定义:
在本教程中,我们将向您展示如何使用PyTorch开发YOLO-v3模型。
我们将定义几个函数来解析配置文件,创建PyTorch模块,并定义Darknet模型。
#myutils.py
import torch
from torch import nn
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
def parse_model_config(path2file):
cfg_file = open(path2file, 'r')
lines = cfg_file.read().split('\n')
lines = [x for x in lines if x and not x.startswith('#')]
lines = [x.rstrip().lstrip() for x in lines]
blocks_list = []
for line in lines:
# start of a new block
if line.startswith('['):
blocks_list.append({})
blocks_list[-1]['type'] = line[1:-1].rstrip()
else:
key, value = line.split("=")
value = value.strip()
blocks_list[-1][key.rstrip()] = value.strip()
return blocks_list
def create_layers(blocks_list):
hyperparams = blocks_list[0]
channels_list = [int(hyperparams["channels"])]
module_list = nn.ModuleList()
for layer_ind, layer_dict in enumerate(blocks_list[1:]):
modules = nn.Sequential()
if layer_dict["type"] == "convolutional":
filters = int(layer_dict["filters"])
kernel_size = int(layer_dict["size"])
pad = (kernel_size - 1) // 2
bn=layer_dict.get("batch_normalize",0)
conv2d= nn.Conv2d(
in_channels=channels_list[-1],
out_channels=filters,
kernel_size=kernel_size,
stride=int(layer_dict["stride"]),
padding=pad,
bias=not bn)
modules.add_module("conv_{0}".format(layer_ind), conv2d)
if bn:
bn_layer = nn.BatchNorm2d(filters,momentum=0.9, eps=1e-5)
modules.add_module("batch_norm_{0}".format(layer_ind), bn_layer)
if layer_dict["activation"] == "leaky":
activn = nn.LeakyReLU(0.1)
modules.add_module("leaky_{0}".format(layer_ind), activn)
elif layer_dict["type"] == "upsample":
stride = int(layer_dict["stride"])
upsample = nn.Upsample(scale_factor = stride)
modules.add_module("upsample_{}".format(layer_ind), upsample)
elif layer_dict["type"] == "shortcut":
backwards=int(layer_dict["from"])
filters = channels_list[1:][backwards]
modules.add_module("shortcut_{}".format(layer_ind), EmptyLayer())
elif layer_dict["type"] == "route":
layers = [int(x) for x in layer_dict["layers"].split(",")]
filters = sum([channels_list[1:][l] for l in layers])
modules.add_module("route_{}".format(layer_ind), EmptyLayer())
elif layer_dict["type"] == "yolo":
anchors = [int(a) for a in layer_dict["anchors"].split(",")]
anchors = [(anchors[i], anchors[i + 1]) for i in range(0, len(anchors), 2)]
mask = [int(m) for m in layer_dict["mask"].split(",")]
anchors = [anchors[i] for i in mask]
num_classes = int(layer_dict["classes"])
img_size = int(hyperparams["height"])
yolo_layer = YOLOLayer(anchors, num_classes, img_size)
modules.add_module("yolo_{}".format(layer_ind), yolo_layer)
module_list.append(modules)
channels_list.append(filters)
return hyperparams, module_list
class EmptyLayer(nn.Module):
def __init__(self):
super(EmptyLayer, self).__init__()
class YOLOLayer(nn.Module):
def __init__(self, anchors, num_classes, img_dim=416):
super(YOLOLayer, self).__init__()
self.anchors = anchors
self.num_anchors = len(anchors)
self.num_classes = num_classes
self.img_dim = img_dim
self.grid_size = 0
def forward(self, x_in):
batch_size = x_in.size(0)
grid_size = x_in.size(2)
devide=x_in.device
prediction=x_in.view(batch_size, self.num_anchors,
self.num_classes + 5, grid_size, grid_size)
prediction=prediction.permute(0, 1, 3, 4, 2)
prediction=prediction.contiguous()
obj_score = torch.sigmoid(prediction[..., 4])
pred_cls = torch.sigmoid(prediction[..., 5:])
if grid_size != self.grid_size:
self.compute_grid_offsets(grid_size, cuda=x_in.is_cuda)
pred_boxes=self.transform_outputs(prediction)
output = torch.cat(
(
pred_boxes.view(batch_size, -1, 4),
obj_score.view(batch_size, -1, 1),
pred_cls.view(batch_size, -1, self.num_classes),
), -1,)
return output
# 计算网格偏移
def compute_grid_offsets(self, grid_size, cuda=True):
self.grid_size = grid_size
self.stride = self.img_dim / self.grid_size
self.grid_x = torch.arange(grid_size, device=device).repeat(1, 1, grid_size, 1 ).type(torch.float32)
self.grid_y = torch.arange(grid_size, device=device).repeat(1, 1, grid_size, 1).transpose(3, 2).type(torch.float32)
scaled_anchors=[(a_w / self.stride, a_h / self.stride) for a_w, a_h in self.anchors]
self.scaled_anchors=torch.tensor(scaled_anchors,device=device)
self.anchor_w = self.scaled_anchors[:, 0:1].view((1, self.num_anchors, 1, 1))
self.anchor_h = self.scaled_anchors[:, 1:2].view((1, self.num_anchors, 1, 1))
# 将预测结果转换为边界框
def transform_outputs(self,prediction):
device=prediction.device
x = torch.sigmoid(prediction[..., 0]) # Center x
y = torch.sigmoid(prediction[..., 1]) # Center y
w = prediction[..., 2] # Width
h = prediction[..., 3] # Height
pred_boxes = torch.zeros_like(prediction[..., :4]).to(device)
pred_boxes[..., 0] = x.data + self.grid_x
pred_boxes[..., 1] = y.data + self.grid_y
pred_boxes[..., 2] = torch.exp(w.data) * self.anchor_w
pred_boxes[..., 3] = torch.exp(h.data) * self.anchor_h
return pred_boxes * self.stride
我们需要解析配置文件,以便能够构建模型。我们提供了一个myutils.py文件,其中包含一个辅助函数,您可以使用它来实现这一点。配置文件yolov3.cfg之前请下载yolov3.cfg 配置文件。
#1. 首先,我们导入parse_model_config辅助函数
from myutils import parse_model_config
#2. 使用parse_model_config函数读取和打印配置文件
path2config="./config/yolov3.cfg"
blocks_list=parse_model_config(path2config)
print(blocks_list)
# [{'type': 'net',
# 'batch': '1',
# 'subdivisions': '1',
# 'width': '416',
# 'height': '416',
# ...
在本节中,我们将基于已解析的配置文件创建PyTorch模块。我们提供了myutils.py文件,其中包含一个可以用来创建YOLOv3网络的PyTorch模块的辅助函数。
#1. 首先,导入create_layer辅助函数,将blocks_list转换为PyTorch模块
from myutils import create_layers
#2. 调用create_layers函数,获得PyTorch模块的列表
hy_pa,m_l=create_layers(blocks_list)
print(hy_pa)
print(m_l)
# 打印超参数
# {'type': 'net',
# 'batch': '1',
# 'subdivisions': '1',
# 'width': '416',
# 'height': '416',
# 'channels': '3',
# ....
# 打印一些模块
# ModuleList(
# (0): Sequential(
# (conv_0): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1,1), bias=False)
# (batch_norm_0): BatchNorm2d(32, eps=1e-05,momentum=0.9,affine=True,track_running_stats=True)
# (leaky_0): LeakyReLU(negative_slope=0.1)
# )
# .....
class Darknet(nn.Module):
#1. 首先定义__init__函数
def __init__(self, config_path, img_size=416):
super(Darknet, self).__init__()
self.blocks_list=parse_model_config(config_path)
self.hyperparams, self.module_list=create_layers(self.blocks_list)
self.img_size=img_size
#2. 定义forward函数
def forward(self, x):
img_dim = x.shape[2]
layer_outputs, yolo_outputs = [], []
for block, module in zip(self.blocks_list[1:],self.module_list):
if block["type] in ["convolutional", "upsample", "maxpool"]:
x = module(x)
elif block["type"] == "shortcut":
layer_ind = int(block["from"])
x = layer_outputs[-1] + layer_outputs[layer_ind]
elif block["type"] == "yolo":
x = module[0](x)
yolo_outputs.append(x)
elif block["type"] == "route":
x = torch.cat([layer_outputs[int(l_i)] for l_i in block["layers"].split(",")],1)
layer_outputs.append(x)
yolo_out_cat = torch.cat(yolo_outputs, 1)
return yolo_out_cat, yolo_outputs
#3. 创建Darknet类的实例对象
model = Darknet(path2config).to(device)
print(model)
# Darknet(
# (module_list): ModuleList(
# (0): Sequential(
# (conv_0): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1),
# padding=(1, 1), bias=False)
# (batch_norm_0): BatchNorm2d(32, eps=1e-05, momentum=0.9,
# affine=True, track_running_stats=True)
# (leaky_0): LeakyReLU(negative_slope=0.1)
# ...
#4. 接下来,让我们使用一个虚拟输入来测试这个模型:
dummy_img = torch.rand(1,3,416,416).to(device)
with torch.no_grad():
dummy_out_cat, dummy_out = model.forward(dummy_img)
print(dummy_out_cat.shape)
print(dummy_out[0].shape, dummy_out[1].shape, dummy_out[2].shape)
# torch.Size([1, 10647, 85])
# torch.Size([1, 507, 85]) torch.Size([1, 2028, 85]) torch.Size([1, 8112, 85])
代码解析:
在解析配置文件小节中,我们定义了一个辅助函数来解析YOLO-v3配置文件。为了简短起见,我们在myutils.py中定义了辅助函数。函数的输入是path2file,一个字符串,可以在"./config/yolov3.cfg"中找到。该函数解析配置文件,并将层通过字典列表返回。在辅助函数中,我们使用.split("\n")将文件分割成列表。然后,我们删除了任何空行和注释。接下来,我们分别使用rstrip()和lstrip()删除一行的左右空白区域。接下来,我们定义了一个名为layers_list的空列表,并对列表进行循环。如果一行以“[”开头,一个新层被创建。对于新层,我们增加一个字典到layers_list。字典有一个“type”键,显示层的类型。第一层的类型是“net”,它定义了网络的超参数。我们将“height”和“width”参数设置为“416”。如果某一行没有以“[”开头,则作为层的属性添加到当前字典中。
在创建PyTorch模块小节中,我们定义了一个名为create_layers的辅助函数来创建对应于这些层的PyTorch模块。为了简短起见,我们在myutils.py文件中定义了辅助函数,并从myutils.py中导入了辅助函数。不过,我们将解释辅助函数。
函数的输入是blocks_list,它提供了解析配置文件小节中从配置文件中解析的层列表。
在函数中,我们从第一个block中提取超参数。接下来,我们创建了channels_list来保存每个层的输入通道数量。对于第一层,输入通道数为3(与图像通道相同)。接下来,我们创建了一个nn.ModuleList类的对象来保存子模块。然后,我们创建了一个循环来检查blocks列表。
接下来,我们创建了nn.Sequentia类的一个对象,将多个层添加到模块中。例如,对于“convolutional”块,我们使用add_module方法依次添加nn.Conv2d,nn.BatchNorm2d和nn.LeakyReLU。
接下来,在主循环中,我们检查了上采样块。在YOLO-v3中使用上采样层,将特征图上采样到更高的分辨率,以便与之前的层堆叠。nn.Upsample模块取stride=2参数,向上采样feature maps的倍数为2。我们使用默认模式="nearest"来进行上采样。
接下来,在循环中,我们检查了shortcut块。它们用于创建跳过连接。
配置文件中的shortcut块定义如下:
[shortcut]
from=-3
activation=linear
这里,from=-3表示跳过连接来自shortcut块从后(当前)向之前推的第三层。滤波器个数为channels_list中存储的跳过连接的滤波器个数。
接下来,在循环中,我们检查了route块。在配置文件中使用一个或两个参数定义了route块。
如果使用一个参数定义,如以下代码段所示,则返回第4层的特性映射(从路由层向后):
[route]
layers=-4
如果定义了两个参数,则它们用于连接来自多个层的特性映射。例如,在下面的代码片段中,来自前一层和第61层的特征映射沿着深度维度连接起来:
[route]
layers=-1,61
注意,实际的连接将发生在网络类的前向函数中。在这里,我们只汇总了路由层中的滤波器数量。
下一个循环,我们检查了yolo块。配置文件中有三个yolo块对应于三个检测输出。让我们看看配置文件的第一个yolo块:
[yolo]
mask = 6,7,8
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198,
373,326
classes=80
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1
在我们的代码中,我们提取了anchors属性,并将anchors分为一个由9个元组组成的列表,这些元组对应于YOLO-v3中的9个锚。第一个元组在索引0处,为(10,13),而最后一个元组在索引8处,为(373,326)。每个YOLO输出有三个anchors。
接下来,我们提取mask属性作为一个包含三个值的列表。因为每个yolo块有三个锚点,所以mask属性表示这三个锚点的索引。例如,索引在6,7,8的最后三个锚点属于第一个yolo输出。因此,第一个yolo输出的锚点如下: anchors = [(116,90), (156,198), (373,326)]
然后提取目标类别数和图像大小,分别为80和416。现在,我们检查了create_layers函数主循环中的所有层类型。每个层类型都会产生一个附加到module_list的PyTorch模块。然后,我们返回module_list和hyperparams。
接下来,我们定义了EmptyLayer类,它在跳过或shortcut连接中什么也不做。然后,我们定义了YOLOLayer类。首先,我们定义了YOLOLayer类的大部分,并逐个添加函数,以提高代码的可读性。然后,我们定义了__init__函数并初始化了变量。该函数有三个输入:
接下来,在类中,我们定义了具有一个输入的前向函数。对于第一个yolo层,函数的输入是一个形状为[batch_size, 3*85, 13, 13]的特征图。该函数首先提取批处理大小和网格大小。对于第一个yolo层,网格大小是13。接下来,我们将输入特征图调整为[batch_size, 3, 13, 13, 85]。
接下来,我们对目标得分和类别预测应用sigmoid函数。如前所述,网络对每个网格的每个anchor预测85个值:4个值为[x, y, w, h],一个目标得分和80个类预测。然后,我们使用compute_grid_offsets函数计算网格偏移量。我们在第4步中定义了这个辅助函数。接下来,我们使用transform_outputs函数将预测转换为边界框,该函数在步骤5中定义。
然后,将边界框预测与object score和类别预测连接起来,作为函数的输出返回。
接下来,我们定义了compute_grid_offset辅助函数。辅助函数是YOLOLayer类的一个方法,有三个输入:
在步骤2中,我们调用了create_layers函数以确保它能正常工作。这个函数的输入是我们通过解析配置文件获得的blocks_list。该函数返回超参数和106个PyTorch模块的列表,这些模块对应于已解析的层。
在Defining the Darknet model小节中,我们定义了Darknet类,这是一个完整的检测网络。首先,我们定义了带有三个输入的__init__函数:
在循环中,如果遇到 convolutional块,我们将输入张量传递给模块,并将输出添加到layer_outputs列表。然后,如果遇到shortcut块,我们提取shortcut层索引,将shortcut层的输出添加到上一层的输出。然后将结果添加到layer_outputs列表中。接下来,如果遇到yolo层,我们将把模块输出添加到layer_outputs和yolo_outputs列表。最后,如果遇到route层,我们提取route层索引并连接route层的输出。最后,我们将yolo_outputs列表中的张量连接为yolo_out_cat张量,并返回两个输出。
在步骤3中,我们创建了一个名为model的Darknet类对象并将其打印出来。正如我们所看到的,已经创建了完整的YOLO-v3架构。当模型被打印出来时,我们检查了它的架构。它应该有106个层,其中三个YOLOLayer模块的索引为82、94和106。
在YOLO-v3架构中总共有106个层。层82、94、106是三个yolo输出。
在步骤4中,为了检查结构并验证我们的代码,我们向模型传递了一个虚拟图像并获得了输出。提醒一下,有三个yolo输出的形状(1,507, 85)、(1,2028,85)和(1,8112,85)。最后的输出是维度1中这三个层的堆叠。输出大小为(1,10647,85)因为507 + 2028 + 8112 = 10647。换句话说,对于每个输入图像,YOLO-v3预测10647个边界框。
在本教程中,我们将为YOLO-v3体系结构定义一个损失函数。要深入了解YOLO-v3损失,请回忆一下包含以下元素的模型输出:
def get_loss_batch(output, targets, params_loss, opt=None):
ignore_thres=params_loss["ignore_thres"]
scaled_anchors=params_loss["scaled_anchors"]
mse_loss = params_loss["mse_loss"]
bce_loss = params_loss["bce_loss"]
num_yolos=params_loss["num_yolos"]
num_anchors=params_loss["num_yolos"]
obj_scale=params_loss["obj_scale"]
noobj_scale=params_loss["noobj_scale"]
loss=0.0
for yolo_ind in range(num_yolos):
yolo_out=output[yolo_ind]
batch_size, num_bbxs, _=yolo_out.shape
gz_2=num_bbxs/num_anchors
grid_size=int(np.sqrt(gz_2))
yolo_out = yolo_out.view(batch_size, num_anchors, grid_size, grid_size, -1)
# 提取预测的边界框
pred_boxes=yolo_out[:,:,:,:,:4]
x,y,w,h = transform_bbox(pred_boxes,scaled_anchors[yolo_ind])
pred_conf=yolo_out[:,:,:,:,4]
pred_clas_prob=yolo_out[:,:,:,:,5:]
yolo_targets=get_yolo_targets({
"pred_cls_prob":pred_cls_prob,
"pred_boxes":pred_boxes,
"targets":targets,
"anchors":scaled_anchors[yolo_ind],
"ignore_thres":ignore_thres,
})
obj_mask=yolo_targets["obj_mask"]
noobj_msk=yolo_targets["noobj_mask"]
tx=yolo_targets["tx"]
ty=yolo_targets["ty"]
tw=yolo_targets["tw"]
th=yolo_targets["th"]
tcls=yolo_targets["tcls"]
t_conf=yolo_targets["t_conf"]
loss_x = mse_loss(x[obj_mask], tx[obj_mask])
loss_y = mse_loss(y[obj_mask], ty[obj_mask])
loss_w = mse_loss(w[obj_mask], tw[obj_mask])
loss_h = mse_loss(h[obj_mask], th[obj_mask])
loss_conf_obj = bce_loss(pred_conf[obj_mask], t_conf[obj_mask])
loss_conf_noobj = bce_loss(pred_conf[noobj_mask], t_conf[noobj_mask])
loss_conf = obj_scale * loss_conf_obj + noobjscale * loss_conf_noobj
loss_cls = bce_loss(pred_cls_prob[obj_mask], tcls[obj_mask])
loss += loss_x + loss_y + loss_w + loss_h + loss_conf + loss+cls
if opt is not None:
opt.zero_grad()
loss.backward()
opt.step()
return loss.item()
在step1中,调用了transform_bbox 函数,该函数定义如下
def transform_bbox(bbox, anchors):
x=bbox[:,:,:,:,0]
y=bbox[:,:,:,:,1]
w=bbox[:,:,:,:,2]
h=bbox[:,:,:,:,3]
anchor_w=anchors[:,0].view((1,3,1,1))
anchor_h=anchors[:,1].view((1,3,1,1))
x=x-x.floor()
y=y-y.floor()
w=torch.log(w/anchor_w+1e-16)
h=torch.log(h/anchor_h+1e-16)
return x,y,w,h
def get_yolo_targets(params):
pred_boxes=params["pred_boxes"]
pred_cls_prob=params["pred_cls_prob"]
target=params["targets"]
anchors=params["anchors"]
ignore_thres=params["ignore_thres"]
batch_size=pred_boxes.size(0)
num_anchors=pred_boxes.size(1)
grid_size=pred_boxes.size(2)
num_cls=pred_cls_prob.size(-1)
sizeT=batch_size, num_anchors, grid_size, grid_size
obj_mask=torch.zeros(sizeT, device=device,dtype=torch.uint8)
noobj_mask=torch.zeros(sizeT,device=device,dtype=torch.uint8)
tx=torch.zeros(sizeT,device=device,dtype=torch.float32)
ty=torch.zeros(sizeT,device=device,dtype=torch.float32)
tw=torch.zeros(sizeT,device=device,dtype=torch.float32)
th=torch.zeros(sizeT,device=device,dtype=torch.float32)
sizeT=batch_size, num_anchors,grid_size,grid_size,num_cls
tcls=torch.zeros(sizeT,device=device,dtype=torch.float32)
# 缩放和提取目标边界框
target_bboxes=target[:,2:]*grid_size
t_xy=target_bboxes[:,:2]
t_wh = target_bboxes[:,2:]
t_x, t_y=t_xy.t()
t_w, t_h=t_wh.t()
grid_i, grid_j = t_xy.long().t()
# 选择与targets有最高IOU的anchor
iou_with_anchors = [get_iou_WH(anchor, t_wh) for anchor in anchors]
iou_with_anchors = torch.stack(iou_with_anchors)
best_iou_wa, best_anchor_ind=iou_with_anchors.max(0)
# 设置objext mask tesnors
batch_inds, target_labels = target[:,:2].long().t()
obj_mask[batch_inds, best_anchor_ind, grid_j, grid_i] = 1
noobj_mask[batch_inds, best_anchor_ind, grid_j, grid_i] = 0
for ind, iou_wa in enumerate(iou_with_anchors.t()):
noobj_mask[batch_inds[ind], iou_wa>ignore_thres, grid_j[ind], grid_i[ind]] = 0
# 设置x和y
tx[batch_inds, best_anchor_ind, grid_j, grid_i] = t_x - t_x.floor()
ty[batch_inds, best_anchor_ind, grid_j, grid_i] = t_y - t_y.floor()
# 设置w和h
anchor_w = anchors[best_anchor_ind][:,0]
tw[batch_inds, best_anchor_ind, grid_j, grid_i] = torch.log(t_w/anchor_w + 1e-16)
th[batch_inds, best_anchor_ind, grid_j, grid_i] = torch.log(t_h/anchor_h + 1e-16)
# 设置target类别
tcls[batch_inds, best_anchor_ind, grid_j, grid_i, target_labels] = 1
# 最后,以Python字典的形式返回输出
output = {
"obj_mask":obj_mask,
"noobj_mask":noobj_mask,
"tx":tx,
"ty":ty,
"tw":tw,
"th":th,
"tcls":tcls,
"t_conf":obj_mask.float(),
}
return output
def get_iou_WH(wh1, wh2):
wh2 = wh2.t()
w1,h1=wh1[0],wh1[1]
w2,h2=wh2[0],wh2[1]
inter_area=torch.min(w1,w2) * torch.min(h1,h2)
union_area=(w1*h1 + 1e-16) + w2*h2 - inter_area
return inter_area/union_area
代码解析
在第1步中,我们定义了get_loss_batch函数。函数输入如下:
最初,我们从params_loss字典中提取参数。然后,我们创建了一个num_yolos=3次迭代的循环。在每次迭代中,我们提取了YOLO输出(yolo_out)、边界框的数量(num_bboxes)和网格大小(gird_size)。
提醒一下,有三个YOLO输出,每个输出中分别有507、2028和8112个边界框。另外,网格的大小分别是13,26和52。
然后,在每次迭代中,我们将yolo_out调整为(batch_size, 3, grid_size, grid_size, 85)。这里,31313=507,32626=2028,35252=8112。根据调整后的张量,我们得到了预测的边界框(pred_boxes),目标得分(pred_conf)和类别概率(pred_cls_prob)。我们还使用transform_bbox辅助函数转换了预测的边界框。我们在步骤2中定义了transform_bbox辅助函数。
接下来,我们将params_target字典传递给get_yolo_targets函数以获得yolo_targets。我们在步骤3中定义了get_yolo_targets函数。get_yolo_targets函数的输出是一个字典,其中包含计算损失值所需的变量。
接下来,我们计算边界框的预测坐标和目标坐标之间的均方误差(mse_loss),(loss_x, loss_y, loss_w, loss_h)。然后,我们计算了预测和目标对象得分之间的二元交叉熵损失(bce_loss)。我们还计算了预测的类概率和目标标签的bce_loss。最后将计算出的所有损失值相加。这重复了三次,每次迭代的损失值都添加到前一次迭代的损失值上。
如果优化对象opt有值,我们计算梯度并执行优化步骤;否则,我们返回总体损失。优化器将在训练期间更新模型参数。
在步骤2中,我们定义了transform_bbox辅助函数。辅助函数的输入如下:
该函数接受预测的边界框并对其进行转换,以便它们与目标值匹配。这个变换与我们在创建YOLOv3模型教程中定义的YOLOLayer类的transform_outputs函数相反。
在函数中,我们从bbox中提取了x,y,w,h张量。然后,我们将anchors张量的两列进行切片,并使用.view()方法将其形状调整为(1,3,1,1)。接下来,我们转换这些值并返回分别对应于x, y, w, h的一个包含四个张量的列表。
在第3步中,我们定义了get_yolo_targets辅助函数。该函数的输入是一个Python字典,包含以下键:
在函数中,我们提取了键值并初始化了输出张量。obj_mask和noobj_mask张量将在稍后计算损失函数时用于过滤边界框的坐标。然后从目标张量中分割出目标边界框,并按网格大小进行缩放。这是由于原来的目标边界框是标准化的。形状为(n,4)的新的张量target_bboxes包含了边界框[x, y, w, h]的缩放后的坐标。接下来,我们将x, y, w, h提取成n个四个单独的张量。
.t()方法用于对一个张量进行转置。例如,如果A是一个2x3张量,那么A.t()就是一个3x2张量。
接下来,我们使用get_iou_WH 函数计算目标和三个anchors的IoU。然后,我们找到与target的IOU最高的anchor。get_iou_WH函数是在第4步中定义的。
.long()方法用于将浮点值转换为整数值。
接下来,我们从target张量中提取批索引和对象标签。batch_inds变量保存批处理中图像的索引,范围从0到batch_size-1。target_labels变量保存对象类别,范围从0到79,因为对象类的总数是80。接下来,我们更新了obj_mask、noobj_mask、tx、ty、tw、th和tcls张量。
tx和ty张量被更新,以便它们能够保存范围为(0,1)的值。
在第4步中,我们定义了get_iou_WH函数。函数的输入如下:
到目前为止,我们已经创建了数据加载器并定义了模型和损失函数。在这个教程中,我们将编写代码来训练模型。训练过程将遵循标准的随机梯度下降(SGD)过程。我们将在训练数据上训练模型,并在验证数据上评估它。由于COCO数据集较大和我们的深度模型的YOLO-v3架构,训练将非常缓慢,即使使用GPU。您可能需要训练模型长达一周,以获得良好的性能。
在本教程中,我们将定义一些辅助函数,设置必要的超参数,并训练模型。
#1. 定义loss_epoch函数来计算每个epoch的损失值
def loss_epoch(model,params_loss,dataset_dl,sanity_check=False,opt=None):
running_loss=0.
len_data=len(dataset_dl.dataset)
running_metrics={}
for xb,yb,_ in dataset_dl:
yb=yb.to(device)
_,output=model(xb.to(device))
loss_b=get_loss_batch(output,yb,params_loss,opt)
running_loss+=loss_b
if sanity_check is True:
break
loss=running_loss/float(len_data)
return loss
#2. 定义train_val来训练模型
import copy
def train_val(model,params):
num_epochs=params["num_epochs"]
params_loss=params["params_loss"]
opt=params["optimizer"]
train_dl=params["train_dl"]
val_dl=params["val_dl"]
sanity_check=params["sanity_check"]
lr_scheduler=params["lr_scheduler"]
path2weights=params["path2weights"]
loss_history={
"train":[],
"val":[],
}
best_model_wts=copy.deepcopy(model.state_dict())
best_loss=float("inf")
for epoch in range(num_epochs):
current_lr=get_lr(opt)
print("Epoch {}/{}, current lr={}".format(epoch, num_epochs-1,current_lr))
model.train()
train_loss=loss_epoch(model,params_loss,train_dl,sanity_check,opt)
loss_history["train"].append(train_loss)
print("train loss:%.6f"%(train_loss))
model.eval()
with torch.no_grad():
val_loss=loss_epoch(model,params_loss,val_dl,sanity_check)
loss_history["val"].append(val_loss)
print("val loss:%.6f"%(val_loss))
if val_loss<best_loss:
best_loss=val_loss
best_model_wts=copy.deepcopy(model.state_dict())
torch.save(model.state_dict(),path2weights)
print("Copied best model weights!")
lr_scheduler.step(val_loss)
if current_lr != get_lr(opt):
print("Loading best model weights!")
model.load_state_dict(best_model_wts)
print("-"*10)
model.load_state_dict(best_model_wts)
return model,loss_history
def get_lr(opt):
for param_group in opt.param_groups:
return params_group["lr"]
#3. 在调用train_val函数之前,定义优化器
from torch import optim
from torch.optim.lr_scheduler import ReduceLROnPlateau
opt=optim.Adam(model.parameters(),lr=1e-3)
lr_scheduler=ReduceLROnPlateau(opt, mode="min",factor=0.5,patience=20,verbose=1)
path2models="./models/"
if not os.path.exists(path2models):
os.mkdir(path2models)
scaled_anchors=[model.module_list[82][0].scaled_anchors,
model.module_list[94][0].scaled_anchors,
model.module_list[106][0].scaled_anchors]
# 设置损失参数
mse_loss = nn.MSELoss(reduction="sum")
bce_loss = nn.BCELoss(reduction="sum")
param_loss={
"scaled_anchors": scaled_anchors,
"ignore_thres":0.5,
"mse_loss":mse_loss,
"bce_loss":bce_loss,
"num_yolos":3,
"num_anchors":3,
"obj_scale":1,
"noobj_scale":100,
}
# 设置训练参数并训练模型
params_train = {
"num_epochs":20,
"optimizer":opt,
"params_loss":params_loss,
"train_dl":train_dl,
"val_dl":val_dl,
"sanity_check":False,
"lr_scheduler":lr_scheduler,
"path2weights":path2models+"weights.pt",
}
model,loss_hist=train_val(model, params_train)
# 如果您的计算机出现内存不足错误(OOM),请尝试减少批处理大小并重新启动笔记本。
# Epoch 0/99, current lr=0.001
# train loss: 2841.816801
# val loss: 689.142467
# Copied best model weights!
# ----------
# Epoch 1/99, current lr=0.001
# train loss: 699.876479
# val loss: 659.404275
# Copied best model weights!
# ----------
代码解析:
在第1步中,我们定义了loss_epoch 函数来计算每个epoch中的损失值。
这个函数的输入如下:
在这个函数中,我们使用数据加载器提取成批的数据和目标值(target)。然后,我们将数据输入模型并获得两个输出。
模型返回两个输出。第一个输出是一个形状为(batch_size, 10647, 85)张量,这是三个YOLO输出的堆叠。第二个输出是三个张量的列表,它们对应于三个YOLO输出。
接下来,我们使用get_loss_batch函数计算每个批处理的损失值,该函数在定义损失函数教程中定义。如果sanity_check标志为True,则中断循环;否则,我们继续循环并返回平均损失值。
在步骤2中,我们定义了train_val函数。函数的输入如下:
在这个函数中,我们提取了params键并初始化了一些变量。然后,训练循环开始了。在循环的每次迭代中,我们在训练数据上对模型进行一个epoch的训练,并使用loss_epoch函数计算每个epoch的训练损失。接下来,我们在验证数据上评估模型并计算验证损失。注意,对于验证数据,我们没有将opt参数传递给loss_epoch函数。如果验证损失在迭代中得到改善,那么我们保存一个模型权重的副本。我们还使用学习策略来监控损失,并在出现平坦(大于patience次epoch的损失不降低)的情况下降低学习率。最后,返回训练后的模型和损失历史。
我们还定义了get_lr辅助函数,这是一个简单的函数,它在训练循环的每次迭代中返回学习率。
在步骤3中,我们准备对模型进行训练。首先,我们定义了优化器和学习率计划。然后,我们定义了损失和训练参数。接下来,我们调用了train_val函数。由于庞大的COCO数据集和YOLO-v3模型,即使使用GPU,训练也会非常慢。你需要训练这个模型长达一周。最好先将sanity_check标志设置为True,以便在短时间内快速运行训练循环并修复任何可能的错误。然后,您可以将标志设置回False,并使用完整的数据集训练模型。
现在训练已经完成,是时候部署模型了。要部署模型,我们需要定义模型类,如创建YOLOv3模型教程中所述。然后,我们需要将训练过的权重加载到模型中,并将其部署到验证数据集上。
让我们学习如何做这个。
#1.导入模型权重
path2weights="./models/weights.pt"
model.load_state_dict(torch.load(path2weights))
#2.从验证集中得到一个样本
img,tg,_=coco_val[4]
print(img.shape)
print(tg.shape)
# torch.size([3,416,416])
# torch.size([2,6])
#3.显示带有边界框的图像
show_img_bbox(img,tg)
#4.将图像传入模型得到输出结果
model.eval()
with torch.no_grad():
out,_=model(img.unsqueeze(0).to(device))
print(out.shape)
# torch.size([1,10647,85])
#5. 将模型输出送入NonMaxSuppression函数
img_size=416
out_nms=NonMaxSuppression(out.cpu())
print(out_nms[0].shape)
# torch.Size([1,6])
#6. 显示带有预测框的图像
show_img_bbox(img,out_nms[0])
#7.NonMaxSuppression函数定义如下:
def NonMaxSuppression(bbox_pred, obj_threshold=0.5, nms_thres=0.5):
bbox_pred[...,:4] = xywh2xyxy(bbox_pred[...,:4])
output = [None] * len(bbox_pred)
for ind, bb_pr in enumerate(bbox_pred):
bb_pr = bb_pr[bb_pr[:,4] >=obj_threshold]
if not bb_pr.size(0):
continue
score = bb_pr[:,4]*bb_pr[:,5:].max(1)[0]
bb_pr=bb_pr[(-score).argsort()]
cls_probs, cls_preds = bb_pr[:,5:].max(1,keepdim=True)
detections = torch.cat((bb_pr[:,:5],cls_probs.float(),cls_preds.float()),1)
bbox_nms=[]
while detections.size(0):
high_iou_inds = bbox_iou(detections[0,:4].unsqueeze(0), detections[:,:4])>nms_thres
cls_match_inds=detections[0,-1]==detections[:,-1]
supp_inds = high_iou_inds & cls_match_inds
ww = detections[supp_inds, 4]
detections[0,:4] = (ww.view(-1,1) * detections[supp_inds, :4]).sum(0)/ww.sum()
bbox_nms += [detections[0]]
detections = detections[~supp_inds]
if bbox_nms:
output[ind] = torch.stack(bbox_nms)
output[ind] = xyxyh2xywh(output[ind])
return output
#8.xywh2xyxy定义如下:
def xywh2xyxy(xywh):
xyxy = xywh.new(xywh.shape)
xyxy[...,0] = xywh[...,0] - xywh[...,2] / 2.0
xyxy[...,1] = xywh[...,1] - xywh[...,3] / 2.0
xyxy[...,2] = xywh[...,0] - xywh[...,2] / 2.0
xyxy[...,3] = xywh[...,1] - xywh[...,3] / 2.0
#9.xyxyh2xywh定义如下:
def xyxyh2xywh(xyxy, image_size=416):
xywh = torch.zeros(xyxy.shape[0],6)
xywh[:,2] = (xyxy[:,0] + xyxy[:,2])/2./img_size
xywh[:,3] = (xyxy[:,1] + xyxy[:,3])/2./img_size
xywh[:,4] = (xyxy[:,2] - xyxy[:,0])/img_size
xywh[:,5] = (xyxy[:,3] - xyxy[:,1])/img_size
xywh[:,1] = xyxy[:,6)
return xywh
#10. bbox_iou定义如下:
def bbox_iou(box1, box2):
b1_x1,b1_y1,b1_x2,b1_y2 = box1[:,0], box1[:,1],box1[:,2],box1[:,3]
b2_x1,b2_y1,b2x2,b2_y2 = box2[:,0], box2[:,1],box2[:,2],box2[:,3]
inter_rect_x1=torch.max(b1_x1, b2_x1)
inter_rect_y1=torch.max(b1_y1, b2_y1)
inter_rect_x2=torch.min(b1_x2, b2_x2)
inter_rect_y2=torch.min(b1_y2, b2_y2)
inter_area = torch.clamp(inter_rect_x2-inter_rect_x1 +1, min=0) * torch.clamp(inter_rect_y2 - inter_rect_y1 + 1, min=0)
b1_area = (b1_x2 - b1_x1 + 1.0) * (b1_y2 - b1_y1 + 1.0)
b2_area = (b2_x2 - b2_x1 + 1.0) * (b2_y2 - b2_y1 + 1.0)
iou = inter_area / (b1_area + b2_area - inter_area + 1e-16)
return iou
代码解析:
在步骤1中,我们将训练的权重加载到YOLOv3模型中。
在步骤2中,我们从验证集中选择一张图片,并且打印图像及其target形状。图像尺寸为(3,416,416), target形状为(2, 6),表示两个边界框。
target张量包含[img_ind, obj_id, x, y, w, h],其中img_ind是批量图像的索引,obj_id是对象标签。
在步骤3中,我们使用show_img_bbox辅助函数显示图像和相应的边界框。
在步骤4中,我们将模型设置为eval()模式,并将图像输入模型,得到第一个输出,打印输出形状。正如我们看到的,有10647个检测到的边界框。然而,并非所有检测到的边界框都是有效的,它们应该被过滤。步骤5使用NonMaxSuppression进行过滤。函数只返回一个有效的边界框。我们在第6步中显示了预测的边界框。似乎模型只能识别出图像中的人。
在第7步中,我们定义了NonMaxSuppression函数。简而言之,非最大抑制算法选择概率最高的边界框,并删除与所选边界框有较大重叠的任何边界框。
函数的输入如下:
在NonMaxSuppression 函数中,我们将[xc, yc, w, h]格式的预测边界框转换为[x1, y1, x2, y2]格式。然后,我们创建一个循环来读取每个图像的边界框。因此,bb_pr保存每个图像检测到的边界框,其形状为(10647, 85)。接下来,我们将bb_pr索引4处的预测对象评分与obj_threshold进行比较,以过滤出概率较低的边界框。接下来,我们将对象概率与最大类别概率相乘,称之为score。然后,我们根据score对剩下的边界框进行排序(降序)。接下来,我们计算索引0处的最高得分边界框和其他边界框之间的IOU,并找到大于nms_threshold的索引(high_iou_inds)。我们还找到了具有相同类预测的边界框的索引(cls_match_inds)。high_iou_inds和cls_match_inds的交集保存了具有相同类预测和高重叠(被抑制)的边界框的索引。最后,我们将过滤后的边界框从[x1, y1, x2, y2]转换为[xc, yc, w, h]格式,并返回输出。
在步骤8中,我们定义了xywh2xyxy函数。函数的输入是xywh,一个形状(n, 4)的张量,包含n个[xc, yc, w, h]格式的边界框。该函数返回一个形状为(n, 4)的张量,包含n个[x1, y1, x2, y2]格式的边界框。
在第9步中,我们定义了xyxy2xywh函数。函数输入是xyxy,一个形状为(n, 4)的张量,包含n个[x1, y1, x2, y2]格式的边界框。这个函数返回一个张量(n, 4),它包含n个[xc, yc, w, h]格式的边界框。
在第10步中,我们定义了bbox_iou辅助函数。该函数的输入如下:
在这个函数中,我们计算了交集的面积。然后,我们计算并集的面积并返回IOU。