shinestacker 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (50) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +229 -41
  3. shinestacker/algorithms/align_auto.py +15 -3
  4. shinestacker/algorithms/align_parallel.py +81 -25
  5. shinestacker/algorithms/balance.py +23 -13
  6. shinestacker/algorithms/base_stack_algo.py +14 -20
  7. shinestacker/algorithms/depth_map.py +9 -14
  8. shinestacker/algorithms/noise_detection.py +3 -1
  9. shinestacker/algorithms/pyramid.py +8 -22
  10. shinestacker/algorithms/pyramid_auto.py +5 -14
  11. shinestacker/algorithms/pyramid_tiles.py +18 -20
  12. shinestacker/algorithms/stack_framework.py +1 -1
  13. shinestacker/algorithms/utils.py +37 -10
  14. shinestacker/algorithms/vignetting.py +2 -0
  15. shinestacker/app/gui_utils.py +10 -0
  16. shinestacker/app/main.py +3 -1
  17. shinestacker/app/project.py +3 -1
  18. shinestacker/app/retouch.py +3 -1
  19. shinestacker/config/gui_constants.py +2 -2
  20. shinestacker/core/core_utils.py +10 -1
  21. shinestacker/gui/action_config.py +172 -7
  22. shinestacker/gui/action_config_dialog.py +443 -452
  23. shinestacker/gui/colors.py +1 -0
  24. shinestacker/gui/folder_file_selection.py +5 -0
  25. shinestacker/gui/gui_run.py +2 -2
  26. shinestacker/gui/main_window.py +18 -9
  27. shinestacker/gui/menu_manager.py +26 -2
  28. shinestacker/gui/new_project.py +5 -5
  29. shinestacker/gui/project_controller.py +4 -0
  30. shinestacker/gui/project_editor.py +6 -4
  31. shinestacker/gui/recent_file_manager.py +93 -0
  32. shinestacker/gui/sys_mon.py +24 -23
  33. shinestacker/retouch/base_filter.py +5 -5
  34. shinestacker/retouch/brush_preview.py +3 -0
  35. shinestacker/retouch/brush_tool.py +11 -11
  36. shinestacker/retouch/display_manager.py +21 -37
  37. shinestacker/retouch/image_editor_ui.py +129 -71
  38. shinestacker/retouch/image_view_status.py +61 -0
  39. shinestacker/retouch/image_viewer.py +89 -431
  40. shinestacker/retouch/io_gui_handler.py +12 -2
  41. shinestacker/retouch/overlaid_view.py +212 -0
  42. shinestacker/retouch/shortcuts_help.py +13 -3
  43. shinestacker/retouch/sidebyside_view.py +479 -0
  44. shinestacker/retouch/view_strategy.py +466 -0
  45. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/METADATA +1 -1
  46. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/RECORD +50 -45
  47. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/WHEEL +0 -0
  48. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/entry_points.txt +0 -0
  49. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/licenses/LICENSE +0 -0
  50. {shinestacker-1.3.0.dist-info → shinestacker-1.4.0.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,15 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0915, R0912
2
- # pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914
2
+ # pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914, C0302
3
3
  import os
4
4
  import traceback
5
- from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QLabel, QScrollArea, QSizePolicy,
6
- QMessageBox, QStackedWidget, QFormLayout, QDialog)
7
5
  from PySide6.QtCore import Qt, QTimer
6
+ from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QLabel, QScrollArea, QMessageBox,
7
+ QStackedWidget, QFormLayout, QDialog)
8
8
  from .. config.constants import constants
9
9
  from .. algorithms.align import validate_align_config
10
- from .project_model import ActionConfig
11
10
  from .base_form_dialog import create_form_layout
12
11
  from . action_config import (
13
- FieldBuilder, ActionConfigurator,
12
+ DefaultActionConfigurator,
14
13
  FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
15
14
  FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO, FIELD_REF_IDX
16
15
  )
@@ -18,7 +17,7 @@ from .folder_file_selection import FolderFileSelectionWidget
18
17
 
19
18
 
20
19
  class ActionConfigDialog(QDialog):
21
- def __init__(self, action: ActionConfig, current_wd, parent=None):
20
+ def __init__(self, action, current_wd, parent=None):
22
21
  super().__init__(parent)
23
22
  self.setWindowTitle(f"Configure {action.type_name}")
24
23
  self.form_layout = create_form_layout(self)
@@ -85,7 +84,7 @@ class ActionConfigDialog(QDialog):
85
84
  frame_geometry.moveCenter(center_point)
86
85
  self.move(frame_geometry.topLeft())
87
86
 
