Package: naps-utilities
#+title: Utility lib for naps#+author: Virgile Daugé#+EMAIL:virgile.dauge@loria.frParamètre d'export de code :#+begin_src emacs-lisp :tangle no :results silent(setq org-src-preserve-indentation t)#+end_src* Construction du Module#+begin_src python :tangle setup.py# -*- coding: utf-8 -*-from setuptools import setup, find_packageswith open('readme.org', 'r') as fh:long_description = fh.read()setup(name='naps_utilities',packages=find_packages(exclude=["examples/*"]),version='0.3.2',description='Lib to handle ease of use of pointclouds ',author=u'Virgile Daugé',author_email='virgile.dauge@loria.fr',url='https://github.com/virgileTN/naps_utilities',keywords=['pointclouds', 'filtering'],install_requires=['numpy','numpy-quaternion'],long_description=long_description,long_description_content_type='text/plain',classifiers=['Development Status :: 4 - Beta','Environment :: Console','Intended Audience :: Developers','License :: OSI Approved :: GNU General Public License (GPL)','Operating System :: MacOS :: MacOS X','Operating System :: POSIX','Programming Language :: Python :: 3.6',],)#+end_src#+begin_src bash :results value verbatim :exports bothmkdir naps_utilitiestouch naps_utilities/__init__.py#+end_src* Classe transform** Mise à dispo de la classe#+begin_src python :tangle naps_utilities/__init__.pyfrom .transform import Transform#+end_src** Imports#+begin_src python :tangle naps_utilities/transform.pyclass MultipleInputData(Exception):"""Raised when data used to populate is not valid"""pass#+end_src#+begin_src python :tangle naps_utilities/transform.py#!/usr/bin/python# -*- coding: utf-8 -*-import osimport mathimport quaternionimport numpy as np#+end_srcCes modules ne sont nécessaires que dans la version dépendante de ROSuniquement, et ne doivent pas êtres chargés dans la version=. standalone .= (il ne seront pas présents sur une machine sans ROS).Pour s'assurer de respecter ces contraintes, on va vérifier que ROStourne sur la machine, en cherchant à récupérer la variabled'environnement 'ROS_DISTRO'. Si elle n'est pas définie, on considèrequ'il faut utiliser la version =. standalone .=.#+begin_src python :tangle naps_utilities/tranform.pystandalone = True if os.getenv('ROS_DISTRO') is None else Falseif not standalone:from geometry_msgs.msg import TransformStamped#+end_src#+end_src** Corps de la fonction#+begin_src python :tangle naps_utilities/transform.pyclass Transform:# self.matrix = None  # matrix de transfo# Constructeurdef __init__(self, mat=None, quat=None, pos=None, ros_msg=None):u""" Constructeur depuis une matrice OU un quaternion et une position."""# Ensure that only one populate method is selected:conditions = [mat is not None, quat is not None and pos is not None, ros_msg is not None]if sum(conditions) > 1:raise MultipleInputDataif mat is not None:self.matrix = np.copy(mat)elif quat is not None and pos is not None:self.from_quatpos(quat, pos)elif ros_msg is not None:self.from_msg(ros_msg)else:self.matrix = np.identity(4)#+end_src** Fonction de peuplement#+begin_src python :tangle naps_utilities/transform.pydef from_quatpos(self, quat, pos):self.matrix = np.identity(4)# (w, x, y, z)quat = np.asarray(quat)npquat = quaternion.quaternion(quat[0], quat[1],quat[2], quat[3])self.matrix[:3, :3] = quaternion.as_rotation_matrix(npquat)self.matrix[:3, 3] = pos#+end_src#+begin_src python :tangle naps_utilities/transform.pydef from_msg(self, msg):self.from_quatpos(pos=[msg.transform.translation.x,msg.transform.translation.y,msg.transform.translation.z,], quat=[msg.transform.rotation.w,msg.transform.rotation.x,msg.transform.rotation.y,msg.transform.rotation.z,])#+end_src** Vers ROS msg#+begin_src python :tangle naps_utilities/transform.pydef to_msg(self, child_frame_id, frame_id='map'):msg = TransformStamped()quaternion = self.quaternion()position = self.position()msg.header.frame_id = frame_idmsg.child_frame_id = child_frame_idmsg.transform.translation.x = position[0]msg.transform.translation.y = position[1]msg.transform.translation.z = position[2]msg.transform.rotation.x = quaternion.xmsg.transform.rotation.y = quaternion.ymsg.transform.rotation.z = quaternion.zmsg.transform.rotation.w = quaternion.wreturn msg#+end_src** Fcontions internesFonctions d'affichage#+begin_src python :tangle naps_utilities/transform.pydef __str__(self):u"""Affichage de la transformation."""return self.matrix.__str__()def __repr__(self):u"""Représentation interne de la classe."""return self.matrix.__repr__()#+end_srcConversion quaternion vers matrice de rotation, ici on utilise lemodule numpy-quaternion.#+begin_src python :tangle naps_utilities/transform.pydef quat_2_mat(self, quat, pos):u"""Conversion quaternion vers matrix."""self.matrix[:3, :3] = quaternion.as_rotation_matrix(quat)self.matrix[:3, 3] = pos#+end_srcOpérations sur la matrice de tranformation :#+begin_src python :tangle naps_utilities/transform.pydef inverse(self):u"""Inverse de la transformation."""return Transform(np.linalg.inv(self.matrix))def __invert__(self):u"""Inverse de la transformation inplace."""return Transform(np.linalg.inv(self.matrix))def __sub__(self, other):u"""Renvoie la transformation dans self du repère à l'origine de la transformation other."""return self.composition(~other)def __isub__(self, other):u"""Version 'inplace' de sub."""self = self.composition(~other)return selfdef composition(self, tr):u"""Composition de transformations."""return Transform(mat=np.dot(self.matrix, tr.matrix))def __mul__(self, other):u"""Composition de la transformation de other dans self."""return self.composition(other)def __imul__(self, other):u""""Version 'inplace' de mul."""self.matrix = self.matrix.dot(other.matrix)return selfdef relative_transform(self, other):u"""Transformation de self dans le repère other."""return ~other.composition(self)def projection(self, pt):u"""Transformation d'un point."""if (len(pt) == 3):return self.matrix.dot(pt + [1])else:return self.matrix.dot(pt)#+end_srcAccès à la position et au quaternion indépendamment :#+begin_src python :tangle naps_utilities/transform.pydef position(self):u"""Extraction de la position depuis matrix."""return self.matrix[:3, 3]def quaternion(self):u"""Extraction du quaternion depuis matrix."""return quaternion.from_rotation_matrix(self.matrix)#+end_src** Tests#+begin_src ipython :session testTransform :file  :exports bothfrom naps_utilities import Transformt = Transform(pos=[1,2,3], quat=[1,0,0,0])t#+end_src#+RESULTS:: # Out[3]:: #+BEGIN_EXAMPLE:   array([[1., 0., 0., 1.],:   [0., 1., 0., 2.],:   [0., 0., 1., 3.],:   [0., 0., 0., 1.]]): #+END_EXAMPLE#+begin_src ipython :session testTransform :file  :exports botht.to_msg('truc')#+end_src#+RESULTS:: # Out[5]:: : geometry_msgs.msg.TransformStamped(header=std_msgs.msg.Header(stamp=builtin_interfaces.msg.Time(sec=0, nanosec=0), frame_id='map'), child_frame_id='truc', transform=geometry_msgs.msg.Transform(translation=geometry_msgs.msg.Vector3(x=1.0, y=2.0, z=3.0), rotation=geometry_msgs.msg.Quaternion(x=-0.0, y=-0.0, z=-0.0, w=1.0)))* Classe Pointcloud** Mise à dispo de la classe#+begin_src python :tangle naps_utilities/__init__.pyfrom .pointcloud import Pointcloud#+end_src** DependencesIl faut pouvoir utiliser cette classe comme interface avec ROS, maiségalement en =. standalone .=, sur une machine qui n'est pas équipéede ROS.Ces modules sont nécessaires dans tous les cas.#+begin_src python :tangle naps_utilities/pointcloud.py# Nécessaires pour la lecture/écriture de fichiersimport osimport jsonimport pickle#Les données sont stockées sous forme de numpy ndarrayimport numpy as np#+end_srcCes modules ne sont nécessaires que dans la version dépendante de ROSuniquement, et ne doivent pas êtres chargés dans la version=. standalone .= (il ne seront pas présents sur une machine sans ROS).Pour s'assurer de respecter ces contraintes, on va vérifier que ROStourne sur la machine, en cherchant à récupérer la variabled'environnement 'ROS_DISTRO'. Si elle n'est pas définie, on considèrequ'il faut utiliser la version =. standalone .=.#+begin_src python :tangle naps_utilities/pointcloud.pystandalone = True if os.getenv('ROS_DISTRO') is None else False#+end_src#+begin_src python :tangle naps_utilities/pointcloud.pyif not standalone:#Nécessaire pour la conversion vers/depuis ROS2from builtin_interfaces.msg import Timefrom sensor_msgs.msg import PointCloud2from sensor_msgs.msg import PointFieldfrom std_msgs.msg import Headerfrom array import array#+end_src#+begin_src python :tangle naps_utilities/pointcloud.pyclass TransformWhileEmpty(Exception):"""Raised when transform method is called and the poincloud is not yetpopulated"""pass#+end_src#+begin_src python :tangle naps_utilities/pointcloud.pyclass InvalidInputData(Exception):"""Raised when data used to populate is not valid"""pass#+end_src#+begin_src python :tangle naps_utilities/pointcloud.pyclass MultipleInputData(Exception):"""Raised when data used to populate is not valid"""pass#+end_src** Corps de la classe#+begin_src python :tangle naps_utilities/pointcloud.pyclass Pointcloud():def __init__(self, ros_msg=None, points=None, keep_ring=True,matrix=None, procrastinate=False, inpath=None):# PoinCloud Metadataself.metadata = {'header': None,'height': None,'width': None,'fields': None,'is_bigendian': None,'point_step': None,'row_step': None,'is_dense': None,'keep_ring': None,'is_populated': False,'procrastinated': True,}# Pointcloud DATAself.points = Noneself.rings = Noneself.matrix = matrix# Additionnal DATAself.devices_matrices = Noneself.devices_names = None# Ensure that only one populate method is selected:conditions = [ros_msg is not None, points is not None, inpath is not None]if sum(conditions) > 1:raise MultipleInputDataelse:if ros_msg is not None:self.from_msg(ros_msg)elif points is not None:self.from_list(points)elif inpath is not None:self.load(inpath)if self.metadata['is_populated']:if not procrastinate:self.filter()if matrix is None:self.matrix = np.identity(4)self.metadata['procrastinated'] = Falseelse:self.transform(matrix)#+end_src** Populate from list#+begin_src python :tangle naps_utilities/pointcloud.pydef from_list(self, data):self.metadata['keep_ring'] = Falseself.points = np.ascontiguousarray(data, dtype=np.float32)if self.points.shape[1] != 3 and self.points.shape[1] != 4:raise InvalidInputDataself.metadata['nb_points'] = len(self.points)self.metadata['height'] = 1self.metadata['width'] = self.metadata['nb_points']self.metadata['is_bigendian'] = Falseself.metadata['point_step'] = 3 * 4self.metadata['row_step'] = self.metadata['point_step']self.metadata['is_dense'] = Falseself.metadata['is_populated'] = True#+end_src** populate from ROS msgUn certain nombre de données ne nécessitent pas de conversion :#+begin_src python :tangle naps_utilities/pointcloud.pydef from_msg(self, msg):#Données conservées "telles quelles"self.metadata['height'] = msg.heightself.metadata['width'] = msg.widthself.metadata['is_bigendian'] = msg.is_bigendianself.metadata['point_step'] = msg.point_stepself.metadata['row_step'] = msg.row_stepself.metadata['is_dense'] = msg.is_dense#+end_srcL'atribut Header est du type std_msgs/Header:#+begin_src python :tangle naps_utilities/pointcloud.pydef from_header(header):return {'time': {'sec': header.stamp.sec, 'nanosec': header.stamp.nanosec},'frame_id': header.frame_id}self.metadata['header'] = from_header(msg.header)#+end_srcL'attribut fields du msg ROS est une liste d'objets PointFields.  Ilconvient également de supprimer le fields ring, si l'on choisit de nepas les garder.#+begin_src python :tangle naps_utilities/pointcloud.pydef from_pointfields(fields):return [{'name': field.name,'offset': field.offset,'datatype': field.datatype,'count': field.count}for field in fields]self.metadata['fields'] = from_pointfields(msg.fields)#+end_srcAfin de préparer l'extraction, on initialise des numpy ndarray afinque tous les points soient dans un espace contigu de la mémoire. Icion sépare les points en un tableau de float32 (x, y, z) et un tableaude (ring). Cela pour faciliter l'encodage décodage (c'est plusdifficile avec des types différents imbriqués)#+begin_src python :tangle naps_utilities/pointcloud.py# Données convertiesself.metadata['nb_points'] = msg.height * msg.widthdata = np.reshape(msg.data, (-1, self.metadata['point_step']))self.points = np.ndarray((self.metadata['nb_points'], 4), dtype=np.float32,buffer=np.ascontiguousarray(data[:, :16]))if self.metadata['keep_ring']:self.metadata['rings'] = np.zeros(self.metadata['nb_points'], dtype=np.uint16)pointcloud['rings'] = np.ndarray((self.metadata['nb_points']), dtype=np.uint16,buffer=np.ascontiguousarray(data[:, 16:]))#+end_srcMise à jour dé métadonnées si nécessaire :#+begin_src python :tangle naps_utilities/pointcloud.pyif not self.metadata['keep_ring']:self.metadata['fields'] = [field for field in self.metadata['fields'] if field['name'] != 'ring']self.metadata['point_step'] = 16self.metadata['row_step'] = self.metadata['point_step'] * len(self.metadata['fields'])self.metadata['is_populated'] = True#+end_src** convert to msgBeaucoup de symétrie avec la fonction précedante.#+begin_src python :tangle naps_utilities/pointcloud.pydef to_msg(self):msg = PointCloud2()#Données conservées "telles quelles"msg.height = self.metadata['height']msg.width = self.metadata['width']msg.is_bigendian = self.metadata['is_bigendian']msg.point_step = self.metadata['point_step']msg.row_step = self.metadata['row_step']msg.is_dense = self.metadata['is_dense']#+end_srcConversion vers Header ROS:#+begin_src python :tangle naps_utilities/pointcloud.pydef to_header(header_data):return Header(stamp=Time(sec=header_data['time']['sec'],nanosec=header_data['time']['nanosec']),frame_id=header_data['frame_id'])msg.header = to_header(self.metadata['header'])#+end_srcConversion vers Pointfield:#+begin_src python :tangle naps_utilities/pointcloud.pydef to_pointfields(pointfields_data):return [PointField(name=field['name'],offset=field['offset'],datatype=field['datatype'],count=field['count']) for field in pointfields_data]msg.fields = to_pointfields(self.metadata['fields'])#+end_srcDeux cas, selon la valeur de 'keep_ring':Si on garde les rings, il faut concatener les deux tableaux et enfaire un array de uint8.#+begin_src python :tangle naps_utilities/pointcloud.pyif self.metadata['keep_ring']:msg.data = array('B', np.concatenate((self.points.view(dtype=np.uint8),self.rings.reshape((self.metadata['nb_points'], -1)).view(dtype=np.uint8)),axis=1).ravel().tolist())#+end_srcSinon, il suffi de créer une liste de uint8 à partir des points auniveau des données.#+begin_src python :tangle naps_utilities/pointcloud.pyelse:msg.data = array('B', self.points.view(dtype=np.uint8).ravel().tolist())return msg#+end_src** filter pointcloudIl y a deux cas a traiter, si l'on garde les rings auquel cas il fautles filter aussi.#+begin_src python :tangle naps_utilities/pointcloud.pydef filter(self, threshold=10):if self.metadata['keep_ring']:concat = np.concatenate((self.points, self.rings.reshape((len(points), 1))), axis=1)concat = concat[np.logical_and(np.logical_not(np.isnan(concat).any(axis=1)),concat[:,3]>=threshold)]self.points = np.ascontiguousarray(concat[:,:4], dtype=np.float32)self.rings = np.ascontiguousarray(concat[:,4:], dtype=np.uint16)#+end_srcAprès avoir été filtré, le poincloud ne peut plus être structuré dansun tableau 2D.#+begin_src python :tangle naps_utilities/pointcloud.pyelse:self.points = self.points[np.logical_and(np.logical_not(np.isnan(self.points).any(axis=1)),self.points[:,3]>=threshold)]self.metadata['nb_points'] = len(self.points)self.metadata['height'] = 1self.metadata['width'] = self.metadata['nb_points']#+end_src** transform pointcloud#+begin_src python :tangle naps_utilities/pointcloud.pydef transform(self, matrix):if self.metadata['is_populated']:self.points[:,:3] = np.transpose(matrix @ np.concatenate((self.points[:,:3].transpose(),np.ones((1, self.metadata['nb_points'])))))[:,:3]self.matrix = matrixself.metadata['procrastinated'] = Falseelse:raise TransformWhileEmpty("Populate pointcloud before applying transform to it")#+end_src** add points#+begin_src python :tangle naps_utilities/pointcloud.pydef update(self, pointcloud):if self.metadata['keep_ring']:if pointcloud.metadata['keep_ring']:self.rings = np.ascontiguousarray(np.concatenate((self.rings, pointcloud.rings)))else:return Falseself.points = np.ascontiguousarray(np.concatenate((self.points, pointcloud.points)))self.metadata['nb_points'] = len(self.points)self.metadata['height'] = 1self.metadata['width'] = self.metadata['nb_points']return True#+end_src** add areas to filterCette méthode permet d'ajouter des zones à filtrer ultérieurement. Ils'agit ici de stocker les zones qui correspondent aux différentsrobots de notre système. En effet, on ne souhaite pas ajouter cesrobots dans la carte d'autant plus qu'ils sont mobiles.#+begin_src python :tangle naps_utilities/pointcloud.pydef set_devices(self, names, matrices):self.devices_names = namesself.devices_matrices = np.asarray(matrices)#+end_src** Export/Import#+begin_src python :tangle naps_utilities/pointcloud.pydef save(self, path):self.save_npz(path)#+end_src*** npz#+begin_src python :tangle naps_utilities/pointcloud.pydef save_npz(self, path):save_path = os.path.expanduser(path)# with open('{}_meta.json'.format(save_path), 'w') as outfile:#     json.dump(self.metadata, outfile, indent=4)np.savez_compressed('{}'.format(save_path),meta=[self.metadata],points=self.points,rings=self.rings,matrix=self.matrix,devices_names=self.devices_names,devices_matrices=self.devices_matrices)#+end_src#+begin_src python :tangle naps_utilities/pointcloud.pydef load(self, path):load_path = os.path.expanduser(path)# with open('{}_meta.json'.format(load_path), 'r') as infile:#     self.metadata = json.load(infile)with np.load(load_path, allow_pickle=True) as data:self.metadata = data['meta'][0]if 'matrix' in data:self.matrix = data['matrix']if 'points' in data:self.points = data['points']if 'rings' in data:self.rings = data['rings']if 'devices_names' in data:self.devices_names = data['devices_names']if 'devices_matrices' in data:self.devices_matrices = data['devices_matrices']#+end_src*** xyz#+begin_src python :tangle naps_utilities/pointcloud.pydef save_xyz(self, path):save_path = os.path.expanduser(path)np.savetxt('{}.xyz'.format(save_path), self.points)#with open('{}.xyz'.format(save_path), 'w') as outfile:#     json.dump(self.metadata, outfile, indent=4)#+end_src* Conversions dépendantes de ROS** Mise à dispo:** Dependances#+begin_src python :tangle naps_utilities/conversion.py#Nécessaire pour la conversion vers/depuis ROS2from builtin_interfaces.msg import Timefrom sensor_msgs.msg import PointCloud2from sensor_msgs.msg import PointFieldfrom std_msgs.msg import Headerfrom array import array#+end_src** to ros headerConversion vers Header ROS:#+begin_src python :tangle naps_utilities/conversion.pydef to_ros_header(header_data):return Header(stamp=Time(sec=header_data['time']['sec'],nanosec=header_data['time']['nanosec']),frame_id=header_data['frame_id'])#+end_src** to ros PointfieldConversion vers Pointfield:#+begin_src python :tangle naps_utilities/conversion.pydef to_ros_pointfields(pointfields_data):return [PointField(name=field['name'],offset=field['offset'],datatype=field['datatype'],count=field['count']) for field in pointfields_data]#+end_src** pointcloud to msgBeaucoup de symétrie avec la fonction précedante.#+begin_src python :tangle naps_utilities/conversion.pydef to_ros_PointCloud2(pc):msg = PointCloud2()#Données conservées "telles quelles"msg.height = pc.metadata['height']msg.width = pc.metadata['width']msg.is_bigendian = pc.metadata['is_bigendian']msg.point_step = pc.metadata['point_step']msg.row_step = pc.metadata['row_step']msg.is_dense = pc.metadata['is_dense']msg.header = to_header(pc.metadata['header'])msg.fields = to_pointfields(pc.metadata['fields'])#+end_srcDeux cas, selon la valeur de 'keep_ring':Si on garde les rings, il faut concatener les deux tableaux et enfaire un array de uint8.#+begin_src python :tangle naps_utilities/conversion.pyif pc.metadata['keep_ring']:msg.data = array('B', np.concatenate((pc.points.view(dtype=np.uint8),pc.rings.reshape((pc.metadata['nb_points'], -1)).view(dtype=np.uint8)),axis=1).ravel().tolist())#+end_srcSinon, il suffi de créer une liste de uint8 à partir des points auniveau des données.#+begin_src python :tangle naps_utilities/conversion.pyelse:msg.data = array('B', self.points.view(dtype=np.uint8).ravel().tolist())return msg#+end_src* Tests#+begin_src ipython :session session01 :file  :exports bothimport numpy as npfrom naps_utilities import Pointcloudpoints = [[0, 0, 0, 4],[1, 0, 0, 5],[0, 1, 0, 105],[0, 0, 1, 452],]p = Pointcloud(points=points)#+end_src#+RESULTS:: # Out[9]:#+begin_src ipython :session session01 :file  :exports bothp = Pointcloud(points=points)#+end_src* Build et distribution#+begin_src bash :results value verbatim :exports bothrm -r build dist naps_utilities.egg-infopython setup.py bdist_wheel sdist#+end_src#+RESULTS:#+begin_examplerunning bdist_wheelrunning buildrunning build_pycopying naps_utilities/conversion.py -> build/lib/naps_utilitiescopying naps_utilities/pointcloud.py -> build/lib/naps_utilitiescopying naps_utilities/transform.py -> build/lib/naps_utilitiescopying naps_utilities/tranform.py -> build/lib/naps_utilitiescopying naps_utilities/__init__.py -> build/lib/naps_utilitiesinstalling to build/bdist.linux-x86_64/wheelrunning installrunning install_libcreating build/bdist.linux-x86_64/wheelcreating build/bdist.linux-x86_64/wheel/naps_utilitiescopying build/lib/naps_utilities/conversion.py -> build/bdist.linux-x86_64/wheel/naps_utilitiescopying build/lib/naps_utilities/pointcloud.py -> build/bdist.linux-x86_64/wheel/naps_utilitiescopying build/lib/naps_utilities/transform.py -> build/bdist.linux-x86_64/wheel/naps_utilitiescopying build/lib/naps_utilities/tranform.py -> build/bdist.linux-x86_64/wheel/naps_utilitiescopying build/lib/naps_utilities/__init__.py -> build/bdist.linux-x86_64/wheel/naps_utilitiesrunning install_egg_inforunning egg_infowriting naps_utilities.egg-info/PKG-INFOwriting dependency_links to naps_utilities.egg-info/dependency_links.txtwriting requirements to naps_utilities.egg-info/requires.txtwriting top-level names to naps_utilities.egg-info/top_level.txtreading manifest file 'naps_utilities.egg-info/SOURCES.txt'writing manifest file 'naps_utilities.egg-info/SOURCES.txt'Copying naps_utilities.egg-info to build/bdist.linux-x86_64/wheel/naps_utilities-0.3.2-py3.8.egg-inforunning install_scriptscreating build/bdist.linux-x86_64/wheel/naps_utilities-0.3.2.dist-info/WHEELcreating 'dist/naps_utilities-0.3.2-py3-none-any.whl' and adding 'build/bdist.linux-x86_64/wheel' to itadding 'naps_utilities/__init__.py'adding 'naps_utilities/conversion.py'adding 'naps_utilities/pointcloud.py'adding 'naps_utilities/tranform.py'adding 'naps_utilities/transform.py'adding 'naps_utilities-0.3.2.dist-info/METADATA'adding 'naps_utilities-0.3.2.dist-info/WHEEL'adding 'naps_utilities-0.3.2.dist-info/top_level.txt'adding 'naps_utilities-0.3.2.dist-info/RECORD'removing build/bdist.linux-x86_64/wheelrunning sdistrunning checkcreating naps_utilities-0.3.2creating naps_utilities-0.3.2/naps_utilitiescreating naps_utilities-0.3.2/naps_utilities.egg-infocopying files to naps_utilities-0.3.2...copying setup.py -> naps_utilities-0.3.2copying naps_utilities/__init__.py -> naps_utilities-0.3.2/naps_utilitiescopying naps_utilities/conversion.py -> naps_utilities-0.3.2/naps_utilitiescopying naps_utilities/pointcloud.py -> naps_utilities-0.3.2/naps_utilitiescopying naps_utilities/tranform.py -> naps_utilities-0.3.2/naps_utilitiescopying naps_utilities/transform.py -> naps_utilities-0.3.2/naps_utilitiescopying naps_utilities.egg-info/PKG-INFO -> naps_utilities-0.3.2/naps_utilities.egg-infocopying naps_utilities.egg-info/SOURCES.txt -> naps_utilities-0.3.2/naps_utilities.egg-infocopying naps_utilities.egg-info/dependency_links.txt -> naps_utilities-0.3.2/naps_utilities.egg-infocopying naps_utilities.egg-info/requires.txt -> naps_utilities-0.3.2/naps_utilities.egg-infocopying naps_utilities.egg-info/top_level.txt -> naps_utilities-0.3.2/naps_utilities.egg-infoWriting naps_utilities-0.3.2/setup.cfgCreating tar archiveremoving 'naps_utilities-0.3.2' (and everything under it)#+end_example#+begin_src bash :results value verbatim :exports bothtwine upload dist/*#+end_src#+RESULTS:#+begin_src bash :results value verbatim :exports bothpip3 install -e .#+end_src#+RESULTS:#+begin_exampleObtaining file:///home/virgile/naps_utilitiesCollecting numpy (from naps-utilities==0.2.0)Using cached https://files.pythonhosted.org/packages/62/20/4d43e141b5bc426ba38274933ef8e76e85c7adea2c321ecf9ebf7421cedf/numpy-1.18.1-cp36-cp36m-manylinux1_x86_64.whlCollecting numpy-quaternion (from naps-utilities==0.2.0)Installing collected packages: numpy, numpy-quaternion, naps-utilitiesFound existing installation: naps-utilities 0.2.0Uninstalling naps-utilities-0.2.0:Successfully uninstalled naps-utilities-0.2.0Running setup.py develop for naps-utilitiesSuccessfully installed naps-utilities numpy-1.18.1 numpy-quaternion-2019.12.11.22.25.52#+end_example