[深度学习] magic point

一、相关论文

Toward Geometric Deep SLAM

SuperPoint: Self-Supervised Interest Point Detection and Description

github上的pytorch复现项目:https://github.com/eric-yyjau/pytorch-superpoint

需要在这个项目中找出用于提取图像中特征点的网络 magic point部分具体是怎么实现的,然后再尝试自己复现

二、源码阅读(弄清楚所有实现细节)

样本数据跟标签预处理部分

样本图像:[深度学习] magic point_第1张图片

H=120,W=160

标签:

标签是一个.npy文件,储存了一个矩阵,表示了样本图像中特征点的数目以及每个特征点的坐标
[深度学习] magic point_第2张图片

标签预处理(SyntheticDataset_gaussian.py 文件中 381行开始)

[深度学习] magic point_第3张图片
总的来说,是确保了 .npy文件中每个点的坐标都大于等于0且在图像范围内,即确保坐标是有效的

[深度学习] magic point_第4张图片
[深度学习] magic point_第5张图片
标签预处理部分,从 .npy文件中保存的坐标矩阵,得到一个维度跟样本图像矩阵一样的labels_2D矩阵,矩阵中0值的元素所在的位置,表示样本图像中对应位置的像素点不是特征点,而1值的元素所在的位置,表示样本图像中对应位置的像素点是特征点。

模型(SuperPointNet_gauss2.py 中 Class SuperPointNet_gauss2 的 forward 43行)

共同编码器部分

代码不难,模型结构就是常见的VGG网络,就不一句句看代码了,重点是模型的结构。

首先是输入,输入就是上面提到的样本图像数据,batch_size 是64,也就是说一次载入64个数据,形成的 tensor是(64,1,120,160),为了简单理解模型结构,这里就当作 batch_size=1,即一次只处理一张样本图像数据(1,120,160),1的意思是输入的图像是1通道的,也就是灰度图像。

[深度学习] magic point_第6张图片

Detector Head 部分

cPa = self.relu(self.bnPa(self.convPa(x4)))
semi = self.bnPb(self.convPb(cPa))

一步步来

1. self.convPa(x4)

跳转后发现,在 SuperPointNet_gauss2.py 文件中定义的 SuperPointNet_gauss2 类中,定义了convPa:
[深度学习] magic point_第7张图片
[深度学习] magic point_第8张图片

self.convPa = torch.nn.Conv2d(c4, c5, kernel_size=3, stride=1, padding=1)

其中,c4=128,c5=256

convPa 是一个 128通道 到 256通道的 3×3 卷积层,卷积完的结果数据的形状是 (64,256,15,20)

