Module simple_3dviz.io.mesh
Load a mesh into three arrays vertices, faces and vertex_colors.
Expand source code
"""Load a mesh into three arrays vertices, faces and vertex_colors."""
import numpy as np
import os
from plyfile import PlyData
from ._utils import get_file, close_file
class MeshReader(object):
"""MeshReader defines the common interface for reading all supported mesh
files."""
def __init__(self, filename=None):
self._vertices = None
self._normals = None
self._colors = None
self._uv = None
self._material_file = None
if filename is not None:
self.read(filename)
def read(self, filename):
raise NotImplementedError()
@property
def vertices(self):
if self._vertices is None:
raise NotImplementedError()
return self._vertices
@property
def normals(self):
if self._normals is None:
raise NotImplementedError()
return self._normals
@property
def colors(self):
if self._colors is None:
raise NotImplementedError()
return self._colors
@property
def uv(self):
if self._uv is None:
raise NotImplementedError()
return self._uv
@property
def material_file(self):
if self._material_file is None:
raise NotImplementedError()
return self._material_file
class PlyMeshReader(MeshReader):
"""Read binary or ascii PLY files."""
def __init__(self, filename=None, vertex="vertex", face="face",
vertex_indices=None):
self._names = {
"vertex": vertex,
"face": face,
"vertex_indices": vertex_indices
}
super(PlyMeshReader, self).__init__(filename)
def _get_colors(self, p):
colors = np.zeros((p.count, 4), dtype=np.float32)
colors[:, 0] = p["red"]
colors[:, 1] = p["green"]
colors[:, 2] = p["blue"]
colors[:, 3] = 255
try:
colors[:, 3] = p["alpha"]
except ValueError:
pass
colors /= 255
return colors
def read(self, filename):
# Make local copies of the names of the ply elements
V = self._names["vertex"]
F = self._names["face"]
VI = self._names["vertex_indices"]
# Read the file into a PlyData datastucture
data = PlyData.read(filename)
# Get the vertices
if V in data:
self._vertices = np.vstack([
data[V]["x"],
data[V]["y"],
data[V]["z"]
]).T
# If we have per vertex colors get them as well
per_vertex_colors = None
try:
per_vertex_colors = self._get_colors(data[V])
except ValueError:
pass
if F in data:
if VI is None:
VI = data[F].properties[0].name
triangles = sum(
len(indices)-2
for indices in data[F][VI]
)
if triangles == len(data[F][VI]):
faces = np.vstack(data[F][VI])
else:
raise NotImplementedError(("Dealing with non-triangulated "
"faces is not supported"))
self._vertices = self._vertices[faces].reshape(-1, 3)
if per_vertex_colors is not None:
self._colors = per_vertex_colors[faces].reshape(-1, 4)
else:
try:
colors = self._get_colors(data[F])
self._colors = np.repeat(colors, 3, axis=0)
except ValueError:
pass
class ObjMeshReader(MeshReader):
"""Read OBJ mesh files."""
def _triangulate_faces(self, faces):
triangles = []
for f in faces:
if len(f) == 3:
triangles.append(f)
else:
for i in range(2, len(f)):
triangles.append([f[0], f[i-1], f[i]])
return triangles
def read(self, filename):
def extract_vertex(face):
return int(face.split("/")[0])-1
def extract_normal(face):
return int(face.split("/")[2])-1
def extract_uv(face):
return int(face.split("/")[1])-1
try:
f = get_file(filename)
lines = f.readlines()
# Collect all the vertices, namely lines starting with 'v' followed
# by 3 floats and arrange them according to faces
vertices = np.array([
list(map(float, l.strip().split()[1:4]))
for l in lines if l.startswith("v ")
], dtype=np.float32)
faces = np.array(self._triangulate_faces([
list(map(extract_vertex, l.strip().split()[1:]))
for l in lines if l.startswith("f ")
]))
self._vertices = vertices[faces].reshape(-1, 3)
# Collect all the vertex normals, namely lines starting with 'vn'
# followed by 3 floats and arrange the according to faces
try:
normals = np.array([
list(map(float, l.strip().split()[1:]))
for l in lines if l.startswith("vn ")
])
faces = np.array(self._triangulate_faces([
list(map(extract_normal, l.strip().split()[1:]))
for l in lines if l.startswith("f ")
]))
self._normals = normals[faces].reshape(-1, 3)
except IndexError:
pass
# Collect all the texture coordinates, namely u, v [,w] coordinates
try:
uv = np.array([
list(map(float, l.strip().split()[1:]))
for l in lines if l.startswith("vt ")
])
faces = np.array(self._triangulate_faces([
list(map(extract_uv, l.strip().split()[1:]))
for l in lines if l.startswith("f ")
]))
self._uv = uv[faces].reshape(-1, uv.shape[1])[:, :2]
except IndexError:
pass
# Collect a material file
try:
material_file = [
l.strip().split()[1:][0]
for l in lines if l.startswith("mtllib")
][0]
if isinstance(filename, str):
self._material_file = os.path.join(
os.path.dirname(filename),
material_file
)
else:
if hasattr(filename, "name"):
self._material_file = os.path.join(
os.path.dirname(filename.name),
material_file
)
else:
pass
except IndexError:
pass
finally:
close_file(filename, f)
class OffMeshReader(MeshReader):
"""Read OFF mesh files."""
def read(self, filename):
try:
f = get_file(filename)
# Read lines and clean them from comments and empty lines
lines = f.readlines()
lines = [l.strip() for l in lines if l[0]!="#" and l.strip() != ""]
# Ensure it is an OFF file
if not lines[0].startswith("OFF"):
raise ValueError("Invalid OFF file.")
# Extract the number of vertices, faces and edges
if len(lines[0].split()) > 1:
n_vertices, n_faces, n_edges = [
int(x)
for x in lines[0].split()[1:]
]
lines = lines[1:]
else:
n_vertices, n_faces, n_edges = [
int(x)
for x in lines[1].split()
]
lines = lines[2:]
# Collect the vertices and faces
vertices = np.array([
[float(x) for x in l.split()]
for l in lines[:n_vertices]
], dtype=np.float32)
faces = np.array([
[float(x) for x in l.split()]
for l in lines[n_vertices:n_vertices+n_faces]
], dtype=np.float32)
if not np.all(faces[:, 0] == 3):
raise NotImplementedError(("Dealing with non-triangulated "
"faces is not supported"))
# Set the triangles
vertex_indices = faces[:, 1:4].astype(int).ravel()
self._vertices = vertices[:, :3][vertex_indices]
# Set the colors
if vertices.shape[1] > 3:
self._colors = vertices[:, 4:][vertex_indices]
elif faces.shape[1] > 4:
self._colors = np.repeat(faces[:, 4:], 3, axis=0)
finally:
close_file(filename, f)
class StlMeshReader(MeshReader):
"""Read STL mesh files."""
def read(self, filename):
# Decide if it is an ASCII STL or not
ascii_stl = True
try:
f = get_file(filename, "rb")
try:
past_header = str(f.read(100), encoding="ascii")
except UnicodeDecodeError:
ascii_stl = False
f.seek(0)
if ascii_stl:
vertices = []
normals = []
START_SOLID = 1
START_FACE = 2
START_VERTEX = 3
END_VERTEX = 4
END_FACE = 5
END_SOLID = 6
normal = None
state = START_SOLID
for line in f:
line = str(line, encoding="ascii")
fields = line.strip().split()
if state == START_SOLID:
if fields[0] != "solid":
raise ValueError("Non ASCII STL")
state = START_FACE
elif state == START_FACE:
if fields[0] == "endsolid":
state = START_SOLID
continue
else:
assert fields[0] == "facet" and fields[1] == "normal"
normal = [float(x) for x in fields[2:5]]
state = START_VERTEX
elif state == START_VERTEX:
if fields[0] == "outer" and fields[1] == "loop":
continue
elif fields[0] == "vertex":
v = [float(x) for x in fields[1:4]]
vertices.append(v)
normals.append(normal)
elif fields[0] == "endloop":
state = END_FACE
elif state == END_FACE:
if fields[0] == "outer" and fields[1] == "loop":
state = START_VERTEX
else:
assert fields[0] == "endfacet"
state = START_FACE
self._vertices = np.array(vertices)
self._normals = np.array(normals)
else:
header = f.read(80)
assert len(header) == 80
triangles = np.frombuffer(f.read(4), "<i4", 1)[0]
dtype = np.dtype([
("normal", "<f4", (3,)),
("vertices", "<f4", (3, 3)),
("attr", "<u2")
])
mesh = np.frombuffer(f.read(), dtype, triangles)
self._vertices = mesh["vertices"].reshape(-1, 3)
self._normals = np.repeat(mesh["normal"], 3, 0).reshape(-1, 3)
finally:
close_file(filename, f)
Classes
class MeshReader (filename=None)
-
MeshReader defines the common interface for reading all supported mesh files.
Expand source code
class MeshReader(object): """MeshReader defines the common interface for reading all supported mesh files.""" def __init__(self, filename=None): self._vertices = None self._normals = None self._colors = None self._uv = None self._material_file = None if filename is not None: self.read(filename) def read(self, filename): raise NotImplementedError() @property def vertices(self): if self._vertices is None: raise NotImplementedError() return self._vertices @property def normals(self): if self._normals is None: raise NotImplementedError() return self._normals @property def colors(self): if self._colors is None: raise NotImplementedError() return self._colors @property def uv(self): if self._uv is None: raise NotImplementedError() return self._uv @property def material_file(self): if self._material_file is None: raise NotImplementedError() return self._material_file
Subclasses
Instance variables
var colors
-
Expand source code
@property def colors(self): if self._colors is None: raise NotImplementedError() return self._colors
var material_file
-
Expand source code
@property def material_file(self): if self._material_file is None: raise NotImplementedError() return self._material_file
var normals
-
Expand source code
@property def normals(self): if self._normals is None: raise NotImplementedError() return self._normals
var uv
-
Expand source code
@property def uv(self): if self._uv is None: raise NotImplementedError() return self._uv
var vertices
-
Expand source code
@property def vertices(self): if self._vertices is None: raise NotImplementedError() return self._vertices
Methods
def read(self, filename)
-
Expand source code
def read(self, filename): raise NotImplementedError()
class ObjMeshReader (filename=None)
-
Read OBJ mesh files.
Expand source code
class ObjMeshReader(MeshReader): """Read OBJ mesh files.""" def _triangulate_faces(self, faces): triangles = [] for f in faces: if len(f) == 3: triangles.append(f) else: for i in range(2, len(f)): triangles.append([f[0], f[i-1], f[i]]) return triangles def read(self, filename): def extract_vertex(face): return int(face.split("/")[0])-1 def extract_normal(face): return int(face.split("/")[2])-1 def extract_uv(face): return int(face.split("/")[1])-1 try: f = get_file(filename) lines = f.readlines() # Collect all the vertices, namely lines starting with 'v' followed # by 3 floats and arrange them according to faces vertices = np.array([ list(map(float, l.strip().split()[1:4])) for l in lines if l.startswith("v ") ], dtype=np.float32) faces = np.array(self._triangulate_faces([ list(map(extract_vertex, l.strip().split()[1:])) for l in lines if l.startswith("f ") ])) self._vertices = vertices[faces].reshape(-1, 3) # Collect all the vertex normals, namely lines starting with 'vn' # followed by 3 floats and arrange the according to faces try: normals = np.array([ list(map(float, l.strip().split()[1:])) for l in lines if l.startswith("vn ") ]) faces = np.array(self._triangulate_faces([ list(map(extract_normal, l.strip().split()[1:])) for l in lines if l.startswith("f ") ])) self._normals = normals[faces].reshape(-1, 3) except IndexError: pass # Collect all the texture coordinates, namely u, v [,w] coordinates try: uv = np.array([ list(map(float, l.strip().split()[1:])) for l in lines if l.startswith("vt ") ]) faces = np.array(self._triangulate_faces([ list(map(extract_uv, l.strip().split()[1:])) for l in lines if l.startswith("f ") ])) self._uv = uv[faces].reshape(-1, uv.shape[1])[:, :2] except IndexError: pass # Collect a material file try: material_file = [ l.strip().split()[1:][0] for l in lines if l.startswith("mtllib") ][0] if isinstance(filename, str): self._material_file = os.path.join( os.path.dirname(filename), material_file ) else: if hasattr(filename, "name"): self._material_file = os.path.join( os.path.dirname(filename.name), material_file ) else: pass except IndexError: pass finally: close_file(filename, f)
Ancestors
Methods
def read(self, filename)
-
Expand source code
def read(self, filename): def extract_vertex(face): return int(face.split("/")[0])-1 def extract_normal(face): return int(face.split("/")[2])-1 def extract_uv(face): return int(face.split("/")[1])-1 try: f = get_file(filename) lines = f.readlines() # Collect all the vertices, namely lines starting with 'v' followed # by 3 floats and arrange them according to faces vertices = np.array([ list(map(float, l.strip().split()[1:4])) for l in lines if l.startswith("v ") ], dtype=np.float32) faces = np.array(self._triangulate_faces([ list(map(extract_vertex, l.strip().split()[1:])) for l in lines if l.startswith("f ") ])) self._vertices = vertices[faces].reshape(-1, 3) # Collect all the vertex normals, namely lines starting with 'vn' # followed by 3 floats and arrange the according to faces try: normals = np.array([ list(map(float, l.strip().split()[1:])) for l in lines if l.startswith("vn ") ]) faces = np.array(self._triangulate_faces([ list(map(extract_normal, l.strip().split()[1:])) for l in lines if l.startswith("f ") ])) self._normals = normals[faces].reshape(-1, 3) except IndexError: pass # Collect all the texture coordinates, namely u, v [,w] coordinates try: uv = np.array([ list(map(float, l.strip().split()[1:])) for l in lines if l.startswith("vt ") ]) faces = np.array(self._triangulate_faces([ list(map(extract_uv, l.strip().split()[1:])) for l in lines if l.startswith("f ") ])) self._uv = uv[faces].reshape(-1, uv.shape[1])[:, :2] except IndexError: pass # Collect a material file try: material_file = [ l.strip().split()[1:][0] for l in lines if l.startswith("mtllib") ][0] if isinstance(filename, str): self._material_file = os.path.join( os.path.dirname(filename), material_file ) else: if hasattr(filename, "name"): self._material_file = os.path.join( os.path.dirname(filename.name), material_file ) else: pass except IndexError: pass finally: close_file(filename, f)
class OffMeshReader (filename=None)
-
Read OFF mesh files.
Expand source code
class OffMeshReader(MeshReader): """Read OFF mesh files.""" def read(self, filename): try: f = get_file(filename) # Read lines and clean them from comments and empty lines lines = f.readlines() lines = [l.strip() for l in lines if l[0]!="#" and l.strip() != ""] # Ensure it is an OFF file if not lines[0].startswith("OFF"): raise ValueError("Invalid OFF file.") # Extract the number of vertices, faces and edges if len(lines[0].split()) > 1: n_vertices, n_faces, n_edges = [ int(x) for x in lines[0].split()[1:] ] lines = lines[1:] else: n_vertices, n_faces, n_edges = [ int(x) for x in lines[1].split() ] lines = lines[2:] # Collect the vertices and faces vertices = np.array([ [float(x) for x in l.split()] for l in lines[:n_vertices] ], dtype=np.float32) faces = np.array([ [float(x) for x in l.split()] for l in lines[n_vertices:n_vertices+n_faces] ], dtype=np.float32) if not np.all(faces[:, 0] == 3): raise NotImplementedError(("Dealing with non-triangulated " "faces is not supported")) # Set the triangles vertex_indices = faces[:, 1:4].astype(int).ravel() self._vertices = vertices[:, :3][vertex_indices] # Set the colors if vertices.shape[1] > 3: self._colors = vertices[:, 4:][vertex_indices] elif faces.shape[1] > 4: self._colors = np.repeat(faces[:, 4:], 3, axis=0) finally: close_file(filename, f)
Ancestors
Methods
def read(self, filename)
-
Expand source code
def read(self, filename): try: f = get_file(filename) # Read lines and clean them from comments and empty lines lines = f.readlines() lines = [l.strip() for l in lines if l[0]!="#" and l.strip() != ""] # Ensure it is an OFF file if not lines[0].startswith("OFF"): raise ValueError("Invalid OFF file.") # Extract the number of vertices, faces and edges if len(lines[0].split()) > 1: n_vertices, n_faces, n_edges = [ int(x) for x in lines[0].split()[1:] ] lines = lines[1:] else: n_vertices, n_faces, n_edges = [ int(x) for x in lines[1].split() ] lines = lines[2:] # Collect the vertices and faces vertices = np.array([ [float(x) for x in l.split()] for l in lines[:n_vertices] ], dtype=np.float32) faces = np.array([ [float(x) for x in l.split()] for l in lines[n_vertices:n_vertices+n_faces] ], dtype=np.float32) if not np.all(faces[:, 0] == 3): raise NotImplementedError(("Dealing with non-triangulated " "faces is not supported")) # Set the triangles vertex_indices = faces[:, 1:4].astype(int).ravel() self._vertices = vertices[:, :3][vertex_indices] # Set the colors if vertices.shape[1] > 3: self._colors = vertices[:, 4:][vertex_indices] elif faces.shape[1] > 4: self._colors = np.repeat(faces[:, 4:], 3, axis=0) finally: close_file(filename, f)
class PlyMeshReader (filename=None, vertex='vertex', face='face', vertex_indices=None)
-
Read binary or ascii PLY files.
Expand source code
class PlyMeshReader(MeshReader): """Read binary or ascii PLY files.""" def __init__(self, filename=None, vertex="vertex", face="face", vertex_indices=None): self._names = { "vertex": vertex, "face": face, "vertex_indices": vertex_indices } super(PlyMeshReader, self).__init__(filename) def _get_colors(self, p): colors = np.zeros((p.count, 4), dtype=np.float32) colors[:, 0] = p["red"] colors[:, 1] = p["green"] colors[:, 2] = p["blue"] colors[:, 3] = 255 try: colors[:, 3] = p["alpha"] except ValueError: pass colors /= 255 return colors def read(self, filename): # Make local copies of the names of the ply elements V = self._names["vertex"] F = self._names["face"] VI = self._names["vertex_indices"] # Read the file into a PlyData datastucture data = PlyData.read(filename) # Get the vertices if V in data: self._vertices = np.vstack([ data[V]["x"], data[V]["y"], data[V]["z"] ]).T # If we have per vertex colors get them as well per_vertex_colors = None try: per_vertex_colors = self._get_colors(data[V]) except ValueError: pass if F in data: if VI is None: VI = data[F].properties[0].name triangles = sum( len(indices)-2 for indices in data[F][VI] ) if triangles == len(data[F][VI]): faces = np.vstack(data[F][VI]) else: raise NotImplementedError(("Dealing with non-triangulated " "faces is not supported")) self._vertices = self._vertices[faces].reshape(-1, 3) if per_vertex_colors is not None: self._colors = per_vertex_colors[faces].reshape(-1, 4) else: try: colors = self._get_colors(data[F]) self._colors = np.repeat(colors, 3, axis=0) except ValueError: pass
Ancestors
Methods
def read(self, filename)
-
Expand source code
def read(self, filename): # Make local copies of the names of the ply elements V = self._names["vertex"] F = self._names["face"] VI = self._names["vertex_indices"] # Read the file into a PlyData datastucture data = PlyData.read(filename) # Get the vertices if V in data: self._vertices = np.vstack([ data[V]["x"], data[V]["y"], data[V]["z"] ]).T # If we have per vertex colors get them as well per_vertex_colors = None try: per_vertex_colors = self._get_colors(data[V]) except ValueError: pass if F in data: if VI is None: VI = data[F].properties[0].name triangles = sum( len(indices)-2 for indices in data[F][VI] ) if triangles == len(data[F][VI]): faces = np.vstack(data[F][VI]) else: raise NotImplementedError(("Dealing with non-triangulated " "faces is not supported")) self._vertices = self._vertices[faces].reshape(-1, 3) if per_vertex_colors is not None: self._colors = per_vertex_colors[faces].reshape(-1, 4) else: try: colors = self._get_colors(data[F]) self._colors = np.repeat(colors, 3, axis=0) except ValueError: pass
class StlMeshReader (filename=None)
-
Read STL mesh files.
Expand source code
class StlMeshReader(MeshReader): """Read STL mesh files.""" def read(self, filename): # Decide if it is an ASCII STL or not ascii_stl = True try: f = get_file(filename, "rb") try: past_header = str(f.read(100), encoding="ascii") except UnicodeDecodeError: ascii_stl = False f.seek(0) if ascii_stl: vertices = [] normals = [] START_SOLID = 1 START_FACE = 2 START_VERTEX = 3 END_VERTEX = 4 END_FACE = 5 END_SOLID = 6 normal = None state = START_SOLID for line in f: line = str(line, encoding="ascii") fields = line.strip().split() if state == START_SOLID: if fields[0] != "solid": raise ValueError("Non ASCII STL") state = START_FACE elif state == START_FACE: if fields[0] == "endsolid": state = START_SOLID continue else: assert fields[0] == "facet" and fields[1] == "normal" normal = [float(x) for x in fields[2:5]] state = START_VERTEX elif state == START_VERTEX: if fields[0] == "outer" and fields[1] == "loop": continue elif fields[0] == "vertex": v = [float(x) for x in fields[1:4]] vertices.append(v) normals.append(normal) elif fields[0] == "endloop": state = END_FACE elif state == END_FACE: if fields[0] == "outer" and fields[1] == "loop": state = START_VERTEX else: assert fields[0] == "endfacet" state = START_FACE self._vertices = np.array(vertices) self._normals = np.array(normals) else: header = f.read(80) assert len(header) == 80 triangles = np.frombuffer(f.read(4), "<i4", 1)[0] dtype = np.dtype([ ("normal", "<f4", (3,)), ("vertices", "<f4", (3, 3)), ("attr", "<u2") ]) mesh = np.frombuffer(f.read(), dtype, triangles) self._vertices = mesh["vertices"].reshape(-1, 3) self._normals = np.repeat(mesh["normal"], 3, 0).reshape(-1, 3) finally: close_file(filename, f)
Ancestors
Methods
def read(self, filename)
-
Expand source code
def read(self, filename): # Decide if it is an ASCII STL or not ascii_stl = True try: f = get_file(filename, "rb") try: past_header = str(f.read(100), encoding="ascii") except UnicodeDecodeError: ascii_stl = False f.seek(0) if ascii_stl: vertices = [] normals = [] START_SOLID = 1 START_FACE = 2 START_VERTEX = 3 END_VERTEX = 4 END_FACE = 5 END_SOLID = 6 normal = None state = START_SOLID for line in f: line = str(line, encoding="ascii") fields = line.strip().split() if state == START_SOLID: if fields[0] != "solid": raise ValueError("Non ASCII STL") state = START_FACE elif state == START_FACE: if fields[0] == "endsolid": state = START_SOLID continue else: assert fields[0] == "facet" and fields[1] == "normal" normal = [float(x) for x in fields[2:5]] state = START_VERTEX elif state == START_VERTEX: if fields[0] == "outer" and fields[1] == "loop": continue elif fields[0] == "vertex": v = [float(x) for x in fields[1:4]] vertices.append(v) normals.append(normal) elif fields[0] == "endloop": state = END_FACE elif state == END_FACE: if fields[0] == "outer" and fields[1] == "loop": state = START_VERTEX else: assert fields[0] == "endfacet" state = START_FACE self._vertices = np.array(vertices) self._normals = np.array(normals) else: header = f.read(80) assert len(header) == 80 triangles = np.frombuffer(f.read(4), "<i4", 1)[0] dtype = np.dtype([ ("normal", "<f4", (3,)), ("vertices", "<f4", (3, 3)), ("attr", "<u2") ]) mesh = np.frombuffer(f.read(), dtype, triangles) self._vertices = mesh["vertices"].reshape(-1, 3) self._normals = np.repeat(mesh["normal"], 3, 0).reshape(-1, 3) finally: close_file(filename, f)