shinestacker 1.8.0__py3-none-any.whl → 1.9.3__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 (46) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +202 -81
  3. shinestacker/algorithms/align_auto.py +13 -11
  4. shinestacker/algorithms/align_parallel.py +50 -21
  5. shinestacker/algorithms/balance.py +1 -1
  6. shinestacker/algorithms/base_stack_algo.py +1 -1
  7. shinestacker/algorithms/exif.py +848 -127
  8. shinestacker/algorithms/multilayer.py +6 -4
  9. shinestacker/algorithms/noise_detection.py +10 -8
  10. shinestacker/algorithms/pyramid_tiles.py +1 -1
  11. shinestacker/algorithms/stack.py +33 -17
  12. shinestacker/algorithms/stack_framework.py +16 -11
  13. shinestacker/algorithms/utils.py +18 -2
  14. shinestacker/algorithms/vignetting.py +16 -3
  15. shinestacker/app/main.py +1 -1
  16. shinestacker/app/settings_dialog.py +297 -173
  17. shinestacker/config/constants.py +10 -6
  18. shinestacker/config/settings.py +25 -7
  19. shinestacker/core/exceptions.py +1 -1
  20. shinestacker/core/framework.py +2 -2
  21. shinestacker/gui/action_config.py +23 -20
  22. shinestacker/gui/action_config_dialog.py +38 -25
  23. shinestacker/gui/config_dialog.py +6 -5
  24. shinestacker/gui/folder_file_selection.py +3 -2
  25. shinestacker/gui/gui_images.py +27 -3
  26. shinestacker/gui/gui_run.py +2 -2
  27. shinestacker/gui/main_window.py +6 -0
  28. shinestacker/gui/menu_manager.py +8 -2
  29. shinestacker/gui/new_project.py +23 -12
  30. shinestacker/gui/project_controller.py +14 -6
  31. shinestacker/gui/project_editor.py +12 -2
  32. shinestacker/gui/project_model.py +4 -4
  33. shinestacker/retouch/brush_tool.py +20 -0
  34. shinestacker/retouch/exif_data.py +106 -38
  35. shinestacker/retouch/file_loader.py +3 -3
  36. shinestacker/retouch/image_editor_ui.py +79 -3
  37. shinestacker/retouch/image_viewer.py +6 -1
  38. shinestacker/retouch/io_gui_handler.py +13 -16
  39. shinestacker/retouch/shortcuts_help.py +15 -8
  40. shinestacker/retouch/view_strategy.py +12 -2
  41. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/METADATA +37 -39
  42. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/RECORD +46 -46
  43. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/WHEEL +0 -0
  44. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/entry_points.txt +0 -0
  45. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/licenses/LICENSE +0 -0
  46. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,174 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, W0718, R0903, E0611, R0902
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, E1121
2
+ from abc import ABC, abstractmethod
2
3
  from PySide6.QtCore import Signal
3
- from PySide6.QtWidgets import QFrame, QLabel, QCheckBox, QComboBox, QDoubleSpinBox, QSpinBox
4
+ from PySide6.QtWidgets import QLabel, QCheckBox, QComboBox, QDoubleSpinBox, QSpinBox
4
5
  from .. config.settings import Settings
5
6
  from .. config.constants import constants
6
7
  from .. config.gui_constants import gui_constants
7
8
  from .. gui.config_dialog import ConfigDialog
9
+ from .. gui.action_config import add_tab, create_tab_widget
8
10
  from .. gui.action_config_dialog import AlignFramesConfigBase
9
11
 
10
12
 
