Toward Geometric Deep SLAM
SuperPoint: Self-Supervised Interest Point Detection and Description
github上的pytorch复现项目:https://github.com/eric-yyjau/pytorch-superpoint
需要在这个项目中找出用于提取图像中特征点的网络 magic point部分具体是怎么实现的,然后再尝试自己复现
标签是一个.npy文件,储存了一个矩阵,表示了样本图像中特征点的数目以及每个特征点的坐标
总的来说,是确保了 .npy文件中每个点的坐标都大于等于0且在图像范围内,即确保坐标是有效的
标签预处理部分,从 .npy文件中保存的坐标矩阵,得到一个维度跟样本图像矩阵一样的labels_2D矩阵,矩阵中0值的元素所在的位置,表示样本图像中对应位置的像素点不是特征点,而1值的元素所在的位置,表示样本图像中对应位置的像素点是特征点。
代码不难,模型结构就是常见的VGG网络,就不一句句看代码了,重点是模型的结构。
首先是输入,输入就是上面提到的样本图像数据,batch_size 是64,也就是说一次载入64个数据,形成的 tensor是(64,1,120,160),为了简单理解模型结构,这里就当作 batch_size=1,即一次只处理一张样本图像数据(1,120,160),1的意思是输入的图像是1通道的,也就是灰度图像。
cPa = self.relu(self.bnPa(self.convPa(x4)))
semi = self.bnPb(self.convPb(cPa))
一步步来
跳转后发现,在 SuperPointNet_gauss2.py 文件中定义的 SuperPointNet_gauss2 类中,定义了convPa:
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+2P−FH+1
其中,OH为输出数据高度,H为输入数据高度,P为填充尺寸,FH为卷积核高度,S为步长。宽度同理计算。
)
self.bnPa = nn.BatchNorm2d(c5)
其中 c5=256
bnPa 是对数据的 256个通道的归一化操作,结果数据的形状是 (64,256,15,20)
self.relu = torch.nn.ReLU(inplace=True)
relu 是一次简单的ReLU激活函数(不知道作者为什么不直接用 torch.nn.ReLU,而是这样隔了一次调用),结果数据的形状是 (64,256,15,20)
x4:(64,128,15,20)
cPa:(64,256,15,20)
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)
self.bnPb = nn.BatchNorm2d(det_h)
其中,det_h=65
bnPb是一次对数据的 65个通道的归一化操作,结果数据的形状是 (64,65,15,20)
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.
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)
self.bnDa = nn.BatchNorm2d(c5)
其中c5=256
bnDa是一次对数据的256个通道的归一化操作,结果数据的形状是(64,256,15,20)
self.relu = torch.nn.ReLU(inplace=True)
ReLU 激活函数,结果数据的形状是(64,256,15,20)
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)
self.bnDb = nn.BatchNorm2d(d1)
其中d1=256
bnDb是一次对数据的256个通道的归一化操作,结果数据的形状是(64,256,15,20)
desc:(64,256,15,20)
https://blog.csdn.net/weixin_43490422/article/details/123028255
对desc的1维度,也就是 256个通道,求 2范数,结果数据的形状是(64,15,20)
https://blog.csdn.net/xiexu911/article/details/80820028
扩充数据维度,结果数据的形状是 (64,1,15,20)
https://zhuanlan.zhihu.com/p/411245427
张量和标量做逐元素除法
或者两个可广播的张量之间做逐元素除法
output = {'semi': semi, 'desc': desc}
semi:(64, 65, 15, 20)
desc:(64, 256, 15, 20)
return output
回到 Train_model_heatmap.py
逐个取出输出:
semi:(64, 65, 15, 20)
coarse_desc:(64, 256, 15, 20)
# 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。
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’,意味着计算结果以向量的形式返回。
预 测 值 : x = ( 0.1598 , 0.5659 , 0.8104 ) 预测值:x=(0.1598,0.5659,0.8104) 预测值:x=(0.1598,0.5659,0.8104)
标 签 : y = ( 1 , 0 , 1 ) 标签:y=(1,0,1) 标签:y=(1,0,1)
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)+0,0+1×ln(1−0.5659),1×ln(0.8104)+0)
= ( 1.833 , 0.8344 , 0.2102 ) =(1.833,0.8344,0.2102) =(1.833,0.8344,0.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×10−10,加上一个极小值,防止当 mask.sum()=0时,这里出现除数为0的情况。
至此,完成特征点检测器(detector head)的损失函数的计算。当然,记得batch是64,这个loss是64个数据样本的损失函数loss的总和。
代码后面部分是Descriptor Head 部分的损失函数的计算,由于我不打算使用这部分网络,就到此为止了(看得脑壳痛…)
总结一下重点的部分。