88
- def get_configurator(self, action_type: str) -> ActionConfigurator:
87
+ def get_configurator(self, action_type):
89
88
  configurators = {
90
89
  constants.ACTION_JOB: JobConfigurator,
91
90
  constants.ACTION_COMBO: CombinedActionsConfigurator,
@@ -115,58 +114,9 @@ class ActionConfigDialog(QDialog):
115
114
  return self.parent().expert_options
116
115
 
117
116
 
118
- class NoNameActionConfigurator(ActionConfigurator):
119
- def __init__(self, expert, current_wd):
120
- super().__init__(expert, current_wd)
121
- self.builder = None
122
-
123
- def get_builder(self):
124
- return self.builder
125
-
126
- def update_params(self, params):
127
- return self.builder.update_params(params)
128
-
129
- def add_bold_label(self, label):
130
- label = QLabel(label)
131
- label.setStyleSheet("font-weight: bold")
132
- self.add_row(label)
133
-
134
- def add_row(self, row):
135
- self.builder.main_layout.addRow(row)
136
-
137
- def add_field(self, tag, field_type, label,
138
- required=False, add_to_layout=None, **kwargs):
139
- return self.builder.add_field(tag, field_type, label, required, add_to_layout, **kwargs)
140
-
141
- def labelled_widget(self, label, widget):
142
- row = QWidget()
143
- layout = QHBoxLayout()
144
- layout.setContentsMargins(2, 2, 2, 2)
145
- layout.setSpacing(8)
146
- label_widget = QLabel(label)
147
- label_widget.setFixedWidth(120)
148
- label_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
149
- layout.addWidget(label_widget)
150
- layout.addWidget(widget)
151
- layout.setStretch(0, 1)
152
- layout.setStretch(1, 3)
153
- row.setLayout(layout)
154
- return row
155
-
156
- def add_labelled_row(self, label, widget):
157
- self.add_row(self.labelled_widget(label, widget))
158
-
159
-
160
- class DefaultActionConfigurator(NoNameActionConfigurator):
161
- def create_form(self, layout, action, tag='Action'):
162
- self.builder = FieldBuilder(layout, action, self.current_wd)
163
- self.add_field(
164
- 'name', FIELD_TEXT, f'{tag} name', required=True)
165
-
166
-
167
117
  class JobConfigurator(DefaultActionConfigurator):
168
118
  def __init__(self, expert, current_wd):
169
- super().__init__(expert, current_wd)
119
+ super().__init__(expert, current_wd, expert_toggle=False)
170
120
  self.working_path_label = None
171
121
  self.input_path_label = None
172
122
  self.frames_label = None
@@ -183,20 +133,11 @@ class JobConfigurator(DefaultActionConfigurator):
183
133
  input_filepaths = input_filepaths.split(constants.PATH_SEPARATOR)
184
134
  self.working_path_label = QLabel(working_path or "Not set")
185
135
  self.input_path_label = QLabel(input_path or "Not set")
186
- has_existing_data = working_path or input_path or input_filepaths
136
+ self.input_widget.path_edit.setText('')
187
137
  if input_filepaths:
188
138
  self.input_widget.files_mode_radio.setChecked(True)
189
- self.input_widget.selected_files = input_filepaths
190
- if input_filepaths:
191
- parent_dir = os.path.dirname(input_filepaths[0])
192
- self.input_widget.path_edit.setText(parent_dir)
193
- elif input_path and working_path:
194
- self.input_widget.folder_mode_radio.setChecked(True)
195
- input_fullpath = os.path.join(working_path, input_path)
196
- self.input_widget.path_edit.setText(input_fullpath)
197
- elif input_path:
198
- self.input_widget.folder_mode_radio.setChecked(True)
199
- self.input_widget.path_edit.setText(input_path)
139
+ else:
140
+ self.input_widget.folder_mode_radio.setChecked(False)
200
141
  self.input_widget.text_changed_connect(self.update_paths_and_frames)
201
142
  self.input_widget.folder_mode_radio.toggled.connect(self.update_paths_and_frames)
202
143
  self.input_widget.files_mode_radio.toggled.connect(self.update_paths_and_frames)
@@ -206,29 +147,27 @@ class JobConfigurator(DefaultActionConfigurator):
206
147
  self.add_bold_label("Derived Paths:")
207
148
  self.add_labelled_row("Working path: ", self.working_path_label)
208
149
  self.add_labelled_row("Input path:", self.input_path_label)
209
- if not has_existing_data:
210
- self.update_paths_and_frames()
211
- else:
212
- self.update_frames_count()
150
+ self.set_paths_and_frames(working_path, input_path, input_filepaths)
213
151
 
214
152
  def update_frames_count(self):
215
153
  if self.input_widget.get_selection_mode() == 'files':
216
- count = len(self.input_widget.get_selected_files())
154
+ count = self.input_widget.num_selected_files()
217
155
  else:
218
156
  count = self.count_image_files(self.input_widget.get_path())
219
157
  self.frames_label.setText(str(count))
220
158
 
221
- def update_paths_and_frames(self):
159
+ def set_paths_and_frames(self, working_path, input_path, input_filepaths):
160
+ self.input_path_label.setText(input_path or "Not set")
161
+ self.working_path_label.setText(working_path or "Not set")
162
+ self.frames_label.setText(str(len(input_filepaths)))
163
+
164
+ def update_paths_and_frames(self, ):
165
+ input_fullpath = self.input_widget.get_path()
166
+ input_path = os.path.basename(os.path.normpath(input_fullpath)) if input_fullpath else ""
167
+ working_path = os.path.dirname(input_fullpath) if input_fullpath else ""
168
+ self.input_path_label.setText(input_path or "Not set")
169
+ self.working_path_label.setText(working_path or "Not set")
222
170
  self.update_frames_count()
223
- selection_mode = self.input_widget.get_selection_mode()
224
- selected_files = self.input_widget.get_selected_files()
225
- input_path = self.input_widget.get_path()
226
- if selection_mode == 'files' and selected_files:
227
- input_path = os.path.dirname(selected_files[0])
228
- input_path_value = os.path.basename(os.path.normpath(input_path)) if input_path else ""
229
- working_path_value = os.path.dirname(input_path) if input_path else ""
230
- self.input_path_label.setText(input_path_value or "Not set")
231
- self.working_path_label.setText(working_path_value or "Not set")
232
171
 
233
172
  def count_image_files(self, path):
234
173
  if not path or not os.path.isdir(path):
@@ -244,18 +183,16 @@ class JobConfigurator(DefaultActionConfigurator):
244
183
  return False
245
184
  selection_mode = self.input_widget.get_selection_mode()
246
185
  selected_files = self.input_widget.get_selected_files()
247
- input_path = self.input_widget.get_path()
248
186
  if selection_mode == 'files' and selected_files:
187
+ input_full_path = os.path.dirname(selected_files[0])
249
188
  params['input_filepaths'] = self.input_widget.get_selected_filenames()
250
- params['input_path'] = os.path.dirname(selected_files[0])
251
189
  else:
190
+ input_full_path = self.input_widget.get_path()
252
191
  params['input_filepaths'] = []
253
- params['input_path'] = input_path
254
- if 'working_path' not in params or not params['working_path']:
255
- if params['input_path']:
256
- params['working_path'] = os.path.dirname(params['input_path'])
257
- else:
258
- params['working_path'] = ''
192
+ input_path = os.path.basename(os.path.normpath(input_full_path)) if input_full_path else ""
193
+ working_path = os.path.dirname(input_full_path) if input_full_path else ""
194
+ params['input_path'] = input_path
195
+ params['working_path'] = working_path
259
196
  return True
260
197
 
261
198
 
@@ -279,10 +216,10 @@ class NoiseDetectionConfigurator(DefaultActionConfigurator):
279
216
  required=False, size=3,
280
217
  default=constants.DEFAULT_CHANNEL_THRESHOLDS,
281
218
  labels=constants.RGB_LABELS, min_val=[1] * 3, max_val=[1000] * 3)
