shinestacker 1.7.0__py3-none-any.whl → 1.8.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 (45) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +184 -80
  3. shinestacker/algorithms/align_auto.py +13 -11
  4. shinestacker/algorithms/align_parallel.py +41 -16
  5. shinestacker/algorithms/base_stack_algo.py +1 -1
  6. shinestacker/algorithms/noise_detection.py +10 -8
  7. shinestacker/algorithms/pyramid_tiles.py +1 -1
  8. shinestacker/algorithms/stack.py +9 -0
  9. shinestacker/algorithms/stack_framework.py +49 -25
  10. shinestacker/algorithms/utils.py +5 -1
  11. shinestacker/algorithms/vignetting.py +16 -3
  12. shinestacker/app/settings_dialog.py +303 -136
  13. shinestacker/config/constants.py +10 -5
  14. shinestacker/config/settings.py +29 -8
  15. shinestacker/core/core_utils.py +1 -0
  16. shinestacker/core/exceptions.py +1 -1
  17. shinestacker/core/framework.py +9 -4
  18. shinestacker/gui/action_config.py +23 -20
  19. shinestacker/gui/action_config_dialog.py +107 -64
  20. shinestacker/gui/gui_images.py +27 -3
  21. shinestacker/gui/gui_run.py +1 -2
  22. shinestacker/gui/img/dark/close-round-line-icon.png +0 -0
  23. shinestacker/gui/img/dark/forward-button-icon.png +0 -0
  24. shinestacker/gui/img/dark/play-button-round-icon.png +0 -0
  25. shinestacker/gui/img/dark/plus-round-line-icon.png +0 -0
  26. shinestacker/gui/img/dark/shinestacker_bkg.png +0 -0
  27. shinestacker/gui/main_window.py +20 -7
  28. shinestacker/gui/menu_manager.py +18 -7
  29. shinestacker/gui/new_project.py +18 -9
  30. shinestacker/gui/project_controller.py +13 -6
  31. shinestacker/gui/project_editor.py +12 -2
  32. shinestacker/gui/project_model.py +4 -4
  33. shinestacker/gui/tab_widget.py +16 -6
  34. shinestacker/retouch/adjustments.py +5 -0
  35. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/METADATA +35 -39
  36. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/RECORD +45 -40
  37. /shinestacker/gui/img/{close-round-line-icon.png → light/close-round-line-icon.png} +0 -0
  38. /shinestacker/gui/img/{forward-button-icon.png → light/forward-button-icon.png} +0 -0
  39. /shinestacker/gui/img/{play-button-round-icon.png → light/play-button-round-icon.png} +0 -0
  40. /shinestacker/gui/img/{plus-round-line-icon.png → light/plus-round-line-icon.png} +0 -0
  41. /shinestacker/gui/{ico → img/light}/shinestacker_bkg.png +0 -0
  42. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/WHEEL +0 -0
  43. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/entry_points.txt +0 -0
  44. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/licenses/LICENSE +0 -0
  45. {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/top_level.txt +0 -0
@@ -416,6 +416,29 @@ class FieldBuilder:
416
416
  return checkbox
417
417
 
418
418
 
419
+ def create_tab_layout():
420
+ tab_layout = QFormLayout()
421
+ tab_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
422
+ tab_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
423
+ tab_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
424
+ tab_layout.setLabelAlignment(Qt.AlignLeft)
425
+ return tab_layout
426
+
427
+
428
+ def add_tab(tab_widget, title):
429
+ tab = QWidget()
430
+ tab_layout = create_tab_layout()
431
+ tab.setLayout(tab_layout)
432
+ tab_widget.addTab(tab, title)
433
+ return tab_layout
434
+
435
+
436
+ def create_tab_widget(main_layout):
437
+ tab_widget = QTabWidget()
438
+ main_layout.addRow(tab_widget)
439
+ return tab_widget
440
+
441
+
419
442
  class NoNameActionConfigurator(ActionConfigurator):
420
443
  def __init__(self, current_wd):
421
444
  super().__init__(current_wd)
@@ -457,26 +480,6 @@ class NoNameActionConfigurator(ActionConfigurator):
457
480
  def add_labelled_row(self, label, widget):
458
481
  self.add_row(self.labelled_widget(label, widget))
459
482
 
460
- def create_tab_widget(self, main_layout):
461
- tab_widget = QTabWidget()
462
- main_layout.addRow(tab_widget)
463
- return tab_widget
464
-
465
- def create_tab_layout(self):
466
- tab_layout = QFormLayout()
467
- tab_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
468
- tab_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
469
- tab_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
470
- tab_layout.setLabelAlignment(Qt.AlignLeft)
471
- return tab_layout
472
-
473
- def add_tab(self, tab_widget, title):
474
- tab = QWidget()
475
- tab_layout = self.create_tab_layout()
476
- tab.setLayout(tab_layout)
477
- tab_widget.addTab(tab, title)
478
- return tab_layout
479
-
480
483
  def add_field_to_layout(self, main_layout, tag, field_type, label, required=False, **kwargs):
481
484
  return self.add_field(tag, field_type, label, required, add_to_layout=main_layout, **kwargs)
482
485
 
@@ -1,5 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0915, R0912
2
- # pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914, C0302
2
+ # pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914, C0302, R0903
3
3
  import os
4
4
  import traceback
5
5
  from PySide6.QtCore import QTimer
@@ -8,7 +8,7 @@ from .. config.constants import constants
8
8
  from .. config.app_config import AppConfig
9
9
  from .. algorithms.align import validate_align_config
10
10
  from . action_config import (
11
- DefaultActionConfigurator,
11
+ DefaultActionConfigurator, add_tab, create_tab_layout, create_tab_widget,
12
12
  FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
13
13
  FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO, FIELD_REF_IDX
14
14
  )
@@ -76,10 +76,15 @@ class JobConfigurator(DefaultActionConfigurator):
76
76
  input_filepaths = input_filepaths.split(constants.PATH_SEPARATOR)
77
77
  self.working_path_label = QLabel(working_path or "Not set")
78
78
  self.input_path_label = QLabel(input_path or "Not set")
79
- self.input_widget.path_edit.setText('')
80
79
  if input_filepaths:
80
+ full_input_dir = os.path.join(working_path, input_path)
81
+ self.input_widget.selected_files = [os.path.join(full_input_dir, f)
82
+ for f in input_filepaths]
83
+ self.input_widget.path_edit.setText(full_input_dir)
81
84
  self.input_widget.files_mode_radio.setChecked(True)
82
85
  else:
86
+ full_input_dir = os.path.join(working_path, input_path)
87
+ self.input_widget.path_edit.setText(full_input_dir)
83
88
  self.input_widget.folder_mode_radio.setChecked(False)
84
89
  self.input_widget.text_changed_connect(self.update_paths_and_frames)
85
90
  self.input_widget.folder_mode_radio.toggled.connect(self.update_paths_and_frames)
@@ -195,10 +200,10 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
195
200
 
196
201
  def create_form(self, layout, action):
197
202
  super().create_form(layout, action)
198
- self.tab_widget = self.create_tab_widget(layout)
199
- self.general_tab_layout = self.add_tab(self.tab_widget, "General Parameters")
203
+ self.tab_widget = create_tab_widget(layout)
204
+ self.general_tab_layout = add_tab(self.tab_widget, "General Parameters")
200
205
  self.create_general_tab(self.general_tab_layout)
201
- self.algorithm_tab_layout = self.add_tab(self.tab_widget, "Stacking Algorithm")
206
+ self.algorithm_tab_layout = add_tab(self.tab_widget, "Stacking Algorithm")
202
207
  self.create_algorithm_tab(self.algorithm_tab_layout)
203
208
 
204
209
  def create_general_tab(self, layout):
@@ -214,7 +219,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
214
219
  expert=True,
215
220
  placeholder='relative to working path')
