Retinaface实现人脸检测与关键点定位-深度学习学习笔记-1

前言

本文基于人工智能领域大佬Bubbliiiing睿智的目标检测42——Pytorch搭建Retinaface人脸检测与关键点定位平台

原文链接:https://blog.csdn.net/weixin_44791964/article/details/106872072

这是是我的学习笔记,记录我复现与拓展的学习过程,万分感谢大佬的开源和无私奉献。本文部分内容来自网上搜集与个人实践。如果任何信息存在错误,欢迎读者批评指正。本文仅用于学习交流,不用作任何商业用途。
Retinaface实现人脸检测与关键点定位-深度学习学习笔记-1
Facenet实现人脸特征比对-深度学习学习笔记-2
RetinaFace人脸检测模型-Gradio界面设计
FaceNet人脸识别模型-Gradio界面设计
Retinaface+FaceNet人脸识别系统-Gradio界面设计

文章目录

  • 前言
  • 需求分析
  • 技术选取
    • 数据集选取
  • 实现思路
    • 主干网络介绍
    • 主干网络选取
    • FPN特征金字塔
    • SSH进一步加强特征提取
    • 从特征获取预测结果
    • 预测结果的解码
  • 训练部分
    • 真实框的处理过程
    • 利用处理完的真实框与对应图片的预测结果计算loss

需求分析

  • 人脸检测:通过端到端的卷积神经网络将人脸映射到特征空间中的向量,使同一人脸的特征向量彼此邻近,不同人脸的特征向量彼此远离,从而实现面向人脸识别的深度学习。
  • 关键点定位:关键点定位是指在人脸检测的基础上,进一步确定人脸上特定位置的关键点。这些关键点可以是眼睛、鼻子、嘴巴等重要的面部特征点,通过标记它们的位置可以帮助我们进行更精细的人脸分析和识别。
  • 深度学习:深度学习是一种机器学习方法,其核心是构建和训练具有多个神经网络层的模型,以从大量数据中提取复杂的特征表示。深度学习在计算机视觉、自然语言处理等领域取得了很大的成功。

技术选取

  • PyTorch:PyTorch是一个开源的深度学习框架,它提供了丰富的工具和函数来简化神经网络模型的构建和训练过程。
  • Retinaface:Retinaface是一种人脸检测算法,它能够准确地检测出图像中的人脸,并标记出人脸的关键点位置(例如眼睛、鼻子、嘴巴等)。这种算法基于one-stage的网络结构,由insightFace团队开发。

数据集选取

  • Widerface是一个人脸检测领域常用的开源数据集,具有以下几个特征:
    • 包含32,203张图像,共393,703个人脸标注框。图像来自各种场景,包括聚会、演唱会等复杂环境。
    • 标注质量高,所有人脸框都是由人工画出,边界精准。标注包含人脸框位置、遮挡、姿态、表情等属性信息。
    • 数据集分为训练集、验证集和测试集。其中训练集40%、验证集10%、测试集50%。测试集没有公开标注,需要自己在服务器上评测。
    • 为了评估算法在不同环境下的鲁棒性,根据场景复杂度将图像分为 Easy、Medium、Hard 3大类别。类别内又有许多子类别如人群、大尺度变化等
    • 测试集包含各类别的测试图像,用于提交算法结果到评测服务器,得到检测性能指标如AP。验证集用于调参、分析算法性能。
    • 提供了在线评测系统,上传结果文件即可得到性能指标feedback。方便算法性能的评估与比较。
    • 由斯坦福CVPR2016组织,是学术界公认的人脸检测重要Benchmark。算法在Widerface上的结果直接反映了其检测性能和稳定性。

实现思路

主干网络介绍

Retinaface在实际训练的时候使用两种网络作为主干特征提取网络。分别是MobilenetV1-0.25和Resnet。
Keras API documentation
MobilenetV1-0.25:这是一种特殊的计算方法,它被设计成非常轻巧快速。使用MobilenetV1-0.25作为RetinaFace的特征提取方法,可以在资源有限的设备上实现实时的人脸检测,比如手机和一些不太强大的电脑。但是,由于其轻量级设计,可能在一些情况下会稍微牺牲一些精度。

Resnet:这是另一种特征提取方法,它相对复杂但更精确。通过使用Resnet,RetinaFace可以在更复杂的场景下找到更准确的人脸位置和特征点。但因为它的复杂性,它需要更多计算资源,所以在一些设备上可能不太适合实时应用。

因此,RetinaFace有两种特征提取方法可供选择。如果需要快速的实时人脸检测,可以选择使用MobilenetV1-0.25,在手机等设备上表现较好。而如果对于更高的精度要求,可以切换到Resnet,但这可能需要更强大的计算设备。

总的来说,RetinaFace是一种灵活的人脸识别技术,能够根据不同场景和设备的需求,选择合适的特征提取方法,从而在不同情况下实现准确的人脸检测和特征点定位。

主干网络选取

我们主要使用MobilenetV1-0.25作为主干网络,设计的一个轻量级的深层神经网络模型。它的核心思想是使用depthwise separable convolution(深度可分离卷积)来减少模型参数量和计算量。
我们先了解一些基本概念:

  • 卷积:

    • 卷积是一种数学运算,用于将一个函数与另一个函数进行操作,以产生一个新的函数。在图像处理中,我们可以把卷积看作是一种滤波操作。它通过在图像上滑动一个小的窗口(称为卷积核),对窗口内的像素进行加权求和,从而得到新的像素值。

    • 新像素 = 卷积核中的权重 * 窗口内像素的加权平均值

    • 我们可以把卷积想象成用一个小小的滤网去过滤咖啡。
      比如我们有一张5x5的图片,每一个小格子是一个像素点,用数字1到5表示它的颜色:
      1 1 2 3 4
      2 2 3 4 5
      3 3 3 4 5
      4 4 4 5 5
      5 5 5 5 5
      现在我们定义一个3x3的滤波器,就是一个3行3列的小矩阵:
      0 1 0
      1 0 1
      0 1 0
      我们把这个滤波器放在图片上,让它从左到右、从上到下滑动,每次停在一个位置。
      当它停在最上面最左边时,会覆盖图片中的:
      1 1 2
      2 2 3
      3 3 3
      对应元素相乘就是:
      (1 x 0) (1 x 1) (2 x 0)
      (2 x 1) (2 x 0) (3 x 1)
      (3 x 0) (3 x 1) (3 x 0)
      然后我们把滤波器中的数字与覆盖的图片中对应的数字分别相乘,再把 9 个乘积加起来,就可以得到一个新的数字,比如这里是18。
      18就会成为输出图片中对应位置的新的像素值。
      我们让滤波器继续在图片上滑动,每次输出一个乘积求和的结果,最终就可以得到一个新的图片,它保留了原图片在这个滤波器下的特征。
      如果我们改变滤波器的数字的安排组合,就可以得到不同的特征。
      这个过程,就像我们用不同的滤网去过滤咖啡,不同的滤网会提取咖啡中的不同成分。
      所以卷积核其实就是一个提取图像特征的滤波器,经过卷积操作,可以得到代表不同特征的图像。这一技术在图像处理和机器学习中很重要,比如可以用来进行图像识别等任务。

  • 卷积层:

    • 卷积层就像是神经网络中的一个特殊工人,负责从输入数据中提取有用的信息。它可以看作是一个滑动窗口,不停地在输入数据上移动。每次移动,它会与当前窗口内的数据进行一种特殊的计算,这个计算方式就叫做卷积。通过卷积,卷积层可以捕捉到输入数据中的不同特征,比如边缘、纹理等。卷积层可以有多个窗口,每个窗口可以提取不同的特征。卷积层的输出结果就是一组特征图,它们包含了输入数据中不同特征的信息。
  • 卷积核:

    • 卷积核就像是卷积层的工具箱,它是卷积层中的一个小矩阵。每个卷积核都有一组权重参数,这些参数决定了卷积核如何与输入数据进行计算。卷积核的大小和形状是由设计者事先确定的。
    • 通过与输入数据进行卷积操作,卷积核可以突出输入数据中的不同特征。卷积核的参数是可以学习的,通过神经网络的训练过程,它们会自动调整以提取最有用的特征。
  • 卷积层和卷积核之间的关系:卷积层包含了多个卷积核。每个卷积核可以提取不同的特征。卷积层通过并行地使用多个卷积核,可以同时提取多个特征。每个卷积核在卷积层中滑动并与输入数据进行卷积操作,生成对应的特征图。这些特征图可以被传递给神经网络的下一层进行进一步的处理和分析。

  • 总结起来

    • 卷积是一种数学运算,用于将一个函数与另一个函数进行操作,以产生一个新的函数。在图像处理和深度学习中,卷积是指将一个卷积核与输入数据进行操作,以生成输出特征图。
    • 卷积层是神经网络中的一种基本层级结构,用于处理图像、语音和其他类型的数据。它由多个卷积核组成,每个卷积核都会对输入数据进行卷积操作,并生成一个对应的输出特征图。卷积层在神经网络中起到了提取特征的重要作用。
    • 卷积核是卷积层中的一个小矩阵,它的大小和形状由设计者事先确定。每个卷积核都包含一组权重参数,这些参数在训练过程中会被优化。每个卷积核通过与输入数据进行卷积操作,可以提取出不同的特征。卷积核实际上是神经网络中的一种学习到的参数,它的作用是通过与输入数据进行卷积操作,从输入数据中提取出有用的特征。每个卷积核可以捕捉到不同的特征,比如边缘、纹理、形状等。卷积层中的每个卷积核都可以学习到不同的特征,从而帮助神经网络理解和处理复杂的数据。
    • 卷积是一种数学运算,卷积层是神经网络中的一种基本层级结构,而卷积核是卷积层中用于对输入数据进行卷积操作的小矩阵。它们共同作用于神经网络中,帮助网络理解和处理复杂的数据。
  • 特征图(featuremap)

    • 是指卷积操作的输出结果。在卷积层中,每个卷积核都会生成一个特征图,它包含了从输入数据中提取的特征信息。特征图可以传递给神经网络的下一层进行进一步的处理和分析
  • 通道:

    • 通道对应着特征图的深度维度,每一通道都是一张2D特征图。
    • 例如,RGB图像有3个通道,那么它可以看作是由3张2D特征图堆叠起来的。而在模型中,不同的通道可以检测图像的不同属性,卷积层输出的通道数由输出通道数决定。
  • 步长:

    • 步长用来控制卷积核在输入特征图上滑动的步进,它决定着输出特征图相对于输入特征图的缩小程度。
    • 步长越大,输出特征图相对输入特征图的缩小程度越大,特征的提取会更加粗糙。步长为2时,输出特征图的大小会减半。
  • 深度可分离卷积(depthwise separable convolution)

    • 深度可分离卷积是一种卷积操作的变种。它将卷积操作分为两个步骤:深度卷积和融合操作。
    • 深度卷积是指使用一个小的卷积核(比如3×3)分别遍历输入数据的每个通道,得到多个特征图谱。
    • 融合操作是指使用一个1×1大小的卷积核将这些特征图谱融合在一起,生成最终的输出特征图。通过这种方式,深度可分离卷积可以减少模型的参数量,提高计算效率。