13
+ class BaseParameter(ABC):
14
+ def __init__(self, key, label, tooltip=""):
15
+ self.key = key
16
+ self.label = label
17
+ self.tooltip = tooltip
18
+ self.widget = None
19
+
20
+ @abstractmethod
21
+ def create_widget(self, parent):
22
+ pass
23
+
24
+ @abstractmethod
25
+ def get_value(self):
26
+ pass
27
+
28
+ @abstractmethod
29
+ def set_value(self, value):
30
+ pass
31
+
32
+ @abstractmethod
33
+ def set_default(self):
34
+ pass
35
+
36
+
37
+ class NestedParameter(BaseParameter):
38
+ def __init__(self, parent_key, key, label, tooltip=""):
39
+ super().__init__(key, label, tooltip)
40
+ self.parent_key = parent_key
41
+
42
+ def get_nested_value(self, settings):
43
+ return settings.get(self.parent_key).get(self.key)
44
+
45
+ def set_nested_value(self, settings, value):
46
+ nested_dict = settings.get(self.parent_key).copy()
47
+ nested_dict[self.key] = value
48
+ settings.set(self.parent_key, nested_dict)
49
+
50
+
51
+ class CheckBoxParameter(BaseParameter):
52
+ def __init__(self, key, label, default_value, tooltip=""):
53
+ super().__init__(key, label, tooltip)
54
+ self.default_value = default_value
55
+
56
+ def create_widget(self, parent):
57
+ self.widget = QCheckBox(parent)
58
+ if self.tooltip:
59
+ self.widget.setToolTip(self.tooltip)
60
+ return self.widget
61
+
62
+ def get_value(self):
63
+ return self.widget.isChecked()
64
+
65
+ def set_value(self, value):
66
+ self.widget.setChecked(value)
67
+
68
+ def set_default(self):
69
+ self.widget.setChecked(self.default_value)
70
+
71
+
72
+ class SpinBoxParameter(BaseParameter):
73
+ def __init__(self, key, label, default_value, min_val, max_val, step=1, tooltip=""):
74
+ super().__init__(key, label, tooltip)
75
+ self.default_value = default_value
76
+ self.min_val = min_val
77
+ self.max_val = max_val
78
+ self.step = step
79
+
80
+ def create_widget(self, parent):
81
+ self.widget = QSpinBox(parent)
82
+ self.widget.setRange(self.min_val, self.max_val)
83
+ self.widget.setSingleStep(self.step)
84
+ if self.tooltip:
85
+ self.widget.setToolTip(self.tooltip)
86
+ return self.widget
87
+
88
+ def get_value(self):
89
+ return self.widget.value()
90
+
91
+ def set_value(self, value):
92
+ self.widget.setValue(value)
93
+
94
+ def set_default(self):
95
+ self.widget.setValue(self.default_value)
96
+
97
+
98
+ class DoubleSpinBoxParameter(SpinBoxParameter):
99
+ def create_widget(self, parent):
100
+ self.widget = QDoubleSpinBox(parent)
101
+ self.widget.setRange(self.min_val, self.max_val)
102
+ self.widget.setSingleStep(self.step)
103
+ if self.tooltip:
104
+ self.widget.setToolTip(self.tooltip)
105
+ return self.widget
106
+
107
+
108
+ class ComboBoxParameter(BaseParameter):
109
+ def __init__(self, key, label, default_value, options, tooltip=""):
110
+ super().__init__(key, label, tooltip)
111
+ self.default_value = default_value
112
+ self.options = options
113
+
114
+ def create_widget(self, parent):
115
+ self.widget = QComboBox(parent)
116
+ for display_text, data in self.options:
117
+ self.widget.addItem(display_text, data)
118
+ if self.tooltip:
119
+ self.widget.setToolTip(self.tooltip)
120
+ return self.widget
121
+
122
+ def get_value(self):
123
+ return self.widget.itemData(self.widget.currentIndex())
124
+
125
+ def set_value(self, value):
126
+ idx = self.widget.findData(value)
127
+ if idx >= 0:
128
+ self.widget.setCurrentIndex(idx)
129
+
130
+ def set_default(self):
131
+ idx = self.widget.findData(self.default_value)
132
+ if idx >= 0:
133
+ self.widget.setCurrentIndex(idx)
134
+
135
+
136
+ class CallbackComboBoxParameter(ComboBoxParameter):
137
+ def __init__(self, key, label, default_value, options, tooltip="", on_change=None):
138
+ super().__init__(key, label, default_value, options, tooltip)
139
+ self.on_change = on_change
140
+
141
+ def create_widget(self, parent):
142
+ widget = super().create_widget(parent)
143
+ if self.on_change:
144
+ widget.currentIndexChanged.connect(self.on_change)
145
+ return widget
146
+
147
+
148
+ class NestedSpinBoxParameter(SpinBoxParameter, NestedParameter):
149
+ def __init__(self, parent_key, key, label, default_value, min_val, max_val, step=1, tooltip=""):
150
+ SpinBoxParameter.__init__(
151
+ self, key, label, default_value, min_val, max_val, step, tooltip)
152
+ NestedParameter.__init__(
153
+ self, parent_key, key, label, tooltip)
154
+
155
+
156
+ class NestedDoubleSpinBoxParameter(DoubleSpinBoxParameter, NestedParameter):
157
+ def __init__(self, parent_key, key, label, default_value, min_val, max_val, step=1, tooltip=""):
158
+ DoubleSpinBoxParameter.__init__(
159
+ self, key, label, default_value, min_val, max_val, step, tooltip)
160
+ NestedParameter.__init__(
161
+ self, parent_key, key, label, tooltip)
162
+
163
+
164
+ class NestedCallbackComboBoxParameter(CallbackComboBoxParameter, NestedParameter):
165
+ def __init__(self, parent_key, key, label, default_value,
166
+ options, tooltip="", on_change=None):
167
+ CallbackComboBoxParameter.__init__(
168
+ self, key, label, default_value, options, tooltip, on_change)
169
+ NestedParameter.__init__(self, parent_key, key, label, tooltip)
170
+
171
+
11
172
  class SettingsDialog(ConfigDialog, AlignFramesConfigBase):
