Dynamic Graph CNN for Learning on Point Clouds 代码注解

论文解读参阅:https://blog.csdn.net/weixin_39373480/article/details/88724518 

                       https://blog.csdn.net/qq_39426225/article/details/101980690

                       https://blog.csdn.net/hongbin_xu/article/details/85258278

下面我们来看论文主要核心部分的代码:

1、为点云变换准备3X3变换矩阵的代码,knn部分代码,邻接矩阵代码,如下:

def input_transform_net(edge_feature, is_training, bn_decay=None, K=3, is_dist=False):
  """ Input (XYZ) Transform Net, input is BxNx3 gray image
    Return:
      Transformation matrix of size 3xK """
  batch_size = edge_feature.get_shape()[0].value
  num_point = edge_feature.get_shape()[1].value

  # input_image = tf.expand_dims(point_cloud, -1)

  net = tf_util.conv2d(edge_feature, 64, [1, 1],
             padding='VALID', stride=[1, 1],
             bn=True, is_training=is_training,
             scope='tconv1', bn_decay=bn_decay, is_dist=is_dist)
  net = tf_util.conv2d(net, 128, [1, 1],
             padding='VALID', stride=[1, 1],
             bn=True, is_training=is_training,
             scope='tconv2', bn_decay=bn_decay, is_dist=is_dist)
  
  net = tf.reduce_max(net, axis=-2, keep_dims=True)
  
  net = tf_util.conv2d(net, 1024, [1, 1],
             padding='VALID', stride=[1, 1],
             bn=True, is_training=is_training,
             scope='tconv3', bn_decay=bn_decay, is_dist=is_dist)
  net = tf_util.max_pool2d(net, [num_point, 1],
               padding='VALID', scope='tmaxpool')

  net = tf.reshape(net, [batch_size, -1])
  net = tf_util.fully_connected(net, 512, bn=True, is_training=is_training,
                  scope='tfc1', bn_decay=bn_decay, is_dist=is_dist)
  net = tf_util.fully_connected(net, 256, bn=True, is_training=is_training,
                  scope='tfc2', bn_decay=bn_decay, is_dist=is_dist)

  with tf.variable_scope('transform_XYZ') as sc:
    # assert(K==3)
    with tf.device('/cpu:0'):
      weights = tf.get_variable('weights', [256, K*K],
                    initializer=tf.constant_initializer(0.0),
                    dtype=tf.float32)
      biases = tf.get_variable('biases', [K*K],
                   initializer=tf.constant_initializer(0.0),
                   dtype=tf.float32)
    biases += tf.constant(np.eye(K).flatten(), dtype=tf.float32)
    transform = tf.matmul(net, weights)
    transform = tf.nn.bias_add(transform, biases)

  transform = tf.reshape(transform, [batch_size, K, K])
  return transform

 得到k邻居(包括自己)

def knn(adj_matrix, k=20):
  """Get KNN based on the pairwise distance.
  Args:
    pairwise distance: (batch_size, num_points, num_points)
    k: int

  Returns:
    nearest neighbors: (batch_size, num_points, k)
  """

  # 邻接矩阵取负值,如果原始点云中两个点距离越远,这里值越小
  # 原始邻接矩阵中自己指向自己时,距离为0,所以取负值,然后选k个最大的值,自己会被选出来
  # 选出来的结果是按从大到小排序的,索引对应值从大到小排序
  neg_adj = -adj_matrix  
  value, nn_idx = tf.nn.top_k(neg_adj, k=k)
  return nn_idx

tf.nn.top_k(input, k, name=None)

这个函数的作用是返回 input 中每行最大的 k 个数,并且返回它们所在位置的索引。

