物体检测
目标检测和图片分类的区别:
图像分类(Image Classification)
目的:图像分类的目的是识别出图像中主要物体的类别。它试图回答“图像是什么?”的问题。
输出:通常输出是一个标签或一组概率值,表示图像属于各个预定义类别的可能性。例如,对于一张包含猫的图片,分类器可能会输出“猫”这个标签。
应用场景:适用于只需要了解图像整体内容的场景,如识别照片中的动物种类、区分不同的风景类型等。
目标检测(Object Detection)
目的:目标检测不仅需要识别图像中所有感兴趣物体的类别,还需要确定每个物体在图像中的具体位置。它试图回答“图像中有什么?它们在哪里?”的问题。
输出:除了给出物体的类别外,还会输出物体所在的边界框(bounding box),即用矩形框标记出每个物体的位置。例如,在自动驾驶场景下,系统不仅要能识别出行人、车辆等物体,还要精确地定位它们的位置以便做出安全决策。
应用场景:适合于需要知道图像内特定对象位置的情况,比如视频监控、自动驾驶汽车、医学影像分析等领域。
边缘框
- 一个边缘框可以通过 4 个数字定义
- (左上 x,左上 y,右下 x,右下 y)
- (左上 x,左上 y,宽,高)
目标检测数据集
- 每行表示一个物体
- 图片文件名,物体类别,边缘框
- COCO (cocodataset.org)
- 80 物体,330K 图片,1.5M 物体
总结
- 物体检测识别图片里的多个物体的类别和位置
- 位置通常用边缘框表示。
边缘框实现
%matplotlib inline
import torch
from d2l import torch as d2l
d2l.set_figsize()
img = d2l.plt.imread('../img/catdog.jpg')
d2l.plt.imshow(img);
定义在这两种表示法之间进行转换的函数
#@save
def box_corner_to_center(boxes):
"""从(左上,右下)转换到(中间,宽度,高度)"""
x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
cx = (x1 + x2) / 2
cy = (y1 + y2) / 2
w = x2 - x1
h = y2 - y1
boxes = torch.stack((cx, cy, w, h), axis=-1)
return boxes
#@save
def box_center_to_corner(boxes):
"""从(中间,宽度,高度)转换到(左上,右下)"""
cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
x1 = cx - 0.5 * w
y1 = cy - 0.5 * h
x2 = cx + 0.5 * w
y2 = cy + 0.5 * h
boxes = torch.stack((x1, y1, x2, y2), axis=-1)
return boxes
定义图像中狗和猫的边界框
# bbox是边界框的英文缩写
dog_bbox, cat_bbox = [60.0, 45.0, 378.0, 516.0], [400.0, 112.0, 655.0, 493.0]
boxes = torch.tensor((dog_bbox, cat_bbox))
box_center_to_corner(box_corner_to_center(boxes)) == boxes
将边界框在图中画出
#@save
def bbox_to_rect(bbox, color):
# 将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式:
# ((左上x,左上y),宽,高)
return d2l.plt.Rectangle(
xy=(bbox[0], bbox[1]), width=bbox[2]-bbox[0], height=bbox[3]-bbox[1],
fill=False, edgecolor=color, linewidth=2)
fig = d2l.plt.imshow(img)
fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue'))
fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red'));
数据集
李沐老师收集并标记了一个小型数据集,下面首先是下载该数据集:
%matplotlib inline
import os
import pandas as pd
import torch
import torchvision
from d2l import torch as d2l
#@save
d2l.DATA_HUB['banana-detection'] = (
d2l.DATA_URL + 'banana-detection.zip',
'5de26c8fce5ccdea9f91267273464dc968d20d72')
读取香蕉检测数据集
#@save
def read_data_bananas(is_train=True):
"""读取香蕉检测数据集中的图像和标签"""
data_dir = d2l.download_extract('banana-detection')
csv_fname = os.path.join(data_dir, '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(data_dir, 'bananas_train' if is_train else
'bananas_val', 'images', f'{img_name}')))
# 这里的target包含(类别,左上角x,左上角y,右下角x,右下角y),
# 其中所有图像都具有相同的香蕉类(索引为0)
targets.append(list(target))
return images, torch.tensor(targets).unsqueeze(1) / 256
创建一个自定义 Dataseet 实例:
#@save
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)
为训练集和测试集返回两个数据加载器实例
#@save
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
读取一个小批量,并打印其中的图像和标签的形状
batch_size, edge_size = 32, 256
train_iter, _ = load_data_bananas(batch_size)
batch = next(iter(train_iter))
batch[0].shape, batch[1].shape
演示:
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'])
QA 思考
Q1:如果在工业检测中数据集非常小(近百张),除了进行数据增强外,还有什么更好的方法吗?
A1:迁移学习:找一个非常好的,在一个比较大的目标检测数据集上训练的比较好的模型,然后拿过来进行微调。近百张其实也不算小的了,只是说模型训练出来不是很好。数据增强的话,对图像进行处理,对应的框也必须要相应的变化一下,这是比较麻烦的点。
后记
自己实现了一遍,然后也是使用的李沐老师在课件中提供的狗猫图片:
import torch
from matplotlib import pyplot as plt
from PIL import Image
def set_image_display_size():
plt.figure(figsize=(8, 6))
def load_image(img_path):
return Image.open(img_path)
def show_single_image(img):
plt.imshow(img)
plt.axis('on') # 显示坐标轴
plt.show()
def box_corner_to_center(boxes):
x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
cx = (x1 + x2) / 2
cy = (y1 + y2) / 2
w = x2 - x1
h = y2 - y1
boxes = torch.stack((cx, cy, w, h), axis=-1)
return boxes
def box_center_to_corner(boxes):
# 这里向左和向上都是减小的
cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3]
x1 = cx - 0.5 * w
y1 = cy - 0.5 * h
x2 = cx + 0.5 * w
y2 = cy + 0.5 * h
boxes = torch.stack((x1, y1, x2, y2), axis=-1)
return boxes
def bbox_to_rect(bbox, color):
# 将边界框(左上x, 左上y, 右下x, 右下y)格式转换成matplotlib格式:
# ((左上x, 左上y), 宽, 高)
return plt.Rectangle(
xy=(bbox[0], bbox[1]), width=bbox[2] - bbox[0], height=bbox[3] - bbox[1],
fill=False, edgecolor=color, linewidth=2)
if __name__ == "__main__":
# 设置图像显示大小
set_image_display_size()
# 加载图像
img_path = '../image/catdog.jpg' # 确保路径正确
img = load_image(img_path)
show_single_image(img)
# dog, cat 边界框
dog_bbox, cat_bbox = [60.0, 45.0, 378.0, 516.0], [400.0, 112.0, 655.0, 493.0]
# 转换为张量并测试转换函数是否正确
boxes = torch.tensor((dog_bbox, cat_bbox))
converted_boxes = box_center_to_corner(box_corner_to_center(boxes))
# torch.allclose,判断两个张量是否在一定容差范围内相等。
print("转换是否一致:", torch.allclose(converted_boxes, boxes))
# 显示图像并绘制边界框,imshow 将 img 显示在画布上
fig = plt.imshow(img)
# add_patch 是 matplotlib 中用于在坐标轴上添加图形元素(如矩形、圆形等)的函数
fig.axes.add_patch(bbox_to_rect(dog_bbox, 'blue')) # 狗的边界框
fig.axes.add_patch(bbox_to_rect(cat_bbox, 'red')) # 猫的边界框
plt.show()
我在colab上跑了一下下面的代码,发现可以,下载不了的可能需要一点 “魔法”
import hashlib
import os
import tarfile
import zipfile
import pandas as pd
import requests
import torch
import torchvision
from matplotlib import pyplot as plt
"""
这段代码本身并没有包含训练过程。它只是加载了已经标注好的香蕉检测数据集,并可视化了图像及其对应的边界框。
这些边界框信息是预先标注好的(存储在 CSV 文件中),而不是通过模型训练得到的。
read_data_bananas 函数读取数据集中的图像和标签:
图像存储在文件夹中(如 bananas_train/images/)。
标签存储在 CSV 文件中(如 bananas_train/label.csv)。
每个标签包括图像名称、目标类别(香蕉类别的索引为 0)、以及边界框坐标。
zip 结构类似如下:
banana-detection/
├── bananas_train/
│ ├── images/
│ │ ├── image1.jpg
│ │ ├── image2.jpg
│ │ └── ...
│ └── label.csv # 包含每张图像的标注信息(即边界框和类别)
└── bananas_val/
├── images/
│ ├── image1.jpg
│ ├── image2.jpg
│ └── ...
└── label.csv
"""
DATA_HUB = dict()
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/'
DATA_HUB['banana-detection'] = (
DATA_URL + 'banana-detection.zip',
'5de26c8fce5ccdea9f91267273464dc968d20d72')
def download(name, cache_dir=os.path.join('..', 'data')):
"""下载一个DATA_HUB中的文件,返回本地文件名"""
# assert 条件, 错误信息
assert name in DATA_HUB, f"{name} 不存在于 {DATA_HUB}"
"""
url:文件的下载地址。 http://d2l-data.s3-accelerate.amazonaws.com/banana-detection.zip
sha1_hash:文件内容的 SHA-1 校验值,用于验证文件完整性。5de26c8fce5ccdea9f91267273464dc968d20d72
"""
url, sha1_hash = DATA_HUB[name]
"""
递归创建目录:包括所有必要的父目录。
避免重复创建导致的错误:如果目录已经存在,不会抛出异常。
"""
os.makedirs(cache_dir, exist_ok=True)
"""
url.split('/')[-1]:提取 URL 路径的最后一部分(通常是文件名)。
os.path.join(cache_dir, ...):将缓存目录和文件名组合成完整的本地文件路径。
"""
fname = os.path.join(cache_dir, url.split('/')[-1]) # fname = ../data/banana-detection.zip
if os.path.exists(fname): # 文件存在,进入校验流程
# 创建一个 SHA-1 哈希对象,用于计算文件的哈希值。
sha1 = hashlib.sha1()
# 以二进制模式读取文件
with open(fname, 'rb') as f:
while True:
# 每次读取 1MB 数据(1048576 字节),更新哈希对象。
data = f.read(1048576)
# 为空,退出循环
if not data:
break
# 将读取的数据块逐步添加到哈希计算中
sha1.update(data)
"""
计算哈希值:
调用 sha1.hexdigest() 获取最终的 SHA-1 哈希值(40 个字符的十六进制字符串)。
校验哈希值:
将计算得到的哈希值与预期的哈希值 sha1_hash 进行比较。
如果两者相等,说明文件完整且未被篡改,直接返回文件路径(命中缓存)。
否则,继续执行下载逻辑。
"""
if sha1.hexdigest() == sha1_hash:
return fname # 命中缓存
print(f'正在从{url}下载{fname}...')
"""
使用 requests.get 方法发送 HTTP 请求,从远程服务器下载文件。
参数说明:
stream=True:以流式方式下载文件,避免一次性加载整个文件到内存中。
verify=True:启用 SSL 证书验证,确保安全连接。
"""
r = requests.get(url, stream=True, verify=True)
"""
打开本地文件 fname,以二进制写入模式('wb')创建或覆盖文件。
将下载的内容 r.content 写入文件。
下载完成后,关闭文件。
"""
with open(fname, 'wb') as f:
f.write(r.content)
return fname
def download_extract(name, folder=None):
"""下载并解压zip/tar文件"""
fname = download(name) # 下载文件的完整路径
"""
os.path.dirname(fname):提取文件所在的目录路径(即父目录)。
例如,如果 fname 是 '../data/example.zip',则 base_dir 是 '../data'。
os.path.splitext(fname):将文件路径拆分为文件名和扩展名。
例如,如果 fname 是 '../data/example.zip',则 data_dir 是 '../data/example',ext 是 '.zip'。
"""
base_dir = os.path.dirname(fname)
data_dir, ext = os.path.splitext(fname)
if ext == '.zip':
fp = zipfile.ZipFile(fname, 'r')
elif ext in ('.tar', '.gz'):
fp = tarfile.open(fname, 'r')
else:
assert False, '只有zip/tar/gz 文件可以被解压缩'
# 解压文件到指定的目录 base_dir
fp.extractall(base_dir)
"""
如果 folder 参数存在:
返回 os.path.join(base_dir, folder),即解压后目录下的指定子目录。
如果 folder 参数不存在:
返回 data_dir,即解压后的默认目录。
"""
return os.path.join(base_dir, folder) if folder else data_dir
def read_data_bananas(is_train=True):
"""读取香蕉检测数据集中的图像和标签"""
data_dir = download_extract('banana-detection')
"""
根据 is_train 参数,确定加载训练集还是验证集的标注文件:
如果 is_train=True,加载 'bananas_train/label.csv'。
如果 is_train=False,加载 'bananas_val/label.csv'。
使用 pandas 库的 read_csv 方法读取 CSV 文件内容。
将 img_name 列设置为索引列(方便后续按图像名称访问数据)。
"""
csv_fname = os.path.join(data_dir, 'bananas_train' if is_train
else 'bananas_val', 'label.csv') # for example: ../data/banana-detection/banana_train/label.csv
csv_data = pd.read_csv(csv_fname) # 读取 csv 文件
# 将 img_name 列设置为索引列
csv_data = csv_data.set_index('img_name')
"""
images:用于存储读取的图像数据。
targets:用于存储每张图像对应的标注信息。
"""
images, targets = [], []
"""
img_name:当前行的索引值(即图像名称)。
target:当前行的数据(即标注信息)。
"""
for img_name, target in csv_data.iterrows():
images.append(torchvision.io.read_image(
os.path.join(data_dir, 'bananas_train' if is_train else
'bananas_val', 'images', f'{img_name}')))
# 这里的target包含(类别,左上角x,左上角y,右下角x,右下角y),
# 其中所有图像都具有相同的香蕉类(索引为0)
targets.append(list(target))
"""
images:图像数据列表。
torch.tensor(targets).unsqueeze(1) / 256:
将 targets 转换为 PyTorch 张量。
使用 unsqueeze(1) 在维度 1 上增加一个维度,使其形状变为 (N, 1, 5),其中 N 是样本数量,5 是每个标注信息的长度。
所有边界框坐标除以 256,进行归一化处理(假设图像大小为 256x256)。
"""
return images, torch.tensor(targets).unsqueeze(1) / 256
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'))
# such as : read 1000 training examples
def __getitem__(self, idx):
return self.features[idx].float(), self.labels[idx]
def __len__(self):
return len(self.features)
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
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
figsize = (num_cols * scale, num_rows * scale)
"""
_:表示整个图形对象(这里用下划线忽略)
axes:表示所有子图的数组
"""
_, axes = plt.subplots(num_rows, num_cols, figsize=figsize)
axes = axes.flatten()
for i, (ax, img) in enumerate(zip(axes, imgs)):
if torch.is_tensor(img):
ax.imshow(img.numpy())
else:
ax.imshow(img)
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
if titles:
ax.set_title(titles[i])
return axes
def bbox_to_rect(bbox, color):
"""将边界框(左上x,左上y,右下x,右下y)格式转换成matplotlib格式:
((左上x,左上y),宽,高)"""
return plt.Rectangle(
xy=(bbox[0], bbox[1]), width=bbox[2] - bbox[0], height=bbox[3] - bbox[1],
fill=False, edgecolor=color, linewidth=2)
# 定义一个名为 numpy 的函数,它能够将 PyTorch 张量转换为 NumPy 数组,并且自动处理张量的梯度信息
numpy = lambda x, *args, **kwargs: x.detach().numpy(*args, **kwargs)
def show_bboxes(axes, bboxes, labels=None, colors=None):
"""显示所有边界框"""
"""
功能:将输入对象转换为列表形式。
如果 obj 是 None,返回默认值 default_values。
如果 obj 不是列表或元组,则将其包装成单元素列表。
否则,直接返回 obj。
作用:确保 labels 和 colors 参数始终是列表形式,以便后续迭代操作。
"""
def _make_list(obj, default_values=None):
if obj is None:
obj = default_values
elif not isinstance(obj, (list, tuple)):
obj = [obj]
return obj
"""
将 labels 转换为列表形式。如果未提供标签,则返回空列表。
将 colors 转换为列表形式。如果未提供颜色,则使用默认颜色列表 ['b', 'g', 'r', 'm', 'c'](蓝色、绿色、红色、洋红色、青色)。
"""
labels = _make_list(labels)
colors = _make_list(colors, ['b', 'g', 'r', 'm', 'c'])
"""
遍历 bboxes 列表中的每个边界框 bbox。
根据索引 i,从 colors 列表中循环选择颜色(防止颜色不足时重复使用)。
调用 bbox_to_rect 函数,将边界框坐标转换为 Matplotlib 的矩形对象。
假设 bbox_to_rect 是一个已定义的函数,用于将边界框坐标转换为 matplotlib.patches.Rectangle 对象。
numpy(bbox):将边界框数据转换为 NumPy 数组(假设 bbox 是 PyTorch 张量)。
使用 axes.add_patch(rect) 将矩形添加到绘图区域中。
"""
for i, bbox in enumerate(bboxes):
color = colors[i % len(colors)]
rect = bbox_to_rect(numpy(bbox), color)
axes.add_patch(rect)
"""
检查是否提供了标签,并且当前索引 i 在标签范围内。
根据边界框颜色选择标签文字的颜色:
如果边界框颜色为白色('w'),标签文字颜色为黑色('k')。
否则,标签文字颜色为白色('w')。
使用 axes.text 方法,在边界框的左上角位置添加标签文本。
rect.xy[0] 和 rect.xy[1]:矩形左上角的 x 和 y 坐标。
va='center' 和 ha='center':设置文本垂直和水平居中对齐。
fontsize=9:设置字体大小为 9。
color=text_color:设置文本颜色。
bbox=dict(facecolor=color, lw=0):为文本添加背景框,背景颜色与边界框一致,边框宽度为 0。
"""
if labels and len(labels) > i:
text_color = 'k' if color == 'w' else 'w'
axes.text(rect.xy[0], rect.xy[1], labels[i],
va='center', ha='center', fontsize=9, color=text_color,
bbox=dict(facecolor=color, lw=0))
if __name__ == "__main__":
batch_size, edge_size = 32, 256
train_iter, _ = load_data_bananas(batch_size)
"""
使用 iter(train_iter) 创建一个迭代器对象。
调用 next() 获取迭代器的第一个批次的数据。
"""
batch = next(iter(train_iter))
"""
torch.Size([32, 3, 256, 256]):表示 32 张 RGB 图像,每张图像大小为 256x256。
torch.Size([32, num_boxes, 5]):表示每张图像有 num_boxes 个边界框,每个边界框包含 5 个值(类别 + 坐标)。
"""
print(batch[0].shape, batch[1].shape)
"""
功能:
batch[0][0:10]:从 batch[0] 中提取前 10 张图像。
.permute(0, 2, 3, 1):调整张量的维度顺序,将 (N, C, H, W) 转换为 (N, H, W, C),即从 PyTorch 的默认格式转换为适合显示的格式。
/ 255:将像素值归一化到 [0, 1] 范围(假设原始像素值范围是 [0, 255])。
结果:
imgs 是一个形状为 (10, 256, 256, 3) 的张量,表示 10 张归一化的 RGB 图像。
"""
imgs = (batch[0][0:10].permute(0, 2, 3, 1)) / 255
axes = show_images(imgs, 2, 5, scale=2)
for ax, label in zip(axes, batch[1][0:10]):
"""
label[0][1:5]:提取第一个边界框的坐标信息([x_min, y_min, x_max, y_max])。
* edge_size:将归一化的坐标值还原为原始像素坐标。
show_bboxes(ax, ...):在子图 ax 上绘制边界框,颜色为白色 ('w')。
"""
show_bboxes(ax, [label[0][1:5] * edge_size], colors=['w'])
plt.show()
"""
归一化是为了模型训练:确保输入数据在合理的范围内,加速收敛并提高稳定性。
还原是为了可视化:将数据转换回原始范围,以便人类可以直观地理解图像和标注信息。
"""