shinestacker 1.2.1__py3-none-any.whl → 1.3.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (46) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +152 -112
  3. shinestacker/algorithms/align_auto.py +76 -0
  4. shinestacker/algorithms/align_parallel.py +336 -0
  5. shinestacker/algorithms/balance.py +3 -1
  6. shinestacker/algorithms/base_stack_algo.py +25 -22
  7. shinestacker/algorithms/depth_map.py +9 -14
  8. shinestacker/algorithms/multilayer.py +8 -8
  9. shinestacker/algorithms/noise_detection.py +10 -10
  10. shinestacker/algorithms/pyramid.py +10 -24
  11. shinestacker/algorithms/pyramid_auto.py +21 -24
  12. shinestacker/algorithms/pyramid_tiles.py +31 -25
  13. shinestacker/algorithms/stack.py +21 -17
  14. shinestacker/algorithms/stack_framework.py +98 -47
  15. shinestacker/algorithms/utils.py +16 -0
  16. shinestacker/algorithms/vignetting.py +13 -10
  17. shinestacker/app/gui_utils.py +10 -0
  18. shinestacker/app/main.py +10 -4
  19. shinestacker/app/project.py +3 -1
  20. shinestacker/app/retouch.py +3 -1
  21. shinestacker/config/constants.py +60 -25
  22. shinestacker/config/gui_constants.py +1 -1
  23. shinestacker/core/core_utils.py +4 -0
  24. shinestacker/core/framework.py +104 -23
  25. shinestacker/gui/action_config.py +4 -5
  26. shinestacker/gui/action_config_dialog.py +409 -239
  27. shinestacker/gui/base_form_dialog.py +2 -2
  28. shinestacker/gui/colors.py +1 -0
  29. shinestacker/gui/folder_file_selection.py +106 -0
  30. shinestacker/gui/gui_run.py +12 -10
  31. shinestacker/gui/main_window.py +10 -5
  32. shinestacker/gui/new_project.py +171 -73
  33. shinestacker/gui/project_controller.py +10 -6
  34. shinestacker/gui/project_converter.py +4 -2
  35. shinestacker/gui/project_editor.py +40 -28
  36. shinestacker/gui/select_path_widget.py +1 -1
  37. shinestacker/gui/sys_mon.py +97 -0
  38. shinestacker/gui/time_progress_bar.py +4 -3
  39. shinestacker/retouch/exif_data.py +1 -1
  40. shinestacker/retouch/image_editor_ui.py +2 -0
  41. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/METADATA +6 -6
  42. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/RECORD +46 -42
  43. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/WHEEL +0 -0
  44. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/entry_points.txt +0 -0
  45. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/licenses/LICENSE +0 -0
  46. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,9 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0915, R0912
2
- # pylint: disable=E0606, W0718, R1702, W0102, W0221
2
+ # pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914
3
+ import os
3
4
  import traceback
4
- from typing import Dict, Any
5
- from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QLabel, QScrollArea,
6
- QMessageBox, QStackedWidget, QFormLayout, QDialog)
5
+ from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QLabel, QScrollArea, QSizePolicy,
6
+ QMessageBox, QStackedWidget, QFormLayout, QDialog, QTabWidget)
7
7
  from PySide6.QtCore import Qt, QTimer
8
8
  from .. config.constants import constants
9
9
  from .. algorithms.align import validate_align_config
@@ -14,6 +14,7 @@ from . action_config import (
14
14
  FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
15
15
  FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO, FIELD_REF_IDX
16
16
  )
17
+ from .folder_file_selection import FolderFileSelectionWidget
17
18
 
18
19
 
19
20
  class ActionConfigDialog(QDialog):
@@ -104,8 +105,6 @@ class ActionConfigDialog(QDialog):
104
105
  if self.configurator.update_params(self.action.params):
105
106
  self.parent().mark_as_modified(True, "Modify Configuration")
106
107
  super().accept()
107
- else:
108
- self.parent().project_editor.pop_undo()
109
108
 
110
109
  def reset_to_defaults(self):
111
110
  builder = self.configurator.get_builder()
@@ -124,7 +123,7 @@ class NoNameActionConfigurator(ActionConfigurator):
124
123
  def get_builder(self):
125
124
  return self.builder
126
125
 
127
- def update_params(self, params: Dict[str, Any]) -> bool:
126
+ def update_params(self, params):
128
127
  return self.builder.update_params(params)
129
128
 
130
129
  def add_bold_label(self, label):
@@ -139,6 +138,49 @@ class NoNameActionConfigurator(ActionConfigurator):
139
138
  required=False, add_to_layout=None, **kwargs):
140
139
  return self.builder.add_field(tag, field_type, label, required, add_to_layout, **kwargs)
141
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
+ def create_tab_widget(self, layout):
160
+ tab_widget = QTabWidget()
161
+ layout.addRow(tab_widget)
162
+ return tab_widget
163
+
164
+ def add_tab(self, tab_widget, title):
165
+ tab = QWidget()
166
+ tab_layout = QFormLayout()
167
+ tab_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
168
+ tab_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
169
+ tab_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
170
+ tab_layout.setLabelAlignment(Qt.AlignLeft)
171
+ tab.setLayout(tab_layout)
172
+ tab_widget.addTab(tab, title)
173
+ return tab_layout
174
+
175
+ def add_field_to_layout(self, layout, tag, field_type, label, required=False, **kwargs):
176
+ return self.add_field(tag, field_type, label, required, add_to_layout=layout, **kwargs)
177
+
178
+ def add_bold_label_to_layout(self, layout, label):
179
+ label_widget = QLabel(label)
180
+ label_widget.setStyleSheet("font-weight: bold")
181
+ layout.addRow(label_widget)
182
+ return label_widget
183
+
142
184
 