282
- if self.expert:
283
- self.add_field(
284
- 'blur_size', FIELD_INT, 'Blur size (px)', required=False,
285
- default=constants.DEFAULT_BLUR_SIZE, min_val=1, max_val=50)
219
+ self.add_field(
220
+ 'blur_size', FIELD_INT, 'Blur size (px)', required=False,
221
+ expert=True,
222
+ default=constants.DEFAULT_BLUR_SIZE, min_val=1, max_val=50)
286
223
  self.add_field(
287
224
  'file_name', FIELD_TEXT, 'File name', required=False,
288
225
  default=constants.DEFAULT_NOISE_MAP_FILENAME,
@@ -307,38 +244,45 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
307
244
  FLOAT_OPTIONS = ['float 32 bits', 'float 64 bits']
308
245
  MODE_OPTIONS = ['Auto', 'All in memory', 'Tiled I/O buffered']
309
246
 
247
+ def __init__(self, expert, current_wd):
248
+ super().__init__(expert, current_wd)
249
+ self.tab_widget = None
250
+ self.general_tab_layout = None
251
+ self.algorithm_tab_layout = None
252
+
310
253
  def create_form(self, layout, action):
311
254
  super().create_form(layout, action)
312
- if self.expert:
313
- self.add_field(
314
- 'working_path', FIELD_ABS_PATH, 'Working path', required=False)
315
- self.add_field(
316
- 'input_path', FIELD_REL_PATH, 'Input path', required=False,
317
- placeholder='relative to working path')
318
- self.add_field(
319
- 'output_path', FIELD_REL_PATH, 'Output path', required=False,
320
- placeholder='relative to working path')
321
- self.add_field(
322
- 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
255
+ self.tab_widget = self.create_tab_widget(layout)
256
+ self.general_tab_layout = self.add_tab(self.tab_widget, "General Parameters")
257
+ self.create_general_tab(self.general_tab_layout)
258
+ self.algorithm_tab_layout = self.add_tab(self.tab_widget, "Stacking Algorithm")
259
+ self.create_algorithm_tab(self.algorithm_tab_layout)
260
+
261
+ def create_general_tab(self, layout):
262
+ self.add_field_to_layout(
263
+ layout, 'working_path', FIELD_ABS_PATH, 'Working path', required=False,
264
+ expert=True)
265
+ self.add_field_to_layout(
266
+ layout, 'input_path', FIELD_REL_PATH, 'Input path', required=False,
267
+ expert=True,
268
+ placeholder='relative to working path')
269
+ self.add_field_to_layout(
270
+ layout, 'output_path', FIELD_REL_PATH, 'Output path', required=False,
271
+ expert=True,
272
+ placeholder='relative to working path')
273
+ self.add_field_to_layout(
274
+ layout, 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
323
275
  required=False, default=True)
324
276
 
325
- def common_fields(self, layout):
326
- self.add_field(
327
- 'denoise_amount', FIELD_FLOAT, 'Denoise', required=False,
328
- default=0, min_val=0, max_val=10)
329
- self.add_bold_label("Stacking algorithm:")
330
- combo = self.add_field(
331
- 'stacker', FIELD_COMBO, 'Stacking algorithm', required=True,
277
+ def create_algorithm_tab(self, layout):
278
+ self.add_bold_label_to_layout(layout, "Stacking algorithm:")
279
+ combo = self.add_field_to_layout(
280
+ layout, 'stacker', FIELD_COMBO, 'Stacking algorithm', required=True,
332
281
  options=constants.STACK_ALGO_OPTIONS,
333
282
  default=constants.STACK_ALGO_DEFAULT)
334
283
  q_pyramid, q_depthmap = QWidget(), QWidget()
335
284
  for q in [q_pyramid, q_depthmap]:
336
- layout = QFormLayout()
337
- layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
338
- layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
339
- layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
340
- layout.setLabelAlignment(Qt.AlignLeft)
341
- q.setLayout(layout)
285
+ q.setLayout(self.create_tab_layout())
342
286
  stacked = QStackedWidget()
343
287
  stacked.addWidget(q_pyramid)
344
288
  stacked.addWidget(q_depthmap)
@@ -349,158 +293,160 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
349
293
  stacked.setCurrentWidget(q_pyramid)
350
294
  elif text == constants.STACK_ALGO_DEPTH_MAP:
351
295
  stacked.setCurrentWidget(q_depthmap)
296
+
352
297
  change()
353
- if self.expert:
354
- self.add_field(
355
- 'pyramid_min_size', FIELD_INT, 'Minimum size (px)',
356
- required=False, add_to_layout=q_pyramid.layout(),
357
- default=constants.DEFAULT_PY_MIN_SIZE, min_val=2, max_val=256)
358
- self.add_field(
359
- 'pyramid_kernel_size', FIELD_INT, 'Kernel size (px)',
360
- required=False, add_to_layout=q_pyramid.layout(),
361
- default=constants.DEFAULT_PY_KERNEL_SIZE, min_val=3, max_val=21)
362
- self.add_field(
363
- 'pyramid_gen_kernel', FIELD_FLOAT, 'Gen. kernel',
364
- required=False, add_to_layout=q_pyramid.layout(),
365
- default=constants.DEFAULT_PY_GEN_KERNEL,
366
- min_val=0.0, max_val=2.0)
367
- self.add_field(
368
- 'pyramid_float_type', FIELD_COMBO, 'Precision', required=False,
369
- add_to_layout=q_pyramid.layout(),
370
- options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
371
- default=dict(zip(constants.VALID_FLOATS,
372
- self.FLOAT_OPTIONS))[constants.DEFAULT_PY_FLOAT])
373
- mode = self.add_field(
374
- 'pyramid_mode', FIELD_COMBO, 'Mode',
375
- required=False, add_to_layout=q_pyramid.layout(),
376
- options=self.MODE_OPTIONS, values=constants.PY_VALID_MODES,
377
- default=dict(zip(constants.PY_VALID_MODES,
378
- self.MODE_OPTIONS))[constants.DEFAULT_PY_MODE])
379
- memory_limit = self.add_field(
380
- 'pyramid_memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
381
- required=False, add_to_layout=q_pyramid.layout(),
382
- default=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
383
- min_val=1.0, max_val=64.0)
384
- max_threads = self.add_field(
385
- 'pyramid_max_threads', FIELD_INT, 'Max num. of cores',
386
- required=False, add_to_layout=q_pyramid.layout(),
387
- default=constants.DEFAULT_PY_MAX_THREADS,
388
- min_val=1, max_val=64)
389
- tile_size = self.add_field(
390
- 'pyramid_tile_size', FIELD_INT, 'Tile size (px)',
391
- required=False, add_to_layout=q_pyramid.layout(),
392
- default=constants.DEFAULT_PY_TILE_SIZE,
393
- min_val=128, max_val=2048)
394
- n_tiled_layers = self.add_field(
395
- 'pyramid_n_tiled_layers', FIELD_INT, 'Num. tiled layers',
396
- required=False, add_to_layout=q_pyramid.layout(),
397
- default=constants.DEFAULT_PY_N_TILED_LAYERS,
398
- min_val=0, max_val=6)
399
-
400
- def change_mode():
401
- text = mode.currentText()
402
- enabled = text == self.MODE_OPTIONS[2]
403
- tile_size.setEnabled(enabled)
404
- n_tiled_layers.setEnabled(enabled)
405
- memory_limit.setEnabled(text == self.MODE_OPTIONS[0])
406
- max_threads.setEnabled(text != self.MODE_OPTIONS[1])
407
-
408
- mode.currentIndexChanged.connect(change_mode)
409
- change_mode()
410
- self.add_field(
411
- 'depthmap_energy', FIELD_COMBO, 'Energy', required=False,
412
- add_to_layout=q_depthmap.layout(),
298
+ self.add_field_to_layout(
299
+ q_pyramid.layout(), 'pyramid_min_size', FIELD_INT, 'Minimum size (px)',
300
+ expert=True,
301
+ required=False, default=constants.DEFAULT_PY_MIN_SIZE, min_val=2, max_val=256)
302
+ self.add_field_to_layout(
303
+ q_pyramid.layout(), 'pyramid_kernel_size', FIELD_INT, 'Kernel size (px)',
304
+ expert=True,
305
+ required=False, default=constants.DEFAULT_PY_KERNEL_SIZE, min_val=3, max_val=21)
306
+ self.add_field_to_layout(
307
+ q_pyramid.layout(), 'pyramid_gen_kernel', FIELD_FLOAT, 'Gen. kernel',
308
+ expert=True,
309
+ required=False, default=constants.DEFAULT_PY_GEN_KERNEL,
310
+ min_val=0.0, max_val=2.0)
311
+ self.add_field_to_layout(
312
+ q_pyramid.layout(), 'pyramid_float_type', FIELD_COMBO, 'Precision', required=False,
313
+ expert=True,
314
+ options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
315
+ default=dict(zip(constants.VALID_FLOATS,
316
+ self.FLOAT_OPTIONS))[constants.DEFAULT_PY_FLOAT])
317
+ mode = self.add_field_to_layout(
318
+ q_pyramid.layout(), 'pyramid_mode', FIELD_COMBO, 'Mode',
319
+ expert=True,
320
+ required=False, options=self.MODE_OPTIONS, values=constants.PY_VALID_MODES,
321
+ default=dict(zip(constants.PY_VALID_MODES,
322
+ self.MODE_OPTIONS))[constants.DEFAULT_PY_MODE])
323
+ memory_limit = self.add_field_to_layout(
324
+ q_pyramid.layout(), 'pyramid_memory_limit', FIELD_FLOAT,
325
+ 'Memory limit (approx., GBytes)',
326
+ expert=True,
327
+ required=False, default=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
328
+ min_val=1.0, max_val=64.0)
329
+ max_threads = self.add_field_to_layout(
330
+ q_pyramid.layout(), 'pyramid_max_threads', FIELD_INT, 'Max num. of cores',
331
+ expert=True,
332
+ required=False, default=constants.DEFAULT_PY_MAX_THREADS,
333
+ min_val=1, max_val=64)
334
+ tile_size = self.add_field_to_layout(
335
+ q_pyramid.layout(), 'pyramid_tile_size', FIELD_INT, 'Tile size (px)',
336
+ expert=True,
337
+ required=False, default=constants.DEFAULT_PY_TILE_SIZE,
338
+ min_val=128, max_val=2048)
339
+ n_tiled_layers = self.add_field_to_layout(
340
+ q_pyramid.layout(), 'pyramid_n_tiled_layers', FIELD_INT, 'Num. tiled layers',
341
+ expert=True,
342
+ required=False, default=constants.DEFAULT_PY_N_TILED_LAYERS,
343
+ min_val=0, max_val=6)
344
+
345
+ def change_mode():
346
+ text = mode.currentText()
347
+ enabled = text == self.MODE_OPTIONS[2]
348
+ tile_size.setEnabled(enabled)
349
+ n_tiled_layers.setEnabled(enabled)
350
+ memory_limit.setEnabled(text == self.MODE_OPTIONS[0])
351
+ max_threads.setEnabled(text != self.MODE_OPTIONS[1])
352
+
353
+ mode.currentIndexChanged.connect(change_mode)
354
+ change_mode()
355
+
356
+ self.add_field_to_layout(
357
+ q_depthmap.layout(), 'depthmap_energy', FIELD_COMBO, 'Energy', required=False,
413
358
  options=self.ENERGY_OPTIONS, values=constants.VALID_DM_ENERGY,
414
359
  default=dict(zip(constants.VALID_DM_ENERGY,
415
360
  self.ENERGY_OPTIONS))[constants.DEFAULT_DM_ENERGY])
416
- self.add_field(
417
- 'map_type', FIELD_COMBO, 'Map type', required=False,
418
- add_to_layout=q_depthmap.layout(),
361
+ self.add_field_to_layout(
362
+ q_depthmap.layout(), 'map_type', FIELD_COMBO, 'Map type', required=False,
419
363
  options=self.MAP_TYPE_OPTIONS, values=constants.VALID_DM_MAP,
420
364
  default=dict(zip(constants.VALID_DM_MAP,
421
365
  self.MAP_TYPE_OPTIONS))[constants.DEFAULT_DM_MAP])
422
- if self.expert:
423
- self.add_field(
424
- 'depthmap_kernel_size', FIELD_INT, 'Kernel size (px)',
425
- required=False, add_to_layout=q_depthmap.layout(),
426
- default=constants.DEFAULT_DM_KERNEL_SIZE, min_val=3, max_val=21)
427
- self.add_field(
428
- 'depthmap_blur_size', FIELD_INT, 'Blurl size (px)',
429
- required=False, add_to_layout=q_depthmap.layout(),
430
- default=constants.DEFAULT_DM_BLUR_SIZE, min_val=1, max_val=21)
431
- self.add_field(
432
- 'depthmap_smooth_size', FIELD_INT, 'Smooth size (px)',
433
- required=False, add_to_layout=q_depthmap.layout(),
434
- default=constants.DEFAULT_DM_SMOOTH_SIZE, min_val=0, max_val=256)
435
- self.add_field(
436
- 'depthmap_temperature', FIELD_FLOAT, 'Temperature',
437
- required=False, add_to_layout=q_depthmap.layout(),
438
- default=constants.DEFAULT_DM_TEMPERATURE,
439
- min_val=0, max_val=1, step=0.05)
440
- self.add_field(
441
- 'depthmap_levels', FIELD_INT, 'Levels', required=False,
442
- add_to_layout=q_depthmap.layout(),
443
- default=constants.DEFAULT_DM_LEVELS, min_val=2, max_val=6)
444
- self.add_field(
445
- 'depthmap_float_type', FIELD_COMBO, 'Precision', required=False,
446
- add_to_layout=q_depthmap.layout(), options=self.FLOAT_OPTIONS,
447
- values=constants.VALID_FLOATS,
448
- default=dict(zip(constants.VALID_FLOATS,
449
- self.FLOAT_OPTIONS))[constants.DEFAULT_DM_FLOAT])
450
- self.add_row(stacked)
366
+ self.add_field_to_layout(
367
+ q_depthmap.layout(), 'depthmap_kernel_size', FIELD_INT, 'Kernel size (px)',
368
+ expert=True,
369
+ required=False, default=constants.DEFAULT_DM_KERNEL_SIZE, min_val=3, max_val=21)
370
+ self.add_field_to_layout(
371
+ q_depthmap.layout(), 'depthmap_blur_size', FIELD_INT, 'Blurl size (px)',
372
+ expert=True,
373
+ required=False, default=constants.DEFAULT_DM_BLUR_SIZE, min_val=1, max_val=21)
374
+ self.add_field_to_layout(
375
+ q_depthmap.layout(), 'depthmap_smooth_size', FIELD_INT, 'Smooth size (px)',
376
+ expert=True,
377
+ required=False, default=constants.DEFAULT_DM_SMOOTH_SIZE, min_val=0, max_val=256)
378
+ self.add_field_to_layout(
379
+ q_depthmap.layout(), 'depthmap_temperature', FIELD_FLOAT, 'Temperature',
380
+ expert=True,
381
+ required=False, default=constants.DEFAULT_DM_TEMPERATURE,
382
+ min_val=0, max_val=1, step=0.05)
383
+ self.add_field_to_layout(
384
+ q_depthmap.layout(), 'depthmap_levels', FIELD_INT, 'Levels', required=False,
385
+ expert=True,
386
+ default=constants.DEFAULT_DM_LEVELS, min_val=2, max_val=6)
387
+ self.add_field_to_layout(
388
+ q_depthmap.layout(), 'depthmap_float_type', FIELD_COMBO,
389
+ 'Precision', required=False,
390
+ expert=True,
391
+ options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
392
+ default=dict(zip(constants.VALID_FLOATS,
393
+ self.FLOAT_OPTIONS))[constants.DEFAULT_DM_FLOAT])
394
+ layout.addRow(stacked)
451
395
  combo.currentIndexChanged.connect(change)
452
396
 
453
397
 
454
398
  class FocusStackConfigurator(FocusStackBaseConfigurator):
455
399
  def create_form(self, layout, action):
456
400
  super().create_form(layout, action)
457
- if self.expert:
458
- self.add_field(
459
- 'exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
460
- placeholder='relative to working path')
461
- self.add_field(
462
- 'prefix', FIELD_TEXT, 'Ouptut filename prefix', required=False,
463
- default=constants.DEFAULT_STACK_PREFIX,
464
- placeholder=constants.DEFAULT_STACK_PREFIX)
465
- self.add_field(
466
- 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
401
+ self.add_field_to_layout(
402
+ self.general_tab_layout, 'exif_path', FIELD_REL_PATH,
403
+ 'Exif data path', required=False,
404
+ expert=True,
405
+ placeholder='relative to working path')
406
+ self.add_field_to_layout(
407
+ self.general_tab_layout, 'prefix', FIELD_TEXT,
408
+ 'Output filename prefix', required=False,
409
+ expert=True,
410
+ default=constants.DEFAULT_STACK_PREFIX,
411
+ placeholder=constants.DEFAULT_STACK_PREFIX)
412
+ self.add_field_to_layout(
413
+ self.general_tab_layout, 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
467
414
  default=constants.DEFAULT_PLOT_STACK)
468
- super().common_fields(layout)
469
415
 
470
416
 
471
417
  class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
472
418
  def create_form(self, layout, action):
473
419
  super().create_form(layout, action)
474
- self.add_field(
475
- 'frames', FIELD_INT, 'Frames', required=False,
420
+ self.add_field_to_layout(
421
+ self.general_tab_layout, 'frames', FIELD_INT, 'Frames', required=False,
476
422
  default=constants.DEFAULT_FRAMES, min_val=1, max_val=100)
477
- self.add_field(
478
- 'overlap', FIELD_INT, 'Overlapping frames', required=False,
423
+ self.add_field_to_layout(
424
+ self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
479
425
  default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
480
- self.add_field(
481
- 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
426
+ self.add_field_to_layout(
427
+ self.general_tab_layout, 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
482
428
  default=constants.DEFAULT_PLOT_STACK_BUNCH)
483
- super().common_fields(layout)
484
429
 
485
430
 
486
431
  class MultiLayerConfigurator(DefaultActionConfigurator):
487
432
  def create_form(self, layout, action):
488
433
  super().create_form(layout, action)
489
- if self.expert:
490
- self.add_field(
491
- 'working_path', FIELD_ABS_PATH, 'Working path', required=False)
434
+ self.add_field(
435
+ 'working_path', FIELD_ABS_PATH, 'Working path', required=False,
436
+ expert=True)
492
437
  self.add_field(
493
438
  'input_path', FIELD_REL_PATH,
494
439
  f'Input path (separate by {constants.PATH_SEPARATOR})',
495
440
  required=False, multiple_entries=True,
496
441
  placeholder='relative to working path')
497
- if self.expert:
498
- self.add_field(
499
- 'output_path', FIELD_REL_PATH, 'Output path', required=False,
500
- placeholder='relative to working path')
501
- self.add_field(
502
- 'exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
503
- placeholder='relative to working path')
442
+ self.add_field(
443
+ 'output_path', FIELD_REL_PATH, 'Output path', required=False,
444
+ expert=True,
445
+ placeholder='relative to working path')
446
+ self.add_field(
447
+ 'exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
448
+ expert=True,
449
+ placeholder='relative to working path')
504
450
  self.add_field(
505
451
  'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
506
452
  required=False, default=True)
@@ -512,38 +458,45 @@ class MultiLayerConfigurator(DefaultActionConfigurator):
512
458
  class CombinedActionsConfigurator(DefaultActionConfigurator):
513
459
  def create_form(self, layout, action):
514
460
  super().create_form(layout, action)
515
- if self.expert:
516
- self.add_field(
517
- 'working_path', FIELD_ABS_PATH, 'Working path', required=False)
518
- self.add_field(
519
- 'input_path', FIELD_REL_PATH, 'Input path', required=False,
520
- must_exist=True, placeholder='relative to working path')
521
- self.add_field(
522
- 'output_path', FIELD_REL_PATH, 'Output path', required=False,
523
- placeholder='relative to working path')
461
+ self.add_field(
462
+ 'working_path', FIELD_ABS_PATH, 'Working path', required=False,
463
+ expert=True)
464
+ self.add_field(
465
+ 'input_path', FIELD_REL_PATH, 'Input path', required=False,
466
+ expert=True,
467
+ must_exist=True, placeholder='relative to working path')
468
+ self.add_field(
469
+ 'output_path', FIELD_REL_PATH, 'Output path', required=False,
470
+ expert=True,
471
+ placeholder='relative to working path')
524
472
  self.add_field(
525
473
  'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
526
474
  required=False, default=True)
527
- if self.expert:
528
- self.add_field(
529
- 'plot_path', FIELD_REL_PATH, 'Plots path', required=False,
530
- default="plots", placeholder='relative to working path')
531
- self.add_field(
532
- 'resample', FIELD_INT, 'Resample frame stack', required=False,
533
- default=1, min_val=1, max_val=100)
534
- self.add_field(
535
- 'reference_index', FIELD_REF_IDX, 'Reference frame', required=False,
536
- default=0)
537
- self.add_field(
538
- 'step_process', FIELD_BOOL, 'Step process', required=False,
539
- default=True)
540
- self.add_field(
541
- 'max_threads', FIELD_INT, 'Max num. of cores',
542
- required=False, default=constants.DEFAULT_MAX_FWK_THREADS,
543
- min_val=1, max_val=64)
544
- self.add_field(
545
- 'chunk_submit', FIELD_BOOL, 'Submit in chunks',
546
- required=False, default=constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
475
+ self.add_field(
476
+ 'plot_path', FIELD_REL_PATH, 'Plots path', required=False,
477
+ expert=True,
478
+ default="plots", placeholder='relative to working path')
479
+ self.add_field(
480
+ 'resample', FIELD_INT, 'Resample frame stack', required=False,
481
+ expert=True,
482
+ default=1, min_val=1, max_val=100)
483
+ self.add_field(
484
+ 'reference_index', FIELD_REF_IDX, 'Reference frame', required=False,
485
+ expert=True,
486
+ default=0)
487
+ self.add_field(
488
+ 'step_process', FIELD_BOOL, 'Step process', required=False,
489
+ expert=True,
490
+ default=True)
491
+ self.add_field(
492
+ 'max_threads', FIELD_INT, 'Max num. of cores',
493
+ required=False, default=constants.DEFAULT_MAX_FWK_THREADS,
494
+ expert=True,
495
+ min_val=1, max_val=64)
496
+ self.add_field(
497
+ 'chunk_submit', FIELD_BOOL, 'Submit in chunks',
498
+ expert=True,
499
+ required=False, default=constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
547
500
 
548
501
 
549
502
  class MaskNoiseConfigurator(DefaultActionConfigurator):
@@ -554,13 +507,14 @@ class MaskNoiseConfigurator(DefaultActionConfigurator):
554
507
  path_type='file', must_exist=True,
555
508
  default=constants.DEFAULT_NOISE_MAP_FILENAME,
556
509
  placeholder=constants.DEFAULT_NOISE_MAP_FILENAME)
557
- if self.expert:
558
- self.add_field(
559
- 'kernel_size', FIELD_INT, 'Kernel size', required=False,
560
- default=constants.DEFAULT_MN_KERNEL_SIZE, min_val=1, max_val=10)
561
- self.add_field(
562
- 'method', FIELD_COMBO, 'Interpolation method', required=False,
563
- options=['Mean', 'Median'], default='Mean')
510
+ self.add_field(
511
+ 'kernel_size', FIELD_INT, 'Kernel size', required=False,
512
+ expert=True,
513
+ default=constants.DEFAULT_MN_KERNEL_SIZE, min_val=1, max_val=10)
514
+ self.add_field(
515
+ 'method', FIELD_COMBO, 'Interpolation method', required=False,
516
+ expert=True,
517
+ options=['Mean', 'Median'], default='Mean')
564
518
 
565
519
 
566
520
  class SubsampleActionConfigurator(DefaultActionConfigurator):
@@ -569,18 +523,23 @@ class SubsampleActionConfigurator(DefaultActionConfigurator):
569
523
  self.subsample_field = None
570
524
  self.fast_subsampling_field = None
571
525
 
572
- def add_subsample_fields(self):
526
+ def add_subsample_fields(self, add_to_layout=None):
527
+ if add_to_layout is None:
528
+ add_to_layout = self.builder.main_layout
573
529
  self.subsample_field = self.add_field(
574
530
  'subsample', FIELD_COMBO, 'Subsample', required=False,
531
+ expert=True,
575
532
  options=constants.FIELD_SUBSAMPLE_OPTIONS,
576
533
  values=constants.FIELD_SUBSAMPLE_VALUES,
577
- default=constants.FIELD_SUBSAMPLE_DEFAULT)
534
+ default=constants.FIELD_SUBSAMPLE_DEFAULT,
535
+ add_to_layout=add_to_layout)
578
536
  self.fast_subsampling_field = self.add_field(
579
537
  'fast_subsampling', FIELD_BOOL, 'Fast subsampling', required=False,
580
- default=constants.DEFAULT_ALIGN_FAST_SUBSAMPLING)
538
+ expert=True,
539
+ default=constants.DEFAULT_ALIGN_FAST_SUBSAMPLING,
540
+ add_to_layout=add_to_layout)
581
541
 
582
542
  self.subsample_field.currentTextChanged.connect(self.change_subsample)
583
-
584
543
  self.change_subsample()
585
544
 
586
545
  def change_subsample(self):
@@ -602,13 +561,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
602
561
  self.detector_field = None
603
562
  self.descriptor_field = None
604
563
  self.matching_method_field = None
564
+ self.tab_widget = None
565
+ self.current_tab_layout = None
605
566
 
606
567
  def show_info(self, message, timeout=3000):
607
568
  self.info_label.setText(message)
608
- self.info_label.setVisible(True)
609
569
  timer = QTimer(self.info_label)
610
570
  timer.setSingleShot(True)
611
- timer.timeout.connect(self.info_label.hide)
571
+ timer.timeout.connect(lambda: self.info_label.setText(''))
612
572
  timer.start(timeout)
613
573
 
614
574
  def change_match_config(self):
@@ -643,158 +603,187 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
643
603
  self.detector_field = None
644
604
  self.descriptor_field = None
645
605
  self.matching_method_field = None
646
- if self.expert:
647
- self.add_bold_label("Feature identification:")
648
- self.info_label = QLabel()
649
- self.info_label.setStyleSheet("color: orange; font-style: italic;")
650
- self.info_label.setVisible(False)
651
- layout.addRow(self.info_label)
652
- self.detector_field = self.add_field(
653
- 'detector', FIELD_COMBO, 'Detector', required=False,
654
- options=constants.VALID_DETECTORS, default=constants.DEFAULT_DETECTOR)
655
- self.descriptor_field = self.add_field(
656
- 'descriptor', FIELD_COMBO, 'Descriptor', required=False,
657
- options=constants.VALID_DESCRIPTORS, default=constants.DEFAULT_DESCRIPTOR)
658
- self.add_bold_label("Feature matching:")
659
- self.matching_method_field = self.add_field(
660
- 'match_method', FIELD_COMBO, 'Match method', required=False,
661
- options=self.MATCHING_METHOD_OPTIONS, values=constants.VALID_MATCHING_METHODS,
662
- default=constants.DEFAULT_MATCHING_METHOD)
663
- self.detector_field.setToolTip(
664
- "SIFT: Requires SIFT descriptor and K-NN matching\n"
665
- "ORB/AKAZE: Work best with Hamming distance"
666
- )
667
- self.descriptor_field.setToolTip(
668
- "SIFT: Requires K-NN matching\n"
669
- "ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors"
670
- )
671
- self.matching_method_field.setToolTip(
672
- "Automatically selected based on detector/descriptor combination"
673
- )
674
- self.detector_field.currentIndexChanged.connect(self.change_match_config)
675
- self.descriptor_field.currentIndexChanged.connect(self.change_match_config)
676
- self.matching_method_field.currentIndexChanged.connect(self.change_match_config)
677
- self.add_field(
678
- 'flann_idx_kdtree', FIELD_INT, 'Flann idx kdtree', required=False,
679
- default=constants.DEFAULT_FLANN_IDX_KDTREE,
680
- min_val=0, max_val=10)
681
- self.add_field(
682
- 'flann_trees', FIELD_INT, 'Flann trees', required=False,
683
- default=constants.DEFAULT_FLANN_TREES,
684
- min_val=0, max_val=10)
685
- self.add_field(
686
- 'flann_checks', FIELD_INT, 'Flann checks', required=False,
687
- default=constants.DEFAULT_FLANN_CHECKS,
688
- min_val=0, max_val=1000)
689
- self.add_field(
690
- 'threshold', FIELD_FLOAT, 'Threshold', required=False,
691
- default=constants.DEFAULT_ALIGN_THRESHOLD,
692
- min_val=0, max_val=1, step=0.05)
693
- self.add_bold_label("Transform:")
694
- transform = self.add_field(
695
- 'transform', FIELD_COMBO, 'Transform', required=False,
696
- options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
697
- default=constants.DEFAULT_TRANSFORM)
698
- method = self.add_field(
699
- 'align_method', FIELD_COMBO, 'Align method', required=False,
700
- options=self.METHOD_OPTIONS, values=constants.VALID_ALIGN_METHODS,
701
- default=constants.DEFAULT_ALIGN_METHOD)
702
- rans_threshold = self.add_field(
703
- 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
704
- default=constants.DEFAULT_RANS_THRESHOLD, min_val=0, max_val=20, step=0.1)
705
- self.add_field(
706
- 'min_good_matches', FIELD_INT, "Min. good matches", required=False,
707
- default=constants.DEFAULT_ALIGN_MIN_GOOD_MATCHES, min_val=0, max_val=500)
708
-
709
- def change_method():
710
- text = method.currentText()
711
- if text == self.METHOD_OPTIONS[0]:
712
- rans_threshold.setEnabled(True)
713
- elif text == self.METHOD_OPTIONS[1]:
714
- rans_threshold.setEnabled(False)
715
-
716
- method.currentIndexChanged.connect(change_method)
717
- change_method()
718
- self.add_field(
719
- 'align_confidence', FIELD_FLOAT, 'Confidence (%)',
720
- required=False, decimals=1,
721
- default=constants.DEFAULT_ALIGN_CONFIDENCE,
722
- min_val=70.0, max_val=100.0, step=0.1)
723
-
724
- refine_iters = self.add_field(
725
- 'refine_iters', FIELD_INT, 'Refinement iterations (Rigid)', required=False,
726
- default=constants.DEFAULT_REFINE_ITERS, min_val=0, max_val=1000)
727
- max_iters = self.add_field(
728
- 'max_iters', FIELD_INT, 'Max. iterations (Homography)', required=False,
729
- default=constants.DEFAULT_ALIGN_MAX_ITERS, min_val=0, max_val=5000)
730
-
731
- def change_transform():
732
- text = transform.currentText()
733
- if text == self.TRANSFORM_OPTIONS[0]:
734
- refine_iters.setEnabled(True)
735
- max_iters.setEnabled(False)
736
- elif text == self.TRANSFORM_OPTIONS[1]:
737
- refine_iters.setEnabled(False)
738
- max_iters.setEnabled(True)
739
-
740
- transform.currentIndexChanged.connect(change_transform)
741
- change_transform()
742
- self.add_field(
743
- 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
744
- required=False, default=constants.DEFAULT_ALIGN_ABORT_ABNORMAL)
745
- self.add_subsample_fields()
746
- self.add_bold_label("Border:")
747
- self.add_field(
748
- 'border_mode', FIELD_COMBO, 'Border mode', required=False,
749
- options=self.BORDER_MODE_OPTIONS,
750
- values=constants.VALID_BORDER_MODES,
751
- default=constants.DEFAULT_BORDER_MODE)
752
- self.add_field(
753
- 'border_value', FIELD_INT_TUPLE,
754
- 'Border value (if constant)', required=False, size=4,
755
- default=constants.DEFAULT_BORDER_VALUE,
756
- labels=constants.RGBA_LABELS,
757
- min_val=constants.DEFAULT_BORDER_VALUE, max_val=[255] * 4)
758
- self.add_field(
759
- 'border_blur', FIELD_FLOAT, 'Border blur', required=False,
760
- default=constants.DEFAULT_BORDER_BLUR,
761
- min_val=0, max_val=1000, step=1)
762
- self.add_bold_label("Miscellanea:")
763
- if self.expert:
764
- mode = self.add_field(
765
- 'mode', FIELD_COMBO, 'Mode',
766
- required=False, options=self.MODE_OPTIONS, values=constants.ALIGN_VALID_MODES,
767
- default=dict(zip(constants.ALIGN_VALID_MODES,
768
- self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
769
- memory_limit = self.add_field(
770
- 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
771
- required=False, default=constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
772
- min_val=1.0, max_val=64.0)
773
- max_threads = self.add_field(
774
- 'max_threads', FIELD_INT, 'Max num. of cores',
775
- required=False, default=constants.DEFAULT_ALIGN_MAX_THREADS,
776
- min_val=1, max_val=64)
777
- chunk_submit = self.add_field(
778
- 'chunk_submit', FIELD_BOOL, 'Submit in chunks',
779
- required=False, default=constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
780
- bw_matching = self.add_field(
781
- 'bw_matching', FIELD_BOOL, 'Match using black & white',
782
- required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
783
-
784
- def change_mode():
785
- text = mode.currentText()
786
- enabled = text != self.MODE_OPTIONS[1]
787
- memory_limit.setEnabled(enabled)
788
- max_threads.setEnabled(enabled)
789
- chunk_submit.setEnabled(enabled)
790
- bw_matching.setEnabled(enabled)
791
-
792
- mode.currentIndexChanged.connect(change_mode)
793
- self.add_field(
794
- 'plot_summary', FIELD_BOOL, 'Plot summary',
606
+ self.tab_widget = self.create_tab_widget(layout)
607
+ feature_layout = self.add_tab(self.tab_widget, "Feature extraction")
608
+ self.create_feature_tab(feature_layout)
609
+ transform_layout = self.add_tab(self.tab_widget, "Transform")
610
+ self.create_transform_tab(transform_layout)
611
+ border_layout = self.add_tab(self.tab_widget, "Border")
612
+ self.create_border_tab(border_layout)
613
+ misc_layout = self.add_tab(self.tab_widget, "Miscellanea")
614
+ self.create_miscellanea_tab(misc_layout)
615
+
616
+ def create_feature_tab(self, layout):
617
+ self.add_bold_label_to_layout(layout, "Feature identification:")
618
+ self.detector_field = self.add_field_to_layout(
619
+ layout, 'detector', FIELD_COMBO, 'Detector', required=False,
620
+ options=constants.VALID_DETECTORS, default=constants.DEFAULT_DETECTOR)
621
+ self.descriptor_field = self.add_field_to_layout(
622
+ layout, 'descriptor', FIELD_COMBO, 'Descriptor', required=False,
623
+ options=constants.VALID_DESCRIPTORS, default=constants.DEFAULT_DESCRIPTOR)
624
+ self.detector_field.setToolTip(
625
+ "SIFT: Requires SIFT descriptor and K-NN matching\n"
626
+ "ORB/AKAZE: Work best with Hamming distance"
627
+ )
628
+ self.descriptor_field.setToolTip(
629
+ "SIFT: Requires K-NN matching\n"
630
+ "ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors"
631
+ )
632
+ self.detector_field.currentIndexChanged.connect(self.change_match_config)
633
+ self.descriptor_field.currentIndexChanged.connect(self.change_match_config)
634
+ self.info_label = QLabel()
635
+ self.info_label.setStyleSheet("color: orange; font-style: italic;")
636
+ layout.addRow(self.info_label)
637
+ self.add_bold_label_to_layout(layout, "Feature matching:")
638
+ self.matching_method_field = self.add_field_to_layout(
639
+ layout, 'match_method', FIELD_COMBO, 'Match method', required=False,
640
+ options=self.MATCHING_METHOD_OPTIONS, values=constants.VALID_MATCHING_METHODS,
641
+ default=constants.DEFAULT_MATCHING_METHOD)
642
+ self.matching_method_field.setToolTip(
643
+ "Automatically selected based on detector/descriptor combination"
644
+ )
645
+ self.matching_method_field.currentIndexChanged.connect(self.change_match_config)
646
+ self.add_field_to_layout(
647
+ layout, 'flann_idx_kdtree', FIELD_INT, 'Flann idx kdtree', required=False,
648
+ expert=True,
649
+ default=constants.DEFAULT_FLANN_IDX_KDTREE,
650
+ min_val=0, max_val=10)
651
+ self.add_field_to_layout(
652
+ layout, 'flann_trees', FIELD_INT, 'Flann trees', required=False,
653
+ expert=True,
654
+ default=constants.DEFAULT_FLANN_TREES,
655
+ min_val=0, max_val=10)
656
+ self.add_field_to_layout(
657
+ layout, 'flann_checks', FIELD_INT, 'Flann checks', required=False,
658
+ expert=True,
659
+ default=constants.DEFAULT_FLANN_CHECKS,
660
+ min_val=0, max_val=1000)
661
+ self.add_field_to_layout(
662
+ layout, 'threshold', FIELD_FLOAT, 'Threshold', required=False,
663
+ expert=True,
664
+ default=constants.DEFAULT_ALIGN_THRESHOLD,
665
+ min_val=0, max_val=1, step=0.05)
666
+ self.add_subsample_fields(add_to_layout=layout)
667
+
668
+ def create_transform_tab(self, layout):
669
+ self.add_bold_label_to_layout(layout, "Transform:")
670
+ transform = self.add_field_to_layout(
671
+ layout, 'transform', FIELD_COMBO, 'Transform', required=False,
672
+ options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
673
+ default=constants.DEFAULT_TRANSFORM)
674
+ method = self.add_field_to_layout(
675
+ layout, 'align_method', FIELD_COMBO, 'Align method', required=False,
676
+ options=self.METHOD_OPTIONS, values=constants.VALID_ALIGN_METHODS,
677
+ default=constants.DEFAULT_ALIGN_METHOD)
678
+ rans_threshold = self.add_field_to_layout(
679
+ layout, 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
680
+ expert=True,
681
+ default=constants.DEFAULT_RANS_THRESHOLD, min_val=0, max_val=20, step=0.1)
682
+ self.add_field_to_layout(
683
+ layout, 'min_good_matches', FIELD_INT, "Min. good matches", required=False,
684
+ expert=True,
685
+ default=constants.DEFAULT_ALIGN_MIN_GOOD_MATCHES, min_val=0, max_val=500)
686
+
687
+ def change_method():
688
+ text = method.currentText()
689
+ if text == self.METHOD_OPTIONS[0]:
690
+ rans_threshold.setEnabled(True)
691
+ elif text == self.METHOD_OPTIONS[1]:
692
+ rans_threshold.setEnabled(False)
693
+
694
+ method.currentIndexChanged.connect(change_method)
695
+ change_method()
696
+ self.add_field_to_layout(
697
+ layout, 'align_confidence', FIELD_FLOAT, 'Confidence (%)',
698
+ required=False, decimals=1,
699
+ expert=True,
700
+ default=constants.DEFAULT_ALIGN_CONFIDENCE,
701
+ min_val=70.0, max_val=100.0, step=0.1)
702
+ refine_iters = self.add_field_to_layout(
703
+ layout, 'refine_iters', FIELD_INT, 'Refinement iterations (Rigid)', required=False,
704
+ expert=True,
705
+ default=constants.DEFAULT_REFINE_ITERS, min_val=0, max_val=1000)
706
+ max_iters = self.add_field_to_layout(
707
+ layout, 'max_iters', FIELD_INT, 'Max. iterations (Homography)', required=False,
708
+ expert=True,
709
+ default=constants.DEFAULT_ALIGN_MAX_ITERS, min_val=0, max_val=5000)
710
+
711
+ def change_transform():
712
+ text = transform.currentText()
713
+ if text == self.TRANSFORM_OPTIONS[0]:
714
+ refine_iters.setEnabled(True)
715
+ max_iters.setEnabled(False)
716
+ elif text == self.TRANSFORM_OPTIONS[1]:
717
+ refine_iters.setEnabled(False)
718
+ max_iters.setEnabled(True)
719
+
720
+ transform.currentIndexChanged.connect(change_transform)
721
+ change_transform()
722
+ self.add_field_to_layout(
723
+ layout, 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
724
+ expert=True,
725
+ required=False, default=constants.DEFAULT_ALIGN_ABORT_ABNORMAL)
726
+
727
+ def create_border_tab(self, layout):
728
+ self.add_bold_label_to_layout(layout, "Border:")
729
+ self.add_field_to_layout(
730
+ layout, 'border_mode', FIELD_COMBO, 'Border mode', required=False,
731
+ options=self.BORDER_MODE_OPTIONS,
732
+ values=constants.VALID_BORDER_MODES,
733
+ default=constants.DEFAULT_BORDER_MODE)
734
+ self.add_field_to_layout(
735
+ layout, 'border_value', FIELD_INT_TUPLE,
736
+ 'Border value (if constant)', required=False, size=4,
737
+ expert=True,
738
+ default=constants.DEFAULT_BORDER_VALUE,
739
+ labels=constants.RGBA_LABELS,
740
+ min_val=constants.DEFAULT_BORDER_VALUE, max_val=[255] * 4)
741
+ self.add_field_to_layout(
742
+ layout, 'border_blur', FIELD_FLOAT, 'Border blur', required=False,
743
+ expert=True,
744
+ default=constants.DEFAULT_BORDER_BLUR,
745
+ min_val=0, max_val=1000, step=1)
746
+
747
+ def create_miscellanea_tab(self, layout):
748
+ self.add_bold_label_to_layout(layout, "Miscellanea:")
749
+ mode = self.add_field_to_layout(
750
+ layout, 'mode', FIELD_COMBO, 'Mode',
751
+ required=False, options=self.MODE_OPTIONS, values=constants.ALIGN_VALID_MODES,
752
+ default=dict(zip(constants.ALIGN_VALID_MODES,
753
+ self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
754
+ memory_limit = self.add_field_to_layout(
755
+ layout, 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
756
+ required=False, default=constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
757
+ min_val=1.0, max_val=64.0)
758
+ max_threads = self.add_field_to_layout(
759
+ layout, 'max_threads', FIELD_INT, 'Max num. of cores',
760
+ required=False, default=constants.DEFAULT_ALIGN_MAX_THREADS,
761
+ min_val=1, max_val=64)
762
+ chunk_submit = self.add_field_to_layout(
763
+ layout, 'chunk_submit', FIELD_BOOL, 'Submit in chunks',
764
+ expert=True,
765
+ required=False, default=constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
766
+ bw_matching = self.add_field_to_layout(
767
+ layout, 'bw_matching', FIELD_BOOL, 'Match using black & white',
768
+ expert=True,
769
+ required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
770
+
771
+ def change_mode():
772
+ text = mode.currentText()
773
+ enabled = text != self.MODE_OPTIONS[1]
774
+ memory_limit.setEnabled(enabled)
775
+ max_threads.setEnabled(enabled)
776
+ chunk_submit.setEnabled(enabled)
777
+ bw_matching.setEnabled(enabled)
778
+
779
+ mode.currentIndexChanged.connect(change_mode)
780
+
781
+ self.add_field_to_layout(
782
+ layout, 'plot_summary', FIELD_BOOL, 'Plot summary',
795
783
  required=False, default=False)
796
- self.add_field(
797
- 'plot_matches', FIELD_BOOL, 'Plot matches',
784
+
785
+ self.add_field_to_layout(
786
+ layout, 'plot_matches', FIELD_BOOL, 'Plot matches',
798
787
  required=False, default=False)
799
788
 
800
789
  def update_params(self, params):
@@ -821,16 +810,17 @@ class BalanceFramesConfigurator(SubsampleActionConfigurator):
821
810
 
822
811
  def create_form(self, layout, action):
823
812
  super().create_form(layout, action)
824
- if self.expert:
825
- self.add_field(
826
- 'mask_size', FIELD_FLOAT, 'Mask size', required=False,
827
- default=0, min_val=0, max_val=5, step=0.1)
828
- self.add_field(
829
- 'intensity_interval', FIELD_INT_TUPLE, 'Intensity range',
830
- required=False, size=2,
831
- default=[v for k, v in constants.DEFAULT_INTENSITY_INTERVAL.items()],
832
- labels=['min', 'max'], min_val=[-1] * 2, max_val=[65536] * 2)
833
- self.add_subsample_fields()
813
+ self.add_field(
814
+ 'mask_size', FIELD_FLOAT, 'Mask size', required=False,
815
+ expert=True,
816
+ default=0, min_val=0, max_val=5, step=0.1)
817
+ self.add_field(
818
+ 'intensity_interval', FIELD_INT_TUPLE, 'Intensity range',
819
+ required=False, size=2,
820
+ expert=True,
821
+ default=[v for k, v in constants.DEFAULT_INTENSITY_INTERVAL.items()],
822
+ labels=['min', 'max'], min_val=[-1] * 2, max_val=[65536] * 2)
823
+ self.add_subsample_fields()
834
824
  self.add_field(
835
825
  'corr_map', FIELD_COMBO, 'Correction map', required=False,
836
826
  options=self.CORRECTION_MAP_OPTIONS, values=constants.VALID_BALANCE,
@@ -851,15 +841,16 @@ class BalanceFramesConfigurator(SubsampleActionConfigurator):
851
841
  class VignettingConfigurator(SubsampleActionConfigurator):
852
842
  def create_form(self, layout, action):
853
843
  super().create_form(layout, action)
854
- if self.expert:
855
- self.add_field(
856
- 'r_steps', FIELD_INT, 'Radial steps', required=False,
857
- default=constants.DEFAULT_R_STEPS, min_val=1, max_val=1000)
858
- self.add_field(
859
- 'black_threshold', FIELD_INT, 'Black intensity threshold',
860
- required=False, default=constants.DEFAULT_BLACK_THRESHOLD,
861
- min_val=0, max_val=1000)
862
- self.add_subsample_fields()
844
+ self.add_field(
845
+ 'r_steps', FIELD_INT, 'Radial steps', required=False,
846
+ expert=True,
847
+ default=constants.DEFAULT_R_STEPS, min_val=1, max_val=1000)
848
+ self.add_field(
849
+ 'black_threshold', FIELD_INT, 'Black intensity threshold',
850
+ expert=True,
851
+ required=False, default=constants.DEFAULT_BLACK_THRESHOLD,
852
+ min_val=0, max_val=1000)
853
+ self.add_subsample_fields()
863
854
  self.add_field(
864
855
  'max_correction', FIELD_FLOAT, 'Max. correction', required=False,
865
856
  default=constants.DEFAULT_MAX_CORRECTION,