shinestacker 1.6.0__py3-none-any.whl → 1.7.0__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.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (37) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/corrections.py +26 -0
  3. shinestacker/app/args_parser_opts.py +39 -0
  4. shinestacker/app/gui_utils.py +19 -2
  5. shinestacker/app/main.py +16 -25
  6. shinestacker/app/project.py +12 -21
  7. shinestacker/app/retouch.py +12 -20
  8. shinestacker/core/core_utils.py +2 -12
  9. shinestacker/core/logging.py +4 -3
  10. shinestacker/gui/ico/shinestacker.icns +0 -0
  11. shinestacker/gui/ico/shinestacker_bkg.png +0 -0
  12. shinestacker/gui/tab_widget.py +1 -5
  13. shinestacker/retouch/adjustments.py +93 -0
  14. shinestacker/retouch/base_filter.py +63 -8
  15. shinestacker/retouch/denoise_filter.py +1 -1
  16. shinestacker/retouch/display_manager.py +1 -2
  17. shinestacker/retouch/image_editor_ui.py +39 -39
  18. shinestacker/retouch/image_viewer.py +17 -9
  19. shinestacker/retouch/io_gui_handler.py +96 -44
  20. shinestacker/retouch/io_threads.py +78 -0
  21. shinestacker/retouch/layer_collection.py +12 -0
  22. shinestacker/retouch/overlaid_view.py +13 -5
  23. shinestacker/retouch/paint_area_manager.py +30 -0
  24. shinestacker/retouch/sidebyside_view.py +3 -3
  25. shinestacker/retouch/transformation_manager.py +1 -2
  26. shinestacker/retouch/undo_manager.py +15 -13
  27. shinestacker/retouch/unsharp_mask_filter.py +13 -28
  28. shinestacker/retouch/view_strategy.py +65 -22
  29. shinestacker/retouch/vignetting_filter.py +1 -1
  30. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/METADATA +2 -2
  31. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/RECORD +35 -32
  32. shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  33. shinestacker/retouch/io_manager.py +0 -69
  34. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/WHEEL +0 -0
  35. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/entry_points.txt +0 -0
  36. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/licenses/LICENSE +0 -0
  37. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,7 @@ from .shortcuts_help import ShortcutsHelp
14
14
  from .brush import Brush
15
15
  from .brush_tool import BrushTool
16
16
  from .layer_collection import LayerCollectionHandler
17
+ from .paint_area_manager import PaintAreaManager
17
18
  from .undo_manager import UndoManager
18
19
  from .layer_collection import LayerCollection
19
20
  from .io_gui_handler import IOGuiHandler
@@ -23,6 +24,7 @@ from .denoise_filter import DenoiseFilter
23
24
  from .unsharp_mask_filter import UnsharpMaskFilter
24
25
  from .white_balance_filter import WhiteBalanceFilter
25
26
  from .vignetting_filter import VignettingFilter
27
+ from .adjustments import LumiContrastFilter, SaturationVibranceFilter
26
28
  from .transformation_manager import TransfromationManager
27
29
 
28
30
 
@@ -35,9 +37,9 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
35
37
  self.brush = Brush()
36
38
  self.brush_tool = BrushTool()
37
39
  self.modified = False
38
- self.mask_layer = None
39
40
  self.transformation_manager = TransfromationManager(self)
40
- self.undo_manager = UndoManager(self.transformation_manager)
41
+ self.paint_area_manager = PaintAreaManager()
42
+ self.undo_manager = UndoManager(self.transformation_manager, self.paint_area_manager)
41
43
  self.undo_action = None
42
44
  self.redo_action = None
43
45
  self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
@@ -49,13 +51,13 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
49
51
  central_widget = QWidget()
50
52
  self.setCentralWidget(central_widget)
51
53
  layout = QHBoxLayout(central_widget)
52
- self.image_viewer = ImageViewer(self.layer_collection)
54
+ self.image_viewer = ImageViewer(
55
+ self.layer_collection, self.brush_tool, self.paint_area_manager)
53
56
  self.image_viewer.connect_signals(
54
57
  self.handle_temp_view,
55
- self.begin_copy_brush_area,
56
- self.continue_copy_brush_area,
57
58
  self.end_copy_brush_area,
58
- self.handle_brush_size_change)
59
+ self.handle_brush_size_change,
60
+ self.handle_needs_update)
59
61
  side_panel = QWidget()
