sgtlib 3.3.9__py3-none-any.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.
Files changed (72) hide show
  1. StructuralGT/__init__.py +31 -0
  2. StructuralGT/apps/__init__.py +0 -0
  3. StructuralGT/apps/cli_main.py +258 -0
  4. StructuralGT/apps/gui_main.py +69 -0
  5. StructuralGT/apps/gui_mcw/__init__.py +0 -0
  6. StructuralGT/apps/gui_mcw/checkbox_model.py +91 -0
  7. StructuralGT/apps/gui_mcw/controller.py +1073 -0
  8. StructuralGT/apps/gui_mcw/image_provider.py +74 -0
  9. StructuralGT/apps/gui_mcw/imagegrid_model.py +75 -0
  10. StructuralGT/apps/gui_mcw/qthread_worker.py +102 -0
  11. StructuralGT/apps/gui_mcw/table_model.py +79 -0
  12. StructuralGT/apps/gui_mcw/tree_model.py +154 -0
  13. StructuralGT/apps/sgt_qml/CenterMainContent.qml +19 -0
  14. StructuralGT/apps/sgt_qml/LeftContent.qml +48 -0
  15. StructuralGT/apps/sgt_qml/MainWindow.qml +762 -0
  16. StructuralGT/apps/sgt_qml/RightLoggingPanel.qml +125 -0
  17. StructuralGT/apps/sgt_qml/assets/icons/.DS_Store +0 -0
  18. StructuralGT/apps/sgt_qml/assets/icons/back_icon.png +0 -0
  19. StructuralGT/apps/sgt_qml/assets/icons/brightness_icon.png +0 -0
  20. StructuralGT/apps/sgt_qml/assets/icons/cancel_icon.png +0 -0
  21. StructuralGT/apps/sgt_qml/assets/icons/crop_icon.png +0 -0
  22. StructuralGT/apps/sgt_qml/assets/icons/edit_icon.png +0 -0
  23. StructuralGT/apps/sgt_qml/assets/icons/graph_icon.png +0 -0
  24. StructuralGT/apps/sgt_qml/assets/icons/hide_panel.png +0 -0
  25. StructuralGT/apps/sgt_qml/assets/icons/next_icon.png +0 -0
  26. StructuralGT/apps/sgt_qml/assets/icons/notify_icon.png +0 -0
  27. StructuralGT/apps/sgt_qml/assets/icons/rescale_icon.png +0 -0
  28. StructuralGT/apps/sgt_qml/assets/icons/show_panel.png +0 -0
  29. StructuralGT/apps/sgt_qml/assets/icons/square_icon.png +0 -0
  30. StructuralGT/apps/sgt_qml/assets/icons/undo_icon.png +0 -0
  31. StructuralGT/apps/sgt_qml/components/ImageFilters.qml +82 -0
  32. StructuralGT/apps/sgt_qml/components/ImageProperties.qml +112 -0
  33. StructuralGT/apps/sgt_qml/components/ProjectNav.qml +127 -0
  34. StructuralGT/apps/sgt_qml/widgets/BinaryFilterWidget.qml +151 -0
  35. StructuralGT/apps/sgt_qml/widgets/BrightnessControlWidget.qml +103 -0
  36. StructuralGT/apps/sgt_qml/widgets/CreateProjectWidget.qml +112 -0
  37. StructuralGT/apps/sgt_qml/widgets/GTWidget.qml +94 -0
  38. StructuralGT/apps/sgt_qml/widgets/GraphComputeWidget.qml +77 -0
  39. StructuralGT/apps/sgt_qml/widgets/GraphExtractWidget.qml +175 -0
  40. StructuralGT/apps/sgt_qml/widgets/GraphPropertyWidget.qml +77 -0
  41. StructuralGT/apps/sgt_qml/widgets/ImageFilterWidget.qml +137 -0
  42. StructuralGT/apps/sgt_qml/widgets/ImagePropertyWidget.qml +78 -0
  43. StructuralGT/apps/sgt_qml/widgets/ImageViewWidget.qml +585 -0
  44. StructuralGT/apps/sgt_qml/widgets/MenuBarWidget.qml +137 -0
  45. StructuralGT/apps/sgt_qml/widgets/MicroscopyPropertyWidget.qml +80 -0
  46. StructuralGT/apps/sgt_qml/widgets/ProjectWidget.qml +141 -0
  47. StructuralGT/apps/sgt_qml/widgets/RescaleControlWidget.qml +83 -0
  48. StructuralGT/apps/sgt_qml/widgets/RibbonWidget.qml +406 -0
  49. StructuralGT/apps/sgt_qml/widgets/StatusBarWidget.qml +173 -0
  50. StructuralGT/compute/__init__.py +0 -0
  51. StructuralGT/compute/c_lang/include/sgt_base.h +21 -0
  52. StructuralGT/compute/graph_analyzer.py +1499 -0
  53. StructuralGT/entrypoints.py +49 -0
  54. StructuralGT/imaging/__init__.py +0 -0
  55. StructuralGT/imaging/base_image.py +403 -0
  56. StructuralGT/imaging/image_processor.py +780 -0
  57. StructuralGT/modules.py +29 -0
  58. StructuralGT/networks/__init__.py +0 -0
  59. StructuralGT/networks/fiber_network.py +490 -0
  60. StructuralGT/networks/graph_skeleton.py +425 -0
  61. StructuralGT/networks/sknw_mod.py +199 -0
  62. StructuralGT/utils/__init__.py +0 -0
  63. StructuralGT/utils/config_loader.py +244 -0
  64. StructuralGT/utils/configs.ini +97 -0
  65. StructuralGT/utils/progress_update.py +67 -0
  66. StructuralGT/utils/sgt_utils.py +291 -0
  67. sgtlib-3.3.9.dist-info/METADATA +789 -0
  68. sgtlib-3.3.9.dist-info/RECORD +72 -0
  69. sgtlib-3.3.9.dist-info/WHEEL +5 -0
  70. sgtlib-3.3.9.dist-info/entry_points.txt +3 -0
  71. sgtlib-3.3.9.dist-info/licenses/LICENSE +674 -0
  72. sgtlib-3.3.9.dist-info/top_level.txt +1 -0
