shinestacker 1.3.0__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.

@@ -3,7 +3,7 @@
3
3
  import os
4
4
  import traceback
5
5
  from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QLabel, QScrollArea, QSizePolicy,
6
- QMessageBox, QStackedWidget, QFormLayout, QDialog)
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
@@ -156,6 +156,31 @@ class NoNameActionConfigurator(ActionConfigurator):
156
156
  def add_labelled_row(self, label, widget):
157
157
  self.add_row(self.labelled_widget(label, widget))
158
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
+
159
184
 
160
185
  class DefaultActionConfigurator(NoNameActionConfigurator):
161
186
  def create_form(self, layout, action, tag='Action'):
@@ -183,20 +208,11 @@ class JobConfigurator(DefaultActionConfigurator):
183
208
  input_filepaths = input_filepaths.split(constants.PATH_SEPARATOR)
184
209
  self.working_path_label = QLabel(working_path or "Not set")
185
210
  self.input_path_label = QLabel(input_path or "Not set")
186
- has_existing_data = working_path or input_path or input_filepaths
211
+ self.input_widget.path_edit.setText('')
187
212
  if input_filepaths:
188
213
  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)
214
+ else:
215
+ self.input_widget.folder_mode_radio.setChecked(False)
200
216
  self.input_widget.text_changed_connect(self.update_paths_and_frames)
201
217
  self.input_widget.folder_mode_radio.toggled.connect(self.update_paths_and_frames)
202
218
  self.input_widget.files_mode_radio.toggled.connect(self.update_paths_and_frames)
@@ -206,29 +222,27 @@ class JobConfigurator(DefaultActionConfigurator):
206
222
  self.add_bold_label("Derived Paths:")
207
223
  self.add_labelled_row("Working path: ", self.working_path_label)
208
224
  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()
225
+ self.set_paths_and_frames(working_path, input_path, input_filepaths)
213
226
 
214
227
  def update_frames_count(self):
215
228
  if self.input_widget.get_selection_mode() == 'files':
216
- count = len(self.input_widget.get_selected_files())
229
+ count = self.input_widget.num_selected_files()
217
230
  else:
218
231
  count = self.count_image_files(self.input_widget.get_path())
219
232
  self.frames_label.setText(str(count))
220
233
 
221
- def update_paths_and_frames(self):
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")
222
245
  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
246
 
233
247
  def count_image_files(self, path):
234
248
  if not path or not os.path.isdir(path):
@@ -244,18 +258,16 @@ class JobConfigurator(DefaultActionConfigurator):
244
258
  return False
245
259
  selection_mode = self.input_widget.get_selection_mode()
246
260
  selected_files = self.input_widget.get_selected_files()
247
- input_path = self.input_widget.get_path()
248
261
  if selection_mode == 'files' and selected_files:
262
+ input_full_path = os.path.dirname(selected_files[0])
249
263
  params['input_filepaths'] = self.input_widget.get_selected_filenames()
250
- params['input_path'] = os.path.dirname(selected_files[0])
251
264
  else:
265
+ input_full_path = self.input_widget.get_path()
252
266
  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'] = ''
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
259
271
  return True
260
272
 
261
273
 
@@ -307,38 +319,48 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
307
319
  FLOAT_OPTIONS = ['float 32 bits', 'float 64 bits']
308
320
  MODE_OPTIONS = ['Auto', 'All in memory', 'Tiled I/O buffered']
309
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
+
310
328
  def create_form(self, layout, action):
311
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):
312
337
  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,
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,
317
342
  placeholder='relative to working path')
318
- self.add_field(
319
- '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,
320
345
  placeholder='relative to working path')
321
- self.add_field(
322
- 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
346
+ self.add_field_to_layout(
347
+ layout, 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
323
348
  required=False, default=True)
324
349
 
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,
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,
332
354
  options=constants.STACK_ALGO_OPTIONS,
333
355
  default=constants.STACK_ALGO_DEFAULT)
334
356
  q_pyramid, q_depthmap = QWidget(), QWidget()
335
357
  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)
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)
342
364
  stacked = QStackedWidget()
343
365
  stacked.addWidget(q_pyramid)
344
366
  stacked.addWidget(q_depthmap)
@@ -349,52 +371,45 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
349
371
  stacked.setCurrentWidget(q_pyramid)