假设有一个3×3大小的卷积层,其输入通道为16、输出通道为32。具体为,32个3×3大小的卷积核会遍历16个通道中的每个数据,最后可得到所需的32个输出通道,所需参数为16×32×3×3=4608个。

应用深度可分离卷积,用16个3×3大小的卷积核分别遍历16通道的数据,得到了16个特征图谱。在融合操作之前,接着用32个1×1大小的卷积核遍历这16个特征图谱,所需参数为16×3×3+16×32×1×1=656个。
可以看出来depthwise separable convolution可以减少模型的参数。

  • 通俗来说

    • 我们可以把一张图片看成是多层薄薄的玻璃板叠在一起。每层玻璃板显示图片的一个特定特征,比如颜色、边缘等。这些玻璃板就可以看成是图片的通道。我们做一个比喻:
      输入图片 -> 多层玻璃板
      图片通道 -> 每层玻璃板
      普通卷积核 -> 小刷子
  • 普通的卷积操作中,我们的输入图片有 16 个通道(可以看成 16 层玻璃板),我们希望卷积层输出 32 个通道(特征图)。

    • 普通的卷积操作每个输出通道需要一个对应的卷积核来生成, 所以需要准备 32 个卷积核,即 32 个小刷子
    • 就像用32个小刷子来刷这张图片。 每个小刷子要刷过输入图片的全部 16 个层,才能输出 32 个通道,捕捉所有特征,得到对应的输出,参数数量非常大,需要16×32×3×3=4608个。
  • 而深度可分离卷积则是分两步刷:

    • 第一步,用16个刷子分别刷每一层玻璃板,每个刷子只刷一层输入通道。
    • 第一步参数量为:输入通道数*刷子大小 = 16×3×3
    • 第二步,再用32个刷子只刷第一步刷过的结果,32个小刷子作用在第一步产生的16个结果通道上,不会再操作原始的16个输入通道。
    • 第二步参数量:第一步输出通道数输出通道数刷子大小 = 16×3×3+16×32×1×1=656个。
  • 这样参数数量只需要16×3×3+16×32×1×1=656个。大约减少到原来的1/7。
    所以深度可分离卷积通过分解步骤,显著减少了参数量。这使得模型更加轻量化,也降低了计算量。
    希望这个通俗的刷玻璃板比喻可以让你更直观地理解卷积通道和深度可分离卷积的工作原理。

如下这张图就是depthwise separable convolution的结构
Retinaface实现人脸检测与关键点定位-深度学习学习笔记-1_第1张图片

在建立模型的时候,可以将卷积group设置成in_filters层实现深度可分离卷积,然后再利用1x1卷积调整channels数。

通俗地理解就是3x3的卷积核厚度只有一层,然后在输入张量上一层一层地滑动,每一次卷积完生成一个输出通道,当卷积完成后,在利用1x1的卷积调整厚度。

如下就是MobileNet的结构,其中Conv dw就是分层卷积,在其之后都会接一个1x1的卷积进行通道处理,
Retinaface实现人脸检测与关键点定位-深度学习学习笔记-1_第2张图片

上图所示是的mobilenetV1-1的结构,本文所用的mobilenetV1-0.25是mobilenetV1-1通道数压缩为原来1/4的网络。
Retinaface实现人脸检测与关键点定位-深度学习学习笔记-1_第3张图片

# nets/mobilenet025.py

