文章目录
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torchvision
import torch
import os
1. 多尺度边界框检测
# 测试图像
# imagepath = 'E:\学习\机器学习\数据集\VOC2012\VOCdevkit\VOC2012\JPEGImages\\2007_001423.jpg'
imagepath = 'E:\学习\机器学习\数据集\VOC2012\VOCdevkit\VOC2012\JPEGImages\\2007_001526.jpg'
# 读取并显示图像
image = plt.imread(imagepath)
h, w = image.shape[:2]
定义一些列的显示函数
# 功能: 指定输入图像、尺度列表和宽高比列表,然后此函数将生成以每个像素为中心具有不同形状的锚框,返回所有的锚框
def multibox_prior(data, sizes, ratios):
device = data.device
size_tensor = torch.tensor(sizes, device=device)
ratio_tensor = torch.tensor(ratios, device=device)
# print(data.shape)
# 获取图像宽高
img_height, img_width = data.shape[-2:]
# 避免anchor太密集,只挑选特定的boxes
boxes_per_pixel = len(sizes) + len(ratios) - 1
# 获取每个像素的中心点
steps_h = 1.0 / img_height # 高度步长
steps_w = 1.0 / img_width # 宽度步长
# 根据图像像素点位置 * 步长 来实现归一化处理,使得图像尺寸计算为1
# 0.5 指的是像素点中心位置的偏移量
center_h = (torch.arange(img_height, device=device) + 0.5) * steps_h
center_w = (torch.arange(img_width, device=device) + 0.5) * steps_w
# print(center_h.shape, center_w.shape) # torch.Size([333]) torch.Size([500])
# 根据步长位置构建每个像素点的坐标信息
shift_y, shift_x = torch.meshgrid(center_h, center_w)
# print(shift_y.shape, shift_x.shape) # torch.Size([333, 500]) torch.Size([333, 500])
# 分别转换成列表,方便拼接,其中(shift_x, shift_y)就代表了图像中全部像素点的中心坐标
shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)
# 现在对(shift_x, shift_y)进行拼接,方便一会转换成左上角与右下角的坐标格式,所以需要设置两组坐标
# 其中参数dim=1表示的是对列进行拼接
center_point = torch.stack([shift_x, shift_y, shift_x, shift_y], dim=1)
# 由于每个像素点会生成(n+m−1)个anchor,所以需要对坐标列表重复5次
# repeat_interleave函数是对每一行分别进行先复制; repeat函数是对每一块分别进行复制
center_point = center_point.repeat_interleave(boxes_per_pixel, dim=0)
# print(center_point)
# 现在构造出了中心点坐标,接着需要构造偏移信息列表,使中心坐标+偏移量就转换成转换成左上角与右下角的坐标格式
# 其中: anchor_w = s * sqrt(w * h * r) anchor_h = s * sqrt(w * h / r)
# 这样使得 anchor_w / anchor_h = r anchor_w * anchor_h = (ws)*(hs)
# anchor_w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]), size_tensor[0] * torch.sqrt(ratio_tensor[1:]))) \
# * math.sqrt(img_width * img_height)
# anchor_h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]), size_tensor[0] / torch.sqrt(ratio_tensor[1:]))) \
# * math.sqrt(img_width * img_height)
# anchor_w, anchor_h:
# tensor([306.0331, 204.0221, 102.0110, 432.7961, 216.3981])
# tensor([306.0331, 204.0221, 102.0110, 216.3981, 432.7962])
# 现在得到的5个anchor是在图像上的像素大小,需要同样对其进行归一化操作
# 而另一种方法是:
# 其中size值的是相比原图的大小, ratio值的宽高比
anchor_w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]), size_tensor[0] * torch.sqrt(ratio_tensor[1:]))) \
* img_height / img_width # 由于图像一般是矩形的,为了显示出是正方形,这里需要对宽度做一个缩放因子
anchor_h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]), size_tensor[0] / torch.sqrt(ratio_tensor[1:])))
# anchor_w, anchor_h:
# tensor([0.4995, 0.3330, 0.1665, 0.7064, 0.3532])
# tensor([0.7500, 0.5000, 0.2500, 0.5303, 1.0607])
# print(anchor_w)
# print(anchor_h)
# 获得偏移量
anchor_offset = torch.stack((-anchor_w, -anchor_h, anchor_w, anchor_h))
anchor_offset = anchor_offset.T.repeat(img_height * img_width, 1) / 2 # 先转置再按偏移块来重复
# 更加中心点坐标与偏移量,获取anchor
anchors = center_point + anchor_offset
return anchors.unsqueeze(0)
# 将边界框 (左上x, 左上y, 右下x, 右下y) 格式转换成 matplotlib 格式:
# ((左上x, 左上y), 宽, 高)
def bbox_to_rect(bbox, color, linewidth=2):
# 注意,这里输入的是单个边界框
xy = (bbox[0], bbox[1]) # 左上角坐标
width = bbox[2]-bbox[0] # 右下角的x坐标 - 左上角的x坐标
height = bbox[3]-bbox[1] # 右下角的y坐标 - 左上角的y坐标
# 返回matplotlib 的边界框格式
# fill=False: 取消填充功能,否则不是边界框而是一个色块
# edgecolor: 边界框颜色
# linewidth: 边界框的宽度
return plt.Rectangle(xy, width, height, fill=False, edgecolor=color, linewidth=linewidth)
# 功能: 显示一个像素点上的所有边界框(这里设置了一个像素点上会有5个anchor)
def show_bboxes(axes, bboxes, labels=None, colors=None):
# 如果没有传入颜色设置,这里会进行颜色一个初始化设置
if colors is None:
colors = ['blue', 'red', 'green', 'gray', 'pink']
# 如果没有传入标签设置,这里会进行标签一个初始化设置
if labels is None:
labels = [i for i in range(len(bboxes))]
# print(labels)
# 以增加补丁的方式在原图上绘制矩形框
for i, bbox in enumerate(bboxes):
color = colors[i % len(colors)]
rect = bbox_to_rect(bbox, color) # 循环采用列表中的5种颜色
# 增加矩形框补丁
axes.add_patch(rect)
# 增加文本补丁
axes.text(rect.xy[0], rect.xy[1], labels[i], fontsize=8, color='white',
va='center', ha='center', bbox=dict(facecolor=color, edgecolor="black"))
def display_anchors(fmap_w, fmap_h, s):
# d2l.set_figsize()
# 前两个维度上的值不影响输出
fmap = torch.zeros((1, 10, fmap_h, fmap_w))
anchors = multibox_prior(fmap, sizes=s, ratios=[1, 2, 0.5])
bbox_scale = torch.tensor((w, h, w, h))
show_bboxes(plt.imshow(image).axes, anchors[0] * bbox_scale)
使用不同的尺度构建anchor
display_anchors(fmap_w=4, fmap_h=4, s=[0.15])
torch.Size([1, 10, 4, 4])
tensor([0.1500, 0.2121, 0.1061])
tensor([0.1500, 0.1061, 0.2121])
display_anchors(fmap_w=2, fmap_h=2, s=[0.4])
torch.Size([1, 10, 2, 2])
tensor([0.4000, 0.5657, 0.2828])
tensor([0.4000, 0.2828, 0.5657])
display_anchors(fmap_w=1, fmap_h=1, s=[0.8])
torch.Size([1, 10, 1, 1])
tensor([0.8000, 1.1314, 0.5657])
tensor([0.8000, 0.5657, 1.1314])
假设此处的 c 张特征图是 CNN 基于输入图像的正向传播算法获得的中间输出。 既然每张特征图上都有 hw 个不同的空间位置,那么相同空间位置可以看作含有 c 个单元。感受野的定义,特征图在相同空间位置的 c 个单元在输入图像上的感受野相同: 它们表征了同一感受野内的输入图像信息。 因此,我们可以将特征图在同一空间位置的 c 个单元变换为使用此空间位置生成的 a 个锚框类别和偏移量。 本质上,我们用输入图像在某个感受野区域内的信息,来预测输入图像上与该区域位置相近的锚框类别和偏移量
当不同层的特征图在输入图像上分别拥有不同大小的感受野时,它们可以用于检测不同大小的目标。 例如,我们可以设计一个神经网络,其中靠近输出层的特征图单元具有更宽的感受野,这样它们就可以从输入图像中检测到较大的目标。
2. 目标检测数据集
这里代码提供的下载数据集方式我好像无法成功,只能手动下载,同时自己实现一下加载此数据集
from torch.utils.data import DataLoader, Dataset
# 一个用于加载香蕉检测数据集的自定义数据集
class BananasDataset(Dataset):
def __init__(self, is_train):
# 加载图像与标签信息
self.images, self.labels = self.read_data_bananas(is_train)
# 根据训练集或测试集打印出数量
print('read ' + str(len(self.images)) + (' training examples' if is_train else ' validation examples'))
def __getitem__(self, idx):
# 对每一张图像的处理, 这里转变为了浮点数
return (self.images[idx].float(), self.labels[idx])
def __len__(self):
# 返回图像数量
return len(self.images)
def read_data_bananas(self, is_train=True):
# 保存测试集与训练集的图像是一致的
datapath = 'E:\学习\机器学习\数据集\\banana-detection'
csv_file = os.path.join(datapath, 'bananas_train' if is_train else 'bananas_val', 'label.csv')
csv_data = pd.read_csv(csv_file)
csv_data = csv_data.set_index('img_name')
images, targets = [], []
# 迭代每一行,添加信息
for img_name, target in csv_data.iterrows():
# 每张图像的路径
imagepath = os.path.join(datapath, 'bananas_train' if is_train else 'bananas_val', 'images', img_name)
# 读取图片数据, 类似于Image.open(x).convert('RGB')
images.append(torchvision.io.read_image(imagepath))
# target中包含了类别与边界框信息(左上角与右下角组成),eg:[0, 104, 20, 143, 58]组成的一个列表
targets.append(list(target))
# 对于边界框与类别信息在第2个维度前做了增维处理, 并且进行归一化处理, 图像的尺寸是256
# labels.shape: torch.Size([1000, 1, 5])
return images, torch.tensor(targets).unsqueeze(1) / 256
batch_size = 16
# 对于测试集,无须按随机顺序读取
train_iter = DataLoader(BananasDataset(is_train=True), batch_size, shuffle=True)
val_iter = DataLoader(BananasDataset(is_train=False), batch_size)
read 1000 training examples
read 100 validation examples
展示 10 幅带有真实边界框的图像
def bbox_to_rect(bbox, color, linewidth=2):
# 注意,这里输入的是单个边界框
xy = (bbox[0], bbox[1]) # 左上角坐标
width = bbox[2] - bbox[0] # 右下角的x坐标 - 左上角的x坐标
height = bbox[3] - bbox[1] # 右下角的y坐标 - 左上角的y坐标
# 返回matplotlib 的边界框格式
# fill=False: 取消填充功能,否则不是边界框而是一个色块
# edgecolor: 边界框颜色
# linewidth: 边界框的宽度
return plt.Rectangle(xy, width, height, fill=False, edgecolor=color, linewidth=linewidth)
需要注意,这里对图像数值进行了归一化处理; 而且label的信息也是经过相对处理的,所以一会如果想要显示label需要对其乘上原图像大小
images, labels = next(iter(train_iter))
images = images.permute(0, 2, 3, 1) / 255
images.shape
torch.Size([16, 256, 256, 3])
根据图像与标签显示数据
index = 1
plt.figure(figsize=(9, 8), dpi=100)
for image, label in zip(images, labels):
plt.subplot(4, 4, index)
plt.axis('off')
# 显示图像
fig = plt.imshow(image)
# 注意这里的label是归一化处理后的,需要乘上原数值
bbox = label[0,1:] * 255
# 添加矩形框
rect = bbox_to_rect(bbox, 'w')
# rect = plt.Rectangle((bbox[0], bbox[1]), bbox[2] - bbox[0], bbox[3] - bbox[1],
# fill=False, edgecolor='red', linewidth=2)
fig.axes.add_patch(rect)
index += 1
这是一个比较小的数据集,方便调试测试使用,利于学习。