12
173
  update_project_config_requested = Signal()
13
174
  update_retouch_config_requested = Signal()
@@ -17,163 +178,144 @@ class SettingsDialog(ConfigDialog, AlignFramesConfigBase):
17
178
  self.project_settings = project_settings
18
179
  self.retouch_settings = retouch_settings
19
180
  self.settings = Settings.instance()
20
- self.expert_options = None
21
- self.combined_actions_max_threads = None
22
- self.align_frames_max_threads = None
23
- self.detector = None
24
- self.descriptor = None
25
- self.matching_method = None
26
- self.focus_stack_max_threads = None
27
- self.view_strategy = None
28
- self.min_mouse_step_brush_fraction = None
29
- self.paint_refresh_time = None
30
- self.display_refresh_time = None
31
- self.cursor_update_time = None
181
+ self.project_parameters = []
182
+ self.retouch_parameters = []
183
+ self._init_parameters()
32
184
  super().__init__("Settings", parent)
33
185
 
186
+ def _init_parameters(self):
187
+ if self.project_settings:
188
+ self.project_parameters = [
189
+ CheckBoxParameter(
190
+ 'expert_options', 'Expert options:',
191
+ constants.DEFAULT_EXPERT_OPTIONS),
192
+ NestedSpinBoxParameter(
193
+ 'combined_actions_params', 'max_threads',
194
+ 'Combined actions, max num. of cores:',
195
+ constants.DEFAULT_FWK_MAX_THREADS, 0, 64),
196
+ NestedDoubleSpinBoxParameter(
197
+ 'align_frames_params', 'memory_limit',
198
+ 'Align frames, mem. limit (approx., GBytes):',
199
+ constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB, 1.0, 64.0, 1.0),
200
+ NestedSpinBoxParameter(
201
+ 'align_frames_params', 'max_threads',
202
+ 'Align frames, max num. of cores:',
203
+ constants.DEFAULT_ALIGN_MAX_THREADS, 0, 64),
204
+ NestedCallbackComboBoxParameter(
205
+ 'align_frames_params', 'detector', 'Detector:',
206
+ constants.DEFAULT_DETECTOR, [(d, d) for d in constants.VALID_DETECTORS],
207
+ tooltip=self.DETECTOR_DESCRIPTOR_TOOLTIPS['detector'],
208
+ on_change=self.change_match_config_settings),
209
+ NestedCallbackComboBoxParameter(
210
+ 'align_frames_params', 'descriptor', 'Descriptor:',
211
+ constants.DEFAULT_DESCRIPTOR, [(d, d) for d in constants.VALID_DESCRIPTORS],
212
+ tooltip=self.DETECTOR_DESCRIPTOR_TOOLTIPS['descriptor'],
213
+ on_change=self.change_match_config_settings),
214
+ NestedCallbackComboBoxParameter(
215
+ 'align_frames_params', 'match_method', 'Match method:',
216
+ constants.DEFAULT_MATCHING_METHOD,
217
+ list(zip(self.MATCHING_METHOD_OPTIONS, constants.VALID_MATCHING_METHODS)),
218
+ tooltip=self.DETECTOR_DESCRIPTOR_TOOLTIPS['match_method'],
219
+ on_change=self.change_match_config_settings),
220
+ NestedDoubleSpinBoxParameter(
221
+ 'focus_stack_params', 'memory_limit',
222
+ 'Focus stacking, mem. limit (approx., GBytes):',
223
+ constants.DEFAULT_PY_MEMORY_LIMIT_GB, 1.0, 64.0, 1.0),
224
+ NestedSpinBoxParameter(
225
+ 'focus_stack_params', 'max_threads', 'Focus stacking, max. num. of cores:',
226
+ constants.DEFAULT_PY_MAX_THREADS, 0, 64),
227
+ ]
228
+ if self.retouch_settings:
229
+ self.retouch_parameters = [
230
+ ComboBoxParameter(
231
+ 'view_strategy', 'View strategy:',
232
+ constants.DEFAULT_VIEW_STRATEGY,
233
+ [
234
+ ("Overlaid", "overlaid"),
235
+ ("Side by side", "sidebyside"),
236
+ ("Top-Bottom", "topbottom")
237
+ ]),
238
+ DoubleSpinBoxParameter(
239
+ 'min_mouse_step_brush_fraction', 'Min. mouse step in brush units:',
240
+ gui_constants.DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION, 0, 1, 0.02),
241
+ SpinBoxParameter(
242
+ 'paint_refresh_time', 'Paint refresh time:',
243
+ gui_constants.DEFAULT_PAINT_REFRESH_TIME, 0, 1000),
244
+ SpinBoxParameter(
245
+ 'display_refresh_time', 'Display refresh time:',
246
+ gui_constants.DEFAULT_DISPLAY_REFRESH_TIME, 0, 200),
247
+ SpinBoxParameter(
248
+ 'cursor_update_time', 'Cursor refresh time:',
249
+ gui_constants.DEFAULT_CURSOR_UPDATE_TIME, 0, 50),
250
+ ]
251
+
34
252
  def create_form_content(self):
