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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +184 -80
- shinestacker/algorithms/align_auto.py +13 -11
- shinestacker/algorithms/align_parallel.py +41 -16
- shinestacker/algorithms/base_stack_algo.py +1 -1
- shinestacker/algorithms/noise_detection.py +10 -8
- shinestacker/algorithms/pyramid_tiles.py +1 -1
- shinestacker/algorithms/stack.py +9 -0
- shinestacker/algorithms/stack_framework.py +49 -25
- shinestacker/algorithms/utils.py +5 -1
- shinestacker/algorithms/vignetting.py +16 -3
- shinestacker/app/settings_dialog.py +303 -136
- shinestacker/config/constants.py +10 -5
- shinestacker/config/settings.py +29 -8
- shinestacker/core/core_utils.py +1 -0
- shinestacker/core/exceptions.py +1 -1
- shinestacker/core/framework.py +9 -4
- shinestacker/gui/action_config.py +23 -20
- shinestacker/gui/action_config_dialog.py +107 -64
- shinestacker/gui/gui_images.py +27 -3
- shinestacker/gui/gui_run.py +1 -2
- shinestacker/gui/img/dark/close-round-line-icon.png +0 -0
- shinestacker/gui/img/dark/forward-button-icon.png +0 -0
- shinestacker/gui/img/dark/play-button-round-icon.png +0 -0
- shinestacker/gui/img/dark/plus-round-line-icon.png +0 -0
- shinestacker/gui/img/dark/shinestacker_bkg.png +0 -0
- shinestacker/gui/main_window.py +20 -7
- shinestacker/gui/menu_manager.py +18 -7
- shinestacker/gui/new_project.py +18 -9
- shinestacker/gui/project_controller.py +13 -6
- shinestacker/gui/project_editor.py +12 -2
- shinestacker/gui/project_model.py +4 -4
- shinestacker/gui/tab_widget.py +16 -6
- shinestacker/retouch/adjustments.py +5 -0
- {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/METADATA +35 -39
- {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/RECORD +45 -40
- /shinestacker/gui/img/{close-round-line-icon.png → light/close-round-line-icon.png} +0 -0
- /shinestacker/gui/img/{forward-button-icon.png → light/forward-button-icon.png} +0 -0
- /shinestacker/gui/img/{play-button-round-icon.png → light/play-button-round-icon.png} +0 -0
- /shinestacker/gui/img/{plus-round-line-icon.png → light/plus-round-line-icon.png} +0 -0
- /shinestacker/gui/{ico → img/light}/shinestacker_bkg.png +0 -0
- {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.7.0.dist-info → shinestacker-1.8.1.dist-info}/licenses/LICENSE +0 -0
- {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 =
|
|
199
|
-
self.general_tab_layout =
|
|
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 =
|
|
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
|
|
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(
|
|
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=
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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(
|
|
518
|
-
|
|
519
|
-
|
|
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))[
|
|
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
|
-
|
|
542
|
+
show_info(str(e))
|
|
527
543
|
if descriptor == constants.DETECTOR_SIFT and \
|
|
528
544
|
match_method == constants.MATCHING_NORM_HAMMING:
|
|
529
|
-
|
|
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
|
-
|
|
548
|
+
matching_method_field.setCurrentText(constants.MATCHING_NORM_HAMMING)
|
|
533
549
|
if detector == constants.DETECTOR_BRISK and descriptor == constants.DESCRIPTOR_AKAZE:
|
|
534
|
-
|
|
550
|
+
descriptor_field.setCurrentText('BRISK')
|
|
535
551
|
if detector == constants.DETECTOR_SURF and descriptor == constants.DESCRIPTOR_AKAZE:
|
|
536
|
-
|
|
552
|
+
descriptor_field.setCurrentText('SIFT')
|
|
537
553
|
if detector == constants.DETECTOR_SIFT and descriptor != constants.DESCRIPTOR_SIFT:
|
|
538
|
-
|
|
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
|
-
|
|
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 =
|
|
550
|
-
feature_layout =
|
|
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 =
|
|
585
|
+
transform_layout = add_tab(self.tab_widget, "Transform")
|
|
553
586
|
self.create_transform_tab(transform_layout)
|
|
554
|
-
border_layout =
|
|
587
|
+
border_layout = add_tab(self.tab_widget, "Border")
|
|
555
588
|
self.create_border_tab(border_layout)
|
|
556
|
-
misc_layout =
|
|
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=
|
|
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=
|
|
567
|
-
self.detector_field.setToolTip(
|
|
568
|
-
|
|
569
|
-
|
|
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=
|
|
585
|
-
self.matching_method_field.setToolTip(
|
|
586
|
-
|
|
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, '
|
|
619
|
-
options=self.METHOD_OPTIONS, values=constants.
|
|
620
|
-
default=constants.
|
|
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=
|
|
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
|
|
shinestacker/gui/gui_images.py
CHANGED
|
@@ -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
|
-
|
|
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)
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -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
|
-
|
|
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()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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)
|
shinestacker/gui/menu_manager.py
CHANGED
|
@@ -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,
|
|
62
|
-
|
|
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
|
-
|
|
72
|
-
if
|
|
73
|
-
action.setIcon(self.get_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()
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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]:
|