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.

Files changed (37) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/__init__.py +4 -1
  3. shinestacker/algorithms/align.py +128 -14
  4. shinestacker/algorithms/balance.py +362 -163
  5. shinestacker/algorithms/base_stack_algo.py +33 -4
  6. shinestacker/algorithms/depth_map.py +9 -12
  7. shinestacker/algorithms/multilayer.py +12 -2
  8. shinestacker/algorithms/noise_detection.py +8 -3
  9. shinestacker/algorithms/pyramid.py +57 -42
  10. shinestacker/algorithms/pyramid_auto.py +141 -0
  11. shinestacker/algorithms/pyramid_tiles.py +264 -0
  12. shinestacker/algorithms/stack.py +14 -11
  13. shinestacker/algorithms/stack_framework.py +17 -11
  14. shinestacker/algorithms/utils.py +180 -1
  15. shinestacker/algorithms/vignetting.py +23 -5
  16. shinestacker/config/constants.py +31 -5
  17. shinestacker/gui/action_config.py +6 -7
  18. shinestacker/gui/action_config_dialog.py +425 -258
  19. shinestacker/gui/base_form_dialog.py +11 -6
  20. shinestacker/gui/flow_layout.py +105 -0
  21. shinestacker/gui/gui_run.py +24 -19
  22. shinestacker/gui/main_window.py +4 -3
  23. shinestacker/gui/menu_manager.py +12 -2
  24. shinestacker/gui/new_project.py +28 -22
  25. shinestacker/gui/project_controller.py +40 -23
  26. shinestacker/gui/project_converter.py +6 -6
  27. shinestacker/gui/project_editor.py +21 -7
  28. shinestacker/gui/time_progress_bar.py +2 -2
  29. shinestacker/retouch/exif_data.py +5 -5
  30. shinestacker/retouch/shortcuts_help.py +4 -4
  31. shinestacker/retouch/vignetting_filter.py +12 -8
  32. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/METADATA +20 -1
  33. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/RECORD +37 -34
  34. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/WHEEL +0 -0
  35. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/entry_points.txt +0 -0
  36. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/licenses/LICENSE +0 -0
  37. {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.layout = QFormLayout(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.layout.addRow(item)
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()
@@ -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
- if extension_pdf(path):
206
- image_view = GuiPdfView(path, self)
207
- elif extension_tif_jpg(path):
208
- image_view = GuiImageView(path, self)
209
- else:
210
- raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
211
- self.image_views.append(image_view)
212
- self.image_layout.addWidget(image_view)
213
- max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
214
- needed_width = max_width + 20
215
- self.right_area.setFixedWidth(needed_width)
216
- self.image_area_widget.setFixedWidth(needed_width)
217
- self.right_area.updateGeometry()
218
- self.image_area_widget.updateGeometry()
219
- QTimer.singleShot(
220
- 0, lambda: self.right_area.verticalScrollBar().setValue(
221
- self.right_area.verticalScrollBar().maximum()))
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):
@@ -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
- self.menu_manager.run_all_jobs_action.setEnabled(True)
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():
@@ -82,7 +82,10 @@ class MenuManager:
82
82
 
83
83
  def add_edit_menu(self):
84
84
  menu = self.menubar.addMenu("&Edit")
85
- for name in ["&Undo", "&Cut", "Cop&y", "&Paste", "Duplicate"]:
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.run_all_jobs_action.setEnabled(False)
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)
@@ -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.layout.addRow(label)
39
+ self.form_layout.addRow(label)
40
40
 
41
41
  def add_label(self, label):
42
42
  label = QLabel(label)
43
- self.layout.addRow(label)
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.layout.addRow(icon_label)
52
+ self.form_layout.addRow(icon_label)
53
53
  spacer = QLabel("")
54
54
  spacer.setFixedHeight(10)
