726 views
--- title: VISI601_CMI Maillages en Python et numpy --- # Maillages en Python et numpy > [name=Jacques-Olivier Lachaud][time=January 2022][color=#907bf7] > (Les images peuvent être soumises à des droits d'auteur. Elles sont utilisées ici exclusivement dans un but pédagogique) ###### tags: `visi601` Retour à [VISI601_CMI (Main) Algorithmique numérique](https://codimd.math.cnrs.fr/s/IWTaBkA9m) --- [TOC] --- Il y a beaucoup de façon de représenter des maillages (surfaces triangulées ou polygonales) en Python. Nous en proposons ici une qui a l'avantage d'être simple (en gros des tableaux `numpy`) et d'offrir une interface très pratique et évolutive [Polyscope - python](https://polyscope.run/py). ## Installation On suppose que vous avez déjà installé [numpy](https://numpy.org/doc/stable/user/index.html). Pour installer Polyscope, rien de plus simple: ```python python -m pip install polyscope ``` ## Utilisation Dans vos code importez `numpy`et `polyscope` avant d'utiliser les fonctions associées ```python import polyscope as ps import numpy as np # Initialise polyscope ps.init() ``` ## Créer une surface avec des faces polygonales Voici comment créer une pyramide avec une base carrée. ```python # `verts` is a Nx3 numpy array of vertex positions # `faces` is a Fx3 array of indices, or a nested list verts=np.array([[1.,0.,0.],[0.,1.,0.],[-1.,0.,0.],[0.,-1.,0.],[0.,0.,1.]]) faces=[[0,1,2,3],[1,0,4],[2,1,4],[3,2,4],[0,3,4]] ps.register_surface_mesh("my mesh", verts, faces ) ``` ## Donner la main à Polyscope pour visualiser la surface Il suffit d'appeler la fonction `show()` de Polyscope et il affiche automatiquement toutes les surfaces qui ont été déclarées. ```python ps.show() ``` ![poly-pyramide](https://codimd.math.cnrs.fr/uploads/upload_d8e236f9293699cf337070f81e645a39.png =x300) ## Charger une surface définie dans le format OBJ (Wavefront) OBJ est un format de fichier relativement simple pour représenter des objets 3D. On va utiliser un code tout fait pour charger et/ou sauvegarder de telles données. Il est à 90% pris de http://jamesgregson.ca/loadsave-wavefront-obj-files-in-python.html, merci à lui. Copiez le code ci-dessous dans un fichier `wavefront.py`. ```python # wavefront.py import numpy as np class WavefrontOBJ: def __init__( self, default_mtl='default_mtl' ): self.path = None # path of loaded object self.mtllibs = [] # .mtl files references via mtllib self.mtls = [ default_mtl ] # materials referenced self.mtlid = [] # indices into self.mtls for each polygon self.vertices = [] # vertices as an Nx3 or Nx6 array (per vtx colors) self.normals = [] # normals self.texcoords = [] # texture coordinates self.polygons = [] # M*Nv*3 array, Nv=# of vertices, stored as\ vid,tid,nid (-1 for N/A) def only_coordinates( self ): V=np.zeros( ( len( self.vertices ), 3 ), np.float64 ) for i in range(len(self.vertices)): V[i][0]= self.vertices[i][0] V[i][1]= self.vertices[i][1] V[i][2]= self.vertices[i][2] return V def only_faces( self ): all_faces=[] for f in self.polygons: face=[] for indices in f: face.append( indices[0] ) all_faces.append( face ) return all_faces def load_obj( filename: str, default_mtl='default_mtl', triangulate=False ) -> WavefrontOBJ: """Reads a .obj file from disk and returns a WavefrontOBJ instance Handles only very rudimentary reading and contains no error handling! Does not handle: - relative indexing - subobjects or groups - lines, splines, beziers, etc. """ # parses a vertex record as either vid, vid/tid, vid//nid or vid/tid/nid # and returns a 3-tuple where unparsed values are replaced with -1 def parse_vertex( vstr ): vals = vstr.split('/') vid = int(vals[0])-1 tid = int(vals[1])-1 if len(vals) > 1 and vals[1] else -1 nid = int(vals[2])-1 if len(vals) > 2 else -1 return (vid,tid,nid) with open( filename, 'r' ) as objf: obj = WavefrontOBJ(default_mtl=default_mtl) obj.path = filename cur_mat = obj.mtls.index(default_mtl) for line in objf: toks = line.split() if not toks: continue if toks[0] == 'v': obj.vertices.append( [ float(v) for v in toks[1:]] ) elif toks[0] == 'vn': obj.normals.append( [ float(v) for v in toks[1:]] ) elif toks[0] == 'vt': obj.texcoords.append( [ float(v) for v in toks[1:]] ) elif toks[0] == 'f': poly = [ parse_vertex(vstr) for vstr in toks[1:] ] if triangulate: for i in range(2,len(poly)): obj.mtlid.append( cur_mat ) obj.polygons.append( (poly[0], poly[i-1], poly[i] ) ) else: obj.mtlid.append(cur_mat) obj.polygons.append( poly ) elif toks[0] == 'mtllib': obj.mtllibs.append( toks[1] ) elif toks[0] == 'usemtl': if toks[1] not in obj.mtls: obj.mtls.append(toks[1]) cur_mat = obj.mtls.index( toks[1] ) return obj def save_obj( obj: WavefrontOBJ, filename: str ): """Saves a WavefrontOBJ object to a file Warning: Contains no error checking! """ with open( filename, 'w' ) as ofile: for mlib in obj.mtllibs: ofile.write('mtllib {}\n'.format(mlib)) for vtx in obj.vertices: ofile.write('v '+' '.join(['{}'.format(v) for v in vtx])+'\n') for tex in obj.texcoords: ofile.write('vt '+' '.join(['{}'.format(vt) for vt in tex])+'\n') for nrm in obj.normals: ofile.write('vn '+' '.join(['{}'.format(vn) for vn in nrm])+'\n') if not obj.mtlid: obj.mtlid = [-1] * len(obj.polygons) poly_idx = np.argsort( np.array( obj.mtlid ) ) cur_mat = -1 for pid in poly_idx: if obj.mtlid[pid] != cur_mat: cur_mat = obj.mtlid[pid] ofile.write('usemtl {}\n'.format(obj.mtls[cur_mat])) pstr = 'f ' for v in obj.polygons[pid]: # UGLY! vstr = '{}/{}/{} '.format(v[0]+1,v[1]+1 if v[1] >= 0 else 'X', v[2]+1 if v[2] >= 0 else 'X' ) vstr = vstr.replace('/X/','//').replace('/X ', ' ') pstr += vstr ofile.write( pstr+'\n') ``` Ensuite pour charger un fichier OBJ, puis créer une surface, rien de plus simple: ```python from wavefront import * obj = load_obj( 'spot.obj') ps.init() ps_mesh = ps.register_surface_mesh("spot", obj.only_coordinates(), obj.only_faces() ) ps.show() ``` ![poly-spot](https://codimd.math.cnrs.fr/uploads/upload_93e73560f87dd9e33887833315b54fa0.png =x300) ## Extraire les bords dans un maillage Le code ci-dessous, à ajouter à la classe `WavefrontOBJ` vous permet d'extraire les bords dans un maillage (i.e. les arêtes incidentes à une seule face). La méthode ci-dessous retourne les sommets sur le bord, sous forme d'une liste `[]`. ```python ### return boundary vertices def boundary_vertices( self ): bdry_v= set() darts = {} faces = self.only_faces() for f in faces: for i in range( len(f) ): if ( f[i] in darts ): darts[ f[ i ] ].append( f[ (i+1)%len(f) ] ) else: darts[ f[ i ] ] = [ f[ (i+1)%len(f) ] ] for i, i_list in darts.items(): for j in i_list: j_list = darts[ j ] if ( not i in j_list ): bdry_v.add( i ) bdry_v.add( j ) return bdry_v ``` La méthode ci-dessous retourne une liste d'arcs (arêtes orientées) sur le bord du maillage. ```python ### return boundary edges def boundary_edges( self ): bdry_e= set() darts = {} faces = self.only_faces() for f in faces: for i in range( len(f) ): if ( f[i] in darts ): darts[ f[ i ] ].append( f[ (i+1)%len(f) ] ) else: darts[ f[ i ] ] = [ f[ (i+1)%len(f) ] ] for i, i_list in darts.items(): for j in i_list: j_list = darts[ j ] if ( not i in j_list ): bdry_e.add( (j,i) ) return bdry_e ``` La méthode ci-dessous fait la même chose mais retourne cette liste d'arcs sous la forme d'un tableau numpy à deux dimensions. Cela permet d'afficher directement ces arcs sous polyscope. ```python ### return boundary edges def numpy_boundary_edges( self ): edges = self.boundary_edges() np_edges = np.zeros( ( len(edges), 2 ), np.int32 ) k = 0; for e in edges: np_edges[k][0]=e[0] np_edges[k][1]=e[1] k = k+1 return np_edges ``` Enfin, la méthode ci-dessous reconstruit la liste ordonnée des sommets sur le bord (si il y a un seul bord). ```python def ordered_boundary( self ): edges = self.boundary_edges() if len(edges)==0: return [] d = {} first = -1 for e in edges: d[ e[0] ] = e[1] if first == -1: first = e[ 0 ] list_e = [first] cur = d[ first ] while cur != first: list_e.append( cur ) cur = d[ cur ] return list_e ``` Voilà ci-dessous un petit exemple d'utilisation. ```python= import polyscope as ps import numpy as np from wavefront import * name='bunnyhead' obj_name=name+'.obj' obj = load_obj( obj_name ) ps.init() ps_mesh = ps.register_surface_mesh(name, obj.only_coordinates(), obj.only_faces() ) bdry = obj.numpy_boundary_edges() ps_net= ps.register_curve_network("boundary", obj.only_coordinates(), bdry ) ps.show() ``` ![](https://codimd.math.cnrs.fr/uploads/upload_bdf8da1b6d69a92ddbaf02268c480356.jpg) Vous pouvez récupérer la liste ordonnée des sommets sur le bord via: ```python >>> L=obj.ordered_boundary() >>> L [566, 567, 547, 543, 540, 533, 529, 526, 519, 517, 515, 40, 816, 1412, 1411, 1433, 1442, 1449, 1379, 639, 57, 56, 278, 1480, 1465, 392, 119, 393, 506, 793, 227, 96, 771, 747, 309, 645, 1197, 67, 632, 613, 571, 537, 513, 483, 438, 427, 397, 386, 365, 349, 323, 1148, 1100, 1137, 1091, 1015, 1022, 1031, 1058, 1065, 1070, 1074, 1084, 322, 911, 656, 658, 1382, 249, 424, 83, 916, 250, 254, 1306, 685, 700, 718, 742, 743, 770, 792, 800, 229, 426, 1199, 436, 505, 1207, 1502, 1179, 457, 458, 1174, 446, 401, 511, 569] ```