shinestacker 1.7.0__py3-none-any.whl → 1.8.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (45) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +184 -80
  3. shinestacker/algorithms/align_auto.py +13 -11
  4. shinestacker/algorithms/align_parallel.py +41 -16
  5. shinestacker/algorithms/base_stack_algo.py +1 -1
  6. shinestacker/algorithms/noise_detection.py +10 -8
  7. shinestacker/algorithms/pyramid_tiles.py +1 -1
  8. shinestacker/algorithms/stack.py +9 -0
  9. shinestacker/algorithms/stack_framework.py +49 -25
  10. shinestacker/algorithms/utils.py +5 -1
  11. shinestacker/algorithms/vignetting.py +16 -3
  12. shinestacker/app/settings_dialog.py +303 -136
  13. shinestacker/config/constants.py +10 -5
  14. shinestacker/config/settings.py +29 -8
  15. shinestacker/core/core_utils.py +1 -0
  16. shinestacker/core/exceptions.py +1 -1
  17. shinestacker/core/framework.py +9 -4
  18. shinestacker/gui/action_config.py +23 -20
  19. shinestacker/gui/action_config_dialog.py +107 -64
  20. shinestacker/gui/gui_images.py +27 -3
  21. shinestacker/gui/gui_run.py +1 -2
  22. shinestacker/gui/img/dark/close-round-line-icon.png +0 -0
  23. shinestacker/gui/img/dark/forward-button-icon.png +0 -0
  24. shinestacker/gui/img/dark/play-button-round-icon.png +0 -0
  25. shinestacker/gui/img/dark/plus-round-line-icon.png +0 -0
  26. shinestacker/gui/img/dark/shinestacker_bkg.png +0 -0
  27. shinestacker/gui/main_window.py +20 -7
  28. shinestacker/gui/menu_manager.py +18 -7
  29. shinestacker/gui/new_project.py +18 -9
  30. shinestacker/gui/project_controller.py +13 -6
  31. shinestacker/gui/project_editor.py +12 -2
  32. shinestacker/gui/project_model.py +4 -4
  33. shinestacker/gui/tab_widget.py +16 -6
  34. shinestacker/retouch/adjustments.py +5 -0
  35. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/METADATA +35 -39
  36. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/RECORD +45 -40
  37. /shinestacker/gui/img/{close-round-line-icon.png → light/close-round-line-icon.png} +0 -0
  38. /shinestacker/gui/img/{forward-button-icon.png → light/forward-button-icon.png} +0 -0
  39. /shinestacker/gui/img/{play-button-round-icon.png → light/play-button-round-icon.png} +0 -0
  40. /shinestacker/gui/img/{plus-round-line-icon.png → light/plus-round-line-icon.png} +0 -0
  41. /shinestacker/gui/{ico → img/light}/shinestacker_bkg.png +0 -0
  42. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/WHEEL +0 -0
  43. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/entry_points.txt +0 -0
  44. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/licenses/LICENSE +0 -0
  45. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/top_level.txt +0 -0
@@ -1,141 +1,321 @@
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 .. gui.config_dialog import ConfigDialog
4
+ from PySide6.QtWidgets import QLabel, QCheckBox, QComboBox, QDoubleSpinBox, QSpinBox
5
5
  from .. config.settings import Settings
6
6
  from .. config.constants import constants
7
7
  from .. config.gui_constants import gui_constants
8
+ from .. gui.config_dialog import ConfigDialog
9
+ from .. gui.action_config import add_tab, create_tab_widget
10
+ from .. gui.action_config_dialog import AlignFramesConfigBase
11
+
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
8
121
 
122
+ def get_value(self):
123
+ return self.widget.itemData(self.widget.currentIndex())
9
124
 
10
- class SettingsDialog(ConfigDialog):
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
+
172
+ class SettingsDialog(ConfigDialog, AlignFramesConfigBase):
11
173
  update_project_config_requested = Signal()
12
174
  update_retouch_config_requested = Signal()
13
175
 
14
176
  def __init__(self, parent=None, project_settings=True, retouch_settings=True):
177
+ AlignFramesConfigBase.__init__(self)
15
178
  self.project_settings = project_settings
16
179
  self.retouch_settings = retouch_settings
17
180
  self.settings = Settings.instance()
