物体检测
目标检测和图片分类的区别:
图像分类(Image Classification)
目的 :图像分类的目的是识别出图像中主要物体的类别。它试图回答"图像是什么?"的问题。
输出 :通常输出是一个标签或一组概率值,表示图像属于各个预定义类别的可能性。例如,对于一张包含猫的图片,分类器可能会输出"猫"这个标签。
应用场景:适用于只需要了解图像整体内容的场景,如识别照片中的动物种类、区分不同的风景类型等。
目标检测(Object Detection)
目的 :目标检测不仅需要识别图像中所有感兴趣物体的类别,还需要确定每个物体在图像中的具体位置。它试图回答"图像中有什么?它们在哪里?"的问题。
输出 :除了给出物体的类别外,还会输出物体所在的边界框(bounding box),即用矩形框标记出每个物体的位置。例如,在自动驾驶场景下,系统不仅要能识别出行人、车辆等物体,还要精确地定位它们的位置以便做出安全决策。
应用场景:适合于需要知道图像内特定对象位置的情况,比如视频监控、自动驾驶汽车、医学影像分析等领域。
边缘框
- 一个边缘框可以通过 4 个数字定义
- (左上 x,左上 y,右下 x,右下 y)
- (左上 x,左上 y,宽,高)
目标检测数据集
- 每行表示一个物体
- 图片文件名,物体类别,边缘框
- COCO (cocodataset.org)
- 80 物体,330K 图片,1.5M 物体
总结
- 物体检测识别图片里的多个物体的类别和位置
- 位置通常用边缘框表示。
边缘框实现
python
%matplotlib inline
import torch
from d2l import torch as d2l
d2l.set_figsize()
img = d2l.plt.imread('../img/catdog.jpg')
d2l.plt.imshow(img);

定义在这两种表示法之间进行转换的函数
python
#@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
定义图像中狗和猫的边界框
python
# 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

将边界框在图中画出
python
#@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'));

数据集
李沐老师收集并标记了一个小型数据集,下面首先是下载该数据集:
python
%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')
读取香蕉检测数据集
python
#@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 实例:
python
#@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)
为训练集和测试集返回两个数据加载器实例
python
#@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
读取一个小批量,并打印其中的图像和标签的形状
python
batch_size, edge_size = 32, 256
train_iter, _ = load_data_bananas(batch_size)
batch = next(iter(train_iter))
batch[0].shape, batch[1].shape

演示:
python
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:迁移学习:找一个非常好的,在一个比较大的目标检测数据集上训练的比较好的模型,然后拿过来进行微调。近百张其实也不算小的了,只是说模型训练出来不是很好。数据增强的话,对图像进行处理,对应的框也必须要相应的变化一下,这是比较麻烦的点。
后记
自己实现了一遍,然后也是使用的李沐老师在课件中提供的狗猫图片:
python
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上跑了一下下面的代码,发现可以,下载不了的可能需要一点 "魔法"
python
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()
"""
归一化是为了模型训练:确保输入数据在合理的范围内,加速收敛并提高稳定性。
还原是为了可视化:将数据转换回原始范围,以便人类可以直观地理解图像和标注信息。
"""