55
- self.layout.addRow(spacer)
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.layout.addRow("Input folder:", container)
96
- self.layout.addRow("Number of frames: ", self.frames_label)
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.layout.addRow("Automatic noise detection:", self.noise_detection)
101
- self.layout.addRow("Vignetting correction:", self.vignetting_correction)
102
- self.layout.addRow("Align layers:", self.align_frames)
103
- self.layout.addRow("Balance layers:", self.balance_frames)
104
- self.layout.addRow("Bunch stack:", self.bunch_stack)
105
- self.layout.addRow("Bunch frames:", self.bunch_frames)
106
- self.layout.addRow("Bunch overlap:", self.bunch_overlap)
107
- self.layout.addRow("Number of bunches: ", self.bunches_label)
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.layout.addRow("Focus stack (pyramid):", self.focus_stack_pyramid)
110
- self.layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
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.layout.addRow("Focus stack:", self.focus_stack_pyramid)
113
- self.layout.addRow("Save multi layer TIFF:", self.multi_layer)
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
- n_mbytes = float(n_bytes * height * width * self.n_image_files) / 10**6
185
- if n_mbytes > 500 and not self.bunch_stack.isChecked():
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 of memory.\n\n"
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(constants.ACTION_JOB,
159
- {'name': 'detect-noise', 'working_path': working_path,
160
- 'input_path': input_path})
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', 'working_path': working_path,
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
- combo_action = ActionConfig(constants.ACTION_COMBO, {'name': 'align'})
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(constants.ACTION_MASKNOISE, {'name': 'mask-noise'})
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(constants.ACTION_VIGNETTING, {'name': 'vignetting'})
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(constants.ACTION_ALIGNFRAMES, {'name': 'align'})
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(constants.ACTION_BALANCEFRAMES, {'name': 'balance'})
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
- bunch_stack = ActionConfig(constants.ACTION_FOCUSSTACKBUNCH,
186
- {'name': 'bunches', 'frames': dialog.get_bunch_frames(),
187
- 'overlap': dialog.get_bunch_overlap()})
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': 'focus-stack-pyramid',
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': 'focus-stack-depth-map',
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
- input_path = []
215
+ multi_input_path = []
201
216
  if dialog.get_focus_stack_pyramid():
202
- input_path.append("focus-stack-pyramid")
217
+ multi_input_path.append(focus_pyramid_name)
203
218
  if dialog.get_focus_stack_depth_map():
204
- input_path.append("focus-stack-depth-map")
219
+ multi_input_path.append(focus_depth_map_name)
205
220
  if dialog.get_bunch_stack():
206
- input_path.append("bunches")
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
- 'name': 'multi-layer',
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.pyramid import PyramidStack
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(action_config.params, 'pyramid_')
108
- stack_algo = PyramidStack(**algo_dict)
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(action_config.params, 'depthmap_')
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
- def __init__(self):
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
- return self._undo_buffer.pop()
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 = "&nbsp;&nbsp;&nbsp;↪&nbsp;&nbsp;&nbsp;"
@@ -99,7 +110,7 @@ class ProjectEditor(QObject):
99
110
 
100
111
  def __init__(self, parent=None):
101
112
  super().__init__(parent)
102
- self._undo_manager = ProjectUndoManager()
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._undo_manager.add(item)
126
+ self.undo_manager.add(item)
113
127
 
114
128
  def pop_undo(self):
115
- return self._undo_manager.pop()
129
+ return self.undo_manager.pop()
116
130
 
117
131
  def filled_undo(self):
118
- return self._undo_manager.filled()
132
+ return self.undo_manager.filled()
119
133
 
120
134
  def mark_as_modified(self, modified=True):
121
135
  self._modified = modified
@@ -39,8 +39,8 @@ class TimerProgressBar(QProgressBar):
39
39
  """)
40
40
 
41
41
  def time_str(self, secs):
42
- ss = int(secs)
43
- x = secs - ss
42
+ x = secs % 1
43
+ ss = int(secs // 1)
44
44
  s = ss % 60
45
45
  mm = ss // 60
46
46
  m = mm % 60
@@ -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.layout.addRow(label)
28
+ self.form_layout.addRow(label)
29
29
 
30
30
  def create_form(self):
31
- self.layout.addRow(icon_container())
31
+ self.form_layout.addRow(icon_container())
32
32
 
33
33
  spacer = QLabel("")
34
34
  spacer.setFixedHeight(10)
35
- self.layout.addRow(spacer)
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.layout.addRow(f"<b>{k}:</b>", QLabel(d))
50
+ self.form_layout.addRow(f"<b>{k}:</b>", QLabel(d))
51
51
  else:
52
- self.layout.addRow("-", QLabel("Empty EXIF dictionary"))
52
+ self.form_layout.addRow("-", QLabel("Empty EXIF dictionary"))