关于基于SPFCN库位检测算法的解读与源码分析

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

前言

SPFCN是一个偏工程性且实用的车库位检测算法,本人菜鸟一个,又看不到有关此论文的解读,所以在此写下关于自己的解读,希望能帮助大家,更是在写文章的过程中一边梳理和理清思路,文章中可能出现一些错误,请大佬们指正。

一、SPFCN的算法流程

SPFCN算法是用Stacked Hourglass Network作为基本结构,其具体网络结构和示意图下图所示,输入尺寸为224*224BEV(鸟瞰图),输出入口点,入口线,边界线的heatmap。

关于基于SPFCN库位检测算法的解读与源码分析

Hourglass的具体网络结构图

关于基于SPFCN库位检测算法的解读与源码分析

Hourglass的网络结构示意图

该模型的主要特点在于其SP模块(Select-Prune Module),SP模块分为Select模块和Prune模块两部分,Select模块主要用于选择拥有更合适的感受野的卷积核。Prune模块负责修剪卷积核中的通道。这两个模块我们到后面再进行详细说明:而这个网络模型主要进行三个阶段来进行训练,分别为 预训练阶段选择阶段修剪阶段,不同阶段使用不同的损失函数。所以我们主要从这三个阶段来进行解读。

; 二、训练阶段

1.预训练阶段

数据预处理

加载自己的数据集

该有关数据集的处理论文中并无详细说明,所以这里通过源码来进行理解和解释说明,该加载数据集并进行预处理的代码主体在SPFCN/dataset/dataset.py中。下面源码解读:

import numpy as np
from scipy.io import loadmat
from torch.utils.data import Dataset
import glob

给出二维高斯核的分布大于0.1,方便后面代码进行高斯膨胀
GAUSSIAN_VALUE = np.array([[0.0111, 0.0388, 0.0821, 0.1054, 0.0821, 0.0388, 0.0111],
                           [0.0388, 0.1353, 0.2865, 0.3679, 0.2865, 0.1353, 0.0388],
                           [0.0821, 0.2865, 0.6065, 0.7788, 0.6065, 0.2865, 0.0821],
                           [0.1054, 0.3679, 0.7788, 1.0000, 0.7788, 0.3679, 0.1054],
                           [0.0821, 0.2865, 0.6065, 0.7788, 0.6065, 0.2865, 0.0821],
                           [0.0388, 0.1353, 0.2865, 0.3679, 0.2865, 0.1353, 0.0388],
                           [0.0111, 0.0388, 0.0821, 0.1054, 0.0821, 0.0388, 0.0111]])

注:这是个7*7的高斯核,这在后面的高斯膨胀运算非常重要。

class VisionParkingSlotDataset(Dataset):
    def __init__(self, image_path, label_path, data_size, resolution):
        self.length = data_size
        # 这是储存图片的列表
        self.image_list = []
        # 这是储存数据标签的列表
        self.label_list = []
        # 这是为了计数,防止数据的长度超过定义的需求
        index = 0
        # for item_name in os.listdir(image_path):
        for item_name in glob.glob(os.path.join(image_path,'*.jpg')):
            # item_label = loadmat("%s%s.mat" % (label_path, item_name[:-4]))
            item_label = loadmat("%s.mat" % (item_name[:-4]))
            slots = item_label['slots']
            if len(slots) > 0:
                item_image = cv2.resize(cv2.imread(image_path + item_name),(resolution,resolution))

                item_image = np.transpose(item_image, (2, 0, 1))
                self.image_list.append(item_image)

注:.mat里的内容的实例如下

