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.

Files changed (67) hide show
  1. shinestacker/__init__.py +3 -0
  2. shinestacker/_version.py +1 -0
  3. shinestacker/algorithms/__init__.py +14 -0
  4. shinestacker/algorithms/align.py +307 -0
  5. shinestacker/algorithms/balance.py +367 -0
  6. shinestacker/algorithms/core_utils.py +22 -0
  7. shinestacker/algorithms/depth_map.py +164 -0
  8. shinestacker/algorithms/exif.py +238 -0
  9. shinestacker/algorithms/multilayer.py +187 -0
  10. shinestacker/algorithms/noise_detection.py +182 -0
  11. shinestacker/algorithms/pyramid.py +176 -0
  12. shinestacker/algorithms/stack.py +112 -0
  13. shinestacker/algorithms/stack_framework.py +248 -0
  14. shinestacker/algorithms/utils.py +71 -0
  15. shinestacker/algorithms/vignetting.py +137 -0
  16. shinestacker/app/__init__.py +0 -0
  17. shinestacker/app/about_dialog.py +24 -0
  18. shinestacker/app/app_config.py +39 -0
  19. shinestacker/app/gui_utils.py +35 -0
  20. shinestacker/app/help_menu.py +16 -0
  21. shinestacker/app/main.py +176 -0
  22. shinestacker/app/open_frames.py +39 -0
  23. shinestacker/app/project.py +91 -0
  24. shinestacker/app/retouch.py +82 -0
  25. shinestacker/config/__init__.py +4 -0
  26. shinestacker/config/config.py +53 -0
  27. shinestacker/config/constants.py +174 -0
  28. shinestacker/config/gui_constants.py +85 -0
  29. shinestacker/core/__init__.py +5 -0
  30. shinestacker/core/colors.py +60 -0
  31. shinestacker/core/core_utils.py +52 -0
  32. shinestacker/core/exceptions.py +50 -0
  33. shinestacker/core/framework.py +210 -0
  34. shinestacker/core/logging.py +89 -0
  35. shinestacker/gui/__init__.py +0 -0
  36. shinestacker/gui/action_config.py +879 -0
  37. shinestacker/gui/actions_window.py +283 -0
  38. shinestacker/gui/colors.py +57 -0
  39. shinestacker/gui/gui_images.py +152 -0
  40. shinestacker/gui/gui_logging.py +213 -0
  41. shinestacker/gui/gui_run.py +393 -0
  42. shinestacker/gui/img/close-round-line-icon.png +0 -0
  43. shinestacker/gui/img/forward-button-icon.png +0 -0
  44. shinestacker/gui/img/play-button-round-icon.png +0 -0
  45. shinestacker/gui/img/plus-round-line-icon.png +0 -0
  46. shinestacker/gui/main_window.py +599 -0
  47. shinestacker/gui/new_project.py +170 -0
  48. shinestacker/gui/project_converter.py +148 -0
  49. shinestacker/gui/project_editor.py +539 -0
  50. shinestacker/gui/project_model.py +138 -0
  51. shinestacker/retouch/__init__.py +0 -0
  52. shinestacker/retouch/brush.py +9 -0
  53. shinestacker/retouch/brush_controller.py +57 -0
  54. shinestacker/retouch/brush_preview.py +126 -0
  55. shinestacker/retouch/exif_data.py +65 -0
  56. shinestacker/retouch/file_loader.py +104 -0
  57. shinestacker/retouch/image_editor.py +651 -0
  58. shinestacker/retouch/image_editor_ui.py +380 -0
  59. shinestacker/retouch/image_viewer.py +356 -0
  60. shinestacker/retouch/shortcuts_help.py +98 -0
  61. shinestacker/retouch/undo_manager.py +38 -0
  62. shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
  63. shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
  64. shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
  65. shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
  66. shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
  67. 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)