目标检测是计算机视觉中的经典问题之一:
识别给定图像中的对象是什么以及它们在图像中的位置。
检测是一个比分类更复杂的问题,分类也可以识别对象,但不能准确告诉您对象在图像中的位置,而且它不适用于包含多个对象的图像。
YOLO是一个聪明的神经网络,用于实时进行对象检测。
在这篇博文中,我将描述如何使用 Metal Performance Shaders 在 iOS 上运行“微型”版本的 YOLOv2。
在继续之前,请务必观看精彩的 YOLOv2 预告片。 😎
YOLO 的工作原理
您可以使用VGGNet或Inception等分类器,通过在图像上滑动一个小窗口,将其转变为对象检测器。在每一步中,您都运行分类器来预测当前窗口内的对象类型。使用滑动窗口可以对该图像提供数百或数千个预测,但您只保留分类器最确定的预测。
这种方法有效,但显然会非常慢,因为您需要多次运行分类器。稍微更有效的方法是首先预测图像的哪些部分包含有趣的信息(所谓的区域建议),然后仅在这些区域上运行分类器。与滑动窗口相比,分类器要做的工作更少,但仍然会运行很多次。
YOLO 采用了完全不同的方法。它不是一个被重新设计为目标检测器的传统分类器。 YOLO 实际上只查看图像一次(因此得名:You Only Look Once),但方式很巧妙。
YOLO 将图像划分为 13 x 13 个单元格的网格:
每个单元负责预测 5 个边界框。边界框描述了包围对象的矩形。
YOLO 还输出一个置信度分数,告诉我们预测的边界框实际上包含某个对象的确定性。这个分数并没有说明盒子里有什么类型的物体,只是说明盒子的形状是否良好。
预测的边界框可能如下所示(置信度分数越高,框画得越粗):
对于每个边界框,单元格还预测一个类别。这就像分类器一样:它给出所有可能类别的概率分布。我们使用的 YOLO 版本是在PASCAL VOC 数据集上进行训练的,它可以检测 20 个不同的类别,例如:
- 自行车
- 船
- 车
- 猫
- 狗
- 人
- 等等…
边界框的置信度分数和类别预测合并为一个最终分数,该分数告诉我们该边界框包含特定类型对象的概率。例如,左边的大黄框有 85% 的把握包含对象“狗”:
由于有 13×13 = 169 个网格单元,每个单元预测 5 个边界框,因此我们最终得到总共 845 个边界框。事实证明,大多数这些框的置信度分数非常低,因此我们只保留最终分数为 30% 或更高的框(您可以根据您希望检测器的准确度更改此阈值)。
那么最终的预测是:
在总共 845 个边界框中,我们只保留这三个,因为它们给出了最好的结果。但请注意,尽管有 845 个单独的预测,但它们都是同时做出的——神经网络只运行一次。这就是 YOLO 如此强大和快速的原因。
(以上图片来自pjreddie.com。)
神经网络
YOLO的架构很简单,就是一个卷积神经网络:
Layer kernel stride output shape
---------------------------------------------
Input (416, 416, 3)
Convolution 3×3 1 (416, 416, 16)
MaxPooling 2×2 2 (208, 208, 16)
Convolution 3×3 1 (208, 208, 32)
MaxPooling 2×2 2 (104, 104, 32)
Convolution 3×3 1 (104, 104, 64)
MaxPooling 2×2 2 (52, 52, 64)
Convolution 3×3 1 (52, 52, 128)
MaxPooling 2×2 2 (26, 26, 128)
Convolution 3×3 1 (26, 26, 256)
MaxPooling 2×2 2 (13, 13, 256)
Convolution 3×3 1 (13, 13, 512)
MaxPooling 2×2 1 (13, 13, 512)
Convolution 3×3 1 (13, 13, 1024)
Convolution 3×3 1 (13, 13, 1024)
Convolution 1×1 1 (13, 13, 125)
---------------------------------------------
该神经网络仅使用标准层类型:具有 3×3 内核的卷积和具有 2×2 内核的最大池化。没有花哨的东西。 YOLOv2中没有全连接层。
注意:我们将使用的 YOLO 的“微型”版本只有这 9 个卷积层和 6 个池化层。完整的 YOLOv2 模型使用了三倍的层数,并且形状稍微复杂一些,但它仍然只是一个常规的卷积网络。
最后一个卷积层具有 1×1 内核,旨在将数据减少到 13×13×125 的形状。这个 13×13 应该看起来很熟悉:这是图像被划分成的网格的大小。
因此,最终每个网格单元有 125 个通道。这 125 个数字包含边界框和类别预测的数据。为什么是125?那么,每个网格单元预测 5 个边界框,一个边界框由 25 个数据元素描述:
- 边界框矩形的 x、y、宽度、高度
- 置信度得分
- 20 个类别的概率分布
使用 YOLO 很简单:你给它一个输入图像(大小调整为 416×416 像素),它单次通过卷积网络,并从另一端出来一个 13×13×125 张量,描述边界框网格单元。然后您需要做的就是计算边界框的最终分数,并丢弃分数低于 30% 的分数。
提示:要了解有关 YOLO 工作原理及其训练方式的更多信息,请查看其发明者之一的精彩演讲。该视频实际上描述了 YOLOv1,这是一个较旧版本的网络,其架构略有不同,但主要思想仍然相同。值得一看!
解码还原
从网络输出预测值到最终将边界框还原到图像的原始尺寸,整个过程包括多个步骤:解码预测值、非极大值抑制(NMS)、以及将边界框坐标映射回原始图像尺寸。
好的,我们来详细讲解一下 YOLOv5 从网络输出预测值,到解码、NMS,最后将框还原到图像原始尺寸的完整流程。
1. 网络输出预测值(Prediction)
-
输出层结构: YOLOv5 使用 Head 部分来生成最终的预测结果。Head 部分通常包含多个检测层,每个检测层负责检测不同尺度的目标。每个检测层会输出一个张量,其维度通常是 [Batch, Anchor * (5 + num_classes), Grid_H, Grid_W]。
-
Batch: 批次大小。
-
Anchor: 每个网格单元的 Anchor 数量(通常是 3)。
-
5: 每个 Anchor Box 的预测值,包括 tx, ty, tw, th, confidence。
-
tx, ty: 相对于网格单元左上角的偏移量,用于确定 Box 的中心位置。
-
tw, th: 相对于 Anchor 的宽度和高度的缩放因子,用于确定 Box 的宽度和高度。
-
confidence: Box 的置信度,表示 Box 包含目标的可能性以及 Box 预测的准确性。
-
-
num_classes: 目标类别的数量。
-
Grid_H, Grid_W: 检测层特征图的高度和宽度,决定了将图像划分成多少个网格单元。
-
-
例如: 假设一个检测层输出的张量维度是 [1, 3 * (5 + 80), 80, 80],表示:
-
Batch Size = 1
-
每个网格单元有 3 个 Anchor
-
有 80 个类别
-
特征图大小为 80x80
-
2. 解码(Decoding)
解码过程将网络输出的相对预测值转换为实际的绝对边界框坐标(中心坐标和宽高)和置信度。
-
将张量变形: 首先,将输出张量变形为 [Batch, Grid_H, Grid_W, Anchor, 5 + num_classes] 的形状。 这使得处理每个 Anchor Box 的预测值更加方便。
-
计算 Box 中心坐标和宽高: 使用以下公式将 tx, ty, tw, th 转换为相对于原始图像的中心坐标 bx, by 和宽高 bw, bh,需要注意的是,这里计算出的bx,by,bw,bh都是相对于特征图的尺寸。
bx = sigmoid(tx) + cx by = sigmoid(ty) + cy bw = pw * exp(tw) bh = ph * exp(th)
-
sigmoid(tx), sigmoid(ty):对偏移量进行 Sigmoid 激活函数处理,将其限制在 0 到 1 之间,这样做是因为需要表示相对于网格单元左上角的偏移量百分比。 例如,sigmoid(tx) = 0.5 表示边界框的中心点在网格单元水平方向的中间位置
-
cx, cy:网格单元的左上角坐标。 通过将 Sigmoid 输出的偏移量加上 cx 和 cy,就可以得到预测框中心点在特征图上的绝对坐标 bx 和 by。 假设特征图大小为 80x80,当前网格单元是 (10, 20),则 cx = 10, cy = 20。 如果 sigmoid(tx) = 0.3,sigmoid(ty) = 0.7,那么 bx = 10.3, by = 20.7。 这意味着预测框的中心点在网格单元 (10, 20) 的基础上,水平方向偏移了 0.3 个单元格,垂直方向偏移了 0.7 个单元格。
-
pw, ph:tw 和 th 表示预测框的宽度和高度相对于 Anchor 宽高的缩放因子。通过将 Anchor 的宽高 pw 和 ph 乘以这个缩放因子,可以得到预测框的实际宽度 bw 和高度 bh。每个检测层都有其预定义的 Anchor 尺寸。
-
网络只需要预测相对于 Anchor 的偏移量和缩放因子,而不需要直接预测目标的绝对尺寸,这大大降低了学习难度。通常使用 K-means 聚类算法在训练数据集上聚类得到 Anchor 尺寸,以便更好地适应数据集中的目标形状分布。
-
bx, by, bw, bh 通常表示中心坐标和宽高(相对于特征图大小的比例),为了方便后续计算,通常需要将其转换为左上角和右下角坐标 x1, y1, x2, y2 的格式。使用如下公式
x1 = bx - bw / 2 y1 = by - bh / 2 x2 = bx + bw / 2 y2 = by + bh / 2
补充一点:特征图是经过多次下采样得到到,特征图上每一个网格对应原始图像一块区域。如果特征图的大小为 Grid_W x Grid_H,而输入图像的大小为 img_size x img_size,那么每个网格单元对应于原始图像上的 img_size / Grid_W x img_size / Grid_H 的区域。因此,在解码得到 bx, by 之后,为了使其具有普遍性,通常会将它们除以特征图的宽度和高度,得到相对于归一化图像大小的比例
假设: 原始图像大小: 640x480 缩放后的图像大小: 640x640 特征图大小: 80x80 一个预测框的中心点坐标 (解码后): bx = 40, by = 20 那么: bx 相对于特征图的位置: 40 / 80 = 0.5 by 相对于特征图的位置: 20 / 80 = 0.25 这意味着中心点位于缩放后图像的水平方向的 50%,垂直方向的 25%。
-
3. 非极大值抑制(NMS, Non-Maximum Suppression)
经过解码和坐标缩放后,会得到大量的候选框。NMS 用于消除重复的、置信度较低的边界框,保留最准确的预测结果。
-
过滤低置信度 Box: 首先,根据置信度阈值,过滤掉置信度低于阈值的 Box,减少后续计算量。
-
按类别进行 NMS: 对每个类别分别进行 NMS。 这是因为不同类别的 Box 不应该互相抑制。
-
计算 IoU (Intersection over Union): 对于同一类别的 Box,计算两两之间的 IoU。 IoU 表示两个 Box 的重叠程度,是它们交集面积与并集面积之比。
-
抑制重叠 Box: 根据 IoU 阈值(例如 0.6),如果两个 Box 的 IoU 大于阈值,且其中一个 Box 的置信度较低,则抑制(移除)该 Box。 通常保留置信度最高的 Box。
-
输出最终 Box: 经过 NMS 后,剩余的 Box 就是最终的检测结果。
4. 框还原到图像原始尺寸
经过以上步骤,我们得到了在缩放后的图像上的边界框坐标。为了在原始图像上显示检测结果,需要将这些坐标还原到原始图像的尺寸。
-
计算缩放比例: 在图像预处理阶段,通常会将原始图像缩放到一个固定尺寸(例如 640x640)。 计算缩放比例 scale:
scale = min(img_size / img_height, img_size / img_width)
-
img_size:缩放后的图像尺寸 (例如 640)。
-
img_height, img_width:原始图像的高度和宽度。
-
-
计算 Padding: 如果缩放后的图像与目标尺寸不一致,则需要进行 Padding。 计算 Padding 的大小:
pad_w = (img_size - img_width * scale) / 2 pad_h = (img_size - img_height * scale) / 2
-
还原坐标: 将缩放后的 Box 坐标还原到原始图像坐标:
x1 = (x1 - pad_w) / scale y1 = (y1 - pad_h) / scale x2 = (x2 - pad_w) / scale y2 = (y2 - pad_h) / scale
-
x1, y1, x2, y2:缩放后的 Box 的左上角和右下角坐标。
-
-
裁剪坐标: 为了确保 Box 不超出图像边界,将坐标裁剪到图像范围内:
x1 = max(0, x1) y1 = max(0, y1) x2 = min(img_width, x2) y2 = min(img_height, y2)
总结
整个流程可以概括为以下几个步骤:
-
网络输出: YOLOv5 网络 Head 部分输出预测张量。
-
解码: 将网络输出的相对预测值转换为实际的边界框坐标和置信度。
-
NMS: 使用非极大值抑制消除重复的、置信度较低的边界框。
-
坐标还原: 将缩放后的边界框坐标还原到原始图像尺寸。
代码示例(PyTorch):
以下是一个简化的示例,展示了解码和坐标还原的过程:
import torch
import torch.nn.functional as F
def process_output(prediction, anchors, num_classes, img_size, img_height, img_width):
"""
处理 YOLOv5 的输出,解码预测框并还原到原始图像尺寸。
"""
batch_size, _, grid_h, grid_w = prediction.shape
stride = img_size / grid_h # 计算 stride
# 将 prediction 变形为 [Batch, Grid_H, Grid_W, Anchor, 5 + num_classes]
prediction = prediction.view(batch_size, 3, num_classes + 5, grid_h, grid_w).permute(0, 1, 3, 4, 2).contiguous()
scale = min(img_size / img_height, img_size / img_width)
pad_w = (img_size - img_width * scale) / 2
pad_h = (img_size - img_height * scale) / 2
boxes = []
for b in range(batch_size):
for i in range(grid_h):
for j in range(grid_w):
for a in range(3): # 遍历每个 Anchor
# 解码
tx = prediction[b, a, i, j, 0]
ty = prediction[b, a, i, j, 1]
tw = prediction[b, a, i, j, 2]
th = prediction[b, a, i, j, 3]
confidence = prediction[b, a, i, j, 4]
class_probs = torch.sigmoid(prediction[b, a, i, j, 5:]) # Sigmoid for multi-label, Softmax for single-label
bx = torch.sigmoid(tx) + j
by = torch.sigmoid(ty) + i
bw = anchors[a, 0] * torch.exp(tw)
bh = anchors[a, 1] * torch.exp(th)
# 转换为中心坐标和宽高格式 (缩放到0-1)
x_center = bx / grid_w
y_center = by / grid_h
box_width = bw / img_size
box_height = bh / img_size
# 转换为左上角和右下角坐标
x1 = x_center - box_width / 2
y1 = y_center - box_height / 2
x2 = x_center + box_width / 2
y2 = y_center + box_height / 2
# 还原到原始图像尺寸
x1 = (x1 * img_size - pad_w) / scale
y1 = (y1 * img_size - pad_h) / scale
x2 = (x2 * img_size - pad_w) / scale
y2 = (y2 * img_size - pad_h) / scale
# 裁剪坐标
x1 = max(0, x1.item())
y1 = max(0, y1.item())
x2 = min(img_width, x2.item())
y2 = min(img_height, y2.item())
# 置信度
confidence = torch.sigmoid(confidence).item()
# 存储结果
class_conf, class_id = torch.max(class_probs, -1)
boxes.append([x1, y1, x2, y2, confidence* class_conf.item(), class_id.item()])
return boxes
# 示例用法
if __name__ == '__main__':
# 模拟网络输出
batch_size = 1
num_anchors = 3
num_classes = 80
grid_h = 80
grid_w = 80
img_size = 640
img_height = 480 # 原始图像高度
img_width = 640 # 原始图像宽度
prediction = torch.randn(batch_size, num_anchors * (5 + num_classes), grid_h, grid_w)
anchors = torch.tensor([[10, 13], [16, 30], [33, 23]])
boxes = process_output(prediction, anchors, num_classes, img_size, img_height, img_width)
print(f"检测到的框数量: {len(boxes)}")
for box in boxes:
print(box)
代码解释:
-
process_output() 函数接收网络的预测输出、Anchor、类别数量、图像尺寸等参数。
-
代码首先将预测张量变形,方便后续处理。
-
然后,对 tx, ty, tw, th 进行解码,得到 Box 的中心坐标和宽高。
-
接着,将坐标还原到原始图像尺寸。
-
最后,将结果存储在一个列表中。
-
示例用法部分模拟了网络的输出,并调用 process_output() 函数进行处理。
这个例子只是一个简化的版本,实际的 YOLOv5 实现可能更加复杂,例如使用不同的激活函数、更高级的 NMS 算法等。 但是,这个例子涵盖了核心的解码和坐标还原步骤。
希望这个详细的讲解能够帮助你理解 YOLOv5 的预测流程。 如果有任何问题,欢迎随时提出。