# 得到邻接矩阵
def pairwise_distance(point_cloud):
  """Compute pairwise distance of a point cloud.

  Args:
    point_cloud: tensor (batch_size, num_points, num_dims)

  Returns:
    pairwise distance: (batch_size, num_points, num_points)
  """
  og_batch_size = point_cloud.get_shape().as_list()[0]
  point_cloud = tf.squeeze(point_cloud)
  if og_batch_size == 1:
    point_cloud = tf.expand_dims(point_cloud, 0)
  
  # 将点云的后两个维度置换  
  point_cloud_transpose = tf.transpose(point_cloud, perm=[0, 2, 1])
   
  # 类似二维矩阵A和A的转置相乘
  point_cloud_inner = tf.matmul(point_cloud, point_cloud_transpose) 

  # 这里为什么乘以-2?
  point_cloud_inner = -2*point_cloud_inner

  # 点云自身的每个特征平方求和,这个和等于转置乘以本身的对角线部分
  point_cloud_square = tf.reduce_sum(tf.square(point_cloud), axis=-1, keep_dims=True)

  # 转置的意义?
  point_cloud_square_tranpose = tf.transpose(point_cloud_square, perm=[0, 2, 1])

  # 这一步操作其实就是(x-y)^2,其中x和y代表向量
  return point_cloud_square + point_cloud_inner + point_cloud_square_tranpose

 2、边特征提取的原理部分

 3、边特征提取的代码部分

def get_edge_feature(point_cloud, nn_idx, k=20):
  """Construct edge feature for each point
  Args:
    point_cloud: (batch_size, num_points, 1, num_dims)
    nn_idx: (batch_size, num_points, k)
    k: int

  Returns:
    edge features: (batch_size, num_points, k, num_dims)
  """
  # 得到点云的 batch_size, num_points, 1, num_dims,放入一个列表[]中,然后*[0]取出第一个元素

  # 删除point_cloud为1的维度索引,变为 (batch_size, num_points, num_dims)
  og_batch_size = point_cloud.get_shape().as_list()[0]  
  point_cloud = tf.squeeze(point_cloud)                 
  if og_batch_size == 1:
    point_cloud = tf.expand_dims(point_cloud, 0)

  # 整个点云复制给 point_cloud_central (batch_size, num_points, num_dims)
  point_cloud_central = point_cloud                     

  # 得到点云的 batch_size, num_points, 1, num_dims,放入一个元组()中
  point_cloud_shape = point_cloud.get_shape()           
  batch_size = point_cloud_shape[0].value
  num_points = point_cloud_shape[1].value
  num_dims = point_cloud_shape[2].value

  idx_ = tf.range(batch_size) * num_points
  # batch_size = 32 --> idx_.shape = (batch_size, 1, 1)
  idx_ = tf.reshape(idx_, [batch_size, 1, 1])          

  # 点云变为batch_size X num_point行,num_dims列的矩阵
  # 第一个 num_points 行代表第一个点云,第二个 num_points 行代表第二个点云
  # ......第batch_size个 num_points 行代表第batch_size个点云
  point_cloud_flat = tf.reshape(point_cloud, [-1, num_dims]) 

  # 将point_cloud变成point_cloud_flat后(batch_size*num_points行),每隔num_points行取出k行,
  # 要取的行号根据为nn_idx里面的索引元素(k个)+batch_size*num_points
  # 所以每个点云(batch_size个)都会得到自己的k个邻居
  # 最后得到 point_cloud_neighbors:(batch_size, num_points, k, num_dims)  
  # nn_idx.shape= (batch_size, num_points, k) k为点的维度    
  point_cloud_neighbors = tf.gather(point_cloud_flat, nn_idx+idx_)  
  
  # 在维度倒数第二个位置增加一个维度 (batch_size, num_points,1, num_dims)
  point_cloud_central = tf.expand_dims(point_cloud_central, axis=-2)
  
  # point_cloud_central 规模变为 (batch_size, num_points,k, num_dims)
  point_cloud_central = tf.tile(point_cloud_central, [1, 1, k, 1])  

  # 把两个点云按最后一个维度连接起来
  edge_feature = tf.concat([point_cloud_central, point_cloud_neighbors-point_cloud_central], axis=-1)
  return edge_feature

我们来图解看一下,point_cloud_neighbors-point_cloud_central 到底干了什么?

# nn_idx.shape= (batch_size, num_points, k) k为点的维度
point_cloud_neighbors = tf.gather(point_cloud_flat, nn_idx+idx_)  

# 在维度倒数第二个位置增加一个维度 (batch_size, num_points,1, num_dims)
point_cloud_central = tf.expand_dims(point_cloud_central, axis=-2)

# point_cloud_central 规模变为 (batch_size, num_points,k, num_dims)
point_cloud_central = tf.tile(point_cloud_central, [1, 1, k, 1])