60
62
  side_layout = QVBoxLayout(side_panel)
61
63
  side_layout.setContentsMargins(0, 0, 0, 0)
@@ -295,6 +297,21 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
295
297
  transf_menu.addAction(rotate_180_action)
296
298
  edit_menu.addMenu(transf_menu)
297
299
 
300
+ adjust_menu = QMenu("&Adjust")
301
+ luminosity_action = QAction("Luminosity, Contrast", self)
302
+ luminosity_action.setProperty("requires_file", True)
303
+ luminosity_action.triggered.connect(self.luminosity_filter)
304
+ adjust_menu.addAction(luminosity_action)
305
+ saturation_action = QAction("Saturation, Vibrance", self)
306
+ saturation_action.setProperty("requires_file", True)
307
+ saturation_action.triggered.connect(self.saturation_filter)
308
+ adjust_menu.addAction(saturation_action)
309
+ white_balance_action = QAction("White Balance", self)
310
+ white_balance_action.setProperty("requires_file", True)
311
+ white_balance_action.triggered.connect(self.white_balance)
312
+ adjust_menu.addAction(white_balance_action)
313
+ edit_menu.addMenu(adjust_menu)
314
+
298
315
  edit_menu.addSeparator()
299
316
 
300
317
  copy_current_to_master_action = QAction("Copy Current Layer to Master", self)
@@ -342,6 +359,10 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
342
359
  self.mark_as_modified,
343
360
  self.view_strategy_menu.setEnabled
344
361
  )
362
+ self.filter_manager.register_filter(
363
+ "Luminosity, Contrast", LumiContrastFilter, *filter_handles)
364
+ self.filter_manager.register_filter(
365
+ "Saturation, Vibrance", SaturationVibranceFilter, *filter_handles)
345
366
  self.filter_manager.register_filter(
346
367
  "Denoise", DenoiseFilter, *filter_handles)
347
368
  self.filter_manager.register_filter(
@@ -461,10 +482,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
461
482
  unsharp_mask_action.setProperty("requires_file", True)
462
483
  unsharp_mask_action.triggered.connect(self.unsharp_mask)
463
484
  filter_menu.addAction(unsharp_mask_action)
464
- white_balance_action = QAction("White Balance", self)
465
- white_balance_action.setProperty("requires_file", True)
466
- white_balance_action.triggered.connect(self.white_balance)
467
- filter_menu.addAction(white_balance_action)
468
485
  vignetting_action = QAction("Vignetting Correction", self)
469
486
  vignetting_action.setProperty("requires_file", True)
470
487
  vignetting_action.triggered.connect(self.vignetting_correction)
@@ -629,35 +646,11 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
629
646
  self.mark_as_modified()
630
647
  self.statusBar().showMessage(f"Copied layer {self.current_layer_idx() + 1} to master")
631
648
 
632
- def copy_brush_area_to_master(self, view_pos):
633
- if self.layer_stack() is None or self.number_of_layers() == 0 \
634
- or self.display_manager.view_mode != 'master':
635
- return
636
- area = self.brush_tool.apply_brush_operation(
637
- self.master_layer_copy(),
638
- self.current_layer(),
639
- self.master_layer(), self.mask_layer,
640
- view_pos)
641
- self.undo_manager.extend_undo_area(*area)
642
-
643
- def begin_copy_brush_area(self, pos):
644
- if self.display_manager.view_mode == 'master':
645
- self.mask_layer = self.io_gui_handler.blank_layer.copy()
646
- self.copy_master_layer()
647
- self.undo_manager.reset_undo_area()
648
- self.copy_brush_area_to_master(pos)
649
- self.display_manager.needs_update = True
650
- if not self.display_manager.update_timer.isActive():
651
- self.display_manager.update_timer.start()
652
- self.mark_as_modified()
653
-
654
- def continue_copy_brush_area(self, pos):
655
- if self.display_manager.view_mode == 'master':
656
- self.copy_brush_area_to_master(pos)
657
- self.display_manager.needs_update = True
658
- if not self.display_manager.update_timer.isActive():
659
- self.display_manager.update_timer.start()
660
- self.mark_as_modified()
649
+ def handle_needs_update(self):
650
+ self.display_manager.needs_update = True
651
+ if not self.display_manager.update_timer.isActive():
652
+ self.display_manager.update_timer.start()
653
+ self.mark_as_modified()
661
654
 
662
655
  def end_copy_brush_area(self):
663
656
  if self.display_manager.update_timer.isActive():
@@ -667,6 +660,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
667
660
  self.mark_as_modified()
668
661
 
669
662
  def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):