18
- self.expert_options = None
19
- self.combined_actions_max_threads = None
20
- self.align_frames_max_threads = None
21
- self.focus_stack_max_threads = None
22
- self.view_strategy = None
23
- self.min_mouse_step_brush_fraction = None
24
- self.paint_refresh_time = None
25
- self.display_refresh_time = None
26
- self.cursor_update_time = None
181
+ self.project_parameters = []
182
+ self.retouch_parameters = []
183
+ self._init_parameters()
27
184
  super().__init__("Settings", parent)
28
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
+
29
252
  def create_form_content(self):
253
+ self.tab_widget = create_tab_widget(self.container_layout)
30
254
  if self.project_settings:
31
- self.create_project_settings()
32
- separator = QFrame()
33
- separator.setFrameShape(QFrame.HLine)
34
- separator.setFrameShadow(QFrame.Sunken)
35
- separator.setLineWidth(1)
36
- self.container_layout.addRow(separator)
255
+ project_tab_layout = add_tab(self.tab_widget, "Project Settings")
256
+ self.create_project_settings(project_tab_layout)
37
257
  if self.retouch_settings:
38
- self.create_retouch_settings()
258
+ retouch_tab_layout = add_tab(self.tab_widget, "Retouch Settings")
259
+ self.create_retouch_settings(retouch_tab_layout)
39
260
 
40
- def create_project_settings(self):
41
- 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:")
42
265
  label.setStyleSheet("font-weight: bold")
43
- self.container_layout.addRow(label)
44
- self.expert_options = QCheckBox()
45
- self.expert_options.setChecked(self.settings.get('expert_options'))
46
- self.container_layout.addRow("Expert options:", self.expert_options)
47
- self.combined_actions_max_threads = QSpinBox()
48
- self.combined_actions_max_threads.setRange(0, 64)
49
- self.combined_actions_max_threads.setValue(
50
- self.settings.get('combined_actions_params')['max_threads'])
51
- self.container_layout.addRow("Max num. of cores, combined actions:",
52
- self.combined_actions_max_threads)
53
-
54
- self.align_frames_max_threads = QSpinBox()
55
- self.align_frames_max_threads.setRange(0, 64)
56
- self.align_frames_max_threads.setValue(
57
- self.settings.get('align_frames_params')['max_threads'])
58
- self.container_layout.addRow("Max num. of cores, align frames:",
59
- self.align_frames_max_threads)
60
-
61
- self.focus_stack_max_threads = QSpinBox()
62
- self.focus_stack_max_threads.setRange(0, 64)
63
- self.focus_stack_max_threads.setValue(
64
- self.settings.get('align_frames_params')['max_threads'])
65
- self.container_layout.addRow("Max num. of cores, focus stacking:",
66
- self.focus_stack_max_threads)
67
-
68
- def create_retouch_settings(self):
69
- label = QLabel("Retouch settings")
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)
271
+ self.info_label = QLabel()
272
+ self.info_label.setStyleSheet("color: orange; font-style: italic;")
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:")
70
279
  label.setStyleSheet("font-weight: bold")
71
- self.container_layout.addRow(label)
72
- self.view_strategy = QComboBox()
73
- self.view_strategy.addItem("Overlaid", "overlaid")
74
- self.view_strategy.addItem("Side by side", "sidebyside")
75
- self.view_strategy.addItem("Top-Bottom", "topbottom")
76
- idx = self.view_strategy.findData(self.settings.get('view_strategy'))
77
- if idx >= 0:
78
- self.view_strategy.setCurrentIndex(idx)
79
- self.container_layout.addRow("View strategy:", self.view_strategy)
80
- self.min_mouse_step_brush_fraction = QDoubleSpinBox()
81
- self.min_mouse_step_brush_fraction.setValue(
82
- self.settings.get('min_mouse_step_brush_fraction'))
83
- self.min_mouse_step_brush_fraction.setRange(0, 1)
84
- self.min_mouse_step_brush_fraction.setDecimals(2)
85
- self.min_mouse_step_brush_fraction.setSingleStep(0.02)
86
- self.container_layout.addRow("Min. mouse step in brush units:",
87
- self.min_mouse_step_brush_fraction)
88
- self.paint_refresh_time = QSpinBox()
89
- self.paint_refresh_time.setRange(0, 1000)
90
- self.paint_refresh_time.setValue(
91
- self.settings.get('paint_refresh_time'))
92
- self.container_layout.addRow("Paint refresh time:",
93
- self.paint_refresh_time)
94
- self.display_refresh_time = QSpinBox()
95
- self.display_refresh_time.setRange(0, 200)
96
- self.display_refresh_time.setValue(
97
- self.settings.get('display_refresh_time'))
98
- self.container_layout.addRow("Display refresh time:",
99
- self.display_refresh_time)
100
-
101
- self.cursor_update_time = QSpinBox()
102
- self.cursor_update_time.setRange(0, 50)
103
- self.cursor_update_time.setValue(
104
- self.settings.get('cursor_update_time'))
105
- self.container_layout.addRow("Cursor refresh time:",
106
- 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)
107
313
 