253
+ self.tab_widget = create_tab_widget(self.container_layout)
35
254
  if self.project_settings:
36
- self.create_project_settings()
37
- separator = QFrame()
38
- separator.setFrameShape(QFrame.HLine)
39
- separator.setFrameShadow(QFrame.Sunken)
40
- separator.setLineWidth(1)
41
- self.container_layout.addRow(separator)
255
+ project_tab_layout = add_tab(self.tab_widget, "Project Settings")
256
+ self.create_project_settings(project_tab_layout)
42
257
  if self.retouch_settings:
43
- self.create_retouch_settings()
258
+ retouch_tab_layout = add_tab(self.tab_widget, "Retouch Settings")
259
+ self.create_retouch_settings(retouch_tab_layout)
44
260
 
45
- def create_project_settings(self):
46
- label = QLabel("Project settings")
261
+ def create_project_settings(self, layout=None):
262
+ if layout is None:
263
+ layout = self.container_layout
264
+ label = QLabel("Project settings:")
47
265
  label.setStyleSheet("font-weight: bold")
48
- self.container_layout.addRow(label)
49
- self.expert_options = QCheckBox()
50
- self.expert_options.setChecked(self.settings.get('expert_options'))
51
- self.container_layout.addRow("Expert options:", self.expert_options)
52
- self.combined_actions_max_threads = QSpinBox()
53
- self.combined_actions_max_threads.setRange(0, 64)
54
- self.combined_actions_max_threads.setValue(
55
- self.settings.get('combined_actions_params')['max_threads'])
56
- self.container_layout.addRow("Max num. of cores, combined actions:",
57
- self.combined_actions_max_threads)
58
-
59
- self.align_frames_max_threads = QSpinBox()
60
- self.align_frames_max_threads.setRange(0, 64)
61
- self.align_frames_max_threads.setValue(
62
- self.settings.get('align_frames_params')['max_threads'])
63
- self.container_layout.addRow("Max num. of cores, align frames:",
64
- self.align_frames_max_threads)
65
-
66
- def change_match_config():
67
- self.change_match_config(
68
- self.detector, self.descriptor,
69
- self. matching_method, self.show_info)
70
-
71
- self.detector = QComboBox()
72
- self.detector.addItems(constants.VALID_DETECTORS)
73
- self.descriptor = QComboBox()
74
- self.descriptor.addItems(constants.VALID_DESCRIPTORS)
75
- self.matching_method = QComboBox()
266
+ layout.addRow(label)
267
+ for param in self.project_parameters:
268
+ widget = param.create_widget(self)
269
+ param.set_value(self._get_current_value(param))
270
+ layout.addRow(param.label, widget)
76
271
  self.info_label = QLabel()
77
272
  self.info_label.setStyleSheet("color: orange; font-style: italic;")
78
- self.matching_method = QComboBox()
79
- for k, v in zip(self.MATCHING_METHOD_OPTIONS, constants.VALID_MATCHING_METHODS):
80
- self.matching_method.addItem(k, v)
81
- self.detector.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['detector'])
82
- self.descriptor.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['descriptor'])
83
- self.matching_method.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['match_method'])
84
- self.detector.currentIndexChanged.connect(change_match_config)
85
- self.descriptor.currentIndexChanged.connect(change_match_config)
86
- self.matching_method.currentIndexChanged.connect(change_match_config)
87
- self.container_layout.addRow('Detector:', self.detector)
88
- self.container_layout.addRow('Descriptor:', self.descriptor)
89
- self.container_layout.addRow(self.info_label)
90
- self.container_layout.addRow('Match method:', self.matching_method)
91
-
92
- self.focus_stack_max_threads = QSpinBox()
93
- self.focus_stack_max_threads.setRange(0, 64)
94
- self.focus_stack_max_threads.setValue(
95
- self.settings.get('align_frames_params')['max_threads'])
96
- self.container_layout.addRow("Max num. of cores, focus stacking:",
97
- self.focus_stack_max_threads)
98
-
99
- def create_retouch_settings(self):
100
- label = QLabel("Retouch settings")
273
+ layout.addRow(self.info_label)
274
+
275
+ def create_retouch_settings(self, layout=None):
276
+ if layout is None:
277
+ layout = self.container_layout
278
+ label = QLabel("Retouch settings:")
101
279
  label.setStyleSheet("font-weight: bold")