216
221
  self.add_field_to_layout(
217
- layout, 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
222
+ layout, 'scratch_output_dir', FIELD_BOOL, 'Scratch output folder before run',
218
223
  required=False, default=True)
219
224
 
220
225
  def create_algorithm_tab(self, layout):
@@ -225,7 +230,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
225
230
  default=constants.STACK_ALGO_DEFAULT)
226
231
  q_pyramid, q_depthmap = QWidget(), QWidget()
227
232
  for q in [q_pyramid, q_depthmap]:
228
- q.setLayout(self.create_tab_layout())
233
+ q.setLayout(create_tab_layout())
229
234
  stacked = QStackedWidget()
230
235
  stacked.addWidget(q_pyramid)
231
236
  stacked.addWidget(q_depthmap)
@@ -267,7 +272,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
267
272
  q_pyramid.layout(), 'pyramid_memory_limit', FIELD_FLOAT,
268
273
  'Memory limit (approx., GBytes)',
269
274
  expert=True,
270
- required=False, default=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
275
+ required=False, default=AppConfig.get('focus_stack_params')['memory_limit'],
271
276
  min_val=1.0, max_val=64.0)
272
277
  max_threads = self.add_field_to_layout(
273
278
  q_pyramid.layout(), 'pyramid_max_threads', FIELD_INT, 'Max num. of cores',
@@ -366,6 +371,14 @@ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
366
371
  self.add_field_to_layout(
367
372
  self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
368
373
  default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
374
+ self.add_field_to_layout(
375
+ self.general_tab_layout, 'scratch_output_dir', FIELD_BOOL,
376
+ 'Scratch output folder before run',
377
+ required=False, default=True)
378
+ self.add_field_to_layout(
379
+ self.general_tab_layout, 'delete_output_at_end', FIELD_BOOL,
380
+ 'Delete output at end of job',
381
+ required=False, default=False)
369
382
  self.add_field_to_layout(
370
383
  self.general_tab_layout, 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
371
384
  default=constants.DEFAULT_PLOT_STACK_BUNCH)
@@ -391,7 +404,7 @@ class MultiLayerConfigurator(DefaultActionConfigurator):
391
404
  expert=True,
392
405
  placeholder='relative to working path')
393
406
  self.add_field(
394
- 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
407
+ 'scratch_output_dir', FIELD_BOOL, 'Scratch output folder before run',
395
408
  required=False, default=True)
396
409
  self.add_field(
397
410
  'reverse_order', FIELD_BOOL, 'Reverse file order', required=False,
@@ -413,8 +426,11 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
413
426
  expert=True,
414
427
  placeholder='relative to working path')
415
428
  self.add_field(
416
- 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
429
+ 'scratch_output_dir', FIELD_BOOL, 'Scratch output folder before run',
417
430
  required=False, default=True)
431
+ self.add_field(
432
+ 'delete_output_at_end', FIELD_BOOL, 'Delete output at end of job',
433
+ required=False, default=False)
418
434
  self.add_field(
419
435
  'plot_path', FIELD_REL_PATH, 'Plots path', required=False,
420
436
  expert=True,
@@ -429,8 +445,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
429
445
  default=0)
430
446
  self.add_field(
431
447
  'step_process', FIELD_BOOL, 'Step process', required=False,
432
- expert=True,
433
- default=True)
448
+ expert=True, default=constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS)
434
449
  self.add_field(
435
450
  'max_threads', FIELD_INT, 'Max num. of cores',
436
451
  required=False, default=AppConfig.get('combined_actions_params')['max_threads'],
@@ -439,7 +454,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
439
454
  self.add_field(
440
455
  'chunk_submit', FIELD_BOOL, 'Submit in chunks',
441
456
  expert=True,
442
- required=False, default=constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
457
+ required=False, default=constants.DEFAULT_FWK_CHUNK_SUBMIT)
443
458
 
444
459
 
445
460
  class MaskNoiseConfigurator(DefaultActionConfigurator):
@@ -490,22 +505,22 @@ class SubsampleActionConfigurator(DefaultActionConfigurator):
490
505
  self.subsample_field.currentText() not in constants.FIELD_SUBSAMPLE_OPTIONS[:2])
