shinestacker 1.6.0__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/app/main.py +3 -1
- shinestacker/app/project.py +3 -1
- shinestacker/app/retouch.py +5 -0
- shinestacker/core/core_utils.py +3 -12
- shinestacker/core/logging.py +3 -2
- shinestacker/retouch/base_filter.py +1 -1
- shinestacker/retouch/display_manager.py +1 -2
- shinestacker/retouch/image_editor_ui.py +13 -35
- 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/view_strategy.py +65 -22
- {shinestacker-1.6.0.dist-info → shinestacker-1.6.1.dist-info}/METADATA +1 -1
- {shinestacker-1.6.0.dist-info → shinestacker-1.6.1.dist-info}/RECORD +25 -24
- shinestacker/retouch/io_manager.py +0 -69
- {shinestacker-1.6.0.dist-info → shinestacker-1.6.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.6.0.dist-info → shinestacker-1.6.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.6.0.dist-info → shinestacker-1.6.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.6.0.dist-info → shinestacker-1.6.1.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.6.
|
|
1
|
+
__version__ = '1.6.1'
|
shinestacker/app/main.py
CHANGED
|
@@ -13,6 +13,7 @@ from PySide6.QtCore import Qt, QEvent, QTimer, Signal
|
|
|
13
13
|
from shinestacker.config.config import config
|
|
14
14
|
config.init(DISABLE_TQDM=True, COMBINED_APP=True, DONT_USE_NATIVE_MENU=True)
|
|
15
15
|
from shinestacker.config.constants import constants
|
|
16
|
+
from shinestacker.config.settings import StdPathFile
|
|
16
17
|
from shinestacker.core.logging import setup_logging
|
|
17
18
|
from shinestacker.gui.main_window import MainWindow
|
|
18
19
|
from shinestacker.retouch.image_editor_ui import ImageEditorUI
|
|
@@ -229,7 +230,8 @@ open retouch window at startup instead of project windows.
|
|
|
229
230
|
if filename and path:
|
|
230
231
|
print("can't specify both arguments --filename and --path", file=sys.stderr)
|
|
231
232
|
sys.exit(1)
|
|
232
|
-
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True
|
|
233
|
+
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
|
|
234
|
+
log_file=StdPathFile('shinestacker.log').get_file_path())
|
|
233
235
|
app = Application(sys.argv)
|
|
234
236
|
if config.DONT_USE_NATIVE_MENU:
|
|
235
237
|
app.setAttribute(Qt.AA_DontUseNativeMenuBar)
|
shinestacker/app/project.py
CHANGED
|
@@ -12,6 +12,7 @@ from PySide6.QtCore import Qt, QTimer, QEvent
|
|
|
12
12
|
from shinestacker.config.config import config
|
|
13
13
|
config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
|
|
14
14
|
from shinestacker.config.constants import constants
|
|
15
|
+
from shinestacker.config.settings import StdPathFile
|
|
15
16
|
from shinestacker.core.logging import setup_logging
|
|
16
17
|
from shinestacker.gui.main_window import MainWindow
|
|
17
18
|
from shinestacker.app.gui_utils import (
|
|
@@ -57,7 +58,8 @@ project filename.
|
|
|
57
58
|
''')
|
|
58
59
|
add_project_arguments(parser)
|
|
59
60
|
args = vars(parser.parse_args(sys.argv[1:]))
|
|
60
|
-
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True
|
|
61
|
+
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
|
|
62
|
+
log_file=StdPathFile('shinestacker.log').get_file_path())
|
|
61
63
|
app = Application(sys.argv)
|
|
62
64
|
if config.DONT_USE_NATIVE_MENU:
|
|
63
65
|
app.setAttribute(Qt.AA_DontUseNativeMenuBar)
|
shinestacker/app/retouch.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
|
+
import logging
|
|
4
5
|
import argparse
|
|
5
6
|
from PySide6.QtWidgets import QApplication, QMenu
|
|
6
7
|
from PySide6.QtGui import QIcon
|
|
@@ -8,6 +9,8 @@ from PySide6.QtCore import Qt, QEvent
|
|
|
8
9
|
from shinestacker.config.config import config
|
|
9
10
|
config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
|
|
10
11
|
from shinestacker.config.constants import constants
|
|
12
|
+
from shinestacker.config.settings import StdPathFile
|
|
13
|
+
from shinestacker.core.logging import setup_logging
|
|
11
14
|
from shinestacker.retouch.image_editor_ui import ImageEditorUI
|
|
12
15
|
from shinestacker.app.gui_utils import (
|
|
13
16
|
disable_macos_special_menu_items, fill_app_menu, set_css_style)
|
|
@@ -54,6 +57,8 @@ Multiple files can be specified separated by ';'.
|
|
|
54
57
|
if filename and path:
|
|
55
58
|
print("can't specify both arguments --filename and --path", file=sys.stderr)
|
|
56
59
|
sys.exit(1)
|
|
60
|
+
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
|
|
61
|
+
log_file=StdPathFile('shinestacker.log').get_file_path())
|
|
57
62
|
app = Application(sys.argv)
|
|
58
63
|
if config.DONT_USE_NATIVE_MENU:
|
|
59
64
|
app.setAttribute(Qt.AA_DontUseNativeMenuBar)
|
shinestacker/core/core_utils.py
CHANGED
|
@@ -26,19 +26,10 @@ def make_tqdm_bar(name, size, ncols=80):
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def get_app_base_path():
|
|
29
|
-
sep = '\\' if (platform.system() == 'Windows') else '/'
|
|
30
29
|
if getattr(sys, 'frozen', False):
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for i in range(len(dirs) - 1, -1, -1):
|
|
35
|
-
if dirs[i] == 'shinestacker':
|
|
36
|
-
last = i
|
|
37
|
-
break
|
|
38
|
-
path = sep.join(dirs if last == 1 else dirs[:last + 1])
|
|
39
|
-
elif __file__:
|
|
40
|
-
path = sep.join(os.path.dirname(os.path.abspath(__file__)).split(sep)[:-3])
|
|
41
|
-
return path
|
|
30
|
+
return os.path.dirname(os.path.abspath(sys.executable))
|
|
31
|
+
else:
|
|
32
|
+
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
42
33
|
|
|
43
34
|
|
|
44
35
|
def running_under_windows() -> bool:
|
shinestacker/core/logging.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116
|
|
2
|
+
import os
|
|
2
3
|
import logging
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
@@ -64,8 +65,8 @@ def setup_logging(console_level=logging.INFO, file_level=logging.DEBUG, log_file
|
|
|
64
65
|
if log_file == '':
|
|
65
66
|
today = datetime.date.today().strftime("%Y-%m-%d")
|
|
66
67
|
log_file = f"logs/{constants.APP_STRING.lower()}-{today}.log"
|
|
67
|
-
if log_file
|
|
68
|
-
log_file =
|
|
68
|
+
if not os.path.isabs(log_file):
|
|
69
|
+
log_file = os.path.join(get_app_base_path(), {log_file})
|
|
69
70
|
Path(log_file).parent.mkdir(parents=True, exist_ok=True)
|
|
70
71
|
file_handler = logging.FileHandler(log_file)
|
|
71
72
|
file_handler.setLevel(file_level)
|
|
@@ -157,7 +157,7 @@ class BaseFilter(QObject, LayerCollectionHandler):
|
|
|
157
157
|
except Exception:
|
|
158
158
|
h, w = self.master_layer_copy().shape[:2]
|
|
159
159
|
try:
|
|
160
|
-
self.undo_manager.
|
|
160
|
+
self.undo_manager.set_paint_area(0, 0, w, h)
|
|
161
161
|
self.undo_manager.save_undo_state(
|
|
162
162
|
self.master_layer_copy(),
|
|
163
163
|
self.name
|
|
@@ -215,7 +215,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
215
215
|
def refresh_master_view(self):
|
|
216
216
|
if self.has_no_master_layer():
|
|
217
217
|
return
|
|
218
|
-
self.image_viewer.
|
|
218
|
+
self.image_viewer.update_master_display_area()
|
|
219
219
|
self.update_master_thumbnail()
|
|
220
220
|
|
|
221
221
|
def refresh_current_view(self):
|
|
@@ -229,7 +229,6 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
229
229
|
self.status_message_requested.emit("Temporary view: Individual layer.")
|
|
230
230
|
else:
|
|
231
231
|
self._master_refresh_and_thumb()
|
|
232
|
-
self.image_viewer.strategy.brush_preview.hide()
|
|
233
232
|
self.status_message_requested.emit("Temporary view: Master.")
|
|
234
233
|
|
|
235
234
|
def end_temp_view(self):
|
|
@@ -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
|
|
@@ -35,9 +36,9 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
35
36
|
self.brush = Brush()
|
|
36
37
|
self.brush_tool = BrushTool()
|
|
37
38
|
self.modified = False
|
|
38
|
-
self.mask_layer = None
|
|
39
39
|
self.transformation_manager = TransfromationManager(self)
|
|
40
|
-
self.
|
|
40
|
+
self.paint_area_manager = PaintAreaManager()
|
|
41
|
+
self.undo_manager = UndoManager(self.transformation_manager, self.paint_area_manager)
|
|
41
42
|
self.undo_action = None
|
|
42
43
|
self.redo_action = None
|
|
43
44
|
self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
|
|
@@ -49,13 +50,13 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
49
50
|
central_widget = QWidget()
|
|
50
51
|
self.setCentralWidget(central_widget)
|
|
51
52
|
layout = QHBoxLayout(central_widget)
|
|
52
|
-
self.image_viewer = ImageViewer(
|
|
53
|
+
self.image_viewer = ImageViewer(
|
|
54
|
+
self.layer_collection, self.brush_tool, self.paint_area_manager)
|
|
53
55
|
self.image_viewer.connect_signals(
|
|
54
56
|
self.handle_temp_view,
|
|
55
|
-
self.begin_copy_brush_area,
|
|
56
|
-
self.continue_copy_brush_area,
|
|
57
57
|
self.end_copy_brush_area,
|
|
58
|
-
self.handle_brush_size_change
|
|
58
|
+
self.handle_brush_size_change,
|
|
59
|
+
self.handle_needs_update)
|
|
59
60
|
side_panel = QWidget()
|
|
60
61
|
side_layout = QVBoxLayout(side_panel)
|
|
61
62
|
side_layout.setContentsMargins(0, 0, 0, 0)
|
|
@@ -629,35 +630,11 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
629
630
|
self.mark_as_modified()
|
|
630
631
|
self.statusBar().showMessage(f"Copied layer {self.current_layer_idx() + 1} to master")
|
|
631
632
|
|
|
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()
|
|
633
|
+
def handle_needs_update(self):
|
|
634
|
+
self.display_manager.needs_update = True
|
|
635
|
+
if not self.display_manager.update_timer.isActive():
|
|
636
|
+
self.display_manager.update_timer.start()
|
|
637
|
+
self.mark_as_modified()
|
|
661
638
|
|
|
662
639
|
def end_copy_brush_area(self):
|
|
663
640
|
if self.display_manager.update_timer.isActive():
|
|
@@ -667,6 +644,7 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
|
|
|
667
644
|
self.mark_as_modified()
|
|
668
645
|
|
|
669
646
|
def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):
|
|
647
|
+
self.image_viewer.update_brush_cursor()
|
|
670
648
|
if self.undo_action:
|
|
671
649
|
if has_undo:
|
|
672
650
|
self.undo_action.setText(f"Undo {undo_desc}")
|
|
@@ -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)
|
|
@@ -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__)
|
|
@@ -6,13 +6,10 @@ from .. config.gui_constants import gui_constants
|
|
|
6
6
|
class UndoManager(QObject):
|
|
7
7
|
stack_changed = Signal(bool, str, bool, str)
|
|
8
8
|
|
|
9
|
-
def __init__(self, transformation_manager):
|
|
9
|
+
def __init__(self, transformation_manager, paint_area_manager):
|
|
10
10
|
super().__init__()
|
|
11
11
|
self.transformation_manager = transformation_manager
|
|
12
|
-
self.
|
|
13
|
-
self.y_start = None
|
|
14
|
-
self.x_end = None
|
|
15
|
-
self.y_end = None
|
|
12
|
+
self.paint_area_manager = paint_area_manager
|
|
16
13
|
self.undo_stack = None
|
|
17
14
|
self.redo_stack = None
|
|
18
15
|
self.reset()
|
|
@@ -24,22 +21,25 @@ class UndoManager(QObject):
|
|
|
24
21
|
self.stack_changed.emit(False, "", False, "")
|
|
25
22
|
|
|
26
23
|
def reset_undo_area(self):
|
|
27
|
-
self.
|
|
28
|
-
self.x_start = self.y_start = gui_constants.MAX_UNDO_SIZE
|
|
24
|
+
self.paint_area_manager.reset()
|
|
29
25
|
|
|
30
26
|
def extend_undo_area(self, x_start, y_start, x_end, y_end):
|
|
31
|
-
self.x_start
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
self.
|
|
27
|
+
self.paint_area_manager.extend(x_start, y_start, x_end, y_end)
|
|
28
|
+
|
|
29
|
+
def paint_area(self):
|
|
30
|
+
return self.paint_area_manager.area()
|
|
31
|
+
|
|
32
|
+
def set_paint_area(self, x_start, y_start, x_end, y_end):
|
|
33
|
+
self.paint_area_manager.set_area(x_start, y_start, x_end, y_end)
|
|
35
34
|
|
|
36
35
|
def save_undo_state(self, layer, description):
|
|
37
36
|
if layer is None:
|
|
38
37
|
return
|
|
39
38
|
self.redo_stack = []
|
|
39
|
+
x_start, y_start, x_end, y_end = self.paint_area()
|
|
40
40
|
undo_state = {
|
|
41
|
-
'master': layer[
|
|
42
|
-
'area': (
|
|
41
|
+
'master': layer[y_start:y_end, x_start:x_end].copy(),
|
|
42
|
+
'area': (x_start, y_start, x_end, y_end),
|
|
43
43
|
'description': description
|
|
44
44
|
}
|
|
45
45
|
if len(self.undo_stack) >= gui_constants.MAX_UNDO_SIZE:
|
|
@@ -54,6 +54,7 @@ class UndoManager(QObject):
|
|
|
54
54
|
return False
|
|
55
55
|
undo_state = self.undo_stack.pop()
|
|
56
56
|
x_start, y_start, x_end, y_end = undo_state['area']
|
|
57
|
+
self.set_paint_area(x_start, y_start, x_end, y_end)
|
|
57
58
|
redo_state = {
|
|
58
59
|
'master': layer[y_start:y_end, x_start:x_end].copy(),
|
|
59
60
|
'area': (x_start, y_start, x_end, y_end),
|
|
@@ -80,6 +81,7 @@ class UndoManager(QObject):
|
|
|
80
81
|
return False
|
|
81
82
|
redo_state = self.redo_stack.pop()
|
|
82
83
|
x_start, y_start, x_end, y_end = redo_state['area']
|
|
84
|
+
self.set_paint_area(x_start, y_start, x_end, y_end)
|
|
83
85
|
undo_state = {
|
|
84
86
|
'master': layer[y_start:y_end, x_start:x_end].copy(),
|
|
85
87
|
'area': (x_start, y_start, x_end, y_end),
|
|
@@ -79,10 +79,9 @@ class BrushCursor(QGraphicsItemGroup):
|
|
|
79
79
|
|
|
80
80
|
class ViewSignals:
|
|
81
81
|
temp_view_requested = Signal(bool)
|
|
82
|
-
|
|
83
|
-
brush_operation_continued = Signal(QPoint)
|
|
84
|
-
brush_operation_ended = Signal()
|
|
82
|
+
end_copy_brush_area_requested = Signal()
|
|
85
83
|
brush_size_change_requested = Signal(int) # +1 or -1
|
|
84
|
+
needs_update_requested = Signal()
|
|
86
85
|
|
|
87
86
|
|
|
88
87
|
class ImageGraphicsViewBase(QGraphicsView):
|
|
@@ -103,9 +102,12 @@ class ImageGraphicsViewBase(QGraphicsView):
|
|
|
103
102
|
|
|
104
103
|
|
|
105
104
|
class ViewStrategy(LayerCollectionHandler):
|
|
106
|
-
def __init__(self, layer_collection, status):
|
|
105
|
+
def __init__(self, layer_collection, status, brush_tool, paint_area_manager):
|
|
107
106
|
LayerCollectionHandler.__init__(self, layer_collection)
|
|
108
107
|
self.status = status
|
|
108
|
+
self.brush_tool = brush_tool
|
|
109
|
+
self.paint_area_manager = paint_area_manager
|
|
110
|
+
self.mask_layer = None
|
|
109
111
|
self.brush = None
|
|
110
112
|
self.brush_cursor = None
|
|
111
113
|
self.brush_preview = BrushPreviewItem(layer_collection)
|
|
@@ -123,6 +125,7 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
123
125
|
self.last_update_time = QTime.currentTime()
|
|
124
126
|
self.last_color_update_time = 0
|
|
125
127
|
self.last_cursor_update_time = 0
|
|
128
|
+
self.enable_paint = True
|
|
126
129
|
|
|
127
130
|
@abstractmethod
|
|
128
131
|
def create_pixmaps(self):
|
|
@@ -274,6 +277,25 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
274
277
|
view.verticalScrollBar().setValue(self.status.v_scroll)
|
|
275
278
|
self.arrange_images()
|
|
276
279
|
|
|
280
|
+
def update_master_display_area(self):
|
|
281
|
+
if self.empty():
|
|
282
|
+
return
|
|
283
|
+
x_start, y_start, x_end, y_end = self.paint_area_manager.area()
|
|
284
|
+
dirty_region = self.master_layer()[y_start:y_end, x_start:x_end]
|
|
285
|
+
qimage = self.numpy_to_qimage(dirty_region)
|
|
286
|
+
if not qimage:
|
|
287
|
+
return
|
|
288
|
+
pixmap = QPixmap.fromImage(qimage)
|
|
289
|
+
master_pixmap_item = self.get_master_pixmap()
|
|
290
|
+
current_pixmap = master_pixmap_item.pixmap()
|
|
291
|
+
if current_pixmap.isNull():
|
|
292
|
+
self.update_master_display()
|
|
293
|
+
return
|
|
294
|
+
painter = QPainter(current_pixmap)
|
|
295
|
+
painter.drawPixmap(x_start, y_start, pixmap)
|
|
296
|
+
painter.end()
|
|
297
|
+
master_pixmap_item.setPixmap(current_pixmap)
|
|
298
|
+
|
|
277
299
|
def update_master_display(self):
|
|
278
300
|
self.update_view_display(
|
|
279
301
|
self.master_layer(),
|
|
@@ -698,6 +720,42 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
698
720
|
self.status.set_scroll(view.horizontalScrollBar().value(),
|
|
699
721
|
view.verticalScrollBar().value())
|
|
700
722
|
|
|
723
|
+
def copy_brush_area_to_master(self, view_pos):
|
|
724
|
+
if self.layer_stack() is None or self.number_of_layers() == 0:
|
|
725
|
+
return
|
|
726
|
+
area = self.brush_tool.apply_brush_operation(
|
|
727
|
+
self.master_layer_copy(),
|
|
728
|
+
self.current_layer(),
|
|
729
|
+
self.master_layer(), self.mask_layer,
|
|
730
|
+
view_pos)
|
|
731
|
+
self.paint_area_manager.extend(*area)
|
|
732
|
+
|
|
733
|
+
def begin_copy_brush_area(self, pos):
|
|
734
|
+
self.mask_layer = self.blank_layer().copy()
|
|
735
|
+
self.copy_master_layer()
|
|
736
|
+
self.paint_area_manager.reset()
|
|
737
|
+
self.copy_brush_area_to_master(pos)
|
|
738
|
+
self.needs_update_requested.emit()
|
|
739
|
+
|
|
740
|
+
def continue_copy_brush_area(self, pos):
|
|
741
|
+
self.copy_brush_area_to_master(pos)
|
|
742
|
+
self.needs_update_requested.emit()
|
|
743
|
+
|
|
744
|
+
def mouse_press_event(self, event):
|
|
745
|
+
if self.empty():
|
|
746
|
+
return
|
|
747
|
+
if self.enable_paint and event.button() & Qt.LeftButton and self.has_master_layer():
|
|
748
|
+
if self.space_pressed:
|
|
749
|
+
self.scrolling = True
|
|
750
|
+
self.last_mouse_pos = event.position()
|
|
751
|
+
self.setCursor(Qt.ClosedHandCursor)
|
|
752
|
+
else:
|
|
753
|
+
self.last_brush_pos = event.position()
|
|
754
|
+
self.begin_copy_brush_area(event.position().toPoint())
|
|
755
|
+
self.dragging = True
|
|
756
|
+
if not self.scrolling:
|
|
757
|
+
self.show_brush_cursor()
|
|
758
|
+
|
|
701
759
|
def mouse_move_event(self, event):
|
|
702
760
|
if self.empty():
|
|
703
761
|
return
|
|
@@ -710,7 +768,7 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
710
768
|
brush_size = self.brush.size
|
|
711
769
|
if not self.space_pressed:
|
|
712
770
|
self.update_brush_cursor()
|
|
713
|
-
if self.dragging and event.buttons() & Qt.LeftButton:
|
|
771
|
+
if self.enable_paint and self.dragging and event.buttons() & Qt.LeftButton:
|
|
714
772
|
current_time = QTime.currentTime()
|
|
715
773
|
paint_refresh_time = AppConfig.get('paint_refresh_time')
|
|
716
774
|
if self.last_update_time.msecsTo(current_time) >= paint_refresh_time:
|
|
@@ -726,7 +784,7 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
726
784
|
for i in range(0, n_steps + 1):
|
|
727
785
|
pos = QPoint(self.last_brush_pos.x() + i * delta_x,
|
|
728
786
|
self.last_brush_pos.y() + i * delta_y)
|
|
729
|
-
self.
|
|
787
|
+
self.continue_copy_brush_area(pos)
|
|
730
788
|
self.last_brush_pos = position
|
|
731
789
|
self.last_update_time = current_time
|
|
732
790
|
if self.scrolling and event.buttons() & Qt.LeftButton:
|
|
@@ -738,21 +796,6 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
738
796
|
self.last_mouse_pos = position
|
|
739
797
|
self.scroll_view(master_view, delta.x(), delta.y())
|
|
740
798
|
|
|
741
|
-
def mouse_press_event(self, event):
|
|
742
|
-
if self.empty():
|
|
743
|
-
return
|
|
744
|
-
if event.button() == Qt.LeftButton and self.has_master_layer():
|
|
745
|
-
if self.space_pressed:
|
|
746
|
-
self.scrolling = True
|
|
747
|
-
self.last_mouse_pos = event.position()
|
|
748
|
-
self.setCursor(Qt.ClosedHandCursor)
|
|
749
|
-
else:
|
|
750
|
-
self.last_brush_pos = event.position()
|
|
751
|
-
self.brush_operation_started.emit(event.position().toPoint())
|
|
752
|
-
self.dragging = True
|
|
753
|
-
if not self.scrolling:
|
|
754
|
-
self.show_brush_cursor()
|
|
755
|
-
|
|
756
799
|
def mouse_release_event(self, event):
|
|
757
800
|
if self.empty():
|
|
758
801
|
return
|
|
@@ -769,4 +812,4 @@ class ViewStrategy(LayerCollectionHandler):
|
|
|
769
812
|
self.last_mouse_pos = None
|
|
770
813
|
elif self.dragging:
|
|
771
814
|
self.dragging = False
|
|
772
|
-
self.
|
|
815
|
+
self.end_copy_brush_area_requested.emit()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
-
shinestacker/_version.py,sha256=
|
|
2
|
+
shinestacker/_version.py,sha256=DH87PU9LcZeI_uvttoH-lyhGyb55UShgeFtsG2qIZgE,21
|
|
3
3
|
shinestacker/algorithms/__init__.py,sha256=1FwVJ3w9GGbFFkjYJRUedTvcdE4j0ieSgaH9RC9iCY4,877
|
|
4
4
|
shinestacker/algorithms/align.py,sha256=mb44u-YxZI1TTSHz81nRpX_2c8awlOhnGrK0LyfTQeQ,33543
|
|
5
5
|
shinestacker/algorithms/align_auto.py,sha256=pJetw6zZEWQLouzcelkI8gD4cPiOp887ePXzVbm0E6Q,3800
|
|
@@ -25,10 +25,10 @@ shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9y
|
|
|
25
25
|
shinestacker/app/args_parser_opts.py,sha256=c6IUXOI0SJIFckPWPXYnwmBmdNnOcrtvU5S8hUDU_AQ,979
|
|
26
26
|
shinestacker/app/gui_utils.py,sha256=EGZejp0XZXRLVa_Wd_2VYAwK3oe9hMSoZzNgT7NUNRw,2986
|
|
27
27
|
shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
|
|
28
|
-
shinestacker/app/main.py,sha256=
|
|
28
|
+
shinestacker/app/main.py,sha256=OQwPJvhrsAThv9kmiqtX7CX-smfwXdFXagQmGsQdmuU,11258
|
|
29
29
|
shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
|
|
30
|
-
shinestacker/app/project.py,sha256=
|
|
31
|
-
shinestacker/app/retouch.py,sha256=
|
|
30
|
+
shinestacker/app/project.py,sha256=jn4LlMTMxrJeAyQQrR5nVS4Di1rOrjiOJ8LOg0CZbwE,3045
|
|
31
|
+
shinestacker/app/retouch.py,sha256=fyk74fgpeoBqY1mRJiXWf1hRzSeG7xXFx4V-Sphr3Kg,3069
|
|
32
32
|
shinestacker/app/settings_dialog.py,sha256=0P3nqqZEiTIFgidW1_e3Q_zE7NbAouNsuj-yNsU41vk,8192
|
|
33
33
|
shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
|
|
34
34
|
shinestacker/config/app_config.py,sha256=rM1Rndk1GDa5c0AhcVNEN9zSAzxPZixzQYfjODbJUwE,771
|
|
@@ -38,10 +38,10 @@ shinestacker/config/gui_constants.py,sha256=PNxzwmVEppJ2mV_vwp68NhWzJOEitVy1Pk9S
|
|
|
38
38
|
shinestacker/config/settings.py,sha256=4p4r6wKOCbttzfH9tyHQSTd-iv-GfgCd1LxI3C7WIjU,3861
|
|
39
39
|
shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
|
|
40
40
|
shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
|
|
41
|
-
shinestacker/core/core_utils.py,sha256=
|
|
41
|
+
shinestacker/core/core_utils.py,sha256=wmpu9_idDRe866JFEylMvGPJRT8lSVhikK9PljcbPSI,1392
|
|
42
42
|
shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk2c,1618
|
|
43
43
|
shinestacker/core/framework.py,sha256=QaTfnzEUHwzlbyFG7KzeyteckTSWHWEEJE4d5Tc8H18,11015
|
|
44
|
-
shinestacker/core/logging.py,sha256=
|
|
44
|
+
shinestacker/core/logging.py,sha256=T-ZTGYjN2n2_5Vu2mBJ2id9qj1c9NA79OD2H2w-W0nM,3096
|
|
45
45
|
shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
46
|
shinestacker/gui/action_config.py,sha256=Xv7SGbhPl1F_dUnU04VBt_E-wIItnN_q6QuhU_d9GfI,25929
|
|
47
47
|
shinestacker/gui/action_config_dialog.py,sha256=QN95FiVPYL6uin2sYO5F7tq6G5rBWh9yRkeTVvwKrwU,38341
|
|
@@ -75,35 +75,36 @@ shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzd
|
|
|
75
75
|
shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
|
|
76
76
|
shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
|
|
77
77
|
shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
78
|
-
shinestacker/retouch/base_filter.py,sha256=
|
|
78
|
+
shinestacker/retouch/base_filter.py,sha256=ObrAcwZv9YJbIhWMHcryHEEj41oAk9hfHmE4phEd1gE,11263
|
|
79
79
|
shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
|
|
80
80
|
shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
|
|
81
81
|
shinestacker/retouch/brush_preview.py,sha256=cOFVMCbEsgR_alzmr_-LLghtGU_unrE-hAjLHcvrZAY,5484
|
|
82
82
|
shinestacker/retouch/brush_tool.py,sha256=8uVncTA375uC3Nhp2YM0eZjpOR-nN47i2eGjN8tJzOU,8714
|
|
83
83
|
shinestacker/retouch/denoise_filter.py,sha256=UpNKbFs7uArdglEej8AUHan7oCVYV5E7HNzkovj7XMQ,571
|
|
84
|
-
shinestacker/retouch/display_manager.py,sha256=
|
|
84
|
+
shinestacker/retouch/display_manager.py,sha256=fTZTGbvmX5DXagexuvbNgOF5GiH2Vv-stLUQQwoglp8,10181
|
|
85
85
|
shinestacker/retouch/exif_data.py,sha256=LF-fRXW-reMq-xJ_QRE5j8DC2LVGKIlC6MR3QbC1cdg,1896
|
|
86
86
|
shinestacker/retouch/file_loader.py,sha256=z02-A8_uDZxayI1NFTxT2GVUvEBWStchX9hlN1o5-0U,4784
|
|
87
87
|
shinestacker/retouch/filter_manager.py,sha256=tOGIWj5HjViL1-iXHkd91X-sZ1c1G531pDmLO0x6zx0,866
|
|
88
88
|
shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
|
|
89
|
-
shinestacker/retouch/image_editor_ui.py,sha256=
|
|
89
|
+
shinestacker/retouch/image_editor_ui.py,sha256=r2S4Fgyi3xQi2XeqlVYTUxZ9YlPZe2rMaIpvIak8Aog,33181
|
|
90
90
|
shinestacker/retouch/image_view_status.py,sha256=2rWi2ugdyjMhWCtRJkwOnb7-tCtVfnGfCY_54qpZhwM,1970
|
|
91
|
-
shinestacker/retouch/image_viewer.py,sha256=
|
|
92
|
-
shinestacker/retouch/io_gui_handler.py,sha256=
|
|
93
|
-
shinestacker/retouch/
|
|
94
|
-
shinestacker/retouch/layer_collection.py,sha256=
|
|
95
|
-
shinestacker/retouch/overlaid_view.py,sha256=
|
|
91
|
+
shinestacker/retouch/image_viewer.py,sha256=xf1vYZRPb9ClCQbqrqAFhPubdqIIpku7DgcY8O5bvYU,4694
|
|
92
|
+
shinestacker/retouch/io_gui_handler.py,sha256=tyHMmR6_uE_IEI-CIcJibVhupxarKYHzX2fuhnesHaI,14616
|
|
93
|
+
shinestacker/retouch/io_threads.py,sha256=r0X4it2PfwnmiAU7eStniIfcHhPvuaqdqf5VlnvjZ-4,2832
|
|
94
|
+
shinestacker/retouch/layer_collection.py,sha256=xx8INSLCXIeTQn_nxfCo4QljAmQK1qukSYO1Zk4rqqo,6183
|
|
95
|
+
shinestacker/retouch/overlaid_view.py,sha256=QTTdegUWs99YBZZPlIRdPI5O80U3t_c3HnyegbRqNbA,7029
|
|
96
|
+
shinestacker/retouch/paint_area_manager.py,sha256=ilK6uQT7lzNyvdc8uNv4xTHHHAbk5hGEClJRNmiA4P8,894
|
|
96
97
|
shinestacker/retouch/shortcuts_help.py,sha256=BFWTT5QvodqMhqa_9LI25hZqjICfckgyWG4fGrGzvnM,4283
|
|
97
|
-
shinestacker/retouch/sidebyside_view.py,sha256=
|
|
98
|
-
shinestacker/retouch/transformation_manager.py,sha256=
|
|
99
|
-
shinestacker/retouch/undo_manager.py,sha256=
|
|
98
|
+
shinestacker/retouch/sidebyside_view.py,sha256=4sNa_IUMbNH18iECO7eDO9S_ls3v2ENwP1AWrBFhofI,18907
|
|
99
|
+
shinestacker/retouch/transformation_manager.py,sha256=QFYCL-l9V6qlhw3y7tcs0saWWClNPsh7F9pTBkfPbRU,1711
|
|
100
|
+
shinestacker/retouch/undo_manager.py,sha256=J4hEAnv9bKLQ0N1wllWswjJBhgRgasCnBoMT5LEw-dM,4453
|
|
100
101
|
shinestacker/retouch/unsharp_mask_filter.py,sha256=Iapc8UmSVpj3V0LcJq_38P5qerRqTevMynbbk5Rk6iE,3634
|
|
101
|
-
shinestacker/retouch/view_strategy.py,sha256=
|
|
102
|
+
shinestacker/retouch/view_strategy.py,sha256=jZxB_vX3_0notH0ClxKkLzbdtx4is3vQiYoIP-sDv3M,30216
|
|
102
103
|
shinestacker/retouch/vignetting_filter.py,sha256=JhFr6OVIripQzSJrZEG4lxq7wBsmpofLqJQ-aP2bKw8,3789
|
|
103
104
|
shinestacker/retouch/white_balance_filter.py,sha256=UaH4yxG3fU4vPutBAkV5oTXIQyUTN09x0uTywAzv3sY,8286
|
|
104
|
-
shinestacker-1.6.
|
|
105
|
-
shinestacker-1.6.
|
|
106
|
-
shinestacker-1.6.
|
|
107
|
-
shinestacker-1.6.
|
|
108
|
-
shinestacker-1.6.
|
|
109
|
-
shinestacker-1.6.
|
|
105
|
+
shinestacker-1.6.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
106
|
+
shinestacker-1.6.1.dist-info/METADATA,sha256=9sevPHbXS-yaj-wYc-42q28V34PNOdnPlntqNzlsgOo,6978
|
|
107
|
+
shinestacker-1.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
108
|
+
shinestacker-1.6.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
109
|
+
shinestacker-1.6.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
110
|
+
shinestacker-1.6.1.dist-info/RECORD,,
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# pylint: disable=E1101, C0114, C0115, C0116, E0611, W0718, R0903
|
|
2
|
-
import traceback
|
|
3
|
-
import cv2
|
|
4
|
-
from PySide6.QtCore import QThread, Signal
|
|
5
|
-
from .. algorithms.utils import read_img, validate_image, get_img_metadata
|
|
6
|
-
from .. algorithms.exif import get_exif, write_image_with_exif_data
|
|
7
|
-
from .. algorithms.multilayer import write_multilayer_tiff_from_images
|
|
8
|
-
from .layer_collection import LayerCollectionHandler
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class FileMultilayerSaver(QThread):
|
|
12
|
-
finished = Signal()
|
|
13
|
-
error = Signal(str)
|
|
14
|
-
|
|
15
|
-
def __init__(self, images_dict, path, exif_path=None):
|
|
16
|
-
super().__init__()
|
|
17
|
-
self.images_dict = images_dict
|
|
18
|
-
self.path = path
|
|
19
|
-
self.exif_path = exif_path
|
|
20
|
-
|
|
21
|
-
def run(self):
|
|
22
|
-
try:
|
|
23
|
-
write_multilayer_tiff_from_images(
|
|
24
|
-
self.images_dict, self.path, exif_path=self.exif_path)
|
|
25
|
-
self.finished.emit()
|
|
26
|
-
except Exception as e:
|
|
27
|
-
traceback.print_tb(e.__traceback__)
|
|
28
|
-
self.error.emit(str(e))
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class IOManager(LayerCollectionHandler):
|
|
32
|
-
def __init__(self, layer_collection):
|
|
33
|
-
super().__init__(layer_collection)
|
|
34
|
-
self.exif_path = ''
|
|
35
|
-
self.exif_data = None
|
|
36
|
-
|
|
37
|
-
def import_frames(self, file_paths):
|
|
38
|
-
stack = []
|
|
39
|
-
labels = []
|
|
40
|
-
master = None
|
|
41
|
-
shape, dtype = get_img_metadata(self.master_layer())
|
|
42
|
-
for path in file_paths:
|
|
43
|
-
try:
|
|
44
|
-
label = path.split("/")[-1].split(".")[0]
|
|
45
|
-
img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
|
|
46
|
-
if shape is not None and dtype is not None:
|
|
47
|
-
validate_image(img, shape, dtype)
|
|
48
|
-
else:
|
|
49
|
-
shape, dtype = get_img_metadata(img)
|
|
50
|
-
label_x = label
|
|
51
|
-
i = 0
|
|
52
|
-
while label_x in labels:
|
|
53
|
-
i += 1
|
|
54
|
-
label_x = f"{label} ({i})"
|
|
55
|
-
labels.append(label_x)
|
|
56
|
-
stack.append(img)
|
|
57
|
-
if master is None:
|
|
58
|
-
master = img.copy()
|
|
59
|
-
except Exception as e:
|
|
60
|
-
raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
|
|
61
|
-
return stack, labels, master
|
|
62
|
-
|
|
63
|
-
def save_master(self, path):
|
|
64
|
-
img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
|
|
65
|
-
write_image_with_exif_data(self.exif_data, img, path)
|
|
66
|
-
|
|
67
|
-
def set_exif_data(self, path):
|
|
68
|
-
self.exif_path = path
|
|
69
|
-
self.exif_data = get_exif(path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|