关于基于SPFCN库位检测算法的解读与源码分析
                marks = item_label['marks']
                # 获取到入口点和其向量的所对应的x,y,cos值,sin值所形成的列表
                mark_label = self._get_mark_label(marks, slots, resolution)
                # 初始化slot的标签值
                slot_label = np.zeros([3, resolution, resolution])
                for mark in mark_label:
                    # 去第一个通道的像素值作为灰度值,将点进行高斯膨胀,从点向外左边右边都是3个像素的后键入高斯模糊值
                    slot_label[0, mark[1] - 3:mark[1] + 4, mark[0] - 3:mark[0] + 4] += GAUSSIAN_VALUE
                    # 第二个加入cos值
                    slot_label[1, mark[1] - 3:mark[1] + 4, mark[0] - 3:mark[0] + 4] += mark[2]
                    # 第三个加入sin值
                    slot_label[2, mark[1] - 3:mark[1] + 4, mark[0] - 3:mark[0] + 4] += mark[3]
                self.label_list.append(slot_label)

                index += 1
                if index == data_size:
                    break

注:slot_label[0, mark[1] – 3:mark[1] + 4, mark[0] – 3:mark[0] + 4] += GAUSSIAN_VALUE
这行为什么是mark[1] – 3, mark[1] + 4)解释了为什么是7*7的高斯核能进行矩阵相加。

    @staticmethod
    def _get_mark_label(marks, slots, resolution):
"""
        :param marks:       x, y
        :param slots:       pt1, pt2, _, rotate_angle
        :param resolution:  224x224
        :return:
"""
        # temp_mark_label中的变量有,标签入口点的x值,y值,cos值,sin值
        temp_mark_label = []
        for mark in marks:
            mark_x_re, mark_y_re = mark[0] * resolution / 600, mark[1] * resolution / 600
            temp_mark_label.append([mark_x_re, mark_y_re, [], []])

        for slot in slots:
            # mark_vector是指两个入口点之间在图像坐标系中的向量。
            mark_vector = marks[slot[1] - 1] - marks[slot[0] - 1]
            # print(f"mark vector :{mark_vector}")
            # mark_vector[0]即为入口点的x坐标, mark_vector[1]即为入口点的y坐标。
            mark_vector_length = np.sqrt(mark_vector[0] ** 2 + mark_vector[1] ** 2)
            # 当它的向量为正,即证明该向量的单位向量与图像坐标系成锐角,否则就问大于等于90度的角
            if mark_vector[0] > 0:
                mark_direction = np.arcsin(mark_vector[1] / mark_vector_length)
            else:
                mark_direction = np.pi - np.arcsin(mark_vector[1] / mark_vector_length)
            slot_direction = mark_direction - slot[3] * np.pi / 180
            slot_cos = np.cos(slot_direction)
            slot_sin = np.sin(slot_direction)

            temp_mark_label[slot[0] - 1][2].append(slot_cos)
            temp_mark_label[slot[0] - 1][3].append(slot_sin)
            temp_mark_label[slot[1] - 1][2].append(slot_cos)
            temp_mark_label[slot[1] - 1][3].append(slot_sin)

        mark_label = []
        for mark in temp_mark_label:
            if len(mark[2]) > 0:
                mark_cos = np.mean(mark[2])
                mark_sin = np.mean(mark[3])
                # 求数据集中标记角度均值
                mark_angle_base = np.sqrt(mark_cos ** 2 + mark_sin ** 2)
                # 这里涉及到关于角度的均值,这个有关知识可以参考一下链接
                # https://blog.csdn.net/liuchengzimozigreat/article/details/83068696?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164931064216782246446180%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=164931064216782246446180&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-22-83068696.142^v5^pc_search_insert_es_download,157^v4^control&utm_term=%E6%B1%82%E6%95%B0%E6%8D%AE%E9%9B%86%E8%A7%92%E5%BA%A6%E7%9A%84%E5%9D%87%E5%80%BC&spm=1018.2226.3001.4187
                mark_cos = mark_cos / mark_angle_base
                mark_sin = mark_sin / mark_angle_base
                mark_label.append([int(round(mark[0])), int(round(mark[1])), mark_cos, mark_sin])
        return mark_label

    def __getitem__(self, item):
        return self.image_list[item], self.label_list[item]

    def __len__(self):
        return self.length

关于基于SPFCN库位检测算法的解读与源码分析

2. 训练阶段