491
506
 
492
507
 
493
- class AlignFramesConfigurator(SubsampleActionConfigurator):
494
- BORDER_MODE_OPTIONS = ['Constant', 'Replicate', 'Replicate and blur']
495
- TRANSFORM_OPTIONS = ['Rigid', 'Homography']
496
- METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
508
+ class AlignFramesConfigBase:
497
509
  MATCHING_METHOD_OPTIONS = ['K-nearest neighbors', 'Hamming distance']
498
- MODE_OPTIONS = ['Auto', 'Sequential', 'Parallel']
510
+ DETECTOR_DESCRIPTOR_TOOLTIPS = {
511
+ 'detector':
512
+ "SIFT: Requires SIFT descriptor and K-NN matching\n"
513
+ "ORB/AKAZE: Work best with Hamming distance",
514
+ 'descriptor':
515
+ "SIFT: Requires K-NN matching\n"
516
+ "ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors",
517
+ 'match_method':
518
+ "Automatically selected based on detector/descriptor combination"
499
519
 
500
- def __init__(self, expert, current_wd):
501
- super().__init__(expert, current_wd)
502
- self.matching_method_field = None
520
+ }
521
+
522
+ def __init__(self):
503
523
  self.info_label = None
504
- self.detector_field = None
505
- self.descriptor_field = None
506
- self.matching_method_field = None
507
- self.tab_widget = None
508
- self.current_tab_layout = None
509
524
 
510
525
  def show_info(self, message, timeout=3000):
