#!/usr/bin/env python
import numpy as np
try: from . import vector
except: import vector #Allows it to be run from package dir.
try: from . import simple_cl
except: import simple_cl #Allows it to be run from package dir.
import re
import os
from StringIO import StringIO
import warnings
class MeshWarning(Warning):
pass
class ShapeError(Exception):
pass
class ToDoError(Exception):
pass
def shape_str(shape):
return '(' + ', '.join(map(lambda d: '-any-' if d < 0 else str(d), shape)) + ')'
def enforce_shape(arr, shape):
arr = np.asarray(arr)
if arr.shape == shape: #Shortcut
return arr
if arr.ndim > len(shape):
raise ShapeError('input has two many dimensions (%s) to cast into shape %s' % (arr.ndim, shape_str(shape)))
elif arr.ndim < len(shape):
arr.shape = (1, ) * (len(shape) - arr.ndim) + arr.shape
new_shape = tuple(so if st<=0 else st for so, st in zip(arr.shape, shape))
if arr.shape == new_shape: #Shortcut
return arr
else:
tile = []
for so, st in zip(arr.shape, new_shape):
if so == st: tile.append(1)
elif so == 1: tile.append(st)
else: raise ShapeError('input not castable into array of shape %s' % shape_str(shape))
return np.tile(arr, tile)
STL_DTYPE = [('n', '3f'), ('c1', '3f'), ('c2', '3f'), ('c3', '3f'), ('byte_count', 'u2')]
def colors_astype(c, dtype):
new_dtype = np.dtype(dtype)
if c.dtype == np.dtype('u1') and new_dtype in ('f', 'd'):
return c.astype(new_dtype) / 255.
elif c.dtype in ('f', 'd') and new_dtype == np.dtype('u1'):
return (np.clip(c, 0, 1) * 255).astype(new_dtype)
else:
return c.astype(new_dtype)
CL = simple_cl.CLSession(device='cpu', use_doubles=False)
CODE = os.path.join(os.path.split(__file__)[0], 'cl_src', 'mesh.cl')
def _check_code(cl=CL):
if 'mesh_dummy_kernel' not in getattr(cl, 'kernels', {}):
cl.compile_file(CODE)
[docs]def set_opencl_params(use_doubles=None, device=None, context=None):
'''Set the parameters of the OpenCL Session used for computations.
See ``simple_cl`` documentation for details.
*Note:* this function must be called *before* any functions that use it,
or an error will be raised.
Parameters
----------
use_doubles : bool, optional (default: False)
device : string or list, optional (default: 'cpu')
context : pyopencl Context or CLSession object, optional
If specified, causes CLSession to share a context.
'''
if use_doubles is not None: CL.use_doubles = use_doubles
if device is not None: CL.device = device
if context is not None: CL.initialize(context=context)
[docs]class Mesh(object):
'''3D triangular mesh object.
Parameters
----------
points : (num points, 3) array
triangles : (num triangles, 3) array
normals : (num points, 3) array (optional)
Vertex normal vectors.
colors : (num points, 3 or 4) array (optional)
Vertex colors, as RGB or RGBA. Alpha channel is optional.
Type should either be ``"float"`` (range: [0.0-1.0]) or
``"u1"`` (range: [0-255]).
attached : dict of (num points,) arrays (optional)
Arbitrary attributes attached to vertices. Shape is checked on
initialization, and an error will be thrown if it is not a 1D array
with correct length. *(Note: vector or other multivalued arrays must
be split into components; this is necessary to get consistent results
when saved to a PLY mesh.)*
'''
def __init__(self, points=None, triangles=None, normals=None, colors=None, attached={}):
if points is None: points = np.zeros((0, 3), 'f')
if triangles is None: triangles = np.zeros((0, 3), 'i')
self.points = enforce_shape(points, (-1, 3))
self.triangles = enforce_shape(triangles, (-1, 3))
if normals is not None:
self.normals = enforce_shape(normals, (len(self.points), 3))
if colors is not None:
self.colors = enforce_shape(colors, (len(self.points), -1))
self.attached = {}
for k, v in attached.iteritems():
self.attached[k] = enforce_shape(v, (len(self.points), ))
[docs] def attach(self, key, val):
'''Attach a named vertex parameter.
Equivalent to setting an item in the ``attached`` attribute, but also
does shape checking.
Parameters
----------
key : string
val : numpy array shaped (num_points, )
'''
self.attached[key] = enforce_shape(val, (len(self.points), ))
def __getattr__(self, key):
#Allow access to attached attributes directly.
if key in self.attached:
return self.attached[key]
else:
raise AttributeError("mesh does not have property '%s'" % key)
[docs] def save(self, fn, file_type=None):
'''Save mesh as a file.
Supported file types:
* ``ply``: The most useful format; will also save attached parameters.
* ``stl``: Used primarily by 3D printers. Only save geometry, not
colors, normals, etc.
* ``pov``: Used to render object in POVRay. If normals exists,
smooth triangles will be saved, but colors, etc.
are not retained. See :meth:`save_pov` for more
options and details, including sample scene
generation.
Parameters
----------
fn : string
File name
file_type : string, optional
File type, should be one of the supported formats.
If not specified, determined from the file name.
'''
if file_type is None: file_type = os.path.splitext(fn)[1].strip('.')
if file_type.lower() == 'ply':
with open(fn, 'wb') as f:
f.write(encode_ply(self.points, self.triangles,
normals=getattr(self, "normals", None),
colors=getattr(self, "colors", None),
extra_vertex_attributes=self.attached))
elif file_type.lower() == 'stl':
#if extra_vertex_attributes is not None:
# raise ValueError('extra_vertex_attributes can only be saved in PLY files')
stl_dat = np.empty(len(self.triangles), dtype=STL_DTYPE)
stl_dat['byte_count'] = 0 #Just how STL works.
for i, a in enumerate(['c1', 'c2', 'c3']):
stl_dat[a] = self._tps(i)
stl_dat['n'] = self.face_normals()
with open(fn, 'wb') as f:
header = '\x00\x00This is an STL file. (http://en.wikipedia.org/wiki/STL_(file_format))'
f.write(header + ' ' * (80 - len(header)))
f.write(np.array(len(self.triangles), 'u4').tostring())
f.write(stl_dat.tostring())
elif file_type.lower() == 'pov':
self.save_pov(fn)
else:
raise ValueError('file type should be "ply", "stl", or "pov"')
[docs] def rotate(self, rv):
'''Rotate the mesh (in place).
Parameters
----------
rv : vector
Rotation vector, will rotate counter-clockwise around axis by an amount
given be the length of the vector (in radians).
'''
self.points = vector.rot(rv, self.points)
if hasattr(self, "normals"):
self.normals = vector.rot(rv, self.normals)
[docs] def rotated(self, rv):
'''Return a rotated copy of a mesh'''
m = self.copy()
m.rotate(rv)
return m
[docs] def scale(self, s):
'''Scale the Mesh in place.
Parameters
----------
scale : float or vector
'''
self.points *= s
[docs] def scaled(self, s):
'''Return a scaled copy of a mesh.'''
m = self.copy()
m.scale(s)
return m
[docs] def invert(self):
'''Flip the mesh normals, including triangle winding order and the
``normals`` field (if present).'''
self.triangles = self.triangles[:, ::-1]
if hasattr(self, "normals"):
self.normals *= -1
[docs] def inverted(self):
'''Return an inverted copy of a mesh.'''
m = self.copy()
m.inverted()
return m
[docs] def translate(self, delta):
'''Rotate the mesh (in place).
Parameters
----------
delta : vector
'''
self.points += delta
[docs] def translated(self, delta):
'''Return an translated copy of a mesh.'''
m = self.copy()
m.translate(delta)
return m
[docs] def volume(self):
'''Return the volume of the mesh.
The mesh is assumed to be closed and non-interecting.
'''
px, py, pz = self.tps(0).T
qx, qy, qz = self.tps(1).T
rx, ry, rz = self.tps(2).T
return (px*qy*rz + py*qz*rx + pz*qx*ry - px*qz*ry - py*qx*rz - pz*qy*rx).sum() / 6.
[docs] def is_closed(self, tol=None):
'''Check if the mesh is closed by calculating the volume of a
transposed copy.
This routine is not fool-proof, but catches most open meshes.
Parameters
----------
tol : float (optional)
The relative tolerance of the volume comparison. Defaults to 1E-12
if the vertex data type is double precision, otherwise 1E-6.
'''
if tol is None: tol = 1E-6 if self.points.dtype == np.dtype('f') else 1E-12
x, y, z = self.points.T
m2 = self.copy()
m2.points += 2 * np.array((max(x) - min(x), max(y) - min(y), max(z) - min(z)))
v1 = self.volume()
v2 = m2.volume()
return abs((v1 - v2) / v1) < tol
[docs] def colorize(self, c, cmap='jet', clim=None):
'''Color a mesh using matplotlib specifications.
Parameters
----------
c : string or array
* If type is string, it is interpreted like a normal matplotlib color
(e.g., ``"r"`` for red, or ``"0.5"`` for 50% gray, or ``"#FF8888"`` for pink).
* If type is array, it should have the same length as the number of points,
and it will be converted to a colormap.
cmap : string or matplotlib colormap (default: 'jet')
The color map to use. Only relevant if ``c`` is an array.
clim : tuple (default: None)
The color limits. If None, the max/min of c are used.
'''
if isinstance(c, basestring):
import matplotlib.colors
self.colors = np.tile(matplotlib.colors.colorConverter.to_rgb(c), (len(self.points), 1))
else:
import matplotlib.cm
c = enforce_shape(c, (len(self.points),))
if clim is None: clim = (c.min(), c.max())
self.colors = matplotlib.cm.get_cmap(cmap)((c - clim[0]) / (clim[1] - clim[0]))
[docs] def copy(self):
'''Return a copy of the mesh, with sub-arrays copied as well.'''
return Mesh(
points=self.points.copy(),
triangles=self.triangles.copy(),
normals=self.normals.copy() if hasattr(self, 'normals') else None,
colors=self.colors.copy() if hasattr(self, 'colors') else None
)
def _tps(self, n):
'''Returns the vortices corrsesponding to one corner of each triangle.
Parameters
----------
n : 0, 1, 2 (int)
Triangle corner number
Returns
-------
X : [num points, 3] array
The vertices corresponding to the specified corner number
'''
return self.points[self.triangles[:, n]]
[docs] def face_normals(self, right_hand=True, normalize=True):
'''Compute triangle face normals.
Parameters
----------
right_hand : bool (default: True)
Right-hand or left hand normals? Normally, right-hand normals are
used, meaning triangles are wound counter-clockwise when viewed
from "outside" the mesh.
normalize : bool (default: True)
If True, normals are normalized after being computed.
Returns
-------
n : [num triangles, 3] array
Normals corresponding to each face, generated as a cross product.
'''
n = vector.cross(self._tps(2) - self._tps(0), self._tps(1) - self._tps(0))
if normalize:
n = vector.norm(n)
if not right_hand:
n *= -1
return n
[docs] def convert_colors(self, dtype):
'''Convert type of colors field, rescaling if required.
**Note: usually the type of the color field is not important, e.g. when
saving to ``"ply"`` format the color is automatically converted on saving.**
Parameters
----------
dtype : valid numpy data type (usually ``"f"`` or ``"u1"``)
Data type to convert colors to. If converting from ``"u1"`` to a float
type, rescales from [0-255] to [0.0-1.0], and vice-versa. If converting
from float to ``"u1"``, colors will first be clipped from (0-1).
'''
self.colors = colors_astype(self.colors, dtype)
def __iadd__(self, other):
if hasattr(other, 'points') and hasattr(other, 'triangles'):
NO = len(self.points)
self.points = np.vstack([self.points, other.points])
self.triangles = np.vstack([self.triangles, other.triangles + NO])
shc = hasattr(self, 'colors')
ohc = hasattr(other, 'colors')
missing_props = []
if shc or ohc:
if not ohc:
#warnings.warn('trying to add two meshes when only one has colors defined; making uncolored mesh white', MeshWarning, stacklevel=2)
missing_props.append('colors')
oc = np.ones((len(other.points), 3), 'f')
else:
oc = other.colors
if not shc:
#warnings.warn('trying to add two meshes when only one has colors defined; making uncolored mesh white', MeshWarning, stacklevel=2)
missing_props.append('colors')
mc = np.ones((NO, 3), dtype=oc.dtype)
if mc.dtype == np.dtype('u1'): mc *= 255
else:
mc = self.colors
oc = colors_astype(oc, mc.dtype)
self.colors = np.ones((len(self.points), max(oc.shape[1], mc.shape[1])), mc.dtype)
if self.colors.shape[1] == 4 and self.colors.dtype == np.dtype('u1'):
self.colors[:, 3] = 255
self.colors[:mc.shape[0], :mc.shape[1]] = mc
self.colors[mc.shape[0]:, :oc.shape[1]] = oc
for prop in set(['normals'] + self.attached.keys() + other.attached.keys()):
p1 = getattr(self, prop, None)
p2 = getattr(other, prop, None)
if p1 is not None:
if p2 is not None:
#self.normals = np.vstack([self.normals, other.normals])
p_new = np.concatenate([p1, p2], axis=0)
if prop == 'normals': self.normals = p_new
else: self.attached[prop] = p_new
else:
#warnings.warn('trying to add two meshes when only one has normals defined; dropping normals from result', MeshWarning, stacklevel=2)
missing_props.append(prop)
if prop == 'normals': del self.normals
else: del self.attached[prop]
elif p2 is not None:
#warnings.warn('trying to add two meshes when only one has normals defined; dropping normals from result', MeshWarning, stacklevel=2)
missing_props.append(prop)
if missing_props:
warnings.warn('tried to add two meshes with different attached properties (conflicted values: %s)\nmissing properties dropped from result' % missing_props)
return self
else: raise TypeError('can only add a Mesh to another Mesh')
def __add__(self, other):
m = self.copy()
m += other
return m
[docs] def euler_char(self):
'''Return the *total* Euler Characteristic of a mesh: :math:`\chi = V - E + F`.
Note that orphaned points (not connected to triangles) are automatically
excluded.
If the mesh is composed of multiple parts, they can first be separated
with :meth:`Mesh.separate`.
'''
#http://en.wikipedia.org/wiki/Euler_characteristic
#Create a list of edges with index p0 + NP*p1 [where p1<p0]
#Because we presorted the triangles, we gaurantee the lowest index point
# is p0, thus this is a unique edge index.
tt = self.triangles.copy()
tt.sort(axis=-1)
N = len(self.points)
num_edges = len(np.unique(np.array([
tt[:, 0] + N*tt[:, 1],
tt[:, 0] + N*tt[:, 2],
tt[:, 1] + N*tt[:, 2],
])))
#Unreferenced points not included this way!
num_points = len(np.unique(self.triangles))
return num_points - num_edges + len(self.triangles)
[docs] def separate(self, return_point_indices=False):
'''Break the mesh into disconnected parts.
Parts are considered connected if any of their triangles share a corner.
*Note:* This algorithm is unoptimized and may run slower than you expect.
Parameters
----------
return_point_indices : bool, optional (default: False)
If true, returns the indices of the points in the original mesh
which are in each sub-mesh.
Returns
-------
meshes : list of meshes
point_indices : list of int arrays (optional)
Only returned if ``return_point_indices==True``.
'''
#----------------------------------------------------------------------
#Old, slower method
#----------------------------------------------------------------------
#
#t = self.triangles
#marked = np.zeros(len(self.points), bool)
#groups = []
#
#while len(t):
# g = [t[-1:]]
# t = t[:-1]
#
# while True:
# ptt = np.unique(g[-1])
# #print ptt, t.shape
# ptt = ptt[np.where(~marked[ptt])]
#
# if not len(ptt): break
#
# connected = (ptt.reshape(-1, 1, 1) == t).any(0).any(-1)
#
# if connected.any():
# g.append(t[np.where(connected)])
# t = t[np.where(~connected)]
# marked[ptt] = True
# else: break
#
# groups.append(np.vstack(g))
#
#parts = []
#point_i = []
#for t in groups:
# ip, it = np.unique(t, return_inverse=True)
#
# extras = {}
# for k in ['normals', 'colors']:
# if hasattr(self, k):
# extras[k] = getattr(self, k)[ip]
#
# parts.append(Mesh(points=self.points[ip], triangles=it.reshape(-1, 3), **extras))
# point_i.append(ip)
#----------------------------------------------------------------------
#New, 10x faster method (~10k points in a few 100 ms on 2012 hardware)
#----------------------------------------------------------------------
#
#Find all the triangles connected to a point
tn = np.argsort(self.triangles.flat)
ip = self.triangles.flat[tn]
tn //= 3
breaks = np.where(ip[:-1] != ip[1:])[0]
start = np.zeros(len(self.points), 'i8')
end = np.zeros(len(self.points), 'i8')
#triangles connected to point i => tn[start[i]:end[i]]
end[ip[breaks]] = breaks+1
start[ip[breaks+1]] = breaks+1
end[ip[breaks[-1]+1]] = len(ip)
tris_at_point = {}
for i in range(len(self.points)):
tris_at_point[i] = tn[start[i]:end[i]]
t_free = np.ones(len(self.triangles), bool)
ffunc = lambda x: t_free[x]
nt = 0
groups = []
dummy = np.empty(0, dtype='i8') #Used to prevent type conversion on concatenates
while nt < len(self.triangles):
new_tris = np.array([np.argmax(t_free)]) #Pick a free one.
group = []
while True:
t_free[new_tris] = False
nt += len(new_tris)
group.append(new_tris)
connected_tris = np.unique(np.concatenate(
[tris_at_point.pop(i, dummy) for i in self.triangles[group[-1]].flat]))
new_tris = connected_tris[np.where(t_free[connected_tris])]
if not len(new_tris):
groups.append(np.concatenate(group))
break
parts = []
point_i = []
for t in groups:
ip, it = np.unique(self.triangles[t], return_inverse=True)
extras = {}
for k in ['normals', 'colors']:
if hasattr(self, k):
extras[k] = getattr(self, k)[ip]
attached = {}
for k in self.attached:
attached[k] = self.attached[k][ip]
extras['attached'] = attached
parts.append(Mesh(points=self.points[ip], triangles=it.reshape(-1, 3), **extras))
point_i.append(ip)
if return_point_indices: return parts, point_i
else: return parts
[docs] def periodic_separate(self, top=None):
'''Separate into sections and then reconnect across periodic boundaries.
Similar to :meth:`separate`, but checks for periodic reconnections.
Ideally this mase generated by :meth:`ilpm.geometry_extractor.periodic_isosurface`, but in principle
this is not necessary. The algorithm looks for sections that touch the
bottom edge (x/y/z=0) and tries to move them to the top and join them.
Parameters
----------
m : ilpm.mesh.Mesh
top : list of floats, optional
The period of each axis. If not specified it will be the maximum value
of any points along each axis.
*Note: this is generally ok even
if the maximum value isn't the period; in this case there should
also be no points touching the bottom boundary, so this axis will
have no effect. However, if you have a mesh where the points touch
the "bottom" (x/y/z=0) and not the "top", then you must manually
specify the "top" to get correct results.*
Returns
-------
meshes : list of Mesh objects
The reconnected sections. Any mesh touching a bottom boundary will be
pushed to the top and merged with coindident sections.
'''
if top is None: top = self.points.max(0)
parts = self.separate()
for axis in range(3):
bulk = []
edge = []
for m in parts:
on_top = m.points[:, axis] == top[axis]
if on_top.any():
edge.append((m, on_top))
else:
on_bottom = m.points[:, axis] == 0.0
if on_bottom.any():
m.points[:, axis] += top[axis]
edge.append((m, on_bottom))
else:
bulk.append(m)
while len(edge) > 1:
for i, (m1, mask1) in enumerate(edge):
break_now = False
for j, (m2, mask2) in enumerate(edge[:i]):
merged = _attempt_merge(m1, m2, mask1, mask2)
if merged is not False:
edge.pop(i)
edge.pop(j)
mask = merged.points[:, axis] == top[axis]
if mask.any():
edge.append((merged, mask))
else:
bulk.append(merged)
#If we changed the edge array, we need to restart the merging loop.
break_now = True
break
if break_now: break
else: break #No parts separated
parts = bulk + map(lambda x: x[0], edge)
return parts
[docs] def clip(self, a, level=0, both_sides=False):
'''Clip a mesh by some parameter defined for each point.
Parameters
----------
a : [N] array
An array with the same size as the number of points.
level : float, optional (default: 0)
The level at which to clip the mesh, keeps all sections with
(a-level) >=0, clipping triangles that span the boundary.
both_sides : bool, optional (default: False)
If True, returns two meshes, one for each side of the split.
Returns
-------
clipped_mesh : Mesh
A clipped version of the mesh. All attached attributes are clipped
and retained as well.
other_side: Mesh, optional
If both_sides == True, returns the other side of the clip.
'''
a = enforce_shape(a, (len(self.points), ))
if level: a -= level
ab = a >= 0
#Inside and outside point index.
ipi = np.where(ab)[0]
ni = len(ipi)
opi = np.where(~ab)[0]
no = len(opi)
#Point map for inside and outside points; we can share a map for both,
# since there is no overlap. This map is used to go from old point
# index to new point index.
point_map = np.zeros(len(a), dtype='i8')
point_map[ipi] = np.arange(ni)
point_map[opi] = np.arange(no)
#Triangle corner in
tci = ab[self.triangles]
new_tri = []
if both_sides:
new_tri_out = []
clipped_edges = {}
ak = filter(lambda k: hasattr(self, k), ['points', 'normals', 'colors']) + self.attached.keys()
new_points = {}
for k in ak: new_points[k] = []
def get_edge(i1, i2):
if i1 > i2: i1, i2 = i2, i1
ii = (i1, i2)
if ii not in clipped_edges:
clipped_edges[ii] = len(new_points['points'])
z = -a[i1] / (a[i2] - a[i1])
for k in ak:
attr = getattr(self, k)
v = attr[i1] * (1-z) + attr[i2] * z
#print k, v
if k == 'normals': v = vector.norm(v)
new_points[k].append(v)
return clipped_edges[ii]
#Partial in triangles
#if False:
for i in np.where(tci.all(1) ^ tci.any(1))[0]:
c = self.triangles[i]
aab = ab[c]
#Roll the corner indices until index 0 is in and 2 is out.
#A while loop is avoided for safety, although it should be ok.
for i in range(2):
if aab[0] and not aab[2]:
break
else:
c = np.roll(c, 1)
aab = np.roll(aab, 1)
cm = point_map[c]
#Now there are two options: index 1 is in or out, i.e. 1 or 2
# corners are inside the clip. We'll handle each case
# individually.
if aab[1]: #Corners 0, 1 inside.
#Create clipped points
e1 = get_edge(c[0], c[2])
e2 = get_edge(c[1], c[2])
#Add triangles, maintaining winding order!
new_tri += [(cm[0], e2+ni, e1+ni), (cm[0], cm[1], e2+ni)]
if both_sides:
new_tri_out += [(e1+no, e2+no, cm[2])]
else: #Same as above, but now only corner 0 is in.
e1 = get_edge(c[0], c[1])
e2 = get_edge(c[0], c[2])
new_tri += [(cm[0], e1+ni, e2+ni)]
if both_sides:
new_tri_out += [(e1+no, cm[1], cm[2]), (e1+no, cm[2], e2+no)]
ai_ti = np.where(tci.all(1))[0]
kw = dict(
triangles=np.concatenate([point_map[self.triangles[ai_ti]], np.asarray(new_tri, dtype='i8').reshape(-1, 3)]),
attached = {})
if both_sides:
ao_ti = np.where(~tci.any(1))[0]
kw_out = dict(
triangles=np.concatenate([point_map[self.triangles[ao_ti]], np.asarray(new_tri_out, dtype='i8').reshape(-1, 3)]),
attached = {})
for k in ak:
v = getattr(self, k)
vn = np.array(new_points[k], dtype=v.dtype).reshape((-1, ) + v.shape[1:])
vi = np.concatenate([v[ipi], vn])
if both_sides:
vo = np.concatenate([v[opi], vn])
if k in ('points', 'normals', 'colors'):
kw[k] = vi
if both_sides: kw_out[k] = vo
else:
kw['attached'][k] = vi
if both_sides: kw_out['attached'][k] = vo
if both_sides:
return Mesh(**kw), Mesh(**kw_out)
else:
return Mesh(**kw)
[docs] def color_by_proximity(self, p, c=None, w=None, rgb_only=True, power=-6, period=None):
'''Color a mesh by proximity to another path, tangle, or set of colored points.
Parameters
----------
p : Path, Tangle or [N, 3] array
c : [N, 3 or 4] array
The color of each point; required if p is an array, and ignored if
it is a Path or Tangle (in this case the ``color`` attribute is
used).
w : [N] array, optional
Weight array.
rgb_only : bool, optional (default: True)
If True, the alpha component is dropped
power : int, optional (default: -6)
The power of the radial weighting function.
Recommended values are <= -4.
period : None, float or (3,) array_like (optional)
If not None, the period to use for wrapping the proximity
calculation (data assumed to be go from 0-period on each axis).
If one number is specified, the period is assumed to be the same
on all three axes.
'''
if hasattr(p, '_color_by_proximity_points'):
p, c, w = p._color_by_proximity_points()
p = enforce_shape(p, (-1, 3))
if c is None: raise ValueError('if specified as points, a color array must also be specified')
c = enforce_shape(c, (len(p), -1))
if c.shape[-1] == 3: c = np.concatenate([c, np.zeros((len(c), 1))], axis=-1)
elif c.shape[-1] != 4: raise ValueError('last dimension of color array must be 3 or 4')
if w is None: w = np.ones((len(c), 1))
else: w = enforce_shape(w, (len(c), 1) )
_check_code()
pw = CL.to_device(np.concatenate([p, w], axis=-1).astype(CL.real))
c = CL.to_device(c.astype(CL.real))
#self._color_by_proximity_inner(pw, c, rgb_only, power)
x = CL.to_device(self.points)
color = CL.empty((len(self.points), 4))
#__kernel void path_color_by_proximity(
# global REAL* X, int nx,
# global REAL4* pw, global REAL4* c, int nc,
# global REAL4* color, int power
# )
if period is None:
CL.mesh_color_by_proximity(
x, len(x),
pw, c, len(pw),
color, power)
else:
p = np.ones(3, dtype=CL.real)
p *= period
px, py, pz = p
CL.mesh_color_by_proximity_periodic(
x, len(x),
pw, c, len(pw),
color, power,
px, py, pz)
color = color.get()
if rgb_only: self.colors = color[..., :3]
else: self.colors = color
def _pov_vec(self, V, prec=5):
return '<%s>' % (','.join(['%%.%df' % prec % x for x in V]))
def _pov_point(self, i, prec=5):
s = self._pov_vec(self.points[i], prec)
if hasattr(self, 'normals'):
s += ',' + self._pov_vec(self.normals[i], prec)
return s
[docs] def save_pov(self, fn, include_extras=False, mesh_name=None, precision=5):
'''Save a mesh into a POV file; optionally create a sample scene.
The resulting POV file contains only a definition of the mesh, which
can be imported into other files.
The POV file will not contain colors or other attached attributes,
but corner normals will cause it to render smooth triangles.
Parameters
----------
fn : string
Filename to save to; ``.pov`` extension will be forced.
include_extras : bool, optional (default: False)
If True, creates ``<mesh_name>_scene.pov`` and
``<mesh_name>_scene.sh``, which are a sample scene and script for
rendering the mesh.
mesh_name : string, optional (default: ``<mesh_name>_mesh``)
The name of the mesh in the declare statement. (i.e. this is the
name of the object created in the POV file.)
precision : int, optional (default: 5)
The precision of the the points
'''
fnb, ext = os.path.splitext(fn)
if not ext: ext = '.pov'
if mesh_name is None: mesh_name = os.path.split(fnb)[-1] + "_mesh"
main_name = fnb + ext
output = open(main_name, 'wt')
output.write('#declare %s = mesh\n{\n' % mesh_name)
if hasattr(self, 'normals'):
for t in self.triangles:
output.write(' smooth_triangle {%s}\n' % ','.join(map(lambda x: self._pov_point(x, precision), t)))
else:
for t in self.triangles:
output.write(' triangle {%s}\n' % ','.join(map(lambda x: self._pov_point(x, precision), t)))
output.write('}\n')
output.close()
if include_extras:
center = self.points.mean(0)
p = self.points - center
max_size = abs(p).max()
bottom = min(p[..., 1]) / max_size
scene_name = fnb + "_scene" + ext
output = open(scene_name, 'wt')
output.write('''#include "%s"
camera {
location <2.5, 2.5, 0>
look_at 0
}
light_source {
<10, 0, 0>, color rgb 1.3
area_light 2*y, 2*z, 10, 10
adaptive 1
circular
rotate y * 20
rotate z * 60
}
object {
%s
translate -%s
scale 1 / %0.5f
texture {
pigment {color rgb <1, 0.2, 0.2>}
finish {phong 0.5}
}
}
plane {
<0, 1, 0>, %f
texture {
pigment {color rgb 1.5}
finish {ambient 0.2}
}
}''' % (main_name, mesh_name, self._pov_vec(center), max_size, bottom)
)
output.close()
script_file = fnb + "_scene.sh"
output = open(script_file, 'wt')
output.write('#!/bin/bash\npovray +I%s +O%s.png +A0.3 +W1024 +H768\n' % (scene_name, fnb))
output.close()
os.chmod(script_file, 0777)
[docs] def remove_degenerate_triangles(self):
'''Remove triangles with repeated edges.'''
self.triangles = self.triangles[
np.where((self.triangles != np.roll(self.triangles, 1, axis=-1)).all(-1))]
#class Polygon(object):
# '''2D Polygon Object.
#
# Used primarily by :func:`make_tube` for describing boundaries.
#
# Polygons should be defined counter-clockwise for positive area.
#
# Parameters
# ----------
# points : [N, 2] array
# The corners of the polygon in order. Assumed to be closed (the last
# point should *not* be a repeat of the first point.)
# normals : [N, 2] array, optional
# The normal of each corner. If not specified, automatically generated.
# (The normal is used when producing a mesh outline from a given shape)
# '''
#
# def __init__(self, points, normals=None):
# self.points = enforce_shape(points, (-1, 2))
#
# plus = np.roll(self.points, -1, axis=0)
# t = vector.norm(plus - self.points)
# self.seg_n = np.vstack([-t[:, 1:2], t[:, 0:1]])
#
# if normals is None:
# self.normals = vector.norm(self.seg_n + np.roll(self.seg_n, +1, axis=0))
# else:
# self.normals = vector.norm(enforce_shape(normals, self.points.shape))
#
# def area(self):
# '''Return the signed polygon area.
#
# Counterclockwise winding produces positive area.
# '''
# plus = np.roll(self.points, -1, axis=0)
# return 0.5 * (self.points[:, 0]*plus[:, 1] - plus[:, 0]-self.points[:, 1])
#
# def flip(self):
# '''Flip the polygon winding and normals.'''
# self.points = self.points[::-1]
# self.normals *= -1
# self.seg_n *= -1
#
# def check_orientation(self):
# '''Check if the polygon is wound counterclockwise, flip if it isn't.'''
# if self.area() < 0: self.flip()
#
# def extrude(self, X, A, B, enforce_normality=True, closed=True):
# '''Extrude the polygon as the boundary of a 3D path, producing a mesh.
#
# Parameters
# ----------
# X : [N, 3] array
# The centerline of the extrusion
# A, B : [N, 3] arrays
# The vectors which will correspond to the x and y coordinate of the
# polygon.
# enforce_normality : bool, optional (default: True)
# If True, A and B are forced to be unit length and perpendicular.
# (A is normalized, and B is reorientated and normalized with
# Graham Schmidt.)
# closed : bool, optional (default: True)
# If True, the path is taken to be closed.
# '''
#
# X = enforce_shape(X, (-1, 2))
# A = enforce_shape(A, X.shape)
# B = enforce_shape(B, X.shape)
#
# if enforce_normality:
# A = vector.norm(A)
# B = vector.norm(B - A*vector.dot1(A, B))
#
[docs]def surface_of_revolution(r, z, N=100):
'''Create a mesh from a surface of revolution.
Surface is revolved around the z axis.
Parameters
----------
r, z : array_like, 1D
The radius and z coordinate of the surface of revolution.
N : int, optional (default: 100)
The number of points in the revolution
Returns
-------
surface : Mesh
The revolved surface.
'''
r = enforce_shape(r, (-1,))
z = enforce_shape(z, r.shape)
phi = np.linspace(0, 2*np.pi, N, endpoint=False).reshape(-1, 1)
X = np.empty((N, len(r), 3))
X[..., 0] = r * np.cos(phi)
X[..., 1] = r * np.sin(phi)
X[..., 2] = z
p00 = np.arange(np.prod(X.shape[:2])).reshape(-1, len(r))
p10 = np.roll(p00, -1, axis=0)
p01 = np.roll(p00, -1, axis=1)
p11 = np.roll(p10, -1, axis=1)
triangles = np.swapaxes(np.array([[p00, p01, p10], [p01, p11, p10]]), 1, -1)
#triangles = np.array([[p00, p01, p10], [p01, p11, p10]])
return Mesh(points=X.reshape(-1, 3), triangles=triangles.reshape(-1, 3))
[docs]def extrude_shape(X, A, B, outline=10, outline_normals=None, scale=1, closed=True,
enforce_normality=True, enforce_direction=True, outline_closed=True):
'''Create a tubular mesh by extruding a shape along a path.
Parameters
----------
X : [N, 3] array
The centerline of the extrusion
A, B : [N, 3] arrays
The vectors which will correspond to the x and y coordinate of the
polygon. Nomimally, dX, A, B form an orthonormal basis.
outline : int or array
If int, a circle is automatically generated with that number of points.
If array, shape should be [M, 2] -or- [N, M, 2], where M is the number
of points in the outline (for an outline which varies along the tube).
outline_normals: array, optional
The 2D normals of each point in the outline. Shape should match
outline.
scale : float or [N] array
The scale of the outline.
*Note:* If scale is negative, the mesh will be inside out.
closed : bool, optional (default: True)
If True, the centerline is taken to be closed.
enforce_normality : bool, optional (default: True)
If True, A and B are forced to be unit length and perpendicular.
(A is normalized, and B is reorientated and normalized with
Graham Schmidt.)
enforce_direction : bool, optional (default: True)
If True, orients the path counter-clockwise automatically, accounting
for the fact that the basis vector may be right or left-handed.
*Note:* if variable outline is specified, it only checks the *first*
outline, and flips all outlines.
outline_closed : bool, optional (default: True)
If True, the outline is assumed to be closed.
'''
X = enforce_shape(X, (-1, 3))
A = enforce_shape(A, X.shape)
B = enforce_shape(B, X.shape)
#Create tangent vector and ds
T = vector.norm(np.roll(X, -1, axis=0) - np.roll(X, +1, axis=0))
ds = vector.mag1(np.roll(X, -1, axis=0) - X)
if not closed: #End points don't wrap, so fix them!
T[0] = vector.norm(X[1] - X[0])
T[-1] = vector.norm(X[-1] - X[-2])
ds = ds[:-1] #There is one less segment than the number of points for open paths
if enforce_normality:
A = vector.norm(A)
B = vector.norm(B - A*vector.dot1(A, B))
#If there is no outline, make a circle
if isinstance(outline, int):
phi = np.arange(outline) * 2*np.pi / outline
outline = np.array([np.cos(phi), np.sin(phi)]).T
outline = enforce_shape(outline, (-1, -1, 2))
handedness = np.sign(vector.dot(X[1] - X[0], vector.cross(A[0], B[0])))
#If direction enforcement is on...
if enforce_direction and outline_closed:
o1 = outline[0]
p1 = np.roll(o1, -1, axis=0)
area = 0.5 * (o1[:, 0]*p1[:, 1] - p1[:, 0]*o1[:, 1]).sum()
#print vector.dot(vector.norm(X[1] - X[0]), vector.cross(A[0], B[0]))
#print handedness
if area * handedness < 0:
outline = outline[:, ::-1]
#print outline.shape, np.array(scale).reshape(-1, 1, 1).shape
outline = outline * np.array(scale).reshape(-1, 1, 1)
#Generate outline normals if not specified
if outline_normals is None:
if outline_closed:
t = vector.norm(np.roll(outline, -1, axis=1) - outline)
seg_n = t[..., ::-1] * (-1, 1) * -handedness
outline_normals = vector.norm(seg_n + np.roll(seg_n, +1, axis=1))
else:
t = vector.norm(outline[:, 1:] - outline[:, :-1])
seg_n = t[..., ::-1] * (-1, 1) * -handedness
outline_normals = np.zeros_like(outline)
outline_normals[:, :-1] += seg_n
outline_normals[:, 1:] += seg_n
outline_normals = vector.norm(outline_normals)
outline_normals = enforce_shape(outline_normals, outline.shape)
#Make a basis vector
AB = np.concatenate([A.reshape(-1, 1, 3, 1), B.reshape(-1, 1, 3, 1)], axis=-1)
#Reshape everything as [centerline index, outline index, xyz, ab index]
X = X.reshape(-1, 1, 3)
T = T.reshape(-1, 1, 3)
ds = ds.reshape(-1, 1, 1)
#sum over ab index of basis vectors to convert outline ab to xyz coordinates
offset = (outline[:, :, np.newaxis, :] * AB).sum(-1)
points = X + offset
#ditto for normal vectors
normals = (outline_normals[:, :, np.newaxis, :] * AB).sum(-1)
#Correct normal vectors for changes in the radius of the tube.
#To do this, first compute a m=dr/ds slope for each point
R = offset - T*vector.dot1(T, offset)
r = vector.mag1(R) #Radius of point
dr = np.roll(r, -1, axis=0) - r
if closed:
ms = dr/ds #Slope on segment
m = 0.5 * (ms + np.roll(ms, +1, axis=0)) #Slope on point
else:
dr = dr[:-1] #One less segment for open paths
ms = dr/ds
m = np.zeros_like(r)
m[0] = ms[0]
m[-1] = ms[-1]
m[1:-1] = 0.5 * (ms[:-1] + ms[1:])
#From the slope, we can generate a vector which points along the tube surface
S = vector.norm(T + m*R/r)
#Remove this vector from the normals
normals = vector.norm(normals - S*vector.dot1(S, normals))
Ntot = np.prod(points.shape[:-1])
p00 = np.arange(Ntot).reshape(points.shape[:-1])
if closed:
p10 = np.roll(p00, -1, axis=0)
else:
p10 = p00[1:]
p00 = p00[:-1]
if outline_closed:
p01 = np.roll(p00, -1, axis=1)
p11 = np.roll(p10, -1, axis=1)
else:
p01 = p00[:, 1:]
p00 = p00[:, :-1]
p11 = p10[:, 1:]
p10 = p10[:, :-1]
triangles = np.swapaxes(np.array([[p00, p01, p10], [p01, p11, p10]]), 1, -1)
return Mesh(points=points.reshape(-1, 3), normals=normals.reshape(-1, 3), triangles=triangles.reshape(-1, 3))
#return Mesh(points=points.reshape(-1, 3), normals=S.reshape(-1, 3), triangles=triangles.reshape(-1, 3))
#Used by periodic_separate
def _attempt_merge(m1, m2, mask1=None, mask2=None, r_min=0.0):
if mask1 is None: mask1 = ones(len(m1.points), bool)
if mask2 is None: mask2 = ones(len(m2.points), bool)
i1 = np.where(mask1)[0].reshape(-1, 1)
i2 = np.where(mask2)[0].reshape(1, -1)
r = vector.mag(m1.points[i1] - m2.points[i2])
mr = r.min(0) #Closest distance for a point in m1 for each point in m2
#No merging possible, return None
if not (mr <= r_min).any(): return False
new_i2 = []
keep_i2 = []
ii = len(m1.points)
i2i = 0
for j2 in range(len(m2.points)):
if mask2[j2] and mr[i2i] <= r_min:
new_i2.append(i1[np.argmin(r[:, i2i])])
else:
new_i2.append(ii)
keep_i2.append(j2)
ii += 1
if mask2[j2]: i2i += 1
new_i2 = np.array(new_i2)
new_t = np.vstack([m1.triangles, new_i2[m2.triangles]])
new_p = np.vstack([m1.points, m2.points[keep_i2]])
extras = {'attached':{}}
baseprop = ['normals', 'colors']
for prop in baseprop + m1.attached.keys():
p1 = getattr(m1, prop, None)
p2 = getattr(m2, prop, None)
if p1 is not None and p2 is not None:
p_new = np.concatenate([p1, p2[keep_i2]], axis=0)
if prop in baseprop: extras[prop] = p_new
else: extras['attached'][prop] = p_new
return Mesh(new_p, new_t, **extras)
def all_in(k, d):
for kk in k:
if kk not in d: return False
else:
return True
[docs]def load_mesh(fn, file_type=None):
'''Load a triangular mesh from a file, stored either as a "PLY" or "STL" file.
Parameters
----------
fn : string
file_type : string, optional
File type, either 'ply' or 'stl'. If not specified, determined
from the file name.
Returns
-------
mesh : Mesh object
Mesh object with the data in the file. If type is "PLY", extra vertex
attributes will appear in the ``attached`` attribute.
'''
if file_type is None: file_type = os.path.splitext(fn)[1].strip('.')
if file_type.lower() == 'ply':
data = decode_ply(fn)
try: points = np.array([data['vertex'][k] for k in ('x', 'y', 'z')]).T
except: raise PLYError('ply file %s has missing or invalid vertex position data' % fn)
try: triangles = data['face']['vertex_indices']
except: raise PLYError('ply file %s has missing or invalid triangle data' % fn)
for pl in (['red', 'green', 'blue', 'alpha'], ['red', 'green', 'blue']):
if all_in(pl, data['vertex']):
colors = np.array([data['vertex'][k] for k in pl]).T
break
else:
colors = None
pl = ('nx', 'ny', 'nz')
if all_in(pl, data['vertex']):
normals = np.array([data['vertex'][k] for k in pl]).T
else:
normals = None
extras = {}
for k, v in data['vertex'].iteritems():
if k not in ('x', 'y', 'z', 'nx', 'ny', 'nz', 'red', 'green', 'blue', 'alpha'):
extras[k] = v
return Mesh(points=points, triangles=triangles, colors=colors, normals=normals, attached=extras)
return m
elif file_type.lower() == 'stl':
with open(fn, 'rb') as f:
if f.read(5).lower() == 'solid':
raise ValueError("ASCII STL reading not implemented!")
f.seek(80)
num_triangles = int(np.fromfile(f, 'u4', 1)[0])
stl_dat = np.fromfile(f, STL_DTYPE, num_triangles)
points = np.empty((num_triangles*3, 3), 'f')
for i, a in enumerate(['c1', 'c2', 'c3']):
points[i::3] = stl_dat[a]
triangles = np.arange(num_triangles*3, dtype='i').reshape(num_triangles, 3)
return Mesh(points=points, triangles=triangles)
else:
raise ValueError('file type should be "ply" or "stl"')
class PLYError(Exception): pass
PLY_FORMAT = re.compile(r'format\s+(\w+)\s+([0-9.]+)')
PLY_FORMAT_TYPES = {'ascii':'',
'binary_big_endian':'>',
'binary_little_endian':'<' }
PLY_ELEMENT = re.compile(r'element\s+(\w+)\s+([0-9]+)')
PLY_ELEMENT_TYPES = ('vertex', 'face', 'edge')
PLY_PROPERTY_LIST = re.compile(r'property\s+list\s+(\w+)\s+(\w+)\s+(\w+)')
PLY_PROPERTY = re.compile(r'property\s+(\w+)\s+(\w+)')
PLY_PROPERTY_TYPES = {
'char':'i1', 'uchar':'u1',
'short':'i2', 'ushort':'u2',
'int':'i4', 'uint':'u4',
'float':'f', 'double':'d'
}
PY_TYPE = lambda x: float if x in ('f', 'd') else int
NP_PLY = {}
for k, v in PLY_PROPERTY_TYPES.iteritems(): NP_PLY[np.dtype(v)] = k
[docs]def decode_ply(f, require_triangles=True):
'''Decode a ply file, converting all saved attributes to dictionary entries.
**Note:** this function usually does not need to be used directly; for
converting ply's to meshes, use :func:`open_mesh`. Alternatively, this
function can be used if you require access to fields beyond the basic mesh
geometry, colors and point normals.
For more info on the PLY format, see:
* http://www.mathworks.com/matlabcentral/fx_files/5459/1/content/ply.htm
* http://paulbourke.net/dataformats/ply/
Parameters
----------
f : string or file
A valid PLY file
require_triangles : bool (default: True)
If true, converts ``element_data["face"]["vertex_indices"]`` to an array
with shape ``([number of triangles], 3)``. Raises a PLYError if there
are not triangular faces.
**Note: if ``require_triangles=False``, ``element_data["face"]["vertex_indices"]``
will be a numpy array of data type ``"object"`` which contains arrays
of possibly variable length.**
Returns
-------
element_data : dict
A dictionary of all the properties in the original file.
The main dictinoary should contain two sub-dictionaries with names
``"vertex"`` and ``"face"``.
These dictionaries contain all of the named properties in the PLY file.
Minimal entries in ``"vertex"`` are ``"x"``, ``"y"``, and ``"z"``.
``"face"`` should at least contain ``"vertex_indices"``.
'''
if type(f) is str:
f = open(f, 'rb')
line = f.readline().strip()
if line != 'ply':
raise PLYError('First line of file %s is not "ply", this file is not a PLY mesh!\n[%s]' % (repr(f), line))
format_type = None
format_char = ''
format_version = None
line = ''
elements = []
element_info = {}
current_element = None
while line != 'end_header':
line = f.readline()
if not line.endswith('\n'):
raise PLYError('reached end of file (%s) before end of header; invalid file.' % repr(f))
line = line.strip()
if not line or line.startswith('comment'): continue
m = PLY_FORMAT.match(line)
if m:
if format_type is not None:
raise PLYError('format type in file %s specified more than once\n[%s]' % (repr(f), line))
format_type = m.group(1)
if format_type not in PLY_FORMAT_TYPES:
raise PLYError('format type (%s) in file %s is not known\n[%s]' % (format_type, repr(f), line))
format_char = PLY_FORMAT_TYPES[format_type]
format_version = m.group(2)
continue
m = PLY_ELEMENT.match(line)
if m:
t = m.group(1)
if t not in PLY_ELEMENT_TYPES:
raise PLYError('element type (%s) in file %s is not known\n[%s]' % (t, repr(f), line))
elif t in elements:
raise PLYError('element type (%s) appears multiple times in file %s\n[%s]' % (t, repr(f), line))
elements.append(t)
element_info[t] = [int(m.group(2))]
current_element = element_info[t]
continue
m = PLY_PROPERTY_LIST.match(line)
if m:
if current_element is None:
raise PLYError('property without element in file %s\n[%s]' % (repr(f), line))
tn = m.group(1)
tl = m.group(2)
if tn not in PLY_PROPERTY_TYPES:
raise PLYError('property type (%s) in file %s is not known\n[%s]' % (tn, repr(f), line))
if tl not in PLY_PROPERTY_TYPES:
raise PLYError('property type (%s) in file %s is not known\n[%s]' % (tl, repr(f), line))
current_element.append((m.group(3), (format_char + PLY_PROPERTY_TYPES[tn], format_char + PLY_PROPERTY_TYPES[tl])))
continue
m = PLY_PROPERTY.match(line)
if m:
if current_element is None:
raise PLYError('property without element in file %s\n[%s]' % (repr(f), line))
t = m.group(1)
if t not in PLY_PROPERTY_TYPES:
raise PLYError('property type (%s) in file %s is not known\n[%s]' % (t, repr(f), line))
current_element.append((m.group(2), format_char + PLY_PROPERTY_TYPES[t]))
continue
if line.startswith('end_header'):
break
else:
raise PLYerror('invalid line in header for %s\n[%s]')
element_data = {}
fast_triangles = False
for e in elements:
et = element_info[e]
ne = et.pop(0)
#print e, et
fast_decode = True
dtype_list = []
py_types = []
if e == 'face' and len(et) == 1 and et[0][0] == 'vertex_indices' and require_triangles:
#This mesh only has vertex indices AND we have require_triangles = True
#Lets load the data assuming it's triangles, and check later
dtype_list = [('nv', et[0][1][0]), ('v', '3' + et[0][1][1])]
fast_triangles = True
else:
for name, t in et:
if type(t) is tuple: #This element is a list.
fast_decode = False
dtype_list.append((name, 'O'))
py_types.append((name, map(PY_TYPE, t)))
else:
dtype_list.append((name, t))
py_types.append((name, PY_TYPE(t)))
dtype = np.dtype(dtype_list)
if fast_decode:
if format_type == 'ascii':
s = ''.join(f.readline() for n in range(ne))
dat = np.genfromtxt(StringIO(s), dtype=dtype)
else:
dat = np.fromfile(f, dtype=dtype, count=ne)
else:
def conv(t, x):
try: return t(x)
except: raise PLYError('expected %s, found "%s" (in file %s)' % (t, x, repr(f)))
def get(t, count=1):
t = np.dtype(t)
n = t.itemsize*count
s = f.read(n)
if len(s) != n:
raise PLYEror('reached end of file %s before all data was read' % repr(f))
return np.fromstring(s, t, count=count)
dat = np.zeros(ne, dtype=dtype)
for i in range(ne):
if format_type == 'ascii':
parts = f.readline().split()
for name, t in py_types:
if not parts:
raise PLYError('when decoding %s #%s, not enough items in line (in file %s)' % (e, i, repr(f)))
if type(t) in (list, tuple):
nl = conv(t[0], parts.pop(0))
if len(parts) >= nl:
dat[name][i] = map(lambda x: conv(t[1], x), parts[:nl])
parts = parts[nl:]
else:
raise PLYError('when decoding %s #%s, not enough items for list (in file %s)' % (e, i, repr(f)))
else:
dat[name][i] = conv(t, parts.pop(0))
else:
for name, t in et:
if type(t) is tuple:
nl = get(t[0])
dat[name][i] = get(t[1], nl)
else:
dat[name][i] = get(t)
if fast_triangles and e == 'face':
if (dat['nv'] != 3).any():
raise PLYError("require_triangles=True, but file contains non-triangular faces")
element_data[e] = {'vertex_indices':dat['v']}
else:
element_data[e] = dict((name, dat[name]) for name, t in et)
if require_triangles:
if not fast_triangles:
v = list(element_data["face"]["vertex_indices"])
if not (np.array(map(len, v)) == 3).all():
raise PLYError("require_triangles=True, but file contains non-triangular faces")
element_data["face"]["vertex_indices"] = np.array(v)
return element_data
#def load_ply(f):
# dat = decode_ply(f)
#
# if 'face' not in dat: raise PLYError('"%s" does not include face data' % f)
# if 'vertex' not in dat: raise PLYError('"%s" does not include vertex data' % f)
#
# v = dat['vertex']
#
# for p in ('x', 'y', 'z'):
# if p not in v: raise PLYError('vertices in "%s" missing "%s"' % (f, p))
[docs]def encode_ply(points, tris, normals=None, colors=None, enforce_types=True, extra_vertex_attributes=None):
'''Convert triangular mesh data into a PLY file data.
**Note:** if you wish to convert a :class:`Mesh` object to a "ply" file, use
:meth:`Mesh.save`.
Parameters
----------
points : (num points, 3) array
tris : (num triangles, 3) array
normals : (num points, 3) array, optional
Vertex normals.
colors : (num points, 3 or 4) array, optional
Vertex colors, as ``RGBA``. Alpha channel is optional, and will not
be saved if not included. Type should either be ``float`` (ranging from
0-1) or ``uchar`` (ranging from 0-255). If type is ``float``, will
be clipped, scaled, and converted to ``uchar`` if ``enforce_types=True``.
enforce_types : bool, optional (default: True)
If True, the data types of all the inputs will be converted to standard
values (``float`` for points, tris, and normals and ``uchar`` for colors).
Although saving data with non-standard types is supported by the PLY
format, files may not open in standard programs (e.g. Meshlab).
extra_vertex_attributes : dict, optional
A dictionary of extra attributes to be added to each vertex.
Each entry should consist of: ``[string name]:[numpy array]``
Type will not be altered, regardless of the value for ``enforce_types``.
If vectors need to be saved, the need to be broken out into subarrays:
all arrays in the dictionary should have shape ``(num. vert., )``.
Returns
-------
ply_data : string
The PLY file data as a string.
'''
if extra_vertex_attributes is None: extra_vertex_attributes = {}
#http://www.mathworks.com/matlabcentral/fx_files/5459/1/content/ply.htm
if enforce_types:
points = np.asarray(points, 'f')
tris = np.asarray(tris, 'i')
if normals is not None: normals = np.asarray(normals, 'f')
if colors is not None:
colors = np.asarray(colors)
if colors.dtype in ('f', 'd'):
colors = np.clip(colors, 0, 1) * 255
colors = np.asarray(colors, 'u1')
else:
points = np.asarray(points)
tris = np.asarray(tris)
#8 byte point indices not supported in PLYs
if tris.dtype == 'i8': tris = tris.astype('i4')
if tris.dtype == 'u8': tris = tris.astype('u4')
x_dt = points.dtype
p_dt = [('x', x_dt), ('y', x_dt), ('z', x_dt)]
N = len(points)
header = [
'ply',
'format binary_little_endian 1.0',
'element vertex %d' % N
]
if normals is not None:
normals = np.asarray(normals)
if normals.shape != (N, 3):
raise ValueError('shape of normals array should be ([num points], 3), found %s' % (normals.shape,))
n_dt = normals.dtype
p_dt += [('nx', n_dt), ('ny', n_dt), ('nz', n_dt)]
if colors is not None:
colors = np.asarray(colors)
if colors.shape not in [(N, 3), (N, 4)]:
raise ValueError('shape of colors array should be ([num points], [3, 4]), found %s' % normals.shape)
c_dt = colors.dtype
p_dt += [('red', c_dt), ('green', c_dt), ('blue', c_dt), ('alpha', c_dt)][:colors.shape[1]]
eva = {}
for k, v in extra_vertex_attributes.iteritems():
v = np.asarray(v)
eva[k] = v
if v.shape != (N, ):
raise PLYError('extra vertex attributes must have same length as number of vertices\n(attribute %s has shape %s, not %s)' % (k, v.shape, (N, )))
p_dt.append((k, eva[k].dtype))
#print p_dt
header += ['property %s %s' % (NP_PLY[dt], prop) for prop, dt in p_dt]
header += [
'element face %s' % len(tris),
'property list uchar %s vertex_indices' % NP_PLY[tris.dtype],
'end_header'
]
point_data = np.empty(N, p_dt)
point_data['x'] = points[:, 0]
point_data['y'] = points[:, 1]
point_data['z'] = points[:, 2]
if normals is not None:
point_data['nx'] = normals[:, 0]
point_data['ny'] = normals[:, 1]
point_data['nz'] = normals[:, 2]
if colors is not None:
point_data['red'] = colors[:, 0]
point_data['green'] = colors[:, 1]
point_data['blue'] = colors[:, 2]
if colors.shape[1] == 4:
point_data['alpha'] = colors[:, 3]
for k, v in eva.iteritems():
point_data[k] = v
t_dt = tris.dtype
tri_data = np.empty(len(tris), [('num_vert', 'u1'), ('v1', t_dt), ('v2', t_dt), ('v3', t_dt)])
tri_data['num_vert'] = 3
tri_data['v1'] = tris[:, 0]
tri_data['v2'] = tris[:, 1]
tri_data['v3'] = tris[:, 2]
return '\n'.join(header) + '\n' + point_data.tostring() + tri_data.tostring()