@@ -0,0 +1,425 @@
1
+ # SPDX-License-Identifier: GNU GPL v3
2
+
3
+ """
4
+ Create a graph skeleton from an image binary
5
+ """
6
+
7
+ import math
8
+ import numpy as np
9
+ from scipy import ndimage
10
+ from cv2.typing import MatLike
11
+ from skimage.morphology import binary_dilation as dilate, binary_closing
12
+ from skimage.morphology import disk, skeletonize, remove_small_objects
13
+
14
+
15
+ class GraphSkeleton:
16
+ """A class that is used for estimating the width of edges and compute their weights using binerized 2D/3D images."""
17
+
18
+ def __init__(self, img_bin: MatLike, configs: dict = None, is_2d: bool = True, progress_func = None):
19
+ """
20
+ A class that builds a skeleton graph from an image.
21
+ The skeleton will be 3D so that it can be analyzed with OVITO
22
+
23
+ :param img_bin: OpenCV image in binary format.
24
+ :param configs: Options and parameters.
25
+
26
+ >>> import cv2
27
+ >>> import numpy
28
+ >>> opt_gte = {}
29
+ >>> opt_gte["merge_nearby_nodes"]["value"] = 1
30
+ >>> opt_gte["remove_disconnected_segments"]["value"] = 1
31
+ >>> opt_gte["remove_object_size"]["value"] = 500
32
+ >>> opt_gte["prune_dangling_edges"]["value"] = 1
33
+ >>> dummy_image = 127 * numpy.ones((40, 40), dtype = np.uint8)
34
+ >>> img = cv2.threshold(dummy_image, 127, 255, cv2.THRESH_BINARY)[1]
35
+ >>> graph_skel = GraphSkeleton(img, opt_gte)
36
+
37
+ """
38
+ self.img_bin = img_bin
39
+ self.configs = configs
40
+ self.is_2d = is_2d
41
+ self.update_progress = progress_func
42
+ self.skeleton, self.skeleton_3d = None, None
43
+ if configs is not None:
44
+ self._build_skeleton()
45
+
46
+ def _build_skeleton(self):
47
+ """
48
+ Creates a graph skeleton of the image.
49
+
50
+ :return:
51
+ """
52
+
53
+ # rebuilding the binary image as a boolean for skeletonizing
54
+ self.img_bin = np.squeeze(self.img_bin)
55
+ img_bin_int = np.asarray(self.img_bin, dtype=np.uint16)
56
+
57
+ # making the initial skeleton image
58
+ temp_skeleton = skeletonize(img_bin_int)
59
+
60
+ # Use medial axis with distance transform
61
+ # skeleton, distance = medial_axis(img_bin_int, return_distance=True)
62
+ # Scale thickness by distance (optional)
63
+ # temp_skeleton = skeleton * distance
64
+
65
+ # if self.configs["remove_bubbles"]["value"] == 1:
66
+ # temp_skeleton = GraphSkeleton.remove_bubbles(temp_skeleton, img_bin_int, mask_elements)
67
+ # if self.update_progress is not None:
68
+ # self.update_progress([56, f"Ran remove_bubbles for image skeleton..."])
69
+
70
+ if self.configs["merge_nearby_nodes"]["value"] == 1:
71
+ node_radius_size = 2 # int(self.configs["merge_nearby_nodes"]["items"][0]["value"])
72
+ temp_skeleton = GraphSkeleton.merge_nodes(temp_skeleton, node_radius_size)
73
+ if self.update_progress is not None:
74
+ self.update_progress([52, f"Ran merge_nodes for image skeleton..."])
75
+
76
+ if self.configs["remove_disconnected_segments"]["value"] == 1:
77
+ min_size = int(self.configs["remove_disconnected_segments"]["items"][0]["value"])
78
+ temp_skeleton = remove_small_objects(temp_skeleton, min_size=min_size, connectivity=2)
79
+ if self.update_progress is not None:
80
+ self.update_progress([54, f"Ran remove_small_objects for image skeleton..."])
81
+
82
+ if self.configs["prune_dangling_edges"]["value"] == 1:
83
+ max_iter = 500 # int(self.configs["prune_dangling_edges"]["items"][0]["value"])
84
+ b_points = GraphSkeleton.get_branched_points(temp_skeleton)
85
+ temp_skeleton = GraphSkeleton.prune_edges(temp_skeleton, max_iter, b_points)
86
+ if self.update_progress is not None:
87
+ self.update_progress([56, f"Ran prune_dangling_edges for image skeleton..."])
88
+
89
+ self.skeleton = np.asarray(temp_skeleton, dtype=np.uint16)
90
+ # self.skeleton = self.skeleton.astype(int)
91
+ self.skeleton_3d = np.asarray([self.skeleton]) if self.is_2d else self.skeleton
92
+
93
+ def assign_weights(self, edge_pts: MatLike, weight_type: str = None, weight_options: dict = None,
94
+ pixel_dim: float = 1, rho_dim: float = 1):
95
+ """
96
+ Compute and assign weights to a line edge between 2 nodes.
97
+
98
+ :param edge_pts: A list of pts that trace along a graph edge.
99
+ :param weight_type: Basis of computation for the weight (i.e., length, width, resistance, conductance, etc.)
100
+ :param weight_options: weight types to be used in computation of weights.
101
+ :param pixel_dim: Physical size of a single pixel width in nanometers.
102
+ :param rho_dim: The resistivity value of the material.
103
+ :return: Width pixel count of edge, computed weight.
104
+ """
105
+
106
+ # Initialize parameters: Idea copied from 'sknw' library
107
+ pix_length = np.linalg.norm(edge_pts[1:] - edge_pts[:-1], axis=1).sum()
108
+ epsilon = 0.001 # to avoid division by zero
109
+ pix_length += epsilon
110
+
111
+ if len(edge_pts) < 2:
112
+ # check to see if ge is an empty or unity list, if so, set pixel counts to 0
113
+ # Assume only 1/2 pixel exists between edge points
114
+ pix_width = 0.5
115
+ pix_angle = None
116
+ else:
117
+ # if ge exists, find the midpoint of the trace, and orthogonal unit vector
118
+ pix_width, pix_angle = self._estimate_edge_width(edge_pts)
119
+ pix_width += 0.5 # (normalization) to make it larger than empty widths
120
+
121
+ if weight_type is None:
122
+ wt = pix_width / 10
123
+ elif weight_options.get(weight_type) == weight_options.get('DIA'):
124
+ wt = pix_width * pixel_dim
125
+ elif weight_options.get(weight_type) == weight_options.get('AREA'):
126
+ wt = math.pi * (pix_width * pixel_dim * 0.5) ** 2
127
+ elif weight_options.get(weight_type) == weight_options.get('LEN') or weight_options.get(weight_type) == weight_options.get('INV_LEN'):
128
+ wt = pix_length * pixel_dim
129
+ if weight_options.get(weight_type) == weight_options.get('INV_LEN'):
130
+ wt = wt + epsilon if wt == 0 else wt
131
+ wt = wt ** -1
132
+ elif weight_options.get(weight_type) == weight_options.get('ANGLE'):
133
+ """
134
+ Edge angle centrality" in graph theory refers to a measure of an edge's importance within a network,
135
+ based on the angles formed between the edges connected to its endpoints, essentially assessing how "central"
136
+ an edge is in terms of its connection to other edges within the network, with edges forming more acute
137
+ angles generally considered more central.
138
+ To calculate edge angle centrality, you would typically:
139
+ 1. For each edge, identify the connected edges at its endpoints.
140
+ 2. Calculate the angles between these connected edges.
141
+ 3. Assign a higher centrality score to edges with smaller angles, indicating a more central position in the network structure.
142
+ """
143
+ sym_angle = np.minimum(pix_angle, (360 - pix_angle))
144
+ wt = (sym_angle + epsilon) ** -1
145
+ elif weight_options.get(weight_type) == weight_options.get('FIX_CON') or weight_options.get(weight_type) == weight_options.get('VAR_CON') or weight_options.get(weight_type) == weight_options.get('RES'):
146
+ # Varies with width
147
+ length = pix_length * pixel_dim
148
+ area = math.pi * (pix_width * pixel_dim * 0.5) ** 2
149
+ if weight_options.get(weight_type) == weight_options.get('FIX_CON'):
150
+ area = math.pi * (1 * pixel_dim) ** 2
151
+ num = length * rho_dim
152
+ area = area + epsilon if area == 0 else area
153
+ num = num + epsilon if num == 0 else num
154
+ wt = (num / area) # Resistance
155
+ if weight_options.get(weight_type) == weight_options.get('VAR_CON') or weight_options.get(weight_type) == weight_options.get('FIX_CON'):
156
+ wt = wt ** -1 # Conductance is inverse of resistance
157
+ else:
158
+ raise TypeError('Invalid weight type')
159
+ return pix_width, pix_angle, wt
160
+
161
+ def _estimate_edge_width(self, graph_edge_coords: MatLike):
162
+ """Estimates the edge width of a graph edge."""
163
+
164
+ def find_orthogonal(u, v):
165
+ # Inputs:
166
+ # u, v: two coordinates (x, y) or (x, y, z)
167
+ vec = u - v # find the vector between u and v
168
+
169
+ if np.linalg.norm(vec) == 0:
170
+ n = np.array([0, ] * len(u), dtype=np.float16)
171
+ else:
172
+ # make n a unit vector along u,v
173
+ n = vec / np.linalg.norm(vec)
174
+
175
+ hl = np.linalg.norm(vec) / 2 # find the half-length of the vector u,v
176
+ ortho_arr = np.random.randn(len(u)) # take a random vector
177
+ ortho_arr -= ortho_arr.dot(n) * n # make it orthogonal to vector u,v
178
+ ortho_arr /= np.linalg.norm(ortho_arr) # make it a unit vector
179
+
180
+ # Returns the coordinates of the vector u,v midpoint; the orthogonal unit vector
181
+ return (v + n * hl), ortho_arr
182
+
183
+ # 1. Estimate orthogonal and mid-point
184
+ end_index = len(graph_edge_coords) - 1
185
+ pt1 = graph_edge_coords[0]
186
+ pt2 = graph_edge_coords[end_index]
187
+ # mid_index = int(len(graph_edge_coords) / 2)
188
+ # mid_pt = graph_edge_coords[mid_index]
189
+
190
+ mid_pt, ortho = find_orthogonal(pt1, pt2)
191
+ # mid: the midpoint of a trace of an edge
192
+ # ortho: an orthogonal unit vector
193
+ mid_pt = mid_pt.astype(int)
194
+
195
+ # 2. Compute the angle in Radians
196
+ # Delta X and Y: Compute the difference in x and y coordinates:
197
+ dx = pt2[0] - pt1[0]
198
+ dy = pt2[1] - pt1[1]
199
+ # Angle Calculation: Use the arc-tangent function to get the angle in radians:
200
+ angle_rad = math.atan2(dy, dx)
201
+ angle_deg = math.degrees(angle_rad)
202
+ if angle_deg < 0:
203
+ angle_deg += 360
204
+
205
+ # 3. Estimate width
206
+ check = 0 # initializing boolean check
207
+ i = 0 # initializing iterative variable
208
+ l1 = np.nan
209
+ l2 = np.nan
210
+ while check == 0: # iteratively check along orthogonal vector to see if the coordinate is either...
211
+ pt_check = mid_pt + i * ortho # ... out of bounds, or no longer within the fiber in img_bin
212
+ pt_check = pt_check.astype(int)
213
+ is_in_edge = GraphSkeleton.point_check(self.img_bin, pt_check)
214
+
215
+ if is_in_edge:
216
+ edge = mid_pt + (i - 1) * ortho
217
+ edge = edge.astype(int)
218
+ l1 = edge # When the check indicates oob or black space, assign width to l1
219
+ check = 1
220
+ else:
221
+ i += 1
222
+
223
+ check = 0
224
+ i = 0
225
+ while check == 0: # Repeat, but following the negative orthogonal vector
226
+ pt_check = mid_pt - i * ortho
227
+ pt_check = pt_check.astype(int)
228
+ is_in_edge = GraphSkeleton.point_check(self.img_bin, pt_check)
229
+
230
+ if is_in_edge:
231
+ edge = mid_pt - (i - 1) * ortho
232
+ edge = edge.astype(int)
233
+ l2 = edge # When the check indicates oob or black space, assign width to l2
234
+ check = 1
235
+ else:
236
+ i += 1
237
+
238
+ # returns the length between l1 and l2, which is the width of the fiber associated with an edge, at its midpoint
239
+ edge_width = np.linalg.norm(l1 - l2)
240
+ return edge_width, angle_deg
241
+
242
+ @classmethod
243
+ def _generate_transformations(cls, pattern):
244
+ """
245
+ Generate common transformations for a pattern.
246
+
247
+ * flipud is flipping them up-down
248
+ * t_branch_2 is t_branch_0 transposed, which permutes it in all directions (might not be using that word right)
249
+ * t_branch_3 is t_branch_2 flipped left right
250
+ * those 3 functions are used to create all possible branches with just a few starting arrays below
251
+
252
+ :param pattern: Pattern of the box as a numpy array.
253
+
254
+ """
255
+ return [
256
+ pattern,
257
+ np.flipud(pattern),
258
+ np.fliplr(pattern),
259
+ np.fliplr(np.flipud(pattern)),
260
+ pattern.T,
261
+ np.flipud(pattern.T),
262
+ np.fliplr(pattern.T),
263
+ np.fliplr(np.flipud(pattern.T))
264
+ ]
265
+
266
+ @classmethod
267
+ def get_branched_points(cls, skeleton: MatLike):
268
+ """Identify and retrieve the branched points from the graph skeleton."""
269
+ skel_int = skeleton * 1
270
+
271
+ # Define base patterns
272
+ base_patterns = [
273
+ [[1, 0, 1], [0, 1, 0], [1, 0, 1]], # x_branch
274
+ [[0, 1, 0], [1, 1, 1], [0, 1, 0]], # x_branch variant
275
+ [[0, 0, 0], [1, 1, 1], [0, 1, 0]], # t_branch
276
+ [[1, 0, 1], [0, 1, 0], [1, 0, 0]], # t_branch variant
277
+ [[1, 0, 1], [0, 1, 0], [0, 1, 0]], # y_branch
278
+ [[0, 1, 0], [1, 1, 0], [0, 0, 1]], # y_branch variant
279
+ [[0, 1, 0], [1, 1, 0], [1, 0, 1]], # off_branch
280
+ [[0, 1, 1], [0, 1, 1], [1, 0, 0]], # clust_branch
281
+ [[1, 1, 1], [0, 1, 1], [1, 0, 0]], # clust_branch variant
282
+ [[1, 1, 1], [0, 1, 1], [1, 0, 1]], # clust_branch variant
283
+ [[1, 0, 0], [1, 1, 1], [0, 1, 0]] # cross_branch
284
+ ]
285
+
286
+ # Generate all transformations
287
+ all_patterns = []
288
+ for pattern in base_patterns:
289
+ all_patterns.extend(cls._generate_transformations(np.array(pattern)))
290
+
291
+ # Remove duplicate patterns (if any)
292
+ unique_patterns = []
293
+ for pattern in all_patterns:
294
+ if not any(np.array_equal(pattern, existing) for existing in unique_patterns):
295
+ unique_patterns.append(pattern)
296
+
297
+ # Apply binary hit-or-miss for all unique patterns
298
+ br = sum(ndimage.binary_hit_or_miss(skel_int, pattern) for pattern in unique_patterns)
299
+ return br
300
+
301
+ @classmethod
302
+ def get_end_points(cls, skeleton: MatLike):
303
+ """
304
+ Identify and retrieve the end points from the graph skeleton.
305
+ """
306
+ skel_int = skeleton * 1
307
+
308
+ # List of endpoint patterns
309
+ endpoints = [
310
+ [[0, 0, 0], [0, 1, 0], [0, 1, 0]],
311
+ [[0, 0, 0], [0, 1, 0], [0, 0, 1]],
312
+ [[0, 0, 0], [0, 1, 1], [0, 0, 0]],
313
+ [[0, 0, 1], [0, 1, 0], [0, 0, 0]],
314
+ [[0, 1, 0], [0, 1, 0], [0, 0, 0]],
315
+ [[1, 0, 0], [0, 1, 0], [0, 0, 0]],
316
+ [[0, 0, 0], [1, 1, 0], [0, 0, 0]],
317
+ [[0, 0, 0], [0, 1, 0], [1, 0, 0]],
318
+ [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
319
+ ]
320
+
321
+ # Apply binary hit-or-miss for each pattern and sum results
322
+ ep = sum(ndimage.binary_hit_or_miss(skel_int, np.array(pattern)) for pattern in endpoints)
323
+ return ep
324
+
325
+ @classmethod
326
+ def prune_edges(cls, skeleton: MatLike, max_num, branch_points):
327
+ """Prune dangling edges around b_points. Remove iteratively end points 'size' times from the skeleton"""
328
+ temp_skeleton = skeleton.copy()
329
+ for i in range(0, max_num):
330
+ end_points = GraphSkeleton.get_end_points(temp_skeleton)
331
+ points = np.logical_and(end_points, branch_points)
332
+ end_points = np.logical_xor(end_points, points)
333
+ end_points = np.logical_not(end_points)
334
+ temp_skeleton = np.logical_and(temp_skeleton, end_points)
335
+ return temp_skeleton
336
+
337
+ @classmethod
338
+ def merge_nodes(cls, skeleton: MatLike, node_radius):
339
+ """Merge nearby nodes in the graph skeleton."""
340
+ # overlay a disk over each branch point and find the overlaps to combine nodes
341
+ skeleton_int = 1 * skeleton
342
+ mask_elem = disk(node_radius)
343
+ bp_skel = GraphSkeleton.get_branched_points(skeleton)
344
+ bp_skel = 1 * (dilate(bp_skel, mask_elem))
345
+
346
+ # wide-nodes is initially an empty image the same size as the skeleton image
347
+ skel_shape = skeleton_int.shape
348
+ wide_nodes = np.zeros(skel_shape, dtype='int')
349
+
350
+ # this overlays the two skeletons
351
+ # skeleton_integer is the full map, bp_skel is just the branch points blown up to a larger size
352
+ for x in range(skel_shape[0]):
353
+ for y in range(skel_shape[1]):
354
+ if skeleton_int[x, y] == 0 and bp_skel[x, y] == 0:
355
+ wide_nodes[x, y] = 0
356
+ else:
357
+ wide_nodes[x, y] = 1
358
+
359
+ # re-skeletonizing wide-nodes and returning it, nearby nodes in radius 2 of each other should have been merged
360
+ temp_skeleton = skeletonize(wide_nodes)
361
+ return temp_skeleton
362
+
363
+ @classmethod
364
+ def remove_bubbles(cls, img_bin: MatLike, mask_elements: list):
365
+ """Remove bubbles from graph skeleton."""
366
+ if not isinstance(mask_elements, list):
367
+ return None
368
+
369
+ canvas = img_bin.copy()
370
+ for mask_elem in mask_elements:
371
+ canvas = skeletonize(mask_elem)
372
+ canvas = binary_closing(canvas, footprint=mask_elem)
373
+
374
+ temp_skeleton = skeletonize(canvas)
375
+ return temp_skeleton
376
+
377
+ @staticmethod
378
+ def point_check(img_bin: MatLike, pt_check):
379
+ """Checks and verifies that a point is on a graph edge."""
380
+
381
+ def boundary_check(coord, w, h, d=None):
382
+ """
383
+
384
+ Args:
385
+ coord: the coordinate (x,y) to check; no (x,y,z) compatibility yet.
386
+ w: width of the image to set the boundaries.
387
+ h: the height of the image to set the boundaries.
388
+ d: the depth of the image to set the boundaries.
389
+ Returns:
390
+
391
+ """
392
+ out_of_bounds = 0 # Generate a boolean check for out-of-boundary
393
+ # Check if coordinate is within the boundary
394
+ if d is None:
395
+ if coord[0] < 0 or coord[1] < 0 or coord[-2] > (w - 1) or coord[-1] > (h - 1):
396
+ out_of_bounds = 1
397
+ else:
398
+ # if sum(coord < 0) > 0 or sum(coord > [w - 1, h - 1, d - 1]) > 0:
399
+ if sum(coord < 0) > 0 or coord[-3] > (d - 1) or coord[-2] > (w - 1) or coord[-1] > (h - 1):
400
+ out_of_bounds = 1
401
+
402
+ # returns the boolean oob (1 if there is a boundary error); coordinates (resets to (1,1) if boundary error)
403
+ return out_of_bounds
404
+
405
+ # Check if the image is 2D
406
+ if len(img_bin.shape) == 2:
407
+ is_2d = True
408
+ height, width = img_bin.shape # finds dimensions of img_bin for boundary check
409
+ depth = 0
410
+ else:
411
+ is_2d = False
412
+ depth, height, width = img_bin.shape
413
+
414
+ try:
415
+ if is_2d:
416
+ # Checks if the point in fiber is out-of-bounds (oob) or black space (img_bin(x,y) = 0)
417
+ oob = boundary_check(pt_check, width, height)
418
+ not_in_edge = True if (oob == 1) else True if (img_bin[pt_check[-2], pt_check[-1]] == 0) else False
419
+ else:
420
+ # Checks if the point in fiber is out-of-bounds (oob) or black space (img_bin(d,x,y) = 0)
421
+ oob = boundary_check(pt_check, width, height, d=depth)
422
+ not_in_edge = True if (oob == 1) else True if (img_bin[pt_check[-3], pt_check[-2], pt_check[-1]] == 0) else False
423
+ except IndexError:
424
+ not_in_edge = True
425
+ return not_in_edge
@@ -0,0 +1,199 @@
1
+ """
2
+
3
+ This code is a modified version of the original `sknw.py` by Yan Xiaolong.
4
+
5
+ Modifications were made to address bugs caused by 'numba' on some platforms (e.g., Windows) and to replace fixed-size
6
+ NumPy buffers with dynamically resizable ones, improving robustness for large images and graphs.
7
+
8
+ Original author: Yan Xiaolong
9
+ Original source: https://github.com/Image-Py/sknw.git
10
+
11
+ """
12
+
13
+
14
+ import numpy as np
15
+ # from numba import jit
16
+ import networkx as nx
17
+
18
+
19
+ def neighbors(shape):
20
+ dim = len(shape)
21
+ block = np.ones([3] * dim)
22
+ block[tuple([1] * dim)] = 0
23
+ idx = np.where(block > 0)
24
+ idx = np.array(idx, dtype=np.uint8).T
25
+ idx = np.array(idx - [1] * dim)
26
+ acc = np.cumprod((1,) + shape[::-1][:-1])
27
+ return np.dot(idx, acc[::-1])
28
+
29
+
30
+ # @jit(nopython=True) # my mark
31
+ def mark(img, nbs): # mark the array use (0, 1, 2)
32
+ img = img.ravel()
33
+ for p in range(len(img)):
34
+ if img[p] == 0: continue
35
+ s = 0
36
+ for dp in nbs:
37
+ if img[p + dp] != 0: s += 1
38
+ if s == 2:
39
+ img[p] = 1
40
+ else:
41
+ img[p] = 2
42
+
43
+
44
+ # @jit(nopython=True) # trans index to r, c...
45
+ def idx2rc(idx, acc):
46
+ rst = np.zeros((len(idx), len(acc)), dtype=np.int16)
47
+ for i in range(len(idx)):
48
+ for j in range(len(acc)):
49
+ rst[i, j] = idx[i] // acc[j]
50
+ idx[i] -= rst[i, j] * acc[j]
51
+ rst -= 1
52
+ return rst
53
+
54
+
55
+ # @jit(nopython=True) # fill a node (maybe two or more points)
56
+ def fill(img, p, num, nbs, acc):
57
+ cap = 131072
58
+ buf = np.empty(cap, dtype=np.int64)
59
+ img[p] = num
60
+ buf[0] = p
61
+ cur = 0
62
+ s = 1
63
+ iso = True
64
+
65
+ while cur < s:
66
+ p = buf[cur]
67
+ for dp in nbs:
68
+ cp = p + dp
69
+ if img[cp] == 2:
70
+ img[cp] = num
71
+ if s >= cap:
72
+ cap *= 2
73
+ buf = np.resize(buf, cap)
74
+ buf[s] = cp
75
+ s += 1
76
+ elif img[cp] == 1:
77
+ iso = False
78
+ cur += 1
79
+
80
+ return iso, idx2rc(buf[:s], acc)
81
+
82
+
83
+ # @jit(nopython=True) # trace the edge and use a buffer, then buf.copy if you use [] numba not works
84
+ def trace(img, p, nbs, acc):
85
+ cap = 2048
86
+ buf = np.empty(cap, dtype=np.int64)
87
+ cur = 1
88
+ c1 = 0
89
+ c2 = 0
90
+ newp = 0
91
+
92
+ while True:
93
+ if cur >= cap:
94
+ cap *= 2
95
+ buf = np.resize(buf, cap)
96
+ buf[cur] = p
97
+ img[p] = 0
98
+ cur += 1
99
+
100
+ for dp in nbs:
101
+ cp = p + dp
102
+ if img[cp] >= 10:
103
+ if c1 == 0:
104
+ c1 = img[cp]
105
+ buf[0] = cp
106
+ else:
107
+ c2 = img[cp]
108
+ if cur >= cap:
109
+ cap += 1
110
+ buf = np.resize(buf, cap)
111
+ buf[cur] = cp
112
+ elif img[cp] == 1:
113
+ newp = cp
114
+
115
+ p = newp
116
+ if c2 != 0:
117
+ break
118
+
119
+ return c1 - 10, c2 - 10, idx2rc(buf[:cur + 1], acc)
120
+
121
+
122
+ # @jit(nopython=True) # parse the image, then get the nodes and edges
123
+ def parse_struc(img, nbs, acc, iso, ring):
124
+ img = img.ravel()
125
+ # buf = np.zeros(13107200, dtype=np.int64)
126
+ num = 10
127
+ nodes = []
128
+ for p in range(len(img)):
129
+ if img[p] == 2:
130
+ isiso, nds = fill(img, p, num, nbs, acc)
131
+ if isiso and not iso: continue
132
+ num += 1
133
+ nodes.append(nds)
134
+ edges = []
135
+ for p in range(len(img)):
136
+ if img[p] < 10: continue
137
+ for dp in nbs:
138
+ if img[p + dp] == 1:
139
+ edge = trace(img, p + dp, nbs, acc)
140
+ edges.append(edge)
141
+ if not ring: return nodes, edges
142
+ for p in range(len(img)):
143
+ if img[p] != 1: continue
144
+ img[p] = num
145
+ num += 1
146
+ nodes.append(idx2rc([p], acc))
147
+ for dp in nbs:
148
+ if img[p + dp] == 1:
149
+ edge = trace(img, p + dp, nbs, acc)
150
+ edges.append(edge)
151
+ return nodes, edges
152
+
153
+
154
+ # use nodes and edges to build a networkx graph
155
+ def build_graph(nodes, edges, multi=False, full=True):
156
+ os = np.array([i.mean(axis=0) for i in nodes])
157
+ if full: os = os.round().astype(np.uint16)
158
+ graph = nx.MultiGraph() if multi else nx.Graph()
159
+ for i in range(len(nodes)):
160
+ graph.add_node(i, pts=nodes[i], o=os[i])
161
+ for s, e, pts in edges:
162
+ if full: pts[[0, -1]] = os[[s, e]]
163
+ l = np.linalg.norm(pts[1:] - pts[:-1], axis=1).sum()
164
+ graph.add_edge(s, e, pts=pts, weight=l)
165
+ return graph
166
+
167
+
168
+ def mark_node(ske):
169
+ buf = np.pad(ske, (1, 1), mode='constant').astype(np.uint16)
170
+ nbs = neighbors(buf.shape)
171
+ # acc = np.cumprod((1,) + buf.shape[::-1][:-1])[::-1]
172
+ mark(buf, nbs)
173
+ return buf
174
+
175
+
176
+ def build_sknw(ske, multi=False, iso=True, ring=True, full=True):
177
+ buf = np.pad(ske, (1, 1), mode='constant').astype(np.uint16)
178
+ nbs = neighbors(buf.shape)
179
+ acc = np.cumprod((1,) + buf.shape[::-1][:-1])[::-1]
180
+ mark(buf, nbs)
181
+ nodes, edges = parse_struc(buf, nbs, acc, iso, ring)
182
+ return build_graph(nodes, edges, multi, full)
183
+
184
+
185
+ # draw the graph
186
+ def draw_graph(img, graph, cn=255, ce=128):
187
+ acc = np.cumprod((1,) + img.shape[::-1][:-1])[::-1]
188
+ img = img.ravel()
189
+ for (s, e) in graph.edges():
190
+ eds = graph[s][e]
191
+ if isinstance(graph, nx.MultiGraph):
192
+ for i in eds:
193
+ pts = eds[i]['pts']
194
+ img[np.dot(pts, acc)] = ce
195
+ else:
196
+ img[np.dot(eds['pts'], acc)] = ce
197
+ for idx in graph.nodes():
198
+ pts = graph.nodes[idx]['pts']
199
+ img[np.dot(pts, acc)] = cn
File without changes