shinestacker 1.0.4.post2__py3-none-any.whl → 1.2.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/__init__.py +4 -1
- shinestacker/algorithms/align.py +128 -14
- shinestacker/algorithms/balance.py +362 -163
- shinestacker/algorithms/base_stack_algo.py +33 -4
- shinestacker/algorithms/depth_map.py +9 -12
- shinestacker/algorithms/multilayer.py +12 -2
- shinestacker/algorithms/noise_detection.py +8 -3
- shinestacker/algorithms/pyramid.py +57 -42
- shinestacker/algorithms/pyramid_auto.py +141 -0
- shinestacker/algorithms/pyramid_tiles.py +264 -0
- shinestacker/algorithms/stack.py +14 -11
- shinestacker/algorithms/stack_framework.py +17 -11
- shinestacker/algorithms/utils.py +180 -1
- shinestacker/algorithms/vignetting.py +23 -5
- shinestacker/config/constants.py +31 -5
- shinestacker/gui/action_config.py +6 -7
- shinestacker/gui/action_config_dialog.py +425 -258
- shinestacker/gui/base_form_dialog.py +11 -6
- shinestacker/gui/flow_layout.py +105 -0
- shinestacker/gui/gui_run.py +24 -19
- shinestacker/gui/main_window.py +4 -3
- shinestacker/gui/menu_manager.py +12 -2
- shinestacker/gui/new_project.py +28 -22
- shinestacker/gui/project_controller.py +40 -23
- shinestacker/gui/project_converter.py +6 -6
- shinestacker/gui/project_editor.py +21 -7
- shinestacker/gui/time_progress_bar.py +2 -2
- shinestacker/retouch/exif_data.py +5 -5
- shinestacker/retouch/shortcuts_help.py +4 -4
- shinestacker/retouch/vignetting_filter.py +12 -8
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/METADATA +20 -1
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/RECORD +37 -34
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -3,16 +3,21 @@ from PySide6.QtWidgets import QFormLayout, QDialog
|
|
|
3
3
|
from PySide6.QtCore import Qt
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
def create_form_layout(parent):
|
|
7
|
+
layout = QFormLayout(parent)
|
|
8
|
+
layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
9
|
+
layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
|
10
|
+
layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
11
|
+
layout.setLabelAlignment(Qt.AlignLeft)
|
|
12
|
+
return layout
|
|
13
|
+
|
|
14
|
+
|
|
6
15
|
class BaseFormDialog(QDialog):
|
|
7
16
|
def __init__(self, title, parent=None):
|
|
8
17
|
super().__init__(parent)
|
|
9
18
|
self.setWindowTitle(title)
|
|
10
19
|
self.resize(500, self.height())
|
|
11
|
-
self.
|
|
12
|
-
self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
13
|
-
self.layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
|
14
|
-
self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
15
|
-
self.layout.setLabelAlignment(Qt.AlignLeft)
|
|
20
|
+
self.form_layout = create_form_layout(self)
|
|
16
21
|
|
|
17
22
|
def add_row_to_layout(self, item):
|
|
18
|
-
self.
|
|
23
|
+
self.form_layout.addRow(item)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, C0103, R0914
|
|
2
|
+
from PySide6.QtWidgets import QLayout
|
|
3
|
+
from PySide6.QtCore import Qt, QRect, QSize, QPoint
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FlowLayout(QLayout):
|
|
7
|
+
def __init__(self, parent=None, margin=0, spacing=-1, justify=True):
|
|
8
|
+
super().__init__(parent)
|
|
9
|
+
self._item_list = []
|
|
10
|
+
self._justify = justify
|
|
11
|
+
self.setContentsMargins(margin, margin, margin, margin)
|
|
12
|
+
self.setSpacing(spacing)
|
|
13
|
+
|
|
14
|
+
def addItem(self, item):
|
|
15
|
+
self._item_list.append(item)
|
|
16
|
+
|
|
17
|
+
def count(self):
|
|
18
|
+
return len(self._item_list)
|
|
19
|
+
|
|
20
|
+
def itemAt(self, index):
|
|
21
|
+
if 0 <= index < len(self._item_list):
|
|
22
|
+
return self._item_list[index]
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
def takeAt(self, index):
|
|
26
|
+
if 0 <= index < len(self._item_list):
|
|
27
|
+
return self._item_list.pop(index)
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def expandingDirections(self):
|
|
31
|
+
return Qt.Orientations(0)
|
|
32
|
+
|
|
33
|
+
def hasHeightForWidth(self):
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
def heightForWidth(self, width):
|
|
37
|
+
return self._do_layout(QRect(0, 0, width, 0), True)
|
|
38
|
+
|
|
39
|
+
def setGeometry(self, rect):
|
|
40
|
+
super().setGeometry(rect)
|
|
41
|
+
self._do_layout(rect, False)
|
|
42
|
+
|
|
43
|
+
def sizeHint(self):
|
|
44
|
+
return self.minimumSize()
|
|
45
|
+
|
|
46
|
+
def minimumSize(self):
|
|
47
|
+
size = QSize()
|
|
48
|
+
for item in self._item_list:
|
|
49
|
+
size = size.expandedTo(item.minimumSize())
|
|
50
|
+
margins = self.contentsMargins()
|
|
51
|
+
size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom())
|
|
52
|
+
return size
|
|
53
|
+
|
|
54
|
+
def setJustify(self, justify):
|
|
55
|
+
self._justify = justify
|
|
56
|
+
self.invalidate()
|
|
57
|
+
|
|
58
|
+
def justify(self):
|
|
59
|
+
return self._justify
|
|
60
|
+
|
|
61
|
+
def _do_layout(self, rect, test_only):
|
|
62
|
+
x = rect.x()
|
|
63
|
+
y = rect.y()
|
|
64
|
+
line_height = 0
|
|
65
|
+
spacing = self.spacing()
|
|
66
|
+
lines = []
|
|
67
|
+
current_line = []
|
|
68
|
+
current_line_width = 0
|
|
69
|
+
for item in self._item_list:
|
|
70
|
+
space_x = spacing
|
|
71
|
+
next_x = x + item.sizeHint().width() + space_x
|
|
72
|
+
if next_x - space_x > rect.right() and line_height > 0:
|
|
73
|
+
lines.append((current_line, current_line_width, line_height))
|
|
74
|
+
x = rect.x()
|
|
75
|
+
y = y + line_height + spacing
|
|
76
|
+
next_x = x + item.sizeHint().width() + space_x
|
|
77
|
+
current_line = []
|
|
78
|
+
current_line_width = 0
|
|
79
|
+
line_height = 0
|
|
80
|
+
current_line.append(item)
|
|
81
|
+
current_line_width += item.sizeHint().width()
|
|
82
|
+
x = next_x
|
|
83
|
+
line_height = max(line_height, item.sizeHint().height())
|
|
84
|
+
if current_line:
|
|
85
|
+
lines.append((current_line, current_line_width, line_height))
|
|
86
|
+
y_offset = rect.y()
|
|
87
|
+
for line, line_width, line_height in lines:
|
|
88
|
+
if not test_only:
|
|
89
|
+
available_width = rect.width() - (len(line) - 1) * spacing
|
|
90
|
+
if self._justify and len(line) > 1:
|
|
91
|
+
stretch_factor = available_width / line_width if line_width > 0 else 1
|
|
92
|
+
x_offset = rect.x()
|
|
93
|
+
for item in line:
|
|
94
|
+
item_width = int(item.sizeHint().width() * stretch_factor)
|
|
95
|
+
item.setGeometry(QRect(QPoint(x_offset, y_offset),
|
|
96
|
+
QSize(item_width, line_height)))
|
|
97
|
+
x_offset += item_width + spacing
|
|
98
|
+
else:
|
|
99
|
+
x_offset = rect.x()
|
|
100
|
+
for item in line:
|
|
101
|
+
item.setGeometry(QRect(QPoint(x_offset, y_offset),
|
|
102
|
+
item.sizeHint()))
|
|
103
|
+
x_offset += item.sizeHint().width() + spacing
|
|
104
|
+
y_offset += line_height + spacing
|
|
105
|
+
return y_offset - spacing - rect.y()
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0903, R0915, R0914, R0917, R0913, R0902
|
|
2
2
|
import os
|
|
3
|
+
import traceback
|
|
3
4
|
from PySide6.QtWidgets import (QWidget, QPushButton, QVBoxLayout, QHBoxLayout,
|
|
4
5
|
QMessageBox, QScrollArea, QSizePolicy, QFrame, QLabel, QComboBox)
|
|
5
6
|
from PySide6.QtGui import QColor
|
|
@@ -16,6 +17,7 @@ from .colors import (
|
|
|
16
17
|
ACTION_RUNNING_COLOR, ACTION_COMPLETED_COLOR,
|
|
17
18
|
ACTION_STOPPED_COLOR, ACTION_FAILED_COLOR)
|
|
18
19
|
from .time_progress_bar import TimerProgressBar
|
|
20
|
+
from .flow_layout import FlowLayout
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class ColorButton(QPushButton):
|
|
@@ -55,12 +57,12 @@ class RunWindow(QTextEditLogger):
|
|
|
55
57
|
for label_row in labels:
|
|
56
58
|
self.color_widgets.append([])
|
|
57
59
|
row = QWidget(self)
|
|
58
|
-
h_layout = QHBoxLayout(row)
|
|
60
|
+
h_layout = FlowLayout(row) # QHBoxLayout(row)
|
|
59
61
|
h_layout.setContentsMargins(0, 0, 0, 0)
|
|
60
62
|
h_layout.setSpacing(2)
|
|
61
63
|
for label, enabled in label_row:
|
|
62
64
|
widget = ColorButton(label, enabled)
|
|
63
|
-
h_layout.addWidget(widget, stretch=1)
|
|
65
|
+
h_layout.addWidget(widget) # addWidget(widget, stretch=1)
|
|
64
66
|
self.color_widgets[-1].append(widget)
|
|
65
67
|
layout.addWidget(row)
|
|
66
68
|
self.progress_bar = TimerProgressBar()
|
|
@@ -202,23 +204,26 @@ class RunWindow(QTextEditLogger):
|
|
|
202
204
|
label = QLabel(name, self)
|
|
203
205
|
label.setStyleSheet("QLabel {margin-top: 5px; font-weight: bold;}")
|
|
204
206
|
self.image_layout.addWidget(label)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
self.right_area.verticalScrollBar().
|
|
207
|
+
try:
|
|
208
|
+
if extension_pdf(path):
|
|
209
|
+
image_view = GuiPdfView(path, self)
|
|
210
|
+
elif extension_tif_jpg(path):
|
|
211
|
+
image_view = GuiImageView(path, self)
|
|
212
|
+
else:
|
|
213
|
+
raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
|
|
214
|
+
self.image_views.append(image_view)
|
|
215
|
+
self.image_layout.addWidget(image_view)
|
|
216
|
+
max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
|
|
217
|
+
needed_width = max_width + 20
|
|
218
|
+
self.right_area.setFixedWidth(needed_width)
|
|
219
|
+
self.image_area_widget.setFixedWidth(needed_width)
|
|
220
|
+
self.right_area.updateGeometry()
|
|
221
|
+
self.image_area_widget.updateGeometry()
|
|
222
|
+
QTimer.singleShot(
|
|
223
|
+
0, lambda: self.right_area.verticalScrollBar().setValue(
|
|
224
|
+
self.right_area.verticalScrollBar().maximum()))
|
|
225
|
+
except RuntimeError as e:
|
|
226
|
+
traceback.print_tb(e.__traceback__)
|
|
222
227
|
|
|
223
228
|
@Slot(int, str, str, str)
|
|
224
229
|
def handle_open_app(self, _run_id, name, app, path):
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -135,6 +135,8 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
135
135
|
self.project_editor.refresh_ui_signal.connect(self.refresh_ui)
|
|
136
136
|
self.project_editor.enable_delete_action_signal.connect(
|
|
137
137
|
self.menu_manager.delete_element_action.setEnabled)
|
|
138
|
+
self.project_editor.undo_manager.set_enabled_undo_action_requested.connect(
|
|
139
|
+
self.menu_manager.undo_action.setEnabled)
|
|
138
140
|
self.project_controller.update_title_requested.connect(self.update_title)
|
|
139
141
|
self.project_controller.refresh_ui_requested.connect(self.refresh_ui)
|
|
140
142
|
self.project_controller.activate_window_requested.connect(self.activateWindow)
|
|
@@ -406,14 +408,13 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
406
408
|
if self.job_list_count() == 0:
|
|
407
409
|
self.menu_manager.add_action_entry_action.setEnabled(False)
|
|
408
410
|
self.menu_manager.action_selector.setEnabled(False)
|
|
409
|
-
self.run_job_action.setEnabled(False)
|
|
410
|
-
self.run_all_jobs_action.setEnabled(False)
|
|
411
|
+
self.menu_manager.run_job_action.setEnabled(False)
|
|
411
412
|
else:
|
|
412
413
|
self.menu_manager.add_action_entry_action.setEnabled(True)
|
|
413
414
|
self.menu_manager.action_selector.setEnabled(True)
|
|
414
415
|
self.menu_manager.delete_element_action.setEnabled(True)
|
|
415
416
|
self.menu_manager.run_job_action.setEnabled(True)
|
|
416
|
-
|
|
417
|
+
self.menu_manager.set_enabled_run_all_jobs(self.job_list_count() > 1)
|
|
417
418
|
|
|
418
419
|
def quit(self):
|
|
419
420
|
if self.project_controller.check_unsaved_changes():
|
shinestacker/gui/menu_manager.py
CHANGED
|
@@ -82,7 +82,10 @@ class MenuManager:
|
|
|
82
82
|
|
|
83
83
|
def add_edit_menu(self):
|
|
84
84
|
menu = self.menubar.addMenu("&Edit")
|
|
85
|
-
|
|
85
|
+
self.undo_action = self.action("&Undo")
|
|
86
|
+
self.undo_action.setEnabled(False)
|
|
87
|
+
menu.addAction(self.undo_action)
|
|
88
|
+
for name in ["&Cut", "Cop&y", "&Paste", "Duplicate"]:
|
|
86
89
|
menu.addAction(self.action(name))
|
|
87
90
|
self.delete_element_action = self.action("Delete")
|
|
88
91
|
self.delete_element_action.setEnabled(False)
|
|
@@ -113,7 +116,7 @@ class MenuManager:
|
|
|
113
116
|
self.run_job_action.setEnabled(False)
|
|
114
117
|
menu.addAction(self.run_job_action)
|
|
115
118
|
self.run_all_jobs_action = self.action("Run All Jobs")
|
|
116
|
-
self.
|
|
119
|
+
self.set_enabled_run_all_jobs(False)
|
|
117
120
|
menu.addAction(self.run_all_jobs_action)
|
|
118
121
|
|
|
119
122
|
def add_actions_menu(self):
|
|
@@ -234,3 +237,10 @@ class MenuManager:
|
|
|
234
237
|
self.sub_action_selector.setEnabled(enabled)
|
|
235
238
|
for a in self.sub_action_menu_entries:
|
|
236
239
|
a.setEnabled(enabled)
|
|
240
|
+
|
|
241
|
+
def set_enabled_run_all_jobs(self, enabled):
|
|
242
|
+
tooltip = self.tooltips["Run All Jobs"]
|
|
243
|
+
self.run_all_jobs_action.setEnabled(enabled)
|
|
244
|
+
if not enabled:
|
|
245
|
+
tooltip += " (requires more tha one job)"
|
|
246
|
+
self.run_all_jobs_action.setToolTip(tooltip)
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -36,11 +36,11 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
36
36
|
def add_bold_label(self, label):
|
|
37
37
|
label = QLabel(label)
|
|
38
38
|
label.setStyleSheet("font-weight: bold")
|
|
39
|
-
self.
|
|
39
|
+
self.form_layout.addRow(label)
|
|
40
40
|
|
|
41
41
|
def add_label(self, label):
|
|
42
42
|
label = QLabel(label)
|
|
43
|
-
self.
|
|
43
|
+
self.form_layout.addRow(label)
|
|
44
44
|
|
|
45
45
|
def create_form(self):
|
|
46
46
|
icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
|
|
@@ -49,10 +49,10 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
49
49
|
icon_label = QLabel()
|
|
50
50
|
icon_label.setPixmap(icon_pixmap)
|
|
51
51
|
icon_label.setAlignment(Qt.AlignCenter)
|
|
52
|
-
self.
|
|
52
|
+
self.form_layout.addRow(icon_label)
|
|
53
53
|
spacer = QLabel("")
|
|
54
54
|
spacer.setFixedHeight(10)
|
|
55
|
-
self.
|
|
55
|
+
self.form_layout.addRow(spacer)
|
|
56
56
|
|
|
57
57
|
self.input_folder, container = create_select_file_paths_widget(
|
|
58
58
|
'', 'input files folder', 'input files folder')
|
|
@@ -92,25 +92,26 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
92
92
|
self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
|
|
93
93
|
|
|
94
94
|
self.add_bold_label("1️⃣ Select input folder, all images therein will be merged. ")
|
|
95
|
-
self.
|
|
96
|
-
self.
|
|
95
|
+
self.form_layout.addRow("Input folder:", container)
|
|
96
|
+
self.form_layout.addRow("Number of frames: ", self.frames_label)
|
|
97
97
|
self.add_label("")
|
|
98
98
|
self.add_bold_label("2️⃣ Select basic options.")
|
|
99
99
|
if self.expert():
|
|
100
|
-
self.
|
|
101
|
-
self.
|
|
102
|
-
self.
|
|
103
|
-
self.
|
|
104
|
-
self.
|
|
105
|
-
self.
|
|
106
|
-
self.
|
|
107
|
-
self.
|
|
100
|
+
self.form_layout.addRow("Automatic noise detection:", self.noise_detection)
|
|
101
|
+
self.form_layout.addRow("Vignetting correction:", self.vignetting_correction)
|
|
102
|
+
self.form_layout.addRow("Align layers:", self.align_frames)
|
|
103
|
+
self.form_layout.addRow("Balance layers:", self.balance_frames)
|
|
104
|
+
self.form_layout.addRow("Bunch stack:", self.bunch_stack)
|
|
105
|
+
self.form_layout.addRow("Bunch frames:", self.bunch_frames)
|
|
106
|
+
self.form_layout.addRow("Bunch overlap:", self.bunch_overlap)
|
|
107
|
+
self.form_layout.addRow("Number of bunches: ", self.bunches_label)
|
|
108
108
|
if self.expert():
|
|
109
|
-
self.
|
|
110
|
-
self.
|
|
109
|
+
self.form_layout.addRow("Focus stack (pyramid):", self.focus_stack_pyramid)
|
|
110
|
+
self.form_layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
|
|
111
111
|
else:
|
|
112
|
-
self.
|
|
113
|
-
|
|
112
|
+
self.form_layout.addRow("Focus stack:", self.focus_stack_pyramid)
|
|
113
|
+
if self.expert():
|
|
114
|
+
self.form_layout.addRow("Save multi layer TIFF:", self.multi_layer)
|
|
114
115
|
self.add_label("")
|
|
115
116
|
self.add_bold_label("3️⃣ Push 🆗 for further options, then press ▶️ to run.")
|
|
116
117
|
self.add_label("")
|
|
@@ -181,8 +182,9 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
181
182
|
height, width = img.shape[:2]
|
|
182
183
|
n_bytes = 1 if img.dtype == np.uint8 else 2
|
|
183
184
|
n_bits = 8 if img.dtype == np.uint8 else 16
|
|
184
|
-
|
|
185
|
-
|
|
185
|
+
n_gbytes = float(n_bytes * height * width * self.n_image_files) / constants.ONE_GIGA
|
|
186
|
+
print("GBytes: ", n_gbytes)
|
|
187
|
+
if n_gbytes > 1 and not self.bunch_stack.isChecked():
|
|
186
188
|
msg = QMessageBox()
|
|
187
189
|
msg.setStyleSheet("""
|
|
188
190
|
QMessageBox {
|
|
@@ -200,11 +202,15 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
200
202
|
msg.setWindowTitle("Too many frames")
|
|
201
203
|
msg.setText(f"You selected {self.n_image_files} images "
|
|
202
204
|
f"with resolution {width}×{height} pixels, {n_bits} bits depth. "
|
|
203
|
-
"Processing may require a significant amount
|
|
205
|
+
"Processing may require a significant amount "
|
|
206
|
+
"of memory or I/O buffering.\n\n"
|
|
204
207
|
"Continue anyway?")
|
|
205
208
|
msg.setInformativeText("You may consider to split the processing "
|
|
206
209
|
" using a bunch stack to reduce memory usage.\n\n"
|
|
207
|
-
'✅ Check the option "Bunch stack"
|
|
210
|
+
'✅ Check the option "Bunch stack".\n\n'
|
|
211
|
+
"➡️ Check expert options for the stacking algorithm."
|
|
212
|
+
'Go to "View" > "Expert Options".'
|
|
213
|
+
)
|
|
208
214
|
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
|
|
209
215
|
msg.setDefaultButton(QMessageBox.Cancel)
|
|
210
216
|
if msg.exec_() != QMessageBox.Ok:
|
|
@@ -136,6 +136,7 @@ class ProjectController(QObject):
|
|
|
136
136
|
self.clear_job_list()
|
|
137
137
|
self.clear_action_list()
|
|
138
138
|
self.mark_as_modified(False)
|
|
139
|
+
self.project_editor.reset_undo()
|
|
139
140
|
|
|
140
141
|
def new_project(self):
|
|
141
142
|
if not self.check_unsaved_changes():
|
|
@@ -151,65 +152,79 @@ class ProjectController(QObject):
|
|
|
151
152
|
dialog = NewProjectDialog(self.parent)
|
|
152
153
|
if dialog.exec() == QDialog.Accepted:
|
|
153
154
|
self.save_actions_set_enabled(True)
|
|
155
|
+
self.project_editor.reset_undo()
|
|
154
156
|
input_folder = dialog.get_input_folder().split('/')
|
|
155
157
|
working_path = '/'.join(input_folder[:-1])
|
|
156
158
|
input_path = input_folder[-1]
|
|
157
159
|
if dialog.get_noise_detection():
|
|
158
|
-
job_noise = ActionConfig(
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
job_noise = ActionConfig(
|
|
161
|
+
constants.ACTION_JOB,
|
|
162
|
+
{'name': f'{input_path}-detect-noise', 'working_path': working_path,
|
|
163
|
+
'input_path': input_path})
|
|
161
164
|
noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
|
|
162
|
-
{'name': 'detect-noise'})
|
|
165
|
+
{'name': f'{input_path}-detect-noise'})
|
|
163
166
|
job_noise.add_sub_action(noise_detection)
|
|
164
167
|
self.add_job_to_project(job_noise)
|
|
165
168
|
job = ActionConfig(constants.ACTION_JOB,
|
|
166
|
-
{'name': 'focus-stack',
|
|
169
|
+
{'name': f'{input_path}-focus-stack',
|
|
170
|
+
'working_path': working_path,
|
|
167
171
|
'input_path': input_path})
|
|
172
|
+
preprocess_name = ''
|
|
168
173
|
if dialog.get_noise_detection() or dialog.get_vignetting_correction() or \
|
|
169
174
|
dialog.get_align_frames() or dialog.get_balance_frames():
|
|
170
|
-
|
|
175
|
+
preprocess_name = f'{input_path}-preprocess'
|
|
176
|
+
combo_action = ActionConfig(
|
|
177
|
+
constants.ACTION_COMBO, {'name': preprocess_name})
|
|
171
178
|
if dialog.get_noise_detection():
|
|
172
|
-
mask_noise = ActionConfig(
|
|
179
|
+
mask_noise = ActionConfig(
|
|
180
|
+
constants.ACTION_MASKNOISE, {'name': 'mask-noise'})
|
|
173
181
|
combo_action.add_sub_action(mask_noise)
|
|
174
182
|
if dialog.get_vignetting_correction():
|
|
175
|
-
vignetting = ActionConfig(
|
|
183
|
+
vignetting = ActionConfig(
|
|
184
|
+
constants.ACTION_VIGNETTING, {'name': 'vignetting'})
|
|
176
185
|
combo_action.add_sub_action(vignetting)
|
|
177
186
|
if dialog.get_align_frames():
|
|
178
|
-
align = ActionConfig(
|
|
187
|
+
align = ActionConfig(
|
|
188
|
+
constants.ACTION_ALIGNFRAMES, {'name': 'align'})
|
|
179
189
|
combo_action.add_sub_action(align)
|
|
180
190
|
if dialog.get_balance_frames():
|
|
181
|
-
balance = ActionConfig(
|
|
191
|
+
balance = ActionConfig(
|
|
192
|
+
constants.ACTION_BALANCEFRAMES, {'name': 'balance'})
|
|
182
193
|
combo_action.add_sub_action(balance)
|
|
183
194
|
job.add_sub_action(combo_action)
|
|
184
195
|
if dialog.get_bunch_stack():
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
196
|
+
bunch_stack_name = f'{input_path}-bunches'
|
|
197
|
+
bunch_stack = ActionConfig(
|
|
198
|
+
constants.ACTION_FOCUSSTACKBUNCH,
|
|
199
|
+
{'name': bunch_stack_name, 'frames': dialog.get_bunch_frames(),
|
|
200
|
+
'overlap': dialog.get_bunch_overlap()})
|
|
188
201
|
job.add_sub_action(bunch_stack)
|
|
189
202
|
if dialog.get_focus_stack_pyramid():
|
|
203
|
+
focus_pyramid_name = f'{input_path}-focus-stack-pyramid'
|
|
190
204
|
focus_pyramid = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
191
|
-
{'name':
|
|
205
|
+
{'name': focus_pyramid_name,
|
|
192
206
|
'stacker': constants.STACK_ALGO_PYRAMID})
|
|
193
207
|
job.add_sub_action(focus_pyramid)
|
|
194
208
|
if dialog.get_focus_stack_depth_map():
|
|
209
|
+
focus_depth_map_name = f'{input_path}-focus-stack-depth-map'
|
|
195
210
|
focus_depth_map = ActionConfig(constants.ACTION_FOCUSSTACK,
|
|
196
|
-
{'name':
|
|
211
|
+
{'name': focus_depth_map_name,
|
|
197
212
|
'stacker': constants.STACK_ALGO_DEPTH_MAP})
|
|
198
213
|
job.add_sub_action(focus_depth_map)
|
|
199
214
|
if dialog.get_multi_layer():
|
|
200
|
-
|
|
215
|
+
multi_input_path = []
|
|
201
216
|
if dialog.get_focus_stack_pyramid():
|
|
202
|
-
|
|
217
|
+
multi_input_path.append(focus_pyramid_name)
|
|
203
218
|
if dialog.get_focus_stack_depth_map():
|
|
204
|
-
|
|
219
|
+
multi_input_path.append(focus_depth_map_name)
|
|
205
220
|
if dialog.get_bunch_stack():
|
|
206
|
-
|
|
221
|
+
multi_input_path.append(bunch_stack_name)
|
|
222
|
+
elif preprocess_name:
|
|
223
|
+
multi_input_path.append(preprocess_name)
|
|
207
224
|
multi_layer = ActionConfig(
|
|
208
225
|
constants.ACTION_MULTILAYER,
|
|
209
|
-
{
|
|
210
|
-
'
|
|
211
|
-
'input_path': constants.PATH_SEPARATOR.join(input_path)
|
|
212
|
-
})
|
|
226
|
+
{'name': f'{input_path}-multi-layer',
|
|
227
|
+
'input_path': constants.PATH_SEPARATOR.join(multi_input_path)})
|
|
213
228
|
job.add_sub_action(multi_layer)
|
|
214
229
|
self.add_job_to_project(job)
|
|
215
230
|
self.mark_as_modified(True)
|
|
@@ -231,6 +246,7 @@ class ProjectController(QObject):
|
|
|
231
246
|
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
232
247
|
self.set_project(project)
|
|
233
248
|
self.mark_as_modified(False)
|
|
249
|
+
self.project_editor.reset_undo()
|
|
234
250
|
self.refresh_ui(0, -1)
|
|
235
251
|
if self.job_list_count() > 0:
|
|
236
252
|
self.set_current_job(0)
|
|
@@ -290,6 +306,7 @@ class ProjectController(QObject):
|
|
|
290
306
|
with open(file_path, 'w', encoding="utf-8") as f:
|
|
291
307
|
f.write(json_obj)
|
|
292
308
|
self.mark_as_modified(False)
|
|
309
|
+
self.update_title_requested.emit()
|
|
293
310
|
except Exception as e:
|
|
294
311
|
QMessageBox.critical(self.parent, "Error", f"Cannot save file:\n{str(e)}")
|
|
295
312
|
|
|
@@ -9,7 +9,7 @@ from .. algorithms.vignetting import Vignetting
|
|
|
9
9
|
from .. algorithms.align import AlignFrames
|
|
10
10
|
from .. algorithms.balance import BalanceFrames
|
|
11
11
|
from .. algorithms.stack import FocusStack, FocusStackBunch
|
|
12
|
-
from .. algorithms.
|
|
12
|
+
from .. algorithms.pyramid_auto import PyramidAutoStack
|
|
13
13
|
from .. algorithms.depth_map import DepthMapStack
|
|
14
14
|
from .. algorithms.multilayer import MultiLayer
|
|
15
15
|
from .project_model import Project, ActionConfig
|
|
@@ -104,10 +104,12 @@ class ProjectConverter:
|
|
|
104
104
|
constants.ACTION_FOCUSSTACKBUNCH):
|
|
105
105
|
stacker = action_config.params.get('stacker', constants.STACK_ALGO_DEFAULT)
|
|
106
106
|
if stacker == constants.STACK_ALGO_PYRAMID:
|
|
107
|
-
algo_dict, module_dict = self.filter_dict_keys(
|
|
108
|
-
|
|
107
|
+
algo_dict, module_dict = self.filter_dict_keys(
|
|
108
|
+
action_config.params, 'pyramid_')
|
|
109
|
+
stack_algo = PyramidAutoStack(**algo_dict)
|
|
109
110
|
elif stacker == constants.STACK_ALGO_DEPTH_MAP:
|
|
110
|
-
algo_dict, module_dict = self.filter_dict_keys(
|
|
111
|
+
algo_dict, module_dict = self.filter_dict_keys(
|
|
112
|
+
action_config.params, 'depthmap_')
|
|
111
113
|
stack_algo = DepthMapStack(**algo_dict)
|
|
112
114
|
else:
|
|
113
115
|
raise InvalidOptionError('stacker', stacker, f"valid options are: "
|
|
@@ -117,8 +119,6 @@ class ProjectConverter:
|
|
|
117
119
|
return FocusStack(**module_dict, stack_algo=stack_algo)
|
|
118
120
|
if action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
|
|
119
121
|
return FocusStackBunch(**module_dict, stack_algo=stack_algo)
|
|
120
|
-
raise InvalidOptionError(
|
|
121
|
-
"stracker", stacker, details="valid values are: Pyramid, Depth map.")
|
|
122
122
|
if action_config.type_name == constants.ACTION_MULTILAYER:
|
|
123
123
|
input_path = list(filter(lambda p: p != '',
|
|
124
124
|
action_config.params.get('input_path', '').split(";")))
|
|
@@ -74,19 +74,30 @@ def new_row_after_clone(job, action_row, is_sub_action, cloned):
|
|
|
74
74
|
for action in job.sub_actions[:job.sub_actions.index(cloned)])
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
class ProjectUndoManager:
|
|
78
|
-
|
|
77
|
+
class ProjectUndoManager(QObject):
|
|
78
|
+
set_enabled_undo_action_requested = Signal(bool)
|
|
79
|
+
|
|
80
|
+
def __init__(self, parent=None):
|
|
81
|
+
super().__init__(parent)
|
|
79
82
|
self._undo_buffer = []
|
|
80
83
|
|
|
81
84
|
def add(self, item):
|
|
82
85
|
self._undo_buffer.append(item)
|
|
86
|
+
self.set_enabled_undo_action_requested.emit(True)
|
|
83
87
|
|
|
84
88
|
def pop(self):
|
|
85
|
-
|
|
89
|
+
last = self._undo_buffer.pop()
|
|
90
|
+
if len(self._undo_buffer) == 0:
|
|
91
|
+
self.set_enabled_undo_action_requested.emit(False)
|
|
92
|
+
return last
|
|
86
93
|
|
|
87
94
|
def filled(self):
|
|
88
95
|
return len(self._undo_buffer) != 0
|
|
89
96
|
|
|
97
|
+
def reset(self):
|
|
98
|
+
self._undo_buffer = []
|
|
99
|
+
self.set_enabled_undo_action_requested.emit(False)
|
|
100
|
+
|
|
90
101
|
|
|
91
102
|
class ProjectEditor(QObject):
|
|
92
103
|
INDENT_SPACE = " ↪ "
|
|
@@ -99,7 +110,7 @@ class ProjectEditor(QObject):
|
|
|
99
110
|
|
|
100
111
|
def __init__(self, parent=None):
|
|
101
112
|
super().__init__(parent)
|
|
102
|
-
self.
|
|
113
|
+
self.undo_manager = ProjectUndoManager()
|
|
103
114
|
self._modified = False
|
|
104
115
|
self._project = None
|
|
105
116
|
self._copy_buffer = None
|
|
@@ -108,14 +119,17 @@ class ProjectEditor(QObject):
|
|
|
108
119
|
self._action_list = QListWidget()
|
|
109
120
|
self.dialog = None
|
|
110
121
|
|
|
122
|
+
def reset_undo(self):
|
|
123
|
+
self.undo_manager.reset()
|
|
124
|
+
|
|
111
125
|
def add_undo(self, item):
|
|
112
|
-
self.
|
|
126
|
+
self.undo_manager.add(item)
|
|
113
127
|
|
|
114
128
|
def pop_undo(self):
|
|
115
|
-
return self.
|
|
129
|
+
return self.undo_manager.pop()
|
|
116
130
|
|
|
117
131
|
def filled_undo(self):
|
|
118
|
-
return self.
|
|
132
|
+
return self.undo_manager.filled()
|
|
119
133
|
|
|
120
134
|
def mark_as_modified(self, modified=True):
|
|
121
135
|
self._modified = modified
|
|
@@ -25,14 +25,14 @@ class ExifData(BaseFormDialog):
|
|
|
25
25
|
def add_bold_label(self, label):
|
|
26
26
|
label = QLabel(label)
|
|
27
27
|
label.setStyleSheet("font-weight: bold")
|
|
28
|
-
self.
|
|
28
|
+
self.form_layout.addRow(label)
|
|
29
29
|
|
|
30
30
|
def create_form(self):
|
|
31
|
-
self.
|
|
31
|
+
self.form_layout.addRow(icon_container())
|
|
32
32
|
|
|
33
33
|
spacer = QLabel("")
|
|
34
34
|
spacer.setFixedHeight(10)
|
|
35
|
-
self.
|
|
35
|
+
self.form_layout.addRow(spacer)
|
|
36
36
|
self.add_bold_label("EXIF data")
|
|
37
37
|
shortcuts = {}
|
|
38
38
|
if self.exif is None:
|
|
@@ -47,6 +47,6 @@ class ExifData(BaseFormDialog):
|
|
|
47
47
|
else:
|
|
48
48
|
d = f"{d}"
|
|
49
49
|
if "<<<" not in d and k != 'IPTCNAA':
|
|
50
|
-
self.
|
|
50
|
+
self.form_layout.addRow(f"<b>{k}:</b>", QLabel(d))
|
|
51
51
|
else:
|
|
52
|
-
self.
|
|
52
|
+
self.form_layout.addRow("-", QLabel("Empty EXIF dictionary"))
|