(卷积前后尺寸计算: O H = H + 2 P − F H S + 1 OH=\frac{H+2P-FH}{S}+1 OH=SH+2PFH+1
其中,OH为输出数据高度,H为输入数据高度,P为填充尺寸,FH为卷积核高度,S为步长。宽度同理计算。

2. self.bnPa

self.bnPa = nn.BatchNorm2d(c5)

其中 c5=256

bnPa 是对数据的 256个通道的归一化操作,结果数据的形状是 (64,256,15,20)

3. self.relu

self.relu = torch.nn.ReLU(inplace=True)

relu 是一次简单的ReLU激活函数(不知道作者为什么不直接用 torch.nn.ReLU,而是这样隔了一次调用),结果数据的形状是 (64,256,15,20)

4. x4 经过上述操作后,赋值给 cPa

x4:(64,128,15,20)
cPa:(64,256,15,20)

5. self.convPb(cPa)

self.convPb = torch.nn.Conv2d(c5, det_h, kernel_size=1, stride=1, padding=0)

其中,c5=256 ,det_h=65
convPb 是一个 256通道 到 65通道的 1×1 卷积层,卷积完的结果数据的形状是 (64,65,15,20)

6. self.bnPb

self.bnPb = nn.BatchNorm2d(det_h)

其中,det_h=65

bnPb是一次对数据的 65个通道的归一化操作,结果数据的形状是 (64,65,15,20)

总结:整个 Detector Head 部分,最终从公共编码器的输出 x4(64,128,15,20)得到输出 semi(64,65,15,20)

Descriptor Head 部分

cDa = self.relu(self.bnDa(self.convDa(x4)))
desc = self.bnDb(self.convDb(cDa))
dn = torch.norm(desc, p=2, dim=1) # Compute the norm.
desc = desc.div(torch.unsqueeze(dn, 1)) # Divide by norm to normalize.

1. self.convDa(x4)

self.convDa = torch.nn.Conv2d(c4, c5, kernel_size=3, stride=1, padding=1)

其中,c4=128,c5=256
convDa 是一个 128通道到256通道的 3×3 卷积层,卷积完的结果数据的形状是(64,256,15,20)

2.self.bnDa

self.bnDa = nn.BatchNorm2d(c5)

其中c5=256
bnDa是一次对数据的256个通道的归一化操作,结果数据的形状是(64,256,15,20)

3. self.relu

self.relu = torch.nn.ReLU(inplace=True)

ReLU 激活函数,结果数据的形状是(64,256,15,20)

4. self.convDb(cDa)

self.convDb = torch.nn.Conv2d(c5, d1, kernel_size=1, stride=1, padding=0)

其中c5=256,d1=256
convDb 是一个 256通道到256通道的 1×1 卷积层,卷积完的结果数据的形状是(64,256,15,20)

5.self.bnDb

self.bnDb = nn.BatchNorm2d(d1)

其中d1=256
bnDb是一次对数据的256个通道的归一化操作,结果数据的形状是(64,256,15,20)

desc:(64,256,15,20)

6. dn = torch.norm(desc, p=2, dim=1)

https://blog.csdn.net/weixin_43490422/article/details/123028255

对desc的1维度,也就是 256个通道,求 2范数,结果数据的形状是(64,15,20)

7. desc = desc.div(torch.unsqueeze(dn, 1))

torch.unsqueeze:

https://blog.csdn.net/xiexu911/article/details/80820028

扩充数据维度,结果数据的形状是 (64,1,15,20)

div

https://zhuanlan.zhihu.com/p/411245427

张量和标量做逐元素除法

或者两个可广播的张量之间做逐元素除法

如下图:[深度学习] magic point_第9张图片
结果数据的形状是(64,256,15,20)

总结:整个 Descriptor Head 部分,最终从公共编码器的输出 x4(64,128,15,20)得到输出 desc(64,256,15,20)

输出打包:

output = {'semi': semi, 'desc': desc}

semi:(64, 65, 15, 20)
desc:(64, 256, 15, 20)

return output

回到 Train_model_heatmap.py

[深度学习] magic point_第10张图片
逐个取出输出:
semi:(64, 65, 15, 20)
coarse_desc:(64, 256, 15, 20)

detector loss 部分

代码:

        # detector loss
        from utils.utils import labels2Dto3D

        if self.gaussian:
            labels_2D = sample["labels_2D_gaussian"]
            if if_warp:
                warped_labels = sample["warped_labels_gaussian"]
        else:
            labels_2D = sample["labels_2D"]
            if if_warp:
                warped_labels = sample["warped_labels"]

        add_dustbin = False
        if det_loss_type == "l2":
            add_dustbin = False
        elif det_loss_type == "softmax":
            add_dustbin = True

        labels_3D = labels2Dto3D(
            labels_2D.to(self.device), cell_size=self.cell_size, add_dustbin=add_dustbin
        ).float()
        mask_3D_flattened = self.getMasks(mask_2D, self.cell_size, device=self.device)
        loss_det = self.detector_loss(
            input=outs["semi"],
            target=labels_3D.to(self.device),
            mask=mask_3D_flattened,
            loss_type=det_loss_type,
        )

逐句解析:

取出标签:
labels_2D:(64,1,120,160),里面每个元素都是 0 或 1

add_dustbin = False
if det_loss_type == "l2":
    add_dustbin = False
elif det_loss_type == "softmax":
    add_dustbin = True

我跑的程序步骤中,det_loss_type 为 softmax :用softmax计算 特征点检测器的损失函数,所以这段代码把 add_dustbin 设成了 True

labels_3D = labels2Dto3D(labels_2D.to(self.device), cell_size=self.cell_size, add_dustbin=add_dustbin).float()

其中,self.cell_size=8,add_dustbin 前面设置成了True

调用了一个 labels2Dto3D 函数,进去看看。

https://blog.csdn.net/weixin_44179561/article/details/128058392?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22128058392%22%2C%22source%22%3A%22weixin_44179561%22%7D

该函数输入 labels_2D (64, 1, 120, 160),返回并经转换后的标签, .float() 转换成浮点型,然后赋值给 lebels_3D:(64, 65, 15, 20)

mask_3D_flattened = self.getMasks(mask_2D, self.cell_size, device=self.device)

调用了一个类内函数, getMasks。就不跳转了,直接进去看看。

函数代码:

    def getMasks(self, mask_2D, cell_size, device="cpu"):
        """
        # 2D mask is constructed into 3D (Hc, Wc) space for training
        :param mask_2D:
            tensor [batch, 1, H, W]
        :param cell_size:
            8 (default)
        :param device:
        :return:
            flattened 3D mask for training
        """
        mask_3D = labels2Dto3D(
            mask_2D.to(device), cell_size=cell_size, add_dustbin=False
        ).float()
        mask_3D_flattened = torch.prod(mask_3D, 1)
        return mask_3D_flattened

同样调用了 labels2Dto3D 函数,把输入的 mask_2D(64, 1, 120, 160),转换成 mask_3D(64, 64, 15, 20),因为在这一步中 add_dustbin 设置成了 False,所以通道中最后少了一个 dustbin的通道,因此输出的是 64 通道的数据。

torch.prod ,返回输入张量上,给定维度上的乘积。这里就是把 64 个通道上的数值全部乘起来,因此 mask_3D_flattened(64,15,20)。

返回 mask_3D_flattened

也就是说,.getMasks函数,输入 mask_2D(64,1,120,160),得到一个三维再乘积后的 mask_3D_flattened(64,15,20)。

        loss_det = self.detector_loss(
            input=outs["semi"],
            target=labels_3D.to(self.device),
            mask=mask_3D_flattened,
            loss_type=det_loss_type,
        )

调用计算损失函数的函数, detector_loss,在这一步中,通过Detector Head 的输出部分semi(64,65,15,20),标签 labels_3D(64,65,15,20),计算两者之间的差异性,得到损失函数值。但是这里也输入了前面计算得到的 mask_3D_flattened。

这个掩码似乎跟作者对图像数据做的单应性变换有关,用于指定经过单应性变换后的图像中的有效区域,因为经过旋转平移等变换后,原图像中的某些部分可能超出图像边界

[深度学习] magic point_第11张图片

我暂时不想对数据集做这么复杂的单应性变换处理,所以暂时忽略这个掩码

dector_loss 函数代码:

    def detector_loss(self, input, target, mask=None, loss_type="softmax"):
        """
        # apply loss on detectors, default is softmax
        :param input: prediction
            tensor [batch_size, 65, Hc, Wc]
        :param target: constructed from labels
            tensor [batch_size, 65, Hc, Wc]
        :param mask: valid region in an image
            tensor [batch_size, 1, Hc, Wc]
        :param loss_type:
            str (l2 or softmax)
            softmax is used in original paper
        :return: normalized loss
            tensor
        """
        if loss_type == "l2":
            loss_func = nn.MSELoss(reduction="mean")
            loss = loss_func(input, target)
        elif loss_type == "softmax":
            loss_func_BCE = nn.BCELoss(reduction='none').cuda()
            loss = loss_func_BCE(nn.functional.softmax(input, dim=1), target)
            loss = (loss.sum(dim=1) * mask).sum()
            loss = loss / (mask.sum() + 1e-10)
        return loss

输入时指定了 loss_type 是 softmax,所以重点看这段代码:

loss_func_BCE = nn.BCELoss(reduction='none').cuda()
loss = loss_func_BCE(nn.functional.softmax(input, dim=1), target)
loss = (loss.sum(dim=1) * mask).sum()
loss = loss / (mask.sum() + 1e-10)
return loss

逐句解析:

loss_func_BCE = nn.BCELoss(reduction='none').cuda()

这句话使得 loss_func_BEC = nn.BCELoss 这个函数,以后就通过 loss_func_BCE 调用 nn.BCELoss 函数(二进制交叉熵损失函数)

https://blog.csdn.net/qq_29631521/article/details/104907401

根据输入和目标值(标签),计算二进制交叉熵损失函数。

BCELoss的计算公式为:
在这里插入图片描述
式中, y n y_n yn为理论值,也就是标签。 x n x_n xn为预测值,也就是神经网络的输出, w n w_n wn是权重系数,默认是None,即没有。

reduction=‘none’,意味着计算结果以向量的形式返回。

举个例子:
[深度学习] magic point_第12张图片

预 测 值 : x = ( 0.1598 , 0.5659 , 0.8104 ) 预测值:x=(0.1598,0.5659,0.8104) x=0.15980.56590.8104
标 签 : y = ( 1 , 0 , 1 ) 标签:y=(1,0,1) y=101
l o s s 的 结 果 = − ( 1 × l n ( 0.1598 ) + 0 , 0 + 1 × l n ( 1 − 0.5659 ) , 1 × l n ( 0.8104 ) + 0 ) loss的结果=-(1\times ln(0.1598)+0,0+1\times ln(1-0.5659),1\times ln(0.8104)+0) loss=1×ln(0.1598)+00+1×ln(10.5659),1×ln(0.8104)+0
= ( 1.833 , 0.8344 , 0.2102 ) =(1.833,0.8344,0.2102) =1.8330.83440.2102

从极端情况:
当预测值 x=1,标签 y=1时, l o s s = − ( 1 × l n 1 + 0 ) = 0 loss=-(1\times ln1+0)=0 loss=1×ln1+0=0

当预测值 x=0,标签 y=1时, l o s s = − ( 1 × l n 0 + 0 ) = + ∞ loss=-(1\times ln0 + 0)=+∞ loss=1×ln0+0=+

也就是说,预测值跟标签越接近时,loss越小,相差越远时,loss越大。

loss = loss_func_BCE(nn.functional.softmax(input, dim=1), target)

input(64,65,15,20)是之前经过 Detector Head 网络得到的输出,对它的第1维度,也就是 65 个通道所在的维度做一个 softmax,转换成概率值,形状仍是 (64,65,15,20)。然后跟 target(64,65,15,20)也就是标签,计算二进制交叉熵损失函数。得到结果 loss(64,65,15,20)

loss = (loss.sum(dim=1) * mask).sum()

loss的第1维度,也就是 65 通道所在的那个维度,全部加起来,得到(64,15,20),再将这个矩阵的所有元素加起来,得到一个数值 loss。

loss = loss / (mask.sum() + 1e-10)

数值loss再做一个归一化,这里 mask.sum(),即每一个有效的像素值的数目。由于博主不做与单应性变换有关的数据集处理,实际上 mask应该全部为1,所以这里应该是图像的所有像素值的数目 H × W H\times W H×W
+1e-10 表示加上 1 × 1 0 − 10 1\times 10^{-10} 1×1010,加上一个极小值,防止当 mask.sum()=0时,这里出现除数为0的情况。

至此,完成特征点检测器(detector head)的损失函数的计算。当然,记得batch是64,这个loss是64个数据样本的损失函数loss的总和。

代码后面部分是Descriptor Head 部分的损失函数的计算,由于我不打算使用这部分网络,就到此为止了(看得脑壳痛…)

三、Magic Point Net

总结一下重点的部分。

[深度学习] magic point_第13张图片

你可能感兴趣的:(深度学习,python,人工智能)