# 卷积+BN+LeakyReLU激活函数模块
def conv_bn(inp, oup, stride=1, leaky=0.1):
    # inp:输入通道数,oup:输出通道数,stride:步长,leaky:negative_slope,默认0.1
    return nn.Sequential(
        nn.Conv2d(inp, oup, 3, stride, 1, bias=False),
        # 卷积层        
        # inp:输入通道数,oup:输出通道数,kernel_size=3,stride:步长,padding=1,bias=False
        '''
		参数:
		Conv2d层实际上就是二维卷积层,它对输入信号(由多个输入平面组成)应用二维卷积。
		该层的主要参数有:
		- in_channels:输入图像中的通道数
		- out_channels:卷积产生的通道数
		- kernel_size:卷积核的大小,可以是单个数值或者是一个tuple(h,w)表示height和width
		- stride:卷积步长,控制卷积窗口移动的步幅,默认是1。可以是一个数值或者是一个tuple(sh,sw)表示height和width的步幅 
		- padding:添加在输入两侧的零填充,默认是0。可以是一个数值或者是一个tuple(ph,pw)表示height和width方向的填充量。
		- dilation:卷积核元素之间的间距,默认是1。可以是一个数值或者是一个tuple(dh,dw)表示height和width方向的间距。
		- groups:从输入通道到输出通道的连接块数。默认为1。
		- bias:是否添加可学习的偏置项,默认为True。
		该层的输出形状可以根据以下公式计算:
		out_height = (in_height + 2*padding[0] - dilation[0]*(kernel_size[0] - 1) - 1)/stride[0] + 1
		out_width  = (in_width + 2*padding[1] - dilation[1]*(kernel_size[1] - 1) - 1)/stride[1] + 1
		该层在图像分类、目标检测、语义分割等任务中很常用。可以提取图像中的低层特征,并且可以通过stack多层Conv2d层构建更深的卷积神经网络。
		文档复制自:Conv2d
		'''
        # 卷积层是图像分类和物体检测网络中最核心的模块之一。它实现了卷积操作,可以提取图像的空间特征,构建特征图。
        # 卷积层主要由三部分组成:
        # 1. 卷积核:也叫滤波器,是一个小的权重矩阵,用于与输入特征图的部分区域相乘,来检测输入特征图中的局部模式。
        # 2. 步长:控制卷积核在输入特征图上滑动的步长,步长越大,输出特征图越小。
        # 3. 填充:在输入特征图外围填充0值,可以控制输出特征图的大小。填充为1时,输出特征图大小不变。
        # 卷积层的作用:
        # 1. 特征提取:通过卷积核可以检测输入特征图中的局部模式,实现特征提取。
        # 2. 参数共享:卷积层的参数(卷积核)在空间上重复使用,这种参数共享方式可以大幅减少参数量。
        # 3. 空间cup:通过步长可以减小特征图的大小,实现下采样。
        
        nn.BatchNorm2d(oup),
        # BN层
        # 作用:加速训练,提高精度,稳定性。oup:BN层通道数
        # BN层实现批量归一化,它对每个mini-batch的每个通道进行归一化,使得输入的分布更加均匀。
        # BN层的作用:
        # 1. 提高训练效率:BN层可以对模型的中间激活值施加约束,限制其分布在一定范围内,这可以加速模型的训练过程。
        # 2. 降低过拟合:BN层使得输入的分布更加均匀,这可以在一定程度上减轻过拟合问题。
        # 3. 使训练更加稳定:不使用BN层,在训练过程中,如果激活值变化较大,那么模型的参数也要作出较大调整,这会使得训练过程不稳定。BN层可以限制激活值的变化范围,参数的调整幅度也会相应小一些,模型变得更加稳定。
        # 4. 改善梯度消失问题:激活值变化过小会导致梯度消失或爆炸,BN层可以在一定程度上防止这个问题。
        # 总之,卷积层实现特征提取与降维,BN层可以加速训练、防止过拟合与梯度消失,二者在深度神经网络中起到非常重要的作用。
        
        nn.LeakyReLU(negative_slope=leaky, inplace=True)
        # LeakyReLU激活
        # LeakyReLU是一种修正的ReLU激活函数。它的表达式为:
        # f(x) = max(0.01x, x)   (当x < 0时)
        # f(x) = x                 (当x >= 0时)
        # 也就是说,当x < 0时,LeakyReLU会让一小部分的负值通过,而标准的ReLU函数会完全阻断负值。
        # LeakyReLU的作用主要有:
        # 1. 防止死亡节点问题:标准的ReLU会完全阻断负值,这可能导致某些节点的梯度在训练过程中永远为0,这种节点称为“死亡节点”。LeakyReLU可以让一小部分负值通过,所以可以在一定程度上缓解这个问题。
        # 2. 加速收敛:让一小部分负值通过,可以增加模型表达能力,有利于加速模型的收敛。
        # 3. 防止梯度消失:ReLU会使负值对应的梯度消失,而LeakyReLU只是减小了负值梯度,所以可以在一定程度上改善梯度消失问题。
        # 4. 使得模型对噪声更鲁棒:完全阻断某一部分值的范围可能会使模型对这一部分的输入更加敏感,而LeakyReLU可以缓解这个问题,使模型对噪声输入更加稳定。
        # 所以,总的来说,LeakyReLU是一种修正后的激活函数,它可以防止死亡节点问题,加速收敛,缓解梯度消失问题,使模型对噪声更加鲁棒。这也是为什么该激活函数在深度学习中得到广泛应用的原因。
        # 除了LeakyReLU,其他的修正过的激活函数还有:
        # - ELU:f(x) = max(0,x) + min(0, alpha*(exp(x)-1))
        # - SELE:f(x) = x*(1+(e^(x)-1)*alpha)
        # - GELU:f(x) = x*Phi(x)  (Phi(x)是高斯误差线性单元函数)
        # 它们的作用与LeakyReLU类似,都是为了改进ReLU的一些缺点,得到更优的激活函数。
        # 激活函数:y=x if x>0 else y=leaky*x
    )


# 深度可分离卷积模块
def conv_dw(inp, oup, stride=1, leaky=0.1):
    # inp:输入通道数,oup:输出通道数,stride:步长,leaky:negative_slope
    return nn.Sequential(
        # 深度可分离卷积
        nn.Conv2d(inp, inp, 3, stride, 1, groups=inp, bias=False),
        # groups=inp:每个输入通道自己卷积
        # 3x3的深度可分离卷积,每个输入通道独立卷积,不进行跨channel的互相关联
        # BN
        nn.BatchNorm2d(inp),
        # LeakyReLU
        nn.LeakyReLU(negative_slope=leaky, inplace=True),
        # 点卷积
        nn.Conv2d(inp, oup, 1, 1, 0, bias=False),
        # 1x1的卷积,起到通道数的转换作用,不改变特征图大小
        # BN
        nn.BatchNorm2d(oup),
        # LeakyReLU
        nn.LeakyReLU(negative_slope=leaky, inplace=True),
    )


# MobileNetV1模型
class MobileNetV1(nn.Module):
    def __init__(self):
        super(MobileNetV1, self).__init__()
        # stage1:输入640x640x3,输出320x320x8
        # 用于处理输入图片,获得较浅的特征
        self.stage1 = nn.Sequential(
            # 卷积最大池化,缩小2倍
            conv_bn(3, 8, 2, leaky=0.1),  # 3->8, 640->320
            # 输入3通道,输出8通道,步长为2,输入640x640,输出320x320
            # conv_dw 16
            conv_dw(8, 16, 1),  # 8->16
            # 8通道输入,16通道输出,步长1,输出大小不变
            # conv_dw 32 + conv_dw 32
            conv_dw(16, 32, 2),  # 16->32, 320->160
            # 16通道输入,32通道输出,步长2,输入320x320,输出160x160
            conv_dw(32, 32, 1),  # 32通道输入输出,步长1
            # 32通道输入输出,步长1,输出大小不变
            # conv_dw 64 + conv_dw 64
            conv_dw(32, 64, 2),  # 32->64, 160->80
            # 32通道输入,64通道输出,步长2,输入160x160,输出80x80
            conv_dw(64, 64, 1),  # 64通道输入输出,步长1
            # 64通道输入输出,步长1,输出大小不变
        )
        # stage2:输入80x80x64,输出40x40x128
        # 中间层特征提取模块
        self.stage2 = nn.Sequential(
            conv_dw(64, 128, 2),  # 64->128, 80->40
            # 64通道输入,128通道输出,步长2,输入80x80,输出40x40
            conv_dw(128, 128, 1),  # 128通道输入输出,步长1
            # 128通道输入输出,步长1,输出大小不变
            conv_dw(128, 128, 1),
            # 128通道输入输出,步长1,输出大小不变
            conv_dw(128, 128, 1),
            # 128通道输入输出,步长1,输出大小不变
            conv_dw(128, 128, 1),
            # 128通道输入输出,步长1,输出大小不变
            conv_dw(128, 128, 1),  # 128通道输入输出,步长1
            # 128通道输入输出,步长1,输出大小不变
        )

        # stage3:输入40x40x128,输出20x20x256
        # 用于获取较深层的特征,输出较高维的特征
        self.stage3 = nn.Sequential(
            conv_dw(128, 256, 2),  # 128->256, 40->20
            # 128通道输入,256通道输出,步长2,输入40x40,输出20x20
            conv_dw(256, 256, 1),  # 256通道输入输出,步长1
            # 256通道输入输出,步长1,输出大小不变
        )

        # 自适应平均池化层和全连接层
        self.avg = nn.AdaptiveAvgPool2d((1, 1))
        # 自适应平均池化,输出1x1
        # 这段代码定义了MobileNetV1的最后两层:自适应平均池化层和全连接层。下面对它们进行详细解释:
        # self.avg = nn.AdaptiveAvgPool2d((1, 1))
        # 这行定义了一个自适应平均池化层,池化核的大小是1x1,输出大小也是1x1。
        # 自适应平均池化与平均池化的区别是,它会自适应地根据输入大小选择不同的池化核,使得输出大小是固定的,而不是像平均池化那样,池化核是固定的,输出大小会变化。
        # 这层池化层的作用是:
        # 1. 降维:从256通道的20x20的特征图降维到256维的向量。
        # 2. 起到加权平均的作用:每个值代表了特定通道上20x20个值的平均数,这可以得到该通道的整体响应情况。
        
        self.fc = nn.Linear(256, 1000)
        # 全连接层,输入256维,输出1000类
        # self.fc = nn.Linear(256, 1000)
        # 这行定义了一个全连接层,输入是256维向量,输出是1000维,用于分类。
        # 该全连接层的作用是:
        # 1. 起到分类作用:把256维的特征向量转化为1000维的输出概率,进行图像分类。
        # 2.可以看作是该网络的预测层。
        # 所以,整体来说,自适应平均池化层起到降维和加权平均的作用,而全连接层起到分类和预测的作用,它们共同构成了MobileNetV1的最后两层,实现了对特征的抽象和分类预测。

    def forward(self, x):
        x = self.stage1(x)
        # 经过stage1,输出320x320x8
        
        x = self.stage2(x)
        # 经过stage2,输出40x40x128
        
        x = self.stage3(x)
        # 经过stage3,输出20x20x256
        
        x = self.avg(x)
        # 自适应平均池化,输出1x1
        
        x = x.view(-1, 256)
        # 改变tensor的形状,-1表示由其他维度推断出来
        
        x = self.fc(x)
        return x

