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,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