511
526
  self.info_label.setText(message)
@@ -514,66 +529,83 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
514
529
  timer.timeout.connect(lambda: self.info_label.setText(''))
515
530
  timer.start(timeout)
516
531
 
517
- def change_match_config(self):
518
- detector = self.detector_field.currentText()
519
- descriptor = self.descriptor_field.currentText()
532
+ def change_match_config(
533
+ self, detector_field, descriptor_field, matching_method_field, show_info):
534
+ detector = detector_field.currentText()
535
+ descriptor = descriptor_field.currentText()
520
536
  match_method = dict(
521
537
  zip(self.MATCHING_METHOD_OPTIONS,
522
- constants.VALID_MATCHING_METHODS))[self.matching_method_field.currentText()]
538
+ constants.VALID_MATCHING_METHODS))[matching_method_field.currentText()]
523
539
  try:
524
540
  validate_align_config(detector, descriptor, match_method)
525
541
  except Exception as e:
526
- self.show_info(str(e))
542
+ show_info(str(e))
527
543
  if descriptor == constants.DETECTOR_SIFT and \
528
544
  match_method == constants.MATCHING_NORM_HAMMING:
529
- self.matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[0])
545
+ matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[0])
530
546
  if detector == constants.DETECTOR_ORB and descriptor == constants.DESCRIPTOR_AKAZE and \
531
547
  match_method == constants.MATCHING_NORM_HAMMING:
532
- self.matching_method_field.setCurrentText(constants.MATCHING_NORM_HAMMING)
548
+ matching_method_field.setCurrentText(constants.MATCHING_NORM_HAMMING)
533
549
  if detector == constants.DETECTOR_BRISK and descriptor == constants.DESCRIPTOR_AKAZE:
534
- self.descriptor_field.setCurrentText('BRISK')
550
+ descriptor_field.setCurrentText('BRISK')
535
551
  if detector == constants.DETECTOR_SURF and descriptor == constants.DESCRIPTOR_AKAZE:
536
- self.descriptor_field.setCurrentText('SIFT')
552
+ descriptor_field.setCurrentText('SIFT')
537
553
  if detector == constants.DETECTOR_SIFT and descriptor != constants.DESCRIPTOR_SIFT:
538
- self.descriptor_field.setCurrentText('SIFT')
554
+ descriptor_field.setCurrentText('SIFT')
539
555
  if detector in constants.NOKNN_METHODS['detectors'] and \
540
556
  descriptor in constants.NOKNN_METHODS['descriptors']:
541
557
  if match_method == constants.MATCHING_KNN:
542
- self.matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[1])
558
+ matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[1])
559
+
560
+
561
+ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase):
562
+ BORDER_MODE_OPTIONS = ['Constant', 'Replicate', 'Replicate and blur']
563
+ TRANSFORM_OPTIONS = ['Rigid', 'Homography']
564
+ METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
565
+ MODE_OPTIONS = ['Auto', 'Sequential', 'Parallel']
566
+
567
+ def __init__(self, expert, current_wd):
568
+ SubsampleActionConfigurator.__init__(self, expert, current_wd)
569
+ AlignFramesConfigBase.__init__(self)
570
+ self.matching_method_field = None
571
+ self.detector_field = None
572
+ self.descriptor_field = None
573
+ self.matching_method_field = None
574
+ self.tab_widget = None
575
+ self.current_tab_layout = None
543
576
 
544
577
  def create_form(self, layout, action):
545
578
  super().create_form(layout, action)
546
579
  self.detector_field = None
547
580
  self.descriptor_field = None
548
581
  self.matching_method_field = None
549
- self.tab_widget = self.create_tab_widget(layout)
550
- feature_layout = self.add_tab(self.tab_widget, "Feature extraction")
582
+ self.tab_widget = create_tab_widget(layout)
583
+ feature_layout = add_tab(self.tab_widget, "Feature extraction")
551
584
  self.create_feature_tab(feature_layout)
552
- transform_layout = self.add_tab(self.tab_widget, "Transform")
585
+ transform_layout = add_tab(self.tab_widget, "Transform")
553
586
  self.create_transform_tab(transform_layout)
554
- border_layout = self.add_tab(self.tab_widget, "Border")
587
+ border_layout = add_tab(self.tab_widget, "Border")
555
588
  self.create_border_tab(border_layout)
556
- misc_layout = self.add_tab(self.tab_widget, "Miscellanea")
589
+ misc_layout = add_tab(self.tab_widget, "Miscellanea")
557
590
  self.create_miscellanea_tab(misc_layout)