在了解训练阶段的源码讲解前,必须要了解并且结合他的网络结构

网络结构

在讲SPFCN网络前,必须讲解Stacked Hourglass Network(SHN)

Backone:Stacked Hourglass Network(SHN)

SHN的主要贡献在于利用多尺度特征来识别姿态。以前估计姿态的网络结构,大多只使用最后一层的卷积特征,这样会造成信息的丢失。事实上,对于姿态估计这种关联型任务,全身不同的关节点,并不是在相同的feature map上具有最好的识别精度。举例来说,胳膊可能在第3层的feature map上容易识别,而头部在第5层上更容易识别,见下图。所以,需要设计一种可以同时使用多个feature map的网络结构。

关于基于SPFCN库位检测算法的解读与源码分析

而在SPFCN论文中阐述网络中是用和SHN网络结构相似的网络,即设计的架构相同,但是模块组成不同,接下来将会从局部到整体,来说明论文中的backone为什么会叫作Hourglass-like Network Structture, 而不是直接叫做SHN。

使用下面代码,通过tensorboard将网络结构显示出来。

from torch.utils.tensorboard import SummaryWriter
from SPFCN.model.network import SlotNetwork
import torch
import os

os.environ["CUDA_VISIBLE_DEVICES"] = "0"

writer = SummaryWriter("runs/models")
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
writer.add_graph(SlotNetwork([32, 44, 64, 92, 128]).to(device),
                 input_to_model=torch.randn(1, 3, 224, 224).to(device),verbose=True)

在命令行运行:

tensorboard --logdir=./runs/model --host=服务器的IP地址

显示的网络模型是这样的

关于基于SPFCN库位检测算法的解读与源码分析

在将hourglass展开,即为下图所示:

关于基于SPFCN库位检测算法的解读与源码分析

如上图所示,这个hourglass的模块是有SP Module组合而成,而SP Module的结构是由Select Kernel所组成的,而Select Kernel是由Basic Module和 CEN模块组成的,而CEN网络是一个MLP模块称为贡献评估网络(CEN)。而论文说的不是太清晰,原文是这样说的:选择模块有几个具有不同膨胀值的候选卷积核,其任务是从中选择最佳的卷积核。输入特征被输入到每个内核中,贡献由一个称为贡献评估网络(CEN)的MLP块进行评估,但是源码中的核在训练时并没有用空洞卷积,所以应该结合源码来看。Select Kernel的网络结构图如下:

关于基于SPFCN库位检测算法的解读与源码分析

构成Select Kernel的组件源码如下图所示:

完成对最佳膨胀值的确定,一开始用的是普通卷积核看一看效果。这个模块只在训练上进行。测试时将会退化成真正的空洞卷积。
class SelectKernel(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        nn.Module.__init__(self)
        self.conv3 = BasicModule(in_channels, out_channels, 3, stride=stride)
        self.conv5 = BasicModule(in_channels, out_channels, 5, stride=stride)
        self.cen = nn.Sequential(nn.Linear(8, 8), nn.ReLU(inplace=True), nn.Linear(8, 2), nn.Softmax(dim=0))

        self.alpha = torch.Tensor([1., 0.])

关于基于SPFCN库位检测算法的解读与源码分析
class BasicModule(nn.Module):
    def __init__(self, in_channels, out_channels, kernel=3, stride=1):
        nn.Module.__init__(self)
        # 标准的CBL模块
        self.conv = nn.Conv2d(in_channels, out_channels, kernel, stride=stride, padding=(kernel - 1) // 2)
        self.bn = nn.BatchNorm2d(out_channels)
        self.activate = nn.ReLU(inplace=True)

    # 前向传播
    def forward(self, feature):
        return self.activate(self.bn(self.conv(feature)))

可以看出Basic Module是一个典型的CBL模块。刚刚上面讲了关于Select Kernel中模块的信息,下面将详细地解释Select Kernel训练时的机制和原理(自己理解的,可能不太准确,请斧正)

    def forward(self, feature):
        # 这步是将basicblock中有关weight的变量全部提取出来,因为经过了BN层主要是计算他里面的weight的值的均值和均差。
        vector = torch.cat(
            [torch.mean(self.conv3.bn.running_mean).view(1), torch.std(self.conv3.bn.running_mean).view(1),
             torch.mean(self.conv3.bn.running_var).view(1), torch.std(self.conv3.bn.running_var).view(1),
             torch.mean(self.conv5.bn.running_mean).view(1), torch.std(self.conv5.bn.running_mean).view(1),
             torch.mean(self.conv5.bn.running_var).view(1), torch.std(self.conv5.bn.running_var).view(1)], dim=0)
        # 经过感知机,经过反馈后,在进行aplha值的反向传播和调整,返回alpha值
        self.alpha = self.cen(vector)
        return self.alpha[0] * self.conv3(feature) + self.alpha[1] * self.conv5(feature)

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

白化操作可以使输入的特征分布具有相同的均值和方差,固定了每一层的输入分布,从而加速网络的收敛。然而,白化操作虽然从一定程度上避免了梯度饱和,但也限制了网络中数据的表达能力,浅层学到的参数会被白化操作屏蔽掉,因此,BN层在白化操作后又增加了一个线性变换操作,让数据尽可能地恢复本身的表达能力,如下面的公式和上面的公式所示:

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

论文中提及到了可以新增一个卷积贡献分支,比如7*7的候选核,其公式也已经给予,如下:

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

为了突出显示最优核(为了看

关于基于SPFCN库位检测算法的解读与源码分析的大小,最终只留下关于基于SPFCN库位检测算法的解读与源码分析最大的卷积核),我们需要获得不同核的稀疏权重。考虑到这一点由于关于基于SPFCN库位检测算法的解读与源码分析是经过softmax,作者设计了以下正则项来实现这一点:

关于基于SPFCN库位检测算法的解读与源码分析

因为:

关于基于SPFCN库位检测算法的解读与源码分析

这个正则项是如何设计这权重稀疏的,以下是我的猜测。

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

关于基于SPFCN库位检测算法的解读与源码分析

所以这个损失函数就是:

关于基于SPFCN库位检测算法的解读与源码分析

相关源码如下:

经过感知机,经过反馈后,在进行aplha值的反向传播和调整,返回alpha值
self.alpha = self.cen(vector)
return self.alpha[0] * self.conv3(feature) + self.alpha[1] * self.conv5(feature)

关于基于SPFCN库位检测算法的解读与源码分析
  # 完成对候选卷积核的筛选,当它的贡献值大于0.9时,选择大于0.9值的卷积核,否则就舍弃卷积核
    def auto_select(self, threshold=0.9):
        return self.conv3 if self.alpha[0] > threshold else self.conv5 if self.alpha[1] > threshold else None

    # 完成对候选卷积核的筛选,当3*3卷积核贡献值大于5*5的卷积值,则选择3*3,反之,则亦然
    def enforce_select(self):
        return self.conv3 if self.alpha[0] > self.alpha[1] else self.conv5

上述代码就是选择贡献值大的卷积核,这里有两种选择方式,论文中也没有解释,所以这里结合源码来解释一下,解释的注释已经附上。

    # 通过设定的正则化(损失函数)来得到score
    def get_regularization(self):
        # 如果需要链接的是Select Module,则其得到的正则化值为alpha值-torch.log(torch.sum(alpha**2)),乘10论文中的lamada值的
        if isinstance(self.conv_module, SelectKernel):
            return -10 * torch.log(self.conv_module.alpha[0] ** 2 + self.conv_module.alpha[1] ** 2)

上述源码就是解释了CEN的正则化项,公式且注释也赋予上面了。

总结

以上是训练时的内容,以后有时间在继续补充。

Original: https://blog.csdn.net/Jack003366/article/details/124100656
Author: Jack003366
Title: 关于基于SPFCN库位检测算法的解读与源码分析

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/597497/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球