102
- self.container_layout.addRow(label)
103
- self.view_strategy = QComboBox()
104
- self.view_strategy.addItem("Overlaid", "overlaid")
105
- self.view_strategy.addItem("Side by side", "sidebyside")
106
- self.view_strategy.addItem("Top-Bottom", "topbottom")
107
- idx = self.view_strategy.findData(self.settings.get('view_strategy'))
108
- if idx >= 0:
109
- self.view_strategy.setCurrentIndex(idx)
110
- self.container_layout.addRow("View strategy:", self.view_strategy)
111
- self.min_mouse_step_brush_fraction = QDoubleSpinBox()
112
- self.min_mouse_step_brush_fraction.setValue(
113
- self.settings.get('min_mouse_step_brush_fraction'))
114
- self.min_mouse_step_brush_fraction.setRange(0, 1)
115
- self.min_mouse_step_brush_fraction.setDecimals(2)
116
- self.min_mouse_step_brush_fraction.setSingleStep(0.02)
117
- self.container_layout.addRow("Min. mouse step in brush units:",
118
- self.min_mouse_step_brush_fraction)
119
- self.paint_refresh_time = QSpinBox()
120
- self.paint_refresh_time.setRange(0, 1000)
121
- self.paint_refresh_time.setValue(
122
- self.settings.get('paint_refresh_time'))
123
- self.container_layout.addRow("Paint refresh time:",
124
- self.paint_refresh_time)
125
- self.display_refresh_time = QSpinBox()
126
- self.display_refresh_time.setRange(0, 200)
127
- self.display_refresh_time.setValue(
128
- self.settings.get('display_refresh_time'))
129
- self.container_layout.addRow("Display refresh time:",
130
- self.display_refresh_time)
131
-
132
- self.cursor_update_time = QSpinBox()
133
- self.cursor_update_time.setRange(0, 50)
134
- self.cursor_update_time.setValue(
135
- self.settings.get('cursor_update_time'))
136
- self.container_layout.addRow("Cursor refresh time:",
137
- self.cursor_update_time)
280
+ layout.addRow(label)
281
+ for param in self.retouch_parameters:
282
+ widget = param.create_widget(self)
283
+ param.set_value(self._get_current_value(param))
284
+ layout.addRow(param.label, widget)
285
+
286
+ def _get_current_value(self, param):
287
+ if isinstance(param, NestedParameter):
288
+ return param.get_nested_value(self.settings)
289
+ return self.settings.get(param.key)
290
+
291
+ def _set_current_value(self, param, value):
292
+ if isinstance(param, NestedParameter):
293
+ param.set_nested_value(self.settings, value)
294
+ else:
295
+ self.settings.set(param.key, value)
296
+
297
+ def change_match_config_settings(self):
298
+ detector_widget = None
299
+ descriptor_widget = None
300
+ matching_method_widget = None
301
+ for param in self.project_parameters:
302
+ if (isinstance(param, NestedParameter) and
303
+ param.parent_key == 'align_frames_params'):
304
+ if param.key == 'detector':
305
+ detector_widget = param.widget
306
+ elif param.key == 'descriptor':
307
+ descriptor_widget = param.widget
308
+ elif param.key == 'match_method':
309
+ matching_method_widget = param.widget
310
+ if detector_widget and descriptor_widget and matching_method_widget:
311
+ self.change_match_config(
312
+ detector_widget, descriptor_widget, matching_method_widget, self.show_info)
138
313
 
139
314
  def accept(self):