558
591
 
559
592
  def create_feature_tab(self, layout):
593
+
594
+ def change_match_config():
595
+ self.change_match_config(
596
+ self.detector_field, self.descriptor_field,
597
+ self. matching_method_field, self.show_info)
560
598
  self.add_bold_label_to_layout(layout, "Feature identification:")
561
599
  self.detector_field = self.add_field_to_layout(
562
600
  layout, 'detector', FIELD_COMBO, 'Detector', required=False,
563
- options=constants.VALID_DETECTORS, default=constants.DEFAULT_DETECTOR)
601
+ options=constants.VALID_DETECTORS, default=AppConfig.get('detector'))
564
602
  self.descriptor_field = self.add_field_to_layout(
565
603
  layout, 'descriptor', FIELD_COMBO, 'Descriptor', required=False,
566
- options=constants.VALID_DESCRIPTORS, default=constants.DEFAULT_DESCRIPTOR)
567
- self.detector_field.setToolTip(
568
- "SIFT: Requires SIFT descriptor and K-NN matching\n"
569
- "ORB/AKAZE: Work best with Hamming distance"
570
- )
571
- self.descriptor_field.setToolTip(
572
- "SIFT: Requires K-NN matching\n"
573
- "ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors"
574
- )
575
- self.detector_field.currentIndexChanged.connect(self.change_match_config)
576
- self.descriptor_field.currentIndexChanged.connect(self.change_match_config)
604
+ options=constants.VALID_DESCRIPTORS, default=AppConfig.get('descriptor'))
605
+ self.detector_field.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['detector'])
606
+ self.descriptor_field.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['descriptor'])
607
+ self.detector_field.currentIndexChanged.connect(change_match_config)
608
+ self.descriptor_field.currentIndexChanged.connect(change_match_config)
577
609
  self.info_label = QLabel()
578
610
  self.info_label.setStyleSheet("color: orange; font-style: italic;")
579
611
  layout.addRow(self.info_label)
@@ -581,11 +613,9 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
581
613
  self.matching_method_field = self.add_field_to_layout(
582
614
  layout, 'match_method', FIELD_COMBO, 'Match method', required=False,
583
615
  options=self.MATCHING_METHOD_OPTIONS, values=constants.VALID_MATCHING_METHODS,
584
- default=constants.DEFAULT_MATCHING_METHOD)
585
- self.matching_method_field.setToolTip(
586
- "Automatically selected based on detector/descriptor combination"
587
- )
588
- self.matching_method_field.currentIndexChanged.connect(self.change_match_config)
616
+ default=AppConfig.get('match_method'))
617
+ self.matching_method_field.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['match_method'])
618
+ self.matching_method_field.currentIndexChanged.connect(change_match_config)
589
619
  self.add_field_to_layout(
590
620
  layout, 'flann_idx_kdtree', FIELD_INT, 'Flann idx kdtree', required=False,
591
621
  expert=True,
@@ -615,9 +645,9 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
615
645
  options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
616
646
  default=constants.DEFAULT_TRANSFORM)
