PyGEL3D 0.8.0__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygel3d/PyGEL.dll +0 -0
- pygel3d/__init__.py +389 -0
- pygel3d/experimental/__init__.py +1 -0
- pygel3d/experimental/hmesh.py +32 -0
- pygel3d/gl_display.py +127 -0
- pygel3d/graph.py +290 -0
- pygel3d/hmesh.py +1112 -0
- pygel3d/jupyter_display.py +91 -0
- pygel3d/spatial.py +40 -0
- pygel3d-0.8.0.dist-info/METADATA +159 -0
- pygel3d-0.8.0.dist-info/RECORD +14 -0
- pygel3d-0.8.0.dist-info/WHEEL +5 -0
- pygel3d-0.8.0.dist-info/licenses/LICENSE.md +27 -0
- pygel3d-0.8.0.dist-info/top_level.txt +1 -0
pygel3d/graph.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
""" This module provides a Graph class and functionality for skeletonization using graphs. """
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import ctypes as ct
|
|
4
|
+
from random import shuffle
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import Self
|
|
7
|
+
from numpy.typing import ArrayLike
|
|
8
|
+
# If TYPE_CHECKING is True, we import Manifold from hmesh for type hints.
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pygel3d.hmesh import Manifold
|
|
11
|
+
import numpy as np
|
|
12
|
+
from pygel3d import lib_py_gel, IntVector
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Graph:
|
|
16
|
+
""" This class is for representing graphs embedded in 3D. The class does not in
|
|
17
|
+
itself come with many features: it contains methods for creating, accessing, and
|
|
18
|
+
housekeeping. When vertices are used as parameters in the functions below, we usually
|
|
19
|
+
use the parameter name n (for node). n is simply an index (i.e. an integer) that
|
|
20
|
+
refers to a node (aka vertex)."""
|
|
21
|
+
def __init__(self,orig: Self|None=None):
|
|
22
|
+
if orig == None:
|
|
23
|
+
self.obj = lib_py_gel.Graph_new()
|
|
24
|
+
else:
|
|
25
|
+
self.obj = lib_py_gel.Graph_copy(orig.obj)
|
|
26
|
+
def __del__(self):
|
|
27
|
+
lib_py_gel.Graph_delete(self.obj)
|
|
28
|
+
def clear(self):
|
|
29
|
+
""" Clear the graph. """
|
|
30
|
+
lib_py_gel.Graph_clear(self.obj)
|
|
31
|
+
def cleanup(self):
|
|
32
|
+
""" Cleanup reorders the graph nodes such that there is no
|
|
33
|
+
gap in the index range. """
|
|
34
|
+
lib_py_gel.Graph_cleanup(self.obj)
|
|
35
|
+
def nodes(self) -> IntVector:
|
|
36
|
+
""" Get all nodes as an iterable range """
|
|
37
|
+
nodes = IntVector()
|
|
38
|
+
lib_py_gel.Graph_nodes(self.obj, nodes.obj)
|
|
39
|
+
return nodes
|
|
40
|
+
def neighbors(self, n: int, mode: str = 'n') -> IntVector:
|
|
41
|
+
""" Get the neighbors of node n. The final argument is either 'n' or 'e'. If it is 'n'
|
|
42
|
+
the function returns all neighboring nodes, and if it is 'e' it returns incident edges."""
|
|
43
|
+
nbors = IntVector()
|
|
44
|
+
lib_py_gel.Graph_neighbors(self.obj, n, nbors.obj, ct.c_char(mode.encode('ascii')))
|
|
45
|
+
return nbors
|
|
46
|
+
def positions(self) -> ArrayLike:
|
|
47
|
+
""" Get the vertex positions by reference. You can assign to the
|
|
48
|
+
positions. """
|
|
49
|
+
pos = ct.POINTER(ct.c_double)()
|
|
50
|
+
n = lib_py_gel.Graph_positions(self.obj, ct.byref(pos))
|
|
51
|
+
return np.ctypeslib.as_array(pos,(n,3))
|
|
52
|
+
def average_edge_length(self) -> float:
|
|
53
|
+
""" Returns the average edge length. """
|
|
54
|
+
ael = lib_py_gel.Graph_average_edge_length(self.obj)
|
|
55
|
+
return ael
|
|
56
|
+
def add_node(self, p: ArrayLike) -> int:
|
|
57
|
+
""" Adds node with position p to the graph and returns the
|
|
58
|
+
index of the new node. """
|
|
59
|
+
return lib_py_gel.Graph_add_node(self.obj, np.array(p))
|
|
60
|
+
def remove_node(self, n: int):
|
|
61
|
+
""" Removes the node n passed as argument. This does not change
|
|
62
|
+
any indices of other nodes, but n is then invalid. """
|
|
63
|
+
lib_py_gel.Graph_remove_node(self.obj, n)
|
|
64
|
+
def node_in_use(self, n: int) -> bool:
|
|
65
|
+
""" Checks if n is in_use. This function returns false both
|
|
66
|
+
if n has been removed and if n is an index outside the range of
|
|
67
|
+
indices that are used. """
|
|
68
|
+
return lib_py_gel.Graph_node_in_use(self.obj, n)
|
|
69
|
+
def connect_nodes(self, n0: int, n1: int) -> int:
|
|
70
|
+
""" Creates a new edge connecting nodes n0 and n1. The index of
|
|
71
|
+
the new edge is returned. """
|
|
72
|
+
return lib_py_gel.Graph_connect_nodes(self.obj, n0, n1)
|
|
73
|
+
def disconnect_nodes(self, n0: int, n1: int):
|
|
74
|
+
""" Disconnect nodes n0 and n1"""
|
|
75
|
+
lib_py_gel.Graph_disconnect_nodes(self.obj, n0, n1)
|
|
76
|
+
def merge_nodes(self, n0: int, n1: int, avg_pos: bool):
|
|
77
|
+
""" Merge nodes n0 and n1. avg_pos indicates if you want the position to be the average. """
|
|
78
|
+
lib_py_gel.Graph_merge_nodes(self.obj, n0, n1, avg_pos)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def from_mesh(m: Manifold) -> Graph:
|
|
82
|
+
""" Creates a graph from a mesh. The argument, m, is the input mesh,
|
|
83
|
+
and the function returns a graph with the same vertices and edges
|
|
84
|
+
as m."""
|
|
85
|
+
g = Graph()
|
|
86
|
+
lib_py_gel.graph_from_mesh(m.obj, g.obj)
|
|
87
|
+
return g
|
|
88
|
+
|
|
89
|
+
def load(fn: str) -> Graph | None:
|
|
90
|
+
""" Load a graph from a file. The argument, fn, is the filename which
|
|
91
|
+
is in a special format similar to Wavefront obj. The loaded graph is
|
|
92
|
+
returned by the function - or None if loading failed. """
|
|
93
|
+
s = ct.c_char_p(fn.encode('utf-8'))
|
|
94
|
+
g = Graph()
|
|
95
|
+
if lib_py_gel.graph_load(g.obj, s):
|
|
96
|
+
return g
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def save(fn: str, g: Graph) -> bool:
|
|
100
|
+
""" Save graph to a file. The first argument, fn, is the file name,
|
|
101
|
+
and g is the graph. This function returns True if saving happened and
|
|
102
|
+
False otherwise. """
|
|
103
|
+
s = ct.c_char_p(fn.encode('utf-8'))
|
|
104
|
+
return lib_py_gel.graph_save(g.obj, s)
|
|
105
|
+
|
|
106
|
+
def to_mesh_cyl(g: Graph, fudge: float = 0.0) -> Manifold:
|
|
107
|
+
""" Convert a graph to a cylindrical mesh.
|
|
108
|
+
|
|
109
|
+
This is a compatibility wrapper. Prefer ``hmesh.graph_to_cylinders``.
|
|
110
|
+
"""
|
|
111
|
+
from pygel3d.hmesh import graph_to_cylinders
|
|
112
|
+
return graph_to_cylinders(g, fudge)
|
|
113
|
+
|
|
114
|
+
def to_mesh_iso(g: Graph, fudge: float = 0.0, res: int = 256) -> Manifold:
|
|
115
|
+
""" Convert a graph to an isosurface mesh.
|
|
116
|
+
|
|
117
|
+
This is a compatibility wrapper. Prefer ``hmesh.graph_to_isosurface``.
|
|
118
|
+
"""
|
|
119
|
+
from pygel3d.hmesh import graph_to_isosurface
|
|
120
|
+
return graph_to_isosurface(g, fudge, res)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def smooth(g: Graph, num_iter: int = 1, alpha: float = 1.0):
|
|
124
|
+
""" Simple Laplacian smoothing of a graph. The first argument is the Graph, g, iter
|
|
125
|
+
is the number of iterations, and alpha is the weight. If the weight is high,
|
|
126
|
+
each iteration causes a lot of smoothing, and a high number of iterations
|
|
127
|
+
ensures that the effect of smoothing diffuses throughout the graph, i.e. that the
|
|
128
|
+
effect is more global than local. """
|
|
129
|
+
lib_py_gel.graph_smooth(g.obj, num_iter, alpha)
|
|
130
|
+
|
|
131
|
+
def edge_contract(g: Graph, dist_thresh: float) -> int:
|
|
132
|
+
""" Simplifies a graph by contracting edges. The first argument, g, is the graph,
|
|
133
|
+
and only edges shorter than dist_thresh are contracted. When an edge is contracted
|
|
134
|
+
the merged vertices are moved to the average of their former positions. Thus,
|
|
135
|
+
the ordering in which contractions are carried out matters. Hence, edges are
|
|
136
|
+
contracted in the order of increasing length and edges are only considered if
|
|
137
|
+
neither end point is the result of a contraction, but the process is then repeated
|
|
138
|
+
until no more contractions are possible. Returns total number of contractions. """
|
|
139
|
+
return lib_py_gel.graph_edge_contract(g.obj, dist_thresh)
|
|
140
|
+
|
|
141
|
+
def prune(g: Graph):
|
|
142
|
+
""" Prune leaves of a graph. The graph, g, is passed as the argument. This function
|
|
143
|
+
removes leaf nodes (valency 1) whose only neighbour has valency > 2. In practice
|
|
144
|
+
such isolated leaves are frequently spurious if the graph is a skeleton. Does not
|
|
145
|
+
return a value. """
|
|
146
|
+
lib_py_gel.graph_prune(g.obj)
|
|
147
|
+
|
|
148
|
+
def saturate(g: Graph, hops: int = 2, dist_frac: float = 1.001, rad: float = 1e300):
|
|
149
|
+
""" Saturate the graph with edges. This is not a complete saturation. Edges are
|
|
150
|
+
introduced between a vertex and other vertices that are reachable in hops steps, i.e.
|
|
151
|
+
hops-order neighbors. dist_frac and rad are parameters used to govern the precise
|
|
152
|
+
behaviour. Two nodes are only connected if their distance is less than rad and if
|
|
153
|
+
their distance is less than dist_frac times the length of the path along existing
|
|
154
|
+
edges in the graph. If dist_frac is at approximately 1 and rad is enormous, these
|
|
155
|
+
two parameters make no difference. """
|
|
156
|
+
lib_py_gel.graph_saturate(g.obj, hops, dist_frac, rad)
|
|
157
|
+
|
|
158
|
+
def LS_skeleton(g: Graph, sampling: bool = True) -> Graph:
|
|
159
|
+
""" Skeletonize a graph using the local separators approach. The first argument,
|
|
160
|
+
g, is the graph, and, sampling indicates whether we try to use all vertices
|
|
161
|
+
(False) as starting points for finding separators or just a sampling (True).
|
|
162
|
+
The function returns a new graph which is the skeleton of the input graph. """
|
|
163
|
+
skel = Graph()
|
|
164
|
+
mapping = IntVector()
|
|
165
|
+
lib_py_gel.graph_LS_skeleton(g.obj, skel.obj, mapping.obj, sampling)
|
|
166
|
+
return skel
|
|
167
|
+
|
|
168
|
+
def LS_skeleton_and_map(g: Graph, sampling: bool = True) -> tuple[Graph, IntVector]:
|
|
169
|
+
""" Skeletonize a graph using the local separators approach. The first argument,
|
|
170
|
+
g, is the graph, and, sampling indicates whether we try to use all vertices
|
|
171
|
+
(False) as starting points for finding separators or just a sampling (True).
|
|
172
|
+
The function returns a tuple containing a new graph which is the skeleton of
|
|
173
|
+
the input graph and a map from the graph nodes to the skeletal nodes. """
|
|
174
|
+
skel = Graph()
|
|
175
|
+
mapping = IntVector()
|
|
176
|
+
lib_py_gel.graph_LS_skeleton(g.obj, skel.obj, mapping.obj, sampling)
|
|
177
|
+
return skel, mapping
|
|
178
|
+
|
|
179
|
+
def MSLS_skeleton(g: Graph, grow_thresh: int = 64) -> Graph:
|
|
180
|
+
""" Skeletonize a graph using the multi-scale local separators approach. The first
|
|
181
|
+
argument, g, is the graph. grow_thresh controls how far a separator is allowed
|
|
182
|
+
to grow (larger is coarser and faster). The function returns a new graph which
|
|
183
|
+
is the skeleton of the input graph. """
|
|
184
|
+
skel = Graph()
|
|
185
|
+
mapping = IntVector()
|
|
186
|
+
lib_py_gel.graph_MSLS_skeleton(g.obj, skel.obj, mapping.obj, grow_thresh)
|
|
187
|
+
return skel
|
|
188
|
+
|
|
189
|
+
def MSLS_skeleton_and_map(g: Graph, grow_thresh: int = 64) -> tuple[Graph, IntVector]:
|
|
190
|
+
""" Skeletonize a graph using the multi-scale local separators approach. The first
|
|
191
|
+
argument, g, is the graph. grow_thresh controls how far a separator is allowed
|
|
192
|
+
to grow (larger is coarser and faster). The function returns a tuple containing
|
|
193
|
+
a new graph which is the skeleton of the input graph and a map from the graph
|
|
194
|
+
nodes to the skeletal nodes. """
|
|
195
|
+
skel = Graph()
|
|
196
|
+
mapping = IntVector()
|
|
197
|
+
lib_py_gel.graph_MSLS_skeleton(g.obj, skel.obj, mapping.obj, grow_thresh)
|
|
198
|
+
return skel, mapping
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def front_skeleton_and_map(g: Graph, colors: ArrayLike, intervals: int = 100) -> tuple[Graph, IntVector]:
|
|
202
|
+
""" Skeletonize a graph using the front separators approach. The first argument,
|
|
203
|
+
g, is the graph, and, colors is an nD array where each column contains a sequence
|
|
204
|
+
of floating point values - one for each node. We can have as many columns as needed
|
|
205
|
+
for the front separator computation. We can think of this as a coloring
|
|
206
|
+
of the nodes, hence the name. In practice, a coloring might just be the x-coordinate
|
|
207
|
+
of the nodes or some other function that indicates something about the structure of the
|
|
208
|
+
graph. The function returns a tuple containing a new graph which is the
|
|
209
|
+
skeleton of the input graph and a map from the graph nodes to the skeletal nodes. """
|
|
210
|
+
skel = Graph()
|
|
211
|
+
mapping = IntVector()
|
|
212
|
+
colors_flat = np.asarray(colors, dtype=ct.c_double, order='C')
|
|
213
|
+
N_col = 1 if len(colors_flat.shape)==1 else colors_flat.shape[1]
|
|
214
|
+
lib_py_gel.graph_front_skeleton(g.obj, skel.obj, mapping.obj, N_col, colors_flat.ctypes.data_as(ct.POINTER(ct.c_double)), intervals)
|
|
215
|
+
return skel, mapping
|
|
216
|
+
|
|
217
|
+
def front_skeleton(g: Graph, colors: ArrayLike, intervals: int = 100) -> Graph:
|
|
218
|
+
""" Skeletonize a graph using the front separators approach. The first argument,
|
|
219
|
+
g, is the graph, and, colors is a nD array where each column contains a sequence
|
|
220
|
+
of floating point values - one for each node. We can have as many columns as needed
|
|
221
|
+
for the front separator computation. We can think of this as a coloring
|
|
222
|
+
of the nodes, hence the name. In practice, a coloring might just be the x-coordinate
|
|
223
|
+
of the nodes or some other function that indicates something about the structure of the
|
|
224
|
+
graph. The function returns a new graph which is the skeleton of the input
|
|
225
|
+
graph. """
|
|
226
|
+
skel = Graph()
|
|
227
|
+
mapping = IntVector()
|
|
228
|
+
colors_flat = np.asarray(colors, dtype=ct.c_double, order='C')
|
|
229
|
+
N_col = 1 if len(colors_flat.shape)==1 else colors_flat.shape[1]
|
|
230
|
+
lib_py_gel.graph_front_skeleton(g.obj, skel.obj, mapping.obj, N_col, colors_flat.ctypes.data_as(ct.POINTER(ct.c_double)), intervals)
|
|
231
|
+
return skel
|
|
232
|
+
|
|
233
|
+
def combined_skeleton_and_map(g: Graph, colors: ArrayLike, intervals: int = 100) -> tuple[Graph, IntVector]:
|
|
234
|
+
""" Skeletonize a graph using both the front separators approach and the multi scale local separators.
|
|
235
|
+
The first argument, g, is the graph, and, colors is an nD array where each column contains a sequence
|
|
236
|
+
of floating point values - one for each node. We can have as many columns as needed
|
|
237
|
+
for the front separator computation. We can think of this as a coloring
|
|
238
|
+
of the nodes, hence the name. In practice, a coloring might just be the x-coordinate
|
|
239
|
+
of the nodes or some other function that indicates something about the structure of the
|
|
240
|
+
graph. The function returns a tuple containing a new graph which is the
|
|
241
|
+
skeleton of the input graph and a map from the graph nodes to the skeletal nodes. """
|
|
242
|
+
skel = Graph()
|
|
243
|
+
mapping = IntVector()
|
|
244
|
+
colors_flat = np.asarray(colors, dtype=ct.c_double, order='C')
|
|
245
|
+
N_col = 1 if len(colors_flat.shape)==1 else colors_flat.shape[1]
|
|
246
|
+
lib_py_gel.graph_combined_skeleton(g.obj, skel.obj, mapping.obj, N_col, colors_flat.ctypes.data_as(ct.POINTER(ct.c_double)), intervals)
|
|
247
|
+
return skel, mapping
|
|
248
|
+
|
|
249
|
+
def combined_skeleton(g: Graph, colors: ArrayLike, intervals: int = 100) -> Graph:
|
|
250
|
+
""" Skeletonize a graph using both the front separators approach and the multi scale local separators.
|
|
251
|
+
The first argument, g, is the graph, and, colors is an nD array where each column contains a sequence
|
|
252
|
+
of floating point values - one for each node. We can have as many columns as needed
|
|
253
|
+
for the front separator computation. We can think of this as a coloring
|
|
254
|
+
of the nodes, hence the name. In practice, a coloring might just be the x-coordinate
|
|
255
|
+
of the nodes or some other function that indicates something about the structure of the
|
|
256
|
+
graph. The function returns a new graph which is the skeleton of the input
|
|
257
|
+
graph. """
|
|
258
|
+
skel = Graph()
|
|
259
|
+
mapping = IntVector()
|
|
260
|
+
colors_flat = np.asarray(colors, dtype=ct.c_double, order='C')
|
|
261
|
+
N_col = 1 if len(colors_flat.shape)==1 else colors_flat.shape[1]
|
|
262
|
+
lib_py_gel.graph_combined_skeleton(g.obj, skel.obj, mapping.obj, N_col, colors_flat.ctypes.data_as(ct.POINTER(ct.c_double)), intervals)
|
|
263
|
+
return skel
|
|
264
|
+
|
|
265
|
+
def minimum_spanning_tree(g: Graph, root_node: int = 0) -> Graph:
|
|
266
|
+
""" Compute the minimum spanning tree of g using Prim's algorithm.
|
|
267
|
+
The second argument is the root node to start from. The spanning tree
|
|
268
|
+
of the connected component containing the root node is returned. """
|
|
269
|
+
mst = Graph()
|
|
270
|
+
lib_py_gel.graph_minimum_spanning_tree(g.obj, mst.obj, root_node)
|
|
271
|
+
return mst
|
|
272
|
+
|
|
273
|
+
def close_chordless_cycles(g: Graph, node: int = None, hops: int = 5, rad: float = None):
|
|
274
|
+
""" This function closes chordless cycles. A chordless cycle is a
|
|
275
|
+
cycle in a graph such that two nodes that belong to the cycle are
|
|
276
|
+
not connected unless they are adjacent in the cycle. The first
|
|
277
|
+
argument is the graph, g, the second argument is the starting node.
|
|
278
|
+
If none is provided, the procedure is executed for all nodes. hops
|
|
279
|
+
indicates how far from the starting node we venture in the search
|
|
280
|
+
for cycles. Finally, rad (if provided) indicates how far away the
|
|
281
|
+
farthest node in the cycle is allowed to be."""
|
|
282
|
+
if rad is None:
|
|
283
|
+
rad = g.average_edge_length()
|
|
284
|
+
if node is None:
|
|
285
|
+
l = list(g.nodes())
|
|
286
|
+
shuffle(l)
|
|
287
|
+
for n in l:
|
|
288
|
+
lib_py_gel.graph_close_chordless_cycles(g.obj, n, hops, rad)
|
|
289
|
+
else:
|
|
290
|
+
lib_py_gel.graph_close_chordless_cycles(g.obj, node, hops, rad)
|