shinestacker 0.2.0.post1.dev1__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/__init__.py +3 -0
- shinestacker/_version.py +1 -0
- shinestacker/algorithms/__init__.py +14 -0
- shinestacker/algorithms/align.py +307 -0
- shinestacker/algorithms/balance.py +367 -0
- shinestacker/algorithms/core_utils.py +22 -0
- shinestacker/algorithms/depth_map.py +164 -0
- shinestacker/algorithms/exif.py +238 -0
- shinestacker/algorithms/multilayer.py +187 -0
- shinestacker/algorithms/noise_detection.py +182 -0
- shinestacker/algorithms/pyramid.py +176 -0
- shinestacker/algorithms/stack.py +112 -0
- shinestacker/algorithms/stack_framework.py +248 -0
- shinestacker/algorithms/utils.py +71 -0
- shinestacker/algorithms/vignetting.py +137 -0
- shinestacker/app/__init__.py +0 -0
- shinestacker/app/about_dialog.py +24 -0
- shinestacker/app/app_config.py +39 -0
- shinestacker/app/gui_utils.py +35 -0
- shinestacker/app/help_menu.py +16 -0
- shinestacker/app/main.py +176 -0
- shinestacker/app/open_frames.py +39 -0
- shinestacker/app/project.py +91 -0
- shinestacker/app/retouch.py +82 -0
- shinestacker/config/__init__.py +4 -0
- shinestacker/config/config.py +53 -0
- shinestacker/config/constants.py +174 -0
- shinestacker/config/gui_constants.py +85 -0
- shinestacker/core/__init__.py +5 -0
- shinestacker/core/colors.py +60 -0
- shinestacker/core/core_utils.py +52 -0
- shinestacker/core/exceptions.py +50 -0
- shinestacker/core/framework.py +210 -0
- shinestacker/core/logging.py +89 -0
- shinestacker/gui/__init__.py +0 -0
- shinestacker/gui/action_config.py +879 -0
- shinestacker/gui/actions_window.py +283 -0
- shinestacker/gui/colors.py +57 -0
- shinestacker/gui/gui_images.py +152 -0
- shinestacker/gui/gui_logging.py +213 -0
- shinestacker/gui/gui_run.py +393 -0
- shinestacker/gui/img/close-round-line-icon.png +0 -0
- shinestacker/gui/img/forward-button-icon.png +0 -0
- shinestacker/gui/img/play-button-round-icon.png +0 -0
- shinestacker/gui/img/plus-round-line-icon.png +0 -0
- shinestacker/gui/main_window.py +599 -0
- shinestacker/gui/new_project.py +170 -0
- shinestacker/gui/project_converter.py +148 -0
- shinestacker/gui/project_editor.py +539 -0
- shinestacker/gui/project_model.py +138 -0
- shinestacker/retouch/__init__.py +0 -0
- shinestacker/retouch/brush.py +9 -0
- shinestacker/retouch/brush_controller.py +57 -0
- shinestacker/retouch/brush_preview.py +126 -0
- shinestacker/retouch/exif_data.py +65 -0
- shinestacker/retouch/file_loader.py +104 -0
- shinestacker/retouch/image_editor.py +651 -0
- shinestacker/retouch/image_editor_ui.py +380 -0
- shinestacker/retouch/image_viewer.py +356 -0
- shinestacker/retouch/shortcuts_help.py +98 -0
- shinestacker/retouch/undo_manager.py +38 -0
- shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
- shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
- shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
- shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
- shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
- shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, Any
|
|
3
|
+
import os.path
|
|
4
|
+
from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QFileDialog, QLabel, QComboBox,
|
|
5
|
+
QMessageBox, QSizePolicy, QStackedWidget, QDialog, QFormLayout,
|
|
6
|
+
QLineEdit, QSpinBox, QDoubleSpinBox, QCheckBox, QTreeView, QAbstractItemView, QListView)
|
|
7
|
+
from PySide6.QtCore import Qt, QTimer
|
|
8
|
+
from .. config.constants import constants
|
|
9
|
+
from .project_model import ActionConfig
|
|
10
|
+
from .. algorithms.align import validate_align_config
|
|
11
|
+
|
|
12
|
+
FIELD_TEXT = 'text'
|
|
13
|
+
FIELD_ABS_PATH = 'abs_path'
|
|
14
|
+
FIELD_REL_PATH = 'rel_path'
|
|
15
|
+
FIELD_FLOAT = 'float'
|
|
16
|
+
FIELD_INT = 'int'
|
|
17
|
+
FIELD_INT_TUPLE = 'int_tuple'
|
|
18
|
+
FIELD_BOOL = 'bool'
|
|
19
|
+
FIELD_COMBO = 'combo'
|
|
20
|
+
FIELD_TYPES = [FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
|
|
21
|
+
FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ActionConfigurator(ABC):
|
|
25
|
+
def __init__(self, expert=False):
|
|
26
|
+
self.expert = expert
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def create_form(self, layout: QFormLayout, params: Dict[str, Any]):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def update_params(self, params: Dict[str, Any]):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FieldBuilder:
|
|
38
|
+
def __init__(self, layout, action):
|
|
39
|
+
self.layout = layout
|
|
40
|
+
self.action = action
|
|
41
|
+
self.fields = {}
|
|
42
|
+
|
|
43
|
+
def add_field(self, tag: str, field_type: str, label: str,
|
|
44
|
+
required: bool = False, add_to_layout=None, **kwargs):
|
|
45
|
+
if field_type == FIELD_TEXT:
|
|
46
|
+
widget = self.create_text_field(tag, **kwargs)
|
|
47
|
+
elif field_type == FIELD_ABS_PATH:
|
|
48
|
+
widget = self.create_abs_path_field(tag, **kwargs)
|
|
49
|
+
elif field_type == FIELD_REL_PATH:
|
|
50
|
+
widget = self.create_rel_path_field(tag, **kwargs)
|
|
51
|
+
elif field_type == FIELD_FLOAT:
|
|
52
|
+
widget = self.create_float_field(tag, **kwargs)
|
|
53
|
+
elif field_type == FIELD_INT:
|
|
54
|
+
widget = self.create_int_field(tag, **kwargs)
|
|
55
|
+
elif field_type == FIELD_INT_TUPLE:
|
|
56
|
+
widget = self.create_int_tuple_field(tag, **kwargs)
|
|
57
|
+
elif field_type == FIELD_BOOL:
|
|
58
|
+
widget = self.create_bool_field(tag, **kwargs)
|
|
59
|
+
elif field_type == FIELD_COMBO:
|
|
60
|
+
widget = self.create_combo_field(tag, **kwargs)
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(f"Unknown field type: {field_type}")
|
|
63
|
+
if 'default' in kwargs:
|
|
64
|
+
default_value = kwargs['default']
|
|
65
|
+
else:
|
|
66
|
+
if field_type == FIELD_TEXT:
|
|
67
|
+
default_value = kwargs.get('placeholder', '')
|
|
68
|
+
elif field_type in (FIELD_ABS_PATH, FIELD_REL_PATH):
|
|
69
|
+
default_value = ''
|
|
70
|
+
elif field_type == FIELD_FLOAT:
|
|
71
|
+
default_value = kwargs.get('default', 0.0)
|
|
72
|
+
elif field_type == FIELD_INT:
|
|
73
|
+
default_value = kwargs.get('default', 0)
|
|
74
|
+
elif field_type == FIELD_INT_TUPLE:
|
|
75
|
+
default_value = kwargs.get('default', [0] * kwargs.get('size', 1))
|
|
76
|
+
elif field_type == FIELD_BOOL:
|
|
77
|
+
default_value = kwargs.get('default', False)
|
|
78
|
+
elif field_type == FIELD_COMBO:
|
|
79
|
+
default_value = kwargs.get('default', kwargs.get('options', [''])[0] if 'options' in kwargs else '')
|
|
80
|
+
self.fields[tag] = {
|
|
81
|
+
'widget': widget,
|
|
82
|
+
'type': field_type,
|
|
83
|
+
'label': label,
|
|
84
|
+
'required': required,
|
|
85
|
+
'default_value': default_value,
|
|
86
|
+
**kwargs
|
|
87
|
+
}
|
|
88
|
+
if add_to_layout is None:
|
|
89
|
+
add_to_layout = self.layout
|
|
90
|
+
add_to_layout.addRow(f"{label}:", widget)
|
|
91
|
+
return widget
|
|
92
|
+
|
|
93
|
+
def reset_to_defaults(self):
|
|
94
|
+
for tag, field in self.fields.items():
|
|
95
|
+
if tag not in ['name', 'working_path', 'input_path', 'output_path', 'exif_path', 'plot_path']:
|
|
96
|
+
default = field['default_value']
|
|
97
|
+
widget = field['widget']
|
|
98
|
+
if field['type'] == FIELD_TEXT:
|
|
99
|
+
widget.setText(default)
|
|
100
|
+
elif field['type'] in (FIELD_ABS_PATH, FIELD_REL_PATH):
|
|
101
|
+
self.get_path_widget(widget).setText(default)
|
|
102
|
+
elif field['type'] == FIELD_FLOAT:
|
|
103
|
+
widget.setValue(default)
|
|
104
|
+
elif field['type'] == FIELD_BOOL:
|
|
105
|
+
widget.setChecked(default)
|
|
106
|
+
elif field['type'] == FIELD_INT:
|
|
107
|
+
widget.setValue(default)
|
|
108
|
+
elif field['type'] == FIELD_INT_TUPLE:
|
|
109
|
+
for i in range(field['size']):
|
|
110
|
+
spinbox = widget.layout().itemAt(1 + i * 2).widget()
|
|
111
|
+
spinbox.setValue(default[i])
|
|
112
|
+
elif field['type'] == FIELD_COMBO:
|
|
113
|
+
widget.setCurrentText(default)
|
|
114
|
+
|
|
115
|
+
def get_path_widget(self, widget):
|
|
116
|
+
return widget.layout().itemAt(0).widget()
|
|
117
|
+
|
|
118
|
+
def get_working_path(self):
|
|
119
|
+
if 'working_path' in self.fields.keys():
|
|
120
|
+
working_path = self.get_path_widget(self.fields['working_path']['widget']).text()
|
|
121
|
+
if working_path != '':
|
|
122
|
+
return working_path
|
|
123
|
+
parent = self.action.parent
|
|
124
|
+
while parent is not None:
|
|
125
|
+
if 'working_path' in parent.params.keys() and parent.params['working_path'] != '':
|
|
126
|
+
return parent.params['working_path']
|
|
127
|
+
parent = parent.parent
|
|
128
|
+
else:
|
|
129
|
+
return ''
|
|
130
|
+
|
|
131
|
+
def update_params(self, params: Dict[str, Any]) -> bool:
|
|
132
|
+
for tag, field in self.fields.items():
|
|
133
|
+
if field['type'] == FIELD_TEXT:
|
|
134
|
+
params[tag] = field['widget'].text()
|
|
135
|
+
elif field['type'] in (FIELD_ABS_PATH, FIELD_REL_PATH):
|
|
136
|
+
params[tag] = self.get_path_widget(field['widget']).text()
|
|
137
|
+
elif field['type'] == FIELD_FLOAT:
|
|
138
|
+
params[tag] = field['widget'].value()
|
|
139
|
+
elif field['type'] == FIELD_BOOL:
|
|
140
|
+
params[tag] = field['widget'].isChecked()
|
|
141
|
+
elif field['type'] == FIELD_INT:
|
|
142
|
+
params[tag] = field['widget'].value()
|
|
143
|
+
elif field['type'] == FIELD_INT_TUPLE:
|
|
144
|
+
params[tag] = [field['widget'].layout().itemAt(1 + i * 2).widget().value() for i in range(field['size'])]
|
|
145
|
+
elif field['type'] == FIELD_COMBO:
|
|
146
|
+
values = field.get('values', None)
|
|
147
|
+
options = field.get('options', None)
|
|
148
|
+
text = field['widget'].currentText()
|
|
149
|
+
if values is not None and options is not None:
|
|
150
|
+
text = {k: v for k, v in zip(options, values)}[text]
|
|
151
|
+
params[tag] = text
|
|
152
|
+
if field['required'] and not params[tag]:
|
|
153
|
+
required = True
|
|
154
|
+
if tag == 'working_path' and self.get_working_path() != '':
|
|
155
|
+
required = False
|
|
156
|
+
if required:
|
|
157
|
+
QMessageBox.warning(None, "Error", f"{field['label']} is required")
|
|
158
|
+
return False
|
|
159
|
+
if field['type'] == FIELD_REL_PATH and 'working_path' in params:
|
|
160
|
+
try:
|
|
161
|
+
working_path = self.get_working_path()
|
|
162
|
+
abs_path = os.path.normpath(os.path.join(working_path, params[tag]))
|
|
163
|
+
if not abs_path.startswith(os.path.normpath(working_path)):
|
|
164
|
+
QMessageBox.warning(None, "Invalid Path",
|
|
165
|
+
f"{field['label']} must be a subdirectory of working path")
|
|
166
|
+
return False
|
|
167
|
+
if field.get('must_exist', False):
|
|
168
|
+
paths = [abs_path]
|
|
169
|
+
if field.get('multiple_entries', False):
|
|
170
|
+
paths = abs_path.split(constants.PATH_SEPARATOR)
|
|
171
|
+
for p in paths:
|
|
172
|
+
p = p.strip()
|
|
173
|
+
if not os.path.exists(p):
|
|
174
|
+
QMessageBox.warning(None, "Invalid Path",
|
|
175
|
+
f"{field['label']} {p} does not exist")
|
|
176
|
+
return False
|
|
177
|
+
except Exception as e:
|
|
178
|
+
QMessageBox.warning(None, "Error", f"Invalid path: {str(e)}")
|
|
179
|
+
return False
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
def create_text_field(self, tag, **kwargs):
|
|
183
|
+
value = self.action.params.get(tag, '')
|
|
184
|
+
edit = QLineEdit(value)
|
|
185
|
+
edit.setPlaceholderText(kwargs.get('placeholder', ''))
|
|
186
|
+
return edit
|
|
187
|
+
|
|
188
|
+
def create_abs_path_field(self, tag, **kwargs):
|
|
189
|
+
value = self.action.params.get(tag, '')
|
|
190
|
+
edit = QLineEdit(value)
|
|
191
|
+
edit.setPlaceholderText(kwargs.get('placeholder', ''))
|
|
192
|
+
button = QPushButton("Browse...")
|
|
193
|
+
|
|
194
|
+
def browse():
|
|
195
|
+
path = QFileDialog.getExistingDirectory(None, f"Select {tag.replace('_', ' ')}")
|
|
196
|
+
if path:
|
|
197
|
+
edit.setText(path)
|
|
198
|
+
button.clicked.connect(browse)
|
|
199
|
+
button.setAutoDefault(False)
|
|
200
|
+
layout = QHBoxLayout()
|
|
201
|
+
layout.addWidget(edit)
|
|
202
|
+
layout.addWidget(button)
|
|
203
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
204
|
+
container = QWidget()
|
|
205
|
+
container.setLayout(layout)
|
|
206
|
+
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
207
|
+
return container
|
|
208
|
+
|
|
209
|
+
def create_rel_path_field(self, tag, **kwargs):
|
|
210
|
+
value = self.action.params.get(tag, kwargs.get('default', ''))
|
|
211
|
+
edit = QLineEdit(value)
|
|
212
|
+
edit.setPlaceholderText(kwargs.get('placeholder', ''))
|
|
213
|
+
button = QPushButton("Browse...")
|
|
214
|
+
path_type = kwargs.get('path_type', 'directory')
|
|
215
|
+
label = kwargs.get('label', tag).replace('_', ' ')
|
|
216
|
+
|
|
217
|
+
if kwargs.get('multiple_entries', False):
|
|
218
|
+
def browse():
|
|
219
|
+
working_path = self.get_working_path()
|
|
220
|
+
if not working_path:
|
|
221
|
+
QMessageBox.warning(None, "Error", "Please set working path first")
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
if path_type == 'directory':
|
|
225
|
+
dialog = QFileDialog()
|
|
226
|
+
dialog.setWindowTitle(f"Select {label} (multiple selection allowed)")
|
|
227
|
+
dialog.setDirectory(working_path)
|
|
228
|
+
|
|
229
|
+
# Configurazione per la selezione di directory multiple
|
|
230
|
+
dialog.setFileMode(QFileDialog.Directory)
|
|
231
|
+
dialog.setOption(QFileDialog.DontUseNativeDialog, True)
|
|
232
|
+
dialog.setOption(QFileDialog.ShowDirsOnly, True)
|
|
233
|
+
|
|
234
|
+
# Abilita esplicitamente la selezione multipla
|
|
235
|
+
if hasattr(dialog, 'setSupportedSchemes'):
|
|
236
|
+
dialog.setSupportedSchemes(['file'])
|
|
237
|
+
|
|
238
|
+
# Modifica la vista per permettere selezione multipla
|
|
239
|
+
tree_view = dialog.findChild(QTreeView)
|
|
240
|
+
if tree_view:
|
|
241
|
+
tree_view.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
242
|
+
|
|
243
|
+
list_view = dialog.findChild(QListView)
|
|
244
|
+
if list_view:
|
|
245
|
+
list_view.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
246
|
+
|
|
247
|
+
if dialog.exec_():
|
|
248
|
+
paths = dialog.selectedFiles()
|
|
249
|
+
rel_paths = []
|
|
250
|
+
for path in paths:
|
|
251
|
+
try:
|
|
252
|
+
rel_path = os.path.relpath(path, working_path)
|
|
253
|
+
if rel_path.startswith('..'):
|
|
254
|
+
QMessageBox.warning(None, "Invalid Path",
|
|
255
|
+
f"{label} must be a subdirectory of working path")
|
|
256
|
+
return
|
|
257
|
+
rel_paths.append(rel_path)
|
|
258
|
+
except ValueError:
|
|
259
|
+
QMessageBox.warning(None, "Error", "Could not compute relative path")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
if rel_paths:
|
|
263
|
+
edit.setText(constants.PATH_SEPARATOR.join(rel_paths))
|
|
264
|
+
|
|
265
|
+
elif path_type == 'file':
|
|
266
|
+
paths, _ = QFileDialog.getOpenFileNames(None, f"Select {label}", working_path)
|
|
267
|
+
if paths:
|
|
268
|
+
rel_paths = []
|
|
269
|
+
for path in paths:
|
|
270
|
+
try:
|
|
271
|
+
rel_path = os.path.relpath(path, working_path)
|
|
272
|
+
if rel_path.startswith('..'):
|
|
273
|
+
QMessageBox.warning(None, "Invalid Path",
|
|
274
|
+
f"{label} must be within working path")
|
|
275
|
+
return
|
|
276
|
+
rel_paths.append(rel_path)
|
|
277
|
+
except ValueError:
|
|
278
|
+
QMessageBox.warning(None, "Error", "Could not compute relative path")
|
|
279
|
+
return
|
|
280
|
+
edit.setText(constants.PATH_SEPARATOR.join(rel_paths))
|
|
281
|
+
else:
|
|
282
|
+
raise ValueError("path_type must be 'directory' (default) or 'file'.")
|
|
283
|
+
else:
|
|
284
|
+
def browse():
|
|
285
|
+
working_path = self.get_working_path()
|
|
286
|
+
if not working_path:
|
|
287
|
+
QMessageBox.warning(None, "Error", "Please set working path first")
|
|
288
|
+
return
|
|
289
|
+
if path_type == 'directory':
|
|
290
|
+
dialog = QFileDialog()
|
|
291
|
+
dialog.setDirectory(working_path)
|
|
292
|
+
path = dialog.getExistingDirectory(None, f"Select {label}", working_path)
|
|
293
|
+
elif path_type == 'file':
|
|
294
|
+
dialog = QFileDialog()
|
|
295
|
+
dialog.setDirectory(working_path)
|
|
296
|
+
path = dialog.getOpenFileName(None, f"Select {label}", working_path)[0]
|
|
297
|
+
else:
|
|
298
|
+
raise ValueError("path_type must be 'directory' (default) or 'file'.")
|
|
299
|
+
if path:
|
|
300
|
+
try:
|
|
301
|
+
rel_path = os.path.relpath(path, working_path)
|
|
302
|
+
if rel_path.startswith('..'):
|
|
303
|
+
QMessageBox.warning(None, "Invalid Path",
|
|
304
|
+
f"{label} must be a subdirectory of working path")
|
|
305
|
+
return
|
|
306
|
+
edit.setText(rel_path)
|
|
307
|
+
except ValueError:
|
|
308
|
+
QMessageBox.warning(None, "Error", "Could not compute relative path")
|
|
309
|
+
|
|
310
|
+
button.clicked.connect(browse)
|
|
311
|
+
button.setAutoDefault(False)
|
|
312
|
+
layout = QHBoxLayout()
|
|
313
|
+
layout.addWidget(edit)
|
|
314
|
+
layout.addWidget(button)
|
|
315
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
316
|
+
container = QWidget()
|
|
317
|
+
container.setLayout(layout)
|
|
318
|
+
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
319
|
+
return container
|
|
320
|
+
|
|
321
|
+
def create_float_field(self, tag, default=0.0, min=0.0, max=1.0, step=0.1, decimals=2, **kwargs):
|
|
322
|
+
spin = QDoubleSpinBox()
|
|
323
|
+
spin.setValue(self.action.params.get(tag, default))
|
|
324
|
+
spin.setRange(min, max)
|
|
325
|
+
spin.setDecimals(decimals)
|
|
326
|
+
spin.setSingleStep(step)
|
|
327
|
+
return spin
|
|
328
|
+
|
|
329
|
+
def create_int_field(self, tag, default=0, min=0, max=100, **kwargs):
|
|
330
|
+
spin = QSpinBox()
|
|
331
|
+
spin.setRange(min, max)
|
|
332
|
+
spin.setValue(self.action.params.get(tag, default))
|
|
333
|
+
return spin
|
|
334
|
+
|
|
335
|
+
def create_int_tuple_field(self, tag, size=1, default=[0] * 100, min=[0] * 100, max=[100] * 100, **kwargs):
|
|
336
|
+
layout = QHBoxLayout()
|
|
337
|
+
spins = [QSpinBox() for i in range(size)]
|
|
338
|
+
labels = kwargs.get('labels', ('') * size)
|
|
339
|
+
value = self.action.params.get(tag, default)
|
|
340
|
+
for i, spin in enumerate(spins):
|
|
341
|
+
spin.setRange(min[i], max[i])
|
|
342
|
+
spin.setValue(value[i])
|
|
343
|
+
spin.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
344
|
+
label = QLabel(labels[i] + ":")
|
|
345
|
+
label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
|
346
|
+
layout.addWidget(label)
|
|
347
|
+
layout.addWidget(spin)
|
|
348
|
+
layout.setStretch(layout.count() - 1, 1)
|
|
349
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
350
|
+
container = QWidget()
|
|
351
|
+
container.setLayout(layout)
|
|
352
|
+
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
353
|
+
return container
|
|
354
|
+
|
|
355
|
+
def create_combo_field(self, tag, options=None, default=None, **kwargs):
|
|
356
|
+
options = options or []
|
|
357
|
+
values = kwargs.get('values', None)
|
|
358
|
+
combo = QComboBox()
|
|
359
|
+
combo.addItems(options)
|
|
360
|
+
value = self.action.params.get(tag, default or options[0] if options else '')
|
|
361
|
+
if values is not None and len(options) > 0:
|
|
362
|
+
value = {k: v for k, v in zip(values, options)}.get(value, value)
|
|
363
|
+
combo.setCurrentText(value)
|
|
364
|
+
return combo
|
|
365
|
+
|
|
366
|
+
def create_bool_field(self, tag, default=False, **kwargs):
|
|
367
|
+
checkbox = QCheckBox()
|
|
368
|
+
checkbox.setChecked(self.action.params.get(tag, default))
|
|
369
|
+
return checkbox
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class ActionConfigDialog(QDialog):
|
|
373
|
+
def __init__(self, action: ActionConfig, parent=None):
|
|
374
|
+
super().__init__(parent)
|
|
375
|
+
self.action = action
|
|
376
|
+
self.setWindowTitle(f"Configure {action.type_name}")
|
|
377
|
+
self.resize(500, self.height())
|
|
378
|
+
self.configurator = self.get_configurator(action.type_name)
|
|
379
|
+
self.layout = QFormLayout(self)
|
|
380
|
+
self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
381
|
+
self.layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
|
382
|
+
self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
383
|
+
self.layout.setLabelAlignment(Qt.AlignLeft)
|
|
384
|
+
self.configurator.create_form(self.layout, action)
|
|
385
|
+
button_box = QHBoxLayout()
|
|
386
|
+
ok_button = QPushButton("OK")
|
|
387
|
+
ok_button.setFocus()
|
|
388
|
+
cancel_button = QPushButton("Cancel")
|
|
389
|
+
reset_button = QPushButton("Reset")
|
|
390
|
+
button_box.addWidget(ok_button)
|
|
391
|
+
button_box.addWidget(cancel_button)
|
|
392
|
+
button_box.addWidget(reset_button)
|
|
393
|
+
reset_button.clicked.connect(self.reset_to_defaults)
|
|
394
|
+
self.layout.addRow(button_box)
|
|
395
|
+
ok_button.clicked.connect(self.accept)
|
|
396
|
+
cancel_button.clicked.connect(self.reject)
|
|
397
|
+
|
|
398
|
+
def get_configurator(self, action_type: str) -> ActionConfigurator:
|
|
399
|
+
configurators = {
|
|
400
|
+
constants.ACTION_JOB: JobConfigurator(self.expert()),
|
|
401
|
+
constants.ACTION_COMBO: CombinedActionsConfigurator(self.expert()),
|
|
402
|
+
constants.ACTION_NOISEDETECTION: NoiseDetectionConfigurator(self.expert()),
|
|
403
|
+
constants.ACTION_FOCUSSTACK: FocusStackConfigurator(self.expert()),
|
|
404
|
+
constants.ACTION_FOCUSSTACKBUNCH: FocusStackBunchConfigurator(self.expert()),
|
|
405
|
+
constants.ACTION_MULTILAYER: MultiLayerConfigurator(self.expert()),
|
|
406
|
+
constants.ACTION_MASKNOISE: MaskNoiseConfigurator(self.expert()),
|
|
407
|
+
constants.ACTION_VIGNETTING: VignettingConfigurator(self.expert()),
|
|
408
|
+
constants.ACTION_ALIGNFRAMES: AlignFramesConfigurator(self.expert()),
|
|
409
|
+
constants.ACTION_BALANCEFRAMES: BalanceFramesConfigurator(self.expert()),
|
|
410
|
+
}
|
|
411
|
+
return configurators.get(action_type, DefaultActionConfigurator(self.expert()))
|
|
412
|
+
|
|
413
|
+
def accept(self):
|
|
414
|
+
self.parent()._project_buffer.append(self.parent().project.clone())
|
|
415
|
+
if self.configurator.update_params(self.action.params):
|
|
416
|
+
self.parent().mark_as_modified()
|
|
417
|
+
super().accept()
|
|
418
|
+
else:
|
|
419
|
+
self.parent()._project_buffer.pop()
|
|
420
|
+
|
|
421
|
+
def reset_to_defaults(self):
|
|
422
|
+
builder = self.configurator.get_builder()
|
|
423
|
+
if builder:
|
|
424
|
+
builder.reset_to_defaults()
|
|
425
|
+
|
|
426
|
+
def expert(self):
|
|
427
|
+
return self.parent().expert_options
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class NoNameActionConfigurator(ActionConfigurator):
|
|
431
|
+
def __init__(self, expert=False):
|
|
432
|
+
super().__init__(expert)
|
|
433
|
+
|
|
434
|
+
def get_builder(self):
|
|
435
|
+
return self.builder
|
|
436
|
+
|
|
437
|
+
def update_params(self, params):
|
|
438
|
+
return self.builder.update_params(params)
|
|
439
|
+
|
|
440
|
+
def add_bold_label(self, label):
|
|
441
|
+
label = QLabel(label)
|
|
442
|
+
label.setStyleSheet("font-weight: bold")
|
|
443
|
+
self.builder.layout.addRow(label)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class DefaultActionConfigurator(NoNameActionConfigurator):
|
|
447
|
+
def __init__(self, expert=False):
|
|
448
|
+
super().__init__(expert)
|
|
449
|
+
|
|
450
|
+
def create_form(self, layout, action, tag='Action'):
|
|
451
|
+
self.builder = FieldBuilder(layout, action)
|
|
452
|
+
self.builder.add_field('name', FIELD_TEXT, f'{tag} name', required=True)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class JobConfigurator(DefaultActionConfigurator):
|
|
456
|
+
def __init__(self, expert=False):
|
|
457
|
+
super().__init__(expert)
|
|
458
|
+
|
|
459
|
+
def create_form(self, layout, action):
|
|
460
|
+
super().create_form(layout, action, "Job")
|
|
461
|
+
self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=True)
|
|
462
|
+
self.builder.add_field('input_path', FIELD_REL_PATH, 'Input path', required=False,
|
|
463
|
+
must_exist=True, placeholder='relative to working path')
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class NoiseDetectionConfigurator(DefaultActionConfigurator):
|
|
467
|
+
def __init__(self, expert=False):
|
|
468
|
+
super().__init__(expert)
|
|
469
|
+
|
|
470
|
+
def create_form(self, layout, action):
|
|
471
|
+
super().create_form(layout, action)
|
|
472
|
+
self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=True,
|
|
473
|
+
placeholder='inherit from job')
|
|
474
|
+
self.builder.add_field('input_path', FIELD_REL_PATH, f'Input path (separate by {constants.PATH_SEPARATOR})', required=False,
|
|
475
|
+
multiple_entries=True, placeholder='relative to working path')
|
|
476
|
+
self.builder.add_field('max_frames', FIELD_INT, 'Max. num. of frames', required=False,
|
|
477
|
+
default=-1, min=-1, max=1000)
|
|
478
|
+
self.builder.add_field('channel_thresholds', FIELD_INT_TUPLE, 'Noise threshold', required=False, size=3,
|
|
479
|
+
default=constants.DEFAULT_CHANNEL_THRESHOLDS, labels=constants.RGB_LABELS, min=[1] * 3, max=[1000] * 3)
|
|
480
|
+
if self.expert:
|
|
481
|
+
self.builder.add_field('blur_size', FIELD_INT, 'Blur size (px)', required=False,
|
|
482
|
+
default=constants.DEFAULT_BLUR_SIZE, min=1, max=50)
|
|
483
|
+
self.builder.add_field('file_name', FIELD_TEXT, 'File name', required=False,
|
|
484
|
+
default=constants.DEFAULT_NOISE_MAP_FILENAME,
|
|
485
|
+
placeholder=constants.DEFAULT_NOISE_MAP_FILENAME)
|
|
486
|
+
self.add_bold_label("Miscellanea:")
|
|
487
|
+
self.builder.add_field('plot_histograms', FIELD_BOOL, 'Plot histograms', required=False, default=False)
|
|
488
|
+
self.builder.add_field('plot_path', FIELD_REL_PATH, 'Plots path', required=False, default=constants.DEFAULT_PLOTS_PATH,
|
|
489
|
+
placeholder='relative to working path')
|
|
490
|
+
self.builder.add_field('plot_range', FIELD_INT_TUPLE, 'Plot range', required=False, size=2,
|
|
491
|
+
default=constants.DEFAULT_NOISE_PLOT_RANGE, labels=['min', 'max'], min=[0] * 2, max=[1000] * 2)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
495
|
+
def __init__(self, expert=False):
|
|
496
|
+
super().__init__(expert)
|
|
497
|
+
|
|
498
|
+
ENERGY_OPTIONS = ['Laplacian', 'Sobel']
|
|
499
|
+
MAP_TYPE_OPTIONS = ['Average', 'Maximum']
|
|
500
|
+
FLOAT_OPTIONS = ['float 32 bits', 'float 64 bits']
|
|
501
|
+
|
|
502
|
+
def create_form(self, layout, action):
|
|
503
|
+
super().create_form(layout, action)
|
|
504
|
+
if self.expert:
|
|
505
|
+
self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=False)
|
|
506
|
+
self.builder.add_field('input_path', FIELD_REL_PATH, 'Input path', required=False,
|
|
507
|
+
placeholder='relative to working path')
|
|
508
|
+
self.builder.add_field('output_path', FIELD_REL_PATH, 'Output path', required=False,
|
|
509
|
+
placeholder='relative to working path')
|
|
510
|
+
self.builder.add_field('scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
|
|
511
|
+
required=False, default=True)
|
|
512
|
+
|
|
513
|
+
def common_fields(self, layout, action):
|
|
514
|
+
self.builder.add_field('denoise', FIELD_FLOAT, 'Denoise', required=False,
|
|
515
|
+
default=0, min=0, max=10)
|
|
516
|
+
self.add_bold_label("Stacking algorithm:")
|
|
517
|
+
combo = self.builder.add_field('stacker', FIELD_COMBO, 'Stacking algorithm', required=True,
|
|
518
|
+
options=constants.STACK_ALGO_OPTIONS, default=constants.STACK_ALGO_DEFAULT)
|
|
519
|
+
q_pyramid, q_depthmap = QWidget(), QWidget()
|
|
520
|
+
for q in [q_pyramid, q_depthmap]:
|
|
521
|
+
layout = QFormLayout()
|
|
522
|
+
layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
523
|
+
layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
|
524
|
+
layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
525
|
+
layout.setLabelAlignment(Qt.AlignLeft)
|
|
526
|
+
q.setLayout(layout)
|
|
527
|
+
stacked = QStackedWidget()
|
|
528
|
+
stacked.addWidget(q_pyramid)
|
|
529
|
+
stacked.addWidget(q_depthmap)
|
|
530
|
+
|
|
531
|
+
def change():
|
|
532
|
+
text = combo.currentText()
|
|
533
|
+
if text == 'Pyramid':
|
|
534
|
+
stacked.setCurrentWidget(q_pyramid)
|
|
535
|
+
elif text == 'Depth map':
|
|
536
|
+
stacked.setCurrentWidget(q_depthmap)
|
|
537
|
+
change()
|
|
538
|
+
if self.expert:
|
|
539
|
+
self.builder.add_field('pyramid_min_size', FIELD_INT, 'Minimum size (px)',
|
|
540
|
+
required=False, add_to_layout=q_pyramid.layout(),
|
|
541
|
+
default=constants.DEFAULT_PY_MIN_SIZE, min=2, max=256)
|
|
542
|
+
self.builder.add_field('pyramid_kernel_size', FIELD_INT, 'Kernel size (px)',
|
|
543
|
+
required=False, add_to_layout=q_pyramid.layout(),
|
|
544
|
+
default=constants.DEFAULT_PY_KERNEL_SIZE, min=3, max=21)
|
|
545
|
+
self.builder.add_field('pyramid_gen_kernel', FIELD_FLOAT, 'Gen. kernel',
|
|
546
|
+
required=False, add_to_layout=q_pyramid.layout(),
|
|
547
|
+
default=constants.DEFAULT_PY_GEN_KERNEL, min=0.0, max=2.0)
|
|
548
|
+
self.builder.add_field('pyramid_float_type', FIELD_COMBO, 'Precision', required=False,
|
|
549
|
+
add_to_layout=q_pyramid.layout(), options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
|
|
550
|
+
default={k: v for k, v in
|
|
551
|
+
zip(constants.VALID_FLOATS, self.FLOAT_OPTIONS)}[constants.DEFAULT_PY_FLOAT])
|
|
552
|
+
self.builder.add_field('depthmap_energy', FIELD_COMBO, 'Energy', required=False,
|
|
553
|
+
add_to_layout=q_depthmap.layout(),
|
|
554
|
+
options=self.ENERGY_OPTIONS, values=constants.VALID_DM_ENERGY,
|
|
555
|
+
default={k: v for k, v in
|
|
556
|
+
zip(constants.VALID_DM_ENERGY, self.ENERGY_OPTIONS)}[constants.DEFAULT_DM_ENERGY])
|
|
557
|
+
self.builder.add_field('map_type', FIELD_COMBO, 'Map type', required=False,
|
|
558
|
+
add_to_layout=q_depthmap.layout(),
|
|
559
|
+
options=self.MAP_TYPE_OPTIONS, values=constants.VALID_DM_MAP,
|
|
560
|
+
default={k: v for k, v in
|
|
561
|
+
zip(constants.VALID_DM_MAP, self.MAP_TYPE_OPTIONS)}[constants.DEFAULT_DM_MAP])
|
|
562
|
+
if self.expert:
|
|
563
|
+
self.builder.add_field('depthmap_kernel_size', FIELD_INT, 'Kernel size (px)',
|
|
564
|
+
required=False, add_to_layout=q_depthmap.layout(),
|
|
565
|
+
default=constants.DEFAULT_DM_KERNEL_SIZE, min=3, max=21)
|
|
566
|
+
self.builder.add_field('depthmap_blur_size', FIELD_INT, 'Blurl size (px)',
|
|
567
|
+
required=False, add_to_layout=q_depthmap.layout(),
|
|
568
|
+
default=constants.DEFAULT_DM_BLUR_SIZE, min=1, max=21)
|
|
569
|
+
self.builder.add_field('depthmap_smooth_size', FIELD_INT, 'Smooth size (px)',
|
|
570
|
+
required=False, add_to_layout=q_depthmap.layout(),
|
|
571
|
+
default=constants.DEFAULT_DM_SMOOTH_SIZE, min=0, max=256)
|
|
572
|
+
self.builder.add_field('depthmap_temperature', FIELD_FLOAT, 'Temperature', required=False,
|
|
573
|
+
add_to_layout=q_depthmap.layout(), default=constants.DEFAULT_DM_TEMPERATURE,
|
|
574
|
+
min=0, max=1, step=0.05)
|
|
575
|
+
self.builder.add_field('depthmap_levels', FIELD_INT, 'Levels', required=False,
|
|
576
|
+
add_to_layout=q_depthmap.layout(), default=constants.DEFAULT_DM_LEVELS, min=2, max=6)
|
|
577
|
+
self.builder.add_field('depthmap_float_type', FIELD_COMBO, 'Precision', required=False,
|
|
578
|
+
add_to_layout=q_depthmap.layout(), options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
|
|
579
|
+
default={k: v for k, v in
|
|
580
|
+
zip(constants.VALID_FLOATS, self.FLOAT_OPTIONS)}[constants.DEFAULT_DM_FLOAT])
|
|
581
|
+
self.builder.layout.addRow(stacked)
|
|
582
|
+
combo.currentIndexChanged.connect(change)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class FocusStackConfigurator(FocusStackBaseConfigurator):
|
|
586
|
+
def __init__(self, expert=False):
|
|
587
|
+
super().__init__(expert)
|
|
588
|
+
|
|
589
|
+
def create_form(self, layout, action):
|
|
590
|
+
super().create_form(layout, action)
|
|
591
|
+
if self.expert:
|
|
592
|
+
self.builder.add_field('exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
|
|
593
|
+
placeholder='relative to working path')
|
|
594
|
+
self.builder.add_field('prefix', FIELD_TEXT, 'Ouptut filename prefix', required=False,
|
|
595
|
+
default=constants.DEFAULT_STACK_PREFIX, placeholder="_stack")
|
|
596
|
+
self.builder.add_field('plot_stack', FIELD_BOOL, 'Plot stack', required=False,
|
|
597
|
+
default=constants.DEFAULT_PLOT_STACK)
|
|
598
|
+
super().common_fields(layout, action)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
|
|
602
|
+
def __init__(self, expert=False):
|
|
603
|
+
super().__init__(expert)
|
|
604
|
+
|
|
605
|
+
def create_form(self, layout, action):
|
|
606
|
+
super().create_form(layout, action)
|
|
607
|
+
self.builder.add_field('frames', FIELD_INT, 'Frames', required=False,
|
|
608
|
+
default=constants.DEFAULT_FRAMES, min=1, max=100)
|
|
609
|
+
self.builder.add_field('overlap', FIELD_INT, 'Overlapping frames', required=False,
|
|
610
|
+
default=constants.DEFAULT_OVERLAP, min=0, max=100)
|
|
611
|
+
self.builder.add_field('plot_stack', FIELD_BOOL, 'Plot stack', required=False,
|
|
612
|
+
default=constants.DEFAULT_PLOT_STACK_BUNCH)
|
|
613
|
+
super().common_fields(layout, action)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
class MultiLayerConfigurator(DefaultActionConfigurator):
|
|
617
|
+
def __init__(self, expert=False):
|
|
618
|
+
super().__init__(expert)
|
|
619
|
+
|
|
620
|
+
def create_form(self, layout, action):
|
|
621
|
+
super().create_form(layout, action)
|
|
622
|
+
if self.expert:
|
|
623
|
+
self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=False)
|
|
624
|
+
self.builder.add_field('input_path', FIELD_REL_PATH,
|
|
625
|
+
f'Input path (separate by {constants.PATH_SEPARATOR})', required=False,
|
|
626
|
+
multiple_entries=True, placeholder='relative to working path')
|
|
627
|
+
self.builder.add_field('output_path', FIELD_REL_PATH, 'Output path', required=False,
|
|
628
|
+
placeholder='relative to working path')
|
|
629
|
+
self.builder.add_field('exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
|
|
630
|
+
placeholder='relative to working path')
|
|
631
|
+
self.builder.add_field('scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
|
|
632
|
+
required=False, default=True)
|
|
633
|
+
self.builder.add_field('reverse_order', FIELD_BOOL, 'Reverse file order',
|
|
634
|
+
required=False, default=constants.DEFAULT_MULTILAYER_FILE_REVERSE_ORDER)
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
638
|
+
def __init__(self, expert=False):
|
|
639
|
+
super().__init__(expert)
|
|
640
|
+
|
|
641
|
+
def create_form(self, layout, action):
|
|
642
|
+
DefaultActionConfigurator.create_form(self, layout, action)
|
|
643
|
+
if self.expert:
|
|
644
|
+
self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=False)
|
|
645
|
+
self.builder.add_field('input_path', FIELD_REL_PATH, 'Input path', required=False,
|
|
646
|
+
must_exist=True, placeholder='relative to working path')
|
|
647
|
+
self.builder.add_field('output_path', FIELD_REL_PATH, 'Output path', required=False,
|
|
648
|
+
placeholder='relative to working path')
|
|
649
|
+
self.builder.add_field('scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
|
|
650
|
+
required=False, default=True)
|
|
651
|
+
if self.expert:
|
|
652
|
+
self.builder.add_field('plot_path', FIELD_REL_PATH, 'Plots path', required=False, default="plots",
|
|
653
|
+
placeholder='relative to working path')
|
|
654
|
+
self.builder.add_field('resample', FIELD_INT, 'Resample frame stack', required=False,
|
|
655
|
+
default=1, min=1, max=100)
|
|
656
|
+
self.builder.add_field('ref_idx', FIELD_INT, 'Reference frame index', required=False,
|
|
657
|
+
default=-1, min=-1, max=1000)
|
|
658
|
+
self.builder.add_field('step_process', FIELD_BOOL, 'Step process', required=False, default=True)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class MaskNoiseConfigurator(NoNameActionConfigurator):
|
|
662
|
+
def __init__(self, expert=False):
|
|
663
|
+
super().__init__(expert)
|
|
664
|
+
|
|
665
|
+
def create_form(self, layout, action):
|
|
666
|
+
DefaultActionConfigurator.create_form(self, layout, action)
|
|
667
|
+
self.builder.add_field('noise_mask', FIELD_REL_PATH, 'Noise mask file', required=False,
|
|
668
|
+
path_type='file', must_exist=True,
|
|
669
|
+
default=constants.DEFAULT_NOISE_MAP_FILENAME, placeholder=constants.DEFAULT_NOISE_MAP_FILENAME)
|
|
670
|
+
if self.expert:
|
|
671
|
+
self.builder.add_field('kernel_size', FIELD_INT, 'Kernel size', required=False,
|
|
672
|
+
default=constants.DEFAULT_MN_KERNEL_SIZE, min=1, max=10)
|
|
673
|
+
self.builder.add_field('method', FIELD_COMBO, 'Interpolation method', required=False,
|
|
674
|
+
options=['Mean', 'Median'], default='Mean')
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class VignettingConfigurator(NoNameActionConfigurator):
|
|
678
|
+
def __init__(self, expert=False):
|
|
679
|
+
super().__init__(expert)
|
|
680
|
+
|
|
681
|
+
def create_form(self, layout, action):
|
|
682
|
+
DefaultActionConfigurator.create_form(self, layout, action)
|
|
683
|
+
if self.expert:
|
|
684
|
+
self.builder.add_field('r_steps', FIELD_INT, 'Radial steps', required=False,
|
|
685
|
+
default=constants.DEFAULT_R_STEPS, min=1, max=1000)
|
|
686
|
+
self.builder.add_field('black_threshold', FIELD_INT, 'Black intensity threshold', required=False,
|
|
687
|
+
default=constants.DEFAULT_BLACK_THRESHOLD, min=0, max=1000)
|
|
688
|
+
self.builder.add_field('max_correction', FIELD_FLOAT, 'Max. correction', required=False,
|
|
689
|
+
default=constants.DEFAULT_MAX_CORRECTION, min=0, max=1, step=0.05)
|
|
690
|
+
self.add_bold_label("Miscellanea:")
|
|
691
|
+
self.builder.add_field('plot_correction', FIELD_BOOL, 'Plot correction', required=False, default=False)
|
|
692
|
+
self.builder.add_field('plot_summary', FIELD_BOOL, 'Plot summary', required=False, default=False)
|
|
693
|
+
self.builder.add_field('apply_correction', FIELD_BOOL, 'Apply correction', required=False, default=True)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
class AlignFramesConfigurator(NoNameActionConfigurator):
|
|
697
|
+
BORDER_MODE_OPTIONS = ['Constant', 'Replicate', 'Replicate and blur']
|
|
698
|
+
TRANSFORM_OPTIONS = ['Rigid', 'Homography']
|
|
699
|
+
METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
|
|
700
|
+
MATCHING_METHOD_OPTIONS = ['K-nearest neighbors', 'Hamming distance']
|
|
701
|
+
|
|
702
|
+
def __init__(self, expert=False):
|
|
703
|
+
super().__init__(expert)
|
|
704
|
+
|
|
705
|
+
def show_info(self, message, timeout=3000):
|
|
706
|
+
self.info_label.setText(message)
|
|
707
|
+
self.info_label.setVisible(True)
|
|
708
|
+
QTimer.singleShot(timeout, lambda: self.info_label.setVisible(False))
|
|
709
|
+
|
|
710
|
+
def change_match_config(self):
|
|
711
|
+
detector = self.detector_field.currentText()
|
|
712
|
+
descriptor = self.descriptor_field.currentText()
|
|
713
|
+
match_method = {k: v for k, v in zip(self.MATCHING_METHOD_OPTIONS,
|
|
714
|
+
constants.VALID_MATCHING_METHODS)}[self.matching_method_field.currentText()]
|
|
715
|
+
try:
|
|
716
|
+
print(detector, descriptor, match_method)
|
|
717
|
+
validate_align_config(detector, descriptor, match_method)
|
|
718
|
+
except Exception as e:
|
|
719
|
+
self.show_info(str(e))
|
|
720
|
+
if descriptor == constants.DETECTOR_SIFT and match_method == constants.MATCHING_NORM_HAMMING:
|
|
721
|
+
self.matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[0])
|
|
722
|
+
if detector == constants.DETECTOR_ORB and descriptor == constants.DESCRIPTOR_AKAZE and \
|
|
723
|
+
match_method == constants.MATCHING_NORM_HAMMING:
|
|
724
|
+
self.matching_method_field.setCurrentText(constants.MATCHING_NORM_HAMMING)
|
|
725
|
+
if detector == constants.DETECTOR_BRISK and descriptor == constants.DESCRIPTOR_AKAZE:
|
|
726
|
+
self.descriptor_field.setCurrentText('BRISK')
|
|
727
|
+
if detector == constants.DETECTOR_SURF and descriptor == constants.DESCRIPTOR_AKAZE:
|
|
728
|
+
self.descriptor_field.setCurrentText('SIFT')
|
|
729
|
+
if detector == constants.DETECTOR_SIFT and descriptor != constants.DESCRIPTOR_SIFT:
|
|
730
|
+
self.descriptor_field.setCurrentText('SIFT')
|
|
731
|
+
if detector in constants.NOKNN_METHODS['detectors'] and descriptor in constants.NOKNN_METHODS['descriptors']:
|
|
732
|
+
if match_method == constants.MATCHING_KNN:
|
|
733
|
+
self.matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[1])
|
|
734
|
+
|
|
735
|
+
def create_form(self, layout, action):
|
|
736
|
+
DefaultActionConfigurator.create_form(self, layout, action)
|
|
737
|
+
self.detector_field = 0
|
|
738
|
+
self.descriptor_field = 0
|
|
739
|
+
if self.expert:
|
|
740
|
+
self.add_bold_label("Feature identification:")
|
|
741
|
+
|
|
742
|
+
self.info_label = QLabel()
|
|
743
|
+
self.info_label.setStyleSheet("color: orange; font-style: italic;")
|
|
744
|
+
self.info_label.setVisible(False)
|
|
745
|
+
layout.addRow(self.info_label)
|
|
746
|
+
|
|
747
|
+
detector = self.detector_field = self.builder.add_field('detector', FIELD_COMBO, 'Detector', required=False,
|
|
748
|
+
options=constants.VALID_DETECTORS, default=constants.DEFAULT_DETECTOR)
|
|
749
|
+
descriptor = self.descriptor_field = self.builder.add_field('descriptor', FIELD_COMBO, 'Descriptor', required=False,
|
|
750
|
+
options=constants.VALID_DESCRIPTORS, default=constants.DEFAULT_DESCRIPTOR)
|
|
751
|
+
|
|
752
|
+
self.add_bold_label("Feature matching:")
|
|
753
|
+
match_method = self.matching_method_field = self.builder.add_field('match_method', FIELD_COMBO, 'Match method', required=False,
|
|
754
|
+
options=self.MATCHING_METHOD_OPTIONS, values=constants.VALID_MATCHING_METHODS,
|
|
755
|
+
default=constants.DEFAULT_MATCHING_METHOD)
|
|
756
|
+
self.detector_field.setToolTip(
|
|
757
|
+
"SIFT: Requires SIFT descriptor and K-NN matching\n"
|
|
758
|
+
"ORB/AKAZE: Work best with Hamming distance"
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
self.descriptor_field.setToolTip(
|
|
762
|
+
"SIFT: Requires K-NN matching\n"
|
|
763
|
+
"ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors"
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
self.matching_method_field.setToolTip(
|
|
767
|
+
"Automatically selected based on detector/descriptor combination"
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
detector.currentIndexChanged.connect(self.change_match_config)
|
|
771
|
+
descriptor.currentIndexChanged.connect(self.change_match_config)
|
|
772
|
+
match_method.currentIndexChanged.connect(self.change_match_config)
|
|
773
|
+
self.builder.add_field('flann_idx_kdtree', FIELD_INT, 'Flann idx kdtree', required=False,
|
|
774
|
+
default=constants.DEFAULT_FLANN_IDX_KDTREE, min=0, max=10)
|
|
775
|
+
self.builder.add_field('flann_trees', FIELD_INT, 'Flann trees', required=False,
|
|
776
|
+
default=constants.DEFAULT_FLANN_TREES, min=0, max=10)
|
|
777
|
+
self.builder.add_field('flann_checks', FIELD_INT, 'Flann checks', required=False,
|
|
778
|
+
default=constants.DEFAULT_FLANN_CHECKS, min=0, max=1000)
|
|
779
|
+
self.builder.add_field('threshold', FIELD_FLOAT, 'Threshold', required=False,
|
|
780
|
+
default=constants.DEFAULT_ALIGN_THRESHOLD, min=0, max=1, step=0.05)
|
|
781
|
+
|
|
782
|
+
self.add_bold_label("Transform:")
|
|
783
|
+
transform = self.builder.add_field('transform', FIELD_COMBO, 'Transform', required=False,
|
|
784
|
+
options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
|
|
785
|
+
default=constants.DEFAULT_TRANSFORM)
|
|
786
|
+
method = self.builder.add_field('align_method', FIELD_COMBO, 'Align method', required=False,
|
|
787
|
+
options=self.METHOD_OPTIONS, values=constants.VALID_ALIGN_METHODS,
|
|
788
|
+
default=constants.DEFAULT_ALIGN_METHOD)
|
|
789
|
+
rans_threshold = self.builder.add_field('rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
|
|
790
|
+
default=constants.DEFAULT_RANS_THRESHOLD, min=0, max=20, step=0.1)
|
|
791
|
+
|
|
792
|
+
def change_method():
|
|
793
|
+
text = method.currentText()
|
|
794
|
+
if text == self.METHOD_OPTIONS[0]:
|
|
795
|
+
rans_threshold.setEnabled(True)
|
|
796
|
+
elif text == self.METHOD_OPTIONS[1]:
|
|
797
|
+
rans_threshold.setEnabled(False)
|
|
798
|
+
method.currentIndexChanged.connect(change_method)
|
|
799
|
+
change_method()
|
|
800
|
+
self.builder.add_field('align_confidence', FIELD_FLOAT, 'Confidence (%)', required=False, decimals=1,
|
|
801
|
+
default=constants.DEFAULT_ALIGN_CONFIDENCE, min=70.0, max=100.0, step=0.1)
|
|
802
|
+
|
|
803
|
+
refine_iters = self.builder.add_field('refine_iters', FIELD_INT, 'Refinement iterations (Rigid)', required=False,
|
|
804
|
+
default=constants.DEFAULT_REFINE_ITERS, min=0, max=1000)
|
|
805
|
+
max_iters = self.builder.add_field('max_iters', FIELD_INT, 'Max. iterations (Homography)', required=False,
|
|
806
|
+
default=constants.DEFAULT_ALIGN_MAX_ITERS, min=0, max=5000)
|
|
807
|
+
|
|
808
|
+
def change_transform():
|
|
809
|
+
text = transform.currentText()
|
|
810
|
+
if text == self.TRANSFORM_OPTIONS[0]:
|
|
811
|
+
refine_iters.setEnabled(True)
|
|
812
|
+
max_iters.setEnabled(False)
|
|
813
|
+
elif text == self.TRANSFORM_OPTIONS[1]:
|
|
814
|
+
refine_iters.setEnabled(False)
|
|
815
|
+
max_iters.setEnabled(True)
|
|
816
|
+
transform.currentIndexChanged.connect(change_transform)
|
|
817
|
+
change_transform()
|
|
818
|
+
subsample = self.builder.add_field('subsample', FIELD_INT, 'Subsample factor', required=False,
|
|
819
|
+
default=constants.DEFAULT_ALIGN_SUBSAMPLE, min=0, max=256)
|
|
820
|
+
fast_subsampling = self.builder.add_field('fast_subsampling', FIELD_BOOL, 'Fast subsampling', required=False,
|
|
821
|
+
default=constants.DEFAULT_ALIGN_FAST_SUBSAMPLING)
|
|
822
|
+
|
|
823
|
+
def change_subsample():
|
|
824
|
+
fast_subsampling.setEnabled(subsample.value() > 1)
|
|
825
|
+
subsample.valueChanged.connect(change_subsample)
|
|
826
|
+
change_subsample()
|
|
827
|
+
self.add_bold_label("Border:")
|
|
828
|
+
self.builder.add_field('border_mode', FIELD_COMBO, 'Border mode', required=False,
|
|
829
|
+
options=self.BORDER_MODE_OPTIONS, values=constants.VALID_BORDER_MODES,
|
|
830
|
+
default=constants.DEFAULT_BORDER_MODE)
|
|
831
|
+
self.builder.add_field('border_value', FIELD_INT_TUPLE, 'Border value (if constant)', required=False, size=4,
|
|
832
|
+
default=constants.DEFAULT_BORDER_VALUE, labels=constants.RGBA_LABELS,
|
|
833
|
+
min=constants.DEFAULT_BORDER_VALUE, max=[255] * 4)
|
|
834
|
+
self.builder.add_field('border_blur', FIELD_FLOAT, 'Border blur', required=False,
|
|
835
|
+
default=constants.DEFAULT_BORDER_BLUR, min=0, max=1000, step=1)
|
|
836
|
+
self.add_bold_label("Miscellanea:")
|
|
837
|
+
self.builder.add_field('plot_summary', FIELD_BOOL, 'Plot summary', required=False, default=False)
|
|
838
|
+
self.builder.add_field('plot_matches', FIELD_BOOL, 'Plot matches', required=False, default=False)
|
|
839
|
+
|
|
840
|
+
def update_params(self, params: Dict[str, Any]) -> bool:
|
|
841
|
+
if self.detector_field and self.descriptor_field and self.matching_method_field:
|
|
842
|
+
try:
|
|
843
|
+
detector = self.detector_field.currentText()
|
|
844
|
+
descriptor = self.descriptor_field.currentText()
|
|
845
|
+
match_method = {k: v for k, v in zip(self.MATCHING_METHOD_OPTIONS,
|
|
846
|
+
constants.VALID_MATCHING_METHODS)}[self.matching_method_field.currentText()]
|
|
847
|
+
validate_align_config(detector, descriptor, match_method)
|
|
848
|
+
return super().update_params(params)
|
|
849
|
+
except Exception as e:
|
|
850
|
+
QMessageBox.warning(None, "Error", f"{str(e)}")
|
|
851
|
+
return False
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
class BalanceFramesConfigurator(NoNameActionConfigurator):
|
|
855
|
+
CORRECTION_MAP_OPTIONS = ['Linear', 'Gamma', 'Match histograms']
|
|
856
|
+
CHANNEL_OPTIONS = ['Luminosity', 'RGB', 'HSV', 'HLS']
|
|
857
|
+
|
|
858
|
+
def __init__(self, expert=False):
|
|
859
|
+
super().__init__(expert)
|
|
860
|
+
|
|
861
|
+
def create_form(self, layout, action):
|
|
862
|
+
DefaultActionConfigurator.create_form(self, layout, action)
|
|
863
|
+
if self.expert:
|
|
864
|
+
self.builder.add_field('mask_size', FIELD_FLOAT, 'Mask size', required=False,
|
|
865
|
+
default=0, min=0, max=5, step=0.1)
|
|
866
|
+
self.builder.add_field('intensity_interval', FIELD_INT_TUPLE, 'Intensity range', required=False, size=2,
|
|
867
|
+
default=[v for k, v in constants.DEFAULT_INTENSITY_INTERVAL.items()],
|
|
868
|
+
labels=['min', 'max'], min=[-1] * 2, max=[65536] * 2)
|
|
869
|
+
self.builder.add_field('subsample', FIELD_INT, 'Subsample factor', required=False,
|
|
870
|
+
default=constants.DEFAULT_BALANCE_SUBSAMPLE, min=1, max=256)
|
|
871
|
+
self.builder.add_field('corr_map', FIELD_COMBO, 'Correction map', required=False,
|
|
872
|
+
options=self.CORRECTION_MAP_OPTIONS, values=constants.VALID_BALANCE,
|
|
873
|
+
default='Linear')
|
|
874
|
+
self.builder.add_field('channel', FIELD_COMBO, 'Channel', required=False,
|
|
875
|
+
options=self.CHANNEL_OPTIONS, values=constants.VALID_BALANCE_CHANNELS,
|
|
876
|
+
default='Luminosity')
|
|
877
|
+
self.add_bold_label("Miscellanea:")
|
|
878
|
+
self.builder.add_field('plot_summary', FIELD_BOOL, 'Plot summary', required=False, default=False)
|
|
879
|
+
self.builder.add_field('plot_histograms', FIELD_BOOL, 'Plot histograms', required=False, default=False)
|