shinestacker 1.8.0__py3-none-any.whl → 1.9.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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +184 -80
- shinestacker/algorithms/align_auto.py +13 -11
- shinestacker/algorithms/align_parallel.py +41 -16
- shinestacker/algorithms/base_stack_algo.py +1 -1
- shinestacker/algorithms/exif.py +252 -6
- shinestacker/algorithms/multilayer.py +6 -4
- shinestacker/algorithms/noise_detection.py +10 -8
- shinestacker/algorithms/pyramid_tiles.py +1 -1
- shinestacker/algorithms/stack.py +25 -13
- shinestacker/algorithms/stack_framework.py +16 -11
- shinestacker/algorithms/utils.py +18 -2
- shinestacker/algorithms/vignetting.py +16 -3
- shinestacker/app/settings_dialog.py +297 -173
- shinestacker/config/constants.py +10 -6
- shinestacker/config/settings.py +25 -7
- shinestacker/core/exceptions.py +1 -1
- shinestacker/core/framework.py +2 -2
- shinestacker/gui/action_config.py +23 -20
- shinestacker/gui/action_config_dialog.py +38 -21
- shinestacker/gui/folder_file_selection.py +3 -2
- shinestacker/gui/gui_images.py +27 -3
- shinestacker/gui/gui_run.py +2 -2
- shinestacker/gui/new_project.py +23 -12
- shinestacker/gui/project_controller.py +13 -6
- shinestacker/gui/project_editor.py +12 -2
- shinestacker/gui/project_model.py +4 -4
- shinestacker/retouch/exif_data.py +3 -0
- shinestacker/retouch/file_loader.py +3 -3
- shinestacker/retouch/io_gui_handler.py +4 -4
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/METADATA +37 -39
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/RECORD +36 -36
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,174 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611,
|
|
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
|
|
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.
|
|
21
|
-
self.
|
|
22
|
-
self.
|
|
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.
|
|
37
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
self.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
103
|
-
self.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
self.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
self.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
141
|
-
self.
|
|
142
|
-
|
|
143
|
-
self.
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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)
|
shinestacker/config/constants.py
CHANGED
|
@@ -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
|
-
|
|
94
|
-
|
|
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 = "
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
shinestacker/config/settings.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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 = {
|
|
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)
|
shinestacker/core/exceptions.py
CHANGED
|
@@ -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
|
|
33
|
+
super().__init__(f"Alignment failed for frame {index}: {details}")
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class BitDepthError(FocusStackError):
|
shinestacker/core/framework.py
CHANGED
|
@@ -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.
|
|
201
|
-
self.chunk_submit = kwargs.pop('chunk_submit', constants.
|
|
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
|