663
+ self.image_viewer.update_brush_cursor()
670
664
  if self.undo_action:
671
665
  if has_undo:
672
666
  self.undo_action.setText(f"Undo {undo_desc}")
@@ -682,6 +676,12 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
682
676
  self.redo_action.setText("Redo")
683
677
  self.redo_action.setEnabled(False)
684
678
 
679
+ def luminosity_filter(self):
680
+ self.filter_manager.apply("Luminosity, Contrast")
681
+
682
+ def saturation_filter(self):
683
+ self.filter_manager.apply("Saturation, Vibrance")
684
+
685
685
  def denoise_filter(self):
686
686
  self.filter_manager.apply("Denoise")
687
687
 
@@ -7,13 +7,16 @@ from .sidebyside_view import SideBySideView, TopBottomView
7
7
 
8
8
 
9
9
  class ImageViewer(QWidget):
10
- def __init__(self, layer_collection, parent=None):
10
+ def __init__(self, layer_collection, brush_tool, paint_area_manager, parent=None):
11
11
  super().__init__(parent)
12
12
  self.status = ImageViewStatus()
13
13
  self._strategies = {
14
- 'overlaid': OverlaidView(layer_collection, self.status, self),
15
- 'sidebyside': SideBySideView(layer_collection, self.status, self),
16
- 'topbottom': TopBottomView(layer_collection, self.status, self)
14
+ 'overlaid':
15
+ OverlaidView(layer_collection, self.status, brush_tool, paint_area_manager, self),
16
+ 'sidebyside':
17
+ SideBySideView(layer_collection, self.status, brush_tool, paint_area_manager, self),
18
+ 'topbottom':
19
+ TopBottomView(layer_collection, self.status, brush_tool, paint_area_manager, self)
17
20
  }
18
21
  for strategy in self._strategies.values():
19
22
  strategy.hide()
@@ -48,12 +51,18 @@ class ImageViewer(QWidget):
48
51
  def set_master_image_np(self, img):
49
52
  self.strategy.set_master_image_np(img)
50
53
 
54
+ def arrange_images(self):
55
+ self.strategy.arrange_images()
56
+
51
57
  def show_master(self):
52
58
  self.strategy.show_master()
53
59
 
54
60
  def show_current(self):
55
61
  self.strategy.show_current()
56
62
 
63
+ def update_master_display_area(self):
64
+ self.strategy.update_master_display_area()
65
+
57
66
  def update_master_display(self):
58
67
  self.strategy.update_master_display()
59
68
 
@@ -122,12 +131,11 @@ class ImageViewer(QWidget):
122
131
  st.set_cursor_style(style)
123
132
 
124
133
  def connect_signals(
125
- self, handle_temp_view, begin_copy_brush_area, continue_copy_brush_area,
126
- end_copy_brush_area, handle_brush_size_change):
134
+ self, handle_temp_view, end_copy_brush_area,
135
+ handle_brush_size_change, handle_needs_update):
127
136
  for st in self._strategies.values():
128
137
  st.temp_view_requested.connect(handle_temp_view)
129
- st.brush_operation_started.connect(begin_copy_brush_area)
130
- st.brush_operation_continued.connect(continue_copy_brush_area)
131
- st.brush_operation_ended.connect(end_copy_brush_area)
138
+ st.end_copy_brush_area_requested.connect(end_copy_brush_area)
132
139
  st.brush_size_change_requested.connect(handle_brush_size_change)
140
+ st.needs_update_requested.connect(handle_needs_update)
133
141
  st.setFocusPolicy(Qt.StrongFocus)
