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/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)