该代码实现了MobileNetV1模型的网络结构。下面对代码中的关键部分进行详细解释:

conv_bn函数:这个函数定义了一个卷积层,后面跟着批归一化(Batch Normalization)和LeakyReLU激活函数。卷积层通过3x3卷积核对输入进行卷积操作,并使用步长和填充来控制输出特征图的大小。批归一化层用于加速训练过程并提高模型的稳定性,而LeakyReLU激活函数可以防止梯度消失问题。

conv_dw函数:这个函数定义了一个深度可分离卷积模块,包括深度卷积、批归一化、LeakyReLU激活函数以及点卷积等操作。深度可分离卷积将卷积操作分成两步:先对每个输入通道进行独立的卷积,然后再使用1x1的卷积核进行通道之间的线性组合。这样可以大幅减少参数数量,同时保持较好的特征提取能力。

MobileNetV1类:这个类定义了MobileNetV1模型的网络结构。它包含三个阶段(stage 1、stage 2、stage 3),每个阶段由多个深度可分离卷积模块组成。通过将输入数据依次经过这些阶段,逐渐提取出更高层次的特征表示。

forward方法:这个方法实现了模型的前向传播过程。在该方法中,输入数据首先经过stage1,然后通过stage2和stage3进行进一步处理。最后,通过自适应平均池化层将特征图降维为一个256维的向量,并通过全连接层输出对不同类别的预测结果。

FPN特征金字塔

Alt

  • FPN类:该类定义了特征金字塔网络的结构。它接受输入特征层的通道数列表in_channels_list和输出特征层的通道数out_channels作为参数。在初始化方法中,根据输出通道数是否小于等于64来确定LeakyReLU激活函数的负斜率。
  • output1、output2和output3:分别使用1x1卷积调整输入特征层C3、C4和C5的通道数为out_channels,得到相应的输出特征层。
    merge1和merge2:这两个模块用于融合上采样后的特征层和原始特征层。它们都包含一个1x1卷积层,用于调整通道数。
  • forward方法:该方法实现了FPN的前向传播过程。首先将输入特征层转换为列表形式,并获取三个有效特征层C3、C4和C5。
  • 然后,通过各自的输出模块将每个特征层进行处理,得到相应的输出特征层。接下来,使用最近邻插值法将C5特征层上采样到与C4特征层相同的大小,并将其与C4特征层进行像素级特征融合。
  • 然后,再次使用最近邻插值法将融合后的特征层上采样到与C3特征层相同的大小,并将其与C3特征层进行像素级特征融合。
  • 最后,返回三个尺度的输出特征表达。AIt
class FPN(nn.Module):
    def __init__(self, in_channels_list, out_channels):
        # in_channels_list:输入特征层通道数,out_channels:输出特征层通道数
        super(FPN, self).__init__()
        leaky = 0
        if (out_channels <= 64):  # 如果输出通道数<=64,negative_slope设置为0.1,否则默认为0
            leaky = 0.1

        self.output1 = conv_bn1X1(in_channels_list[0], out_channels, stride=1, leaky=leaky)
        # 获得C3特征层,80x80x64,首先使用1x1卷积调整C3通道数为out_channels
        self.output2 = conv_bn1X1(in_channels_list[1], out_channels, stride=1, leaky=leaky)
        # 获得C4特征层,40x40x64,首先使用1x1卷积调整C4通道数为out_channels
        self.output3 = conv_bn1X1(in_channels_list[2], out_channels, stride=1, leaky=leaky)
        # 获得C5特征层,20x20x64,首先使用1x1卷积调整C5通道数为out_channels

        self.merge1 = conv_bn(out_channels, out_channels, leaky=leaky)
        # 用于融合C3和上采样的C4特征,1x1卷积调整通道数
        self.merge2 = conv_bn(out_channels, out_channels, leaky=leaky)

    # 用于融合C4和上采样的C5特征,1x1卷积调整通道数

    def forward(self, inputs):
        # FPN的前向传播
        inputs = list(inputs.values())
        # 获得三个有效特征层,C3,C4,C5

        output1 = self.output1(inputs[0])
        # 获得C3特征层,80x80xout_channels
        output2 = self.output2(inputs[1])
        # 获得C4特征层,40x40xout_channels
        output3 = self.output3(inputs[2])
        # 获得C5特征层,20x20xout_channels

        up3 = F.interpolate(output3, size=[output2.size(2), output2.size(3)], mode="nearest")
        # 使用最近邻插值上采样,C5特征层上采样到与C4特征层大小相同,40x40
        output2 = output2 + up3
        # C4特征层和上采样的C5特征层进行像素级特征融合
        output2 = self.merge2(output2)
        # 使用1x1卷积融合特征,输出40x40xout_channels

        up2 = F.interpolate(output2, size=[output1.size(2), output1.size(3)], mode="nearest")
        # 将融合后的C4特征层上采样到与C3特征层大小相同,80x80
        output1 = output1 + up2
        # C3特征层和上采样的融合后的C4特征层进行像素级特征融合
        output1 = self.merge1(output1)
        # 使用1x1卷积融合特征,输出80x80xout_channels

        out = [output1, output2, output3]
        # 得到三个尺度80x80xout_channels,
        #           40x40xout_channels和
        #           20x20xout_channels的特征表达
        return out

SSH进一步加强特征提取

ALt
在SSH模块之后,通过分类头部、框的回归头部和关键点回归头部对每个有效特征层进行预测。

  • SSH类:该类定义了SSH模块的结构。它接受输入通道数in_channel和输出通道数out_channel作为参数。在初始化方法中,首先确保输出通道数是4的倍数。然后根据输出通道数是否小于等于64来确定LeakyReLU激活函数的负斜率。
  • conv3X3、conv5X5_1、conv5X5_2、conv7X7_2和conv7x7_3:这些都是卷积层或卷积+批归一化层。其中,conv3X3使用3x3卷积操作将输入特征进行处理;
  • conv5X5_1和conv5X5_2分别用于第一个3x3卷积和第二个3x3卷积操作;conv7X7_2和conv7x7_3分别用于第一个3x3卷积和第二个3x3卷积操作。
  • forward方法:该方法实现了SSH模块的前向传播过程。首先,将输入特征经过不同的卷积操作得到conv3X3、conv5X5和conv7X7三个结果。然后,将这三个结果在通道维度上进行拼接,得到最终的输出特征。最后,通过ReLU激活函数对输出特征进行非线性映射。
  • out:SSH模块的输出结果。
    总结起来,这段代码实现了SSH模块,用于在目标检测任务中提取具有不同感受野的多尺度特征表示。它通过使用不同大小的卷积核和堆叠的卷积操作,在保持高分辨率的同时获取丰富的语义信息。
