//  [python]
def feature_encoder(point_cloud, is_training, k=20, stride=1, scale_invariant=False, bn_decay=False):
  """ ConvNet baseline, input is BxNx9 gray image """
  batch_size = point_cloud.get_shape()[0].value
  num_point = point_cloud.get_shape()[1].value
  input_image = tf.expand_dims(point_cloud, -1)

  k = 20
  adj0 = tf_util.pairwise_distance(point_cloud)
  nn_idx0 = tf_util.knn(adj0, k=k)  # (batch, num_points, k)

  if scale_invariant:
    neighb0_xyz = gather_neighbour(point_cloud, nn_idx0)
    xyz_tile = tf.tile(tf.expand_dims(point_cloud, axis=2), [1, 1, tf.shape(nn_idx0)[-1], 1])
    relative_xyz = (xyz_tile - neighb0_xyz)
    relative_dis = tf.sqrt(tf.reduce_sum(tf.square(relative_xyz), axis=-1, keepdims=True)) # # BxNxkx1
    relative_dis_sum = tf.reduce_sum(relative_dis, axis=-2, keepdims=True) + 1e-7
    relative_xyz_sum = tf.reduce_sum(relative_xyz, axis=-2, keepdims=True)
    
    feature0 = tf.div(relative_xyz_sum, relative_dis_sum)
    feature = tf.tile(feature0, [1, 1, k, 1])
    relative_xyz_dis = tf.div(relative_xyz, relative_dis_sum)
    edge_feature = tf.concat([feature, relative_xyz_dis - feature], axis=-1) # # rel 6
  else:
    edge_feature = tf_util.get_edge_feature(input_image, nn_idx=nn_idx0, k=k)


  out1 = tf_util.conv2d(edge_feature, 64, [1,1],
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training, weight_decay=0.0,
                       scope='adj_conv1', bn_decay=bn_decay, is_dist=True)
  
  out2 = tf_util.conv2d(out1, 64, [1,1],
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training, weight_decay=0.0,
                       scope='adj_conv2', bn_decay=bn_decay, is_dist=True)

  net_1 = tf.reduce_max(out2, axis=-2, keep_dims=True)

  # k=60
  adj = tf_util.pairwise_distance(net_1)
  nn_idx = tf_util.knn(adj, k=k)
  edge_feature = tf_util.get_edge_feature(net_1, nn_idx=nn_idx, k=k)

  out3 = tf_util.conv2d(edge_feature, 64, [1, 1],
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training, weight_decay=0.0,
                       scope='adj_conv3', bn_decay=bn_decay, is_dist=True)

  out4 = tf_util.conv2d(out3, 64, [1,1],
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training, weight_decay=0.0,
                       scope='adj_conv4', bn_decay=bn_decay, is_dist=True)
  
  net_2 = tf.reduce_max(out4, axis=-2, keep_dims=True)

  adj = tf_util.pairwise_distance(net_2)
  nn_idx = tf_util.knn(adj, k=k)
  edge_feature = tf_util.get_edge_feature(net_2, nn_idx=nn_idx, k=k)

  out5 = tf_util.conv2d(edge_feature, 64, [1,1],
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training, weight_decay=0.0,
                       scope='adj_conv5', bn_decay=bn_decay, is_dist=True)

  out6 = tf_util.conv2d(out5, 64, [1,1],
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training, weight_decay=0.0,
                       scope='adj_conv6', bn_decay=bn_decay, is_dist=True)

  net_3 = tf.reduce_max(out6, axis=-2, keep_dims=True)

  out7 = tf_util.conv2d(tf.concat([net_1, net_2, net_3], axis=-1), 1024, [1, 1], 
                       padding='VALID', stride=[1,1],
                       bn=True, is_training=is_training,
                       scope='adj_conv7', bn_decay=bn_decay, is_dist=True)

  out_max = tf_util.max_pool2d(out7, [num_point, 1], padding='VALID', scope='maxpool')

  expand = tf.tile(out_max, [1, num_point, 1, 1])

  concat = tf.concat(axis=3, values=[expand, 
                                     net_1,
                                     net_2,
                                     net_3])
  return concat