#
##
## SPDX-FileCopyrightText: © 2007-2024 Benedict Verhegghe <bverheg@gmail.com>
## SPDX-License-Identifier: GPL-3.0-or-later
##
## This file is part of pyFormex 3.5 (Thu Feb 8 19:11:13 CET 2024)
## 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/.
##
"""Centerline.py
Determine the (inner) voronoi diagram of a triangulated surface.
Determine approximation for the centerline.
"""
import numpy as np
import pyformex as pf
from pyformex.connectivity import Connectivity
from pyformex.plugins import tetgen
[docs]def circumcenter(nodes, elems):
"""Calculate the circumcenters of a list of tetrahedrons.
For a description of the method:
http://mathworld.wolfram.com/Circumsphere.html
The output are the circumcenters and the corresponding radii.
"""
kwadSum = (nodes*nodes).sum(axis=-1)
one = np.zeros(nodes.shape[0])+1
nodesOne = np.append(nodes, one.reshape(-1, 1), 1)
nodesKwadOne = np.append(kwadSum.reshape(-1, 1), nodesOne, 1)
# construct necessary 4 by 4 arrays
Wx = nodesKwadOne[:, [0, 2, 3, 4]]
Wy = nodesKwadOne[:, [0, 1, 3, 4]]
Wz = nodesKwadOne[:, [0, 1, 2, 4]]
# calculate derminants of the 4 by 4 arrays
Dx = pf.at.det4(Wx[elems])
Dy = -pf.at.det4(Wy[elems])
Dz = pf.at.det4(Wz[elems])
alfa = pf.at.det4(nodesOne[elems[:]])
# circumcenters
centers = np.column_stack([Dx[:]/(2*alfa[:]), Dy[:]/(2*alfa[:]), Dz[:]/(2*alfa[:])])
# calculate radii of the circumscribed spheres
vec = centers[:]-nodes[elems[:, 0]]
radii = np.sqrt((vec*vec).sum(axis=-1))
return centers, radii
# Voronoi:
# vor diagram is determined using Tetgen. Some of the vor nodes may fall outside
# the surface. This should be avoided as this may compromise the centerline
# determination. Therefore, we created a second definition to determine the
# inner voronoi diagram (voronoiInner).
[docs]def voronoi(fn):
"""Determine the voronoi diagram of a triangulated surface.
fn is the file name of a surface, including the extension
(.off, .stl, .gts, .neu or .smesh)
The voronoi diagram is determined by Tetgen.
The output are the voronoi nodes and the corresponding radii
of the voronoi spheres.
"""
fn = pf.Path(fn)
S = pf.TriSurface.read(fn)
ftype = fn.lsuffix
if ftype != '.smesh':
fn = fn.with_suffix('.smesh')
S.write(fn)
_ = pf.command('tetgen -zpv %s' % fn)
# information tetrahedra
elems = tetgen.readEleFile(fn.with_suffix('.1.ele'))[0]
nodes = tetgen.readNodeFile(fn.with_suffix('.1.node'))[0]
# voronoi information
nodesVor = tetgen.readNodeFile(fn.with_suffix('.1.v.node'))[0]
# calculate the radii of the voronoi spheres
vec = nodesVor[:]-nodes[elems[:, 0]]
radii = np.sqrt((vec*vec).sum(axis=-1))
return nodesVor, radii
[docs]def voronoiInner(fn):
"""Determine the inner voronoi diagram of a triangulated surface.
fn is the file name of a surface, including the extension
(.off, .stl, .gts, .neu or .smesh)
The output are the voronoi nodes and the corresponding radii
of the voronoi spheres.
"""
fn = pf.Path(fn)
S = pf.TriSurface.read(fn)
ftype = fn.lsuffix
if ftype != '.smesh':
fn = fn.with_suffix('.smesh')
S.write(fn)
_ = pf.command('tetgen -zp %s' % fn)
# information tetrahedra
elems = tetgen.readEleFile(fn.with_suffix('.1.ele'))[0]
nodes = tetgen.readNodeFile(fn.with_suffix('.1.node'))[0]
# calculate surface normal for each point
elemsS = np.array(S.elems)
NT = S.normals()
NP = np.zeros([nodes.shape[0], 3])
for i in [0, 1, 2]:
NP[elemsS[:, i]] = NT
# calculate centrum circumsphere of each tetrahedron
centers = circumcenter(nodes, elems)[0]
# check if circumcenter falls within the geomety described by the surface
ie = np.column_stack([
((nodes[elems[:, j]] - centers[:])*NP[elems[:, j]]).sum(axis=-1)
for j in [0, 1, 2, 3]])
ie = ie[:, :] >= 0
w = np.where(ie.all(1))[0]
elemsInner = elems[w]
nodesVorInner = centers[w]
# calculate the radii of the voronoi spheres
vec = nodesVorInner[:]-nodes[elemsInner[:, 0]]
radii = np.sqrt((vec*vec).sum(axis=-1))
return nodesVorInner, radii
[docs]def selectMaxVor(nodesVor, radii, r1=1., r2=2., q=0.7, maxruns=-1):
"""Select the local maxima of the voronoi spheres.
Procedure:
1. The largest voronoi sphere in the record is selected (voronoi node N and
radius R).
2. All the voronoi nodes laying within a cube all deleted from the record.
This cube is defined by:
- the centrum of the cube N.
- the edge length which is 2*r1*R.
3. Some voronoi nodes laying within a 2nd, larger cube are also deleted.
This is when their corresponding radius is smaller than q times R.
This cube is defined by:
- the centrum of the cube N.
- the edge length which is 2*r2*R.
4. These three operations are repeated until all nodes are deleted.
"""
nodesCent = np.array([])
radCent = np.array([])
run = 0
while nodesVor.shape[0] and (maxruns < 0 or run < maxruns):
# find maximum voronoi sphere in the record
w = np.where(radii[:] == radii[:].max())[0]
maxR = radii[w].reshape(-1)
maxP = nodesVor[w].reshape(-1)
# remove all the nodes within the first cube
t1 = (nodesVor[:] > (maxP-r1*maxR)).all(axis=1)
t2 = (nodesVor[:] < (maxP+r1*maxR)).all(axis=1)
ttot1 = t1*t2
radii = radii[~ttot1]
nodesVor = nodesVor[~ttot1]
# remove some of the nodes within the second cube
t3 = (nodesVor[:] > (maxP-r2*maxR)).all(axis=1)
t4 = (nodesVor[:] < (maxP+r2*maxR)).all(axis=1)
t5 = (radii < maxR*q)
ttot2 = t3*t4*t5
if ttot2.shape[0]:
radii = radii[~ttot2]
nodesVor = nodesVor[~ttot2]
# add local maximum to a list
nodesCent = np.append(nodesCent, maxP)
radCent = np.append(radCent, maxR)
run += 1
return nodesCent.reshape(-1, 1, 3), radCent
[docs]def connectVorNodes(nodes, radii):
"""Create connections between the voronoi nodes.
Each of the nodes is connected with its closest neighbours.
The input is an array of n nodes and an array of n corresponding radii.
Two voronoi nodes are connected if the distance between these two nodes
is smaller than the sum of their corresponding radii.
The output is an array containing the connectivity information.
"""
connections = np.array([]).astype(int)
v = 4
for i in range(nodes.shape[0]):
t1 = (nodes[:] > (nodes[i]-v*radii[i])).all(axis=2)
t2 = (nodes[:] < (nodes[i]+v*radii[i])).all(axis=2)
t = t1*t2
t[i] = False
w1 = np.where(t == 1)[0]
c = pf.Coords(nodes[w1])
d = c.distanceFromPoint(nodes[i]).reshape(-1)
w2 = d < radii[w1] + radii[i]
w = w1[w2]
for j in w:
connections = np.append(connections, i)
connections = np.append(connections, j)
connections = Connectivity(connections.reshape(-1, 2), eltype='line2')
return connections.removeDuplicate()
[docs]def removeTriangles(elems):
"""Remove the triangles from the centerline.
This is a clean-up function for the centerline.
Triangles appearing in the centerline are removed by this function.
Both input and output are the connectivity of the centerline.
"""
rev = Connectivity(elems).inverse(expand=True)
if rev.shape[1] > 2:
w = np.where(rev[:, -3] != -1)[0]
for i in w:
el = rev[i].compress(rev[i] != -1)
u = np.unique(elems[el].reshape(-1))
NB = u.compress(u != i)
inter = np.intersect1d(w, NB)
if inter.shape[0] == 2:
tri = np.append(inter, i)
w1 = np.where(tri != tri.min())[0]
t = (elems[:, 0] == tri[w1[0]])*(elems[:, 1] == tri[w1[1]])
elems[t] = -1
w2 = np.where(elems[:, 0] != -1)[0]
return elems[w2]
[docs]def centerline(fn):
"""Determine approximated centerline of a triangulated surface.
fn is the file name of a surface, including the extension
(.off, .stl, .gts, .neu or .smesh)
The output are the centerline nodes, an array containing the connectivity
information and radii of the voronoi spheres.
"""
nodesVor, radii = voronoiInner(fn)
nodesC, radii = selectMaxVor(nodesVor, radii)
elemsC = connectVorNodes(nodesC, radii)
elemsC = removeTriangles(elemsC)
return nodesC, elemsC, radii
# End