from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import struct
import compas
__all__ = [
'PLY',
'PLYReader',
'PLYParser',
'PLYWriter',
]
[docs]class PLY(object):
"""Polygon file format, or Stanford triangle format.
References
----------
.. [1] http://paulbourke.net/dataformats/ply/
"""
[docs] def __init__(self, filepath, precision=None):
self.filepath = filepath
self.precision = precision
self._is_parsed = False
self._reader = None
self._parser = None
self._writer = None
[docs] def read(self):
self._reader = PLYReader(self.filepath)
self._parser = PLYParser(self._reader, precision=self.precision)
self._is_parsed = True
[docs] def write(self, mesh, **kwargs):
self._writer = PLYWriter(self.filepath, mesh, **kwargs)
self._writer.write()
@property
def reader(self):
if not self._is_parsed:
self.read()
return self._reader
@property
def parser(self):
if not self._is_parsed:
self.read()
return self._parser
[docs]class PLYReader(object):
""""""
keywords = ['ply', 'format', 'comment', 'element', 'property', 'end_header']
property_types = {
'char': int,
'uchar': int,
'short': int,
'ushort': int,
'int': int,
'int32': int,
'int64': int,
'uint': int,
'uint32': int,
'uint64': int,
'float': float,
'float32': float,
'float64': float,
'double': float,
}
binary_property_types = {
'int8': 'i1',
'char': 'i1',
'uint8': 'u1',
'uchar': 'u1',
'int16': 'i2',
'short': 'i2',
'uint16': 'u2',
'ushort': 'u2',
'int32': 'i4',
'int': 'i4',
'uint32': 'u4',
'uint': 'u4',
'float32': 'f4',
'float': 'f4',
'float64': 'f8',
'double': 'f8'
}
number_of_bytes_per_type = {
'char': 1,
'uchar': 1,
'short': 2,
'ushort': 2,
'int': 4,
'uint': 4,
'float': 4,
'double': 8
}
struct_format_per_type = {
'char': 'c',
'uchar': 'B',
'short': 'h',
'ushort': 'H',
'int': 'i',
'uint': 'I',
'float': 'f',
'double': 'd'
}
binary_byte_order = {'binary_big_endian': '>', 'binary_little_endian': '<'}
[docs] def __init__(self, filepath):
self.filepath = filepath
self.file = None
self.format = None
self.comments = []
self.header = []
self.start_header = None
self.end_header = None
self.number_of_vertices = None
self.number_of_edges = None
self.number_of_faces = None
self.vertex_properties = []
self.edge_properties = []
self.face_properties = []
self.sections = []
self.vertices = []
self.edges = []
self.faces = []
self.read()
[docs] def is_valid(self):
self.read_header()
if self.start_header and self.end_header:
return True
return False
[docs] def is_binary(self):
if self.format == 'binary_big_endian':
return True
if self.format == 'binary_little_endian':
return True
return False
[docs] def is_ascii(self):
if self.format == 'ascii':
return True
return False
[docs] def read(self):
self.read_header()
if self.format == 'ascii':
self.read_data()
else:
self.read_data_binary()
# ==========================================================================
# read the header
# ==========================================================================
# ==========================================================================
# read the data
# ==========================================================================
[docs] def read_data(self):
if not self.end_header:
raise Exception('header has not been read, or the file is not valid')
with open(self.filepath) as self.file:
self.file.seek(self.end_header)
for section in self.sections:
if section == 'vertex':
self.read_vertices()
elif section == 'edge':
self.read_edges()
elif section == 'face':
self.read_faces()
else:
print('user-defined elements are not supported: {0}'.format(section))
pass
[docs] def read_data_binary(self):
if not self.end_header:
raise Exception('header has not been read, or the file is not valid')
with open(self.filepath, 'rb') as self.file:
self.file.seek(self.end_header)
for section in self.sections:
if section == 'vertex':
self.read_vertices_binary_wo_numpy()
elif section == 'edge':
self.read_edges_binary_wo_numpy()
elif section == 'face':
self.read_faces_binary_wo_numpy()
else:
print('user-defined elements are not supported: {0}'.format(section))
pass
# ==========================================================================
# read the individual section
# ==========================================================================
[docs] def read_vertices(self):
n = len(self.vertex_properties)
for _ in range(self.number_of_vertices):
vertex = {}
i = 0
while i < n:
line = next(self.file)
parts = line.rstrip().split()
for prop_str in parts:
prop_name, prop_type = self.vertex_properties[i]
vertex[prop_name] = self.property_types[prop_type](prop_str)
i += 1
self.vertices.append(vertex)
# count = 0
# for line in self.file:
# line = line.rstrip()
# parts = line.split()
# vertex = {}
# for i, prop in enumerate(self.vertex_properties):
# pname, ptype = prop
# vertex[pname] = self.property_types[ptype](parts[i])
# self.vertices.append(vertex)
# count += 1
# if count == self.number_of_vertices:
# break
[docs] def read_edges(self):
pass
[docs] def read_faces(self):
count = 0
for line in self.file:
line = line.rstrip()
parts = line.split()
face = {}
for i, prop in enumerate(self.face_properties):
pname, ptype, plen = prop
face[pname] = [self.property_types[ptype](part) for part in parts[1:]]
self.faces.append(face)
count += 1
if count == self.number_of_faces:
break
# ==========================================================================
# binary read the individual section
# ==========================================================================
# remove numpy dependency by reading the file in chuncks
# with each chunck equal to the size specified in the header?
# see: http://stackoverflow.com/questions/4566498/python-file-iterator-over-a-binary-file-with-newer-idiom
# see: http://stackoverflow.com/questions/27532738/python-iterate-through-binary-file-without-lines
[docs] def numpy_vertex_ptypes(self):
ext = self.binary_byte_order[self.format]
dt = []
for prop in self.vertex_properties:
pname, ptype = prop
dt.append((pname, ext + self.binary_property_types[ptype]))
return dt
[docs] def numpy_face_ptypes(self):
ext = self.binary_byte_order[self.format]
dt = []
for prop in self.face_properties:
if len(prop) == 2:
pname, ptype = prop
dt.append((pname, ext + self.binary_property_types[ptype]))
elif len(prop) == 3:
pname, ptype, plen = prop
dt.append(('size', ext + self.binary_property_types[plen]))
# this seems a nit of a hack
dt.append(('v1', ext + self.binary_property_types[ptype]))
dt.append(('v2', ext + self.binary_property_types[ptype]))
dt.append(('v3', ext + self.binary_property_types[ptype]))
else:
pass
return dt
[docs] def read_vertices_binary_wo_numpy(self):
ext = self.binary_byte_order[self.format]
fmt = ext
chunk = 0
for prop in self.vertex_properties:
pname, ptype = prop
chunk += self.number_of_bytes_per_type[ptype]
fmt += self.struct_format_per_type[ptype]
for i in range(self.number_of_vertices):
data = self.file.read(chunk)
data = struct.unpack(fmt, data)
vertex = {}
for i, prop in enumerate(self.vertex_properties):
pname, ptype = prop
vertex[pname] = data[i]
self.vertices.append(vertex)
[docs] def read_vertices_binary(self):
# use pandas to read the data frames
import numpy as np
for line in np.fromfile(self.file, dtype=np.dtype(self.numpy_vertex_ptypes()), count=self.number_of_vertices):
vertex = {}
for i, prop in enumerate(self.vertex_properties):
pname, ptype = prop
vertex[pname] = line[i]
self.vertices.append(vertex)
[docs] def read_edges_binary_wo_numpy(self):
pass
[docs] def read_edges_binary(self):
pass
[docs] def read_faces_binary_wo_numpy(self):
ext = self.binary_byte_order[self.format]
fmt = ext
chunk = 0
for prop in self.face_properties:
if len(prop) == 2:
pname, ptype = prop
chunk += self.number_of_bytes_per_type[ptype]
fmt += self.struct_format_per_type[ptype]
elif len(prop) == 3:
pname, ptype, plen = prop
chunk += self.number_of_bytes_per_type[plen]
chunk += self.number_of_bytes_per_type[ptype] * 3
fmt += self.struct_format_per_type[plen]
fmt += self.struct_format_per_type[ptype] * 3
else:
pass
for i in range(self.number_of_faces):
data = self.file.read(chunk)
data = struct.unpack(fmt, data)
face = {}
for i, prop in enumerate(self.face_properties):
if len(prop) == 2:
pname, ptype = prop
face[pname] = data[i]
elif len(prop) == 3:
pname, ptype, plen = prop
face[pname] = list(data[2:])
self.faces.append(face)
[docs] def read_faces_binary(self):
# use pandas to read the data frames
# how to deal with faces of variable length?
import numpy as np
for line in np.fromfile(self.file, dtype=np.dtype(self.numpy_face_ptypes()), count=self.number_of_faces):
face = {}
for i, prop in enumerate(self.face_properties):
if len(prop) == 2:
pname, ptype = prop
face[pname] = line[i]
elif len(prop) == 3:
pname, ptype, plen = prop
# type(line) => numpy.void
# convert the line to a list
line = list(line)
face[pname] = line[2:]
else:
pass
self.faces.append(face)
[docs]class PLYParser(object):
""""""
[docs] def __init__(self, reader, precision=None):
self.precision = precision
self.reader = reader
self.vertices = None
self.edges = None
self.faces = None
self.parse()
[docs] def parse(self):
self.vertices = [(vertex['x'], vertex['y'], vertex['z']) for vertex in self.reader.vertices]
self.faces = [face['vertex_indices'] for face in self.reader.faces]
[docs]class PLYWriter(object):
""""""
[docs] def __init__(self, filepath, mesh, author=None, email=None, date=None, precision=None):
self.filepath = filepath
self.mesh = mesh
self.author = author
self.email = email
self.date = date
self.precision = precision or compas.PRECISION
self.vertex_tpl = "{0:." + self.precision + "}" + " {1:." + self.precision + "}" + " {2:." + self.precision + "}\n"
self.v = mesh.number_of_vertices()
self.f = mesh.number_of_faces()
self.e = mesh.number_of_edges()
self.file = None
[docs] def write(self):
with open(self.filepath, 'w') as self.file:
self.write_header()
self.write_vertices()
self.write_faces()
[docs] def write_vertices(self):
for key in self.mesh.vertices():
x, y, z = self.mesh.vertex_coordinates(key)
self.file.write(self.vertex_tpl.format(x, y, z))
[docs] def write_faces(self):
key_index = self.mesh.key_index()
for fkey in self.mesh.faces():
vertices = self.mesh.face_vertices(fkey)
v = len(vertices)
self.file.write("{0} {1}\n".format(v, " ".join([str(key_index[key]) for key in vertices])))
# ==============================================================================
# Main
# ==============================================================================
if __name__ == "__main__":
import os
from compas.datastructures import Mesh
FILE = os.path.join(compas.DATA, 'tubemesh.ply')
mesh = Mesh.from_json(compas.get('tubemesh.json'))
mesh.to_ply(FILE, author="Tom Van Mele")
ply = PLY(FILE)
print(len(ply.reader.vertices) == ply.reader.number_of_vertices)
print(len(ply.reader.faces) == ply.reader.number_of_faces)
print(len(ply.reader.faces))
print(ply.reader.number_of_faces)