108
314
  def accept(self):
109
- if self.project_settings:
110
- self.settings.set(
111
- 'expert_options', self.expert_options.isChecked())
112
- self.settings.set(
113
- 'combined_actions_params', {
114
- 'max_threads': self.combined_actions_max_threads.value()
115
- })
116
- self.settings.set(
117
- 'align_frames_params', {
118
- 'max_threads': self.align_frames_max_threads.value()
119
- })
120
- self.settings.set(
121
- 'focus_stack_params', {
122
- 'max_threads': self.focus_stack_max_threads.value()
123
- })
124
- self.settings.set(
125
- 'focus_stack_bunch:params', {
126
- 'max_threads': self.focus_stack_max_threads.value()
127
- })
128
- if self.retouch_settings:
129
- self.settings.set(
130
- 'view_strategy', self.view_strategy.itemData(self.view_strategy.currentIndex()))
131
- self.settings.set(
132
- 'min_mouse_step_brush_fraction', self.min_mouse_step_brush_fraction.value())
133
- self.settings.set(
134
- 'paint_refresh_time', self.paint_refresh_time.value())
135
- self.settings.set(
136
- 'display_refresh_time', self.display_refresh_time.value())
137
- self.settings.set(
138
- '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())
139
319
  self.settings.update()
140
320
  if self.project_settings:
141
321
  self.update_project_config_requested.emit()
@@ -144,27 +324,14 @@ class SettingsDialog(ConfigDialog):
144
324
  super().accept()
145
325
 
146
326
  def reset_to_defaults(self):
147
- if self.project_settings:
148
- self.expert_options.setChecked(constants.DEFAULT_EXPERT_OPTIONS)
149
- self.combined_actions_max_threads.setValue(constants.DEFAULT_MAX_FWK_THREADS)
150
- self.align_frames_max_threads.setValue(constants.DEFAULT_ALIGN_MAX_THREADS)
151
- self.focus_stack_max_threads.setValue(constants.DEFAULT_PY_MAX_THREADS)
152
- if self.retouch_settings:
153
- idx = self.view_strategy.findData(constants.DEFAULT_VIEW_STRATEGY)
154
- if idx >= 0:
155
- self.view_strategy.setCurrentIndex(idx)
156
- self.min_mouse_step_brush_fraction.setValue(
157
- gui_constants.DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION)
158
- self.paint_refresh_time.setValue(
159
- gui_constants.DEFAULT_PAINT_REFRESH_TIME)
160
- self.display_refresh_time.setValue(
161
- gui_constants.DEFAULT_DISPLAY_REFRESH_TIME)
162
- self.cursor_update_time.setValue(
163
- gui_constants.DEFAULT_CURSOR_UPDATE_TIME)
164
-
165
-
166
- def show_settings_dialog(parent, project_settings, retouch_settings,
167
- 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):
168
335
  dialog = SettingsDialog(parent, project_settings, retouch_settings)
169
336
  dialog.update_project_config_requested.connect(handle_project_config)
170
337
  dialog.update_retouch_config_requested.connect(handle_retouch_config)
@@ -89,9 +89,12 @@ class _Constants:
89
89
  DEFAULT_MULTILAYER_FILE_REVERSE_ORDER = True
90
90
  MULTILAYER_WARNING_MEM_GB = 1
91
91
 
92
+ DEFAULT_COMBINED_ACTIONS_STEP_PROCESS = True
93
+
92
94
  DEFAULT_PLOTS_PATH = 'plots'
93
- DEFAULT_MAX_FWK_THREADS = 8
94
- DEFAULT_MAX_FWK_CHUNK_SUBMIT = True
95
+ DEFAULT_FWK_MEMORY_LIMIT_GB = 8
96
+ DEFAULT_FWK_MAX_THREADS = 8
97
+ DEFAULT_FWK_CHUNK_SUBMIT = True
95
98
 
96
99
  FIELD_SUBSAMPLE_VALUES_1 = [2, 3, 4, 6, 8, 12, 16, 24, 32]
97
100
  FIELD_SUBSAMPLE_OPTIONS_1 = [f"1/{n} × 1/{n}" for n in FIELD_SUBSAMPLE_VALUES_1]
@@ -99,7 +102,7 @@ class _Constants:
99
102
  FIELD_SUBSAMPLE_OPTIONS = ['Auto', 'Full resolution'] + FIELD_SUBSAMPLE_OPTIONS_1
100
103
  FIELD_SUBSAMPLE_DEFAULT = FIELD_SUBSAMPLE_VALUES[0]
101
104
 
102
- DEFAULT_NOISE_MAP_FILENAME = "noise-map/hot_pixels.png"
105
+ DEFAULT_NOISE_MAP_FILENAME = "hot_pixels.png"
103
106
  DEFAULT_NOISE_MAX_FRAMES = 10
104
107
  DEFAULT_MN_KERNEL_SIZE = 3
105
108
  INTERPOLATE_MEAN = 'MEAN'
@@ -135,7 +138,7 @@ class _Constants:
135
138
  VALID_MATCHING_METHODS = [MATCHING_KNN, MATCHING_NORM_HAMMING]
136
139
  VALID_TRANSFORMS = [ALIGN_RIGID, ALIGN_HOMOGRAPHY]
137
140
  VALID_BORDER_MODES = [BORDER_CONSTANT, BORDER_REPLICATE, BORDER_REPLICATE_BLUR]
138
- VALID_ALIGN_METHODS = [ALIGN_RANSAC, ALIGN_LMEDS]
141
+ VALID_ESTIMATION_METHODS = [ALIGN_RANSAC, ALIGN_LMEDS]
139
142
  NOKNN_METHODS = {'detectors': [DETECTOR_ORB, DETECTOR_SURF, DETECTOR_AKAZE, DETECTOR_BRISK],
140
143
  'descriptors': [DESCRIPTOR_ORB, DESCRIPTOR_AKAZE, DESCRIPTOR_BRISK]}
141
144
 
@@ -148,7 +151,7 @@ class _Constants:
148
151
  DEFAULT_ALIGN_THRESHOLD = 0.75
149
152
  DEFAULT_TRANSFORM = ALIGN_RIGID
150
153
  DEFAULT_BORDER_MODE = BORDER_REPLICATE_BLUR
151
- DEFAULT_ALIGN_METHOD = 'RANSAC'
154
+ DEFAULT_ESTIMATION_METHOD = 'RANSAC'
152
155
  DEFAULT_RANS_THRESHOLD = 3.0 # px
153
156
  DEFAULT_REFINE_ITERS = 100
154
157
  DEFAULT_ALIGN_CONFIDENCE = 99.9
@@ -160,12 +163,14 @@ class _Constants:
160
163
  DEFAULT_ALIGN_RES_TARGET_MPX = 2
161
164
  DEFAULT_ALIGN_FAST_SUBSAMPLING = False
162
165
  DEFAULT_ALIGN_MIN_GOOD_MATCHES = 20
166
+ DEFAULT_PHASE_CORR_FALLBACK = False
163
167
  ALIGN_VALID_MODES = ['auto', 'sequential', 'parallel']
164
168
  DEFAULT_ALIGN_MODE = 'auto'
165
169
  DEFAULT_ALIGN_MEMORY_LIMIT_GB = 8
166
170
  DEFAULT_ALIGN_MAX_THREADS = min(os.cpu_count() or 4, 8)
167
171
  DEFAULT_ALIGN_CHUNK_SUBMIT = True
168
172
  DEFAULT_ALIGN_BW_MATCHING = False
173
+ DEFAULT_ALIGN_DELTA_MAX = 2
169
174
 
170
175
  BALANCE_LINEAR = "LINEAR"
171
176
  BALANCE_GAMMA = "GAMMA"
@@ -39,20 +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
- 'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS
45
+ 'memory_limit': constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
46
+ 'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS,
47
+ 'detector': constants.DEFAULT_DETECTOR,
48
+ 'descriptor': constants.DEFAULT_DESCRIPTOR,
49
+ 'match_method': constants.DEFAULT_MATCHING_METHOD
46
50
  },