class SSH(nn.Module):
    def __init__(self, in_channel, out_channel):
        # in_channel:输入通道数,out_channel:输出通道数
        super(SSH, self).__init__()
        assert out_channel % 4 == 0
        # 输出通道数约束为4的倍数
        leaky = 0
        if (out_channel <= 64):
            # 如果输出通道数<=64,leaky=0.1,否则默认为0
            leaky = 0.1
        self.conv3X3 = conv_bn_no_relu(in_channel, out_channel // 2, stride=1)
        # 3x3卷积,步长1,输入通道数in_channel,输出通道数out_channel//2
        self.conv5X5_1 = conv_bn(in_channel, out_channel // 4, stride=1, leaky=leaky)
        # 第一个3x3卷积,步长1,输入通道数in_channel,输出通道数out_channel//4,leaky=0.1
        self.conv5X5_2 = conv_bn_no_relu(out_channel // 4, out_channel // 4, stride=1)
        # 第二个3x3卷积,步长1,输入输出通道数out_channel//4
        self.conv7X7_2 = conv_bn(out_channel // 4, out_channel // 4, stride=1, leaky=leaky)
        # 第一个3x3卷积,步长1,输入输出通道数out_channel//4,leaky=0.1
        self.conv7x7_3 = conv_bn_no_relu(out_channel // 4, out_channel // 4, stride=1)

    # 第二个3x3卷积,步长1,输入输出通道数out_channel//4
    def forward(self, inputs):
        # SSH的前向传播
        conv3X3 = self.conv3X3(inputs)

        conv5X5_1 = self.conv5X5_1(inputs)
        conv5X5 = self.conv5X5_2(conv5X5_1)

        conv7X7_2 = self.conv7X7_2(conv5X5_1)
        conv7X7 = self.conv7x7_3(conv7X7_2)

        # 所有结果堆叠起来
        out = torch.cat([conv3X3, conv5X5, conv7X7], dim=1)
        # 在通道维度上拼接,获得out_channel的特征表达
        out = F.relu(out)
        # ReLU激活
        return out

从特征获取预测结果

Retinaface实现人脸检测与关键点定位-深度学习学习笔记-1_第4张图片

通过第三步,我们已经可以获得SSH1,SSH2,SHH3三个有效特征层了。在获得这三个有效特征层后,我们需要通过这三个有效特征层获得预测结果。

  1. SSH模块: SSH是RetinaFace模型中使用的一种特征增强模块,全称为"Specific Scale Histograms"'。通过在不同尺度上计算特征图的直方图来增强特征表示能力,从而提高人脸检测的准确性。
  2. FPN模块:FPN是RetinaFace模型中使用的一种特征金字塔网络,全称为"Feature Pyramid Network"。它通过将不同层级的特征图进行融合和上采样,以实现多尺度的目标检测和分割。
  3. 主干网络:主干网络是指用于提取输入图像特征的基础网络结构。在RetinaFace模型中,可以选择使用MobileNetV1或ResNet50作为主干网络,用于提取图像特征并生成有效特征层供后续处理使用。
  4. 通道数(inchannels):通道数是指特征图中每个位置包含的特征向量的维度。在RetinaFace模型中,通道数决定了每个特征点所携带的信息量和表达能力。
  5. 锚框(anchor):锚框是一种预定义的边界框,在目标检测任务中被用于对可能存在的物体进行建议。在RetinaFace模型中,使用多个锚框来表示不同尺度和长宽比的人脸。
  6. 1x1卷积(1x1 convolution):1x1卷积是一种卷积操作,其卷积核大小为1x1。在RetinaFace模型中,1x1卷积常用于调整特征图的通道数或进行特征融合等操作。
  7. softmax函数:softmax函数是一种常用的激活函数,可以将一个向量转化为概率分布。在RetinaFace模型中,softmax函数被用于将分类预测结果转换为各个类别的概率值。
  8. 堆叠(concatenate):堆叠指的是将多个张量按照某个维度进行连接。在RetinaFace模型中,通过堆叠不同特征层上的预测结果,可以得到整个图像范围内的人脸检测结果。
  9. 模型配置参数(cfg):模型配置参数包含了构建RetinaFace模型所需的各项设置,如主干网络类型、输入通道数、输出通道数等。这些参数决定了模型的结构和行为。
  10. 预训练权重(pretrained weights):预训练权重是指在大规模数据集上事先训练好的模型参数。在RetinaFace模型中,可以选择是否使用预训练权重来初始化主干网络,以加快模型的收敛和提高性能。
  11. 前向传播(forward):前向传播是指将输入数据从模型的输入层经过各个层级的计算,最终得到输出结果的过程。在RetinaFace模型中,通过前向传播可以进行人脸检测,并得到分类预测、框的回归预测和人脸关键点的回归预测等结果。

通过前面的步骤,我们已经得到了三个有效特征层:SSH1、SSH2和SSH3。现在我们需要利用这些特征层来获得预测结果。

RetinaFace模型的预测结果可以分为三部分:分类预测结果、框的回归预测结果和人脸关键点的回归预测结果。

  • 分类预测结果用于判断先验框内是否包含物体(即人脸)。原版的RetinaFace使用softmax函数进行判断。为了实现这一功能,我们可以使用一个1x1的卷积操作将SSH的通道数调整为num_anchors x 2,其中每个通道代表一个先验框内包含人脸的概率。

  • 框的回归预测结果用于调整先验框以获得更准确的预测框。我们需要四个参数来对先验框进行调整。同样地,我们可以使用一个1x1的卷积操作将SSH的通道数调整为num_anchors x 4,其中每个通道代表一个先验框的调整参数。

  • 人脸关键点的回归预测结果用于调整先验框以获得人脸的关键点位置。每个人脸关键点需要两个调整参数,而且共有五个人脸关键点。为了实现这一功能,我们可以使用一个1x1的卷积操作将SSH的通道数调整为num_anchors x 10(即num_anchors x 5 x 2),其中每个通道代表一个先验框的每个人脸关键点的调整参数。
    总之,通过适当的卷积操作,我们可以从SSH特征层中提取出分类预测结果、框的回归预测结果和人脸关键点的回归预测结果,以实现准确的人脸检测。
    实现代码为:

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.models._utils as _utils
from torchvision import models

from nets.layers import FPN, SSH
# 导入FPN和SSH模块
from nets.mobilenet025 import MobileNetV1


# ---------------------------------------------------#
#   种类预测(是否包含人脸)
# ---------------------------------------------------#
class ClassHead(nn.Module):
    def __init__(self, inchannels=512, num_anchors=2):
        # inchannels: 输入通道数,num_anchors: 锚框数
        super(ClassHead, self).__init__()
        self.num_anchors = num_anchors
        # 锚框数
        self.conv1x1 = nn.Conv2d(inchannels, self.num_anchors * 2, kernel_size=(1, 1), stride=1, padding=0)

    def forward(self, x):
        # 分类类别预测
        out = self.conv1x1(x)
        # 1x1卷积
        out = out.permute(0, 2, 3, 1).contiguous()
        # 维度变换 shape=[b, h, w, num_anchors, 2]
        out = out.view(out.shape[0], -1, 2)
        # reshape为 shape=[b, h*w*num_anchors, 2]
        return out


# ---------------------------------------------------#
#   预测框预测
# ---------------------------------------------------#
class BboxHead(nn.Module):
    def __init__(self, inchannels=512, num_anchors=2):
        # inchannels: 输入通道数,num_anchors: 锚框数
        super(BboxHead, self).__init__()
        self.conv1x1 = nn.Conv2d(inchannels, num_anchors * 4, kernel_size=(1, 1), stride=1, padding=0)

    def forward(self, x):
        # 预测框回归
        out = self.conv1x1(x)
        # 1x1卷积
        out = out.permute(0, 2, 3, 1).contiguous()
        # 维度变换 shape=[b, h, w, num_anchors, 4]
        out = out.view(out.shape[0], -1, 4)
        # reshape为 shape=[b, h*w*num_anchors, 4]
        return out


# ---------------------------------------------------#
#   人脸关键点预测
# ---------------------------------------------------#
class LandmarkHead(nn.Module):
    def __init__(self, inchannels=512, num_anchors=2):
        # inchannels: 输入通道数,num_anchors: 锚框数
        super(LandmarkHead, self).__init__()
        self.conv1x1 = nn.Conv2d(inchannels, num_anchors * 10, kernel_size=(1, 1), stride=1, padding=0)

    def forward(self, x):
        # 人脸关键点回归
        out = self.conv1x1(x)
        # 1x1卷积
        out = out.permute(0, 2, 3, 1).contiguous()
        # 维度变换 shape=[b, h, w, num_anchors, 10]
        out = out.view(out.shape[0], -1, 10)
        # reshape为 shape=[b, h*w*num_anchors, 10]
        return out


class RetinaFace(nn.Module):
    def __init__(self, cfg=None, pretrained=False, mode='train'):
        # cfg: 模型配置参数,pretrained: 是否载入预训练权重,mode: 训练或预测模式
        super(RetinaFace, self).__init__()
        backbone = None
        # 主干网络
        # -------------------------------------------#
        #   选择使用mobilenet0.25、resnet50作为主干
        # -------------------------------------------#
        if cfg['name'] == 'mobilenet0.25':
            backbone = MobileNetV1()
            # 选择MobileNetV1作为主干网络
            if pretrained:
                # 如果载入预训练权重
                checkpoint = torch.load("./model_data/mobilenetV1X0.25_pretrain.tar", map_location=torch.device('cpu'))
                from collections import OrderedDict
                new_state_dict = OrderedDict()
                for k, v in checkpoint['state_dict'].items():
                    name = k[7:]
                    new_state_dict[name] = v
                backbone.load_state_dict(new_state_dict)
        elif cfg['name'] == 'Resnet50':
            backbone = models.resnet50(pretrained=pretrained)
        # 选择ResNet50作为主干网络,并载入预训练权重

        self.body = _utils.IntermediateLayerGetter(backbone, cfg['return_layers'])
        # 使用IntermediateLayerGetter获得指定层的输出
        # -------------------------------------------#
        #   获得每个初步有效特征层的通道数
        #   分别是C3 80, 80, 64
        #       C4 40, 40, 128
        #       C5 20, 20, 256
        # -------------------------------------------#
        in_channels_list = [cfg['in_channel'] * 2, cfg['in_channel'] * 4, cfg['in_channel'] * 8]
        # -------------------------------------------#
        #   利用初步有效特征层构建特征金字塔
        #   分别是output1 80, 80, 64
        #         output2 40, 40, 64
        #         output3 20, 20, 64
        # -------------------------------------------#
        self.fpn = FPN(in_channels_list, cfg['out_channel'])
        # FPN模块
        # -------------------------------------------#
        #   利用ssh模块提高模型感受野
        # -------------------------------------------#
        self.ssh1 = SSH(cfg['out_channel'], cfg['out_channel'])
        self.ssh2 = SSH(cfg['out_channel'], cfg['out_channel'])
        self.ssh3 = SSH(cfg['out_channel'], cfg['out_channel'])
        self.ClassHead = self._make_class_head(fpn_num=3, inchannels=cfg['out_channel'])
        # 分类预测模块
        self.BboxHead = self._make_bbox_head(fpn_num=3, inchannels=cfg['out_channel'])
        # 预测框回归模块
        self.LandmarkHead = self._make_landmark_head(fpn_num=3, inchannels=cfg['out_channel'])
        # 关键点回归模块
        self.mode = mode

    def _make_class_head(self, fpn_num=3, inchannels=64, anchor_num=2):
        # 模块运行模式,训练或预测
        classhead = nn.ModuleList()
        for i in range(fpn_num):
            # 在三个特征层上分别获得分类预测
            classhead.append(ClassHead(inchannels, anchor_num))
        return classhead

    def _make_bbox_head(self, fpn_num=3, inchannels=64, anchor_num=2):
        bboxhead = nn.ModuleList()
        for i in range(fpn_num):
            # 在三个特征层上分别获得预测框回归
            bboxhead.append(BboxHead(inchannels, anchor_num))
        return bboxhead

    def _make_landmark_head(self, fpn_num=3, inchannels=64, anchor_num=2):
        landmarkhead = nn.ModuleList()
        for i in range(fpn_num):
            # 在三个特征层上分别获得人脸关键点回归
            landmarkhead.append(LandmarkHead(inchannels, anchor_num))
        return landmarkhead

    def forward(self, inputs):
        # RetinaFace的前向传播
        out = self.body.forward(inputs)
        # 获得三个shape的有效特征层
        fpn = self.fpn.forward(out)
        # 获得三个shape的有效特征层
        feature1 = self.ssh1(fpn[0])  # 使用SSH模块增强特征
        feature2 = self.ssh2(fpn[1])
        feature3 = self.ssh3(fpn[2])
        features = [feature1, feature2, feature3]
        # 将所有结果进行堆叠
        bbox_regressions = torch.cat([self.BboxHead[i](feature) for i, feature in enumerate(features)], dim=1)
        classifications = torch.cat([self.ClassHead[i](feature) for i, feature in enumerate(features)], dim=1)
        ldm_regressions = torch.cat([self.LandmarkHead[i](feature) for i, feature in enumerate(features)], dim=1)

        if self.mode == 'train':
            output = (bbox_regressions, classifications, ldm_regressions)
            # 训练模式,输出回归目标
        else:
            output = (bbox_regressions, F.softmax(classifications, dim=-1), ldm_regressions)
            # 预测模式,输出回归和分类预测
        return output


这段代码实现了RetinaFace人脸检测模型的前向传播过程。下面是对每个部分的详细注释:

  • ClassHead类:用于进行种类预测,判断是否包含人脸。

    • __init__方法:初始化函数,设置输入通道数和锚框数,并定义一个1x1卷积层来进行分类预测。
    • forward方法:前向传播函数,通过卷积操作得到分类预测结果,并将结果进行维度变换和reshape。
  • BboxHead类:用于进行预测框回归,精确定位人脸的边界框。

    • __init__方法:初始化函数,设置输入通道数和锚框数,并定义一个1x1卷积层来进行预测框回归。
    • forward方法:前向传播函数,通过卷积操作得到预测框回归结果,并将结果进行维度变换和reshape。
  • LandmarkHead类:用于进行人脸关键点预测,识别人脸的关键点位置。

    • __init__方法:初始化函数,设置输入通道数和锚框数,并定义一个1x1卷积层来进行人脸关键点回归。
    • forward方法:前向传播函数,通过卷积操作得到人脸关键点回归结果,并将结果进行维度变换和reshape。
  • RetinaFace类:主要的RetinaFace模型类,包含了分类预测、预测框回归和人脸关键点预测等部分。

    • __init__方法:初始化函数,根据配置参数选择主干网络(MobileNetV1或ResNet50),并加载预训练权重。然后使用IntermediateLayerGetter获取指定层的输出作为初步有效特征层。接着构建特征金字塔FPN,并利用SSH模块增强特征表示。最后创建分类预测模块、预测框回归模块和人脸关键点预测模块。
    • _make_class_head方法:根据指定的特征金字塔层数,在每个特征层上创建分类预测模块,并返回一个包含这些模块的ModuleList。
    • _make_bbox_head方法:根据指定的特征金字塔层数,在每个特征层上创建预测框回归模块,并返回一个包含这些模块的ModuleList。
    • _make_landmark_head方法:根据指定的特征金字塔层数,在每个特征层上创建人脸关键点预测模块,并返回一个包含这些模块的ModuleList。
    • forward方法:RetinaFace模型的前向传播函数。首先通过主干网络获取有效特征层,然后经过FPN得到一系列特征图。接着利用SSH模块增强特征表示。最后将特征图输入到分类预测模块、预测框回归模块和人脸关键点预测模块中,生成相应的预测结果并进行堆叠。在训练模式下,输出为回归目标;在预测模式下,输出为回归和分类预测。
  • 总之,这段代码实现了RetinaFace人脸检测模型的各个组件,并将它们整合在一起进行前向传播。通过特征提取、特征金字塔构建以及分类预测、预测框回归和人脸关键点预测等任务的计算,模型能够准确地检测出图像中的人脸。

预测结果的解码

AIt
通过第四步,我们可以获得三个有效特征层SSH1、SSH2、SSH3。

这三个有效特征层相当于将整幅图像划分成不同大小的网格,当我们输入进来的图像是(640, 640, 3)的时候。

SSH1的shape为(80, 80, 64);
SSH2的shape为(40, 40, 64);
SSH3的shape为(20, 20, 64)

SSH1就表示将原图像划分成80x80的网格;SSH2就表示将原图像划分成40x40的网格;SSH3就表示将原图像划分成20x20的网格,每个网格上有两个先验框,每个先验框代表图片上的一定区域。

Retinaface的预测结果用来判断先验框内部是否包含人脸,并且对包含人脸的先验框进行调整获得预测框与人脸关键点。

1、分类预测结果用于判断先验框内部是否包含物体,我们可以利用一个1x1的卷积,将SSH的通道数调整成num_anchors x 2,用于代表每个先验框内部包含人脸的概率。

2、框的回归预测结果用于对先验框进行调整获得预测框,我们需要用四个参数对先验框进行调整。此时我们可以利用一个1x1的卷积,将SSH的通道数调整成num_anchors x 4,用于代表每个先验框的调整参数。每个先验框的四个调整参数中,前两个用于对先验框的中心进行调整,后两个用于对先验框的宽高进行调整。

3、人脸关键点的回归预测结果用于对先验框进行调整获得人脸关键点,每一个人脸关键点需要两个调整参数,一共有五个人脸关键点。此时我们可以利用一个1x1的卷积,将SSH的通道数调整成num_anchors x 10(num_anchors x 5 x 2),用于代表每个先验框的每个人脸关键点的调整。每个人脸关键点的两个调整参数用于对先验框中心的x、y轴进行调整获得关键点坐标。

完成调整、判断之后,还需要进行非极大移植。
下图是经过非极大抑制的。

下图是未经过非极大抑制的。

可以很明显的看出来,未经过非极大抑制的图片有许多重复的框,这些框都指向了同一个物体!

可以用一句话概括非极大抑制的功能就是:

筛选出一定区域内属于同一种类得分最大的框。
这段代码包含了一些辅助函数,用于在RetinaFace人脸检测模型中进行预测结果的解码和后处理。

全部实现代码如下:

def decode(loc, priors, variances):
    # 中心解码,宽高解码
    boxes = torch.cat((priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:],
                    priors[:, 2:] * torch.exp(loc[:, 2:] * variances[1])), 1)
    boxes[:, :2] -= boxes[:, 2:] / 2
    boxes[:, 2:] += boxes[:, :2]
    return boxes

def decode_landm(pre, priors, variances):
    # 关键点解码
    landms = torch.cat((priors[:, :2] + pre[:, :2] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 2:4] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 4:6] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 6:8] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 8:10] * variances[0] * priors[:, 2:],
                        ), dim=1)
    return landms


