基於飛槳復現FP-Net全程解析,補全缺失的鐳射點

飛槳PaddlePaddle發表於2020-09-25


3D點雲補齊是一種輸入某物體部分點雲,輸出完整點雲的任務。在自動駕駛,機器人領域具有廣闊的應用價值。本次復現的論文是來自CVPR2020的一篇文章:PF-Net: Point Fractal Network for 3D Point Cloud Completion。

論文地址:
https://arxiv.org/pdf/2003.00410.pdf
基於飛槳復現FP-Net全程解析,補全缺失的鐳射點
為解決3D點雲補齊,論文作者提出了Point Fractal Network(FP-Net),主要貢獻有:

(1) 與其他方法不同,本方法輸入為部分點雲,輸出為另一部分點雲,這樣可以保持輸入點雲的原有資訊,使網路專注於點雲補齊;
(2) 提出Multi-Resolution Encoder(MRE) 抽取多個解析度的點雲資料,可以提取點雲的細節(low-leve)與框架(high-level),增強網路的幾何資訊提取能力;
(3) 提出Point Pyramid Decoder(PPD),增強補齊能力。

本專案使用飛槳動態圖模式進行論文復現。

方法簡述

如圖一所示,作者依照encoder-decoder的架構,提出基於多解析度資料的表徵和重建架構,輸入端使用iterative farthest point sampling(IFPS)方法,取樣三個解析度的部分點雲,然後將其編碼為隱變數向量V,之後使用特徵金字塔的思想,按照不同的解析度解碼隱變數向量,生成三個解析度的補齊點雲。
基於飛槳復現FP-Net全程解析,補全缺失的鐳射點圖一模型實現

首先實現FP-Net的模型,其由MRE, PPD兩部分組成。MRE首先將三個不同解析度的部分點雲作為輸入,送入Combined Multi-layer Proception (CMLP),其結構如圖二所示。
基於飛槳復現FP-Net全程解析,補全缺失的鐳射點
圖二
與基於PointNet結構的網路相比,CMLP網路對點雲分類任務具有一定的精度提升,因此選擇其作為特徵抽取的骨幹模組,其具體實現如下:

class Convlayer(fluid.dygraph.Layer):     def __init__(self, point_scales):           super(Convlayer, self).__init__()           self.point_scales = point_scales           self.conv1 = Conv2D(1, 64, (1, 3))           self.conv2 = Conv2D(64, 64, 1)           self.conv3 = Conv2D(64, 128, 1)           self.conv4 = Conv2D(128, 256, 1)           self.conv5 = Conv2D(256, 512, 1)           self.conv6 = Conv2D(512, 1024, 1)           self.maxpool = Pool2D(pool_size=(self.point_scales, 1), pool_stride=1)           self.bn1 = BatchNorm(64, act='relu')           self.bn2 = BatchNorm(64, act='relu')           self.bn3 = BatchNorm(128, act='relu')           self.bn4 = BatchNorm(256, act='relu')           self.bn5 = BatchNorm(512, act='relu')           self.bn6 = BatchNorm(1024,  act='relu')       def forward(self, x):           x = fluid.layers.unsqueeze(x, 1)           x = self.bn1(self.conv1(x))           x = self.bn2(self.conv2(x))           x_128 = self.bn3(self.conv3(x))           x_256 = self.bn4(self.conv4(x_128))           x_512 = self.bn5(self.conv5(x_256))           x_1024 = self.bn6(self.conv6(x_512))           x_128 = fluid.layers.squeeze(input=self.maxpool(x_128), axes=[2])           x_256 = fluid.layers.squeeze(input=self.maxpool(x_256), axes=[2])           x_512 = fluid.layers.squeeze(input=self.maxpool(x_512), axes=[2])           x_1024 = fluid.layers.squeeze(input=self.maxpool(x_1024), axes=[2])          L = [x_1024, x_512, x_256, x_128]           x = fluid.layers.concat(L, 1)           return x  

在這裡分別使用1024,512,256,128四種size表徵輸入的部分點雲,最後拼合成為一個向量作為此輸入解析度的合併隱變數(combined latent vector)。之後針對三種解析度輸入,分別將其表徵為三個合併隱變數後使用多層感知器融合為最終的隱變數向量。

程式碼實現如下:

class Latentfeature(fluid.dygraph.Layer):       def __init__(self, num_scales, each_scales_size, point_scales_list):           super(Latentfeature, self).__init__()           self.num_scales = num_scales           self.each_scales_size = each_scales_size           self.point_scales_list = point_scales_list           self.Convlayers1 = Convlayer(point_scales=self.point_scales_list[0])           self.Convlayers2 = Convlayer(point_scales=self.point_scales_list[1])           self.Convlayers3 = Convlayer(point_scales=self.point_scales_list[2])           self.conv1 = Conv1D(prefix='lf', num_channels=3, num_filters=1, size_k=1, act=None)           self.bn1 = BatchNorm(1, act='relu')       def forward(self, x):           outs = [self.Convlayers1(x[0]), self.Convlayers2(x[1]), self.Convlayers3(x[2])]           latentfeature = fluid.layers.concat(outs, 2)           latentfeature = fluid.layers.transpose(latentfeature, perm=[0, 2, 1])           latentfeature = self.bn1(self.conv1(latentfeature))           latentfeature = fluid.layers.squeeze(latentfeature, axes=[1])           return latentfeature  

