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
@@ -0,0 +1,1073 @@
|
|
1
|
+
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
import pickle
|
5
|
+
import logging
|
6
|
+
import requests
|
7
|
+
import numpy as np
|
8
|
+
from packaging import version
|
9
|
+
from ovito import scene
|
10
|
+
from ovito.vis import Viewport
|
11
|
+
from ovito.io import import_file
|
12
|
+
from ovito.gui import create_qwidget
|
13
|
+
from typing import TYPE_CHECKING, Optional
|
14
|
+
from PySide6.QtWidgets import QApplication
|
15
|
+
from PySide6.QtCore import QObject,Signal,Slot
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
# False at run time, only for a type-checker
|
19
|
+
from _typeshed import SupportsWrite
|
20
|
+
|
21
|
+
from .tree_model import TreeModel
|
22
|
+
from .table_model import TableModel
|
23
|
+
from .checkbox_model import CheckBoxModel
|
24
|
+
from .imagegrid_model import ImageGridModel
|
25
|
+
from .qthread_worker import QThreadWorker, WorkerTask
|
26
|
+
|
27
|
+
from ... import __version__
|
28
|
+
from ...utils.sgt_utils import img_to_base64, verify_path
|
29
|
+
from ...imaging.image_processor import ImageProcessor, FiberNetworkBuilder, ALLOWED_IMG_EXTENSIONS
|
30
|
+
from ...compute.graph_analyzer import GraphAnalyzer#, COMPUTING_DEVICE
|
31
|
+
|
32
|
+
|
33
|
+
class MainController(QObject):
|
34
|
+
"""Exposes a method to refresh the image in QML"""
|
35
|
+
|
36
|
+
showAlertSignal = Signal(str, str)
|
37
|
+
errorSignal = Signal(str)
|
38
|
+
updateProgressSignal = Signal(int, str)
|
39
|
+
taskTerminatedSignal = Signal(bool, list)
|
40
|
+
projectOpenedSignal = Signal(str)
|
41
|
+
changeImageSignal = Signal()
|
42
|
+
imageChangedSignal = Signal()
|
43
|
+
enableRectangularSelectionSignal = Signal(bool)
|
44
|
+
showCroppingToolSignal = Signal(bool)
|
45
|
+
showUnCroppingToolSignal = Signal(bool)
|
46
|
+
performCroppingSignal = Signal(bool)
|
47
|
+
|
48
|
+
def __init__(self, qml_app: QApplication):
|
49
|
+
super().__init__()
|
50
|
+
self.qml_app = qml_app
|
51
|
+
self.img_loaded = False
|
52
|
+
self.project_open = False
|
53
|
+
self.allow_auto_scale = True
|
54
|
+
|
55
|
+
# Project data
|
56
|
+
self.project_data = {"name": "", "file_path": ""}
|
57
|
+
|
58
|
+
# Initialize flags
|
59
|
+
self.wait_flag = False
|
60
|
+
|
61
|
+
# Create graph objects
|
62
|
+
self.sgt_objs = {}
|
63
|
+
self.selected_sgt_obj_index = 0
|
64
|
+
|
65
|
+
# Create Models
|
66
|
+
self.imgThumbnailModel = TableModel([])
|
67
|
+
self.imagePropsModel = TableModel([])
|
68
|
+
self.graphPropsModel = TableModel([])
|
69
|
+
self.graphComputeModel = TableModel([])
|
70
|
+
self.microscopyPropsModel = CheckBoxModel([])
|
71
|
+
|
72
|
+
self.gteTreeModel = TreeModel([])
|
73
|
+
self.gtcListModel = CheckBoxModel([])
|
74
|
+
self.exportGraphModel = CheckBoxModel([])
|
75
|
+
self.imgBatchModel = CheckBoxModel([])
|
76
|
+
self.imgControlModel = CheckBoxModel([])
|
77
|
+
self.imgBinFilterModel = CheckBoxModel([])
|
78
|
+
self.imgFilterModel = CheckBoxModel([])
|
79
|
+
self.imgScaleOptionModel = CheckBoxModel([])
|
80
|
+
self.saveImgModel = CheckBoxModel([])
|
81
|
+
self.img3dGridModel = ImageGridModel([], set([]))
|
82
|
+
|
83
|
+
# Create QThreadWorker for long tasks
|
84
|
+
self.worker = QThreadWorker(0, None)
|
85
|
+
self.worker_task = WorkerTask()
|
86
|
+
|
87
|
+
def update_img_models(self, sgt_obj: GraphAnalyzer):
|
88
|
+
"""
|
89
|
+
Reload image configuration selections and controls from saved dict to QML gui_mcw after the image is loaded.
|
90
|
+
|
91
|
+
:param sgt_obj: A GraphAnalyzer object with all saved user-selected configurations.
|
92
|
+
"""
|
93
|
+
try:
|
94
|
+
ntwk_p = sgt_obj.ntwk_p
|
95
|
+
sel_img_batch = ntwk_p.get_selected_batch()
|
96
|
+
first_index = next(iter(sel_img_batch.selected_images), None) # 1st selected image
|
97
|
+
first_index = first_index if first_index is not None else 0 # first image if None
|
98
|
+
options_img = sel_img_batch.images[first_index].configs
|
99
|
+
|
100
|
+
img_controls = [v for v in options_img.values() if v["type"] == "image-control"]
|
101
|
+
bin_filters = [v for v in options_img.values() if v["type"] == "binary-filter"]
|
102
|
+
img_filters = [v for v in options_img.values() if v["type"] == "image-filter"]
|
103
|
+
img_properties = [v for v in options_img.values() if v["type"] == "image-property"]
|
104
|
+
file_options = [v for v in options_img.values() if v["type"] == "file-options"]
|
105
|
+
options_scaling = sel_img_batch.scaling_options
|
106
|
+
batch_list = [{"id": f"batch_{i}", "text": f"Image Batch {i+1}", "value": i}
|
107
|
+
for i in range(len(sgt_obj.ntwk_p.image_batches))]
|
108
|
+
|
109
|
+
self.imgBatchModel.reset_data(batch_list)
|
110
|
+
self.imgScaleOptionModel.reset_data(options_scaling)
|
111
|
+
|
112
|
+
self.imgControlModel.reset_data(img_controls)
|
113
|
+
self.imgBinFilterModel.reset_data(bin_filters)
|
114
|
+
self.imgFilterModel.reset_data(img_filters)
|
115
|
+
self.microscopyPropsModel.reset_data(img_properties)
|
116
|
+
self.saveImgModel.reset_data(file_options)
|
117
|
+
except Exception as err:
|
118
|
+
logging.exception("Fatal Error: %s", err, extra={'user': 'SGT Logs'})
|
119
|
+
self.showAlertSignal.emit("Fatal Error", "Error re-loading image configurations! Close app and try again.")
|
120
|
+
|
121
|
+
def update_graph_models(self, sgt_obj: GraphAnalyzer):
|
122
|
+
"""
|
123
|
+
Reload graph configuration selections and controls from saved dict to QML gui_mcw.
|
124
|
+
Args:
|
125
|
+
sgt_obj: a GraphAnalyzer object with all saved user-selected configurations.
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
|
129
|
+
"""
|
130
|
+
try:
|
131
|
+
ntwk_p = sgt_obj.ntwk_p
|
132
|
+
sel_img_batch = ntwk_p.get_selected_batch()
|
133
|
+
graph_obj = sel_img_batch.graph_obj
|
134
|
+
option_gte = graph_obj.configs
|
135
|
+
options_gtc = sgt_obj.configs
|
136
|
+
|
137
|
+
graph_options = [v for v in option_gte.values() if v["type"] == "graph-extraction"]
|
138
|
+
file_options = [v for v in option_gte.values() if v["type"] == "file-options"]
|
139
|
+
|
140
|
+
self.gteTreeModel.reset_data(graph_options)
|
141
|
+
self.exportGraphModel.reset_data(file_options)
|
142
|
+
self.gtcListModel.reset_data(list(options_gtc.values()))
|
143
|
+
|
144
|
+
self.imagePropsModel.reset_data(sel_img_batch.props)
|
145
|
+
self.graphPropsModel.reset_data(graph_obj.props)
|
146
|
+
self.graphComputeModel.reset_data(sgt_obj.props)
|
147
|
+
except Exception as err:
|
148
|
+
logging.exception("Fatal Error: %s", err, extra={'user': 'SGT Logs'})
|
149
|
+
self.showAlertSignal.emit("Fatal Error", "Error re-loading image configurations! Close app and try again.")
|
150
|
+
|
151
|
+
def get_selected_sgt_obj(self):
|
152
|
+
try:
|
153
|
+
keys_list = list(self.sgt_objs.keys())
|
154
|
+
key_at_index = keys_list[self.selected_sgt_obj_index]
|
155
|
+
sgt_obj = self.sgt_objs[key_at_index]
|
156
|
+
return sgt_obj
|
157
|
+
except IndexError:
|
158
|
+
logging.info("No Image Error: Please import/add an image.", extra={'user': 'SGT Logs'})
|
159
|
+
# self.showAlertSignal.emit("No Image Error", "No image added! Please import/add an image.")
|
160
|
+
return None
|
161
|
+
|
162
|
+
def create_sgt_object(self, img_path):
|
163
|
+
"""
|
164
|
+
A function that processes a selected image file and creates an analyzer object with default configurations.
|
165
|
+
|
166
|
+
Args:
|
167
|
+
img_path: file path to image
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
"""
|
171
|
+
|
172
|
+
success, result = verify_path(img_path)
|
173
|
+
if success:
|
174
|
+
img_path = result
|
175
|
+
else:
|
176
|
+
logging.info(result, extra={'user': 'SGT Logs'})
|
177
|
+
self.showAlertSignal.emit("File/Directory Error", result)
|
178
|
+
return False
|
179
|
+
|
180
|
+
# Create an SGT object as a GraphAnalyzer object.
|
181
|
+
try:
|
182
|
+
ntwk_p, img_file = ImageProcessor.create_imp_object(img_path, config_file="", allow_auto_scale=self.allow_auto_scale)
|
183
|
+
sgt_obj = GraphAnalyzer(ntwk_p)
|
184
|
+
|
185
|
+
# Store the StructuralGT object and sync application
|
186
|
+
self.sgt_objs[img_file] = sgt_obj
|
187
|
+
self.update_img_models(self.get_selected_sgt_obj())
|
188
|
+
self.update_graph_models(self.get_selected_sgt_obj())
|
189
|
+
return True
|
190
|
+
except Exception as err:
|
191
|
+
logging.exception("File Error: %s", err, extra={'user': 'SGT Logs'})
|
192
|
+
self.showAlertSignal.emit("File Error", "Error processing image. Try again.")
|
193
|
+
return False
|
194
|
+
|
195
|
+
def delete_sgt_object(self, index=None):
|
196
|
+
"""
|
197
|
+
Delete SGT Obj stored at the specified index (if not specified, get the current index).
|
198
|
+
"""
|
199
|
+
del_index = index if index is not None else self.selected_sgt_obj_index
|
200
|
+
if 0 <= del_index < len(self.sgt_objs): # Check if the index exists
|
201
|
+
keys_list = list(self.sgt_objs.keys())
|
202
|
+
key_at_del_index = keys_list[self.selected_sgt_obj_index]
|
203
|
+
# Delete the object at index
|
204
|
+
del self.sgt_objs[key_at_del_index]
|
205
|
+
# Update Data
|
206
|
+
img_list, img_cache = self.get_thumbnail_list()
|
207
|
+
self.imgThumbnailModel.update_data(img_list, img_cache)
|
208
|
+
self.selected_sgt_obj_index = 0
|
209
|
+
self.load_image(reload_thumbnails=True)
|
210
|
+
self.imageChangedSignal.emit()
|
211
|
+
|
212
|
+
def save_project_data(self):
|
213
|
+
"""
|
214
|
+
A handler function that handles saving project data.
|
215
|
+
Returns: True if successful, False otherwise.
|
216
|
+
|
217
|
+
"""
|
218
|
+
if not self.project_open:
|
219
|
+
return False
|
220
|
+
try:
|
221
|
+
file_path = self.project_data["file_path"]
|
222
|
+
with open(file_path, 'wb') as project_file: # type: Optional[SupportsWrite[bytes]]
|
223
|
+
pickle.dump(self.sgt_objs, project_file)
|
224
|
+
return True
|
225
|
+
except Exception as err:
|
226
|
+
logging.exception("Project Saving Error: %s", err, extra={'user': 'SGT Logs'})
|
227
|
+
self.showAlertSignal.emit("Save Error", "Unable to save project data. Close app and try again.")
|
228
|
+
return False
|
229
|
+
|
230
|
+
def get_thumbnail_list(self):
|
231
|
+
"""
|
232
|
+
Get names and base64 data of images to be used in Project List thumbnails.
|
233
|
+
"""
|
234
|
+
keys_list = list(self.sgt_objs.keys())
|
235
|
+
if len(keys_list) <= 0:
|
236
|
+
return None, None
|
237
|
+
item_data = []
|
238
|
+
image_cache = {}
|
239
|
+
for key in keys_list:
|
240
|
+
item_data.append([key]) # Store the key
|
241
|
+
sgt_obj = self.sgt_objs[key]
|
242
|
+
ntwk_p = sgt_obj.ntwk_p
|
243
|
+
sel_img_batch = ntwk_p.get_selected_batch()
|
244
|
+
img_cv = sel_img_batch.images[0].img_2d # First image, assuming OpenCV image format
|
245
|
+
base64_data = img_to_base64(img_cv)
|
246
|
+
image_cache[key] = base64_data # Store base64 string
|
247
|
+
return item_data, image_cache
|
248
|
+
|
249
|
+
def get_selected_images(self, img_view: str = None):
|
250
|
+
"""
|
251
|
+
Get selected images from a specific image batch.
|
252
|
+
"""
|
253
|
+
sgt_obj = self.get_selected_sgt_obj()
|
254
|
+
ntwk_p = sgt_obj.ntwk_p
|
255
|
+
sel_img_batch = ntwk_p.get_selected_batch()
|
256
|
+
sel_images = [sel_img_batch.images[i] for i in sel_img_batch.selected_images]
|
257
|
+
if img_view is not None:
|
258
|
+
sel_img_batch.current_view = img_view
|
259
|
+
return sel_images
|
260
|
+
|
261
|
+
def _handle_progress_update(self, progress_val: int, msg: str):
|
262
|
+
"""
|
263
|
+
Handler function for progress updates for ongoing tasks.
|
264
|
+
Args:
|
265
|
+
progress_val: Progress value, range is 0-100%.
|
266
|
+
msg: Progress message to be displayed.
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
|
270
|
+
"""
|
271
|
+
|
272
|
+
if 0 <= progress_val <= 100:
|
273
|
+
self.updateProgressSignal.emit(progress_val, msg)
|
274
|
+
logging.info(f"{progress_val}%: {msg}", extra={'user': 'SGT Logs'})
|
275
|
+
elif progress_val > 100:
|
276
|
+
self.updateProgressSignal.emit(progress_val, msg)
|
277
|
+
logging.info(f"{msg}", extra={'user': 'SGT Logs'})
|
278
|
+
else:
|
279
|
+
logging.exception("Error: %s", msg, extra={'user': 'SGT Logs'})
|
280
|
+
self.errorSignal.emit(msg)
|
281
|
+
|
282
|
+
def _handle_finished(self, success_val: bool, result: None | list | FiberNetworkBuilder | GraphAnalyzer):
|
283
|
+
"""
|
284
|
+
Handler function for sending updates/signals on termination of tasks.
|
285
|
+
Args:
|
286
|
+
success_val:
|
287
|
+
result:
|
288
|
+
|
289
|
+
Returns:
|
290
|
+
|
291
|
+
"""
|
292
|
+
self.wait_flag = False
|
293
|
+
if not success_val:
|
294
|
+
if type(result) is list:
|
295
|
+
logging.info(result[0] + ": " + result[1], extra={'user': 'SGT Logs'})
|
296
|
+
self.taskTerminatedSignal.emit(success_val, result)
|
297
|
+
elif type(result) is GraphAnalyzer:
|
298
|
+
pdf_saved = GraphAnalyzer.write_to_pdf(result, self._handle_progress_update)
|
299
|
+
if pdf_saved:
|
300
|
+
self._handle_finished(True, result)
|
301
|
+
else:
|
302
|
+
if type(result) is ImageProcessor:
|
303
|
+
self._handle_progress_update(100, "Graph extracted successfully!")
|
304
|
+
sgt_obj = self.get_selected_sgt_obj()
|
305
|
+
sgt_obj.ntwk_p = result
|
306
|
+
|
307
|
+
# Load the graph image to the app
|
308
|
+
self.changeImageSignal.emit()
|
309
|
+
|
310
|
+
# Send task termination signal to QML
|
311
|
+
self.taskTerminatedSignal.emit(success_val, [])
|
312
|
+
elif type(result) is GraphAnalyzer:
|
313
|
+
self._handle_progress_update(100, "GT PDF successfully generated!")
|
314
|
+
# Update graph properties
|
315
|
+
self.update_graph_models(self.get_selected_sgt_obj())
|
316
|
+
# Send task termination signal to QML
|
317
|
+
self.taskTerminatedSignal.emit(True, ["GT calculations completed", "The image's GT parameters have been "
|
318
|
+
"calculated. Check out generated PDF in "
|
319
|
+
"'Output Dir'."])
|
320
|
+
elif type(result) is dict:
|
321
|
+
self._handle_progress_update(100, "All GT PDF successfully generated!")
|
322
|
+
# Update graph properties
|
323
|
+
self.update_graph_models(self.get_selected_sgt_obj())
|
324
|
+
# Send task termination signal to QML
|
325
|
+
self.taskTerminatedSignal.emit(True, ["All GT calculations completed", "GT parameters of all "
|
326
|
+
"images have been calculated. Check "
|
327
|
+
"out all the generated PDFs in "
|
328
|
+
"'Output Dir'."])
|
329
|
+
else:
|
330
|
+
self.taskTerminatedSignal.emit(success_val, [])
|
331
|
+
|
332
|
+
@Slot(result=str)
|
333
|
+
def get_sgt_version(self):
|
334
|
+
""""""
|
335
|
+
# Copyright (C) 2024, the Regents of the University of Michigan.
|
336
|
+
# return f"StructuralGT v{__version__}, Computing: {COMPUTING_DEVICE}"
|
337
|
+
return f"v{__version__}"
|
338
|
+
|
339
|
+
@Slot(result=str)
|
340
|
+
def get_about_details(self):
|
341
|
+
about_app = (
|
342
|
+
"<html>"
|
343
|
+
"<p>"
|
344
|
+
"A software tool for performing Graph Theory analysis on <br>microscopy images. This is a modified version "
|
345
|
+
"of StructuralGT <br>initially proposed by Drew A. Vecchio,<br>"
|
346
|
+
"<b>DOI:</b> <a href='https://pubs.acs.org/doi/10.1021/acsnano.1c04711'>10.1021/acsnano.1c04711</a>"
|
347
|
+
"</p><p>"
|
348
|
+
"<b>Main Contributors:</b><br>"
|
349
|
+
"<table border='0.5' cellspacing='0' cellpadding='4'>"
|
350
|
+
# "<tr><th>Name</th><th>Email</th></tr>"
|
351
|
+
"<tr><td>Dickson Owuor</td><td>owuor@umich.edu</td></tr>"
|
352
|
+
"<tr><td>Nicolas Kotov</td><td>kotov@umich.edu</td></tr>"
|
353
|
+
"<tr><td>Alain Kadar</td><td>alaink@umich.edu</td></tr>"
|
354
|
+
"<tr><td>Xiong Ye Xiao</td><td>xiongyex@usc.edu</td></tr>"
|
355
|
+
"<tr><td>Kotov Lab</td><td></td></tr>"
|
356
|
+
"<tr><td>COMPASS</td><td></td></tr>"
|
357
|
+
"</table></p><p><br><br>"
|
358
|
+
"<b>Documentation:</b> <a href='https://structural-gt.readthedocs.io'>structural-gt.readthedocs.io</a>"
|
359
|
+
"<br>"
|
360
|
+
f"<b> Version: </b> {self.get_sgt_version()}<br>"
|
361
|
+
"<b>License:</b> GPL GNU v3"
|
362
|
+
"</p><p>"
|
363
|
+
"Copyright (C) 2018-2025<br>The Regents of the University of Michigan."
|
364
|
+
"</p>"
|
365
|
+
"</html>")
|
366
|
+
return about_app
|
367
|
+
|
368
|
+
@Slot(result=str)
|
369
|
+
def check_for_updates(self):
|
370
|
+
""""""
|
371
|
+
github_url = "https://raw.githubusercontent.com/owuordickson/structural-gt/refs/heads/main/src/StructuralGT/__init__.py"
|
372
|
+
|
373
|
+
try:
|
374
|
+
response = requests.get(github_url, timeout=5)
|
375
|
+
response.raise_for_status()
|
376
|
+
except requests.RequestException as e:
|
377
|
+
msg = f"Error checking for updates: {e}"
|
378
|
+
return msg
|
379
|
+
|
380
|
+
remote_version = None
|
381
|
+
for line in response.text.splitlines():
|
382
|
+
if line.strip().startswith("__install_version__"):
|
383
|
+
try:
|
384
|
+
remote_version = line.split("=")[1].strip().strip("\"'")
|
385
|
+
break
|
386
|
+
except IndexError:
|
387
|
+
msg = "Could not connect to server!"
|
388
|
+
return msg
|
389
|
+
|
390
|
+
if not remote_version:
|
391
|
+
msg = "Could not find the new version!"
|
392
|
+
return msg
|
393
|
+
|
394
|
+
new_version = version.parse(remote_version)
|
395
|
+
current_version = version.parse(__version__)
|
396
|
+
if new_version > current_version:
|
397
|
+
# https://github.com/owuordickson/structural-gt/releases/tag/v3.3.5
|
398
|
+
msg = (
|
399
|
+
"New version available!<br>"
|
400
|
+
f"Download via this <a href='https://github.com/owuordickson/structural-gt/releases/tag/v{remote_version}'>link</a>"
|
401
|
+
)
|
402
|
+
else:
|
403
|
+
msg = "No updates available."
|
404
|
+
return msg
|
405
|
+
|
406
|
+
@Slot(str, result=str)
|
407
|
+
def get_file_extensions(self, option):
|
408
|
+
if option == "img":
|
409
|
+
pattern_string = ' '.join(ALLOWED_IMG_EXTENSIONS)
|
410
|
+
return f"Image files ({pattern_string})"
|
411
|
+
elif option == "proj":
|
412
|
+
return "Project files (*.sgtproj)"
|
413
|
+
else:
|
414
|
+
return ""
|
415
|
+
|
416
|
+
@Slot(result=str)
|
417
|
+
def get_pixmap(self):
|
418
|
+
"""Returns the URL that QML should use to load the image"""
|
419
|
+
curr_img_view = np.random.randint(0, 4)
|
420
|
+
unique_num = self.selected_sgt_obj_index + curr_img_view + np.random.randint(low=21, high=1000)
|
421
|
+
return "image://imageProvider/" + str(unique_num)
|
422
|
+
|
423
|
+
@Slot(result=bool)
|
424
|
+
def is_img_3d(self):
|
425
|
+
sgt_obj = self.get_selected_sgt_obj()
|
426
|
+
if sgt_obj is None:
|
427
|
+
return False
|
428
|
+
sel_img_batch = sgt_obj.ntwk_p.get_selected_batch()
|
429
|
+
is_3d = not sel_img_batch.is_2d
|
430
|
+
return is_3d
|
431
|
+
|
432
|
+
@Slot(result=int)
|
433
|
+
def get_selected_img_batch(self):
|
434
|
+
sgt_obj = self.get_selected_sgt_obj()
|
435
|
+
return sgt_obj.ntwk_p.selected_batch
|
436
|
+
|
437
|
+
@Slot(result=str)
|
438
|
+
def get_selected_img_type(self):
|
439
|
+
sgt_obj = self.get_selected_sgt_obj()
|
440
|
+
sel_img_batch = sgt_obj.ntwk_p.get_selected_batch()
|
441
|
+
return sel_img_batch.current_view
|
442
|
+
|
443
|
+
@Slot(result=str)
|
444
|
+
def get_img_nav_location(self):
|
445
|
+
return f"{(self.selected_sgt_obj_index + 1)} / {len(self.sgt_objs)}"
|
446
|
+
|
447
|
+
@Slot(result=str)
|
448
|
+
def get_output_dir(self):
|
449
|
+
sgt_obj = self.get_selected_sgt_obj()
|
450
|
+
if sgt_obj is None:
|
451
|
+
return ""
|
452
|
+
return f"{sgt_obj.ntwk_p.output_dir}"
|
453
|
+
|
454
|
+
@Slot(result=bool)
|
455
|
+
def get_auto_scale(self):
|
456
|
+
return self.allow_auto_scale
|
457
|
+
|
458
|
+
@Slot(int)
|
459
|
+
def delete_selected_thumbnail(self, img_index):
|
460
|
+
"""Delete the selected image from the list."""
|
461
|
+
self.delete_sgt_object(img_index)
|
462
|
+
|
463
|
+
@Slot(str)
|
464
|
+
def set_output_dir(self, folder_path):
|
465
|
+
|
466
|
+
# Convert QML "file:///" path format to a proper OS path
|
467
|
+
if folder_path.startswith("file:///"):
|
468
|
+
if sys.platform.startswith("win"): # Windows Fix (remove extra '/')
|
469
|
+
folder_path = folder_path[8:]
|
470
|
+
else: # macOS/Linux (remove "file://")
|
471
|
+
folder_path = folder_path[7:]
|
472
|
+
folder_path = os.path.normpath(folder_path) # Normalize the path
|
473
|
+
|
474
|
+
# Update for all sgt_objs
|
475
|
+
key_list = list(self.sgt_objs.keys())
|
476
|
+
for key in key_list:
|
477
|
+
sgt_obj = self.sgt_objs[key]
|
478
|
+
sgt_obj.ntwk_p.output_dir = folder_path
|
479
|
+
self.imageChangedSignal.emit()
|
480
|
+
|
481
|
+
@Slot(bool)
|
482
|
+
def set_auto_scale(self, auto_scale):
|
483
|
+
"""Set the auto-scale parameter for each image."""
|
484
|
+
self.allow_auto_scale = auto_scale
|
485
|
+
|
486
|
+
@Slot(int)
|
487
|
+
def select_img_batch(self, batch_index=-1):
|
488
|
+
if batch_index < 0:
|
489
|
+
return
|
490
|
+
|
491
|
+
try:
|
492
|
+
sgt_obj = self.get_selected_sgt_obj()
|
493
|
+
sgt_obj.ntwk_p.select_image_batch(batch_index)
|
494
|
+
self.update_img_models(sgt_obj)
|
495
|
+
self.changeImageSignal.emit()
|
496
|
+
except Exception as err:
|
497
|
+
logging.exception("Batch Change Error: %s", err, extra={'user': 'SGT Logs'})
|
498
|
+
self.showAlertSignal.emit("Image Batch Error", f"Error encountered while trying to access batch "
|
499
|
+
f"{batch_index}. Restart app and try again.")
|
500
|
+
|
501
|
+
@Slot(int, bool)
|
502
|
+
def toggle_selected_batch_image(self, img_index, selected):
|
503
|
+
sgt_obj = self.get_selected_sgt_obj()
|
504
|
+
sel_img_batch = sgt_obj.ntwk_p.get_selected_batch()
|
505
|
+
if selected:
|
506
|
+
sel_img_batch.selected_images.add(img_index)
|
507
|
+
else:
|
508
|
+
sel_img_batch.selected_images.discard(img_index)
|
509
|
+
self.changeImageSignal.emit()
|
510
|
+
|
511
|
+
@Slot(str)
|
512
|
+
def toggle_current_img_view(self, choice: str = None):
|
513
|
+
"""
|
514
|
+
Change the view of the current image to either: original, binary, processed or graph.
|
515
|
+
|
516
|
+
:param choice: Selected view to be loaded.
|
517
|
+
"""
|
518
|
+
sgt_obj = self.get_selected_sgt_obj()
|
519
|
+
if sgt_obj is None:
|
520
|
+
return
|
521
|
+
sel_img_batch = sgt_obj.ntwk_p.get_selected_batch()
|
522
|
+
if choice is not None:
|
523
|
+
sel_img_batch.current_view = choice
|
524
|
+
self.changeImageSignal.emit()
|
525
|
+
|
526
|
+
@Slot(bool)
|
527
|
+
def reload_graph_image(self, only_giant_graph=False):
|
528
|
+
sgt_obj = self.get_selected_sgt_obj()
|
529
|
+
sel_img_batch = sgt_obj.ntwk_p.get_selected_batch()
|
530
|
+
sgt_obj.ntwk_p.draw_graph_image(sel_img_batch, show_giant_only=only_giant_graph)
|
531
|
+
self.changeImageSignal.emit()
|
532
|
+
|
533
|
+
@Slot()
|
534
|
+
def load_graph_simulation(self):
|
535
|
+
"""Render and visualize OVITO graph network simulation."""
|
536
|
+
try:
|
537
|
+
# Clear any existing scene
|
538
|
+
for p_line in list(scene.pipelines):
|
539
|
+
p_line.remove_from_scene()
|
540
|
+
|
541
|
+
# Create OVITO data pipeline
|
542
|
+
sgt_obj = self.get_selected_sgt_obj()
|
543
|
+
sel_batch = sgt_obj.ntwk_p.get_selected_batch()
|
544
|
+
pipeline = import_file(sel_batch.graph_obj.gsd_file)
|
545
|
+
pipeline.add_to_scene()
|
546
|
+
|
547
|
+
vp = Viewport(type=Viewport.Type.Perspective, camera_dir=(2, 1, -1))
|
548
|
+
ovito_widget = create_qwidget(vp, parent=self.qml_app.activeWindow())
|
549
|
+
ovito_widget.setMinimumSize(800, 500)
|
550
|
+
vp.zoom_all((800, 500))
|
551
|
+
ovito_widget.show()
|
552
|
+
|
553
|
+
"""
|
554
|
+
# Find the QML Rectangle to embed into
|
555
|
+
root = self.qml_engine.rootObjects()[0]
|
556
|
+
ntwk_container = root.findChild(QObject, "ntwkContainer")
|
557
|
+
|
558
|
+
if ntwk_container:
|
559
|
+
# Testing
|
560
|
+
# print(f"Found it! {type(ntwk_container)}")
|
561
|
+
# ntwk_container.setProperty("color", "#8b0000")
|
562
|
+
|
563
|
+
# Grab rectangle properties
|
564
|
+
x = ntwk_container.property("x")
|
565
|
+
y = ntwk_container.property("y")
|
566
|
+
w = ntwk_container.property("width")
|
567
|
+
h = ntwk_container.property("height")
|
568
|
+
|
569
|
+
# Create OVITO data pipeline
|
570
|
+
sgt_obj = self.get_selected_sgt_obj()
|
571
|
+
filename, out_dir = sgt_obj.ntwk_p.get_filenames()
|
572
|
+
gsd_filename = filename + "_skel.gsd"
|
573
|
+
gsd_file = str(os.path.join(out_dir, gsd_filename))
|
574
|
+
pipeline = import_file(gsd_file)
|
575
|
+
pipeline.add_to_scene()
|
576
|
+
|
577
|
+
vp = Viewport(type=Viewport.Type.Perspective, camera_dir=(2, 1, -1))
|
578
|
+
ovito_widget = create_qwidget(vp, parent=self.qml_app.activeWindow())
|
579
|
+
ovito_widget.setMinimumSize(800, 500)
|
580
|
+
vp.zoom_all((800, 500))
|
581
|
+
|
582
|
+
# Re-parent OVITO QWidget
|
583
|
+
ovito_widget.setGeometry(x, y, w, h)
|
584
|
+
ovito_widget.show()"""
|
585
|
+
except Exception as e:
|
586
|
+
print("Graph Simulation Error:", e)
|
587
|
+
|
588
|
+
@Slot(int)
|
589
|
+
def load_image(self, index=None, reload_thumbnails=False):
|
590
|
+
try:
|
591
|
+
if index is not None:
|
592
|
+
if index == self.selected_sgt_obj_index:
|
593
|
+
return
|
594
|
+
else:
|
595
|
+
self.selected_sgt_obj_index = index
|
596
|
+
|
597
|
+
if reload_thumbnails:
|
598
|
+
# Update the thumbnail list data (delete/add image)
|
599
|
+
img_list, img_cache = self.get_thumbnail_list()
|
600
|
+
self.imgThumbnailModel.update_data(img_list, img_cache)
|
601
|
+
|
602
|
+
# Load the SGT Object data of the selected image
|
603
|
+
self.update_img_models(self.get_selected_sgt_obj())
|
604
|
+
self.imgThumbnailModel.set_selected(self.selected_sgt_obj_index)
|
605
|
+
self.changeImageSignal.emit()
|
606
|
+
except Exception as err:
|
607
|
+
self.delete_sgt_object()
|
608
|
+
self.selected_sgt_obj_index = 0
|
609
|
+
logging.exception("Image Loading Error: %s", err, extra={'user': 'SGT Logs'})
|
610
|
+
self.showAlertSignal.emit("Image Error", "Error loading image. Try again.")
|
611
|
+
|
612
|
+
@Slot(result=bool)
|
613
|
+
def load_prev_image(self):
|
614
|
+
"""Load the previous image in the list into view."""
|
615
|
+
if self.selected_sgt_obj_index > 0:
|
616
|
+
self.selected_sgt_obj_index = self.selected_sgt_obj_index - 1
|
617
|
+
self.load_image()
|
618
|
+
return True
|
619
|
+
return False
|
620
|
+
|
621
|
+
@Slot(result=bool)
|
622
|
+
def load_next_image(self):
|
623
|
+
"""Load the next image in the list into view."""
|
624
|
+
if self.selected_sgt_obj_index < (len(self.sgt_objs) - 1):
|
625
|
+
self.selected_sgt_obj_index = self.selected_sgt_obj_index + 1
|
626
|
+
self.load_image()
|
627
|
+
return True
|
628
|
+
return False
|
629
|
+
|
630
|
+
@Slot()
|
631
|
+
def apply_img_ctrl_changes(self):
|
632
|
+
"""Retrieve settings from the model and send to Python."""
|
633
|
+
try:
|
634
|
+
sel_images = self.get_selected_images(img_view='processed')
|
635
|
+
if len(sel_images) <= 0:
|
636
|
+
return
|
637
|
+
for val in self.imgControlModel.list_data:
|
638
|
+
for img in sel_images:
|
639
|
+
img.configs[val["id"]]["value"] = val["value"]
|
640
|
+
self.changeImageSignal.emit()
|
641
|
+
except Exception as err:
|
642
|
+
logging.exception("Unable to Adjust Brightness/Contrast: " + str(err), extra={'user': 'SGT Logs'})
|
643
|
+
self.taskTerminatedSignal.emit(False, ["Unable to Adjust Brightness/Contrast",
|
644
|
+
"Error trying to adjust image brightness/contrast.Try again."])
|
645
|
+
|
646
|
+
@Slot()
|
647
|
+
def apply_microscopy_props_changes(self):
|
648
|
+
"""Retrieve settings from the model and send to Python."""
|
649
|
+
try:
|
650
|
+
sel_images = self.get_selected_images()
|
651
|
+
if len(sel_images) <= 0:
|
652
|
+
return
|
653
|
+
for val in self.microscopyPropsModel.list_data:
|
654
|
+
for img in sel_images:
|
655
|
+
img.configs[val["id"]]["value"] = val["value"]
|
656
|
+
img.get_pixel_width()
|
657
|
+
except Exception as err:
|
658
|
+
logging.exception("Unable to Update Microscopy Property Values: " + str(err), extra={'user': 'SGT Logs'})
|
659
|
+
self.taskTerminatedSignal.emit(False, ["Unable to Update Microscopy Values",
|
660
|
+
"Error trying to update microscopy property values.Try again."])
|
661
|
+
|
662
|
+
@Slot()
|
663
|
+
def apply_img_bin_changes(self):
|
664
|
+
"""Retrieve settings from the model and send to Python."""
|
665
|
+
try:
|
666
|
+
sel_images = self.get_selected_images()
|
667
|
+
if len(sel_images) <= 0:
|
668
|
+
return
|
669
|
+
for val in self.imgBinFilterModel.list_data:
|
670
|
+
for img in sel_images:
|
671
|
+
img.configs[val["id"]]["value"] = val["value"]
|
672
|
+
self.changeImageSignal.emit()
|
673
|
+
except Exception as err:
|
674
|
+
logging.exception("Apply Binary Image Filters: " + str(err), extra={'user': 'SGT Logs'})
|
675
|
+
self.taskTerminatedSignal.emit(False, ["Unable to Apply Binary Filters", "Error while tying to apply "
|
676
|
+
"binary filters to image. Try again."])
|
677
|
+
|
678
|
+
@Slot()
|
679
|
+
def apply_img_filter_changes(self):
|
680
|
+
"""Retrieve settings from the model and send to Python."""
|
681
|
+
try:
|
682
|
+
sel_images = self.get_selected_images()
|
683
|
+
if len(sel_images) <= 0:
|
684
|
+
return
|
685
|
+
for val in self.imgFilterModel.list_data:
|
686
|
+
for img in sel_images:
|
687
|
+
img.configs[val["id"]]["value"] = val["value"]
|
688
|
+
try:
|
689
|
+
img.configs[val["id"]]["dataValue"] = val["dataValue"]
|
690
|
+
except KeyError:
|
691
|
+
pass
|
692
|
+
self.changeImageSignal.emit()
|
693
|
+
except Exception as err:
|
694
|
+
logging.exception("Apply Image Filters: " + str(err), extra={'user': 'SGT Logs'})
|
695
|
+
self.taskTerminatedSignal.emit(False, ["Unable to Apply Image Filters", "Error while tying to apply "
|
696
|
+
"image filters. Try again."])
|
697
|
+
|
698
|
+
@Slot()
|
699
|
+
def apply_img_scaling(self):
|
700
|
+
"""Retrieve settings from the model and send to Python."""
|
701
|
+
try:
|
702
|
+
self.set_auto_scale(True)
|
703
|
+
sgt_obj = self.get_selected_sgt_obj()
|
704
|
+
sgt_obj.ntwk_p.auto_scale = self.allow_auto_scale
|
705
|
+
sel_img_batch = sgt_obj.ntwk_p.get_selected_batch()
|
706
|
+
sel_img_batch.scaling_options = self.imgScaleOptionModel.list_data
|
707
|
+
sgt_obj.ntwk_p.apply_img_scaling()
|
708
|
+
self.changeImageSignal.emit()
|
709
|
+
except Exception as err:
|
710
|
+
logging.exception("Apply Image Scaling: " + str(err), extra={'user': 'SGT Logs'})
|
711
|
+
self.taskTerminatedSignal.emit(False, ["Unable to Rescale Image", "Error while tying to re-scale "
|
712
|
+
"image. Try again."])
|
713
|
+
|
714
|
+
@Slot()
|
715
|
+
def export_graph_to_file(self):
|
716
|
+
"""Export graph data and save as a file."""
|
717
|
+
try:
|
718
|
+
sel_images = self.get_selected_images()
|
719
|
+
if len(sel_images) <= 0:
|
720
|
+
return
|
721
|
+
|
722
|
+
# 1. Get filename
|
723
|
+
sgt_obj = self.get_selected_sgt_obj()
|
724
|
+
ntwk_p = sgt_obj.ntwk_p
|
725
|
+
filename, out_dir = ntwk_p.get_filenames()
|
726
|
+
|
727
|
+
# 2. Update values
|
728
|
+
sel_img_batch = ntwk_p.get_selected_batch()
|
729
|
+
for val in self.exportGraphModel.list_data:
|
730
|
+
sel_img_batch.graph_obj.configs[val["id"]]["value"] = val["value"]
|
731
|
+
|
732
|
+
# 3. Save graph data to the file
|
733
|
+
sel_img_batch.graph_obj.save_graph_to_file(filename, out_dir)
|
734
|
+
self.taskTerminatedSignal.emit(True, ["Exporting Graph", "Exported files successfully stored in 'Output Dir'"])
|
735
|
+
except Exception as err:
|
736
|
+
logging.exception("Unable to Export Graph: " + str(err), extra={'user': 'SGT Logs'})
|
737
|
+
self.taskTerminatedSignal.emit(False, ["Unable to Export Graph", "Error exporting graph to file. Try again."])
|
738
|
+
|
739
|
+
@Slot()
|
740
|
+
def save_img_files(self):
|
741
|
+
"""Retrieve and save images to the file."""
|
742
|
+
try:
|
743
|
+
|
744
|
+
sel_images = self.get_selected_images()
|
745
|
+
if len(sel_images) <= 0:
|
746
|
+
return
|
747
|
+
for val in self.saveImgModel.list_data:
|
748
|
+
for img in sel_images:
|
749
|
+
img.configs[val["id"]]["value"] = val["value"]
|
750
|
+
sgt_obj = self.get_selected_sgt_obj()
|
751
|
+
sgt_obj.ntwk_p.save_images_to_file()
|
752
|
+
self.taskTerminatedSignal.emit(True,
|
753
|
+
["Save Images", "Image files successfully saved in 'Output Dir'"])
|
754
|
+
except Exception as err:
|
755
|
+
logging.exception("Unable to Save Image Files: " + str(err), extra={'user': 'SGT Logs'})
|
756
|
+
self.taskTerminatedSignal.emit(False,
|
757
|
+
["Unable to Save Image Files", "Error saving images to file. Try again."])
|
758
|
+
|
759
|
+
@Slot()
|
760
|
+
def run_extract_graph(self):
|
761
|
+
"""Retrieve settings from the model and send to Python."""
|
762
|
+
|
763
|
+
if self.wait_flag:
|
764
|
+
logging.info("Please Wait: Another Task Running!", extra={'user': 'SGT Logs'})
|
765
|
+
self.showAlertSignal.emit("Please Wait", "Another Task Running!")
|
766
|
+
return
|
767
|
+
|
768
|
+
self.worker_task = WorkerTask()
|
769
|
+
try:
|
770
|
+
self.wait_flag = True
|
771
|
+
sgt_obj = self.get_selected_sgt_obj()
|
772
|
+
|
773
|
+
self.worker = QThreadWorker(func=self.worker_task.task_extract_graph, args=(sgt_obj.ntwk_p,))
|
774
|
+
self.worker_task.inProgressSignal.connect(self._handle_progress_update)
|
775
|
+
self.worker_task.taskFinishedSignal.connect(self._handle_finished)
|
776
|
+
self.worker.start()
|
777
|
+
except Exception as err:
|
778
|
+
self.wait_flag = False
|
779
|
+
logging.exception("Graph Extraction Error: %s", err, extra={'user': 'SGT Logs'})
|
780
|
+
self._handle_progress_update(-1, "Fatal error occurred! Close the app and try again.")
|
781
|
+
self._handle_finished(False, ["Graph Extraction Error",
|
782
|
+
"Fatal error while trying to extract graph. "
|
783
|
+
"Close the app and try again."])
|
784
|
+
|
785
|
+
@Slot()
|
786
|
+
def run_graph_analyzer(self):
|
787
|
+
"""Retrieve settings from the model and send to Python."""
|
788
|
+
if self.wait_flag:
|
789
|
+
logging.info("Please Wait: Another Task Running!", extra={'user': 'SGT Logs'})
|
790
|
+
self.showAlertSignal.emit("Please Wait", "Another Task Running!")
|
791
|
+
return
|
792
|
+
|
793
|
+
self.worker_task = WorkerTask()
|
794
|
+
try:
|
795
|
+
self.wait_flag = True
|
796
|
+
sgt_obj = self.get_selected_sgt_obj()
|
797
|
+
|
798
|
+
self.worker = QThreadWorker(func=self.worker_task.task_compute_gt, args=(sgt_obj,))
|
799
|
+
self.worker_task.inProgressSignal.connect(self._handle_progress_update)
|
800
|
+
self.worker_task.taskFinishedSignal.connect(self._handle_finished)
|
801
|
+
self.worker.start()
|
802
|
+
except Exception as err:
|
803
|
+
self.wait_flag = False
|
804
|
+
logging.exception("GT Computation Error: %s", err, extra={'user': 'SGT Logs'})
|
805
|
+
self._handle_progress_update(-1, "Fatal error occurred! Close the app and try again.")
|
806
|
+
self._handle_finished(False, ["GT Computation Error",
|
807
|
+
"Fatal error while trying calculate GT parameters. "
|
808
|
+
"Close the app and try again."])
|
809
|
+
|
810
|
+
@Slot()
|
811
|
+
def run_multi_graph_analyzer(self):
|
812
|
+
""""""
|
813
|
+
if self.wait_flag:
|
814
|
+
logging.info("Please Wait: Another Task Running!", extra={'user': 'SGT Logs'})
|
815
|
+
self.showAlertSignal.emit("Please Wait", "Another Task Running!")
|
816
|
+
return
|
817
|
+
|
818
|
+
self.worker_task = WorkerTask()
|
819
|
+
try:
|
820
|
+
self.wait_flag = True
|
821
|
+
|
822
|
+
# Update Configs
|
823
|
+
current_sgt_obj = self.get_selected_sgt_obj()
|
824
|
+
keys_list = list(self.sgt_objs.keys())
|
825
|
+
key_at_current = keys_list[self.selected_sgt_obj_index]
|
826
|
+
shared_configs = current_sgt_obj.configs
|
827
|
+
for key in keys_list:
|
828
|
+
if key != key_at_current:
|
829
|
+
s_obj = self.sgt_objs[key]
|
830
|
+
s_obj.configs = shared_configs
|
831
|
+
|
832
|
+
self.worker = QThreadWorker(func=self.worker_task.task_compute_multi_gt, args=(self.sgt_objs,))
|
833
|
+
self.worker_task.inProgressSignal.connect(self._handle_progress_update)
|
834
|
+
self.worker_task.taskFinishedSignal.connect(self._handle_finished)
|
835
|
+
self.worker.start()
|
836
|
+
except Exception as err:
|
837
|
+
self.wait_flag = False
|
838
|
+
logging.exception("GT Computation Error: %s", err, extra={'user': 'SGT Logs'})
|
839
|
+
self._handle_progress_update(-1, "Fatal error occurred! Close the app and try again.")
|
840
|
+
self._handle_finished(False, ["GT Computation Error",
|
841
|
+
"Fatal error while trying calculate GT parameters. "
|
842
|
+
"Close the app and try again."])
|
843
|
+
|
844
|
+
@Slot(result=bool)
|
845
|
+
def run_save_project(self):
|
846
|
+
""""""
|
847
|
+
if self.wait_flag:
|
848
|
+
logging.info("Please Wait: Another Task Running!", extra={'user': 'SGT Logs'})
|
849
|
+
self.showAlertSignal.emit("Please Wait", "Another Task Running!")
|
850
|
+
return False
|
851
|
+
|
852
|
+
self.wait_flag = True
|
853
|
+
success_val = self.save_project_data()
|
854
|
+
self.wait_flag = False
|
855
|
+
return success_val
|
856
|
+
|
857
|
+
@Slot(result=bool)
|
858
|
+
def display_image(self):
|
859
|
+
return self.img_loaded
|
860
|
+
|
861
|
+
@Slot(result=bool)
|
862
|
+
def display_graph(self):
|
863
|
+
if len(self.sgt_objs) <= 0:
|
864
|
+
return False
|
865
|
+
|
866
|
+
sgt_obj = self.get_selected_sgt_obj()
|
867
|
+
sel_img_batch = sgt_obj.ntwk_p.get_selected_batch()
|
868
|
+
if sel_img_batch.graph_obj.img_ntwk is None:
|
869
|
+
return False
|
870
|
+
|
871
|
+
if sel_img_batch.current_view == "graph":
|
872
|
+
return True
|
873
|
+
return False
|
874
|
+
|
875
|
+
@Slot(result=bool)
|
876
|
+
def image_batches_exist(self):
|
877
|
+
if not self.img_loaded:
|
878
|
+
return False
|
879
|
+
|
880
|
+
sgt_obj = self.get_selected_sgt_obj()
|
881
|
+
batch_count = len(sgt_obj.ntwk_p.image_batches)
|
882
|
+
batches_exist = True if batch_count > 1 else False
|
883
|
+
return batches_exist
|
884
|
+
|
885
|
+
@Slot(result=bool)
|
886
|
+
def is_project_open(self):
|
887
|
+
return self.project_open
|
888
|
+
|
889
|
+
@Slot(result=bool)
|
890
|
+
def is_task_running(self):
|
891
|
+
return self.wait_flag
|
892
|
+
|
893
|
+
@Slot(bool)
|
894
|
+
def show_cropping_tool(self, allow_cropping):
|
895
|
+
self.showCroppingToolSignal.emit(allow_cropping)
|
896
|
+
|
897
|
+
@Slot(bool)
|
898
|
+
def perform_cropping(self, allowed):
|
899
|
+
self.performCroppingSignal.emit(allowed)
|
900
|
+
|
901
|
+
@Slot( int, int, int, int, int, int)
|
902
|
+
def crop_image(self, x, y, crop_width, crop_height, qimg_width, qimg_height):
|
903
|
+
"""Crop image using PIL and save it."""
|
904
|
+
try:
|
905
|
+
sgt_obj = self.get_selected_sgt_obj()
|
906
|
+
sgt_obj.ntwk_p.crop_image(x, y, crop_width, crop_height, qimg_width, qimg_height)
|
907
|
+
|
908
|
+
# Emit signal to update UI with new image
|
909
|
+
self.changeImageSignal.emit()
|
910
|
+
self.showCroppingToolSignal.emit(False)
|
911
|
+
self.showUnCroppingToolSignal.emit(True)
|
912
|
+
except Exception as err:
|
913
|
+
logging.exception("Cropping Error: %s", err, extra={'user': 'SGT Logs'})
|
914
|
+
self.showAlertSignal.emit("Cropping Error", "Error occurred while cropping image. Close the app and try again.")
|
915
|
+
|
916
|
+
@Slot(bool)
|
917
|
+
def undo_cropping(self, undo: bool = True):
|
918
|
+
if undo:
|
919
|
+
sgt_obj = self.get_selected_sgt_obj()
|
920
|
+
sgt_obj.ntwk_p.undo_cropping()
|
921
|
+
|
922
|
+
# Emit signal to update UI with new image
|
923
|
+
self.changeImageSignal.emit()
|
924
|
+
self.showUnCroppingToolSignal.emit(False)
|
925
|
+
|
926
|
+
@Slot(bool)
|
927
|
+
def enable_rectangular_selection(self, enabled):
|
928
|
+
self.enableRectangularSelectionSignal.emit(enabled)
|
929
|
+
|
930
|
+
@Slot(result=bool)
|
931
|
+
def enable_prev_nav_btn(self):
|
932
|
+
if (self.selected_sgt_obj_index == 0) or self.is_task_running():
|
933
|
+
return False
|
934
|
+
else:
|
935
|
+
return True
|
936
|
+
|
937
|
+
@Slot(result=bool)
|
938
|
+
def enable_next_nav_btn(self):
|
939
|
+
if (self.selected_sgt_obj_index == (len(self.sgt_objs) - 1)) or self.is_task_running():
|
940
|
+
return False
|
941
|
+
else:
|
942
|
+
return True
|
943
|
+
|
944
|
+
@Slot(str, result=bool)
|
945
|
+
def add_single_image(self, image_path):
|
946
|
+
"""Verify and validate an image path, use it to create an SGT object and load it in view."""
|
947
|
+
is_created = self.create_sgt_object(image_path)
|
948
|
+
if is_created:
|
949
|
+
# pos = (len(self.sgt_objs) - 1)
|
950
|
+
self.load_image(reload_thumbnails=True)
|
951
|
+
return True
|
952
|
+
return False
|
953
|
+
|
954
|
+
@Slot(str, result=bool)
|
955
|
+
def add_multiple_images(self, img_dir_path):
|
956
|
+
"""
|
957
|
+
Verify and validate multiple image paths, use each to create an SGT object, then load the last one in view.
|
958
|
+
"""
|
959
|
+
|
960
|
+
success, result = verify_path(img_dir_path)
|
961
|
+
if success:
|
962
|
+
img_dir_path = result
|
963
|
+
else:
|
964
|
+
logging.info(result, extra={'user': 'SGT Logs'})
|
965
|
+
self.showAlertSignal.emit("File/Directory Error", result)
|
966
|
+
return False
|
967
|
+
|
968
|
+
files = os.listdir(img_dir_path)
|
969
|
+
files = sorted(files)
|
970
|
+
for a_file in files:
|
971
|
+
allowed_extensions = tuple(ext[1:] if ext.startswith('*.') else ext for ext in ALLOWED_IMG_EXTENSIONS)
|
972
|
+
if a_file.endswith(allowed_extensions):
|
973
|
+
img_path = os.path.join(str(img_dir_path), a_file)
|
974
|
+
_ = self.create_sgt_object(img_path)
|
975
|
+
|
976
|
+
if len(self.sgt_objs) <= 0:
|
977
|
+
logging.info("File Error: Files have to be either .tif .png .jpg .jpeg", extra={'user': 'SGT Logs'})
|
978
|
+
self.showAlertSignal.emit("File Error", "No workable images found! Files have to be either .tif, .png, .jpg or .jpeg")
|
979
|
+
return False
|
980
|
+
else:
|
981
|
+
# pos = (len(self.sgt_objs) - 1)
|
982
|
+
self.load_image(reload_thumbnails=True)
|
983
|
+
return True
|
984
|
+
|
985
|
+
@Slot(str, str, result=bool)
|
986
|
+
def create_sgt_project(self, proj_name, dir_path):
|
987
|
+
"""Creates a '.sgtproj' inside the selected directory"""
|
988
|
+
|
989
|
+
self.project_open = False
|
990
|
+
success, result = verify_path(dir_path)
|
991
|
+
if success:
|
992
|
+
dir_path = result
|
993
|
+
else:
|
994
|
+
logging.info(result, extra={'user': 'SGT Logs'})
|
995
|
+
self.showAlertSignal.emit("File/Directory Error", result)
|
996
|
+
return False
|
997
|
+
|
998
|
+
proj_name += '.sgtproj'
|
999
|
+
proj_path = os.path.join(str(dir_path), proj_name)
|
1000
|
+
|
1001
|
+
try:
|
1002
|
+
if os.path.exists(proj_path):
|
1003
|
+
logging.info(f"Project '{proj_name}' already exists.", extra={'user': 'SGT Logs'})
|
1004
|
+
self.showAlertSignal.emit("Project Error", f"Error: Project '{proj_name}' already exists.")
|
1005
|
+
return False
|
1006
|
+
|
1007
|
+
# Open the file in the 'write' mode ('w').
|
1008
|
+
# This will create the file if it doesn't exist
|
1009
|
+
with open(proj_path, 'w'):
|
1010
|
+
pass # Do nothing, just create the file (updates will be done automatically/dynamically)
|
1011
|
+
|
1012
|
+
# Update and notify QML
|
1013
|
+
self.project_data["name"] = proj_name
|
1014
|
+
self.project_data["file_path"] = proj_path
|
1015
|
+
self.project_open = True
|
1016
|
+
self.projectOpenedSignal.emit(proj_name)
|
1017
|
+
logging.info(f"File '{proj_name}' created successfully in '{dir_path}'.", extra={'user': 'SGT Logs'})
|
1018
|
+
return True
|
1019
|
+
except Exception as err:
|
1020
|
+
logging.exception("Create Project Error: %s", err, extra={'user': 'SGT Logs'})
|
1021
|
+
self.showAlertSignal.emit("Create Project Error", "Failed to create SGT project. Close the app and try again.")
|
1022
|
+
return False
|
1023
|
+
|
1024
|
+
@Slot(str, result=bool)
|
1025
|
+
def open_sgt_project(self, sgt_path):
|
1026
|
+
"""Opens and loads the SGT project from the '.sgtproj' file"""
|
1027
|
+
if self.wait_flag:
|
1028
|
+
logging.info("Please Wait: Another Task Running!", extra={'user': 'SGT Logs'})
|
1029
|
+
self.showAlertSignal.emit("Please Wait", "Another Task Running!")
|
1030
|
+
return False
|
1031
|
+
|
1032
|
+
try:
|
1033
|
+
self.wait_flag = True
|
1034
|
+
self.project_open = False
|
1035
|
+
# Verify the path
|
1036
|
+
success, result = verify_path(sgt_path)
|
1037
|
+
if success:
|
1038
|
+
sgt_path = result
|
1039
|
+
else:
|
1040
|
+
logging.info(result, extra={'user': 'SGT Logs'})
|
1041
|
+
self.showAlertSignal.emit("File/Directory Error", result)
|
1042
|
+
self.wait_flag = False
|
1043
|
+
return False
|
1044
|
+
img_dir, proj_name = os.path.split(str(sgt_path))
|
1045
|
+
|
1046
|
+
# Read and load project data and SGT objects
|
1047
|
+
with open(str(sgt_path), 'rb') as sgt_file:
|
1048
|
+
self.sgt_objs = pickle.load(sgt_file)
|
1049
|
+
|
1050
|
+
if self.sgt_objs:
|
1051
|
+
key_list = list(self.sgt_objs.keys())
|
1052
|
+
for key in key_list:
|
1053
|
+
self.sgt_objs[key].ntwk_p.output_dir = img_dir
|
1054
|
+
|
1055
|
+
# Update and notify QML
|
1056
|
+
self.project_data["name"] = proj_name
|
1057
|
+
self.project_data["file_path"] = str(sgt_path)
|
1058
|
+
self.wait_flag = False
|
1059
|
+
self.project_open = True
|
1060
|
+
self.projectOpenedSignal.emit(proj_name)
|
1061
|
+
|
1062
|
+
# Load Image to GUI - activates QML
|
1063
|
+
self.load_image(reload_thumbnails=True)
|
1064
|
+
logging.info(f"File '{proj_name}' opened successfully in '{sgt_path}'.", extra={'user': 'SGT Logs'})
|
1065
|
+
return True
|
1066
|
+
except Exception as err:
|
1067
|
+
self.wait_flag = False
|
1068
|
+
logging.exception("Project Opening Error: %s", err, extra={'user': 'SGT Logs'})
|
1069
|
+
self.showAlertSignal.emit("Open Project Error", "Unable to open .sgtproj file! Try again. If the "
|
1070
|
+
"issue persists, the file may be corrupted or incompatible. "
|
1071
|
+
"Consider restoring from a backup or contacting support for "
|
1072
|
+
"assistance.")
|
1073
|
+
return False
|