shinestacker 1.1.0__py3-none-any.whl → 1.2.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/__init__.py +4 -1
- shinestacker/algorithms/align.py +149 -34
- shinestacker/algorithms/balance.py +364 -166
- shinestacker/algorithms/base_stack_algo.py +6 -0
- shinestacker/algorithms/depth_map.py +1 -1
- shinestacker/algorithms/multilayer.py +22 -13
- shinestacker/algorithms/noise_detection.py +7 -8
- shinestacker/algorithms/pyramid.py +3 -2
- shinestacker/algorithms/pyramid_auto.py +141 -0
- shinestacker/algorithms/pyramid_tiles.py +199 -44
- shinestacker/algorithms/stack.py +20 -20
- shinestacker/algorithms/stack_framework.py +136 -156
- shinestacker/algorithms/utils.py +175 -1
- shinestacker/algorithms/vignetting.py +26 -8
- shinestacker/config/constants.py +31 -6
- shinestacker/core/framework.py +12 -12
- shinestacker/gui/action_config.py +59 -7
- shinestacker/gui/action_config_dialog.py +427 -283
- shinestacker/gui/base_form_dialog.py +11 -6
- shinestacker/gui/gui_images.py +10 -10
- shinestacker/gui/gui_run.py +1 -1
- shinestacker/gui/main_window.py +6 -5
- shinestacker/gui/menu_manager.py +16 -2
- shinestacker/gui/new_project.py +26 -22
- shinestacker/gui/project_controller.py +43 -27
- shinestacker/gui/project_converter.py +2 -8
- shinestacker/gui/project_editor.py +50 -27
- shinestacker/gui/tab_widget.py +3 -3
- shinestacker/retouch/exif_data.py +5 -5
- shinestacker/retouch/shortcuts_help.py +4 -4
- shinestacker/retouch/vignetting_filter.py +12 -8
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/METADATA +1 -1
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/RECORD +38 -37
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.1.0.dist-info → shinestacker-1.2.1.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)
|
shinestacker/gui/gui_images.py
CHANGED
|
@@ -67,13 +67,13 @@ class GuiImageView(QWidget):
|
|
|
67
67
|
super().__init__(parent)
|
|
68
68
|
self.file_path = file_path
|
|
69
69
|
self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
|
|
70
|
-
self.
|
|
71
|
-
self.
|
|
72
|
-
self.
|
|
70
|
+
self.main_layout = QVBoxLayout()
|
|
71
|
+
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
72
|
+
self.main_layout.setSpacing(0)
|
|
73
73
|
self.image_label = QLabel()
|
|
74
74
|
self.image_label.setAlignment(Qt.AlignCenter)
|
|
75
|
-
self.
|
|
76
|
-
self.setLayout(self.
|
|
75
|
+
self.main_layout.addWidget(self.image_label)
|
|
76
|
+
self.setLayout(self.main_layout)
|
|
77
77
|
pixmap = QPixmap(file_path)
|
|
78
78
|
if pixmap:
|
|
79
79
|
scaled_pixmap = pixmap.scaledToWidth(
|
|
@@ -105,13 +105,13 @@ class GuiOpenApp(QWidget):
|
|
|
105
105
|
self.file_path = file_path
|
|
106
106
|
self.app = app
|
|
107
107
|
self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
|
|
108
|
-
self.
|
|
109
|
-
self.
|
|
110
|
-
self.
|
|
108
|
+
self.main_layout = QVBoxLayout()
|
|
109
|
+
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
110
|
+
self.main_layout.setSpacing(0)
|
|
111
111
|
self.image_label = QLabel()
|
|
112
112
|
self.image_label.setAlignment(Qt.AlignCenter)
|
|
113
|
-
self.
|
|
114
|
-
self.setLayout(self.
|
|
113
|
+
self.main_layout.addWidget(self.image_label)
|
|
114
|
+
self.setLayout(self.main_layout)
|
|
115
115
|
pixmap = QPixmap(file_path)
|
|
116
116
|
if pixmap:
|
|
117
117
|
scaled_pixmap = pixmap.scaledToWidth(
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -328,7 +328,7 @@ class RunWorker(LogWorker):
|
|
|
328
328
|
self.status_signal.emit(f"{self.tag} running...", constants.RUN_ONGOING, "", 0)
|
|
329
329
|
self.html_signal.emit(f'''
|
|
330
330
|
<div style="margin: 2px 0; font-family: {constants.LOG_FONTS_STR};">
|
|
331
|
-
<span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-
|
|
331
|
+
<span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-weight: bold;">{self.tag} begins</span>
|
|
332
332
|
</div>
|
|
333
333
|
''') # noqa
|
|
334
334
|
status, error_message = self.do_run()
|
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.set_enabled_undo_action)
|
|
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)
|
|
@@ -146,8 +148,8 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
146
148
|
def modified(self):
|
|
147
149
|
return self.project_editor.modified()
|
|
148
150
|
|
|
149
|
-
def mark_as_modified(self, modified=True):
|
|
150
|
-
self.project_editor.mark_as_modified(modified)
|
|
151
|
+
def mark_as_modified(self, modified=True, description=''):
|
|
152
|
+
self.project_editor.mark_as_modified(modified, description)
|
|
151
153
|
|
|
152
154
|
def set_project(self, project):
|
|
153
155
|
self.project_editor.set_project(project)
|
|
@@ -407,13 +409,12 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
407
409
|
self.menu_manager.add_action_entry_action.setEnabled(False)
|
|
408
410
|
self.menu_manager.action_selector.setEnabled(False)
|
|
409
411
|
self.menu_manager.run_job_action.setEnabled(False)
|
|
410
|
-
self.menu_manager.run_all_jobs_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():
|
|
@@ -425,7 +426,7 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
425
426
|
self.expert_options = self.menu_manager.expert_options_action.isChecked()
|
|
426
427
|
|
|
427
428
|
def set_expert_options(self):
|
|
428
|
-
self.expert_options_action.setChecked(True)
|
|
429
|
+
self.menu_manager.expert_options_action.setChecked(True)
|
|
429
430
|
self.expert_options = True
|
|
430
431
|
|
|
431
432
|
def before_thread_begins(self):
|
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,14 @@ 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)
|
|
247
|
+
|
|
248
|
+
def set_enabled_undo_action(self, enabled, description):
|
|
249
|
+
self.undo_action.setEnabled(enabled)
|
|
250
|
+
self.undo_action.setText(f"&Undo {description}")
|
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,26 +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.
|
|
112
|
+
self.form_layout.addRow("Focus stack:", self.focus_stack_pyramid)
|
|
113
113
|
if self.expert():
|
|
114
|
-
self.
|
|
114
|
+
self.form_layout.addRow("Save multi layer TIFF:", self.multi_layer)
|
|
115
115
|
self.add_label("")
|
|
116
116
|
self.add_bold_label("3️⃣ Push 🆗 for further options, then press ▶️ to run.")
|
|
117
117
|
self.add_label("")
|
|
@@ -182,8 +182,8 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
182
182
|
height, width = img.shape[:2]
|
|
183
183
|
n_bytes = 1 if img.dtype == np.uint8 else 2
|
|
184
184
|
n_bits = 8 if img.dtype == np.uint8 else 16
|
|
185
|
-
|
|
186
|
-
if
|
|
185
|
+
n_gbytes = float(n_bytes * height * width * self.n_image_files) / constants.ONE_GIGA
|
|
186
|
+
if n_gbytes > 1 and not self.bunch_stack.isChecked():
|
|
187
187
|
msg = QMessageBox()
|
|
188
188
|
msg.setStyleSheet("""
|
|
189
189
|
QMessageBox {
|
|
@@ -201,11 +201,15 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
201
201
|
msg.setWindowTitle("Too many frames")
|
|
202
202
|
msg.setText(f"You selected {self.n_image_files} images "
|
|
203
203
|
f"with resolution {width}×{height} pixels, {n_bits} bits depth. "
|
|
204
|
-
"Processing may require a significant amount
|
|
204
|
+
"Processing may require a significant amount "
|
|
205
|
+
"of memory or I/O buffering.\n\n"
|
|
205
206
|
"Continue anyway?")
|
|
206
207
|
msg.setInformativeText("You may consider to split the processing "
|
|
207
208
|
" using a bunch stack to reduce memory usage.\n\n"
|
|
208
|
-
'✅ Check the option "Bunch stack"
|
|
209
|
+
'✅ Check the option "Bunch stack".\n\n'
|
|
210
|
+
"➡️ Check expert options for the stacking algorithm."
|
|
211
|
+
'Go to "View" > "Expert Options".'
|
|
212
|
+
)
|
|
209
213
|
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
|
|
210
214
|
msg.setDefaultButton(QMessageBox.Cancel)
|
|
211
215
|
if msg.exec_() != QMessageBox.Ok:
|
|
@@ -29,8 +29,8 @@ class ProjectController(QObject):
|
|
|
29
29
|
def refresh_ui(self, job_row=-1, action_row=-1):
|
|
30
30
|
self.refresh_ui_requested.emit(job_row, action_row)
|
|
31
31
|
|
|
32
|
-
def mark_as_modified(self, modified=True):
|
|
33
|
-
self.project_editor.mark_as_modified(modified)
|
|
32
|
+
def mark_as_modified(self, modified=True, description=''):
|
|
33
|
+
self.project_editor.mark_as_modified(modified, description)
|
|
34
34
|
|
|
35
35
|
def modified(self):
|
|
36
36
|
return self.project_editor.modified()
|
|
@@ -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,68 +152,82 @@ 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
|
-
self.mark_as_modified(True)
|
|
230
|
+
self.mark_as_modified(True, "New Project")
|
|
216
231
|
self.refresh_ui(0, -1)
|
|
217
232
|
|
|
218
233
|
def open_project(self, file_path=False):
|
|
@@ -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)
|
|
@@ -353,4 +369,4 @@ class ProjectController(QObject):
|
|
|
353
369
|
dialog = self.action_config_dialog(action)
|
|
354
370
|
if dialog.exec() == QDialog.Accepted:
|
|
355
371
|
self.on_job_selected(self.current_job_index())
|
|
356
|
-
self.mark_as_modified()
|
|
372
|
+
# self.mark_as_modified(True. "Edit Action") <-- done by dialog
|
|
@@ -9,8 +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.
|
|
13
|
-
from .. algorithms.pyramid_tiles import PyramidTilesStack
|
|
12
|
+
from .. algorithms.pyramid_auto import PyramidAutoStack
|
|
14
13
|
from .. algorithms.depth_map import DepthMapStack
|
|
15
14
|
from .. algorithms.multilayer import MultiLayer
|
|
16
15
|
from .project_model import Project, ActionConfig
|
|
@@ -107,11 +106,7 @@ class ProjectConverter:
|
|
|
107
106
|
if stacker == constants.STACK_ALGO_PYRAMID:
|
|
108
107
|
algo_dict, module_dict = self.filter_dict_keys(
|
|
109
108
|
action_config.params, 'pyramid_')
|
|
110
|
-
stack_algo =
|
|
111
|
-
elif stacker == constants.STACK_ALGO_PYRAMID_TILES:
|
|
112
|
-
algo_dict, module_dict = self.filter_dict_keys(
|
|
113
|
-
action_config.params, 'tiles_pyramid_')
|
|
114
|
-
stack_algo = PyramidTilesStack(**algo_dict)
|
|
109
|
+
stack_algo = PyramidAutoStack(**algo_dict)
|
|
115
110
|
elif stacker == constants.STACK_ALGO_DEPTH_MAP:
|
|
116
111
|
algo_dict, module_dict = self.filter_dict_keys(
|
|
117
112
|
action_config.params, 'depthmap_')
|
|
@@ -119,7 +114,6 @@ class ProjectConverter:
|
|
|
119
114
|
else:
|
|
120
115
|
raise InvalidOptionError('stacker', stacker, f"valid options are: "
|
|
121
116
|
f"{constants.STACK_ALGO_PYRAMID}, "
|
|
122
|
-
f"{constants.STACK_ALGO_PYRAMID_TILES}, "
|
|
123
117
|
f"{constants.STACK_ALGO_DEPTH_MAP}")
|
|
124
118
|
if action_config.type_name == constants.ACTION_FOCUSSTACK:
|
|
125
119
|
return FocusStack(**module_dict, stack_algo=stack_algo)
|
|
@@ -74,19 +74,32 @@ 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, str)
|
|
79
|
+
|
|
80
|
+
def __init__(self, parent=None):
|
|
81
|
+
super().__init__(parent)
|
|
79
82
|
self._undo_buffer = []
|
|
80
83
|
|
|
81
|
-
def add(self, item):
|
|
82
|
-
self._undo_buffer.append(item)
|
|
84
|
+
def add(self, item, description):
|
|
85
|
+
self._undo_buffer.append((item, description))
|
|
86
|
+
self.set_enabled_undo_action_requested.emit(True, description)
|
|
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
|
+
else:
|
|
93
|
+
self.set_enabled_undo_action_requested.emit(True, self._undo_buffer[-1][1])
|
|
94
|
+
return last[0]
|
|
86
95
|
|
|
87
96
|
def filled(self):
|
|
88
97
|
return len(self._undo_buffer) != 0
|
|
89
98
|
|
|
99
|
+
def reset(self):
|
|
100
|
+
self._undo_buffer = []
|
|
101
|
+
self.set_enabled_undo_action_requested.emit(False, '')
|
|
102
|
+
|
|
90
103
|
|
|
91
104
|
class ProjectEditor(QObject):
|
|
92
105
|
INDENT_SPACE = " ↪ "
|
|
@@ -99,7 +112,7 @@ class ProjectEditor(QObject):
|
|
|
99
112
|
|
|
100
113
|
def __init__(self, parent=None):
|
|
101
114
|
super().__init__(parent)
|
|
102
|
-
self.
|
|
115
|
+
self.undo_manager = ProjectUndoManager()
|
|
103
116
|
self._modified = False
|
|
104
117
|
self._project = None
|
|
105
118
|
self._copy_buffer = None
|
|
@@ -108,19 +121,25 @@ class ProjectEditor(QObject):
|
|
|
108
121
|
self._action_list = QListWidget()
|
|
109
122
|
self.dialog = None
|
|
110
123
|
|
|
111
|
-
def
|
|
112
|
-
self.
|
|
124
|
+
def reset_undo(self):
|
|
125
|
+
self.undo_manager.reset()
|
|
126
|
+
|
|
127
|
+
def add_undo(self, item, description=''):
|
|
128
|
+
self.undo_manager.add(item, description)
|
|
113
129
|
|
|
114
130
|
def pop_undo(self):
|
|
115
|
-
return self.
|
|
131
|
+
return self.undo_manager.pop()
|
|
116
132
|
|
|
117
133
|
def filled_undo(self):
|
|
118
|
-
return self.
|
|
134
|
+
return self.undo_manager.filled()
|
|
119
135
|
|
|
120
|
-
def
|
|
136
|
+
def set_modified(self, modified):
|
|
137
|
+
self._modified = modified
|
|
138
|
+
|
|
139
|
+
def mark_as_modified(self, modified=True, description=''):
|
|
121
140
|
self._modified = modified
|
|
122
141
|
if modified:
|
|
123
|
-
self.add_undo(self._project.clone())
|
|
142
|
+
self.add_undo(self._project.clone(), description)
|
|
124
143
|
self.modified_signal.emit(modified)
|
|
125
144
|
|
|
126
145
|
def modified(self):
|
|
@@ -306,7 +325,7 @@ class ProjectEditor(QObject):
|
|
|
306
325
|
new_index = job_index + delta
|
|
307
326
|
if 0 <= new_index < self.num_project_jobs():
|
|
308
327
|
jobs = self.project_jobs()
|
|
309
|
-
self.mark_as_modified()
|
|
328
|
+
self.mark_as_modified(True, "Shift Job")
|
|
310
329
|
jobs.insert(new_index, jobs.pop(job_index))
|
|
311
330
|
self.refresh_ui_signal.emit(new_index, -1)
|
|
312
331
|
|
|
@@ -316,12 +335,12 @@ class ProjectEditor(QObject):
|
|
|
316
335
|
if not pos.is_sub_action:
|
|
317
336
|
new_index = pos.action_index + delta
|
|
318
337
|
if 0 <= new_index < len(pos.actions):
|
|
319
|
-
self.mark_as_modified()
|
|
338
|
+
self.mark_as_modified(True, "Shift Action")
|
|
320
339
|
pos.actions.insert(new_index, pos.actions.pop(pos.action_index))
|
|
321
340
|
else:
|
|
322
341
|
new_index = pos.sub_action_index + delta
|
|
323
342
|
if 0 <= new_index < len(pos.sub_actions):
|
|
324
|
-
self.mark_as_modified()
|
|
343
|
+
self.mark_as_modified(True, "Shift Sub-action")
|
|
325
344
|
pos.sub_actions.insert(new_index, pos.sub_actions.pop(pos.sub_action_index))
|
|
326
345
|
new_row = new_row_after_insert(action_row, pos, delta)
|
|
327
346
|
self.refresh_ui_signal.emit(job_row, new_row)
|
|
@@ -343,7 +362,7 @@ class ProjectEditor(QObject):
|
|
|
343
362
|
if 0 <= job_index < self.num_project_jobs():
|
|
344
363
|
job_clone = self.project_job(job_index).clone(self.CLONE_POSTFIX)
|
|
345
364
|
new_job_index = job_index + 1
|
|
346
|
-
self.mark_as_modified()
|
|
365
|
+
self.mark_as_modified(True, "Duplicate Job")
|
|
347
366
|
self.project_jobs().insert(new_job_index, job_clone)
|
|
348
367
|
self.set_current_job(new_job_index)
|
|
349
368
|
self.set_current_action(new_job_index)
|
|
@@ -353,7 +372,7 @@ class ProjectEditor(QObject):
|
|
|
353
372
|
job_row, action_row, pos = self.get_current_action()
|
|
354
373
|
if not pos.actions:
|
|
355
374
|
return
|
|
356
|
-
self.mark_as_modified()
|
|
375
|
+
self.mark_as_modified(True, "Duplicate Action")
|
|
357
376
|
job = self.project_job(job_row)
|
|
358
377
|
if pos.is_sub_action:
|
|
359
378
|
cloned = pos.sub_action.clone(self.CLONE_POSTFIX)
|
|
@@ -384,7 +403,7 @@ class ProjectEditor(QObject):
|
|
|
384
403
|
reply = None
|
|
385
404
|
if not confirm or reply == QMessageBox.Yes:
|
|
386
405
|
self.take_job(current_index)
|
|
387
|
-
self.mark_as_modified()
|
|
406
|
+
self.mark_as_modified(True, "Delete Job")
|
|
388
407
|
current_job = self.project_jobs().pop(current_index)
|
|
389
408
|
self.clear_action_list()
|
|
390
409
|
self.refresh_ui_signal.emit(-1, -1)
|
|
@@ -406,10 +425,11 @@ class ProjectEditor(QObject):
|
|
|
406
425
|
else:
|
|
407
426
|
reply = None
|
|
408
427
|
if not confirm or reply == QMessageBox.Yes:
|
|
409
|
-
self.mark_as_modified()
|
|
410
428
|
if pos.is_sub_action:
|
|
429
|
+
self.mark_as_modified(True, "Delete Action")
|
|
411
430
|
pos.action.pop_sub_action(pos.sub_action_index)
|
|
412
431
|
else:
|
|
432
|
+
self.mark_as_modified(True, "Delete Sub-action")
|
|
413
433
|
self.project_job(job_row).pop_sub_action(pos.action_index)
|
|
414
434
|
new_row = new_row_after_delete(action_row, pos)
|
|
415
435
|
self.refresh_ui_signal.emit(job_row, new_row)
|
|
@@ -432,7 +452,7 @@ class ProjectEditor(QObject):
|
|
|
432
452
|
job_action = ActionConfig("Job")
|
|
433
453
|
self.dialog = self.action_config_dialog(job_action)
|
|
434
454
|
if self.dialog.exec() == QDialog.Accepted:
|
|
435
|
-
self.mark_as_modified()
|
|
455
|
+
self.mark_as_modified(True, "Add Job")
|
|
436
456
|
self.project_jobs().append(job_action)
|
|
437
457
|
self.add_list_item(self.job_list(), job_action, False)
|
|
438
458
|
self.set_current_job(self.job_list_count() - 1)
|
|
@@ -453,7 +473,7 @@ class ProjectEditor(QObject):
|
|
|
453
473
|
action.parent = self.get_current_job()
|
|
454
474
|
self.dialog = self.action_config_dialog(action)
|
|
455
475
|
if self.dialog.exec() == QDialog.Accepted:
|
|
456
|
-
self.mark_as_modified()
|
|
476
|
+
self.mark_as_modified("Add Action")
|
|
457
477
|
self.project_job(current_index).add_sub_action(action)
|
|
458
478
|
self.add_list_item(self.action_list(), action, False)
|
|
459
479
|
self.enable_delete_action_signal.emit(False)
|
|
@@ -493,7 +513,7 @@ class ProjectEditor(QObject):
|
|
|
493
513
|
sub_action = ActionConfig(type_name)
|
|
494
514
|
self.dialog = self.action_config_dialog(sub_action)
|
|
495
515
|
if self.dialog.exec() == QDialog.Accepted:
|
|
496
|
-
self.mark_as_modified()
|
|
516
|
+
self.mark_as_modified("Add Sub-action")
|
|
497
517
|
action.add_sub_action(sub_action)
|
|
498
518
|
self.on_job_selected(current_job_index)
|
|
499
519
|
self.set_current_action(current_action_index)
|
|
@@ -521,7 +541,7 @@ class ProjectEditor(QObject):
|
|
|
521
541
|
job_index = self.current_job_index()
|
|
522
542
|
if 0 <= job_index < self.num_project_jobs():
|
|
523
543
|
new_job_index = job_index
|
|
524
|
-
self.mark_as_modified()
|
|
544
|
+
self.mark_as_modified(True, "Paste Job")
|
|
525
545
|
self.project_jobs().insert(new_job_index, self.copy_buffer())
|
|
526
546
|
self.set_current_job(new_job_index)
|
|
527
547
|
self.set_current_action(new_job_index)
|
|
@@ -533,13 +553,13 @@ class ProjectEditor(QObject):
|
|
|
533
553
|
if not pos.is_sub_action:
|
|
534
554
|
if self.copy_buffer().type_name not in constants.ACTION_TYPES:
|
|
535
555
|
return
|
|
536
|
-
self.mark_as_modified()
|
|
556
|
+
self.mark_as_modified(True, "Paste Action")
|
|
537
557
|
pos.actions.insert(pos.action_index, self.copy_buffer())
|
|
538
558
|
else:
|
|
539
559
|
if pos.action.type_name != constants.ACTION_COMBO or \
|
|
540
560
|
self.copy_buffer().type_name not in constants.SUB_ACTION_TYPES:
|
|
541
561
|
return
|
|
542
|
-
self.mark_as_modified()
|
|
562
|
+
self.mark_as_modified(True, "Paste Sub-action")
|
|
543
563
|
pos.sub_actions.insert(pos.sub_action_index, self.copy_buffer())
|
|
544
564
|
new_row = new_row_after_paste(action_row, pos)
|
|
545
565
|
self.refresh_ui_signal.emit(job_row, new_row)
|
|
@@ -583,7 +603,10 @@ class ProjectEditor(QObject):
|
|
|
583
603
|
action_row = -1
|
|
584
604
|
if current_action:
|
|
585
605
|
if current_action.enabled() != enabled:
|
|
586
|
-
|
|
606
|
+
if enabled:
|
|
607
|
+
self.mark_as_modified(True, "Enable")
|
|
608
|
+
else:
|
|
609
|
+
self.mark_as_modified(True, "Disable")
|
|
587
610
|
current_action.set_enabled(enabled)
|
|
588
611
|
self.refresh_ui_signal.emit(job_row, action_row)
|
|
589
612
|
|
|
@@ -594,7 +617,7 @@ class ProjectEditor(QObject):
|
|
|
594
617
|
self.set_enabled(False)
|
|
595
618
|
|
|
596
619
|
def set_enabled_all(self, enable=True):
|
|
597
|
-
self.mark_as_modified()
|
|
620
|
+
self.mark_as_modified(True, "Enable All")
|
|
598
621
|
job_row = self.current_job_index()
|
|
599
622
|
action_row = self.current_action_index()
|
|
600
623
|
for j in self.project_jobs():
|
shinestacker/gui/tab_widget.py
CHANGED
|
@@ -12,10 +12,10 @@ class TabWidgetWithPlaceholder(QWidget):
|
|
|
12
12
|
|
|
13
13
|
def __init__(self, parent=None):
|
|
14
14
|
super().__init__(parent)
|
|
15
|
-
self.
|
|
16
|
-
self.
|
|
15
|
+
self.main_layout = QVBoxLayout(self)
|
|
16
|
+
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
17
17
|
self.stacked_widget = QStackedWidget()
|
|
18
|
-
self.
|
|
18
|
+
self.main_layout.addWidget(self.stacked_widget)
|
|
19
19
|
self.tab_widget = QTabWidget()
|
|
20
20
|
self.stacked_widget.addWidget(self.tab_widget)
|
|
21
21
|
self.placeholder = QLabel()
|