Skip to content
This repository has been archived by the owner on Sep 28, 2019. It is now read-only.

Increased support for quad faces and ft's #2

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 82 additions & 29 deletions lace/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,49 @@ class Mesh(

Attributes:
v: Vx3 array of vertices
f: Fx3 array of faces
f4: Fx4 array of faces (optional quads)
e: E*2 array of edges
f4: Fx4 array of vert indices for each quad face (with_quads=True)
f: Fx3 array of vert indices for each tri face (with_quads=False)

Optional attributes:
fc: Fx3 array of face colors
vc: Vx3 array of vertex colors
vn: Vx3 array of vertex normals
segm: dictionary of part names to triangle indices
vn: VNx3 array of vertex normals
vt: VTx3 array of texture vertices
vc: Vx3 array of each vertex's color
segm: dictionary of part names to face indices (f or f4)
e: Ex2 array of edges

Optional attributes (with_quads=True):
fc4: Fx4 array of colors for each face
fn4: Fx4 array of vertex normal indices for each face
ft4: Fx4 array of texture vertex indices for each face

Optional attributes (with_quads=False):
fc: Fx3 array of colors for each face
fn: Fx3 array of vertex normal indices for each face
ft: Fx3 array of texture vertex indices for each face

"""
def __init__(self,
filename=None,
v=None, f=None, f4=None,
vc=None, fc=None,
vn=None, fn=None,
vt=None, ft=None,
vc=None, fc=None, fc4=None,
vn=None, fn=None, fn4=None,
vt=None, ft=None, ft4=None,
e=None, segm=None,
basename=None,
landmarks=None, ppfilename=None, lmrkfilename=None):
_properties = ['v', 'f', 'f4', 'fc', 'vc', 'fn', 'vn', 'ft', 'vt', 'e', 'segm']
landmarks=None, ppfilename=None, lmrkfilename=None,
with_quads=False):
self._with_quads = with_quads
self._blocked_properties = [
prop3 if with_quads else prop3 + '4'
for prop3 in ['f', 'fc', 'fn', 'ft']
]

_properties = ['v', 'f', 'f4', 'vc', 'fc', 'fc4', 'vn', 'fn', 'fn4', 'vt', 'ft', 'ft4', 'e', 'segm']
# First, set initial values, so that unset v doesn't override the v
# from a file. We do this explicitly rather than using _properties
# so that pylint will recognize the attributes and make
# attribute-defined-outside-init warnings useful.
self.v = self.f = self.f4 = self.fc = self.vc = self.vn = self.fn = self.vt = self.ft = self.e = self.segm = None
self.v = self.f = self.f4 = self.vc = self.fc = self.fc4 = self.vn = self.fn = self.fn4 = self.vt = self.ft = self.ft4 = self.e = self.segm = None
self.basename = basename

# Load from a file, if one is specified
Expand All @@ -72,8 +89,11 @@ def __init__(self,
# Support `Mesh(mesh)`
import copy
other_mesh = filename
if with_quads != other_mesh.with_quads:
raise ValueError('Must use with_quads={} when copying a mesh with_quads={}'.format(
other_mesh.with_quads, other_mesh.with_quads))
# A deep copy with all of the numpy arrays copied:
for a in ['v', 'f'] + other_mesh.__dict__.keys(): # NB: v and f first as vc and fc need them
for a in ['v', 'f', 'f4'] + other_mesh.__dict__.keys(): # NB: v and f[4] first as vc[4] and fc[4] need them
if a == 'landm_raw_xyz':
# We've deprecated landm_raw_xyz and it raises an error to access it now, but some
# older pickled meshes (don't pickle meshes!) still have it as a property and they
Expand Down Expand Up @@ -131,10 +151,10 @@ def copy(self, only=None):
If only is a list of strings, i.e. ['f', 'v'], then only those properties will be copied
'''
if only is None:
return Mesh(self)
return Mesh(self, with_quads=self.with_quads)
else:
import copy
m = Mesh()
m = Mesh(with_quads=self.with_quads)
for a in only:
setattr(m, a, copy.deepcopy(getattr(self, a)))
return m
Expand All @@ -149,33 +169,50 @@ def copy_fvs(self):
'''
return self.copy(only=['f', 'v', 'segm'])

def _ensure_property_set_is_allowed(self, prop):
if prop in self._blocked_properties:
alternate_prop = prop + '4' if self.with_quads else prop.rstrip('4')
raise ValueError("When with_quads={}, setting {} is not allowed. Use {} instead.".format(
self.with_quads, prop, alternate_prop))

def _set_property(self, prop, val):
if val is not None:
self._ensure_property_set_is_allowed(prop)
self.__dict__[prop] = val

@property
def with_quads(self):
return self._with_quads

# Rather than use a private _x varaible and boilerplate getters for
# these, we'll use the actual var name and just override the setter.
@setter_property
def v(self, val):
# cached properties that are dependent on v
self._clear_cached_properties('vertices_to_edges_matrix', 'vertices_to_edges_matrix_single_axis')
self.__dict__['v'] = as_numeric_array(val, dtype=np.float64, shape=(-1, 3), allow_none=True, empty_as_none=True)
self._set_property('v', as_numeric_array(val, dtype=np.float64, shape=(-1, 3), allow_none=True, empty_as_none=True))

@setter_property
def f(self, val):
# cached properties that are dependent on f
self._clear_cached_properties('faces_per_edge', 'vertices_per_edge', 'vertices_to_edges_matrix', 'vertices_to_edges_matrix_single_axis')
self.__dict__['f'] = as_numeric_array(val, dtype=np.uint64, shape=(-1, 3), allow_none=True, empty_as_none=True)
self._set_property('f', as_numeric_array(val, dtype=np.uint64, shape=(-1, 3), allow_none=True, empty_as_none=True))

@setter_property
def f4(self, val):
self.__dict__['f4'] = as_numeric_array(val, dtype=np.uint64, shape=(-1, 4), allow_none=True, empty_as_none=True)
self._set_property('f4', as_numeric_array(val, dtype=np.uint64, shape=(-1, 4), allow_none=True, empty_as_none=True))

@setter_property
def fc(self, val):
val = color.colors_like(val, self.f)
self.__dict__['fc'] = val
self._set_property('fc', color.colors_like(val, self.f))

@setter_property
def fc4(self, val):
self._set_property('fc4', color.colors_like(val, self.f4))

@setter_property
def vc(self, val):
val = color.colors_like(val, self.v)
self.__dict__['vc'] = val
self._set_property('vc', color.colors_like(val, self.v))

@setter_property
def fn(self, val):
Expand All @@ -184,27 +221,35 @@ def fn(self, val):
Someday we should refactor things so that we have seperate fn (face normal vectors) & fvn (face vertex normal indicies).
Today is not that day.
'''
self.__dict__['fn'] = as_numeric_array(val, dtype=np.float64, shape=(-1, 3), allow_none=True, empty_as_none=True)
self._set_property('fn', as_numeric_array(val, dtype=np.float64, shape=(-1, 3), allow_none=True, empty_as_none=True))

@setter_property
def vn(self, val):
self.__dict__['vn'] = as_numeric_array(val, dtype=np.float64, shape=(-1, 3), allow_none=True, empty_as_none=True)
self._set_property('vn', as_numeric_array(val, dtype=np.float64, shape=(-1, 3), allow_none=True, empty_as_none=True))

@setter_property
def ft(self, val):
self.__dict__['ft'] = as_numeric_array(val, dtype=np.uint32, shape=(-1, 3), allow_none=True, empty_as_none=True)
self._set_property('ft', as_numeric_array(val, dtype=np.uint32, shape=(-1, 3), allow_none=True, empty_as_none=True))

@setter_property
def ft4(self, val):
self._set_property('ft4', as_numeric_array(val, dtype=np.uint32, shape=(-1, 4), allow_none=True, empty_as_none=True))

@setter_property
def fn4(self, val):
self._set_property('fn4', as_numeric_array(val, dtype=np.uint32, shape=(-1, 4), allow_none=True, empty_as_none=True))

@setter_property
def vt(self, val):
self.__dict__['vt'] = as_numeric_array(val, dtype=np.float64, shape=(-1, 2), allow_none=True, empty_as_none=True)
self._set_property('vt', as_numeric_array(val, dtype=np.float64, shape=(-1, 2), allow_none=True, empty_as_none=True))

@setter_property
def e(self, val):
self.__dict__['e'] = as_numeric_array(val, dtype=np.uint64, shape=(-1, 2), allow_none=True, empty_as_none=True)
self._set_property('e', as_numeric_array(val, dtype=np.uint64, shape=(-1, 2), allow_none=True, empty_as_none=True))

@setter_property
def segm(self, val):
self.__dict__['segm'] = val
self._set_property('segm', val)

def _clear_cached_properties(self, *keys):
for cached_property_key in keys:
Expand All @@ -221,16 +266,23 @@ def concatenate(cls, *args):
if nargs == 1:
return args[0]

with_quads = args[0].with_quads
if any([a.with_quads != with_quads for a in args]):
raise ValueError('Expected `with_quads` to match for all args.')

vs = [a.v for a in args if a.v is not None]
vcs = [a.vc for a in args if a.vc is not None]
fs = [a.f for a in args if a.f is not None]
f4s = [a.f4 for a in args if a.f4 is not None]

if vs and len(vs) != nargs:
raise ValueError('Expected `v` for all args or none.')
if vcs and len(vcs) != nargs:
raise ValueError('Expected `vc` for all args or none.')
if fs and len(fs) != nargs:
raise ValueError('Expected `f` for all args or none.')
if f4s and len(f4s) != nargs:
raise ValueError('Expected `f4` for all args or none.')

# Offset face indices by the cumulative vertex count.
face_offsets = np.cumsum([v.shape[0] for v in vs[:-1]])
Expand All @@ -241,4 +293,5 @@ def concatenate(cls, *args):
v=np.vstack(vs) if vs else None,
vc=np.vstack(vcs) if vcs else None,
f=np.vstack(fs) if fs else None,
with_quads=with_quads
)
10 changes: 6 additions & 4 deletions lace/serialization/mesh.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
def load(f, existing_mesh=None):
def load(f, existing_mesh=None, with_quads=False):
from baiji.serialization.util.openlib import ensure_file_open_and_call
return ensure_file_open_and_call(f, _load, mode='rb', existing_mesh=existing_mesh)

def _load(f, existing_mesh=None):
def _load(f, existing_mesh=None, with_quads=False):
import os
from lace.serialization import ply, obj, wrl, stl, xyz, bsf
ext = os.path.splitext(f.name)[1].lower()
if ext == ".obj":
return obj.load(f, existing_mesh=existing_mesh, with_quads=with_quads)
if with_quads:
raise ValueError('Only the OBJ loader supports with_quads=True')
if ext == ".ply":
return ply.load(f, existing_mesh=existing_mesh)
elif ext == ".obj":
return obj.load(f, existing_mesh=existing_mesh)
elif ext == ".wrl":
return wrl.load(f, existing_mesh=existing_mesh)
elif ext == ".ylp":
Expand Down
40 changes: 32 additions & 8 deletions lace/serialization/obj/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

EXTENSION = '.obj'

def load(f, existing_mesh=None):
def load(f, existing_mesh=None, with_quads=False):
from baiji.serialization.util.openlib import ensure_file_open_and_call
return ensure_file_open_and_call(f, _load, mode='rb', mesh=existing_mesh)
return ensure_file_open_and_call(f, _load, mode='rb', mesh=existing_mesh, with_quads=with_quads)

def dump(obj, f, flip_faces=False, ungroup=False, comments=None,
copyright=False, split_normals=False, write_mtl=True): # pylint: disable=redefined-outer-name, redefined-builtin, unused-argument
Expand All @@ -15,16 +15,32 @@ def dump(obj, f, flip_faces=False, ungroup=False, comments=None,
ungroup=ungroup, comments=comments,
split_normals=split_normals, write_mtl=write_mtl)

def _load(fd, mesh=None):
def _load(fd, mesh=None, with_quads=False):
from collections import OrderedDict
from baiji import s3
from lace.mesh import Mesh
from lace.cache import sc
import lace.serialization.obj.objutils as objutils # pylint: disable=no-name-in-module

v, vt, vn, vc, f, ft, fn, mtl_path, landm, segm = objutils.read(fd.name)
v, vt, vn, vc, f, ft, fn, f4, ft4, fn4, mtl_path, landm, segm = objutils.read(fd.name)

if not mesh:
mesh = Mesh()
mesh = Mesh(with_quads=with_quads)

# When loading a quad mesh, the legacy behavior of the loader is to
# triangulate on load, filling f/ft/fn. Now it populates _both_ f/ft/fn
# and f4/ft4/fn4, and when with_quads=False, we want callers to get the
# original behavior, so we discard f4/ft4/fn4. When with_quads=True,
# we keep f4/ft4/fn4, discard f/ft/fn, and reject a triangular mesh.
if with_quads:
has_tris = [attr.size != 0 for attr in [f, ft, fn]]
has_quads = [attr.size != 0 for attr in [f4, ft4, fn4]]
if has_tris and not has_quads:
raise ValueError("Can't use with_quads=True to load a triangulated mesh")
f = ft = fn = np.empty((0, 3))
else:
f4 = ft4 = fn4 = np.empty((0, 4))

if v.size != 0:
mesh.v = v
if f.size != 0:
Expand All @@ -39,6 +55,12 @@ def _load(fd, mesh=None):
mesh.fn = fn
if ft.size != 0:
mesh.ft = ft
if f4.size != 0:
mesh.f4 = f4
if ft4.size != 0:
mesh.ft4 = ft4
if fn4.size != 0:
mesh.fn4 = fn4
if segm:
mesh.segm = OrderedDict([(k, v if isinstance(v, list) else v.tolist()) for k, v in segm.items()])
def path_relative_to_mesh(filename):
Expand Down Expand Up @@ -117,7 +139,9 @@ def write_face_to_obj_file(obj, faces, face_index, obj_file):
vertex_indices = faces[face_index][::ff] + 1

write_normals = obj.fn is not None or (obj.vn is not None and obj.vn.shape == obj.v.shape)
write_texture = obj.ft is not None and obj.vt is not None
write_texture = (obj.ft4 is not None or obj.ft is not None) and obj.vt is not None

texture_faces = obj.ft4 if obj.ft4 is not None else obj.ft

if write_normals and obj.fn is not None:
normal_indices = obj.fn[face_index][::ff] + 1
Expand All @@ -126,7 +150,7 @@ def write_face_to_obj_file(obj, faces, face_index, obj_file):
normal_indices = faces[face_index][::ff] + 1

if write_texture:
texture_indices = obj.ft[face_index][::ff] + 1
texture_indices = texture_faces[face_index][::ff] + 1
assert len(texture_indices) == len(vertex_indices)

# Valid obj face lines are: v, v/vt, v//vn, v/vt/vn
Expand Down Expand Up @@ -182,7 +206,7 @@ def write_face_to_obj_file(obj, faces, face_index, obj_file):
for r in obj.vn:
f.write('vn %f %f %f\n' % (r[0], r[1], r[2]))

if obj.ft is not None and obj.vt is not None:
if (obj.ft4 is not None or obj.ft is not None) and obj.vt is not None:
for r in obj.vt:
if len(r) == 3:
f.write('vt %f %f %f\n' % (r[0], r[1], r[2]))
Expand Down
Loading