def non_max_suppression(boxes, conf_thres=0.5, nms_thres=0.3):
    detection = boxes
    # 1、找出该图片中得分大于门限函数的框。在进行重合框筛选前就进行得分的筛选可以大幅度减少框的数量。
    mask = detection[:,4] >= conf_thres
    detection = detection[mask]
    if not np.shape(detection)[0]:
        return []

    best_box = []
    scores = detection[:,4]
    # 2、根据得分对框进行从大到小排序。
    arg_sort = np.argsort(scores)[::-1]
    detection = detection[arg_sort]

    while np.shape(detection)[0]>0:
        # 3、每次取出得分最大的框,计算其与其它所有预测框的重合程度,重合程度过大的则剔除。
        best_box.append(detection[0])
        if len(detection) == 1:
            break
        ious = iou(best_box[-1],detection[1:])
        detection = detection[1:][ious<nms_thres]

    return np.array(best_box)

def iou(b1,b2):
    b1_x1, b1_y1, b1_x2, b1_y2 = b1[0], b1[1], b1[2], b1[3]
    b2_x1, b2_y1, b2_x2, b2_y2 = b2[:, 0], b2[:, 1], b2[:, 2], b2[:, 3]

    inter_rect_x1 = np.maximum(b1_x1, b2_x1)
    inter_rect_y1 = np.maximum(b1_y1, b2_y1)
    inter_rect_x2 = np.minimum(b1_x2, b2_x2)
    inter_rect_y2 = np.minimum(b1_y2, b2_y2)
    
    inter_area = np.maximum(inter_rect_x2 - inter_rect_x1, 0) * \
                 np.maximum(inter_rect_y2 - inter_rect_y1, 0)
    
    area_b1 = (b1_x2-b1_x1)*(b1_y2-b1_y1)
    area_b2 = (b2_x2-b2_x1)*(b2_y2-b2_y1)
    
    iou = inter_area/np.maximum((area_b1+area_b2-inter_area),1e-6)
    return iou

