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.
- StructuralGT/__init__.py +31 -0
- StructuralGT/apps/__init__.py +0 -0
- StructuralGT/apps/cli_main.py +258 -0
- StructuralGT/apps/gui_main.py +69 -0
- StructuralGT/apps/gui_mcw/__init__.py +0 -0
- StructuralGT/apps/gui_mcw/checkbox_model.py +91 -0
- StructuralGT/apps/gui_mcw/controller.py +1073 -0
- StructuralGT/apps/gui_mcw/image_provider.py +74 -0
- StructuralGT/apps/gui_mcw/imagegrid_model.py +75 -0
- StructuralGT/apps/gui_mcw/qthread_worker.py +102 -0
- StructuralGT/apps/gui_mcw/table_model.py +79 -0
- StructuralGT/apps/gui_mcw/tree_model.py +154 -0
- StructuralGT/apps/sgt_qml/CenterMainContent.qml +19 -0
- StructuralGT/apps/sgt_qml/LeftContent.qml +48 -0
- StructuralGT/apps/sgt_qml/MainWindow.qml +762 -0
- StructuralGT/apps/sgt_qml/RightLoggingPanel.qml +125 -0
- StructuralGT/apps/sgt_qml/assets/icons/.DS_Store +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/back_icon.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/brightness_icon.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/cancel_icon.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/crop_icon.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/edit_icon.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/graph_icon.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/hide_panel.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/next_icon.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/notify_icon.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/rescale_icon.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/show_panel.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/square_icon.png +0 -0
- StructuralGT/apps/sgt_qml/assets/icons/undo_icon.png +0 -0
- StructuralGT/apps/sgt_qml/components/ImageFilters.qml +82 -0
- StructuralGT/apps/sgt_qml/components/ImageProperties.qml +112 -0
- StructuralGT/apps/sgt_qml/components/ProjectNav.qml +127 -0
- StructuralGT/apps/sgt_qml/widgets/BinaryFilterWidget.qml +151 -0
- StructuralGT/apps/sgt_qml/widgets/BrightnessControlWidget.qml +103 -0
- StructuralGT/apps/sgt_qml/widgets/CreateProjectWidget.qml +112 -0
- StructuralGT/apps/sgt_qml/widgets/GTWidget.qml +94 -0
- StructuralGT/apps/sgt_qml/widgets/GraphComputeWidget.qml +77 -0
- StructuralGT/apps/sgt_qml/widgets/GraphExtractWidget.qml +175 -0
- StructuralGT/apps/sgt_qml/widgets/GraphPropertyWidget.qml +77 -0
- StructuralGT/apps/sgt_qml/widgets/ImageFilterWidget.qml +137 -0
- StructuralGT/apps/sgt_qml/widgets/ImagePropertyWidget.qml +78 -0
- StructuralGT/apps/sgt_qml/widgets/ImageViewWidget.qml +585 -0
- StructuralGT/apps/sgt_qml/widgets/MenuBarWidget.qml +137 -0
- StructuralGT/apps/sgt_qml/widgets/MicroscopyPropertyWidget.qml +80 -0
- StructuralGT/apps/sgt_qml/widgets/ProjectWidget.qml +141 -0
- StructuralGT/apps/sgt_qml/widgets/RescaleControlWidget.qml +83 -0
- StructuralGT/apps/sgt_qml/widgets/RibbonWidget.qml +406 -0
- StructuralGT/apps/sgt_qml/widgets/StatusBarWidget.qml +173 -0
- StructuralGT/compute/__init__.py +0 -0
- StructuralGT/compute/c_lang/include/sgt_base.h +21 -0
- StructuralGT/compute/graph_analyzer.py +1499 -0
- StructuralGT/entrypoints.py +49 -0
- StructuralGT/imaging/__init__.py +0 -0
- StructuralGT/imaging/base_image.py +403 -0
- StructuralGT/imaging/image_processor.py +780 -0
- StructuralGT/modules.py +29 -0
- StructuralGT/networks/__init__.py +0 -0
- StructuralGT/networks/fiber_network.py +490 -0
- StructuralGT/networks/graph_skeleton.py +425 -0
- StructuralGT/networks/sknw_mod.py +199 -0
- StructuralGT/utils/__init__.py +0 -0
- StructuralGT/utils/config_loader.py +244 -0
- StructuralGT/utils/configs.ini +97 -0
- StructuralGT/utils/progress_update.py +67 -0
- StructuralGT/utils/sgt_utils.py +291 -0
- sgtlib-3.3.9.dist-info/METADATA +789 -0
- sgtlib-3.3.9.dist-info/RECORD +72 -0
- sgtlib-3.3.9.dist-info/WHEEL +5 -0
- sgtlib-3.3.9.dist-info/entry_points.txt +3 -0
- sgtlib-3.3.9.dist-info/licenses/LICENSE +674 -0
- sgtlib-3.3.9.dist-info/top_level.txt +1 -0
StructuralGT/modules.py
ADDED
@@ -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
|