140
- if self.project_settings:
141
- self.settings.set(
142
- 'expert_options', self.expert_options.isChecked())
143
- self.settings.set(
144
- 'combined_actions_params', {
145
- 'max_threads': self.combined_actions_max_threads.value()
146
- })
147
- self.settings.set(
148
- 'align_frames_params', {
149
- 'max_threads':
150
- self.align_frames_max_threads.value(),
151
- 'detector':
152
- self.descriptor.currentText(),
153
- 'descriptor':
154
- self.descriptor.currentText(),
155
- 'match_method':
156
- self.matching_method.itemData(self.matching_method.currentIndex())
157
- })
158
- self.settings.set(
159
- 'focus_stack_params', {
160
- 'max_threads': self.focus_stack_max_threads.value()
161
- })
162
- self.settings.set(
163
- 'focus_stack_bunch:params', {
164
- 'max_threads': self.focus_stack_max_threads.value()
165
- })
166
- if self.retouch_settings:
167
- self.settings.set(
168
- 'view_strategy', self.view_strategy.itemData(self.view_strategy.currentIndex()))
169
- self.settings.set(
170
- 'min_mouse_step_brush_fraction', self.min_mouse_step_brush_fraction.value())
171
- self.settings.set(
172
- 'paint_refresh_time', self.paint_refresh_time.value())
173
- self.settings.set(
174
- 'display_refresh_time', self.display_refresh_time.value())
175
- self.settings.set(
176
- 'cursor_update_time', self.cursor_update_time.value())
315
+ for param in self.project_parameters:
316
+ self._set_current_value(param, param.get_value())
317
+ for param in self.retouch_parameters:
318
+ self._set_current_value(param, param.get_value())
177
319
  self.settings.update()
178
320
  if self.project_settings:
179
321
  self.update_project_config_requested.emit()
@@ -182,32 +324,14 @@ class SettingsDialog(ConfigDialog, AlignFramesConfigBase):
182
324
  super().accept()
183
325
 
184
326
  def reset_to_defaults(self):
185
- if self.project_settings:
186
- self.expert_options.setChecked(constants.DEFAULT_EXPERT_OPTIONS)
187
- self.combined_actions_max_threads.setValue(constants.DEFAULT_MAX_FWK_THREADS)
188
- self.align_frames_max_threads.setValue(constants.DEFAULT_ALIGN_MAX_THREADS)
189
- self.detector.setCurrentText(constants.DEFAULT_DETECTOR)
190
- self.descriptor.setCurrentText(constants.DEFAULT_DESCRIPTOR)
191
- idx = self.matching_method.findData(constants.DEFAULT_MATCHING_METHOD)
192
- if idx >= 0:
193
- self.matching_method.setCurrentIndex(idx)
194
- self.focus_stack_max_threads.setValue(constants.DEFAULT_PY_MAX_THREADS)
195
- if self.retouch_settings:
196
- idx = self.view_strategy.findData(constants.DEFAULT_VIEW_STRATEGY)
197
- if idx >= 0:
198
- self.view_strategy.setCurrentIndex(idx)
199
- self.min_mouse_step_brush_fraction.setValue(
200
- gui_constants.DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION)
201
- self.paint_refresh_time.setValue(
202
- gui_constants.DEFAULT_PAINT_REFRESH_TIME)
203
- self.display_refresh_time.setValue(
204
- gui_constants.DEFAULT_DISPLAY_REFRESH_TIME)
205
- self.cursor_update_time.setValue(
206
- gui_constants.DEFAULT_CURSOR_UPDATE_TIME)
207
-
208
-
209
- def show_settings_dialog(parent, project_settings, retouch_settings,
210
- handle_project_config, handle_retouch_config):
327
+ for param in self.project_parameters:
328
+ param.set_default()
329
+ for param in self.retouch_parameters:
330
+ param.set_default()
331
+
332
+
333
+ def show_settings_dialog(
334
+ parent, project_settings, retouch_settings, handle_project_config, handle_retouch_config):
211
335
  dialog = SettingsDialog(parent, project_settings, retouch_settings)
212
336
  dialog.update_project_config_requested.connect(handle_project_config)
213
337
  dialog.update_retouch_config_requested.connect(handle_retouch_config)
@@ -7,7 +7,6 @@ import os
7
7
  class _Constants:
8
8
  APP_TITLE = "Shine Stacker"
9
9
  APP_STRING = "ShineStacker"
10
- EXTENSIONS = set(["jpeg", "jpg", "png", "tif", "tiff"])
11
10
 
12
11
  NUM_UINT8 = 256
13
12
  NUM_UINT16 = 65536
@@ -89,9 +88,12 @@ class _Constants:
89
88
  DEFAULT_MULTILAYER_FILE_REVERSE_ORDER = True
90
89
  MULTILAYER_WARNING_MEM_GB = 1
91
90
 
91
+ DEFAULT_COMBINED_ACTIONS_STEP_PROCESS = True
92
+
92
93
  DEFAULT_PLOTS_PATH = 'plots'
