%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
这是一个比较小的数据集,方便调试测试使用,利于学习。