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,29 @@
1
+ # SPDX-License-Identifier: GNU GPL v3
2
+
3
+ """
4
+ A group of algorithms and functions for Graph Theory analysis on microscopy images.
5
+ """
6
+
7
+ # MODULES
8
+ from .imaging.base_image import BaseImage
9
+ from .compute.graph_analyzer import GraphAnalyzer
10
+ from .imaging.image_processor import ImageProcessor, ALLOWED_IMG_EXTENSIONS
11
+ from .networks.fiber_network import FiberNetworkBuilder
12
+ from .networks.graph_skeleton import GraphSkeleton
13
+ from .utils.config_loader import (
14
+ load_gtc_configs,
15
+ load_gte_configs,
16
+ load_img_configs
17
+ )
18
+
19
+ __all__ = [
20
+ "BaseImage",
21
+ "GraphAnalyzer",
22
+ "ImageProcessor",
23
+ "ALLOWED_IMG_EXTENSIONS",
24
+ "FiberNetworkBuilder",
25
+ "GraphSkeleton",
26
+ "load_gtc_configs",
27
+ "load_gte_configs",
28
+ "load_img_configs"
29
+ ]
File without changes
@@ -0,0 +1,490 @@
1
+ # SPDX-License-Identifier: GNU GPL v3
2
+
3
+ """
4
+ Builds a graph network from nanoscale microscopy images.
5
+ """
6
+
7
+ import os
8
+ import igraph
9
+ import itertools
10
+ import numpy as np
11
+ import networkx as nx
12
+ import matplotlib.pyplot as plt
13
+ from PIL import Image, ImageQt
14
+ from cv2.typing import MatLike
15
+ from ovito.vis import Viewport
16
+ from ovito.data import DataCollection, Particles
17
+ from ovito.pipeline import StaticSource, Pipeline
18
+
19
+ from .sknw_mod import build_sknw
20
+ from ..utils.progress_update import ProgressUpdate
21
+ from ..networks.graph_skeleton import GraphSkeleton
22
+ from ..utils.config_loader import load_gte_configs
23
+ from ..utils.sgt_utils import write_csv_file, write_gsd_file
24
+
25
+
26
+ class FiberNetworkBuilder(ProgressUpdate):
27
+ """
28
+ A class for builds a graph network from microscopy images and stores is as a NetworkX object.
29
+
30
+ """
31
+
32
+ def __init__(self, cfg_file=""):
33
+ """
34
+ A class for builds a graph network from microscopy images and stores is as a NetworkX object.
35
+
36
+ Args:
37
+ cfg_file (str): configuration file path
38
+
39
+ """
40
+ super(FiberNetworkBuilder, self).__init__()
41
+ self.configs: dict = load_gte_configs(cfg_file) # graph extraction parameters and options.
42
+ self.props: list = []
43
+ self.img_ntwk: MatLike | None = None
44
+ self.nx_giant_graph: nx.Graph | None = None
45
+ self.nx_graph: nx.Graph | None = None
46
+ self.ig_graph: igraph.Graph | None = None
47
+ self.gsd_file: str | None = None
48
+ self.skel_obj: GraphSkeleton | None = None
49
+
50
+ def fit_graph(self, save_dir: str, img_bin: MatLike = None, is_img_2d: bool = True, px_width_sz: float = 1.0, rho_val: float = 1.0, image_file: str = "img"):
51
+ """
52
+ Execute a function that builds a NetworkX graph from the binary image.
53
+
54
+ :param save_dir: Directory to save the graph to.
55
+ :param img_bin: A binary image for building Graph Skeleton for the NetworkX graph.
56
+ :param is_img_2d: Whether the image is 2D or 3D otherwise.
57
+ :param px_width_sz: Width of a pixel in nanometers.
58
+ :param rho_val: Resistivity coefficient/value of the material.
59
+ :param image_file: Filename of the binary image.
60
+ :return:
61
+ """
62
+
63
+ if self.abort:
64
+ self.update_status([-1, "Task aborted by due to an error. If problem with graph: change/apply different "
65
+ "image/binary filters and graph options. OR change brightness/contrast"])
66
+ return
67
+
68
+ self.update_status([50, "Extracting the graph network..."])
69
+ success = self.extract_graph(image_bin=img_bin, is_img_2d=is_img_2d, px_size=px_width_sz, rho_val=rho_val)
70
+ if not success:
71
+ self.update_status([-1, "Problem encountered, provide GT parameters"])
72
+ self.abort = True
73
+ return
74
+
75
+ self.update_status([75, "Verifying graph network..."])
76
+ if self.nx_graph.number_of_nodes() <= 0:
77
+ self.update_status([-1, "Problem generating graph (change image/binary filters)"])
78
+ self.abort = True
79
+ return
80
+
81
+ self.update_status([77, "Retrieving graph properties..."])
82
+ self.props = self.get_graph_props()
83
+
84
+ self.update_status([90, "Saving graph network..."])
85
+ # Save graph to GSD/HOOMD - For OVITO rendering
86
+ self.configs["export_as_gsd"]["value"] = 1
87
+ self.save_graph_to_file(image_file, save_dir)
88
+
89
+ def reset_graph(self):
90
+ """
91
+ Erase the existing data stored in the object.
92
+ :return:
93
+ """
94
+ self.nx_graph, self.ig_graph, self.img_ntwk = None, None, None
95
+
96
+ def extract_graph(self, image_bin: MatLike = None, is_img_2d: bool = True, px_size: float = 1.0, rho_val: float = 1.0):
97
+ """
98
+ Build a skeleton from the image and use the skeleton to build a NetworkX graph.
99
+
100
+ :param image_bin: Binary image from which the skeleton will be built and graph drawn.
101
+ :param is_img_2d: Whether the image is 2D or 3D otherwise.
102
+ :param px_size: Width of a pixel in nanometers.
103
+ :param rho_val: Resistivity coefficient/value of the material.
104
+ :return:
105
+ """
106
+
107
+ if image_bin is None:
108
+ return False
109
+
110
+ opt_gte = self.configs
111
+ if opt_gte is None:
112
+ return False
113
+
114
+ self.update_status([51, "Build graph skeleton from binary image..."])
115
+ graph_skel = GraphSkeleton(image_bin, opt_gte, is_2d=is_img_2d, progress_func=self.update_status)
116
+ self.skel_obj = graph_skel
117
+ img_skel = graph_skel.skeleton
118
+
119
+ self.update_status([60, "Creating graph network..."])
120
+ # nx_graph = sknw.build_sknw(img_skel)
121
+ nx_graph = build_sknw(img_skel)
122
+
123
+ if opt_gte["remove_self_loops"]["value"]:
124
+ self.update_status([64, "Removing self loops from graph network..."])
125
+
126
+ self.update_status([66, "Assigning weights to graph network..."])
127
+ for (s, e) in nx_graph.edges():
128
+ if opt_gte["remove_self_loops"]["value"]:
129
+ # Removing all instances of edges where the start and end are the same, or "self-loops"
130
+ if s == e:
131
+ nx_graph.remove_edge(s, e)
132
+ continue
133
+
134
+ # 'sknw' library stores the length of edge as 'weight', we create an attribute 'length', and update 'weight'
135
+ nx_graph[s][e]['length'] = nx_graph[s][e]['weight']
136
+ ge = nx_graph[s][e]['pts']
137
+
138
+ if opt_gte["has_weights"]["value"] == 1:
139
+ # We update 'weight'
140
+ wt_type = self.get_weight_type()
141
+ weight_options = FiberNetworkBuilder.get_weight_options()
142
+ pix_width, pix_angle, wt = graph_skel.assign_weights(ge, wt_type, weight_options=weight_options,
143
+ pixel_dim=px_size, rho_dim=rho_val)
144
+ else:
145
+ pix_width, pix_angle, wt = graph_skel.assign_weights(ge, None)
146
+ del nx_graph[s][e]['weight'] # delete 'weight'
147
+ nx_graph[s][e]['width'] = pix_width
148
+ nx_graph[s][e]['angle'] = pix_angle
149
+ nx_graph[s][e]['weight'] = wt
150
+ # print(f"{nx_graph[s][e]}\n")
151
+ self.nx_graph = nx_graph
152
+ self.ig_graph = igraph.Graph.from_networkx(nx_graph)
153
+ return True
154
+
155
+ def plot_graph_network(self, image_arr: MatLike, giant_only: bool = False, plot_nodes: bool = False, a4_size: bool = False):
156
+ """
157
+ Creates a plot figure of the graph network. It draws all the edges and nodes of the graph.
158
+
159
+ :param image_arr: Slides of 2D images to be used to draw the network.
160
+ :param giant_only: If True, only the giant graph is identified and drawn.
161
+ :param plot_nodes: Make the graph's node plot figure.
162
+ :param a4_size: Decision if to create an A4 size plot figure.
163
+
164
+ :return:
165
+ """
166
+
167
+ if self.nx_graph is None:
168
+ return None
169
+
170
+ # Fetch the graph and config options
171
+ if giant_only:
172
+ nx_graph = self.nx_giant_graph
173
+ else:
174
+ nx_graph = self.nx_graph
175
+ show_node_id = (self.configs["display_node_id"]["value"] == 1)
176
+
177
+ # Fetch a single 2D image
178
+ if image_arr is None:
179
+ return None
180
+
181
+ # Create the plot figure(s)
182
+ fig_grp = FiberNetworkBuilder.plot_graph_edges(image_arr, nx_graph, plot_nodes=plot_nodes, show_node_id=show_node_id)
183
+ fig = fig_grp[0]
184
+ if a4_size:
185
+ plt_title = "Graph Node Plot" if plot_nodes else "Graph Edge Plot"
186
+ fig.set_size_inches(8.5, 11)
187
+ fig.set_dpi(400)
188
+ ax = fig.axes[0]
189
+ ax.set_title(plt_title)
190
+ # This moves the Axes to start: 5% from the left, 5% from the bottom,
191
+ # and have a width and height: 80% of the figure.
192
+ # [left, bottom, width, height]
193
+ ax.set_position([0.05, 0.05, 0.9, 0.9])
194
+ return fig
195
+
196
+ def get_config_info(self):
197
+ """
198
+ Get the user selected parameters and options information.
199
+ :return:
200
+ """
201
+
202
+ opt_gte = self.configs
203
+
204
+ run_info = "***Graph Extraction Configurations***\n"
205
+ if opt_gte["has_weights"]["value"] == 1:
206
+ wt_type = self.get_weight_type()
207
+ run_info += f"Weight Type: {FiberNetworkBuilder.get_weight_options().get(wt_type)} || "
208
+ if opt_gte["merge_nearby_nodes"]["value"]:
209
+ run_info += "Merge Nodes || "
210
+ if opt_gte["prune_dangling_edges"]["value"]:
211
+ run_info += "Prune Dangling Edges || "
212
+ run_info = run_info[:-3] + '' if run_info.endswith('|| ') else run_info
213
+ run_info += "\n"
214
+ if opt_gte["remove_disconnected_segments"]["value"]:
215
+ run_info += f"Remove Objects of Size = {opt_gte["remove_disconnected_segments"]["items"][0]["value"]} || "
216
+ if opt_gte["remove_self_loops"]["value"]:
217
+ run_info += "Remove Self Loops || "
218
+ run_info = run_info[:-3] + '' if run_info.endswith('|| ') else run_info
219
+
220
+ return run_info
221
+
222
+ def get_graph_props(self):
223
+ """
224
+ A method that retrieves graph properties and stores them in a list-array.
225
+
226
+ Returns: list of graph properties
227
+ """
228
+
229
+ # 1. Identify the subcomponents (graph segments) that make up the entire NetworkX graph.
230
+ self.update_status([78, "Identifying graph subcomponents..."])
231
+ graph = self.nx_graph.copy()
232
+ connected_components = list(nx.connected_components(graph))
233
+ if not connected_components: # In case the graph is empty
234
+ connected_components = []
235
+ sub_graphs = [graph.subgraph(c).copy() for c in connected_components]
236
+ giant_graph = max(sub_graphs, key=lambda g: g.number_of_nodes())
237
+ num_graphs = len(sub_graphs)
238
+ connect_ratio = giant_graph.number_of_nodes() / graph.number_of_nodes()
239
+
240
+ # 2. Update with the giant graph
241
+ self.nx_giant_graph = giant_graph
242
+ # self.ig_graph = igraph.Graph.from_networkx(giant_graph)
243
+
244
+ # 3. Populate graph properties
245
+ self.update_status([80, "Storing graph properties..."])
246
+ props = [
247
+ ["Weight Type", str(FiberNetworkBuilder.get_weight_options().get(self.get_weight_type()))],
248
+ ["Edge Count", str(graph.number_of_edges())],
249
+ ["Node Count", str(graph.number_of_nodes())],
250
+ ["Graph Count", str(len(connected_components))],
251
+ ["Sub-graph Count", str(num_graphs)],
252
+ ["Giant graph ratio", f"{round((connect_ratio * 100), 3)}%"]]
253
+ return props
254
+
255
+ def get_weight_type(self):
256
+ wt_type = None # Default weight
257
+ if self.configs["has_weights"]["value"] == 0:
258
+ return wt_type
259
+
260
+ for i in range(len(self.configs["has_weights"]["items"])):
261
+ if self.configs["has_weights"]["items"][i]["value"]:
262
+ wt_type = self.configs["has_weights"]["items"][i]["id"]
263
+ return wt_type
264
+
265
+ def save_graph_to_file(self, filename: str, out_dir: str):
266
+ """
267
+ Save graph data into files.
268
+
269
+ :param filename: The filename to save the data to.
270
+ :param out_dir: The directory to save the data to.
271
+ :return:
272
+ """
273
+
274
+ nx_graph = self.nx_graph.copy()
275
+ opt_gte = self.configs
276
+
277
+ g_filename = filename + "_graph.gexf"
278
+ el_filename = filename + "_EL.csv"
279
+ adj_filename = filename + "_adj.csv"
280
+ gsd_filename = filename + "_skel.gsd"
281
+ gexf_file = os.path.join(out_dir, g_filename)
282
+ csv_file = os.path.join(out_dir, el_filename)
283
+ adj_file = os.path.join(out_dir, adj_filename)
284
+
285
+ if opt_gte["export_adj_mat"]["value"] == 1:
286
+ adj_mat = nx.adjacency_matrix(self.nx_graph).todense()
287
+ np.savetxt(str(adj_file), adj_mat, delimiter=",")
288
+
289
+ if opt_gte["export_edge_list"]["value"] == 1:
290
+ if opt_gte["has_weights"]["value"] == 1:
291
+ fields = ['Source', 'Target', 'Weight', 'Length']
292
+ el = nx.generate_edgelist(nx_graph, delimiter=',', data=True)
293
+ write_csv_file(csv_file, fields, el)
294
+ else:
295
+ fields = ['Source', 'Target']
296
+ el = nx.generate_edgelist(nx_graph, delimiter=',', data=False)
297
+ write_csv_file(csv_file, fields, el)
298
+
299
+ if opt_gte["export_as_gexf"]["value"] == 1:
300
+ # deleting extraneous info and then exporting the final skeleton
301
+ for (x) in nx_graph.nodes():
302
+ del nx_graph.nodes[x]['pts']
303
+ del nx_graph.nodes[x]['o']
304
+ for (s, e) in nx_graph.edges():
305
+ del nx_graph[s][e]['pts']
306
+ nx.write_gexf(nx_graph, gexf_file)
307
+
308
+ if opt_gte["export_as_gsd"]["value"] == 1:
309
+ self.gsd_file = os.path.join(out_dir, gsd_filename)
310
+ if self.skel_obj.skeleton_3d is not None:
311
+ write_gsd_file(self.gsd_file, self.skel_obj.skeleton_3d)
312
+
313
+ @staticmethod
314
+ def get_weight_options():
315
+ """
316
+ Returns the weight options for building the graph edges.
317
+
318
+ :return:
319
+ """
320
+ weight_options = {
321
+ 'DIA': 'Diameter',
322
+ 'AREA': 'Area', # surface area of edge
323
+ 'LEN': 'Length',
324
+ 'ANGLE': 'Angle',
325
+ 'INV_LEN': 'InverseLength',
326
+ 'VAR_CON': 'Conductance', # with variable width
327
+ 'FIX_CON': 'FixedWidthConductance',
328
+ 'RES': 'Resistance',
329
+ # '': ''
330
+ }
331
+ return weight_options
332
+
333
+ @staticmethod
334
+ def plot_graph_edges(image: MatLike, nx_graph: nx.Graph, node_distribution_data: list = None, plot_nodes: bool = False, show_node_id: bool = False, transparent: bool = False, line_width: float=1.5, node_marker_size: float = 3):
335
+ """
336
+ Plot graph edges on top of the image
337
+
338
+ :param image: image to be superimposed with graph edges
339
+ :param nx_graph: a NetworkX graph
340
+ :param node_distribution_data: a list of node distribution data for a heatmap plot
341
+ :param plot_nodes: whether to plot graph nodes or not
342
+ :param show_node_id: if True, node IDs are displayed on the plot
343
+ :param transparent: whether to draw the image with a transparent background
344
+ :param line_width: each edge's line width
345
+ :param node_marker_size: the size (diameter) of the node marker
346
+ :return:
347
+ """
348
+
349
+ def plot_graph_nodes(node_ax):
350
+ """
351
+ Plot graph nodes on top of the image.
352
+ :param node_ax: Matplotlib axes
353
+ """
354
+
355
+ node_list = list(nx_graph.nodes())
356
+ gn = np.array([nx_graph.nodes[i]['o'] for i in node_list])
357
+
358
+ if show_node_id:
359
+ i = 0
360
+ for x, y in zip(gn[:, coord_1], gn[:, coord_2]):
361
+ node_ax.annotate(str(i), (x, y), fontsize=5)
362
+ i += 1
363
+
364
+ if node_distribution_data is not None:
365
+ c_set = node_ax.scatter(gn[:, coord_1], gn[:, coord_2], s=node_marker_size, c=node_distribution_data, cmap='plasma')
366
+ return c_set
367
+ else:
368
+ # c_set = node_ax.scatter(gn[:, coord_1], gn[:, coord_2], s=marker_size)
369
+ node_ax.plot(gn[:, coord_1], gn[:, coord_2], 'b.', markersize=node_marker_size)
370
+ return None
371
+
372
+ def create_plt_axes(pos):
373
+ """
374
+ Create a matplotlib axes object.
375
+ Args:
376
+ pos: index position of image frame.
377
+
378
+ Returns:
379
+
380
+ """
381
+ new_fig = plt.Figure()
382
+ new_ax = new_fig.add_axes((0, 0, 1, 1)) # span the whole figure
383
+ new_ax.set_axis_off()
384
+ if transparent:
385
+ new_ax.imshow(image[pos], cmap='gray', alpha=0) # Alpha=0 makes image 100% transparent
386
+ else:
387
+ new_ax.imshow(image[pos], cmap='gray')
388
+ return new_fig
389
+
390
+ fig_group = {}
391
+ # Create axes for the first frame of image (enough if it is 2D)
392
+ fig = create_plt_axes(0)
393
+ fig_group[0] = fig
394
+
395
+ color_list = ['black', 'r', 'g', 'b', 'c', 'm', 'y', 'k']
396
+ color_cycle = itertools.cycle(color_list)
397
+ nx_components = list(nx.connected_components(nx_graph))
398
+ for component in nx_components:
399
+ color = next(color_cycle)
400
+ sg = nx_graph.subgraph(component)
401
+
402
+ for (s, e) in sg.edges():
403
+ ge = sg[s][e]['pts']
404
+ coord_1, coord_2 = 1, 0 # coordinates: (y, x)
405
+ coord_3 = 0
406
+ if ge.shape[1] == 3:
407
+ # image and graph are 3D (not 2D)
408
+ # 3D Coordinates are (x, y, z) ... assume that y and z are the same for 2D graphs and x is depth.
409
+ coord_1, coord_2, coord_3 = 2, 1, 0 # coordinates: (z, y, x)
410
+
411
+ if coord_3 in fig_group and fig_group[coord_3] is not None:
412
+ fig = fig_group[coord_3]
413
+ else:
414
+ fig = create_plt_axes(coord_3)
415
+ fig_group[coord_3] = fig
416
+ ax = fig.get_axes()[0]
417
+ ax.plot(ge[:, coord_1], ge[:, coord_2], color, linewidth=line_width)
418
+
419
+ if plot_nodes:
420
+ for idx, plt_fig in fig_group.items():
421
+ ax = plt_fig.get_axes()[0]
422
+ node_color_set = plot_graph_nodes(ax)
423
+ if node_color_set is not None:
424
+ cbar = plt_fig.colorbar(node_color_set, ax=ax, orientation='vertical', label='Value')
425
+ # [left, bottom, width, height]
426
+ cbar.ax.set_position([0.82, 0.05, 0.05, 0.9])
427
+ return fig_group
428
+
429
+ # TO DELETE IT LATER
430
+ def data_gen_function(self):
431
+ """Populates OVITO's data with particles."""
432
+ data = DataCollection()
433
+ positions = np.asarray(np.where(np.asarray(self.skel_obj.skeleton_3d) != 0)).T
434
+ particles = Particles()
435
+ particles.create_property("Position", data=positions)
436
+ data.objects.append(particles)
437
+ return data
438
+
439
+ # ONLY RUNS ON MAIN THREAD - TO BE DELETED
440
+ def render_graph_to_image(self, bg_image=None, is_img_2d: bool = True):
441
+ """
442
+ Renders the graph network into an image; it can optionally superimpose the graph on the image.
443
+
444
+ :param bg_image: Optional background image.
445
+ :param is_img_2d: Whether the image is 2D or 3D otherwise.
446
+ """
447
+ if self.gsd_file is None:
448
+ return None
449
+
450
+ if bg_image is not None:
451
+ # OVITO doesn’t directly support 3D numpy volumes as backgrounds
452
+ bg_image = bg_image.squeeze()
453
+ if not is_img_2d:
454
+ # (visualize only one slice) Extract a middle slice from 3D grayscale volume
455
+ mid_slice = bg_image[bg_image.shape[0] // 2] # shape: (H, W)
456
+ else:
457
+ mid_slice = bg_image
458
+
459
+ # Convert to RGB for PIL
460
+ # bg_rgb = cv2.cvtColor(mid_slice, cv2.COLOR_GRAY2RGB)
461
+ bg_pil = Image.fromarray(mid_slice).convert("RGB")
462
+
463
+ # Set OVITO render size
464
+ size = (bg_pil.width, bg_pil.height)
465
+ else:
466
+ size = (800, 600)
467
+ bg_pil = None
468
+
469
+ # Load OVITO pipeline and scene
470
+ # pipeline = import_file(self.gsd_file)
471
+ data = self.data_gen_function()
472
+ # print(type(data))
473
+ pipeline = Pipeline(source = StaticSource(data = data))
474
+ pipeline.add_to_scene()
475
+
476
+ vp = Viewport(type=Viewport.Type.Front, camera_dir=(2, 1, -1))
477
+ vp.zoom_all(size)
478
+
479
+ # Render to QImage without the alpha channel
480
+ q_img = vp.render_image(size=size, alpha=True, background=(1, 1, 1))
481
+
482
+ # Convert QImage to PIL Image
483
+ pil_img = ImageQt.fromqimage(q_img).convert("RGB")
484
+
485
+ if bg_pil is not None:
486
+ # Overlay using simple blending (optional: adjust transparency)
487
+ final_img = Image.blend(bg_pil, pil_img, alpha=0.5)
488
+ else:
489
+ final_img = pil_img
490
+ return final_img