提取到的 edge_feature 作为 tf_util.py 文件中函数conv2d的输入   个人理解方程(8)中的θ m和ϕm体现在这段代码里

def conv2d(inputs,
           num_output_channels,
           kernel_size,
           scope,
           stride=[1, 1],
           padding='SAME',
           use_xavier=True,
           stddev=1e-3,
           weight_decay=0.0,
           activation_fn=tf.nn.relu,
           bn=False,
           bn_decay=None,
           is_training=None,
           is_dist=False):
  """ 2D convolution with non-linear operation.

  Args:
    inputs: 4-D tensor variable BxHxWxC
    num_output_channels: int
    kernel_size: a list of 2 ints
    scope: string
    stride: a list of 2 ints
    padding: 'SAME' or 'VALID'
    use_xavier: bool, use xavier_initializer if true
    stddev: float, stddev for truncated_normal init
    weight_decay: float
    activation_fn: function
    bn: bool, whether to use batch norm
    bn_decay: float or float tensor variable in [0,1]
    is_training: bool Tensor variable

  Returns:
    Variable tensor
  """
    with tf.variable_scope(scope) as sc:
      kernel_h, kernel_w = kernel_size
      num_in_channels = inputs.get_shape()[-1].value
      kernel_shape = [kernel_h, kernel_w,
                      num_in_channels, num_output_channels]
      kernel = _variable_with_weight_decay('weights',
                                           shape=kernel_shape,
                                           use_xavier=use_xavier,
                                           stddev=stddev,
                                           wd=weight_decay)
      stride_h, stride_w = stride
      outputs = tf.nn.conv2d(inputs, kernel,
                             [1, stride_h, stride_w, 1],
                             padding=padding)
      biases = _variable_on_cpu('biases', [num_output_channels],
                                tf.constant_initializer(0.0))
      outputs = tf.nn.bias_add(outputs, biases)

      if bn:
        outputs = batch_norm_for_conv2d(outputs, is_training,
                                        bn_decay=bn_decay, scope='bn', is_dist=is_dist)

      if activation_fn is not None:
        outputs = activation_fn(outputs)
      return outputs

对于get_edge_feature提取的边特征,作者貌似没有按照(8)ReLU(θm · (xj − xi) + ϕm · xi),那样进行"+",而代码里变成contact操作了,然后进入conv2D进行卷积,这里没想明白为什么?理论上是一样的么?

4、分类整体架构部分的代码:

def get_model(point_cloud, is_training, bn_decay=None):
  """ Classification PointNet, input is BxNx3, output Bx40 """
  batch_size = point_cloud.get_shape()[0].value
  num_point = point_cloud.get_shape()[1].value
  end_points = {}
  k = 20
  
  # 对最原始的点云提取邻接矩阵
  adj_matrix = tf_util.pairwise_distance(point_cloud)

  # knn操作,得到每个点的k近邻
  nn_idx = tf_util.knn(adj_matrix, k=k)

  # 先提取一个原始点云的边特征,用于估计变换矩阵
  edge_feature = tf_util.get_edge_feature(point_cloud, nn_idx=nn_idx, k=k)

  # 利用边特征估计变换矩阵
  with tf.variable_scope('transform_net1') as sc:
    transform = input_transform_net(edge_feature, is_training, bn_decay, K=3)

  # 变换点云
  point_cloud_transformed = tf.matmul(point_cloud, transform)

  # 变换后点云的邻接矩阵
  adj_matrix = tf_util.pairwise_distance(point_cloud_transformed)

  # knn操作,得到每个点的k近邻
  nn_idx = tf_util.knn(adj_matrix, k=k)

  # 得到变换后点云的边的信息 xi||(xj − xi) 对所有的 i
  edge_feature = tf_util.get_edge_feature(point_cloud_transformed, nn_idx=nn_idx, k=k)

  # 通过2D卷积得到边的特征
  net = tf_util.conv2d(edge_feature, 64, [1,1],
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training,
                       scope='dgcnn1', bn_decay=bn_decay)
  
  #最大池化操作
  net = tf.reduce_max(net, axis=-2, keep_dims=True)
  net1 = net

  adj_matrix = tf_util.pairwise_distance(net)
  nn_idx = tf_util.knn(adj_matrix, k=k)
  edge_feature = tf_util.get_edge_feature(net, nn_idx=nn_idx, k=k)

  net = tf_util.conv2d(edge_feature, 64, [1,1],
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training,
                       scope='dgcnn2', bn_decay=bn_decay)
  net = tf.reduce_max(net, axis=-2, keep_dims=True)
  net2 = net
 
  adj_matrix = tf_util.pairwise_distance(net)
  nn_idx = tf_util.knn(adj_matrix, k=k)
  edge_feature = tf_util.get_edge_feature(net, nn_idx=nn_idx, k=k)  

  net = tf_util.conv2d(edge_feature, 64, [1,1],
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training,
                       scope='dgcnn3', bn_decay=bn_decay)
  net = tf.reduce_max(net, axis=-2, keep_dims=True)
  net3 = net

  adj_matrix = tf_util.pairwise_distance(net)
  nn_idx = tf_util.knn(adj_matrix, k=k)
  edge_feature = tf_util.get_edge_feature(net, nn_idx=nn_idx, k=k)  
  
  net = tf_util.conv2d(edge_feature, 128, [1,1],
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training,
                       scope='dgcnn4', bn_decay=bn_decay)
  net = tf.reduce_max(net, axis=-2, keep_dims=True)
  net4 = net
  
  # 每个边特征提取后的特征连接起来,然后进行 MLP 处理
  net_out_concat = tf.concat([net1, net2, net3, net4], axis=-1)
  net_out = tf_util.conv2d(net_out_concat, 1024, [1, 1],
                       padding='VALID', stride=[1, 1],
                       bn=True, is_training=is_training,
                       scope='agg', bn_decay=bn_decay)
 
  net_out = tf.reduce_max(net_out, axis=1, keep_dims=True)

  # MLP on global point cloud vector
  net_out = tf.reshape(net_out, [batch_size, -1])
  net_out = tf_util.fully_connected(net_out, 512, bn=True, is_training=is_training,
                                scope='fc1', bn_decay=bn_decay)
  net_out = tf_util.dropout(net_out, keep_prob=0.5, is_training=is_training,
                         scope='dp1')
  net_out = tf_util.fully_connected(net_out, 256, bn=True, is_training=is_training,
                                scope='fc2', bn_decay=bn_decay)
  net_out = tf_util.dropout(net_out, keep_prob=0.5, is_training=is_training,
                        scope='dp2')
  net_out = tf_util.fully_connected(net_out, 40, activation_fn=None, scope='fc3')

  return net_out, end_points

 

下面为tensorflow的一些相关的操作 

tensorflow笔记:tf.reshape的详细讲解

原文链接:https://blog.csdn.net/abc13526222160/article/details/85867777

函数原型:目的是为了功能改变张量(tensor)的形状。

tf.reshape(
    tensor,
    shape,
    name=None
)

tensor形参传入一个tensor。shape传入一个向量,代表新tensor的维度数和每个维度的长度。如果传入[3,4,5],就会返回一个内含各分量数值和原传入张量一模一样的3*4*5尺寸的张量。

如果shape传入的向量某一个分量设置为-1,比如[-1,4,5],那么这个分量代表的维度尺寸会被自动计算出来。 

用法一,一个尺寸为1*9的张量转化为3*3的张量:

#coding:utf-8
import tensorflow as tf
t=[1,2,3,4,5,6,7,8,9]
with tf.Session() as sess:
    print (sess.run(tf.reshape(t,[3,3])))

输出结果:
[[1 2 3]
 [4 5 6]
 [7 8 9]]

用法二,一个尺寸为3 * 2 * 3的张量,转换为第二个维度尺寸为9的张量,即n*9的张量:

#coding:utf-8
import tensorflow as tf
t=[[[1,2],[1,2],[1,2]],[[1,2],[1,2],[1,2]],[[1,2],[1,2],[1,2]]]
with tf.Session() as sess:
    print (sess.run(tf.reshape(t,[-1,9])))

输出结果:显然,n被计算为2。
[[1 2 1 2 1 2 1 2 1]
 [2 1 2 1 2 1 2 1 2]]

用法三,仅含有单个元素的张量转化为标量:
t为张量[7]

#coding:utf-8
import tensorflow as tf
t=[7]
with tf.Session() as sess:
    print (sess.run(tf.reshape(t,[])))

输出结果:
7

tensorflow笔记:tf.shape()和(tensor)x.get_shape().as_list()

原文链接:https://blog.csdn.net/abc13526222160/article/details/85135517

1、Tensorflow中的tf.shape()
先说tf.shape()很显然这个是获取张量的大小的,用法无需多说,直接上例子吧!

import tensorflow as tf
import numpy as np
 
a=np.array([[1,2,3],[4,5,6]])
b=[[1,2,3],[4,5,6]]
c=tf.constant([[1,2,3],[4,5,6]])
 
with tf.Session() as sess:
    print(sess.run(tf.shape(a)))
    print(sess.run(tf.shape(b)))
    print(sess.run(tf.shape(c)))

输出结果:
[2 3]
[2 3]
[2 3]

2、(Tensor)x.get_shape().as_list()
这个简单说明一下,x.get_shape(),只有tensor(张量)才可以使用这种方法,返回的是一个元组。

import tensorflow as tf
import numpy as np
 
a=np.array([[1,2,3],[4,5,6]])
b=[[1,2,3],[4,5,6]]
c=tf.constant([[1,2,3],[4,5,6]])
 
print(c.get_shape())                   #这里返回的是一个元组
print(c.get_shape().as_list())         #返回的元组重新变回一个列表
 
with tf.Session() as sess:
    print(sess.run(tf.shape(a)))
    print(sess.run(tf.shape(b)))
    print(sess.run(tf.shape(c)))

输出结果:
(2, 3)
[2, 3]
[2 3]
[2 3]
[2 3]
不是张量进行操作的话,会报错!

下面强调一些注意点:

第一点:tensor.get_shape()返回的是元组不能放到sess.run()里面,这个里面只能放operation和tensor;

第二点:tf.shape()返回的是一个tensor。要想知道是多少,必须通过sess.run()

 

tf.range()函数

python中的range()的用法基本一样,只不过这里返回的是一个1-D的tensor。有以下两种形式:

range(limit, delta=1, dtype=None, name='range')
range(start, limit, delta=1, dtype=None, name='range')

该数字序列开始于 start 并且将以 delta 为增量扩展到不包括 limit 时的最大值结束,类似python的range函数。

#-*-coding:utf-8-*-

import tensorflow as tf

x=tf.range(8.0, 13.0, 2.0)
y=tf.range(10, 15)
z=tf.range(3, 1, -0.5)
w=tf.range(3)
sess = tf.Session()
sess.run(tf.global_variables_initializer())
print (sess.run(x))#输出[  8.  10.  12.]
print (sess.run(y))#输出[10 11 12 13 14]
print (sess.run(z))#输出[ 3.   2.5  2.   1.5]
print (sess.run(w))#输出[0 1 2]

 

直观的理解tensorflow中的tf.tile()函数

原文链接:https://blog.csdn.net/tsyccnh/article/details/82459859

tensorflow中的tile()函数是用来对张量(Tensor)进行扩展的,其特点是对当前张量内的数据进行一定规则的复制。最终的输出张量维度不变。

函数定义:

tf.tile(
    input,
    multiples,
    name=None
)

input是待扩展的张量,multiples是扩展方法。
假如input是一个3维的张量。那么mutiples就必须是一个1x3的1维张量。这个张量的三个值依次表示input的第1,第2,第3维数据扩展几倍。
具体举一个例子:

import tensorflow as tf

a = tf.constant([[1, 2], [3, 4], [5, 6]], dtype=tf.float32)
a1 = tf.tile(a, [2, 3])
with tf.Session() as sess:
    print(sess.run(a))
    print(sess.run(tf.shape(a)))
    print(sess.run(a1))
    print(sess.run(tf.shape(a1)))


[[1. 2.]
 [3. 4.]
 [5. 6.]]
[3 2]
[[1. 2. 1. 2. 1. 2.]
 [3. 4. 3. 4. 3. 4.]
 [5. 6. 5. 6. 5. 6.]
 [1. 2. 1. 2. 1. 2.]
 [3. 4. 3. 4. 3. 4.]
 [5. 6. 5. 6. 5. 6.]]
[6 6]

tf.tile()具体的操作过程如下:
(原始矩阵应为3*2)


请注意:上面绘图中第一次扩展后第一维由三个数据变成两行六个数据,多一行并不是多了一维,数据扔为顺序排列,只是为了方便绘制而已。

每一维数据的扩展都是将前面的数据进行复制然后直接接在原数据后面。

如果multiples的某一个数据为1,则表示该维数据保持不变。

就这样。

 

TensorFlow的tf.concat实例详细介绍

原文链接:https://blog.csdn.net/sinat_29957455/article/details/86100641

tf.concat函数:函数功能比较简单,主要用于连接两个数组
参数:

values:需要连接的数组
axis:从哪个维度来连接数组
例子:

一维张量

import tensorflow as tf
a = [1,2,3]
b = [4,5,6]
c = tf.concat([a,b],0)
sess = tf.InteractiveSession()
print(sess.run(c))

输出: [1 2 3 4 5 6]

注意:axis参数不能超过数组的维度。如果超过数组的维度,如下:

    c = tf.concat([a,b],1)
1
则会报,ValueError: Shape must be at least rank 2 but is rank 1 for 'concat',意思是数组至少是二维,axis才能为1。

二维张量

    a = [[1,1],[2,2],[3,3]]
    b = [[4,4],[5,5],[6,6]]
    c = tf.concat([a,b],0)
    print(sess.run(c))
    
输出:
    [[1 1]
     [2 2]
     [3 3]
     [4 4]
     [5 5]
     [6 6]]
    

    c = tf.concat([a,b],1) #等价于tf.concat([a,b],-1)
    print(sess.run(c))
    
输出:
    [[1 1 4 4]
     [2 2 5 5]
     [3 3 6 6]]

三维张量

    a = [[[1,1],[2,2]],[[3,3],[4,4]]]
    b = [[[5,5]],[[6,6]]]

    c = tf.concat([a,b],1)
    print(sess.run(c))
    """
    [[[1 1]
      [2 2]
      [5 5]]

     [[3 3]
      [4 4]
      [6 6]]]
    """

注意:在使用tf.concat函数连接两个数组的时候,数组该维度必须是一致的,否则会报错,如下:

    c = tf.concat([a,b],0)
错误提示ValueError: Dimension 0 in both shapes must be equal, but are 2 and 1,意思是a在第1个维度上shape是2,而b在第一个维度上shape是1。
总结:如何来判断数组是否在该个维度上的shape是相同的呢?其实很简单,我们根据tf.concat的axis参数来去数组的[],0表示去掉最外面的一层,1去掉两层,以此类推,下面举例说明一下。
如:最后一个例子中的c = tf.concat([a,b],1),我们先将a去掉最外面两层[],变成了[1,1],[2,2]和[3,3],[4,4]],然后再将b去掉最外面两层[],变成了[5,5]和[6,6],此时再进行concat,可以发现此时的shape是相等的。

tf.gather()用法

原文链接:https://blog.csdn.net/Eric_LH/article/details/83794038

原文链接:https://blog.csdn.net/LoseInVain/article/details/85339985

tf.gather(
    params,  # 需要被索引的张量
    indices,  # 索引
    validate_indices=None,
    name=None,
    axis=0
)

其作用很简单,就是根据提供的indices在axis这个轴上对params进行索引,拼接成一个新的张量,其示意图如下所示:

其中的indices可以是标量张量,向量张量或者是更高阶的张量,但是其元素都必须是整数类型,比如int32,int64等,而且注意检查不要越界了,因为如果越界了,如果使用的CPU,则会报错,如果在GPU上进行操作的,那么相应的输出值将会被置为0,而不会报错,因此认真检查是否越界。

简单的说: tf.gather(等待被取元素的张量,索引)

tf.gather根据索引,从输入张量中依次取元素,构成一个新的张量。

索引的维度可以小于张量的维度。这时,取张量元素时,会把相应的低维当作一个整体取出来。

例如

假设输入张量 [[1,2,3],[4,5,6],[7,8,9]] 是个二维的

如果只给一个一维索引0. 它就把[1,2,3]整体取出:

如果给两个一维索引,0和1,它就形成[[1,2,3],[4,5,6]]

  • 17
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值