617
647
  method = self.add_field_to_layout(
618
- layout, 'align_method', FIELD_COMBO, 'Align method', required=False,
619
- options=self.METHOD_OPTIONS, values=constants.VALID_ALIGN_METHODS,
620
- default=constants.DEFAULT_ALIGN_METHOD)
648
+ layout, 'align_method', FIELD_COMBO, 'Estimation method', required=False,
649
+ options=self.METHOD_OPTIONS, values=constants.VALID_ESTIMATION_METHODS,
650
+ default=constants.DEFAULT_ESTIMATION_METHOD)
621
651
  rans_threshold = self.add_field_to_layout(
622
652
  layout, 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
623
653
  expert=True,
@@ -662,6 +692,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
662
692
 
663
693
  transform.currentIndexChanged.connect(change_transform)
664
694
  change_transform()
695
+ phase_corr_fallback = self.add_field_to_layout(
696
+ layout, 'phase_corr_fallback', FIELD_BOOL, "Phase correlation as fallback",
697
+ required=False, expert=True, default=constants.DEFAULT_PHASE_CORR_FALLBACK)
698
+ phase_corr_fallback.setToolTip(
699
+ "Align using phase correlation algorithm if the number of matches\n"
700
+ "is too low to determine the transformation.\n"
701
+ "This algorithm is not very precise,\n"
702
+ "and may help only in case of blurred images.")
665
703
  self.add_field_to_layout(
666
704
  layout, 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
667
705
  expert=True,
@@ -696,7 +734,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
696
734
  self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
697
735
  memory_limit = self.add_field_to_layout(
698
736
  layout, 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
699
- required=False, default=constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
737
+ required=False, default=AppConfig.get('align_frames_params')['memory_limit'],
700
738
  min_val=1.0, max_val=64.0)
701
739
  max_threads = self.add_field_to_layout(
702
740
  layout, 'max_threads', FIELD_INT, 'Max num. of cores',
@@ -710,6 +748,10 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
710
748
  layout, 'bw_matching', FIELD_BOOL, 'Match using black & white',
711
749
  expert=True,
712
750
  required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
751
+ delta_max = self.add_field_to_layout(
752
+ layout, 'delta_max', FIELD_INT, 'Max frames skip',
753
+ required=False, default=constants.DEFAULT_ALIGN_DELTA_MAX,
754
+ min_val=1, max_val=128)
713
755
 
714
756
  def change_mode():
715
757
  text = mode.currentText()
@@ -718,6 +760,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
718
760
  max_threads.setEnabled(enabled)
719
761
  chunk_submit.setEnabled(enabled)
720
762
  bw_matching.setEnabled(enabled)
763
+ delta_max.setEnabled(enabled)
721
764
 
722
765
  mode.currentIndexChanged.connect(change_mode)
723
766
 
@@ -1,14 +1,17 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, W0718, E1101, C0103
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0718, E1101, C0103, R0914
2
2
  import webbrowser
3
3
  import subprocess
4
4
  import os
5
+ import numpy as np
6
+ import cv2
5
7
  from PySide6.QtWidgets import QSizePolicy, QVBoxLayout, QWidget, QLabel, QStackedWidget
6
8
  from PySide6.QtPdf import QPdfDocument
7
9
  from PySide6.QtPdfWidgets import QPdfView
8
10
  from PySide6.QtCore import Qt, QMargins
9
- from PySide6.QtGui import QPixmap
11
+ from PySide6.QtGui import QPixmap, QImage
10
12
  from .. config.gui_constants import gui_constants
11
13
  from .. core.core_utils import running_under_windows, running_under_macos
14
+ from .. algorithms.utils import read_img
12
15
 
13
16
 
14
17
  def open_file(file_path):
@@ -74,7 +77,28 @@ class GuiImageView(QWidget):
74
77
  self.image_label.setAlignment(Qt.AlignCenter)
75
78
  self.main_layout.addWidget(self.image_label)
76
79
  self.setLayout(self.main_layout)
77
- pixmap = QPixmap(file_path)
80
+ try:
81
+ img = read_img(file_path)
82
+ height, width = img.shape[:2]
83
+ scale_factor = gui_constants.GUI_IMG_WIDTH / width
84
+ new_height = int(height * scale_factor)
85
+ img = cv2.resize(img, (gui_constants.GUI_IMG_WIDTH, new_height),
86
+ interpolation=cv2.INTER_LINEAR)
87
+ except Exception as e:
88
+ raise RuntimeError(f"Can't load file: {file_path}.") from e
89
+ if img.dtype == np.uint16:
90
+ img = (img // 256).astype(np.uint8)
91
+ if len(img.shape) == 3:
92
+ h, w, ch = img.shape
93
+ bytes_per_line = ch * w
94
+ rgb_image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
95
+ q_img = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
96
+ else:
97
+ h, w = img.shape
98
+ bytes_per_line = w
99
+ q_img = QImage(img.data, w, h, bytes_per_line, QImage.Format_Grayscale8)
100
+ pixmap = QPixmap.fromImage(q_img)
101
+ self.image_label.setPixmap(pixmap)
78
102
  if pixmap:
79
103
  scaled_pixmap = pixmap.scaledToWidth(
80
104
  gui_constants.GUI_IMG_WIDTH, Qt.SmoothTransformation)
@@ -215,8 +215,7 @@ class RunWindow(QTextEditLogger):
215
215
  raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
216
216
  self.image_views.append(image_view)
217
217
  self.image_layout.addWidget(image_view)
218
- max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
219
- needed_width = max_width + 20
218
+ needed_width = gui_constants.GUI_IMG_WIDTH + 20
220
219
  self.right_area.setFixedWidth(needed_width)
221
220
  self.image_area_widget.setFixedWidth(needed_width)
222
221
  self.right_area.updateGeometry()
@@ -3,9 +3,9 @@
3
3
  import os
4
4
  import subprocess
5
5
  from PySide6.QtCore import Qt
6
- from PySide6.QtGui import QGuiApplication, QAction, QIcon
6
+ from PySide6.QtGui import QGuiApplication, QAction, QPalette
7
7
  from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QMessageBox,
8
- QSplitter, QToolBar, QMenu, QMainWindow)
8
+ QSplitter, QToolBar, QMenu, QMainWindow, QApplication)
9
9
  from .. config.constants import constants
10
10
  from .. config.app_config import AppConfig
11
11
  from .. core.core_utils import running_under_windows, running_under_macos
@@ -82,7 +82,9 @@ class MainWindow(QMainWindow, LogManager):
82
82
  "Run Job": self.run_job,
83
83
  "Run All Jobs": self.run_all_jobs,
84
84
  }
85
- self.menu_manager = MenuManager(self.menuBar(), actions, self.project_editor, self)
85
+ dark_theme = self.is_dark_theme()
86
+ self.menu_manager = MenuManager(
87
+ self.menuBar(), actions, self.project_editor, dark_theme, self)
86
88
  self.script_dir = os.path.dirname(__file__)
87
89
  self._windows = []
88
90
  self._workers = []
@@ -107,7 +109,7 @@ class MainWindow(QMainWindow, LogManager):
107
109
  top_widget = QWidget()
108
110
  top_widget.setLayout(h_layout)
109
111
  h_splitter.addWidget(top_widget)
110
- self.tab_widget = TabWidgetWithPlaceholder()
112
+ self.tab_widget = TabWidgetWithPlaceholder(dark_theme)
111
113
  self.tab_widget.resize(1000, 500)
112
114
  h_splitter.addWidget(self.tab_widget)
113
115
  self.job_list().currentRowChanged.connect(self.project_editor.on_job_selected)
@@ -128,6 +130,7 @@ class MainWindow(QMainWindow, LogManager):
128
130
  layout.addWidget(h_splitter)
129
131
  self.central_widget.setLayout(layout)
130
132
  self.update_title()
133
+ QApplication.instance().paletteChanged.connect(self.on_theme_changed)
131
134
 
132
135
  def handle_modified(modified):
133
136
  self.save_actions_set_enabled(modified)
@@ -367,9 +370,6 @@ class MainWindow(QMainWindow, LogManager):
367
370
  menu.exec(event.globalPos())
368
371
  # pylint: enable=C0103
369
372
 
370
- def get_icon(self, icon):
371
- return QIcon(os.path.join(self.script_dir, f"img/{icon}.png"))
372
-
373
373
  def get_retouch_path(self, job):
374
374
  frames_path = [get_action_output_path(action)[0]
375
375
  for action in job.sub_actions
@@ -581,3 +581,16 @@ class MainWindow(QMainWindow, LogManager):
581
581
  for action in self.findChildren(QAction):
582
582
  if action.property("requires_file"):
583
583
  action.setEnabled(enabled)
584
+
585
+ def is_dark_theme(self):
586
+ palette = QApplication.palette()
587
+ window_color = palette.color(QPalette.Window)
588
+ brightness = (window_color.red() * 0.299 +
589
+ window_color.green() * 0.587 +
590
+ window_color.blue() * 0.114)
591
+ return brightness < 128
592
+
593
+ def on_theme_changed(self):
594
+ dark_theme = self.is_dark_theme()
595
+ self.menu_manager.change_theme(dark_theme)
596
+ self.tab_widget.change_theme(dark_theme)
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, R0904, E0611, R0902, W0201
1
+ # pylint: disable=C0114, C0115, C0116, R0904, E0611, R0902, W0201, R0913, R0917
2
2
  import os
3
3
  from functools import partial
4
4
  from PySide6.QtCore import Signal, QObject
@@ -12,11 +12,12 @@ from .recent_file_manager import RecentFileManager
12
12
  class MenuManager(QObject):
13
13
  open_file_requested = Signal(str)
14
14
 
15
- def __init__(self, menubar, actions, project_editor, parent):
15
+ def __init__(self, menubar, actions, project_editor, dark_theme, parent):
16
16
  super().__init__(parent)
17
17
  self.script_dir = os.path.dirname(__file__)
18
18
  self._recent_file_manager = RecentFileManager("shinestacker-recent-project-files.txt")
19
19
  self.project_editor = project_editor
20
+ self.dark_theme = dark_theme
20
21
  self.parent = parent
21
22
  self.menubar = menubar
22
23
  self.actions = actions
@@ -58,8 +59,9 @@ class MenuManager(QObject):
58
59
  "Run All Jobs": "Run all jobs",
59
60
  }
60
61
 
61
- def get_icon(self, icon):
62
- return QIcon(os.path.join(self.script_dir, f"img/{icon}.png"))
62
+ def get_icon(self, icon_name):
63
+ icon_dir = 'dark' if self.dark_theme else 'light'
64
+ return QIcon(os.path.join(self.script_dir, f"img/{icon_dir}/{icon_name}.png"))
63
65
 
64
66
  def action(self, name, requires_file=False):
65
67
  action = QAction(name, self.parent)
@@ -68,9 +70,11 @@ class MenuManager(QObject):
68
70
  shortcut = self.shortcuts.get(name, '')
69
71
  if shortcut:
70
72
  action.setShortcut(shortcut)
71
- icon = self.icons.get(name, '')
72
- if icon:
73
- action.setIcon(self.get_icon(icon))
73
+ icon_name = self.icons.get(name, '')
74
+ if icon_name:
75
+ action.setIcon(self.get_icon(icon_name))
76
+ action.setProperty('theme_dependent', True)
77
+ action.setProperty('base_icon_name', icon_name)
74
78
  tooltip = self.tooltips.get(name, '')
75
79
  if tooltip:
76
80
  action.setToolTip(tooltip)
@@ -79,6 +83,13 @@ class MenuManager(QObject):
79
83
  action.triggered.connect(action_fun)
80
84
  return action
81
85
 
86
+ def change_theme(self, dark_theme):
87
+ self.dark_theme = dark_theme
88
+ for action in self.parent.findChildren(QAction):
89
+ if action.property("theme_dependent"):
90
+ base_name = action.property("base_icon_name")
91
+ action.setIcon(self.get_icon(base_name))
92
+
82
93
  def update_recent_files(self):
83
94
  self.recent_files_menu.clear()
84
95
  recent_files = self._recent_file_manager.get_files_with_display_names()
@@ -76,21 +76,32 @@ class NewProjectDialog(BaseFormDialog):
76
76
  self.focus_stack_depth_map.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_DEPTH_MAP)