@@ -1,13 +1,16 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0902, W0718
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0902, W0718, R0904, E1101
2
2
  import os
3
3
  import traceback
4
4
  import numpy as np
5
- from PySide6.QtWidgets import QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog, QApplication
5
+ import cv2
6
+ from PySide6.QtWidgets import (QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog,
7
+ QApplication, QProgressBar)
6
8
  from PySide6.QtGui import QGuiApplication, QCursor
7
9
  from PySide6.QtCore import Qt, QObject, QTimer, Signal
10
+ from .. algorithms.exif import get_exif, write_image_with_exif_data
8
11
  from .file_loader import FileLoader
9
12
  from .exif_data import ExifData
10
- from .io_manager import IOManager, FileMultilayerSaver
13
+ from .io_threads import FileMultilayerSaver, FrameImporter
11
14
  from .layer_collection import LayerCollectionHandler
12
15
 
13
16
 
@@ -22,13 +25,11 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
22
25
  def __init__(self, layer_collection, undo_manager, parent):
23
26
  QObject.__init__(self, parent)
24
27
  LayerCollectionHandler.__init__(self)
25
- self.io_manager = IOManager(layer_collection)
26
28
  self.undo_manager = undo_manager
27
29
  self.set_layer_collection(layer_collection)
28
30
  self.loader_thread = None
29
31
  self.display_manager = None
30
32
  self.image_viewer = None
31
- self.blank_layer = None
32
33
  self.loading_dialog = None
33
34
  self.loading_timer = None
34
35
  self.exif_dialog = None
@@ -37,6 +38,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
37
38
  self.saving_timer = None
38
39
  self.current_file_path_master = ''
39
40
  self.current_file_path_multi = ''
41
+ self.frame_importer_thread = None
42
+ self.frame_loading_dialog = None
43
+ self.frame_loading_timer = None
44
+ self.progress_label = None
45
+ self.progress_bar = None
46
+ self.exif_data = None
47
+ self.exif_path = ''
40
48
 
41
49
  def current_file_path(self):
42
50
  return self.current_file_path_master if self.save_master_only.isChecked() \
@@ -57,8 +65,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
57
65
  self.set_layer_labels(labels)
58
66
  self.set_master_layer(master_layer)
59
67
  self.image_viewer.set_master_image_np(master_layer)
68
+ self.set_blank_layer()
60
69
  self.undo_manager.reset()
61
- self.blank_layer = np.zeros(master_layer.shape[:2])
62
70
  self.finish_loading_setup(f"Loaded: {self.current_file_path()}")
63
71
  self.image_viewer.reset_zoom()
64
72
 
@@ -72,7 +80,40 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
72
80
  self.current_file_path_multi = ''
73
81
  self.status_message_requested.emit(f"Error loading: {self.current_file_path()}")
74
82
 
75
- def on_multilayer_save_success(self):
83
+ def on_frames_imported(self, stack, labels, master):
84
+ QApplication.restoreOverrideCursor()
85
+ self.frame_loading_timer.stop()
86
+ self.frame_loading_dialog.hide()
87
+ self.frame_loading_dialog.deleteLater()
88
+ empty_viewer = self.image_viewer.empty()
89
+ self.image_viewer.set_master_image_np(master)
90
+ if self.layer_stack() is None and len(stack) > 0:
91
+ self.set_layer_stack(np.array(stack))
92
+ if labels is None:
93
+ labels = self.layer_labels()
94
+ else:
95
+ self.set_layer_labels(labels)
96
+ self.set_master_layer(master)
97
+ self.set_blank_layer()
98
+ else:
99
+ if labels is None:
100
+ labels = self.layer_labels()
101
+ for img, label in zip(stack, labels):
102
+ self.add_layer_label(label)
103
+ self.add_layer(img)
104
+ self.finish_loading_setup("Selected frames imported")
105
+ if empty_viewer:
106
+ self.image_viewer.update_master_display()
107
+
108
+ def on_frames_import_error(self, error_msg):
109
+ QApplication.restoreOverrideCursor()
110
+ self.frame_loading_timer.stop()
111
+ self.frame_loading_dialog.hide()
112
+ self.frame_loading_dialog.deleteLater()
113
+ QMessageBox.critical(self.parent(), "Import Error", error_msg)
114
+ self.status_message_requested.emit("Error importing frames")
115
+
116
+ def on_multilayer_saved(self):
76
117
  QApplication.restoreOverrideCursor()
