shinestacker 1.5.4__py3-none-any.whl → 1.6.1__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/multilayer.py +1 -1
- shinestacker/algorithms/stack.py +17 -9
- shinestacker/app/args_parser_opts.py +4 -0
- shinestacker/app/gui_utils.py +10 -2
- shinestacker/app/main.py +8 -3
- shinestacker/app/project.py +7 -3
- shinestacker/app/retouch.py +8 -1
- shinestacker/app/settings_dialog.py +171 -0
- shinestacker/config/app_config.py +30 -0
- shinestacker/config/constants.py +3 -0
- shinestacker/config/gui_constants.py +4 -2
- shinestacker/config/settings.py +110 -0
- shinestacker/core/core_utils.py +3 -12
- shinestacker/core/logging.py +3 -2
- shinestacker/gui/action_config.py +6 -5
- shinestacker/gui/action_config_dialog.py +17 -74
- shinestacker/gui/config_dialog.py +78 -0
- shinestacker/gui/main_window.py +6 -6
- shinestacker/gui/menu_manager.py +2 -0
- shinestacker/gui/new_project.py +2 -1
- shinestacker/gui/project_controller.py +8 -6
- shinestacker/gui/project_model.py +16 -1
- shinestacker/gui/recent_file_manager.py +3 -21
- shinestacker/retouch/base_filter.py +1 -1
- shinestacker/retouch/display_manager.py +48 -7
- shinestacker/retouch/image_editor_ui.py +27 -36
- shinestacker/retouch/image_view_status.py +4 -1
- 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 +32 -16
- shinestacker/retouch/transformation_manager.py +1 -3
- shinestacker/retouch/undo_manager.py +15 -13
- shinestacker/retouch/view_strategy.py +79 -26
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/METADATA +1 -1
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/RECORD +44 -39
- shinestacker/retouch/io_manager.py +0 -69
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.5.4.dist-info → shinestacker-1.6.1.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0902
|
|
2
|
-
from PySide6.QtCore import QObject, QRectF
|
|
2
|
+
from PySide6.QtCore import QObject, QRectF, Signal
|
|
3
3
|
from PySide6.QtGui import QPixmap
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ImageViewStatus(QObject):
|
|
7
|
+
set_zoom_factor_requested = Signal(float)
|
|
8
|
+
|
|
7
9
|
def __init__(self, parent=None):
|
|
8
10
|
super().__init__(parent)
|
|
9
11
|
self.pixmap_master = QPixmap()
|
|
@@ -53,6 +55,7 @@ class ImageViewStatus(QObject):
|
|
|
53
55
|
|
|
54
56
|
def set_zoom_factor(self, zoom_factor):
|
|
55
57
|
self.zoom_factor = zoom_factor
|
|
58
|
+
self.set_zoom_factor_requested.emit(zoom_factor)
|
|
56
59
|
|
|
57
60
|
def set_min_scale(self, min_scale):
|
|
58
61
|
self.min_scale = min_scale
|
|
@@ -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)
|
|
@@ -61,6 +61,7 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
|
|
|
61
61
|
self.master_view.setFocusPolicy(Qt.NoFocus)
|
|
62
62
|
self.current_brush_cursor = None
|
|
63
63
|
self.last_color_update_time_current = 0
|
|
64
|
+
self._updating_scrollbars = False
|
|
64
65
|
|
|
65
66
|
def setup_layout(self):
|
|
66
67
|
raise NotImplementedError("Subclasses must implement setup_layout")
|
|
@@ -78,14 +79,25 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
|
|
|
78
79
|
self.master_view.mouse_moved.connect(self.handle_master_mouse_move)
|
|
79
80
|
self.master_view.mouse_released.connect(self.handle_master_mouse_release)
|
|
80
81
|
self.master_view.gesture_event.connect(self.handle_gesture_event)
|
|
82
|
+
|
|
83
|
+
def sync_scrollbars(source, target):
|
|
84
|
+
if not self._updating_scrollbars:
|
|
85
|
+
self._updating_scrollbars = True
|
|
86
|
+
target.setValue(source.value())
|
|
87
|
+
self._updating_scrollbars = False
|
|
88
|
+
|
|
81
89
|
self.current_view.horizontalScrollBar().valueChanged.connect(
|
|
82
|
-
self.
|
|
90
|
+
lambda value: sync_scrollbars(self.current_view.horizontalScrollBar(),
|
|
91
|
+
self.master_view.horizontalScrollBar()))
|
|
83
92
|
self.current_view.verticalScrollBar().valueChanged.connect(
|
|
84
|
-
self.
|
|
93
|
+
lambda value: sync_scrollbars(self.current_view.verticalScrollBar(),
|
|
94
|
+
self.master_view.verticalScrollBar()))
|
|
85
95
|
self.master_view.horizontalScrollBar().valueChanged.connect(
|
|
86
|
-
self.
|
|
96
|
+
lambda value: sync_scrollbars(self.master_view.horizontalScrollBar(),
|
|
97
|
+
self.current_view.horizontalScrollBar()))
|
|
87
98
|
self.master_view.verticalScrollBar().valueChanged.connect(
|
|
88
|
-
self.
|
|
99
|
+
lambda value: sync_scrollbars(self.master_view.verticalScrollBar(),
|
|
100
|
+
self.current_view.verticalScrollBar()))
|
|
89
101
|
self.current_view.wheel_event.connect(self.handle_wheel_event)
|
|
90
102
|
self.master_view.wheel_event.connect(self.handle_wheel_event)
|
|
91
103
|
# pylint: disable=C0103, W0201
|
|
@@ -377,28 +389,32 @@ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
|
|
|
377
389
|
def set_master_image(self, qimage):
|
|
378
390
|
self.status.set_master_image(qimage)
|
|
379
391
|
pixmap = self.status.pixmap_master
|
|
380
|
-
|
|
392
|
+
pixmap_rect = QRectF(pixmap.rect())
|
|
393
|
+
self.pixmap_item_master.setPos(0, 0)
|
|
394
|
+
self.master_view.setSceneRect(pixmap_rect)
|
|
395
|
+
self.master_scene.setSceneRect(pixmap_rect)
|
|
381
396
|
self.pixmap_item_master.setPixmap(pixmap)
|
|
382
|
-
|
|
397
|
+
_img_width, _img_height, scale_factor = self.setup_view_image(self.master_view, pixmap)
|
|
383
398
|
self.master_view.resetTransform()
|
|
384
399
|
self.master_view.scale(scale_factor, scale_factor)
|
|
385
400
|
self.master_view.centerOn(self.pixmap_item_master)
|
|
386
|
-
center = self.master_scene.sceneRect().center()
|
|
387
|
-
self.brush_preview.setPos(max(0, min(center.x(), img_width)),
|
|
388
|
-
max(0, min(center.y(), img_height)))
|
|
389
|
-
self.master_scene.setSceneRect(QRectF(self.pixmap_item_master.boundingRect()))
|
|
390
401
|
self.center_image(self.master_view)
|
|
391
402
|
self.update_cursor_pen_width()
|
|
392
403
|
|
|
393
404
|
def set_current_image(self, qimage):
|
|
394
405
|
self.status.set_current_image(qimage)
|
|
395
406
|
pixmap = self.status.pixmap_current
|
|
396
|
-
|
|
407
|
+
pixmap_rect = QRectF(pixmap.rect())
|
|
408
|
+
self.pixmap_item_current.setPos(0, 0)
|
|
409
|
+
self.current_view.setSceneRect(pixmap_rect)
|
|
410
|
+
self.current_scene.setSceneRect(pixmap_rect)
|
|
397
411
|
self.pixmap_item_current.setPixmap(pixmap)
|
|
412
|
+
_img_width, _img_height, scale_factor = self.setup_view_image(self.current_view, pixmap)
|
|
398
413
|
self.current_view.resetTransform()
|
|
399
|
-
self.
|
|
400
|
-
self.
|
|
414
|
+
self.current_view.scale(scale_factor, scale_factor)
|
|
415
|
+
self.current_view.centerOn(self.pixmap_item_current)
|
|
401
416
|
self.center_image(self.current_view)
|
|
417
|
+
self.update_cursor_pen_width()
|
|
402
418
|
|
|
403
419
|
def arrange_images(self):
|
|
404
420
|
if self.status.empty():
|
|
@@ -16,8 +16,7 @@ class TransfromationManager(LayerCollectionHandler):
|
|
|
16
16
|
if undoable:
|
|
17
17
|
try:
|
|
18
18
|
undo = self.editor.undo_manager
|
|
19
|
-
undo.
|
|
20
|
-
undo.y_start, undo.y_stop = 0, 1
|
|
19
|
+
undo.set_paint_area(0, 1, 0, 1)
|
|
21
20
|
undo.save_undo_state(self.editor.master_layer(), label)
|
|
22
21
|
except Exception as e:
|
|
23
22
|
traceback.print_tb(e.__traceback__)
|
|
@@ -26,7 +25,6 @@ class TransfromationManager(LayerCollectionHandler):
|
|
|
26
25
|
self.copy_master_layer()
|
|
27
26
|
self.editor.image_viewer.update_master_display()
|
|
28
27
|
self.editor.image_viewer.update_current_display()
|
|
29
|
-
self.editor.image_viewer.refresh_display()
|
|
30
28
|
self.editor.display_manager.update_thumbnails()
|
|
31
29
|
self.editor.mark_as_modified()
|
|
32
30
|
|