93
- DEFAULT_MAX_FWK_THREADS = 8
94
- DEFAULT_MAX_FWK_CHUNK_SUBMIT = True
94
+ DEFAULT_FWK_MEMORY_LIMIT_GB = 8
95
+ DEFAULT_FWK_MAX_THREADS = 8
96
+ DEFAULT_FWK_CHUNK_SUBMIT = True
95
97
 
96
98
  FIELD_SUBSAMPLE_VALUES_1 = [2, 3, 4, 6, 8, 12, 16, 24, 32]
97
99
  FIELD_SUBSAMPLE_OPTIONS_1 = [f"1/{n} × 1/{n}" for n in FIELD_SUBSAMPLE_VALUES_1]
@@ -99,7 +101,7 @@ class _Constants:
99
101
  FIELD_SUBSAMPLE_OPTIONS = ['Auto', 'Full resolution'] + FIELD_SUBSAMPLE_OPTIONS_1
100
102
  FIELD_SUBSAMPLE_DEFAULT = FIELD_SUBSAMPLE_VALUES[0]
101
103
 
102
- DEFAULT_NOISE_MAP_FILENAME = "noise-map/hot_pixels.png"
104
+ DEFAULT_NOISE_MAP_FILENAME = "hot_pixels.png"
103
105
  DEFAULT_NOISE_MAX_FRAMES = 10
104
106
  DEFAULT_MN_KERNEL_SIZE = 3
105
107
  INTERPOLATE_MEAN = 'MEAN'
@@ -135,7 +137,7 @@ class _Constants:
135
137
  VALID_MATCHING_METHODS = [MATCHING_KNN, MATCHING_NORM_HAMMING]
136
138
  VALID_TRANSFORMS = [ALIGN_RIGID, ALIGN_HOMOGRAPHY]
137
139
  VALID_BORDER_MODES = [BORDER_CONSTANT, BORDER_REPLICATE, BORDER_REPLICATE_BLUR]
138
- VALID_ALIGN_METHODS = [ALIGN_RANSAC, ALIGN_LMEDS]
140
+ VALID_ESTIMATION_METHODS = [ALIGN_RANSAC, ALIGN_LMEDS]
139
141
  NOKNN_METHODS = {'detectors': [DETECTOR_ORB, DETECTOR_SURF, DETECTOR_AKAZE, DETECTOR_BRISK],
140
142
  'descriptors': [DESCRIPTOR_ORB, DESCRIPTOR_AKAZE, DESCRIPTOR_BRISK]}
141
143
 
@@ -148,7 +150,7 @@ class _Constants:
148
150
  DEFAULT_ALIGN_THRESHOLD = 0.75
149
151
  DEFAULT_TRANSFORM = ALIGN_RIGID
150
152
  DEFAULT_BORDER_MODE = BORDER_REPLICATE_BLUR
151
- DEFAULT_ALIGN_METHOD = 'RANSAC'
153
+ DEFAULT_ESTIMATION_METHOD = 'RANSAC'
152
154
  DEFAULT_RANS_THRESHOLD = 3.0 # px
153
155
  DEFAULT_REFINE_ITERS = 100
154
156
  DEFAULT_ALIGN_CONFIDENCE = 99.9
@@ -160,12 +162,14 @@ class _Constants:
160
162
  DEFAULT_ALIGN_RES_TARGET_MPX = 2
161
163
  DEFAULT_ALIGN_FAST_SUBSAMPLING = False
162
164
  DEFAULT_ALIGN_MIN_GOOD_MATCHES = 20
165
+ DEFAULT_PHASE_CORR_FALLBACK = False
163
166
  ALIGN_VALID_MODES = ['auto', 'sequential', 'parallel']
164
167
  DEFAULT_ALIGN_MODE = 'auto'
165
168
  DEFAULT_ALIGN_MEMORY_LIMIT_GB = 8
166
169
  DEFAULT_ALIGN_MAX_THREADS = min(os.cpu_count() or 4, 8)
167
170
  DEFAULT_ALIGN_CHUNK_SUBMIT = True
168
171
  DEFAULT_ALIGN_BW_MATCHING = False
172
+ DEFAULT_ALIGN_DELTA_MAX = 2
169
173
 
170
174
  BALANCE_LINEAR = "LINEAR"
171
175
  BALANCE_GAMMA = "GAMMA"