77
118
  self.saving_timer.stop()
78
119
  self.saving_dialog.hide()
@@ -90,6 +131,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
90
131
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {error_msg}")
91
132
 
92
133
  def open_file(self, file_paths=None):
134
+ self.cleanup_old_threads()
93
135
  if file_paths is None:
94
136
  file_paths, _ = QFileDialog.getOpenFileNames(
95
137
  self.parent(), "Open Image", "",
@@ -128,37 +170,37 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
128
170
  "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
129
171
  if file_paths:
130
172
  self.import_frames_from_files(file_paths)
131
- self.status_message_requested.emit("Imported selected frames")
132
173
 
133
174
  def import_frames_from_files(self, file_paths):
134
- empty_viewer = self.image_viewer.empty()
135
- try:
136
- stack, labels, master = self.io_manager.import_frames(file_paths)
137
- except Exception as e:
138
- msg = QMessageBox()
139
- msg.setIcon(QMessageBox.Critical)
140
- msg.setWindowTitle("Import error")
141
- msg.setText(str(e))
142
- msg.exec()
143
- return
144
- self.image_viewer.set_master_image_np(master)
145
- if self.layer_stack() is None and len(stack) > 0:
146
- self.set_layer_stack(np.array(stack))
147
- if labels is None:
148
- labels = self.layer_labels()
149
- else:
150
- self.set_layer_labels(labels)
151
- self.set_master_layer(master)
152
- self.blank_layer = np.zeros(master.shape[:2])
153
- else:
154
- if labels is None:
155
- labels = self.layer_labels()
156
- for img, label in zip(stack, labels):
157
- self.add_layer_label(label)
158
- self.add_layer(img)
159
- self.finish_loading_setup("Selected frames imported")
160
- if empty_viewer:
161
- self.image_viewer.reset_zoom()
175
+ self.cleanup_old_threads()
176
+ QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
177
+ self.frame_loading_dialog = QDialog(self.parent())
178
+ self.frame_loading_dialog.setWindowTitle("Loading Frames")
179
+ self.frame_loading_dialog.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
180
+ self.frame_loading_dialog.setModal(True)
181
+ layout = QVBoxLayout()
182
+ self.progress_label = QLabel("Frames loading...")
183
+ layout.addWidget(self.progress_label)
184
+ self.progress_bar = QProgressBar()
185
+ self.progress_bar.setRange(0, 100)
186
+ self.progress_bar.setValue(0)
187
+ layout.addWidget(self.progress_bar)
188
+ self.frame_loading_dialog.setLayout(layout)
189
+ self.frame_loading_timer = QTimer()
190
+ self.frame_loading_timer.setSingleShot(True)
191
+ self.frame_loading_timer.timeout.connect(self.frame_loading_dialog.show)
192
+ self.frame_loading_timer.start(100)
193
+ self.frame_importer_thread = FrameImporter(file_paths, self.master_layer())
194
+ self.frame_importer_thread.finished.connect(self.on_frames_imported)
195
+ self.frame_importer_thread.error.connect(self.on_frames_import_error)
196
+ self.frame_importer_thread.progress.connect(self.update_import_progress)
197
+ self.frame_importer_thread.start()
198
+
199
+ def update_import_progress(self, percent, filename):
200
+ if hasattr(self, 'progress_bar'):
201
+ self.progress_bar.setValue(percent)
202
+ if hasattr(self, 'progress_label'):
203
+ self.progress_label.setText(f"Loading: {filename} ({percent}%)")
162
204
 
163
205
  def finish_loading_setup(self, message):
164
206
  self.display_manager.update_thumbnails()
@@ -203,6 +245,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
203
245
  self.save_multilayer_to_path(path)
204
246
 
205
247
  def save_multilayer_to_path(self, path):
248
+ self.cleanup_old_threads()
206
249
  try:
207
250
  master_layer = {'Master': self.master_layer().copy()}
208
251
  individual_layers = dict(zip(
@@ -211,8 +254,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
211
254
  ))
212
255
  images_dict = {**master_layer, **individual_layers}
213
256
  self.saver_thread = FileMultilayerSaver(
214
- images_dict, path, exif_path=self.io_manager.exif_path)
215
- self.saver_thread.finished.connect(self.on_multilayer_save_success)
257
+ images_dict, path, exif_path=self.exif_path)
258
+ self.saver_thread.finished.connect(self.on_multilayer_saved)
216
259
  self.saver_thread.error.connect(self.on_multilayer_save_error)
