# #CREDIT TO https://github.com/daavoo/pyntcloud/blob/master/pyntcloud/io/ply.py # #User daavoo # # HAKUNA MATATA import sys import numpy as np import pandas as pd from collections import defaultdict sys_byteorder = ('>', '<')[sys.byteorder == 'little'] ply_dtypes = dict([ (b'int8', 'i1'), (b'char', 'i1'), (b'uint8', 'u1'), (b'uchar', 'b1'), (b'uchar', 'u1'), (b'int16', 'i2'), (b'short', 'i2'), (b'uint16', 'u2'), (b'ushort', 'u2'), (b'int32', 'i4'), (b'int', 'i4'), (b'uint32', 'u4'), (b'uint', 'u4'), (b'float32', 'f4'), (b'float', 'f4'), (b'float64', 'f8'), (b'double', 'f8') ]) valid_formats = {'ascii': '', 'binary_big_endian': '>', 'binary_little_endian': '<'} def read_ply(filename, allow_bool=False): """ Read a .ply (binary or ascii) file and store the elements in pandas DataFrame. Parameters ---------- filename: str Path to the filename allow_bool: bool flag to allow bool as a valid PLY dtype. False by default to mirror original PLY specification. Returns ------- data: dict Elements as pandas DataFrames; comments and ob_info as list of string """ if allow_bool: ply_dtypes[b'bool'] = '?' with open(filename, 'rb') as ply: if b'ply' not in ply.readline(): raise ValueError('The file does not start with the word ply') # get binary_little/big or ascii fmt = ply.readline().split()[1].decode() # get extension for building the numpy dtypes ext = valid_formats[fmt] line = [] dtypes = defaultdict(list) count = 2 points_size = None mesh_size = None has_texture = False comments = [] while b'end_header' not in line and line != b'': line = ply.readline() if b'element' in line: line = line.split() name = line[1].decode() size = int(line[2]) if name == "vertex": points_size = size elif name == "face": mesh_size = size elif b'property' in line: line = line.split() # element mesh if b'list' in line: if b"vertex_indices" in line[-1] or b"vertex_index" in line[-1]: mesh_names = ["n_points", "v1", "v2", "v3"] else: has_texture = True mesh_names = ["n_coords"] + ["v1_u", "v1_v", "v2_u", "v2_v", "v3_u", "v3_v"] if fmt == "ascii": # the first number has different dtype than the list dtypes[name].append( (mesh_names[0], ply_dtypes[line[2]])) # rest of the numbers have the same dtype dt = ply_dtypes[line[3]] else: # the first number has different dtype than the list dtypes[name].append( (mesh_names[0], ext + ply_dtypes[line[2]])) # rest of the numbers have the same dtype dt = ext + ply_dtypes[line[3]] for j in range(1, len(mesh_names)): dtypes[name].append((mesh_names[j], dt)) else: if fmt == "ascii": dtypes[name].append( (line[2].decode(), ply_dtypes[line[1]])) else: dtypes[name].append( (line[2].decode(), ext + ply_dtypes[line[1]])) elif b'comment' in line: line = line.split(b" ", 1) comment = line[1].decode().rstrip() comments.append(comment) count += 1 # for bin end_header = ply.tell() data = {} if comments: data["comments"] = comments if fmt == 'ascii': top = count bottom = 0 if mesh_size is None else mesh_size names = [x[0] for x in dtypes["vertex"]] data["points"] = pd.read_csv(filename, sep=" ", header=None, engine="python", skiprows=top, skipfooter=bottom, usecols=names, names=names) for n, col in enumerate(data["points"].columns): data["points"][col] = data["points"][col].astype( dtypes["vertex"][n][1]) if mesh_size : top = count + points_size names = np.array([x[0] for x in dtypes["face"]]) usecols = [1, 2, 3, 5, 6, 7, 8, 9, 10] if has_texture else [1, 2, 3] names = names[usecols] data["mesh"] = pd.read_csv( filename, sep=" ", header=None, engine="python", skiprows=top, usecols=usecols, names=names) for n, col in enumerate(data["mesh"].columns): data["mesh"][col] = data["mesh"][col].astype( dtypes["face"][n + 1][1]) else: with open(filename, 'rb') as ply: ply.seek(end_header) points_np = np.fromfile(ply, dtype=dtypes["vertex"], count=points_size) if ext != sys_byteorder: points_np = points_np.byteswap().newbyteorder() data["points"] = pd.DataFrame(points_np) if mesh_size: mesh_np = np.fromfile(ply, dtype=dtypes["face"], count=mesh_size) if ext != sys_byteorder: mesh_np = mesh_np.byteswap().newbyteorder() data["mesh"] = pd.DataFrame(mesh_np) data["mesh"].drop('n_points', axis=1, inplace=True) return data def write_ply(filename, points=None, mesh=None, as_text=False, comments=None): """Write a PLY file populated with the given fields. Parameters ---------- filename: str The created file will be named with this points: ndarray mesh: ndarray as_text: boolean Set the write mode of the file. Default: binary comments: list of string Returns ------- boolean True if no problems """ if not filename.endswith('ply'): filename += '.ply' # open in text mode to write the header with open(filename, 'w') as ply: header = ['ply'] if as_text: header.append('format ascii 1.0') else: header.append('format binary_' + sys.byteorder + '_endian 1.0') if comments: for comment in comments: header.append('comment ' + comment) if points is not None: header.extend(describe_element('vertex', points)) if mesh is not None: mesh = mesh.copy() mesh.insert(loc=0, column="n_points", value=3) mesh["n_points"] = mesh["n_points"].astype("u1") header.extend(describe_element('face', mesh)) header.append('end_header') for line in header: ply.write("%s\n" % line) if as_text: if points is not None: points.to_csv(filename, sep=" ", index=False, header=False, mode='a', encoding='ascii') if mesh is not None: mesh.to_csv(filename, sep=" ", index=False, header=False, mode='a', encoding='ascii') else: with open(filename, 'ab') as ply: if points is not None: points.to_records(index=False).tofile(ply) if mesh is not None: mesh.to_records(index=False).tofile(ply) return True def describe_element(name, df): """ Takes the columns of the dataframe and builds a ply-like description Parameters ---------- name: str df: pandas DataFrame Returns ------- element: list[str] """ property_formats = {'f': 'float', 'u': 'uchar', 'i': 'int', 'b': 'bool'} element = ['element ' + name + ' ' + str(len(df))] if name == 'face': element.append("property list uchar int vertex_indices") else: for i in range(len(df.columns)): # get first letter of dtype to infer format f = property_formats[str(df.dtypes[i])[0]] element.append('property ' + f + ' ' + df.columns.values[i]) return element def write_ply_float(filename, points=None, mesh=None, as_text=False, comments=None): """Write a PLY file populated with the given fields. Parameters ---------- filename: str The created file will be named with this points: ndarray mesh: ndarray as_text: boolean Set the write mode of the file. Default: binary comments: list of string Returns ------- boolean True if no problems """ if not filename.endswith('ply'): filename += '.ply' # open in text mode to write the header with open(filename, 'w') as ply: header = ['ply'] #header.append('format ascii 1.0') header.append('format binary_' + sys.byteorder + '_endian 1.0') if comments: for comment in comments: header.append('comment ' + comment) if points is not None: header.extend(describe_element('vertex', points)) if mesh is not None: mesh = mesh.copy() mesh.insert(loc=0, column="n_points", value=3) mesh["n_points"] = mesh["n_points"].astype("u1") header.extend(describe_element('face', mesh)) header.append('end_header') for line in header: ply.write("%s\n" % line) with open(filename, 'ab') as ply: if points is not None: pointsNumPy = points.to_numpy() pointsNumPy.tofile(filename,format='