YOLO原理,看这一篇就够了

目标检测是计算机视觉中的经典问题之一:

识别给定图像中的对象是什么以及它们在图像中的位置。

检测是一个比分类更复杂的问题,分类也可以识别对象,但不能准确告诉您对象在图像中的位置,而且它不适用于包含多个对象的图像。

a3da2d45c91001ad0453b64900521ef9.png

YOLO是一个聪明的神经网络,用于实时进行对象检测。

在这篇博文中,我将描述如何使用 Metal Performance Shaders 在 iOS 上运行“微型”版本的 YOLOv2。

在继续之前,请务必观看精彩的 YOLOv2 预告片。 😎

YOLO 的工作原理

您可以使用VGGNetInception等分类器,通过在图像上滑动一个小窗口,将其转变为对象检测器。在每一步中,您都运行分类器来预测当前窗口内的对象类型。使用滑动窗口可以对该图像提供数百或数千个预测,但您只保留分类器最确定的预测。

这种方法有效,但显然会非常慢,因为您需要多次运行分类器。稍微更有效的方法是首先预测图像的哪些部分包含有趣的信息(所谓的区域建议),然后仅在这些区域上运行分类器。与滑动窗口相比,分类器要做的工作更少,但仍然会运行很多次。

YOLO 采用了完全不同的方法。它不是一个被重新设计为目标检测器的传统分类器。 YOLO 实际上只查看图像一次(因此得名:You Only Look Once),但方式很巧妙。

YOLO 将图像划分为 13 x 13 个单元格的网格:

afda0031ebdb0bb088d96e8404f8a9e1.png

每个单元负责预测 5 个边界框。边界框描述了包围对象的矩形。

YOLO 还输出一个置信度分数,告诉我们预测的边界框实际上包含某个对象的确定性。这个分数并没有说明盒子里有什么类型的物体,只是说明盒子的形状是否良好。

预测的边界框可能如下所示(置信度分数越高,框画得越粗):

1ccd131c969af401898235c7f2c8f9aa.png

对于每个边界框,单元格还预测一个类别。这就像分类器一样:它给出所有可能类别的概率分布。我们使用的 YOLO 版本是在PASCAL VOC 数据集上进行训练的,它可以检测 20 个不同的类别,例如:

  • 自行车
  • 等等…

边界框的置信度分数和类别预测合并为一个最终分数,该分数告诉我们该边界框包含特定类型对象的概率。例如,左边的大黄框有 85% 的把握包含对象“狗”:

2f535b5d97e943433b7a5b825d9d7f69.png

由于有 13×13 = 169 个网格单元,每个单元预测 5 个边界框,因此我们最终得到总共 845 个边界框。事实证明,大多数这些框的置信度分数非常低,因此我们只保留最终分数为 30% 或更高的框(您可以根据您希望检测器的准确度更改此阈值)。

那么最终的预测是:

465e29c013f0c1442088cf49ff2dfe66.png

在总共 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)
        

总结

整个流程可以概括为以下几个步骤:

  1. 网络输出: YOLOv5 网络 Head 部分输出预测张量。

  2. 解码: 将网络输出的相对预测值转换为实际的边界框坐标和置信度。

  3. NMS: 使用非极大值抑制消除重复的、置信度较低的边界框。

  4. 坐标还原: 将缩放后的边界框坐标还原到原始图像尺寸。

代码示例(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 的预测流程。 如果有任何问题,欢迎随时提出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值