217
260
  QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
218
261
  self.saving_dialog = QDialog(self.parent())
@@ -250,12 +293,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
250
293
 
251
294
  def save_master_to_path(self, path):
252
295
  try:
253
- self.io_manager.save_master(path)
296
+ img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
297
+ write_image_with_exif_data(self.exif_data, img, path)
254
298
  self.current_file_path_master = os.path.abspath(path)
255
- self.mark_as_modified_requested.emit(False)
299
+ # self.mark_as_modified_requested.emit(False)
256
300
  self.update_title_requested.emit()
257
- self.status_message_requested.emit(f"Saved master layer to: {path}")
258
301
  self.add_recent_file_requested.emit(self.current_file_path_master)
302
+ self.status_message_requested.emit(f"Saved master layer to: {path}")
259
303
  except Exception as e:
260
304
  traceback.print_tb(e.__traceback__)
261
305
  QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
@@ -263,14 +307,14 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
263
307
  def select_exif_path(self):
264
308
  path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
265
309
  if path:
266
- self.io_manager.set_exif_data(path)
310
+ self.exif_path = path
311
+ self.exif_data = get_exif(path)
267
312
  self.status_message_requested.emit(f"EXIF data extracted from {path}.")
268
- self.exif_dialog = ExifData(self.io_manager.exif_data, self.parent())
313
+ self.exif_dialog = ExifData(self.exif_data, self.parent())
269
314
  self.exif_dialog.exec()
270
315
 
271
316
  def close_file(self):
272
317
  self.mark_as_modified_requested.emit(False)
273
- self.blank_layer = None
274
318
  self.layer_collection.reset()
275
319
  self.current_file_path_master = ''
276
320
  self.current_file_path_multi = ''
@@ -281,3 +325,11 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
281
325
  self.update_title_requested.emit()
282
326
  self.set_enabled_file_open_close_actions_requested.emit(False)
283
327
  self.status_message_requested.emit("File closed")
328
+
329
+ def cleanup_old_threads(self):
330
+ if self.loader_thread and self.loader_thread.isFinished():
331
+ self.loader_thread = None
332
+ if self.frame_importer_thread and self.frame_importer_thread.isFinished():
333
+ self.frame_importer_thread = None
334
+ if self.saver_thread and self.saver_thread.isFinished():
335
+ self.saver_thread = None
@@ -0,0 +1,78 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, E1101, W0718, R0903, R0914
2
+
3
+ # import time
4
+ import os
5
+ import traceback
6
+ import cv2
7
+ from PySide6.QtCore import QThread, Signal
8
+ from .. algorithms.utils import read_img, validate_image, get_img_metadata
9
+ from .. algorithms.multilayer import write_multilayer_tiff_from_images
10
+
11
+
12
+ class FileMultilayerSaver(QThread):
13
+ finished = Signal()
14
+ error = Signal(str)
15
+
16
+ def __init__(self, images_dict, path, exif_path=None):
17
+ super().__init__()
18
+ self.images_dict = images_dict
19
+ self.path = path
20
+ self.exif_path = exif_path
21
+
22
+ def run(self):
23
+ try:
24
+ write_multilayer_tiff_from_images(
25
+ self.images_dict, self.path, exif_path=self.exif_path)
26
+ self.finished.emit()
27
+ except Exception as e:
28
+ traceback.print_tb(e.__traceback__)
29
+ self.error.emit(str(e))
30
+
31
+
32
+ class FrameImporter(QThread):
33
+ finished = Signal(object, object, object)
34
+ error = Signal(str)
35
+ progress = Signal(int, str)
36
+
37
+ def __init__(self, file_paths, master_layer):
38
+ super().__init__()
39
+ self.file_paths = file_paths
40
+ self.master_layer = master_layer
41
+
42
+ def run(self):
43
+ try:
44
+ stack = []
45
+ labels = []
46
+ master = None
47
+ current_master = self.master_layer
48
+ shape, dtype = None, None
49
+ if current_master is not None:
50
+ shape, dtype = get_img_metadata(current_master)
51
+ total_files = len(self.file_paths)
52
+ for i, path in enumerate(self.file_paths):
53
+ progress_percent = int((i / total_files) * 100)
54
+ self.progress.emit(progress_percent, os.path.basename(path))
55
+ try:
56
+ label = path.split("/")[-1].split(".")[0]
57
+ img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
58
+ if shape is not None and dtype is not None:
59
+ validate_image(img, shape, dtype)
60
+ else:
61
+ shape, dtype = get_img_metadata(img)
62
+ label_x = label
63
+ counter = 0
64
+ while label_x in labels:
65
+ counter += 1
66
+ label_x = f"{label} ({counter})"
67
+ labels.append(label_x)
68
+ stack.append(img)
69
+ if master is None:
70
+ master = img.copy()
71
+ # Add delay for testing
72
+ # time.sleep(0.2)
73
+ except Exception as e:
74
+ raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
75
+ self.progress.emit(100, "Complete")
76
+ self.finished.emit(stack, labels, master)
77
+ except Exception as e:
78
+ self.error.emit(str(e))
@@ -7,6 +7,7 @@ class LayerCollection:
7
7
  self.master_layer = None
