import sys from typing import List from collections import defaultdict import numpy as np import pandas as pd sys_byteorder = ('>', '<')[sys.byteorder == 'little'] ply_dtypes = { 'int8': 'i1', b'char': 'i1', b'uint8': 'u1', b'uchar': 'b1', b'uchar': 'u1', 'int16': 'i2', b'short': 'i2', b'uint16': 'u2', b'ushort': 'u2', 'int32': 'i4', b'int': 'i4', b'uint32': 'u4', b'uint': 'u4', 'float32': 'f4', b'float': 'f4', b'float64': 'f8', b'double': 'f8' } valid_formats = {'ascii': '', 'binary_big_endian': '>', 'binary_little_endian': '<'} def describe_element(name: str, df: pd.DataFrame) -> List[str]: """ Takes the columns of the dataframe and builds a ply-like description :param name: :param df: """ property_formats = {'f': 'float', 'u': 'uchar', 'i': 'int'} 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 read_ply(filename): """ Read a .ply (binary or ascii) file and store the elements in pandas DataFrame Parameters ---------- filename: str Path to the filename Returns ------- data: dict Elements as pandas DataFrames; comments and ob_info as list of string """ if not type(filename) is str: filename = str(filename) with open(filename, 'rb') as ply: if b'ply' not in ply.readline(): raise ValueError('The file does not start whith 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 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 faces 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]])) count += 1 # for bin end_header = ply.tell() data = {} 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["faces"] = pd.read_csv(filename, sep=" ", header=None, engine="python", skiprows=top, usecols=usecols, names=names) for n, col in enumerate(data["faces"].columns): data["faces"][col] = data["faces"][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["faces"] = pd.DataFrame(mesh_np) data["faces"].drop('n_points', axis=1, inplace=True) return data def write_ply(filename, points=None, faces=None, as_text=True): """ Parameters ---------- filename: str The created file will be named with this points: ndarray faces: ndarray as_text: boolean Set the write mode of the file. Default: binary Returns ------- boolean True if no problems """ filename = str(filename) 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 points is not None: points = pd.DataFrame(points, columns=['x', 'y', 'z']) header.extend(describe_element('vertex', points)) if faces is not None: faces = pd.DataFrame(faces.copy()) faces.insert(loc=0, column="n_points", value=3) faces["n_points"] = faces["n_points"].astype("u1") header.extend(describe_element('face', faces)) 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 faces is not None: faces.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 faces is not None: faces.to_records(index=False).tofile(ply) return True