Source code for lib.clust

#
##
##  SPDX-FileCopyrightText: © 2007-2023 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.4  (Thu Nov 16 18:07:39 CET 2023)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##
#

"""Point based clustering module.

"""
import numpy as np
from scipy import sparse

from pyformex import arraytools as at
from pyformex.trisurface import TriSurface
from pyformex.lib import clust_c


[docs]class Clustering(): """Uniform point clustering based on ACVD. Parameters ---------- mesh : TriSurface Notes ----- The algorithm is based on pyvista/pyacvd but is a reimplementation using pyFormex data types and techniques. """ def __init__(self, S, _opt=False): """Check inputs and initializes neighbors""" self.S = S self.clusters = None self.nclus = None self.remesh = None # Compute point weights and weighted points self._area, self._wcent = weighted_points(self.S) # neighbors and edges self._neigh, self._nneigh = neighbors_from_mesh(S) self._edges = S.edges.astype(at.Int) self._opt = _opt
[docs] def cluster(self, nclus, maxiter=100): """Cluster points """ clusters, ndisc = clust_c.cluster( self._neigh, self._nneigh, self._area, self._wcent, self._edges, nclus, maxiter) if clusters.min() < 0: raise ValueError("Cluster optimization failed: {ndisc} isolated clusters") clusters = at.renumberClusters(clusters) self.clusters = clusters self.nclus = clusters.max() + 1 return clusters, ndisc
[docs] def create_mesh(self, flipnorm=True): """ Generates mesh from clusters """ if flipnorm: cnorm = self.cluster_norm() else: cnorm = None # Generate mesh self.remesh = create_mesh(self.S, self._area, self.clusters, cnorm, flipnorm) return self.remesh
[docs] def cluster_norm(self): """ Return cluster norms """ if not hasattr(self, 'clusters'): raise Exception('No clusters available') # Normals of original mesh norm = self.S.avgVertexNormals() # Compute normalized mean cluster normals cnorm = np.empty((self.nclus, 3)) cnorm[:, 0] = np.bincount(self.clusters, weights=norm[:, 0] * self._area) cnorm[:, 1] = np.bincount(self.clusters, weights=norm[:, 1] * self._area) cnorm[:, 2] = np.bincount(self.clusters, weights=norm[:, 2] * self._area) weights = ((cnorm * cnorm).sum(1)**0.5).reshape((-1, 1)) weights[weights == 0] = 1 cnorm /= weights return cnorm
[docs]def cluster_centroid(cent, area, clusters): """ Computes an area normalized centroid for each cluster """ # Check if null cluster exists null_clusters = np.any(clusters == -1) if null_clusters: clusters = clusters.copy() clusters[clusters == -1] = clusters.max() + 1 wval = cent * area.reshape(-1, 1) cweight = np.bincount(clusters, weights=area) cweight[cweight == 0] = 1 cval = np.vstack((np.bincount(clusters, weights=wval[:, 0]), np.bincount(clusters, weights=wval[:, 1]), np.bincount(clusters, weights=wval[:, 2]))) / cweight if null_clusters: cval[:, -1] = np.inf return cval.T
[docs]def create_mesh(S, area, clusters, cnorm, flipnorm=True): """Generates a new TriSurface given cluster data Returns ------- TriSurface """ elems = S.elems points = S.coords if points.dtype != np.double: points = points.astype(np.double) # Compute centroids ccent = np.ascontiguousarray(cluster_centroid(points, area, clusters)) # Create sparse matrix storing the number of adjcent clusters a point has rng = np.arange(elems.shape[0]).reshape((-1, 1)) a = np.hstack((rng, rng, rng)).ravel() b = clusters[elems].ravel() # take? c = np.ones(len(a), dtype='bool') boolmatrix = sparse.csr_matrix((c, (a, b)), dtype='bool') # Find all points with three neighboring clusters. Each of the three # cluster neighbors becomes a point on a triangle nadjclus = boolmatrix.sum(1) adj = np.array(nadjclus == 3).nonzero()[0] idx = boolmatrix[adj].nonzero()[1] # Append these points and faces points = ccent f = idx.reshape((-1, 3)) # Remove duplicate faces f = f[unique_row_indices(np.sort(f, 1))] # Mean normals of clusters each face is build from if flipnorm: adjcnorm = cnorm[f].sum(1) adjcnorm /= np.linalg.norm(adjcnorm, axis=1).reshape(-1, 1) # and compare this with the normals of each face newnorm = TriSurface(points, f).normals() # print(f"newnorm {newnorm.shape}") # If the dot is negative, reverse the order of those faces agg = (adjcnorm * newnorm).sum(1) # dot product mask = agg < 0.0 f[mask] = f[mask, ::-1] return TriSurface(points,f)
[docs]def unique_row_indices(a): """ Indices of unique rows """ b = np.ascontiguousarray(a).view( np.dtype((np.void, a.dtype.itemsize * a.shape[1]))) _, idx = np.unique(b, return_index=True) return idx
[docs]def weighted_points(S): """Returns point weight based on area weight and weighted points. Points are weighted by adjcent area faces. Parameters ---------- S : TriSurface Triangular surface mesh. Returns ------- pweight : np.ndarray, np.double Point weight array wvertex : np.ndarray, np.double Vertices multiplied by their corresponding weights. """ areas = S.areas() pweight = at.nodalSum(areas, S.elems)[0] wvertex = S.coords*pweight return pweight.reshape(-1), wvertex
[docs]def neighbors_from_mesh(S): """Assemble neighbor array. Assumes all-triangular mesh. Parameters ---------- S : TriSurface TriSurface to assemble neighbors from. Returns ------- neigh : int np.ndarray [:, ::1] Indices of each neighboring node for each node. -1 entries are at the end! nneigh : int np.ndarray [::1] Number of neighbors for each node. """ adj = S.adjacency(kind='n') adj = np.require(adj[:,::-1], dtype=np.int32, requirements='C') nadj = (adj>=0).sum(-1) nadj = np.require(nadj, dtype=np.int32, requirements='C') return adj, nadj
[docs]def remesh_acvd(S, npoints=-1, ndiv=3): """Remesh a TriSurface using an ACDV clustering method Parameters ---------- S: TriSurface The TriSurface to be remeshed. npoints: int, optional The approximat number of vertices in the output mesh. If negative(default), it is set to the number of vertices in the input surface. ndiv: int, optional The number of subdivisions to created in order to have a finer mesh for the clustering method. A higher number results in a more regular mesh, at the expense of a longer computation time. Returns ------- TriSurface The remeshed TriSurface, resembling the input mesh, but having a more regular mesh. Note that if the input Mesh contains sharp folds, you may need to clean up the surface by calling :meth:`removeNonManifold` and/or :meth:`fixNormals`. Notes ----- This uses a clustering technique based on https://www.creatis.insa-lyon.fr/site7/en/acvd to resample the mesh. The actual implementation is a modification of https://github.com/pyvista/pyacvd, directly using pyFormex data structures instead of pyvista/vtk PolyData. The meaning of the ndiv paramter is different from that in the pyvista/pyacvd module. In pyFormex we can directly set the final number of subdivisions and the sub division is done in a single step. In pyvista/pyacvd one specifies a number of subdivision steps and each step subdivides in 2. Thus a value of nsub = 3 in pyvista/pyacvd corresponds to ndiv = 2^3 = 8 in pyFormex. pyFormex allows subdivision numbers that are not powers of two. This is not possible in pyvista/pyacvd. """ if npoints < 0: npoints = S.ncoords() if ndiv > 1: S = S.subdivide(ndiv).fuse().compact() print(f"Subdivide: {S.ncoords()} vertices") clus = Clustering(S) clus.cluster(npoints) return clus.create_mesh()
# End