8
8
  self.master_layer_copy = None
9
9
  self.layer_stack = None
10
+ self.blank_layer = None
10
11
  self.layer_labels = []
11
12
  self.current_layer_idx = 0
12
13
  self.sorted_indices = None
@@ -15,6 +16,7 @@ class LayerCollection:
15
16
  self.master_layer = None
16
17
  self.master_layer_copy = None
17
18
  self.layer_stack = None
19
+ self.blank_layer = None
18
20
  self.layer_labels = []
19
21
  self.current_layer_idx = 0
20
22
  self.sorted_indices = None
@@ -62,6 +64,10 @@ class LayerCollection:
62
64
  def set_master_layer(self, img):
63
65
  self.master_layer = img
64
66
 
67
+ def set_blank_layer(self):
68
+ if self.master_layer is not None:
69
+ self.blank_layer = np.zeros(self.master_layer.shape[:2])
70
+
65
71
  def restore_master_layer(self):
66
72
  self.master_layer = self.master_layer_copy.copy()
67
73
 
@@ -122,6 +128,9 @@ class LayerCollectionHandler:
122
128
  def current_layer(self):
123
129
  return self.layer_collection.current_layer()
124
130
 
131
+ def blank_layer(self):
132
+ return self.layer_collection.blank_layer
133
+
125
134
  def layer_stack(self):
126
135
  return self.layer_collection.layer_stack
127
136
 
@@ -152,6 +161,9 @@ class LayerCollectionHandler:
152
161
  def set_master_layer(self, img):
153
162
  self.layer_collection.set_master_layer(img)
154
163
 
164
+ def set_blank_layer(self):
165
+ self.layer_collection.set_blank_layer()
166
+
155
167
  def add_layer_label(self, label):
156
168
  self.layer_collection.add_layer_label(label)
157
169
 
@@ -1,11 +1,11 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, E1101, R0904, R0912, R0914, R0902, E0202
1
+ # pylint: disable=C0114, C0115, C0116, E0611, E1101, R0904, R0912, R0914, R0902, E0202, R0913, R0917
2
2
  from PySide6.QtCore import Qt, QPointF, QEvent, QRectF
3
3
  from .view_strategy import ViewStrategy, ImageGraphicsViewBase, ViewSignals
4
4
 
5
5
 
6
6
  class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
7
- def __init__(self, layer_collection, status, parent):
8
- ViewStrategy.__init__(self, layer_collection, status)
7
+ def __init__(self, layer_collection, status, brush_tool, paint_area_manager, parent):
8
+ ViewStrategy.__init__(self, layer_collection, status, brush_tool, paint_area_manager)
9
9
  ImageGraphicsViewBase.__init__(self, parent)
10
10
  self.scene = self.create_scene(self)