350
372
  elif text == constants.STACK_ALGO_DEPTH_MAP:
351
373
  stacked.setCurrentWidget(q_depthmap)
374
+
352
375
  change()
353
376
  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,
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,
366
386
  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(),
387
+ self.add_field_to_layout(
388
+ q_pyramid.layout(), 'pyramid_float_type', FIELD_COMBO, 'Precision', required=False,
370
389
  options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
371
390
  default=dict(zip(constants.VALID_FLOATS,
372
391
  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,
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,
377
395
  default=dict(zip(constants.PY_VALID_MODES,
378
396
  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,
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,
383
401
  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,
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,
388
405
  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,
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,
393
409
  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,
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,
398
413
  min_val=0, max_val=6)
399
414
 
400
415
  def change_mode():
@@ -407,47 +422,41 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
407
422
 
408
423
  mode.currentIndexChanged.connect(change_mode)
409
424
  change_mode()
410
- self.add_field(
411
- 'depthmap_energy', FIELD_COMBO, 'Energy', required=False,
412
- add_to_layout=q_depthmap.layout(),
425
+
426
+ self.add_field_to_layout(
427
+ q_depthmap.layout(), 'depthmap_energy', FIELD_COMBO, 'Energy', required=False,
413
428
  options=self.ENERGY_OPTIONS, values=constants.VALID_DM_ENERGY,
414
429
  default=dict(zip(constants.VALID_DM_ENERGY,
415
430
  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(),
431
+ self.add_field_to_layout(
432
+ q_depthmap.layout(), 'map_type', FIELD_COMBO, 'Map type', required=False,
419
433
  options=self.MAP_TYPE_OPTIONS, values=constants.VALID_DM_MAP,
420
434
  default=dict(zip(constants.VALID_DM_MAP,
421
435
  self.MAP_TYPE_OPTIONS))[constants.DEFAULT_DM_MAP])
422
436
  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,
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,
439
449
  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(),
450
+ self.add_field_to_layout(
451
+ q_depthmap.layout(), 'depthmap_levels', FIELD_INT, 'Levels', required=False,
443
452
  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,
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,
448
457
  default=dict(zip(constants.VALID_FLOATS,
449
458
  self.FLOAT_OPTIONS))[constants.DEFAULT_DM_FLOAT])
450
- self.add_row(stacked)
459
+ layout.addRow(stacked)
451
460
  combo.currentIndexChanged.connect(change)
452
461
 
453
462
 
@@ -455,32 +464,32 @@ class FocusStackConfigurator(FocusStackBaseConfigurator):
455
464
  def create_form(self, layout, action):
456
465
  super().create_form(layout, action)
457
466
  if self.expert:
458
- self.add_field(
459
- '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,
460
470
  placeholder='relative to working path')
461
- self.add_field(
462
- '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,
463
474
  default=constants.DEFAULT_STACK_PREFIX,
464
475
  placeholder=constants.DEFAULT_STACK_PREFIX)
465
- self.add_field(
466
- '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,
467
478
  default=constants.DEFAULT_PLOT_STACK)
468
- super().common_fields(layout)
469
479
 
470
480
 
471
481
  class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
472
482
  def create_form(self, layout, action):
473
483
  super().create_form(layout, action)
474
- self.add_field(
475
- 'frames', FIELD_INT, 'Frames', required=False,
484
+ self.add_field_to_layout(
485
+ self.general_tab_layout, 'frames', FIELD_INT, 'Frames', required=False,
476
486
  default=constants.DEFAULT_FRAMES, min_val=1, max_val=100)
477
- self.add_field(
478
- '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,
479
489
  default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
480
- self.add_field(
481
- '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,
482
492
  default=constants.DEFAULT_PLOT_STACK_BUNCH)
483
- super().common_fields(layout)
484
493
 
485
494
 
486
495
  class MultiLayerConfigurator(DefaultActionConfigurator):
@@ -569,18 +578,21 @@ class SubsampleActionConfigurator(DefaultActionConfigurator):
569
578
  self.subsample_field = None
570
579
  self.fast_subsampling_field = None
571
580
 
572
- 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
573
584
  self.subsample_field = self.add_field(
574
585
  'subsample', FIELD_COMBO, 'Subsample', required=False,
575
586
  options=constants.FIELD_SUBSAMPLE_OPTIONS,
576
587
  values=constants.FIELD_SUBSAMPLE_VALUES,
577
- default=constants.FIELD_SUBSAMPLE_DEFAULT)
588
+ default=constants.FIELD_SUBSAMPLE_DEFAULT,
589
+ add_to_layout=add_to_layout)
578
590
  self.fast_subsampling_field = self.add_field(
579
591
  'fast_subsampling', FIELD_BOOL, 'Fast subsampling', required=False,
580
- default=constants.DEFAULT_ALIGN_FAST_SUBSAMPLING)
592
+ default=constants.DEFAULT_ALIGN_FAST_SUBSAMPLING,
593
+ add_to_layout=add_to_layout)
581
594
 
582
595
  self.subsample_field.currentTextChanged.connect(self.change_subsample)
583
-
584
596
  self.change_subsample()
585
597
 
586
598
  def change_subsample(self):
@@ -602,13 +614,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
602
614
  self.detector_field = None
603
615
  self.descriptor_field = None
604
616
  self.matching_method_field = None
617
+ self.tab_widget = None
618
+ self.current_tab_layout = None
605
619
 
606
620
  def show_info(self, message, timeout=3000):
607
621
  self.info_label.setText(message)
608
- self.info_label.setVisible(True)
609
622
  timer = QTimer(self.info_label)
610
623
  timer.setSingleShot(True)
611
- timer.timeout.connect(self.info_label.hide)
624
+ timer.timeout.connect(lambda: self.info_label.setText(''))
612
625
  timer.start(timeout)
613
626
 
614
627
  def change_match_config(self):
@@ -643,142 +656,157 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
643
656
  self.detector_field = None
644
657
  self.descriptor_field = None
645
658
  self.matching_method_field = None
659
+ self.tab_widget = self.create_tab_widget(layout)
646
660
  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:")
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:")
763
791
  if self.expert:
764
- mode = self.add_field(
765
- 'mode', FIELD_COMBO, 'Mode',
792
+ mode = self.add_field_to_layout(
793
+ layout, 'mode', FIELD_COMBO, 'Mode',
766
794
  required=False, options=self.MODE_OPTIONS, values=constants.ALIGN_VALID_MODES,
767
795
  default=dict(zip(constants.ALIGN_VALID_MODES,
768
796
  self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
769
- memory_limit = self.add_field(
770
- 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
797
+ memory_limit = self.add_field_to_layout(
798
+ layout, 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
771
799
  required=False, default=constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
772
800
  min_val=1.0, max_val=64.0)
773
- max_threads = self.add_field(
774
- 'max_threads', FIELD_INT, 'Max num. of cores',
801
+ max_threads = self.add_field_to_layout(
802
+ layout, 'max_threads', FIELD_INT, 'Max num. of cores',
775
803
  required=False, default=constants.DEFAULT_ALIGN_MAX_THREADS,
776
804
  min_val=1, max_val=64)
777
- chunk_submit = self.add_field(
778
- 'chunk_submit', FIELD_BOOL, 'Submit in chunks',
805
+ chunk_submit = self.add_field_to_layout(
806
+ layout, 'chunk_submit', FIELD_BOOL, 'Submit in chunks',
779
807
  required=False, default=constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
780
- bw_matching = self.add_field(
781
- 'bw_matching', FIELD_BOOL, 'Match using black & white',
808
+ bw_matching = self.add_field_to_layout(
809
+ layout, 'bw_matching', FIELD_BOOL, 'Match using black & white',
782
810
  required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
783
811
 
784
812
  def change_mode():
@@ -790,11 +818,13 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
790
818
  bw_matching.setEnabled(enabled)
791
819
 
792
820
  mode.currentIndexChanged.connect(change_mode)
793
- self.add_field(
794
- 'plot_summary', FIELD_BOOL, 'Plot summary',
821
+
822
+ self.add_field_to_layout(
823
+ layout, 'plot_summary', FIELD_BOOL, 'Plot summary',
795
824
  required=False, default=False)
796
- self.add_field(
797
- 'plot_matches', FIELD_BOOL, 'Plot matches',
825
+
826
+ self.add_field_to_layout(
827
+ layout, 'plot_matches', FIELD_BOOL, 'Plot matches',
798
828
  required=False, default=False)
799
829
 
800
830
  def update_params(self, params):