目标检测数据集#
目标检测领域没有像MNIST和Fashion-MNIST那样的小数据集。 为了快速测试目标检测模型,我们收集并标记了一个小型数据集。 首先,我们拍摄了一组香蕉的照片,并生成了1000张不同角度和大小的香蕉图像。 然后,我们在一些背景图片的随机位置上放一张香蕉的图像。 最后,我们在图片上为这些香蕉标记了边界框。
下载数据集#
包含所有图像和CSV标签文件的香蕉检测数据集可以直接从互联网下载。
%matplotlib inline
import os
import pandas as pd
import torch
import torchvision
import matplotlib.pyplot as plt # 用于绘制图像
import matplotlib.patches as patches # 用于在图像上绘制矩形边界框
import requests
import zipfile
import hashlib
# 文件的 URL
url = "http://d2l-data.s3-accelerate.amazonaws.com/banana-detection.zip"
# 下载到本地的文件路径
local_filename = "../raw/data/banana-detection.zip"
# 解压缩的目录
extract_to = "../raw/data/"
# 下载文件
def download_file(url, local_filename,sha1_hash):
os.makedirs(os.path.dirname(local_filename), exist_ok=True)
try:
sha1 = hashlib.sha1()
with open(local_filename, 'rb') as f:
while True:
data = f.read(1048576)
if not data:
break
sha1.update(data)
if sha1.hexdigest() == sha1_hash:
print(f"文件已存在: {local_filename}")
return local_filename
except Exception as e:
pass
with requests.get(url, stream=True) as r:
r.raise_for_status()
with open(local_filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
print(f"文件已下载: {local_filename}")
return local_filename
# 解压文件
def unzip_file(zip_file, extract_to):
with zipfile.ZipFile(zip_file, 'r') as zip_ref:
zip_ref.extractall(extract_to)
# 返回解压后的文件和目录列表
#extracted_paths = [os.path.join(extract_to, name) for name in zip_ref.namelist()]
print("文件已解压到: ../raw/data/banana-detection/")
#return extracted_paths
download_file(url, local_filename,"5de26c8fce5ccdea9f91267273464dc968d20d72")
unzip_file(local_filename, extract_to)
文件已存在: ../raw/data/banana-detection.zip
文件已解压到: ../raw/data/banana-detection/
读取数据集#
通过read_data_bananas
函数,我们读取香蕉检测数据集。
该数据集包括一个的CSV文件,内含目标类别标签和位于左上角和右下角的真实边界框坐标。
def read_data_bananas(is_train=True):
"""读取香蕉检测数据集中的图像和标签"""
csv_fname = os.path.join("../raw/data/banana-detection", 'bananas_train' if is_train
else 'bananas_val', 'label.csv')
csv_data = pd.read_csv(csv_fname)
csv_data = csv_data.set_index('img_name')
images, targets = [], []
for img_name, target in csv_data.iterrows():
images.append(torchvision.io.read_image(
os.path.join("../raw/data/banana-detection", 'bananas_train' if is_train else
'bananas_val', 'images', f'{img_name}')))
# 这里的target包含表格中的(类别,左上角x,左上角y,右下角x,右下角y)数据
# 类别:类别标签(在这个例子中,只有一个类别香蕉,所以类别是0)。
# x1, y1:边界框左上角的坐标。
# x2, y2:边界框右下角的坐标。
# 而images里面是图片的tensor数据,与target索引相对应,单个图片为torch.Size([3, 256, 256])
targets.append(list(target))
# 把list的targets转换成tensor格式
# 然后用unsqueeze在1维(下标从0开始)的地方加一个维度
# 对坐标进行归一化处理,对所有数据进行除以256,数据被归一化到 [0, 1] 之间
return images, torch.tensor(targets).unsqueeze(1) / 256
通过使用read_data_bananas
函数读取图像和标签,以下BananasDataset
类别将允许我们创建一个自定义Dataset
实例来加载香蕉检测数据集。
class BananasDataset(torch.utils.data.Dataset):
"""一个用于加载香蕉检测数据集的自定义数据集"""
def __init__(self, is_train):
self.features, self.labels = read_data_bananas(is_train) # 获取特征和标签
print('read ' + str(len(self.features)) + (f' training examples' if
is_train else f' validation examples'))
def __getitem__(self, idx): # 获取给定索引的数据样本
return (self.features[idx].float(), self.labels[idx])
def __len__(self): # 返回数据集的大小
return len(self.features)
最后,我们定义load_data_bananas
函数,来为训练集和测试集返回两个数据加载器实例。对于测试集,无须按随机顺序读取它。
def load_data_bananas(batch_size):
"""加载香蕉检测数据集"""
train_iter = torch.utils.data.DataLoader(BananasDataset(is_train=True),
batch_size, shuffle=True) # 随机加载测试数据集
val_iter = torch.utils.data.DataLoader(BananasDataset(is_train=False),
batch_size)
return train_iter, val_iter
让我们读取一个小批量,并打印其中的图像和标签的形状。 图像的小批量的形状为(批量大小、通道数、高度、宽度),看起来很眼熟:它与我们之前图像分类任务中的相同。 标签的小批量的形状为(批量大小,\(m\),5),其中\(m\)是数据集的任何图像中边界框可能出现的最大数量。
小批量计算虽然高效,但它要求每张图像含有相同数量的边界框,以便放在同一个批量中。 通常来说,图像可能拥有不同数量个边界框;因此,在达到\(m\)之前,边界框少于\(m\)的图像将被非法边界框填充。 这样,每个边界框的标签将被长度为5的数组表示。 数组中的第一个元素是边界框中对象的类别,其中-1表示用于填充的非法边界框。 数组的其余四个元素是边界框左上角和右下角的(\(x\),\(y\))坐标值(值域在0~1之间)。 对于香蕉数据集而言,由于每张图像上只有一个边界框,因此\(m=1\)。
batch_size, edge_size = 32, 256
train_iter, _ = load_data_bananas(batch_size)
batch = next(iter(train_iter)) # 里面分别是特征值和标签
batch[0].shape, batch[1].shape
read 1000 training examples
read 100 validation examples
(torch.Size([32, 3, 256, 256]), torch.Size([32, 1, 5]))
演示#
让我们展示10幅带有真实边界框的图像。 我们可以看到在所有这些图像中香蕉的旋转角度、大小和位置都有所不同。 当然,这只是一个简单的人工数据集,实践中真实世界的数据集通常要复杂得多。
首先我们定一下图像函数和边界框函数,抛弃d2l这个包来更深层次的理解下代码
# 显示图像的函数
# 传入的参数分别是:待显示的图像张量,网格的行数,列数,控制图像显示的大小
def show_images(imgs, num_rows, num_cols, scale=1.5):
figsize = (num_cols * scale, num_rows * scale) # 根据行数和列数确定整个图像网格的大小
# 创建一个指定大小的网格,2行每行5个图片,figsize表示会创建一个宽度为 10 英寸、高度为 5 英寸的图像
# 假设你有一个 2x5 的网格布局,并使用 figsize=(10, 5),那么每个子图的大小大致为 2 英寸宽、2.5 英寸高
_, axes = plt.subplots(num_rows, num_cols, figsize=figsize)
# 当你使用 plt.subplots(num_rows, num_cols) 创建多个子图时,axes 通常是一个二维的 NumPy 数组,其中每个元素都是一个子图的 Axes 对象。
# 比如,假设你创建了一个 2x5 的网格,axes 的形状会是 (2, 5),表示有 2 行、5 列的子图。
# flatten() 方法会将二维数组转换为一维数组。例如,一个形状为 (2, 5) 的数组会被展平为一个形状为 (10,) 的一维数组。
axes = axes.flatten() # 将 axes 数组展平,以便能够一一对应地操作每个子图
for ax, img in zip(axes, imgs):
ax.imshow(img)
ax.axis('off') # 用于关闭当前子图 ax 的坐标轴,使得图像显示时没有边框、刻度线和标签。
return axes
# 显示边界框的函数
def show_bboxes(ax, bboxes, color='w'):
for bbox in bboxes:
rect = patches.Rectangle(
(bbox[0], bbox[1]), bbox[2] - bbox[0], bbox[3] - bbox[1],
linewidth=2, # 边框宽度
edgecolor=color, # 边框颜色
facecolor='none') # 矩形内部颜色填充,如果有就是实心的白框
ax.add_patch(rect)
# 显示前10张图片和对应的边界框
# 将图片调整为 [batch, height, width, channels] 格式,并归一化
# 为什么要使用 permute?
# 如果你有一个形状为 [32, 3, 256, 256] 的张量(32 张 3 通道的 256x256 图像),经过 permute(0, 2, 3, 1) 之后,张量的形状将变为 [32, 256, 256, 3]。
# 在很多图像处理库(如 Matplotlib)中,图像通常表示为 [height, width, channels] 这种格式。因此,在将张量传递给这些库进行可视化时,通常需要先使用 permute 将张量的维度从 [channels, height, width] 转换为 [height, width, channels]。
imgs = (batch[0][0:10].permute(0, 2, 3, 1)) / 255
# axes = d2l.show_images(imgs, 2, 5, scale=2) # 显示图片
# for ax, label in zip(axes, batch[1][0:10]):
# d2l.show_bboxes(ax, [label[0][1:5] * edge_size], colors=['w']) # 在图片上绘制边界框
# 显示图片
axes = show_images(imgs, 2, 5, scale=2)
# 显示对应的边界框
for ax, label in zip(axes, batch[1][0:10]):
# 提取便捷坐标数据,因为第0个是类别,所以从[1:5]开始取,然后将归一化的坐标转换回实际的像素坐标
bbox = label[0][1:5] * edge_size
show_bboxes(ax, [bbox], color='w')
plt.show()