PaddlePaddle暫時沒有提供Conv1D(1維卷積層)的實現,在這裡使用Conv2D(2維卷積層)加unsqueeze層替代其功能,具體實現如下:

class Conv1D(fluid.dygraph.Layer):       def __init__(self,                    prefix,                    num_channels=3,                    num_filters=1,                    size_k=1,                    padding=0,                    groups=1,                    act=None):           super(Conv1D, self).__init__()           fan_in = num_channels * size_k * 1           k = 1. / math.sqrt(fan_in)           param_attr = ParamAttr(               name=prefix + "_w",               initializer=fluid.initializer.Uniform(                   low=-k, high=k))           bias_attr = ParamAttr(               name=prefix + "_b",               initializer=fluid.initializer.Uniform(                   low=-k, high=k))           self._conv2d = fluid.dygraph.Conv2D(               num_channels=num_channels,               num_filters=num_filters,               filter_size=(1, size_k),               stride=1,               padding=(0, padding),               groups=groups,               act=act,               param_attr=param_attr,               bias_attr=bias_attr)       def forward(self, x):           x = fluid.layers.unsqueeze(input=x, axes=[2])           x = self._conv2d(x)           x = fluid.layers.squeeze(input=x, axes=[2])           return x  

有了隱變數向量之後,接下來就是介紹decoder部分,也就是PPD部分。它利用了借用金字塔的思想,生成多個解析度的點雲補齊結果。具體地講,如圖三所示,使用隱變數向量依次輸出1024,512,256維的點雲表徵並分別使用Conv1D層處理其表徵,最後輸出不同解析度的點雲補齊結果。
基於飛槳復現FP-Net全程解析,補全缺失的鐳射點
圖三
其程式碼實現如下:

class PFNetG(fluid.dygraph.Layer):       def __init__(self, num_scales, each_scales_size, point_scales_list, crop_point_num):           super(PFNetG, self).__init__()           self.crop_point_num = crop_point_num           self.latentfeature = Latentfeature(num_scales, each_scales_size, point_scales_list)           self.fc1 = Linear(input_dim=1920, output_dim=1024, act='relu')           self.fc2 = Linear(input_dim=1024, output_dim=512, act='relu')           self.fc3 = Linear(input_dim=512, output_dim=256, act='relu')           self.fc1_1 = Linear(input_dim=1024, output_dim=128 * 512, act='relu')           self.fc2_1 = Linear(input_dim=512, output_dim=64 * 128, act='relu')           self.fc3_1 = Linear(input_dim=256, output_dim=64 * 3)           self.conv1_1 = Conv1D(prefix='g1_1', num_channels=512, num_filters=512, size_k=1, act='relu')           self.conv1_2 = Conv1D(prefix='g1_2', num_channels=512, num_filters=256, size_k=1, act='relu')           self.conv1_3 = Conv1D(prefix='g1_3', num_channels=256, num_filters=int((self.crop_point_num * 3) / 128),                                 size_k=1, act=None)           self.conv2_1 = Conv1D(prefix='g2_1', num_channels=128, num_filters=6, size_k=1, act=None)       def forward(self, x):           x = self.latentfeature(x)           x_1 = self.fc1(x)  # 1024           x_2 = self.fc2(x_1)  # 512           x_3 = self.fc3(x_2)  # 256           pc1_feat = self.fc3_1(x_3)           pc1_xyz = fluid.layers.reshape(pc1_feat, [-1, 64, 3], inplace=False)           pc2_feat = self.fc2_1(x_2)           pc2_feat_reshaped = fluid.layers.reshape(pc2_feat, [-1, 128, 64], inplace=False)           pc2_xyz = self.conv2_1(pc2_feat_reshaped)  # 6x64 center2           pc3_feat = self.fc1_1(x_1)           pc3_feat_reshaped = fluid.layers.reshape(pc3_feat, [-1, 512, 128], inplace=False)           pc3_feat = self.conv1_1(pc3_feat_reshaped)           pc3_feat = self.conv1_2(pc3_feat)           pc3_xyz = self.conv1_3(pc3_feat)  # 12x128 fine           pc1_xyz_expand = fluid.layers.unsqueeze(pc1_xyz, axes=[2])           pc2_xyz = fluid.layers.transpose(pc2_xyz, perm=[0, 2, 1])           pc2_xyz_reshaped1 = fluid.layers.reshape(pc2_xyz, [-1, 64, 2, 3], inplace=False)           pc2_xyz = fluid.layers.elementwise_add(pc1_xyz_expand, pc2_xyz_reshaped1)           pc2_xyz_reshaped2 = fluid.layers.reshape(pc2_xyz, [-1, 128, 3], inplace=False)           pc2_xyz_expand = fluid.layers.unsqueeze(pc2_xyz_reshaped2, axes=[2])           pc3_xyz = fluid.layers.transpose(pc3_xyz, perm=[0, 2, 1])           pc3_xyz_reshaped1 = fluid.layers.reshape(pc3_xyz, [-1, 128, int(self.crop_point_num / 128), 3], inplace=False)           pc3_xyz = fluid.layers.elementwise_add(pc2_xyz_expand, pc3_xyz_reshaped1)           pc3_xyz_reshaped2 = fluid.layers.reshape(pc3_xyz, [-1, self.crop_point_num, 3], inplace=False)           return pc1_xyz, pc2_xyz_reshaped2, pc3_xyz_reshaped2  # center1 ,center2 ,fine  