@@ -39,23 +39,26 @@ DEFAULT_SETTINGS = {
39
39
  'cursor_update_time': gui_constants.DEFAULT_CURSOR_UPDATE_TIME,
40
40
  'min_mouse_step_brush_fraction': gui_constants.DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION,
41
41
  'combined_actions_params': {
42
- 'max_threads': constants.DEFAULT_MAX_FWK_THREADS
42
+ 'max_threads': constants.DEFAULT_FWK_MAX_THREADS
43
43
  },
44
44
  'align_frames_params': {
45
+ 'memory_limit': constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
45
46
  'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS,
46
47
  'detector': constants.DEFAULT_DETECTOR,
47
48
  'descriptor': constants.DEFAULT_DESCRIPTOR,
48
49
  'match_method': constants.DEFAULT_MATCHING_METHOD
49
50
  },
50
51
  'focus_stack_params': {
52
+ 'memory_limit': constants.DEFAULT_PY_MEMORY_LIMIT_GB,
51
53
  'max_threads': constants.DEFAULT_PY_MAX_THREADS
52
54
  },
53
55
  'focus_stack_bunch_params': {
56
+ 'memory_limit': constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
54
57
  'max_threads': constants.DEFAULT_PY_MAX_THREADS
55
58
  }
56
59
  }
57
60
 
58
- CURRENT_VERSION = 1
61
+ CURRENT_SETTINGS_FILE_VERSION = 1
59
62
 
60
63
 
61
64
  class Settings(StdPathFile):
@@ -66,18 +69,30 @@ class Settings(StdPathFile):
66
69
  if Settings._instance is not None:
67
70
  raise RuntimeError("Settings is a singleton.")
68
71
  super().__init__(filename)
69
- self.settings = DEFAULT_SETTINGS
72
+ self.settings = self._deep_copy_defaults()
70
73
  file_path = self.get_file_path()
71
74
  if os.path.isfile(file_path):
72
75
  try:
73
76
  with open(file_path, 'r', encoding="utf-8") as file:
74
77
  json_data = json.load(file)
75
- settings = json_data['settings']
78
+ file_settings = json_data['settings']
79
+ self._deep_merge_settings(file_settings)
76
80
  except Exception as e:
77
81
  traceback.print_tb(e.__traceback__)
78
82
  print(f"Can't read file from path {file_path}. Default settings ignored.")
79
- settings = {}
80
- self.settings = {**self.settings, **settings}
83
+
84
+ def _deep_copy_defaults(self):
85
+ return json.loads(json.dumps(DEFAULT_SETTINGS))
86
+
87
+ def _deep_merge_settings(self, file_settings):
88
+ for key, value in file_settings.items():
89
+ if key in self.settings:
90
+ if isinstance(value, dict) and isinstance(self.settings[key], dict):
91
+ for sub_key, sub_value in value.items():
92
+ if sub_key in self.settings[key]:
93
+ self.settings[key][sub_key] = sub_value
94
+ else:
95
+ self.settings[key] = value
81
96
 
82
97
  @classmethod
83
98
  def instance(cls, filename="shinestacker-settings.txt"):
@@ -99,7 +114,10 @@ class Settings(StdPathFile):
99
114
  try:
100
115
  config_dir = self.get_config_dir()
101
116
  os.makedirs(config_dir, exist_ok=True)
102
- json_data = {'version': CURRENT_VERSION, 'settings': self.settings}
117
+ json_data = {
118
+ 'version': CURRENT_SETTINGS_FILE_VERSION,
119
+ 'settings': self.settings
120
+ }
103
121
  json_obj = jsonpickle.encode(json_data)
104
122
  with open(self.get_file_path(), 'w', encoding="utf-8") as f:
105
123
  f.write(json_obj)
@@ -30,7 +30,7 @@ class AlignmentError(FocusStackError):
30
30
  def __init__(self, index, details):
31
31
  self.index = index
32
32
  self.details = details
33
- super().__init__(f"Alignment failed for image {index}: {details}")
33
+ super().__init__(f"Alignment failed for frame {index}: {details}")
34
34
 
35
35
 
36
36
  class BitDepthError(FocusStackError):
@@ -197,8 +197,8 @@ class Job(TaskBase):
197
197
 
198
198
  class SequentialTask(TaskBase):
199
199
  def __init__(self, name, enabled=True, **kwargs):
200
- self.max_threads = kwargs.pop('max_threads', constants.DEFAULT_MAX_FWK_THREADS)
201
- self.chunk_submit = kwargs.pop('chunk_submit', constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
200
+ self.max_threads = kwargs.pop('max_threads', constants.DEFAULT_FWK_MAX_THREADS)
201
+ self.chunk_submit = kwargs.pop('chunk_submit', constants.DEFAULT_FWK_CHUNK_SUBMIT)
202
202
  TaskBase.__init__(self, name, enabled, **kwargs)
203
203
  self.total_action_counts = None
204
204
  self.current_action_count = None