143
185
  class DefaultActionConfigurator(NoNameActionConfigurator):
144
186
  def create_form(self, layout, action, tag='Action'):
@@ -148,13 +190,85 @@ class DefaultActionConfigurator(NoNameActionConfigurator):
148
190
 
149
191
 
150
192
  class JobConfigurator(DefaultActionConfigurator):
193
+ def __init__(self, expert, current_wd):
194
+ super().__init__(expert, current_wd)
195
+ self.working_path_label = None
196
+ self.input_path_label = None
197
+ self.frames_label = None
198
+ self.input_widget = None
199
+
151
200
  def create_form(self, layout, action):
152
201
  super().create_form(layout, action, "Job")
153
- self.add_field(
154
- 'working_path', FIELD_ABS_PATH, 'Working path', required=True)
155
- self.add_field(
156
- 'input_path', FIELD_REL_PATH, 'Input path', required=False,
157
- must_exist=True, placeholder='relative to working path')
202
+ self.input_widget = FolderFileSelectionWidget()
203
+ self.frames_label = QLabel("0")
204
+ working_path = action.params.get('working_path', '')
205
+ input_path = action.params.get('input_path', '')
206
+ input_filepaths = action.params.get('input_filepaths', [])
207
+ if isinstance(input_filepaths, str) and input_filepaths:
208
+ input_filepaths = input_filepaths.split(constants.PATH_SEPARATOR)
209
+ self.working_path_label = QLabel(working_path or "Not set")
210
+ self.input_path_label = QLabel(input_path or "Not set")
211
+ self.input_widget.path_edit.setText('')
212
+ if input_filepaths:
213
+ self.input_widget.files_mode_radio.setChecked(True)
214
+ else:
215
+ self.input_widget.folder_mode_radio.setChecked(False)
216
+ self.input_widget.text_changed_connect(self.update_paths_and_frames)
217
+ self.input_widget.folder_mode_radio.toggled.connect(self.update_paths_and_frames)
218
+ self.input_widget.files_mode_radio.toggled.connect(self.update_paths_and_frames)
219
+ self.add_bold_label("Input Selection:")
220
+ self.add_row(self.input_widget)
221
+ self.add_labelled_row("Number of frames: ", self.frames_label)
222
+ self.add_bold_label("Derived Paths:")
223
+ self.add_labelled_row("Working path: ", self.working_path_label)
224
+ self.add_labelled_row("Input path:", self.input_path_label)
225
+ self.set_paths_and_frames(working_path, input_path, input_filepaths)
226
+
227
+ def update_frames_count(self):
228
+ if self.input_widget.get_selection_mode() == 'files':
229
+ count = self.input_widget.num_selected_files()
230
+ else:
231
+ count = self.count_image_files(self.input_widget.get_path())
232
+ self.frames_label.setText(str(count))
233
+
234
+ def set_paths_and_frames(self, working_path, input_path, input_filepaths):
235
+ self.input_path_label.setText(input_path or "Not set")
236
+ self.working_path_label.setText(working_path or "Not set")
237
+ self.frames_label.setText(str(len(input_filepaths)))
238
+
239
+ def update_paths_and_frames(self, ):
240
+ input_fullpath = self.input_widget.get_path()
241
+ input_path = os.path.basename(os.path.normpath(input_fullpath)) if input_fullpath else ""
242
+ working_path = os.path.dirname(input_fullpath) if input_fullpath else ""
243
+ self.input_path_label.setText(input_path or "Not set")
244
+ self.working_path_label.setText(working_path or "Not set")
245
+ self.update_frames_count()
246
+
247
+ def count_image_files(self, path):
248
+ if not path or not os.path.isdir(path):
249
+ return 0
250
+ count = 0
251
+ for filename in os.listdir(path):
252
+ if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff')):
253
+ count += 1
254
+ return count
255
+
256
+ def update_params(self, params):
257
+ if not super().update_params(params):
258
+ return False
259
+ selection_mode = self.input_widget.get_selection_mode()
260
+ selected_files = self.input_widget.get_selected_files()
261
+ if selection_mode == 'files' and selected_files:
262
+ input_full_path = os.path.dirname(selected_files[0])
263
+ params['input_filepaths'] = self.input_widget.get_selected_filenames()
264
+ else:
265
+ input_full_path = self.input_widget.get_path()
266
+ params['input_filepaths'] = []
267
+ input_path = os.path.basename(os.path.normpath(input_full_path)) if input_full_path else ""
268
+ working_path = os.path.dirname(input_full_path) if input_full_path else ""
269
+ params['input_path'] = input_path
270
+ params['working_path'] = working_path
271
+ return True
158
272
 
159
273
 
160
274
  class NoiseDetectionConfigurator(DefaultActionConfigurator):
@@ -205,38 +319,48 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
205
319
  FLOAT_OPTIONS = ['float 32 bits', 'float 64 bits']
206
320
  MODE_OPTIONS = ['Auto', 'All in memory', 'Tiled I/O buffered']
207
321
 
322
+ def __init__(self, expert, current_wd):
323
+ super().__init__(expert, current_wd)
324
+ self.tab_widget = None
325
+ self.general_tab_layout = None
326
+ self.algorithm_tab_layout = None
327
+
208
328
  def create_form(self, layout, action):
209
329
  super().create_form(layout, action)
330
+ self.tab_widget = self.create_tab_widget(layout)
331
+ self.general_tab_layout = self.add_tab(self.tab_widget, "General Parameters")
332
+ self.create_general_tab(self.general_tab_layout)
333
+ self.algorithm_tab_layout = self.add_tab(self.tab_widget, "Stacking Algorithm")
334
+ self.create_algorithm_tab(self.algorithm_tab_layout)
335
+
336
+ def create_general_tab(self, layout):
210
337
  if self.expert:
211
- self.add_field(
212
- 'working_path', FIELD_ABS_PATH, 'Working path', required=False)
213
- self.add_field(
214
- 'input_path', FIELD_REL_PATH, 'Input path', required=False,
338
+ self.add_field_to_layout(
339
+ layout, 'working_path', FIELD_ABS_PATH, 'Working path', required=False)
340
+ self.add_field_to_layout(
341
+ layout, 'input_path', FIELD_REL_PATH, 'Input path', required=False,
215
342
  placeholder='relative to working path')
216
- self.add_field(
217
- 'output_path', FIELD_REL_PATH, 'Output path', required=False,
343
+ self.add_field_to_layout(
344
+ layout, 'output_path', FIELD_REL_PATH, 'Output path', required=False,
218
345
  placeholder='relative to working path')
219
- self.add_field(
220
- 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
346
+ self.add_field_to_layout(
347
+ layout, 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
221
348
  required=False, default=True)
222
349
 
223
- def common_fields(self, layout):
224
- self.add_field(
225
- 'denoise_amount', FIELD_FLOAT, 'Denoise', required=False,
226
- default=0, min_val=0, max_val=10)
227
- self.add_bold_label("Stacking algorithm:")
228
- combo = self.add_field(
229
- 'stacker', FIELD_COMBO, 'Stacking algorithm', required=True,
350
+ def create_algorithm_tab(self, layout):
351
+ self.add_bold_label_to_layout(layout, "Stacking algorithm:")
352
+ combo = self.add_field_to_layout(
353
+ layout, 'stacker', FIELD_COMBO, 'Stacking algorithm', required=True,
230
354
  options=constants.STACK_ALGO_OPTIONS,
231
355
  default=constants.STACK_ALGO_DEFAULT)
232
356
  q_pyramid, q_depthmap = QWidget(), QWidget()
233
357
  for q in [q_pyramid, q_depthmap]:
234
- layout = QFormLayout()
235
- layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
236
- layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
237
- layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
238
- layout.setLabelAlignment(Qt.AlignLeft)
239
- q.setLayout(layout)
358
+ tab_layout = QFormLayout()
359
+ tab_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
360
+ tab_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
361
+ tab_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
362
+ tab_layout.setLabelAlignment(Qt.AlignLeft)
363
+ q.setLayout(tab_layout)
240
364
  stacked = QStackedWidget()
241
365
  stacked.addWidget(q_pyramid)
242
366
  stacked.addWidget(q_depthmap)
@@ -247,52 +371,45 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
247
371
  stacked.setCurrentWidget(q_pyramid)
248
372
  elif text == constants.STACK_ALGO_DEPTH_MAP:
249
373
  stacked.setCurrentWidget(q_depthmap)
374
+
250
375
  change()
251
376
  if self.expert:
252
- self.add_field(
253
- 'pyramid_min_size', FIELD_INT, 'Minimum size (px)',
254
- required=False, add_to_layout=q_pyramid.layout(),
255
- default=constants.DEFAULT_PY_MIN_SIZE, min_val=2, max_val=256)
256
- self.add_field(
257
- 'pyramid_kernel_size', FIELD_INT, 'Kernel size (px)',
258
- required=False, add_to_layout=q_pyramid.layout(),
259
- default=constants.DEFAULT_PY_KERNEL_SIZE, min_val=3, max_val=21)
260
- self.add_field(
261
- 'pyramid_gen_kernel', FIELD_FLOAT, 'Gen. kernel',
262
- required=False, add_to_layout=q_pyramid.layout(),
263
- default=constants.DEFAULT_PY_GEN_KERNEL,
377
+ self.add_field_to_layout(
378
+ q_pyramid.layout(), 'pyramid_min_size', FIELD_INT, 'Minimum size (px)',
379
+ required=False, default=constants.DEFAULT_PY_MIN_SIZE, min_val=2, max_val=256)
380
+ self.add_field_to_layout(
381
+ q_pyramid.layout(), 'pyramid_kernel_size', FIELD_INT, 'Kernel size (px)',
382
+ required=False, default=constants.DEFAULT_PY_KERNEL_SIZE, min_val=3, max_val=21)
383
+ self.add_field_to_layout(
384
+ q_pyramid.layout(), 'pyramid_gen_kernel', FIELD_FLOAT, 'Gen. kernel',
385
+ required=False, default=constants.DEFAULT_PY_GEN_KERNEL,
264
386
  min_val=0.0, max_val=2.0)
265
- self.add_field(
266
- 'pyramid_float_type', FIELD_COMBO, 'Precision', required=False,
267
- add_to_layout=q_pyramid.layout(),
387
+ self.add_field_to_layout(
388
+ q_pyramid.layout(), 'pyramid_float_type', FIELD_COMBO, 'Precision', required=False,
268
389
  options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
269
390
  default=dict(zip(constants.VALID_FLOATS,
270
391
  self.FLOAT_OPTIONS))[constants.DEFAULT_PY_FLOAT])
271
- mode = self.add_field(
272
- 'pyramid_mode', FIELD_COMBO, 'Mode',
273
- required=False, add_to_layout=q_pyramid.layout(),
274
- options=self.MODE_OPTIONS, values=constants.PY_VALID_MODES,
392
+ mode = self.add_field_to_layout(
393
+ q_pyramid.layout(), 'pyramid_mode', FIELD_COMBO, 'Mode',
394
+ required=False, options=self.MODE_OPTIONS, values=constants.PY_VALID_MODES,
275
395
  default=dict(zip(constants.PY_VALID_MODES,
276
396
  self.MODE_OPTIONS))[constants.DEFAULT_PY_MODE])
277
- memory_limit = self.add_field(
278
- 'pyramid_memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
279
- required=False, add_to_layout=q_pyramid.layout(),
280
- default=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
397
+ memory_limit = self.add_field_to_layout(
398
+ q_pyramid.layout(), 'pyramid_memory_limit', FIELD_FLOAT,
399
+ 'Memory limit (approx., GBytes)',
400
+ required=False, default=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
281
401
  min_val=1.0, max_val=64.0)
282
- max_threads = self.add_field(
283
- 'pyramid_max_threads', FIELD_INT, 'Max num. of cores',
284
- required=False, add_to_layout=q_pyramid.layout(),
285
- default=constants.DEFAULT_PY_MAX_THREADS,
402
+ max_threads = self.add_field_to_layout(
403
+ q_pyramid.layout(), 'pyramid_max_threads', FIELD_INT, 'Max num. of cores',
404
+ required=False, default=constants.DEFAULT_PY_MAX_THREADS,
286
405
  min_val=1, max_val=64)
287
- tile_size = self.add_field(
288
- 'pyramid_tile_size', FIELD_INT, 'Tile size (px)',
289
- required=False, add_to_layout=q_pyramid.layout(),
290
- default=constants.DEFAULT_PY_TILE_SIZE,
406
+ tile_size = self.add_field_to_layout(
407
+ q_pyramid.layout(), 'pyramid_tile_size', FIELD_INT, 'Tile size (px)',
408
+ required=False, default=constants.DEFAULT_PY_TILE_SIZE,
291
409
  min_val=128, max_val=2048)
292
- n_tiled_layers = self.add_field(
293
- 'pyramid_n_tiled_layers', FIELD_INT, 'Num. tiled layers',
294
- required=False, add_to_layout=q_pyramid.layout(),
295
- default=constants.DEFAULT_PY_N_TILED_LAYERS,
410
+ n_tiled_layers = self.add_field_to_layout(
411
+ q_pyramid.layout(), 'pyramid_n_tiled_layers', FIELD_INT, 'Num. tiled layers',
412
+ required=False, default=constants.DEFAULT_PY_N_TILED_LAYERS,
296
413
  min_val=0, max_val=6)
297
414
 
298
415
  def change_mode():
@@ -305,47 +422,41 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
305
422
 
306
423
  mode.currentIndexChanged.connect(change_mode)
307
424
  change_mode()
308
- self.add_field(
309
- 'depthmap_energy', FIELD_COMBO, 'Energy', required=False,
310
- add_to_layout=q_depthmap.layout(),
425
+
426
+ self.add_field_to_layout(
427
+ q_depthmap.layout(), 'depthmap_energy', FIELD_COMBO, 'Energy', required=False,
311
428
  options=self.ENERGY_OPTIONS, values=constants.VALID_DM_ENERGY,
312
429
  default=dict(zip(constants.VALID_DM_ENERGY,
313
430
  self.ENERGY_OPTIONS))[constants.DEFAULT_DM_ENERGY])
314
- self.add_field(
315
- 'map_type', FIELD_COMBO, 'Map type', required=False,
316
- add_to_layout=q_depthmap.layout(),
431
+ self.add_field_to_layout(
432
+ q_depthmap.layout(), 'map_type', FIELD_COMBO, 'Map type', required=False,
317
433
  options=self.MAP_TYPE_OPTIONS, values=constants.VALID_DM_MAP,
318
434
  default=dict(zip(constants.VALID_DM_MAP,
319
435
  self.MAP_TYPE_OPTIONS))[constants.DEFAULT_DM_MAP])
320
436
  if self.expert:
321
- self.add_field(
322
- 'depthmap_kernel_size', FIELD_INT, 'Kernel size (px)',
323
- required=False, add_to_layout=q_depthmap.layout(),
324
- default=constants.DEFAULT_DM_KERNEL_SIZE, min_val=3, max_val=21)
325
- self.add_field(
326
- 'depthmap_blur_size', FIELD_INT, 'Blurl size (px)',
327
- required=False, add_to_layout=q_depthmap.layout(),
328
- default=constants.DEFAULT_DM_BLUR_SIZE, min_val=1, max_val=21)
329
- self.add_field(
330
- 'depthmap_smooth_size', FIELD_INT, 'Smooth size (px)',
331
- required=False, add_to_layout=q_depthmap.layout(),
332
- default=constants.DEFAULT_DM_SMOOTH_SIZE, min_val=0, max_val=256)
333
- self.add_field(
334
- 'depthmap_temperature', FIELD_FLOAT, 'Temperature',
335
- required=False, add_to_layout=q_depthmap.layout(),
336
- default=constants.DEFAULT_DM_TEMPERATURE,
437
+ self.add_field_to_layout(
438
+ q_depthmap.layout(), 'depthmap_kernel_size', FIELD_INT, 'Kernel size (px)',
439
+ required=False, default=constants.DEFAULT_DM_KERNEL_SIZE, min_val=3, max_val=21)
440
+ self.add_field_to_layout(
441
+ q_depthmap.layout(), 'depthmap_blur_size', FIELD_INT, 'Blurl size (px)',
442
+ required=False, default=constants.DEFAULT_DM_BLUR_SIZE, min_val=1, max_val=21)
443
+ self.add_field_to_layout(
444
+ q_depthmap.layout(), 'depthmap_smooth_size', FIELD_INT, 'Smooth size (px)',
445
+ required=False, default=constants.DEFAULT_DM_SMOOTH_SIZE, min_val=0, max_val=256)
446
+ self.add_field_to_layout(
447
+ q_depthmap.layout(), 'depthmap_temperature', FIELD_FLOAT, 'Temperature',
448
+ required=False, default=constants.DEFAULT_DM_TEMPERATURE,
337
449
  min_val=0, max_val=1, step=0.05)
338
- self.add_field(
339
- 'depthmap_levels', FIELD_INT, 'Levels', required=False,
340
- add_to_layout=q_depthmap.layout(),
450
+ self.add_field_to_layout(
451
+ q_depthmap.layout(), 'depthmap_levels', FIELD_INT, 'Levels', required=False,
341
452
  default=constants.DEFAULT_DM_LEVELS, min_val=2, max_val=6)
342
- self.add_field(
343
- 'depthmap_float_type', FIELD_COMBO, 'Precision', required=False,
344
- add_to_layout=q_depthmap.layout(), options=self.FLOAT_OPTIONS,
345
- values=constants.VALID_FLOATS,
453
+ self.add_field_to_layout(
454
+ q_depthmap.layout(), 'depthmap_float_type', FIELD_COMBO,
455
+ 'Precision', required=False,
456
+ options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
346
457
  default=dict(zip(constants.VALID_FLOATS,
347
458
  self.FLOAT_OPTIONS))[constants.DEFAULT_DM_FLOAT])
348
- self.add_row(stacked)
459
+ layout.addRow(stacked)
349
460
  combo.currentIndexChanged.connect(change)
350
461
 
351
462
 
@@ -353,32 +464,32 @@ class FocusStackConfigurator(FocusStackBaseConfigurator):
353
464
  def create_form(self, layout, action):
354
465
  super().create_form(layout, action)
355
466
  if self.expert:
356
- self.add_field(
357
- 'exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
467
+ self.add_field_to_layout(
468
+ self.general_tab_layout, 'exif_path', FIELD_REL_PATH,
469
+ 'Exif data path', required=False,
358
470
  placeholder='relative to working path')
359
- self.add_field(
360
- 'prefix', FIELD_TEXT, 'Ouptut filename prefix', required=False,
471
+ self.add_field_to_layout(
472
+ self.general_tab_layout, 'prefix', FIELD_TEXT,
473
+ 'Output filename prefix', required=False,
361
474
  default=constants.DEFAULT_STACK_PREFIX,
362
475
  placeholder=constants.DEFAULT_STACK_PREFIX)
363
- self.add_field(
364
- 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
476
+ self.add_field_to_layout(
477
+ self.general_tab_layout, 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
365
478
  default=constants.DEFAULT_PLOT_STACK)
366
- super().common_fields(layout)
367
479
 
368
480
 
369
481
  class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
370
482
  def create_form(self, layout, action):
371
483
  super().create_form(layout, action)
372
- self.add_field(
373
- 'frames', FIELD_INT, 'Frames', required=False,
484
+ self.add_field_to_layout(
485
+ self.general_tab_layout, 'frames', FIELD_INT, 'Frames', required=False,
374
486
  default=constants.DEFAULT_FRAMES, min_val=1, max_val=100)
375
- self.add_field(
376
- 'overlap', FIELD_INT, 'Overlapping frames', required=False,
487
+ self.add_field_to_layout(
488
+ self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
377
489
  default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
378
- self.add_field(
379
- 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
490
+ self.add_field_to_layout(
491
+ self.general_tab_layout, 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
380
492
  default=constants.DEFAULT_PLOT_STACK_BUNCH)
381
- super().common_fields(layout)
382
493
 
383
494
 
384
495
  class MultiLayerConfigurator(DefaultActionConfigurator):
@@ -435,6 +546,13 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
435
546
  self.add_field(
436
547
  'step_process', FIELD_BOOL, 'Step process', required=False,
437
548
  default=True)
549
+ self.add_field(
550
+ 'max_threads', FIELD_INT, 'Max num. of cores',
551
+ required=False, default=constants.DEFAULT_MAX_FWK_THREADS,
552
+ min_val=1, max_val=64)
553
+ self.add_field(
554
+ 'chunk_submit', FIELD_BOOL, 'Submit in chunks',
555
+ required=False, default=constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
438
556
 
439
557
 
440
558
  class MaskNoiseConfigurator(DefaultActionConfigurator):
@@ -460,18 +578,21 @@ class SubsampleActionConfigurator(DefaultActionConfigurator):
460
578
  self.subsample_field = None
461
579
  self.fast_subsampling_field = None
462
580
 
463
- def add_subsample_fields(self):
581
+ def add_subsample_fields(self, add_to_layout=None):
582
+ if add_to_layout is None:
583
+ add_to_layout = self.builder.main_layout
464
584
  self.subsample_field = self.add_field(
465
585
  'subsample', FIELD_COMBO, 'Subsample', required=False,
466
586
  options=constants.FIELD_SUBSAMPLE_OPTIONS,
467
587
  values=constants.FIELD_SUBSAMPLE_VALUES,
468
- default=constants.FIELD_SUBSAMPLE_DEFAULT)
588
+ default=constants.FIELD_SUBSAMPLE_DEFAULT,
589
+ add_to_layout=add_to_layout)
469
590
  self.fast_subsampling_field = self.add_field(
470
591
  'fast_subsampling', FIELD_BOOL, 'Fast subsampling', required=False,
471
- default=constants.DEFAULT_ALIGN_FAST_SUBSAMPLING)
592
+ default=constants.DEFAULT_ALIGN_FAST_SUBSAMPLING,
593
+ add_to_layout=add_to_layout)
472
594
 
473
595
  self.subsample_field.currentTextChanged.connect(self.change_subsample)
474
-
475
596
  self.change_subsample()
476
597
 
477
598
  def change_subsample(self):
@@ -484,6 +605,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
484
605
  TRANSFORM_OPTIONS = ['Rigid', 'Homography']
485
606
  METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
486
607
  MATCHING_METHOD_OPTIONS = ['K-nearest neighbors', 'Hamming distance']
608
+ MODE_OPTIONS = ['Auto', 'Sequential', 'Parallel']
487
609
 
488
610
  def __init__(self, expert, current_wd):
489
611
  super().__init__(expert, current_wd)
@@ -492,13 +614,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
492
614
  self.detector_field = None
493
615
  self.descriptor_field = None
494
616
  self.matching_method_field = None
617
+ self.tab_widget = None
618
+ self.current_tab_layout = None
495
619
 
496
620
  def show_info(self, message, timeout=3000):
497
621
  self.info_label.setText(message)
498
- self.info_label.setVisible(True)
499
622
  timer = QTimer(self.info_label)
500
623
  timer.setSingleShot(True)
501
- timer.timeout.connect(self.info_label.hide)
624
+ timer.timeout.connect(lambda: self.info_label.setText(''))
502
625
  timer.start(timeout)
503
626
 
504
627
  def change_match_config(self):
@@ -533,131 +656,178 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
533
656
  self.detector_field = None
534
657
  self.descriptor_field = None
535
658
  self.matching_method_field = None
659
+ self.tab_widget = self.create_tab_widget(layout)
536
660
  if self.expert:
537
- self.add_bold_label("Feature identification:")
538
- self.info_label = QLabel()
539
- self.info_label.setStyleSheet("color: orange; font-style: italic;")
540
- self.info_label.setVisible(False)
541
- layout.addRow(self.info_label)
542
- self.detector_field = self.add_field(
543
- 'detector', FIELD_COMBO, 'Detector', required=False,
544
- options=constants.VALID_DETECTORS, default=constants.DEFAULT_DETECTOR)
545
- self.descriptor_field = self.add_field(
546
- 'descriptor', FIELD_COMBO, 'Descriptor', required=False,
547
- options=constants.VALID_DESCRIPTORS, default=constants.DEFAULT_DESCRIPTOR)
548
- self.add_bold_label("Feature matching:")
549
- self.matching_method_field = self.add_field(
550
- 'match_method', FIELD_COMBO, 'Match method', required=False,
551
- options=self.MATCHING_METHOD_OPTIONS, values=constants.VALID_MATCHING_METHODS,
552
- default=constants.DEFAULT_MATCHING_METHOD)
553
- self.detector_field.setToolTip(
554
- "SIFT: Requires SIFT descriptor and K-NN matching\n"
555
- "ORB/AKAZE: Work best with Hamming distance"
556
- )
557
- self.descriptor_field.setToolTip(
558
- "SIFT: Requires K-NN matching\n"
559
- "ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors"
560
- )
561
- self.matching_method_field.setToolTip(
562
- "Automatically selected based on detector/descriptor combination"
563
- )
564
- self.detector_field.currentIndexChanged.connect(self.change_match_config)
565
- self.descriptor_field.currentIndexChanged.connect(self.change_match_config)
566
- self.matching_method_field.currentIndexChanged.connect(self.change_match_config)
567
- self.add_field(
568
- 'flann_idx_kdtree', FIELD_INT, 'Flann idx kdtree', required=False,
569
- default=constants.DEFAULT_FLANN_IDX_KDTREE,
570
- min_val=0, max_val=10)
571
- self.add_field(
572
- 'flann_trees', FIELD_INT, 'Flann trees', required=False,
573
- default=constants.DEFAULT_FLANN_TREES,
574
- min_val=0, max_val=10)
575
- self.add_field(
576
- 'flann_checks', FIELD_INT, 'Flann checks', required=False,
577
- default=constants.DEFAULT_FLANN_CHECKS,
578
- min_val=0, max_val=1000)
579
- self.add_field(
580
- 'threshold', FIELD_FLOAT, 'Threshold', required=False,
581
- default=constants.DEFAULT_ALIGN_THRESHOLD,
582
- min_val=0, max_val=1, step=0.05)
583
- self.add_bold_label("Transform:")
584
- transform = self.add_field(
585
- 'transform', FIELD_COMBO, 'Transform', required=False,
586
- options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
587
- default=constants.DEFAULT_TRANSFORM)
588
- method = self.add_field(
589
- 'align_method', FIELD_COMBO, 'Align method', required=False,
590
- options=self.METHOD_OPTIONS, values=constants.VALID_ALIGN_METHODS,
591
- default=constants.DEFAULT_ALIGN_METHOD)
592
- rans_threshold = self.add_field(
593
- 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
594
- default=constants.DEFAULT_RANS_THRESHOLD, min_val=0, max_val=20, step=0.1)
595
- self.add_field(
596
- 'min_good_matches', FIELD_INT, "Min. good matches", required=False,
597
- default=constants.DEFAULT_ALIGN_MIN_GOOD_MATCHES, min_val=0, max_val=500)
598
-
599
- def change_method():
600
- text = method.currentText()
601
- if text == self.METHOD_OPTIONS[0]:
602
- rans_threshold.setEnabled(True)
603
- elif text == self.METHOD_OPTIONS[1]:
604
- rans_threshold.setEnabled(False)
605
-
606
- method.currentIndexChanged.connect(change_method)
607
- change_method()
608
- self.add_field(
609
- 'align_confidence', FIELD_FLOAT, 'Confidence (%)',
610
- required=False, decimals=1,
611
- default=constants.DEFAULT_ALIGN_CONFIDENCE,
612
- min_val=70.0, max_val=100.0, step=0.1)
613
-
614
- refine_iters = self.add_field(
615
- 'refine_iters', FIELD_INT, 'Refinement iterations (Rigid)', required=False,
616
- default=constants.DEFAULT_REFINE_ITERS, min_val=0, max_val=1000)
617
- max_iters = self.add_field(
618
- 'max_iters', FIELD_INT, 'Max. iterations (Homography)', required=False,
619
- default=constants.DEFAULT_ALIGN_MAX_ITERS, min_val=0, max_val=5000)
620
-
621
- def change_transform():
622
- text = transform.currentText()
623
- if text == self.TRANSFORM_OPTIONS[0]:
624
- refine_iters.setEnabled(True)
625
- max_iters.setEnabled(False)
626
- elif text == self.TRANSFORM_OPTIONS[1]:
627
- refine_iters.setEnabled(False)
628
- max_iters.setEnabled(True)
629
-
630
- transform.currentIndexChanged.connect(change_transform)
631
- change_transform()
632
- self.add_field(
633
- 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
634
- required=False, default=constants.DEFAULT_ALIGN_ABORT_ABNORMAL)
635
- self.add_subsample_fields()
636
- self.add_bold_label("Border:")
637
- self.add_field(
638
- 'border_mode', FIELD_COMBO, 'Border mode', required=False,
639
- options=self.BORDER_MODE_OPTIONS,
640
- values=constants.VALID_BORDER_MODES,
641
- default=constants.DEFAULT_BORDER_MODE)
642
- self.add_field(
643
- 'border_value', FIELD_INT_TUPLE,
644
- 'Border value (if constant)', required=False, size=4,
645
- default=constants.DEFAULT_BORDER_VALUE,
646
- labels=constants.RGBA_LABELS,
647
- min_val=constants.DEFAULT_BORDER_VALUE, max_val=[255] * 4)
648
- self.add_field(
649
- 'border_blur', FIELD_FLOAT, 'Border blur', required=False,
650
- default=constants.DEFAULT_BORDER_BLUR,
651
- min_val=0, max_val=1000, step=1)
652
- self.add_bold_label("Miscellanea:")
653
- self.add_field(
654
- 'plot_summary', FIELD_BOOL, 'Plot summary',
661
+ feature_layout = self.add_tab(self.tab_widget, "Feature extraction")
662
+ self.create_feature_tab(feature_layout)
663
+ transform_layout = self.add_tab(self.tab_widget, "Transform")
664
+ self.create_transform_tab(transform_layout)
665
+ border_layout = self.add_tab(self.tab_widget, "Border")
666
+ self.create_border_tab(border_layout)
667
+ misc_layout = self.add_tab(self.tab_widget, "Miscellanea")
668
+ self.create_miscellanea_tab(misc_layout)
669
+
670
+ def create_feature_tab(self, layout):
671
+ self.add_bold_label_to_layout(layout, "Feature identification:")
672
+ self.detector_field = self.add_field_to_layout(
673
+ layout, 'detector', FIELD_COMBO, 'Detector', required=False,
674
+ options=constants.VALID_DETECTORS, default=constants.DEFAULT_DETECTOR)
675
+ self.descriptor_field = self.add_field_to_layout(
676
+ layout, 'descriptor', FIELD_COMBO, 'Descriptor', required=False,
677
+ options=constants.VALID_DESCRIPTORS, default=constants.DEFAULT_DESCRIPTOR)
678
+ self.detector_field.setToolTip(
679
+ "SIFT: Requires SIFT descriptor and K-NN matching\n"
680
+ "ORB/AKAZE: Work best with Hamming distance"
681
+ )
682
+ self.descriptor_field.setToolTip(
683
+ "SIFT: Requires K-NN matching\n"
684
+ "ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors"
685
+ )
686
+ self.detector_field.currentIndexChanged.connect(self.change_match_config)
687
+ self.descriptor_field.currentIndexChanged.connect(self.change_match_config)
688
+ self.info_label = QLabel()
689
+ self.info_label.setStyleSheet("color: orange; font-style: italic;")
690
+ layout.addRow(self.info_label)
691
+ self.add_bold_label_to_layout(layout, "Feature matching:")
692
+ self.matching_method_field = self.add_field_to_layout(
693
+ layout, 'match_method', FIELD_COMBO, 'Match method', required=False,
694
+ options=self.MATCHING_METHOD_OPTIONS, values=constants.VALID_MATCHING_METHODS,
695
+ default=constants.DEFAULT_MATCHING_METHOD)
696
+ self.matching_method_field.setToolTip(
697
+ "Automatically selected based on detector/descriptor combination"
698
+ )
699
+ self.matching_method_field.currentIndexChanged.connect(self.change_match_config)
700
+ self.add_field_to_layout(
701
+ layout, 'flann_idx_kdtree', FIELD_INT, 'Flann idx kdtree', required=False,
702
+ default=constants.DEFAULT_FLANN_IDX_KDTREE,
703
+ min_val=0, max_val=10)
704
+ self.add_field_to_layout(
705
+ layout, 'flann_trees', FIELD_INT, 'Flann trees', required=False,
706
+ default=constants.DEFAULT_FLANN_TREES,
707
+ min_val=0, max_val=10)
708
+ self.add_field_to_layout(
709
+ layout, 'flann_checks', FIELD_INT, 'Flann checks', required=False,
710
+ default=constants.DEFAULT_FLANN_CHECKS,
711
+ min_val=0, max_val=1000)
712
+ self.add_field_to_layout(
713
+ layout, 'threshold', FIELD_FLOAT, 'Threshold', required=False,
714
+ default=constants.DEFAULT_ALIGN_THRESHOLD,
715
+ min_val=0, max_val=1, step=0.05)
716
+ self.add_subsample_fields(add_to_layout=layout)
717
+
718
+ def create_transform_tab(self, layout):
719
+ self.add_bold_label_to_layout(layout, "Transform:")
720
+ transform = self.add_field_to_layout(
721
+ layout, 'transform', FIELD_COMBO, 'Transform', required=False,
722
+ options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
723
+ default=constants.DEFAULT_TRANSFORM)
724
+ method = self.add_field_to_layout(
725
+ layout, 'align_method', FIELD_COMBO, 'Align method', required=False,
726
+ options=self.METHOD_OPTIONS, values=constants.VALID_ALIGN_METHODS,
727
+ default=constants.DEFAULT_ALIGN_METHOD)
728
+ rans_threshold = self.add_field_to_layout(
729
+ layout, 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
730
+ default=constants.DEFAULT_RANS_THRESHOLD, min_val=0, max_val=20, step=0.1)
731
+ self.add_field_to_layout(
732
+ layout, 'min_good_matches', FIELD_INT, "Min. good matches", required=False,
733
+ default=constants.DEFAULT_ALIGN_MIN_GOOD_MATCHES, min_val=0, max_val=500)
734
+
735
+ def change_method():
736
+ text = method.currentText()
737
+ if text == self.METHOD_OPTIONS[0]:
738
+ rans_threshold.setEnabled(True)
739
+ elif text == self.METHOD_OPTIONS[1]:
740
+ rans_threshold.setEnabled(False)
741
+
742
+ method.currentIndexChanged.connect(change_method)
743
+ change_method()
744
+ self.add_field_to_layout(
745
+ layout, 'align_confidence', FIELD_FLOAT, 'Confidence (%)',
746
+ required=False, decimals=1,
747
+ default=constants.DEFAULT_ALIGN_CONFIDENCE,
748
+ min_val=70.0, max_val=100.0, step=0.1)
749
+ refine_iters = self.add_field_to_layout(
750
+ layout, 'refine_iters', FIELD_INT, 'Refinement iterations (Rigid)', required=False,
751
+ default=constants.DEFAULT_REFINE_ITERS, min_val=0, max_val=1000)
752
+ max_iters = self.add_field_to_layout(
753
+ layout, 'max_iters', FIELD_INT, 'Max. iterations (Homography)', required=False,
754
+ default=constants.DEFAULT_ALIGN_MAX_ITERS, min_val=0, max_val=5000)
755
+
756
+ def change_transform():
757
+ text = transform.currentText()
758
+ if text == self.TRANSFORM_OPTIONS[0]:
759
+ refine_iters.setEnabled(True)
760
+ max_iters.setEnabled(False)
761
+ elif text == self.TRANSFORM_OPTIONS[1]:
762
+ refine_iters.setEnabled(False)
763
+ max_iters.setEnabled(True)
764
+
765
+ transform.currentIndexChanged.connect(change_transform)
766
+ change_transform()
767
+ self.add_field_to_layout(
768
+ layout, 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
769
+ required=False, default=constants.DEFAULT_ALIGN_ABORT_ABNORMAL)
770
+
771
+ def create_border_tab(self, layout):
772
+ self.add_bold_label_to_layout(layout, "Border:")
773
+ self.add_field_to_layout(
774
+ layout, 'border_mode', FIELD_COMBO, 'Border mode', required=False,
775
+ options=self.BORDER_MODE_OPTIONS,
776
+ values=constants.VALID_BORDER_MODES,
777
+ default=constants.DEFAULT_BORDER_MODE)
778
+ self.add_field_to_layout(
779
+ layout, 'border_value', FIELD_INT_TUPLE,
780
+ 'Border value (if constant)', required=False, size=4,
781
+ default=constants.DEFAULT_BORDER_VALUE,
782
+ labels=constants.RGBA_LABELS,
783
+ min_val=constants.DEFAULT_BORDER_VALUE, max_val=[255] * 4)
784
+ self.add_field_to_layout(
785
+ layout, 'border_blur', FIELD_FLOAT, 'Border blur', required=False,
786
+ default=constants.DEFAULT_BORDER_BLUR,
787
+ min_val=0, max_val=1000, step=1)
788
+
789
+ def create_miscellanea_tab(self, layout):
790
+ self.add_bold_label_to_layout(layout, "Miscellanea:")
791
+ if self.expert:
792
+ mode = self.add_field_to_layout(
793
+ layout, 'mode', FIELD_COMBO, 'Mode',
794
+ required=False, options=self.MODE_OPTIONS, values=constants.ALIGN_VALID_MODES,
795
+ default=dict(zip(constants.ALIGN_VALID_MODES,
796
+ self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
797
+ memory_limit = self.add_field_to_layout(
798
+ layout, 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
799
+ required=False, default=constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
800
+ min_val=1.0, max_val=64.0)
801
+ max_threads = self.add_field_to_layout(
802
+ layout, 'max_threads', FIELD_INT, 'Max num. of cores',
803
+ required=False, default=constants.DEFAULT_ALIGN_MAX_THREADS,
804
+ min_val=1, max_val=64)
805
+ chunk_submit = self.add_field_to_layout(
806
+ layout, 'chunk_submit', FIELD_BOOL, 'Submit in chunks',
807
+ required=False, default=constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
808
+ bw_matching = self.add_field_to_layout(
809
+ layout, 'bw_matching', FIELD_BOOL, 'Match using black & white',
810
+ required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
811
+
812
+ def change_mode():
813
+ text = mode.currentText()
814
+ enabled = text != self.MODE_OPTIONS[1]
815
+ memory_limit.setEnabled(enabled)
816
+ max_threads.setEnabled(enabled)
817
+ chunk_submit.setEnabled(enabled)
818
+ bw_matching.setEnabled(enabled)
819
+
820
+ mode.currentIndexChanged.connect(change_mode)
821
+
822
+ self.add_field_to_layout(
823
+ layout, 'plot_summary', FIELD_BOOL, 'Plot summary',
655
824
  required=False, default=False)
656
- self.add_field(
657
- 'plot_matches', FIELD_BOOL, 'Plot matches',
825
+
826
+ self.add_field_to_layout(
827
+ layout, 'plot_matches', FIELD_BOOL, 'Plot matches',
658
828
  required=False, default=False)
659
829
 
660
- def update_params(self, params: Dict[str, Any]) -> bool:
830
+ def update_params(self, params):
661
831
  if self.detector_field and self.descriptor_field and self.matching_method_field:
662
832
  try:
663
833
  detector = self.detector_field.currentText()