这段代码包含了一些辅助函数,用于在RetinaFace人脸检测模型中进行预测结果的解码和后处理。下面是对每个函数的详细注释:

  • decode函数:该函数用于对定位信息进行解码,得到预测框的坐标。

    • 参数:
      • loc:相对位置偏移量(location offset),大小为[N, 4],N表示先验框的数量,4表示(x, y, w, h)四个值。
      • priors:先验框的坐标,大小为[N, 4],4表示(xmin, ymin, xmax, ymax)四个值。
      • variances:方差参数,大小为[2],分别表示中心点和宽高的方差。
    • 返回值:
      • boxes:解码后的预测框坐标,大小为[N, 4],4表示(xmin, ymin, xmax, ymax)四个值。
  • decode_landm函数:该函数用于对关键点信息进行解码,得到预测的人脸关键点坐标。

    • 参数:
      • pre:相对位置偏移量(location offset),大小为[N, 10],N表示先验框的数量,10表示五个关键点的(x, y)坐标。
      • priors:先验框的坐标,大小为[N, 4],4表示(xmin, ymin, xmax, ymax)四个值。
      • variances:方差参数,大小为[2],表示中心点和宽高的方差。
    • 返回值:
      • landms:解码后的人脸关键点坐标,大小为[N, 10],10表示五个关键点的(x, y)坐标。
  • non_max_suppression函数:该函数实现了非极大值抑制(NMS)算法,用于筛选重叠度较低的预测框。

    • 参数:
      • boxes:预测框列表,大小为[M, 5],5表示(xmin, ymin, xmax, ymax, score)五个值。
      • conf_thres:置信度阈值,默认为0.5。
      • nms_thres:NMS阈值,默认为0.3。
    • 返回值:
      • best_box:经过非极大值抑制处理后保留下来的预测框,大小为[K, 5],K表示保留下来的预测框数量。
  • iou函数:该函数用于计算两个矩形框之间的交并比(IOU)。

    • 参数:
      • b1:第一个矩形框的坐标,大小为[4],分别表示(xmin, ymin, xmax, ymax)四个值。
      • b2:第二个矩形框列表,大小为[N, 4],N表示第二个矩形框的数量。
    • 返回值:
      • iou:两个矩形框之间的交并比,大小为[N]。

训练部分

真实框的处理过程

decode函数:根据定位信息、先验框和方差参数,将预测框的编码值解码为实际坐标。
decode_landm函数:根据关键点信息、先验框和方差参数,将关键点的编码值解码为实际坐标。
non_max_suppression函数:使用非极大值抑制算法对预测框进行筛选,去除重叠度较高的冗余框。
iou函数:计算两个矩形框之间的交并比(IOU)。
match函数:根据阈值和匹配结果,将真实框和关键点信息进行匹配和编码操作。
encode函数:对真实框进行编码,将其转换为模型需要的形式。
encode_landm函数:对关键点信息进行编码,将其转换为模型需要的形式。
这些辅助函数在RetinaFace人脸检测模型中起到了解码、筛选和编码等作用,帮助我们处理预测结果、筛选出准确的目标框,并进行数据编码以便于训练和评估。

def point_form(boxes):
    # 转换形式,转换成左上角右下角的形式
    return torch.cat((boxes[:, :2] - boxes[:, 2:]/2,     # xmin, ymin
                     boxes[:, :2] + boxes[:, 2:]/2), 1)  # xmax, ymax


def center_size(boxes):
    # 转换成中心宽高的形式
    return torch.cat((boxes[:, 2:] + boxes[:, :2])/2,  # cx, cy
                     boxes[:, 2:] - boxes[:, :2], 1)  # w, h


def intersect(box_a, box_b):
    # 计算所有真实框和先验框的交面积
    A = box_a.size(0)
    B = box_b.size(0)
    max_xy = torch.min(box_a[:, 2:].unsqueeze(1).expand(A, B, 2),
                       box_b[:, 2:].unsqueeze(0).expand(A, B, 2))
    min_xy = torch.max(box_a[:, :2].unsqueeze(1).expand(A, B, 2),
                       box_b[:, :2].unsqueeze(0).expand(A, B, 2))
    inter = torch.clamp((max_xy - min_xy), min=0)
    return inter[:, :, 0] * inter[:, :, 1]


def jaccard(box_a, box_b):
    # 计算所有真实框和先验框的交并比
    # 行为真实框,列为先验框
    inter = intersect(box_a, box_b)
    area_a = ((box_a[:, 2]-box_a[:, 0]) *
              (box_a[:, 3]-box_a[:, 1])).unsqueeze(1).expand_as(inter)  # [A,B]
    area_b = ((box_b[:, 2]-box_b[:, 0]) *
              (box_b[:, 3]-box_b[:, 1])).unsqueeze(0).expand_as(inter)  # [A,B]
    union = area_a + area_b - inter
    return inter / union  # [A,B]

