0

I made my own ply exporter and even though it is working, it is quite slow. I'm assuming that the problem comes from specifically looping over the elements and, based on this comment, from using struct.pack.

The challenge with exporting Nodes is that it is a 1D vector of tuples. The challenge with exporting Faces is that each line of ints is preceded by a number "3" in uchar format.

I put the entire code here since it serves as a MWE and also a reference if someone wants to use the code to export meshes in ply format.

Code:

from __future__ import division
import numpy as np
import struct
import timeit

def Timeme(funct,var,NN=10,NNN=10):
    for i in xrange(NN):
        start =timeit.default_timer()
        for t in xrange(NNN):
            funct(*var)
        end =timeit.default_timer()
        print str(i)+': '+str((end - start)/NNN*1000)  

# This function is fictitious. In reality the 
# Nodes array is imported from another module
def MakeNodes(Nr,Nc):
    Nodes=np.zeros(Nr*Nc,dtype=[('x', np.float32), ('y', np.float32), ('z', np.float32)])
    x = np.linspace(0, (Nc-1), Nc, dtype=np.float32)
    y = np.linspace((Nr-1),0 , Nr, dtype=np.float32)
    xv, yv = np.meshgrid(x, y, sparse=False, indexing='xy')
    Nodes['x']=xv.flatten()
    Nodes['y']=yv.flatten()
    Nodes['z']=(1/2-((Nodes['x']/Nc-1/2)**2+ (Nodes['y']/Nr-1/2)**2))*Nr/2
    return Nodes

# Function below explained in https://stackoverflow.com/questions/44934631
def MakeFaces(Nr,Nc):
    out = np.empty((Nr-1,Nc-1,2,3),dtype=int)
    r = np.arange(Nr*Nc).reshape(Nr,Nc)
    l1=r[:-1,:-1]
    l2=r[:-1,1:]
    l3=r[1:,:-1]
    l4=r[1:,1:]
    out[:,:, 0,0] = l2
    out[:,:, 0,1] = l1
    out[:,:, 0,2] = l3
    out[:,:, 1,0] = l4
    out[:,:, 1,1] = l2
    out[:,:, 1,2] = l3
    out.shape =(-1,3)
    return out

def ExportPlyBinary(Nodes,Faces,file):
    LN=len(Nodes)
    LF=len(Faces)

    header= \
    "ply\n" \
    "format binary_little_endian 1.0\n" \
    "element vertex "+str(LN)+"\n" \
    "property float x\n" \
    "property float y\n" \
    "property float z\n" \
    "element face "+str(LF)+"\n" \
    "property list uchar int vertex_indices\n" \
    "end_header\n"

    with open(file, 'wb') as fp:
        fp.write(header)
        s = struct.Struct('<fff')
        for nd in Nodes:
            fp.write(s.pack(nd['x'],nd['y'],nd['z']))

        s = struct.Struct('<Blll')
        for fc in Faces:
            fp.write(s.pack(3,fc[0],fc[1],fc[2]))

Nr=200
Nc=200
Nodes=MakeNodes(Nr,Nc)
Faces=MakeFaces(Nr,Nc)
Timeme(ExportPlyBinary,(Nodes,Faces,"Test.ply"))
Timeme(np.savez,("Test_np.ply", Nodes,Faces))

Result:

0: 366.352801235
1: 386.216017627
2: 383.307741944
3: 359.598214393
4: 363.434228045
5: 397.255473919
6: 433.967095136
7: 407.806616677
8: 393.701390596
9: 379.542319143
0: 15.5258007875
1: 13.2543344563
2: 12.8754439597
3: 24.2303215372
4: 15.9684973291
5: 14.2023306048
6: 13.7465456437
7: 13.6964054484
8: 21.27484093
9: 13.2139143373
Miguel
  • 1,293
  • 1
  • 13
  • 30

2 Answers2

1

Why not use what is available?

For example trimesh? https://pypi.python.org/pypi/trimesh

Even when you wan't only a small part of the package you can copy and adjust this part from their source code. (with mentioning the authors of course)

max9111
  • 6,272
  • 1
  • 16
  • 33
0

I checked Trimesh as suggested by @max9111 and was able to create a more efficient function. The key ideas (in my understanding) are (1) the conversion to the correct data type and order by creating a new array and (2) the use of the .tostring function. I initially avoided this direction as it seemed to waste memory but at this point the advantages are clear. Note that my Nodes array is ready to have .tostring applied to it, but I though I would keep the solution more general.

def NewExportPlyBinary(Nodes,Faces,file):
    LN=len(Nodes)
    LF=len(Faces)

    header= \
    "ply\n" \
    "format binary_little_endian 1.0\n" \
    "element vertex "+str(LN)+"\n" \
    "property float x\n" \
    "property float y\n" \
    "property float z\n" \
    "element face "+str(LF)+"\n" \
    "property list uchar int vertex_indices\n" \
    "end_header\n"

    dtype_vertex = [('vertex', '<f4', (3))]
    vertex = np.empty(LN, dtype=dtype_vertex)
    vertex['vertex']=np.stack((Nodes['x'],Nodes['y'],Nodes['z']),axis=-1)

    # vertex=Nodes

    dtype_face = [('count', '<u1'),('index', '<i4', (3))]
    faces = np.empty(LF, dtype=dtype_face)
    faces['count'] = 3
    faces['index'] = Faces

    with open(file, 'wb') as fp:
        fp.write(header)
        fp.write(vertex.tostring())
        fp.write(faces.tostring())

I now get the following times for the 200x200 example:

0: 373.361611377 # original ExportPlyBinary
0: 20.5686725792 # numpy's savez
0: 4.85469689001 # NewExportPlyBinary

Note: the difference between savez and NewExportPlyBinary basically vanishes when the problem size increases.

Miguel
  • 1,293
  • 1
  • 13
  • 30