def trim(self, points, radius): """ remove points too close to the cut curve. they dont add anything, and only lead to awkward faces """ #some precomputations tree = KDTree(points) cp = self.vertices[self.faces] normal = util.normalize(np.cross(cp[:,0], cp[:,1])) mid = util.normalize(cp.sum(axis=1)) diff = np.diff(cp, axis=1)[:,0,:] edge_radius = np.sqrt(util.dot(diff, diff)/4 + radius**2) index = np.ones(len(points), np.bool) #eliminate near edges def near_edge(e, p): return np.abs(np.dot(points[p]-mid[e], normal[e])) < radius for i,(p,r) in enumerate(izip(mid, edge_radius)): coarse = tree.query_ball_point(p, r) index[[c for c in coarse if near_edge(i, c)]] = 0 #eliminate near points for p in self.vertices: coarse = tree.query_ball_point(p, radius) index[coarse] = 0 return points[index]
def redraw(self): """ set coords and scalar data of roots """ hf = self.datamodel.heightfield hfl, hfh = hf.min(), hf.max() if hfh==hfl: hf[:] = 1 else: hf = (hf-hfl) / (hfh-hfl) #norm to 0-1 range hmin, hmax = 0.95, 1.05 radius = hf*(hmax-hmin)+hmin #precompute normal information if self.parent.mapping_height: if self.parent.vertex_normal: normals = self.complex.normals(radius) else: N = util.normalize(self.complex.geometry.primal) normals = np.array([np.dot( N, T) for T in self.group.transforms.reshape(-1,3,3) ]) if not self.parent.mapping_color: hf = np.ones_like(hf) for index,T in enumerate( self.root): for mirror,M in enumerate(T): _, source, mapper = M ## mapper.lookup_table = None #set positions B = self.group.basis[index,mirror,0] B = util.normalize(B.T).T PP = np.dot(B, self.complex.geometry.decomposed.T).T #primal coords transformed into current frame if self.parent.mapping_height: PP *= radius[:, index][:, None] source.mlab_source.set(points=PP) #set colors source.mlab_source.set(scalars=hf[:,index]) #set normals if self.parent.vertex_normal: M = self.group.transforms[mirror,0] source.data.point_data.normals = np.dot(M, normals[index,:,:].T).T source.data.cell_data.normals = None else: source.data.point_data.normals = None source.data.cell_data.normals = None source.mlab_source.update() for i in self.instances: i.property.representation = 'wireframe' if self.parent.wireframe else 'surface'
def redraw_control(self): """redraw control mesh""" edge, index = self.selected_edge cp = edge.edge.curve.controlpoints()[index] if not self.height_visible: cp = util.normalize(cp) self.control_points.mlab_source.reset(points=cp[1:-1]) cm = edge.edge.curve.controlmesh()[index] if not self.height_visible: cm = util.normalize(cm) self.control_mesh.mlab_source.reset(points=cm)
def __init__(self, hierarchy, points): """pick a set of worldcoords on the sphere Parameters ---------- hierarchy : list of MultiComplex points : ndarray, [n, 3], float Notes ----- hierarchical structure of the triangles allows finding intersection triangle quickly """ self.hierarchy = hierarchy self.points = util.normalize(np.atleast_2d(points)) complex = hierarchy[-1] group = complex.group domains, baries = group.find_support(self.points) #find out which fundamental domain tile each point is in local = np.dot(group.basis[0,0,0],baries.T).T #map all points to the root domain for computations; all domains have identical tesselation anyway # get the face index for each point faces = pick_primal_triangles(hierarchy, local) #calc baries; simple linear bary computation is good enough for these purposes, no? baries = np.einsum('ijk,ik->ij', complex.geometry.inverted_triangle[faces], local) self.baries = baries / baries.sum(axis=1)[:, None] self.raveled_indices = np.ravel_multi_index( (complex.topology.FV[faces].ravel(), np.repeat(domains[0],3)), complex.shape) self.complex = complex
def generate(group, levels): """ create geometry hierarchy from topology hierarchy and group and a pmd basis of its first fundamental domain """ T = topology.generate(levels) pmd = util.normalize( group.basis[0,0,0].T) _t = T[0] G = [Geometry(_t, pmd, None)] _g = G[0] for t in T[1:]: position, planes = _t.subdivide_position(_g.primal) _g = Geometry(t, position, planes) _t = t G.append(_g) #hook up parent-child relations in geometry list for parent,child in zip(G[:-1],G[1:]): parent.child = child child.parent = parent parent.transfer_operators() return G
def __init__(self, datamodel, points): """precomputations which are identical for all mappings""" self.datamodel = datamodel self.hierarchy = self.datamodel.hierarchy self.complex = self.hierarchy[-1] self.group = self.complex.group #cache the index a point is in. if it remains unchanged, no need to update count = len(points) self.index = np.zeros((3, count), np.int8) #precompute subtriangles primal = self.complex.geometry.primal[ self.complex.topology.FV] mid = util.normalize(np.roll(primal, +1, 1) + np.roll(primal, -1, 1)) dual = self.complex.geometry.dual basis = np.empty((self.complex.topology.D0, 3, 2, 3, 3)) for i in range(3): basis[:,i,0,0,:] = primal[:,i ,:] basis[:,i,1,0,:] = primal[:,i ,:] basis[:,i,0,1,:] = mid [:,i-2,:] basis[:,i,1,1,:] = mid [:,i-1,:] basis[:,:,:,2,:] = dual [:,None, None,:] #each subtri shares the dual vert self.subdomain = util.adjoint(basis) #precompute all we can for subdomain compu self.subdomain[:,:,1,:,:] *= -1 #flip sign self.subdomain = self.subdomain.reshape(-1,6,3,3) #fold sign axis self.update(points)
def add_base(index, mirror, B): """ build pipeline, for given basis """ B = util.normalize(B.T).T PP = np.dot(B, self.complex.geometry.decomposed.T).T #primal coords transformed into current frame x,y,z = PP.T FV = self.complex.topology.FV[:,::np.sign(np.linalg.det(B))] #reverse vertex order depending on orientation source = self.scene.mlab.pipeline.triangular_mesh_source(x,y,z, FV) #put these guys under a ui list l=lut_manager.tvtk.LookupTable() lut_manager.set_lut(l, lut_manager.pylab_luts['jet']) ## lut_manager.set_lut(l, lut_manager.pylab_luts.values()[0]) #add polydatamapper, to control color mapping and interpolation mapper = tvtk.PolyDataMapper(lookup_table=l) from tvtk.common import configure_input configure_input(mapper, source.outputs[0]) ## mapper = tvtk.PolyDataMapper(input=source.outputs[0]) mapper.interpolate_scalars_before_mapping = True mapper.immediate_mode_rendering = False return mirror, source, mapper
def compute_angles(self): """compute angles for each triangle-vertex""" edges = self.edges().reshape(-1, 3, 2) vecs = np.diff(self.vertices[edges], axis=2)[:, :, 0] vecs = util.normalize(vecs) angles = np.arccos(-util.dot(vecs[:, [1, 2, 0]], vecs[:, [2, 0, 1]])) assert np.allclose(angles.sum(axis=1), np.pi, rtol=1e-3) return angles
def from_pair(old, new): """ find minimal rotation that maps one direction unto the other """ axis = normalize( np.cross(old, new)) angle = np.arccos( np.dot(old, new) / np.linalg.norm(old) / np.linalg.norm(new)) return Quaternion._from_axis_angle(axis, angle)
def test_triangulation(): #random points on the sphere points = util.normalize(np.random.randn(10000,3)) #build curve. add sharp convex corners, as well as additional cuts N = 267#9 radius = np.cos( np.linspace(0,np.pi*2*12,N, False)) +1.1 curve = np.array([(np.cos(a)*r,np.sin(a)*r,1) for a,r in zip( np.linspace(0,np.pi*2,N, endpoint=False), radius)]) curve = np.append(curve, [[1,0,-4],[-1,0,-4]], axis=0) #add bottom slit curve = util.normalize(curve) curve = cg.Curve(curve) # print curve #do triangulation mesh, curve = cg.triangulate(points, curve) #add partitioning of points here too? partitions = mesh.partition(curve)
def dual_position(self): """calc dual coords from primal; interestingly, this is idential to computing a triangle normal""" #calc direction orthogonal to normal of intesecting plane diff = self.topology.T10 * self.primal #collect these on per-tri basis, including weights, so ordering is correct tri_edge = util.gather(self.topology.FEi, diff) * self.topology.FEs[:, :, None] #for above, could also do cyclical diff on grab(FV, primal) #dual vert les where three mid edge planes intesect self.dual = util.normalize(-util.null(tri_edge))
def redraw(self, reset=True): """only need to update the root datasource""" coords = self.edge.instantiate() for mirror, coord in zip(self.mirrors, coords): if not self.parent.height_visible: coord = util.normalize(coord) x,y,z = coord[0].T func = mirror.mlab_source.reset if reset else mirror.mlab_source.set func(x=x,y=y,z=z)
def compute_gradient(self, field): """compute gradient of scalar function on vertices on faces""" normals = self.face_normals() face_area = np.linalg.norm(normals, axis=1) normals = util.normalize(normals) edges = self.edges().reshape(-1, 3, 2) vecs = np.diff(self.vertices[edges], axis=2)[:, :, 0, :] gradient = (field[self.faces][:, :, None] * np.cross(normals[:, None, :], vecs)).sum(axis=1) return gradient / (2 * face_area[:, None])
def constrain(self, position): """ constrain bary coords, given world coords of zero transform point return constrained point in world coords """ B = self.group.basis[self.domain] B = B * self.get_constraint()[np.newaxis,:] #zero out deactived basis points bary = np.linalg.lstsq(B, position)[0] self.bary = self.normalize(bary) return normalize( np.dot(B, self.bary))
def test_sphere(): """some tests on a sphere""" vertices = util.normalize(np.random.normal(0, 1, (10000, 3))) mesh = cg.Mesh(vertices, scipy.spatial.ConvexHull(vertices).simplices) seed = np.zeros_like(mesh.vertices[:, 0]) seed[np.argmax(vertices[:, 0])] = 1 seed[np.argmax(vertices[:, 1])] = 1 distance = mesh.geodesic(seed) mesh.plot(color=np.cos(distance*10)) print(distance.max())
def triangulate(points, curve): """ return a triangulation of the pointset points, while being constrained by the boundary dicated by curve """ #test curve for self-intersection print 'testing curve for self-intersection' curve.self_intersect() #trim the pointset, to eliminate points co-linear with the cutting curve print 'trimming dataset' diff = np.diff(curve.vertices[curve.faces], axis=1)[:,0,:] length = np.linalg.norm(diff, axis=1) points = curve.trim(points, length.mean()/4) #refine curve iteratively. new points may both obsolete or require novel insertions themselves #so only do the most pressing ones first, then iterate to convergence while True: newcurve = curve.refine(points) if len(newcurve.vertices)==len(curve.vertices): break print 'curve refined' curve = newcurve """ we use the nifty property, that a convex hull of a sphere equals a delauney triangulation of its surface if we have cleverly refined our boundary curve, this trinagulation should also be 'constrained', in the sense of respecting that original boundary curve this is the most computationally expensive part of this function, but we should be done in a minute or so qhull performance; need 51 sec and 2.7gb for 4M points that corresponds to an icosahedron with level 8 subdivision; not too bad editor is very unresponsive at this level anyway """ print 'triangulating' allpoints = np.concatenate((curve.vertices, points)) #include origin; facilitates clipping hull = scipy.spatial.ConvexHull(util.normalize(allpoints)) triangles = hull.simplices #order faces coming from the convex hull print 'ordering faces' FP = util.gather(triangles, allpoints) mid = FP.sum(axis=1) normal = util.normals(FP) sign = util.dot(normal, mid) > 0 triangles = np.where(sign[:,None], triangles[:,::+1], triangles[:,::-1]) mesh = Mesh(allpoints, triangles) assert mesh.is_orientated() return mesh, curve
def refine(self, points): """ refine the contour such as to maintain it as a constrained boundary under triangulation using a convex hull this is really the crux of the method pursued in this module we need to 'shield off' any points that lie so close to the edge such as to threaten our constrained boundary by adding a split at the projection of the point on the line, for all vertices within the swept circle of the edge, we may guarantee that a subsequent convex hull of the sphere respects our original boundary """ allpoints = np.vstack((self.vertices, points)) tree = KDTree(allpoints) cp = util.gather(self.faces, self.vertices) normal = util.normalize(np.cross(cp[:,0], cp[:,1])) mid = util.normalize(cp.sum(axis=1)) diff = np.diff(cp, axis=1)[:,0,:] radius = np.linalg.norm(diff, axis=1) / 2 def insertion_point(e, c): """calculate insertion point""" coeff = np.dot( np.linalg.pinv(cp[e].T), allpoints[c]) coeff = coeff / coeff.sum() return coeff[0], np.dot(cp[e].T, coeff) #build new curves _curve_p = [c for c in self.vertices] _curve_idx = [] for e,(m,r,cidx) in enumerate(izip( mid, radius, self.faces)): try: d,ip = min( #codepath for use in iterative scheme; only insert the most balanced split; probably makes more awkward ones obsolete anyway [insertion_point(e,v) for v in tree.query_ball_point(m, r) if not v in cidx], key=lambda x:(x[0]-0.5)**2) #sort on distance from midpoint nidx = len(_curve_p) _curve_idx.append((cidx[0], nidx)) #attach on both ends _curve_idx.append((nidx, cidx[1])) _curve_p.append(ip) #append insertion point except: _curve_idx.append(cidx) #if edge is not split, just copy it return Curve(_curve_p, _curve_idx)
def self_intersect(self): """ test curve of arc-segments for intersection raises exception in case of intersection alternatively, we might resolve intersections by point insertion but this is unlikely to have any practical utility, and more likely to be annoying """ vertices = self.vertices faces = self.faces tree = KDTree(vertices) # curve points per edge, [n, 2, 3] cp = util.gather(faces, vertices) # normal rotating end unto start normal = util.normalize(np.cross(cp[:,0], cp[:,1])) # midpoints of edges; [n, 3] mid = util.normalize(cp.sum(axis=1)) # vector from end to start, [n, 3] diff = np.diff(cp, axis=1)[:,0,:] # radius of sphere needed to contain edge, [n] radius = np.linalg.norm(diff, axis=1) / 2 * 1.01 # FIXME: this can be vectorized by adapting pinv projector = [np.linalg.pinv(q) for q in np.swapaxes(cp, 1, 2)] # incident[vertex_index] gives a list of all indicent edge indices incident = npi.group_by(faces.flatten(), np.arange(faces.size)) def intersect(i,j): """test if spherical line segments intersect. bretty elegant""" intersection = np.cross(normal[i], normal[j]) #intersection direction of two great circles; sign may go either way though! return all(np.prod(np.dot(projector[e], intersection)) > 0 for e in (i,j)) #this direction must lie within the cone spanned by both sets of endpoints for ei,(p,r,cidx) in enumerate(izip(mid, radius, faces)): V = [v for v in tree.query_ball_point(p, r) if v not in cidx] edges = np.unique([ej for v in V for ej in incident[v]]) for ej in edges: if len(np.intersect1d(faces[ei], faces[ej])) == 0: #does not count if edges touch if intersect(ei, ej): raise Exception('The boundary curves intersect. Check your geometry and try again')
def subdivide_position(self, position): """calc primal coords from parent""" #one child for each parent vertex_vertex = position #each new vert lies at midpoint edge_vertex = util.normalize(self.edge_mid*vertex_vertex) #ordering convention is vertex-vertex + edge-vertex position = np.vstack((vertex_vertex, edge_vertex)) #calc subdivision planes central = util.gather(self.FEi, edge_vertex) planes = util.adjoint(central) return position, planes
def triangle_normals(complex, radius, index): """triangle normals for a root index. do for each index?""" group = complex.group geometry = complex.geometry topology = geometry.topology FV = topology.FV PP = geometry.decomposed B = group.basis[:,0,0] #grab all root bases b = util.normalize(B[index].T).T #now every row is a normalized vertex P = np.dot(b, PP.T).T * radius[:,index][:, None] #go from decomposed coords to local coordinate system fv = FV[:,::np.sign(np.linalg.det(b))] #flip sign for mirrored domains return np.cross(P[fv[:,1]]-P[fv[:,0]], P[fv[:,2]]-P[fv[:,0]])
def save_STL(filename, mesh): """save a mesh to plain stl. vertex ordering is assumed to be correct""" header = np.zeros(80, '<c') triangles = np.array(len(mesh.faces), '<u4') dtype = [('normal', '<f4', 3,),('vertex', '<f4', (3,3)), ('abc', '<u2', 1,)] data = np.empty(triangles, dtype) data['abc'] = 0 #standard stl cruft data['vertex'] = mesh.vertices[mesh.faces] data['normal'] = util.normalize(mesh.face_normals()) with open(filename, 'wb') as fh: header. tofile(fh) triangles.tofile(fh) data. tofile(fh)
def generate_trace(trace): """ convert picked points to well sampled trace of positions and weights """ edges = np.array( zip(trace[1:], trace[:-1])) edges = util.normalize(edges) samples = np.linspace(0, 1, 11) samples = (samples[1:] + samples[:-1])/2 weights = [] positions = [] for l,r in edges: L = np.linalg.norm(l-r) weights.extend([L]*len(samples)) positions.extend( l[None, :] * samples[:, None] + r[None, :] * (1-samples[:, None])) weights = np.array(weights) positions = np.array(positions)#.reshape((-1,3)) return positions, weights
def triangle_area_from_normals(*edge_planes): """compute spherical area from triplet of great circles Parameters ---------- edge_planes : 3 x ndarray, [n, 3], float edge normal vectors of great circles Returns ------- areas : ndarray, [n], float spherical area enclosed by the input planes """ edge_planes = [util.normalize(ep) for ep in edge_planes ] angles = [util.dot(edge_planes[p-2], edge_planes[p-1]) for p in xrange(3)] #a3 x [faces, c3] areas = sum(np.arccos(-a) for a in angles) - np.pi #faces return areas
def metric(self): """ calc metric properties and hodges; nicely vectorized """ topology = self.topology #metrics MP0 = np.ones (topology.P0) MP1 = np.zeros(topology.P1) MP2 = np.zeros(topology.P2) MD0 = np.ones (topology.D0) MD1 = np.zeros(topology.D1) MD2 = np.zeros(topology.D2) #precomputations EVP = util.gather(topology.EVi, self.primal) FEVP = util.gather(topology.FEi, EVP) #[faces, e3, v2, c3] FEM = util.normalize(FEVP.sum(axis=2)) FEV = util.gather(topology.FEi, topology.EVi) #calculate areas; devectorization over e makes things a little more elegant, by avoiding superfluous stacking for e in xrange(3): areas = triangle_area_from_corners(FEVP[:,e,0,:], FEVP[:,e,1,:], self.dual) MP2 += areas #add contribution to primal face util.scatter( #add contributions divided over left and right dual face FEV[:,e,:], #get both verts of each edge np.repeat(areas/2, 2), #half of domain area for both verts MD2) #calc edge lengths MP1 += edge_length(EVP[:,0,:], EVP[:,1,:]) for e in xrange(3): util.scatter( topology.FEi[:,e], edge_length(FEM[:,e,:], self.dual), MD1) #hodge operators self.D2P0 = MD2 / MP0 self.P0D2 = MP0 / MD2 self.D1P1 = MD1 / MP1 self.P1D1 = MP1 / MD1 self.D0P2 = MD0 / MP2 self.P2D0 = MP2 / MD0
def apply(self, tcr): """ apply the edge basis to the abstract subdivided curve output is mirror x rotations x subpoints x 3 """ t, c, r = tcr.T #length equal to subpoints t = t[None, None, :, None] c = c[None, None, :, None] r = r[None, None, :, None] L, R, C = self.edge.basis() #edges x 3 ## print L.shape, 'what?' L = L[:,:,None,:] R = R[:,:,None,:] C = C[:,:,None,:] P = L*t + R*(1-t) + C*c return normalize(P) * r
def generate_vertices(self, group): """instantiate a full sphere by repeating the transformed fundamental domain Returns ------- ndarray, [n, 3], float all points in the geometry, on a unit sphere """ points = np.empty((group.index, group.order, self.topology.P0, 3), np.float) PP = self.decomposed for i, B in enumerate(group.basis): for t, b in enumerate(B.reshape(-1, 3, 3)): b = util.normalize(b.T).T # now every row is a normalized vertex P = np.dot(b, PP.T).T # go from decomposed coords to local coordinate system points[i, t] = P # make single unique point list return npi.unique(points.reshape(-1, 3))
def add_base(mirror, B): """ build pipeline of mesh mappers for given basis """ B = util.normalize(B.T).T mirror = np.sign(np.linalg.det(B)) PP = np.dot(B, self.triangle.decomposed.T).T x,y,z = PP.T FV = self.triangle.topology.FV[:,::mirror] source = self.scene.mlab.pipeline.triangular_mesh_source(x,y,z, FV) source.data.point_data.normals = PP #perfect sphere #add polydatamapper, to control color mapping and interpolation; could add col based on mapper = tvtk.PolyDataMapper(input=source.outputs[0]) mapper.lookup_table = None mapper.scalar_visibility = False return mirror, source, mapper
def geodesic(self, seed, m=1): """Compute geodesic distance map Notes ----- http://www.multires.caltech.edu/pubs/GeodesicsInHeat.pdf """ laplacian = self.laplacian_vertex() mass = self.vertex_areas() t = self.edge_lengths().mean() ** 2 * m heat = lambda x : mass * x - laplacian * (x * t) operator = scipy.sparse.linalg.LinearOperator(shape=laplacian.shape, matvec=heat) diffused = scipy.sparse.linalg.minres(operator, seed.astype(np.float64), tol=1e-5)[0] gradient = -util.normalize(self.compute_gradient(diffused)) # self.plot(facevec=gradient) rhs = self.compute_divergence(gradient) phi = scipy.sparse.linalg.minres(laplacian, rhs)[0] return phi - phi.min()
def save_STL_complete(complex, radius, filename): """ split this as mesh_from_datamodel save a mesh to binary STL format the number of triangles grows quickly shapeway and solidworks tap out at a mere 1M and 20k triangles respectively... or 100k for sw surface """ data = np.empty((complex.group.index, complex.group.order, complex.topology.P2, 3, 3), np.float) #essence here is in exact transformations given by the basis trnasforms. this gives a guaranteed leak-free mesh PP = complex.geometry.decomposed FV = complex.topology.FV for i,B in enumerate(complex.group.basis): for t, b in enumerate(B.reshape(-1,3,3)): b = util.normalize(b.T).T #now every row is a normalized vertex P = np.dot(b, PP.T).T * radius[:,i][:, None] #go from decomposed coords to local coordinate system fv = FV[:,::np.sign(np.linalg.det(b))] data[i,t] = util.gather(fv, P) save_STL(filename, data.reshape(-1,3,3))
def redraw(self): """ reset height and related info (normals) """ radius = self.datamodel.heightfield if not self.parent.height_visible: radius = np.ones_like(radius) #precompute normal information if self.parent.vertex_normal: normals = self.complex.normals(radius) self.recolor() for index,T in enumerate( self.root): for mirror,M in enumerate(T): _, source, mapper = M ## mapper.lookup_table = None #set positions B = self.group.basis[index,mirror,0] B = util.normalize(B.T).T PP = np.dot(B, self.complex.geometry.decomposed.T).T #primal coords transformed into current frame if self.parent.mapping_height: PP *= radius[:, index][:, None] source.mlab_source.set(points=PP) #set normals if self.parent.vertex_normal: M = self.group.transforms[mirror,0] source.data.point_data.normals = np.dot(M, normals[index,:,:].T).T source.data.cell_data.normals = None else: source.data.point_data.normals = None source.data.cell_data.normals = None # needed to force a redraw of these elements source.mlab_source.update()