shinestacker 1.1.0__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 (34) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/__init__.py +4 -1
  3. shinestacker/algorithms/align.py +117 -3
  4. shinestacker/algorithms/balance.py +362 -163
  5. shinestacker/algorithms/base_stack_algo.py +6 -0
  6. shinestacker/algorithms/depth_map.py +1 -1
  7. shinestacker/algorithms/multilayer.py +12 -2
  8. shinestacker/algorithms/noise_detection.py +1 -1
  9. shinestacker/algorithms/pyramid.py +3 -2
  10. shinestacker/algorithms/pyramid_auto.py +141 -0
  11. shinestacker/algorithms/pyramid_tiles.py +199 -44
  12. shinestacker/algorithms/stack.py +3 -3
  13. shinestacker/algorithms/stack_framework.py +13 -4
  14. shinestacker/algorithms/utils.py +175 -1
  15. shinestacker/algorithms/vignetting.py +23 -5
  16. shinestacker/config/constants.py +29 -6
  17. shinestacker/gui/action_config.py +6 -7
  18. shinestacker/gui/action_config_dialog.py +425 -280
  19. shinestacker/gui/base_form_dialog.py +11 -6
  20. shinestacker/gui/main_window.py +3 -2
  21. shinestacker/gui/menu_manager.py +12 -2
  22. shinestacker/gui/new_project.py +27 -22
  23. shinestacker/gui/project_controller.py +39 -23
  24. shinestacker/gui/project_converter.py +2 -8
  25. shinestacker/gui/project_editor.py +21 -7
  26. shinestacker/retouch/exif_data.py +5 -5
  27. shinestacker/retouch/shortcuts_help.py +4 -4
  28. shinestacker/retouch/vignetting_filter.py +12 -8
  29. {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/METADATA +1 -1
  30. {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/RECORD +34 -33
  31. {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/WHEEL +0 -0
  32. {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/entry_points.txt +0 -0
  33. {shinestacker-1.1.0.dist-info → shinestacker-1.2.0.dist-info}/licenses/LICENSE +0 -0
  34. {shinestacker-1.1.0.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)
@@ -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)
@@ -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
- 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,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.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)
112
+ self.form_layout.addRow("Focus stack:", self.focus_stack_pyramid)
113
113
  if self.expert():
114
- self.layout.addRow("Save multi layer TIFF:", self.multi_layer)
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,9 @@ 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
- n_mbytes = float(n_bytes * height * width * self.n_image_files) / 10**6
186
- 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():
187
188
  msg = QMessageBox()
188
189
  msg.setStyleSheet("""
189
190
  QMessageBox {
@@ -201,11 +202,15 @@ class NewProjectDialog(BaseFormDialog):
201
202
  msg.setWindowTitle("Too many frames")
202
203
  msg.setText(f"You selected {self.n_image_files} images "
203
204
  f"with resolution {width}×{height} pixels, {n_bits} bits depth. "
204
- "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"
205
207
  "Continue anyway?")
206
208
  msg.setInformativeText("You may consider to split the processing "
207
209
  " using a bunch stack to reduce memory usage.\n\n"
208
- '✅ 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
+ )
209
214
  msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
210
215
  msg.setDefaultButton(QMessageBox.Cancel)
211
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)
@@ -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.pyramid import PyramidStack
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 = PyramidStack(**algo_dict)
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,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 = "   ↪   "
@@ -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
@@ -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"))
@@ -10,7 +10,7 @@ class ShortcutsHelp(QDialog):
10
10
  super().__init__(parent)
11
11
  self.setWindowTitle("Shortcut Help")
12
12
  self.resize(600, self.height())
13
- self.layout = QVBoxLayout(self)
13
+ self.main_layout = QVBoxLayout(self)
14
14
  main_widget = QWidget()
15
15
  main_layout = QHBoxLayout(main_widget)
16
16
  main_layout.setContentsMargins(0, 0, 0, 0)
@@ -28,14 +28,14 @@ class ShortcutsHelp(QDialog):
28
28
  right_layout.setLabelAlignment(Qt.AlignLeft)
29
29
  main_layout.addWidget(left_column)
30
30
  main_layout.addWidget(right_column)
31
- self.layout.addWidget(main_widget)
31
+ self.main_layout.addWidget(main_widget)
32
32
  self.create_form(left_layout, right_layout)
33
33
  button_box = QHBoxLayout()
34
34
  ok_button = QPushButton("OK")
35
35
  ok_button.setFixedWidth(100)
36
36
  ok_button.setFocus()
37
37
  button_box.addWidget(ok_button)
38
- self.layout.addLayout(button_box)
38
+ self.main_layout.addLayout(button_box)
39
39
  ok_button.clicked.connect(self.accept)
40
40
 
41
41
  def add_bold_label(self, layout, label):
@@ -44,7 +44,7 @@ class ShortcutsHelp(QDialog):
44
44
  layout.addRow(label)
45
45
 
46
46
  def create_form(self, left_layout, right_layout):
47
- self.layout.insertWidget(0, icon_container())
47
+ self.main_layout.insertWidget(0, icon_container())
48
48
 
49
49
  shortcuts = {
50
50
  "M": "show master layer",
@@ -1,6 +1,6 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0902
2
2
  from PySide6.QtCore import Qt
3
- from PySide6.QtWidgets import QSpinBox, QCheckBox, QLabel, QHBoxLayout, QSlider
3
+ from PySide6.QtWidgets import QSpinBox, QCheckBox, QLabel, QHBoxLayout, QSlider, QComboBox
4
4
  from .. config.constants import constants
5
5
  from .. algorithms.vignetting import correct_vignetting
6
6
  from .base_filter import OneSliderBaseFilter
@@ -20,11 +20,15 @@ class VignettingFilter(OneSliderBaseFilter):
20
20
  self.threshold_initial_value = constants.DEFAULT_BLACK_THRESHOLD
21
21
  self.threshold_format = "{:.1f}"
22
22
 
23
+ def get_subsample_factor(self):
24
+ return constants.FIELD_SUBSAMPLE_VALUES[
25
+ constants.FIELD_SUBSAMPLE_OPTIONS.index(self.subsample_box.currentText())]
26
+
23
27
  def apply(self, image, strength):
24
28
  return correct_vignetting(image, max_correction=strength,
25
29
  black_threshold=self.threshold_slider.value(),
26
30
  r_steps=self.r_steps_box.value(),
27
- subsample=self.subsample_box.value(),
31
+ subsample=self.get_subsample_factor(),
28
32
  fast_subsampling=True)
29
33
 
30
34
  def add_widgets(self, layout, dlg):
@@ -42,11 +46,10 @@ class VignettingFilter(OneSliderBaseFilter):
42
46
  layout.addLayout(threshold_layout)
43
47
  subsample_layout = QHBoxLayout()
44
48
  subsample_label = QLabel("Subsample:")
45
- self.subsample_box = QSpinBox()
46
- self.subsample_box.setFixedWidth(50)
47
- self.subsample_box.setRange(1, 50)
48
- self.subsample_box.setValue(constants.DEFAULT_VIGN_SUBSAMPLE)
49
- self.subsample_box.valueChanged.connect(self.threshold_changed)
49
+ self.subsample_box = QComboBox()
50
+ self.subsample_box.addItems(constants.FIELD_SUBSAMPLE_OPTIONS)
51
+ self.subsample_box.setFixedWidth(150)
52
+ self.subsample_box.currentTextChanged.connect(self.threshold_changed)
50
53
  self.fast_subsampling_check = QCheckBox("Fast subsampling")
51
54
  self.fast_subsampling_check.setChecked(constants.DEFAULT_VIGN_FAST_SUBSAMPLING)
52
55
  r_steps_label = QLabel("Radial steps:")
@@ -64,6 +67,7 @@ class VignettingFilter(OneSliderBaseFilter):
64
67
  layout.addWidget(self.fast_subsampling_check)
65
68
 
66
69
  def threshold_changed(self, val):
67
- float_val = self.threshold_max_value * float(val) / self.threshold_max_range
70
+ subsample = self.get_subsample_factor()
71
+ float_val = self.threshold_max_value * float(subsample) / self.threshold_max_range
68
72
  self.threshold_label.setText(self.threshold_format.format(float_val))
69
73
  self.param_changed(val)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -1,21 +1,22 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=XIz3qAg9G9YysQi3Ryp0CN3rtc_JiecHZ9L2vEzcM6s,21
3
- shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
4
- shinestacker/algorithms/align.py,sha256=EhsV50QtpLSJG0uDMfOJw89u8CGFJvBC2sYuJg5cv6g,17516
5
- shinestacker/algorithms/balance.py,sha256=iSjO-pl0vQv58iEQ077EUcDTAExMKDBdtXmJXbMhazk,16721
6
- shinestacker/algorithms/base_stack_algo.py,sha256=O7pDXqLM8MBdLR634Vk3UNV6cEV2q0U7CNcnpC_AOig,2363
2
+ shinestacker/_version.py,sha256=5GblYyMbk8JySosj59Rvi2uzLqfP-DAs77ikwTafXT4,21
3
+ shinestacker/algorithms/__init__.py,sha256=1FwVJ3w9GGbFFkjYJRUedTvcdE4j0ieSgaH9RC9iCY4,877
4
+ shinestacker/algorithms/align.py,sha256=BsE3rp4EfJyWTTbC6U1leQQqURD724YLAmxY-1wM-ok,22558
5
+ shinestacker/algorithms/balance.py,sha256=rz_lmBwnJBYLnYt6yJ30BXWmMwpmmmA3rKUznEWY0eo,23463
6
+ shinestacker/algorithms/base_stack_algo.py,sha256=W-VSrCF0-lE_OOsxsnZvJ3BI0NqRKIRMciQV-ui5t_g,2515
7
7
  shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
8
- shinestacker/algorithms/depth_map.py,sha256=KVThrnynPKuijlh-DrenSkdkZ0Qm6TaNMYKhRByhcN4,5682
8
+ shinestacker/algorithms/depth_map.py,sha256=m0_Qm8FLDeSWyQEMNx29PzXp_VFGar7gY3jWxq_10t8,5713
9
9
  shinestacker/algorithms/exif.py,sha256=SM4ZDDe8hCJ3xY6053FNndOiwzEStzdp0WrXurlcHVc,9429
10
- shinestacker/algorithms/multilayer.py,sha256=-pQXDlooSMGKPhMgF-_naXdkGdolclYvSD-RrjwLiyI,9328
11
- shinestacker/algorithms/noise_detection.py,sha256=CJb57mE7ePJBgrwnsEkeK8xVIl2Hrzti11ZEI6JQczs,9218
12
- shinestacker/algorithms/pyramid.py,sha256=cxwA6gf02009dFv5-m79NpJkD58-Wbu3im4bfA5HVUc,8822
13
- shinestacker/algorithms/pyramid_tiles.py,sha256=967L42MfwSOewisqpAzuXivZgVoKZbIjDIQVWP1_rHk,5094
10
+ shinestacker/algorithms/multilayer.py,sha256=5aIaIU2GX1eoZbGL4z-Xo8XiFlCvIcZKRDYnbjjdojI,9906
11
+ shinestacker/algorithms/noise_detection.py,sha256=lP5iJLFA5xWN-tpyNDH9AWuoZsL7rKhrxwDcBBc9nCk,9250
12
+ shinestacker/algorithms/pyramid.py,sha256=lmd-lw4bzrpcfBaLnBXHuOJ9J7-5sWq4dC9p_EejqXA,8881
13
+ shinestacker/algorithms/pyramid_auto.py,sha256=ByDH7Xblqj4YfNwsCWwN3wv2xL6gYp2lFnvpNPSEawM,6161
14
+ shinestacker/algorithms/pyramid_tiles.py,sha256=SzjTSheme8MP8nQXfOu8QHbzrtpuQX2aIsBVr5aM4Mc,12165
14
15
  shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
15
- shinestacker/algorithms/stack.py,sha256=T1y-qoYUNzcIpnhKcou_4ifiKtGC2ZA1bOZXlfnKB6A,5045
16
- shinestacker/algorithms/stack_framework.py,sha256=fHdU8uYZquRut6NW_1vHbTCjPD99gQfOhVDdaaLZH34,12334
17
- shinestacker/algorithms/utils.py,sha256=GSKPUxU98Q8F0k4TgY9ydEgBul1gf2I0ypdmyDm--Mg,3371
18
- shinestacker/algorithms/vignetting.py,sha256=yW-1TF4tesLWfKQOS0XxRkOEN82U-YDmMaj09C9cH4M,9552
16
+ shinestacker/algorithms/stack.py,sha256=fLDps52QT8bsC3Pz8niU0h7cGc70q9goKsgA8wvu_Bg,5054
17
+ shinestacker/algorithms/stack_framework.py,sha256=YKk-JoLV6IVlTiCbo6e-Hg2dF3hdz4CE6WFIXhMXBy8,12818
18
+ shinestacker/algorithms/utils.py,sha256=jImR2XF73gsLRZMic0kv8cyCuO2Zq21tX4kUhaTcfzI,11301
19
+ shinestacker/algorithms/vignetting.py,sha256=stzrWTxJIIByq_mOI0ofE-7b0bL5MLm9dhlj_E-Kxv0,10165
19
20
  shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
20
21
  shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
22
  shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
@@ -27,7 +28,7 @@ shinestacker/app/project.py,sha256=W0u715LZne_PNJvg9msSy27ybIjgDXiEAQdJ7_6BjYI,2
27
28
  shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
28
29
  shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
29
30
  shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
30
- shinestacker/config/constants.py,sha256=X9e0fXr7ZHN9DCEMiObpanNJfZ5cMgWJdm3Xybmh-Wk,6232
31
+ shinestacker/config/constants.py,sha256=ncuiYyA5NgggkGbazQbccJHGzXJnl37AOyxEB-GLoB0,7116
31
32
  shinestacker/config/gui_constants.py,sha256=5DR-ET1oeMMD7lIsjvAwSuln89A7I9wy9VuAeRo2G64,2575
32
33
  shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
33
34
  shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
@@ -36,20 +37,20 @@ shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk
36
37
  shinestacker/core/framework.py,sha256=zCnJuQrHNpwEgJW23_BgS7iQrLolRWTAMB1oRp_a7Kk,7447
37
38
  shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
38
39
  shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- shinestacker/gui/action_config.py,sha256=yXNDv0MyONbHk4iUrkvMkLKKaDvpJyzA5Yr0Eikgo0c,16986
40
- shinestacker/gui/action_config_dialog.py,sha256=Xb8DryEQt9R3VvM2DVoT_SdlJMU_qzJs2Z7NdLeqsgY,34520
41
- shinestacker/gui/base_form_dialog.py,sha256=yYqMee1mzw9VBx8siBS0jDk1qqsTIKJUgdjh92aprQk,687
40
+ shinestacker/gui/action_config.py,sha256=-J4UCbXHHlIPxRDL0Veglt4brngJP7_zHvrWof9pzsM,16895
41
+ shinestacker/gui/action_config_dialog.py,sha256=USaF2QeQRNXgzLRbkV6hvVRzOMkMVyTtsAzEAMqCr-g,35191
42
+ shinestacker/gui/base_form_dialog.py,sha256=M_BvjpqUWe9YuecKg0pF3vUJ-1mqF24HjKujst2j3BA,753
42
43
  shinestacker/gui/colors.py,sha256=m0pQQ-uvtIN1xmb_-N06BvC7pZYZZnq59ZSEJwutHuk,1432
43
44
  shinestacker/gui/flow_layout.py,sha256=3yBU_z7VtvHKpx1H97CHVd81eq9pe1Dcja2EZBGGKcI,3791
44
45
  shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
45
46
  shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
46
47
  shinestacker/gui/gui_run.py,sha256=ahbl6xMFR78QrcBbEDMuaQpkxw6DBFtSX8DCMIyr_7I,15439
47
- shinestacker/gui/main_window.py,sha256=KVr3ApbQSjJgmhHnrcqTjGQNTj1LoTN2PD6bWLBjsh8,24280
48
- shinestacker/gui/menu_manager.py,sha256=_L6LOikB3impEYqilqwXc0WJuunishjz57ozZlrBn7Q,9616
49
- shinestacker/gui/new_project.py,sha256=c0y2BjnAVaf5Z88UDmuOGR5rdju0Q72ltqiE7T3QivY,10807
50
- shinestacker/gui/project_controller.py,sha256=zVMH8kcNJ75dXPjaTa0IQiavqcWxG1URlVVYWnnu1C0,15123
51
- shinestacker/gui/project_converter.py,sha256=8ko3D4D7x4hhwENxwpTeElnLtEex3lpR51nZbq30Uco,7655
52
- shinestacker/gui/project_editor.py,sha256=uouzmUkrqouQlq-dqPOgSO16r1WOnGNV2v8jTcZlRXU,23749
48
+ shinestacker/gui/main_window.py,sha256=kgozNZWqiPePZB5oX3Twdj_Z_f1RcdoLT2PFMzT0fRM,24361
49
+ shinestacker/gui/menu_manager.py,sha256=SvYV7Dmz4cqoFQtRrO4JQ0uECZ4rL3EQdQg5jNakV6Y,10015
50
+ shinestacker/gui/new_project.py,sha256=Iu8Gm3oRx6tCokv-Ywm_64wsNNxj_QhU1BsyoeLkJaQ,11211
51
+ shinestacker/gui/project_controller.py,sha256=ooHunFKY2-dRBoyx4r3T8vs7UOpGDZUHHaSSR5V8J_E,15821
52
+ shinestacker/gui/project_converter.py,sha256=bNyC1_D_MjcTOCPlQln6CIIlX818-sw_j2omrfQIGQs,7279
53
+ shinestacker/gui/project_editor.py,sha256=UPXNwjpKJi7lgaohlbtojkTkotXeGY7l_W8pVII-UM0,24208
53
54
  shinestacker/gui/project_model.py,sha256=eRUmH3QmRzDtPtZoxgT6amKzN8_5XzwjHgEJeL-_JOE,4263
54
55
  shinestacker/gui/select_path_widget.py,sha256=OfQImOmkzbvl5BBshmb7ePWrSGDJQ8VvyaAOypHAGd4,1023
55
56
  shinestacker/gui/tab_widget.py,sha256=6iUifK-wu0EzjVFccKHirhA2fENglVi6xREKiD96aaY,2950
@@ -71,7 +72,7 @@ shinestacker/retouch/brush_preview.py,sha256=QKD3pL7n7YJbIibinUFYKv7lkyq_AWLpt6o
71
72
  shinestacker/retouch/brush_tool.py,sha256=nxnEuvTioPNw1WeWsT20X1zl-LNZ8i-1ExOcihikEjk,8618
72
73
  shinestacker/retouch/denoise_filter.py,sha256=TDUHzhRKlKvCa3D5SCYCZKTpjcl81kGwmONsgSDtO1k,440
73
74
  shinestacker/retouch/display_manager.py,sha256=XPbOBmoYc_jNA791WkWkOSaFHb0ztCZechl2p2KSlwQ,9597
74
- shinestacker/retouch/exif_data.py,sha256=giqoIaMlhN6H3x8BAd73ghVHODmWIcD_QbSoDymQycU,1864
75
+ shinestacker/retouch/exif_data.py,sha256=WF40bTh0bwIqSQLMkGMCycEED06_q35-TqrBNAyaB-k,1889
75
76
  shinestacker/retouch/file_loader.py,sha256=z02-A8_uDZxayI1NFTxT2GVUvEBWStchX9hlN1o5-0U,4784
76
77
  shinestacker/retouch/filter_manager.py,sha256=SdYIZkZBUvuB6wDG0moGWav5sfEvIcB9ioUJR5wJFts,388
77
78
  shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
@@ -80,14 +81,14 @@ shinestacker/retouch/image_viewer.py,sha256=3ebrLHTDtGd_EbIT2nNFRUjH836rblmmK7jZ
80
81
  shinestacker/retouch/io_gui_handler.py,sha256=pT-49uP0GROMOjZ70LoMLgXHnqSDq8ieAlAKGw0t1TM,11418
81
82
  shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
82
83
  shinestacker/retouch/layer_collection.py,sha256=Q7zoCYRn__jYkfrEC2lY1uKHWfOUbsJ27xaYHIoKVxo,5730
83
- shinestacker/retouch/shortcuts_help.py,sha256=SN4vNa_6yRAFaWxt5HpWn8FHgwmHrIs_wYwjl4iyDmg,3769
84
+ shinestacker/retouch/shortcuts_help.py,sha256=-beAkhxVia-wyuYMiAn2uQZxnUdSNadmZM77EdReoOk,3789
84
85
  shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-KqMlQRo,3257
85
86
  shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
86
- shinestacker/retouch/vignetting_filter.py,sha256=3WuoF38lQOIaU1MWmqviItuQn8NnbMN0nwV7pM9IJqU,3453
87
+ shinestacker/retouch/vignetting_filter.py,sha256=MA97rQkSL0D-Nh-n2L4AiPR064RoTROkvza4tw84g9U,3658
87
88
  shinestacker/retouch/white_balance_filter.py,sha256=glMBYlmrF-i_OrB3sGUpjZE6X4FQdyLC4GBy2bWtaFc,6056
88
- shinestacker-1.1.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
89
- shinestacker-1.1.0.dist-info/METADATA,sha256=cg9TAx9qmIME39z02b5lLxjbtXhf_sT0aGNsfHh296E,6951
90
- shinestacker-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
91
- shinestacker-1.1.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
92
- shinestacker-1.1.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
93
- shinestacker-1.1.0.dist-info/RECORD,,
89
+ shinestacker-1.2.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
90
+ shinestacker-1.2.0.dist-info/METADATA,sha256=01kq9TSpVY2_U67ZhjbPIORbMmWTOWpuiiOFty3IM1Y,6951
91
+ shinestacker-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
92
+ shinestacker-1.2.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
93
+ shinestacker-1.2.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
94
+ shinestacker-1.2.0.dist-info/RECORD,,