47
51
  'focus_stack_params': {
52
+ 'memory_limit': constants.DEFAULT_PY_MEMORY_LIMIT_GB,
48
53
  'max_threads': constants.DEFAULT_PY_MAX_THREADS
49
54
  },
50
55
  'focus_stack_bunch_params': {
56
+ 'memory_limit': constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
51
57
  'max_threads': constants.DEFAULT_PY_MAX_THREADS
52
58
  }
53
59
  }
54
60
 
55
- CURRENT_VERSION = 1
61
+ CURRENT_SETTINGS_FILE_VERSION = 1
56
62
 
57
63
 
58
64
  class Settings(StdPathFile):
@@ -63,18 +69,30 @@ class Settings(StdPathFile):
63
69
  if Settings._instance is not None:
64
70
  raise RuntimeError("Settings is a singleton.")
65
71
  super().__init__(filename)
66
- self.settings = DEFAULT_SETTINGS
72
+ self.settings = self._deep_copy_defaults()
67
73
  file_path = self.get_file_path()
68
74
  if os.path.isfile(file_path):
69
75
  try:
70
76
  with open(file_path, 'r', encoding="utf-8") as file:
71
77
  json_data = json.load(file)
72
- settings = json_data['settings']
78
+ file_settings = json_data['settings']
79
+ self._deep_merge_settings(file_settings)
73
80
  except Exception as e:
74
81
  traceback.print_tb(e.__traceback__)
75
82
  print(f"Can't read file from path {file_path}. Default settings ignored.")
76
- settings = {}
77
- 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
78
96
 
79
97
  @classmethod
80
98
  def instance(cls, filename="shinestacker-settings.txt"):
@@ -96,7 +114,10 @@ class Settings(StdPathFile):
96
114
  try:
97
115
  config_dir = self.get_config_dir()
98
116
  os.makedirs(config_dir, exist_ok=True)
99
- json_data = {'version': CURRENT_VERSION, 'settings': self.settings}
117
+ json_data = {
118
+ 'version': CURRENT_SETTINGS_FILE_VERSION,
119
+ 'settings': self.settings
120
+ }
100
121
  json_obj = jsonpickle.encode(json_data)
101
122
  with open(self.get_file_path(), 'w', encoding="utf-8") as f:
102
123
  f.write(json_obj)
@@ -52,4 +52,5 @@ def setup_matplotlib_mode():
52
52
  __IPYTHON__ # noqa
53
53
  except Exception:
54
54
  matplotlib.use('agg')
55
+ matplotlib.pyplot.set_loglevel("warning")
55
56
  matplotlib.rcParams['pdf.fonttype'] = 42
@@ -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):
@@ -146,6 +146,9 @@ class TaskBase:
146
146
  def sub_message_r(self, msg='', level=logging.INFO):
147
147
  self.sub_message(msg, level, self.end_r, self.begin_r, False)
148
148
 
149
+ def end_job(self):
150
+ pass
151
+
149
152
 
150
153
  class Job(TaskBase):
151
154
  def __init__(self, name, logger_name=None, log_file='', callbacks=None, **kwargs):
@@ -188,12 +191,14 @@ class Job(TaskBase):
188
191
  self.id, self.name) is False:
189
192
  raise RunStopException(self.name)
190
193
  a.run()
194
+ for a in self.__actions:
195
+ a.end_job()
191
196
 
192
197
 
193
198
  class SequentialTask(TaskBase):
194
199
  def __init__(self, name, enabled=True, **kwargs):
195
- self.max_threads = kwargs.pop('max_threads', constants.DEFAULT_MAX_FWK_THREADS)
196
- 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)
197
202
  TaskBase.__init__(self, name, enabled, **kwargs)
198
203
  self.total_action_counts = None
199
204
  self.current_action_count = None
@@ -261,11 +266,11 @@ class SequentialTask(TaskBase):
261
266
  try:
262
267
  result = future.result()
263
268
  if result:
264
- self.print_message(color_str(
269
+ self.print_message_r(color_str(
265
270
  f"completed processing step: {self.idx_tot_str(idx)}",
266
271
  constants.LOG_COLOR_LEVEL_1))
267
272
  else:
268
- self.print_message(color_str(
273
+ self.print_message_r(color_str(
269
274
  f"failed processing step: {self.idx_tot_str(idx)}",
270
275
  constants.LOG_COLOR_WARNING))
271
276
  self.current_action_count += 1