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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/corrections.py +26 -0
- shinestacker/app/args_parser_opts.py +39 -0
- shinestacker/app/gui_utils.py +19 -2
- shinestacker/app/main.py +16 -25
- shinestacker/app/project.py +12 -21
- shinestacker/app/retouch.py +12 -20
- shinestacker/core/core_utils.py +2 -12
- shinestacker/core/logging.py +4 -3
- shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker/gui/ico/shinestacker_bkg.png +0 -0
- shinestacker/gui/tab_widget.py +1 -5
- shinestacker/retouch/adjustments.py +93 -0
- shinestacker/retouch/base_filter.py +63 -8
- shinestacker/retouch/denoise_filter.py +1 -1
- shinestacker/retouch/display_manager.py +1 -2
- shinestacker/retouch/image_editor_ui.py +39 -39
- shinestacker/retouch/image_viewer.py +17 -9
- shinestacker/retouch/io_gui_handler.py +96 -44
- shinestacker/retouch/io_threads.py +78 -0
- shinestacker/retouch/layer_collection.py +12 -0
- shinestacker/retouch/overlaid_view.py +13 -5
- shinestacker/retouch/paint_area_manager.py +30 -0
- shinestacker/retouch/sidebyside_view.py +3 -3
- shinestacker/retouch/transformation_manager.py +1 -2
- shinestacker/retouch/undo_manager.py +15 -13
- shinestacker/retouch/unsharp_mask_filter.py +13 -28
- shinestacker/retouch/view_strategy.py +65 -22
- shinestacker/retouch/vignetting_filter.py +1 -1
- {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/METADATA +2 -2
- {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/RECORD +35 -32
- shinestacker/gui/ico/focus_stack_bkg.png +0 -0
- shinestacker/retouch/io_manager.py +0 -69
- {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {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.
|
|
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(
|
|
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
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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':
|
|
15
|
-
|
|
16
|
-
'
|
|
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,
|
|
126
|
-
|
|
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.
|
|
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
|
-
|
|
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 .
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
self.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
self
|
|
160
|
-
|
|
161
|
-
|
|
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.
|
|
215
|
-
self.saver_thread.finished.connect(self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)
|