11
11
  self.create_pixmaps()
@@ -121,6 +121,7 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
121
121
  self.pixmap_item_master.setVisible(True)
122
122
  self.pixmap_item_current.setVisible(False)
123
123
  self.show_brush_preview()
124
+ self.enable_paint = True
124
125
  if self.brush_cursor:
125
126
  self.scene.removeItem(self.brush_cursor)
126
127
  self.brush_cursor = self.create_circle(self.scene)
@@ -130,21 +131,28 @@ class OverlaidView(ViewStrategy, ImageGraphicsViewBase, ViewSignals):
130
131
  self.pixmap_item_master.setVisible(False)
131
132
  self.pixmap_item_current.setVisible(True)
132
133
  self.hide_brush_preview()
134
+ self.enable_paint = False
133
135
  if self.brush_cursor:
134
136
  self.scene.removeItem(self.brush_cursor)
135
137
  self.brush_cursor = self.create_alt_circle(self.scene)
136
138
  self.update_brush_cursor()
137
139
 
140
+ def master_is_visible(self):
141
+ return self.pixmap_item_master.isVisible()
142
+
143
+ def current_is_visible(self):
144
+ return self.pixmap_item_current.isVisible()
145
+
138
146
  def arrange_images(self):
139
147
  if self.empty():
140
148
  return
141
- if self.pixmap_item_master.isVisible():
149
+ if self.master_is_visible():
142
150
  pixmap = self.pixmap_item_master.pixmap()
143
151
  if not pixmap.isNull():
144
152
  self.setSceneRect(QRectF(pixmap.rect()))
145
153
  self.centerOn(self.pixmap_item_master)
146
154
  self.center_image(self)
147
- elif self.pixmap_item_current.isVisible():
155
+ elif self.current_is_visible():
148
156
  pixmap = self.pixmap_item_current.pixmap()
149
157
  if not pixmap.isNull():
150
158
  self.setSceneRect(QRectF(pixmap.rect()))
@@ -0,0 +1,30 @@
1
+ # pylint: disable=C0114, C0115, C0116
2
+ from .. config.gui_constants import gui_constants
3
+
4
+
5
+ class PaintAreaManager:
6
+ def __init__(self):
7
+ self.x_start = None
8
+ self.y_start = None
9
+ self.x_end = None
10
+ self.y_end = None
11
+ self.reset()
12
+
13
+ def reset(self):
14
+ self.x_end = self.y_end = 0
15
+ self.x_start = self.y_start = gui_constants.MAX_UNDO_SIZE
16
+
17
+ def extend(self, x_start, y_start, x_end, y_end):
18
+ self.x_start = min(self.x_start, x_start)
19
+ self.y_start = min(self.y_start, y_start)
20
+ self.x_end = max(self.x_end, x_end)
21
+ self.y_end = max(self.y_end, y_end)
22
+
23
+ def area(self):
24
+ return self.x_start, self.y_start, self.x_end, self.y_end
25
+
26
+ def set_area(self, x_start, y_start, x_end, y_end):
27
+ self.x_start = x_start
28
+ self.y_start = y_start
29
+ self.x_end = x_end
30
+ self.y_end = y_end
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, R0904, R0915, E0611, R0902, R0911, R0914, E1003
1
+ # pylint: disable=C0114, C0115, C0116, R0904, R0915, E0611, R0902, R0911, R0914, E1003, R0913, R0917
2
2
  import time
3
3
  from PySide6.QtCore import Qt, Signal, QEvent, QRectF
4
4
  from PySide6.QtGui import QCursor
@@ -39,8 +39,8 @@ class ImageGraphicsView(ImageGraphicsViewBase):
39
39
 
40
40
 
41
41
  class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
42
- def __init__(self, layer_collection, status, parent):
43
- ViewStrategy.__init__(self, layer_collection, status)
42
+ def __init__(self, layer_collection, status, brush_tool, paint_area_manager, parent):
43
+ ViewStrategy.__init__(self, layer_collection, status, brush_tool, paint_area_manager)
44
44
  QWidget.__init__(self, parent)
45
45
  self.current_view = ImageGraphicsView(parent)
46
46
  self.master_view = ImageGraphicsView(parent)