def match(threshold, truths, priors, variances, labels, landms, loc_t, conf_t, landm_t, idx):
    # 计算交并比
    overlaps = jaccard(
        truths,
        point_form(priors)
    )

    best_prior_overlap, best_prior_idx = overlaps.max(1, keepdim=True)
    best_prior_idx.squeeze_(1)
    best_prior_overlap.squeeze_(1)

    # 计算每个先验框最对应的真实框
    best_truth_overlap, best_truth_idx = overlaps.max(0, keepdim=True)
    best_truth_idx.squeeze_(0)
    best_truth_overlap.squeeze_(0)

    # 找到与真实框重合程度最好的先验框,用于保证每个真实框都要有对应的一个先验框
    best_truth_overlap.index_fill_(0, best_prior_idx, 2)
    # 对best_truth_idx内容进行设置
    for j in range(best_prior_idx.size(0)):
        best_truth_idx[best_prior_idx[j]] = j

    # Shape: [num_priors,4] 此处为每一个anchor对应的bbox取出来
    matches = truths[best_truth_idx]            
    # Shape: [num_priors] 此处为每一个anchor对应的label取出来
    conf = labels[best_truth_idx]        
           
    conf[best_truth_overlap < threshold] = 0    
    loc = encode(matches, priors, variances)

    matches_landm = landms[best_truth_idx]
    landm = encode_landm(matches_landm, priors, variances)
    loc_t[idx] = loc    # [num_priors,4] encoded offsets to learn
    conf_t[idx] = conf  # [num_priors] top class label for each prior
    landm_t[idx] = landm


def encode(matched, priors, variances):
    # 进行编码的操作
    g_cxcy = (matched[:, :2] + matched[:, 2:])/2 - priors[:, :2]
    # 中心编码
    g_cxcy /= (variances[0] * priors[:, 2:])
    
    # 宽高编码
    g_wh = (matched[:, 2:] - matched[:, :2]) / priors[:, 2:]
    g_wh = torch.log(g_wh) / variances[1]
    return torch.cat([g_cxcy, g_wh], 1)  # [num_priors,4]

def encode_landm(matched, priors, variances):

    matched = torch.reshape(matched, (matched.size(0), 5, 2))
    priors_cx = priors[:, 0].unsqueeze(1).expand(matched.size(0), 5).unsqueeze(2)
    priors_cy = priors[:, 1].unsqueeze(1).expand(matched.size(0), 5).unsqueeze(2)
    priors_w = priors[:, 2].unsqueeze(1).expand(matched.size(0), 5).unsqueeze(2)
    priors_h = priors[:, 3].unsqueeze(1).expand(matched.size(0), 5).unsqueeze(2)
    priors = torch.cat([priors_cx, priors_cy, priors_w, priors_h], dim=2)

    # 减去中心后除上宽高
    g_cxcy = matched[:, :, :2] - priors[:, :, :2]
    g_cxcy /= (variances[0] * priors[:, :, 2:])
    g_cxcy = g_cxcy.reshape(g_cxcy.size(0), -1)
    return g_cxcy

利用处理完的真实框与对应图片的预测结果计算loss

  • MultiBoxLoss类:多任务损失函数的定义。

    • __init__方法:初始化函数,设置损失函数所需的参数和配置。
      • num_classes:目标类别的数量,对于RetinaFace来说,为2(包含人脸和背景)。
      • overlap_thresh:匹配先验框与真实框时的交并比阈值。
      • neg_pos:负样本和正样本的比率。
      • cuda:是否使用GPU加速,默认为True。
    • forward方法:前向传播函数,根据预测结果、先验框和真实框计算多任务损失。
      • predictions:模型的预测结果,包括定位信息、分类置信度和关键点位置。
      • priors:先验框的坐标。
      • targets:真实框和关键点的标签。
    • 返回值:定位损失、分类损失和关键点损失。
      总之,该代码定义了RetinaFace模型中用于训练过程中计算多任务损失的辅助函数,并提供了一个整合了这些辅助函数的MultiBoxLoss类,用于计算定位损失、分类损失和关键点损失。这些损失函数是训练RetinaFace模型时的重要指标,帮助模型学习准确地检测人脸并回归出精确的框和关键点位置。
      实现代码如下:
rgb_mean = (104, 117, 123) # bgr order
class MultiBoxLoss(nn.Module):
    def __init__(self, num_classes, overlap_thresh, neg_pos, cuda=True):
        super(MultiBoxLoss, self).__init__()
        # 对于retinaface而言num_classes等于2
        self.num_classes = num_classes
        # 重合程度在多少以上认为该先验框可以用来预测
        self.threshold = overlap_thresh
        # 正负样本的比率
        self.negpos_ratio = neg_pos
        self.variance = [0.1, 0.2]
        self.cuda = cuda

    def forward(self, predictions, priors, targets):
        loc_data, conf_data, landm_data = predictions
        priors = priors
        num = loc_data.size(0)
        num_priors = (priors.size(0))

        # match priors (default boxes) and ground truth boxes
        loc_t = torch.Tensor(num, num_priors, 4)
        landm_t = torch.Tensor(num, num_priors, 10)
        conf_t = torch.LongTensor(num, num_priors)
        for idx in range(num):
            truths = targets[idx][:, :4].data
            labels = targets[idx][:, -1].data
            landms = targets[idx][:, 4:14].data
            defaults = priors.data
            match(self.threshold, truths, defaults, self.variance, labels, landms, loc_t, conf_t, landm_t, idx)
            
        zeros = torch.tensor(0)
        if self.cuda:
            loc_t = loc_t.cuda()
            conf_t = conf_t.cuda()
            landm_t = landm_t.cuda()
            zeros = zeros.cuda()

        # landm Loss (Smooth L1)
        # Shape: [batch,num_priors,10]
        pos1 = conf_t > zeros
        num_pos_landm = pos1.long().sum(1, keepdim=True)
        N1 = max(num_pos_landm.data.sum().float(), 1)
        
        pos_idx1 = pos1.unsqueeze(pos1.dim()).expand_as(landm_data)
        landm_p = landm_data[pos_idx1].view(-1, 10)
        landm_t = landm_t[pos_idx1].view(-1, 10)
        loss_landm = F.smooth_l1_loss(landm_p, landm_t, reduction='sum')

        pos = conf_t != zeros
        conf_t[pos] = 1

        # Localization Loss (Smooth L1)
        # Shape: [batch,num_priors,4]
        pos_idx = pos.unsqueeze(pos.dim()).expand_as(loc_data)
        loc_p = loc_data[pos_idx].view(-1, 4)
        loc_t = loc_t[pos_idx].view(-1, 4)
        loss_l = F.smooth_l1_loss(loc_p, loc_t, reduction='sum')

        # Compute max conf across batch for hard negative mining
        batch_conf = conf_data.view(-1, self.num_classes)
        loss_c = log_sum_exp(batch_conf) - batch_conf.gather(1, conf_t.view(-1, 1))

        # Hard Negative Mining
        loss_c[pos.view(-1, 1)] = 0 # filter out pos boxes for now
        loss_c = loss_c.view(num, -1)
        _, loss_idx = loss_c.sort(1, descending=True)
        _, idx_rank = loss_idx.sort(1)
        num_pos = pos.long().sum(1, keepdim=True)
        num_neg = torch.clamp(self.negpos_ratio*num_pos, max=pos.size(1)-1)
        neg = idx_rank < num_neg.expand_as(idx_rank)

        # Confidence Loss Including Positive and Negative Examples
        pos_idx = pos.unsqueeze(2).expand_as(conf_data)
        neg_idx = neg.unsqueeze(2).expand_as(conf_data)
        conf_p = conf_data[(pos_idx+neg_idx).gt(0)].view(-1,self.num_classes)
        targets_weighted = conf_t[(pos+neg).gt(0)]
        loss_c = F.cross_entropy(conf_p, targets_weighted, reduction='sum')

        # Sum of losses: L(x,c,l,g) = (Lconf(x, c) + αLloc(x,l,g)) / N
        N = max(num_pos.data.sum().float(), 1)
        loss_l /= N
        loss_c /= N
        loss_landm /= N1

        return loss_l, loss_c, loss_landm

你可能感兴趣的:(python,深度学习,深度学习,学习,笔记)