訓練

接下來介紹訓練的相關實現細節。首先需要將輸入部分點雲取樣生成3個不同解析度的點雲,這裡使用IFPS方法。它是一種非常常用的取樣演算法,由於能夠保證對樣本的均勻取樣,被廣泛使用與3D視覺中。

實現細節如下:

def farthest_point_sample_numpy(xyz, npoint, RAN=True):       B, N, C = xyz.shape       centroids = np.zeros((B, npoint))       distance = np.ones((B, N)) * 1e10       if RAN:           farthest = np.random.randint(low=0, high=1, size=(B,))       else:           farthest = np.random.randint(low=1, high=2, size=(B,))       batch_indices = np.arange(start=0, stop=B)       for i in range(npoint):           centroids[:, i] = farthest           centroid = xyz[batch_indices, farthest, :].view().reshape(B, 1, 3)           dist = np.sum((xyz - centroid) ** 2, -1)           mask = dist < distance           distance[mask] = dist[mask]           farthest = np.argmax(distance, -1)       return centroids.astype('int64')  

接著需要定義損失函式,這裡使用chamfer distance (CD)作為loss,其定義如下:基於飛槳復現FP-Net全程解析,補全缺失的鐳射點
其中和為兩組點雲,具體實現如下:

def chamfer_distance(array1, array2):       batch_size, num_point, num_features = array1.shape       dist = 0       for i in range(batch_size):           av_dist1 = array2samples_distance(array1[i], array2[i])           av_dist2 = array2samples_distance(array2[i], array1[i])           dist = dist + (av_dist1 + av_dist2) / batch_size       return dist   def array2samples_distance(array1, array2):     num_point, num_features = array1.shape       expanded_array1 = fluid.layers.expand(array1, [num_point, 1])       array2_expand = fluid.layers.unsqueeze(array2, [1])       expanded_array2 = fluid.layers.expand(array2_expand, [1, num_point, 1])      expanded_array2_reshaped = fluid.layers.reshape(expanded_array2, [-1, num_features], inplace=False)       distances = (expanded_array1 - expanded_array2_reshaped) * (expanded_array1 - expanded_array2_reshaped)       distances = fluid.layers.reduce_sum(distances, dim=1)       distances_reshaped = fluid.layers.reshape(distances, [num_point, num_point])       distances = fluid.layers.reduce_min(distances_reshaped, dim=1)       distances = fluid.layers.mean(distances)       return distances  

接下來需要準備資料集。這裡使用ShapeNet_part資料集訓練測試網路。同時使用fluid.io.DataLoader.from_generator提供的方法獲取資料集中的資料,具體實現請參照程式碼:

dset = PartDataset(root=root, classification=True, class_choice=None, num_point=opt.pnum, mode='train')   train_loader = fluid.io.DataLoader.from_generator(capacity=10, iterable=True)   train_loader.set_sample_list_generator(dset.get_reader(opt.batchSize), places=place)  

實驗結論

原文提出了兩種訓練方法,一種是直接使用各個解析度的ground truth與生成的補齊點雲端計算CD作為loss,稱為FP-Net (vanilla);另一種在此基礎上訓練一個判別器(discriminator)作為adversarial loss,稱為FP-Net。對於補齊網路部分的loss,其結果如下。與PCN, LGAN-AE和3D-Capsule相比,PF-Net整體達到了state-of-the-art。
基於飛槳復現FP-Net全程解析,補全缺失的鐳射點
表一
由於時間關係,本專案實現了PF-Net (vanilla)部分的程式碼。復現結果如下:
基於飛槳復現FP-Net全程解析,補全缺失的鐳射點
表二
其中,第一列為預測到GT的CD,第二列為GT到預測的CD。可以看到PaddlePaddle(pp)復現模型與Pytorch(torch)模型表現相當。

個人心得

本次復現營,我基於自己的研究興趣選擇了3D視覺文章作為主題復現文章。這對我是一個挑戰,因為這的確是第一次復現文章。我採取了比較謹慎的策略,分別完成了模型網路的搭建,資料集準備,損失函式等模組的復現。對於每一個子功能都與Pytorch原始碼進行了精度對齊,保證子功能無誤後才進行下一步工作。好在需要使用的層和功能PaddlePaddle基本都有相關實現,這使得復現工作變得簡單不少。當然由於時間的限制,一些內容如多執行緒訓練等還沒有實現,今後會繼續探索PaddlePaddle。

相關文章