mitsuba-visualize/io_util/ply.py

220 lines
7.4 KiB
Python
Raw Permalink Normal View History

2020-04-19 21:22:34 +08:00
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