77
77
  self.multi_layer = QCheckBox()
78
78
  self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
79
+
79
80
  step1_group = QGroupBox("1) Select Input")
80
81
  step1_layout = QVBoxLayout()
81
82
  step1_layout.setContentsMargins(15, 0, 15, 15)
82
83
  step1_layout.addWidget(
83
84
  QLabel("Select a folder containing "
84
85
  "all your images, or specific image files."))
85
- input_form = QFormLayout()
86
- input_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
87
- input_form.setFormAlignment(Qt.AlignLeft)
88
- input_form.setLabelAlignment(Qt.AlignLeft)
86
+ input_layout = QHBoxLayout()
87
+ input_layout.setContentsMargins(0, 0, 0, 0)
88
+ input_layout.setSpacing(10)
89
+ input_label = QLabel("Input:")
90
+ input_label.setFixedWidth(60)
89
91
  self.input_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
92
+ input_layout.addWidget(input_label)
93
+ input_layout.addWidget(self.input_widget)
94
+ frames_layout = QHBoxLayout()
95
+ frames_layout.setContentsMargins(0, 0, 0, 0)
96
+ frames_layout.setSpacing(10)
97
+ frames_label = QLabel("Number of selected frames:")
98
+ frames_label.setFixedWidth(180)
90
99
  self.frames_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
91
- input_form.addRow("Input:", self.input_widget)
92
- input_form.addRow("Number of frames: ", self.frames_label)
93
- step1_layout.addLayout(input_form)
100
+ frames_layout.addWidget(frames_label)
101
+ frames_layout.addWidget(self.frames_label)
102
+ frames_layout.addStretch()
103
+ step1_layout.addLayout(input_layout)
104
+ step1_layout.addLayout(frames_layout)
94
105
  step1_group.setLayout(step1_layout)
95
106
  self.form_layout.addRow(step1_group)
96
107
  step2_group = QGroupBox("2) Basic Options")
@@ -169,13 +180,11 @@ class NewProjectDialog(BaseFormDialog):
169
180
  border-radius: 5px;
170
181
  margin-top: 10px;
171
182
  padding-top: 15px;
172
- background-color: #f8f8f8;
173
183
  }
174
184
  QGroupBox::title {
175
185
  subcontrol-origin: margin;
176
186
  left: 10px;
177
187
  padding: 0 5px 0 5px;
178
- background-color: #f8f8f8;
179
188
  }
180
189
  """
181
190
  for group in [step1_group, step2_group, step3_group, step4_group]: