text: from torch import nn, Tensor
  import numpy as np
  from lib.pointnet2.pointnet2_modules import PointnetSAModule
  
  
  def get_siamese_features(net, in_features, aggregator=None):
      """ Applies a network in a siamese way, to 'each' in_feature independently
      :param net: nn.Module, Feat-Dim to new-Feat-Dim
      :param in_features: B x N-objects x Feat-Dim
      :param aggregator, (opt, None, torch.stack, or torch.cat)
      :return: B x N-objects x new-Feat-Dim
      """
      independent_dim = 1
      n_items = in_features.size(independent_dim)
      out_features = []
      for i in range(n_items):
          out_features.append(net(in_features[:, i]))
      if aggregator is not None:
          out_features = aggregator(out_features, dim=independent_dim)
      return out_features
  
  
  def break_up_pc(pc: Tensor):
      """
      Split the pointcloud into xyz positions and features tensors.
      This method is taken from VoteNet codebase (https://github.com/facebookresearch/votenet)
  
      @param pc: pointcloud [N, 3 + C]
      :return: the xyz tensor and the feature tensor
      """
      xyz = pc[..., 0:3].contiguous()
      features = (
          pc[..., 3:].transpose(1, 2).contiguous()
          if pc.size(-1) > 3 else None
      )
      return xyz, features
  
  
  class PointNetPP(nn.Module):
      """
      Pointnet++ encoder.
      For the hyper parameters please advise the paper (https://arxiv.org/abs/1706.02413)
      """
  
      def __init__(self, sa_n_points: list,
                   sa_n_samples: list,
                   sa_radii: list,
                   sa_mlps: list,
                   bn=True,
                   use_xyz=True):
          super().__init__()
  
          n_sa = len(sa_n_points)
          if not (n_sa == len(sa_n_samples) == len(sa_radii) == len(sa_mlps)):
              raise ValueError('Lens of given hyper-params are not compatible')
  
          self.encoder = nn.ModuleList()
  
          for i in range(n_sa):
              self.encoder.append(PointnetSAModule(
                  npoint=sa_n_points[i],
                  nsample=sa_n_samples[i],
                  radius=sa_radii[i],
                  mlp=sa_mlps[i],
                  bn=bn,
                  use_xyz=use_xyz,
              ))
  
          out_n_points = sa_n_points[-1] if sa_n_points[-1] is not None else 1
          self.fc = nn.Linear(out_n_points * sa_mlps[-1][-1], sa_mlps[-1][-1])
  
      def forward(self, features):
          """
          @param features: B x N_objects x N_Points x 3 + C
          """
          xyz, features = break_up_pc(features)
          for i in range(len(self.encoder)):
              xyz, features = self.encoder[i](xyz, features)
  
          return self.fc(features.view(features.size(0), -1))
  
  
  def show_point_clouds(pts, out):
      fout = open(out, 'w')
      MEAN_COLOR_RGB = np.array([109.8, 97.2, 83.8])
      color = pts[:, 3:6] + MEAN_COLOR_RGB / 255
      for i in range(pts.shape[0]):
          fout.write('v %f %f %f %f %f %f\n' % (
              pts[i, 0], pts[i, 1], pts[i, 2], color[i, 0], color[i, 1], color[i, 2]))
      fout.close()