shinestacker 1.8.0__py3-none-any.whl → 1.9.3__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 +202 -81
- shinestacker/algorithms/align_auto.py +13 -11
- shinestacker/algorithms/align_parallel.py +50 -21
- shinestacker/algorithms/balance.py +1 -1
- shinestacker/algorithms/base_stack_algo.py +1 -1
- shinestacker/algorithms/exif.py +848 -127
- shinestacker/algorithms/multilayer.py +6 -4
- shinestacker/algorithms/noise_detection.py +10 -8
- shinestacker/algorithms/pyramid_tiles.py +1 -1
- shinestacker/algorithms/stack.py +33 -17
- shinestacker/algorithms/stack_framework.py +16 -11
- shinestacker/algorithms/utils.py +18 -2
- shinestacker/algorithms/vignetting.py +16 -3
- shinestacker/app/main.py +1 -1
- shinestacker/app/settings_dialog.py +297 -173
- shinestacker/config/constants.py +10 -6
- shinestacker/config/settings.py +25 -7
- shinestacker/core/exceptions.py +1 -1
- shinestacker/core/framework.py +2 -2
- shinestacker/gui/action_config.py +23 -20
- shinestacker/gui/action_config_dialog.py +38 -25
- shinestacker/gui/config_dialog.py +6 -5
- shinestacker/gui/folder_file_selection.py +3 -2
- shinestacker/gui/gui_images.py +27 -3
- shinestacker/gui/gui_run.py +2 -2
- shinestacker/gui/main_window.py +6 -0
- shinestacker/gui/menu_manager.py +8 -2
- shinestacker/gui/new_project.py +23 -12
- shinestacker/gui/project_controller.py +14 -6
- shinestacker/gui/project_editor.py +12 -2
- shinestacker/gui/project_model.py +4 -4
- shinestacker/retouch/brush_tool.py +20 -0
- shinestacker/retouch/exif_data.py +106 -38
- shinestacker/retouch/file_loader.py +3 -3
- shinestacker/retouch/image_editor_ui.py +79 -3
- shinestacker/retouch/image_viewer.py +6 -1
- shinestacker/retouch/io_gui_handler.py +13 -16
- shinestacker/retouch/shortcuts_help.py +15 -8
- shinestacker/retouch/view_strategy.py +12 -2
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/METADATA +37 -39
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/RECORD +46 -46
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/WHEEL +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.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
|
|
|
@@ -6,9 +6,10 @@ from PySide6.QtCore import QTimer
|
|
|
6
6
|
from PySide6.QtWidgets import QWidget, QLabel, QMessageBox, QStackedWidget
|
|
7
7
|
from .. config.constants import constants
|
|
8
8
|
from .. config.app_config import AppConfig
|
|
9
|
+
from .. algorithms.utils import EXTENSIONS_SUPPORTED
|
|
9
10
|
from .. algorithms.align import validate_align_config
|
|
10
11
|
from . action_config import (
|
|
11
|
-
DefaultActionConfigurator,
|
|
12
|
+
DefaultActionConfigurator, add_tab, create_tab_layout, create_tab_widget,
|
|
12
13
|
FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
|
|
13
14
|
FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO, FIELD_REF_IDX
|
|
14
15
|
)
|
|
@@ -76,10 +77,15 @@ class JobConfigurator(DefaultActionConfigurator):
|
|
|
76
77
|
input_filepaths = input_filepaths.split(constants.PATH_SEPARATOR)
|
|
77
78
|
self.working_path_label = QLabel(working_path or "Not set")
|
|
78
79
|
self.input_path_label = QLabel(input_path or "Not set")
|
|
79
|
-
self.input_widget.path_edit.setText('')
|
|
80
80
|
if input_filepaths:
|
|
81
|
+
full_input_dir = os.path.join(working_path, input_path)
|
|
82
|
+
self.input_widget.selected_files = [os.path.join(full_input_dir, f)
|
|
83
|
+
for f in input_filepaths]
|
|
84
|
+
self.input_widget.path_edit.setText(full_input_dir)
|
|
81
85
|
self.input_widget.files_mode_radio.setChecked(True)
|
|
82
86
|
else:
|
|
87
|
+
full_input_dir = os.path.join(working_path, input_path)
|
|
88
|
+
self.input_widget.path_edit.setText(full_input_dir)
|
|
83
89
|
self.input_widget.folder_mode_radio.setChecked(False)
|
|
84
90
|
self.input_widget.text_changed_connect(self.update_paths_and_frames)
|
|
85
91
|
self.input_widget.folder_mode_radio.toggled.connect(self.update_paths_and_frames)
|
|
@@ -117,7 +123,7 @@ class JobConfigurator(DefaultActionConfigurator):
|
|
|
117
123
|
return 0
|
|
118
124
|
count = 0
|
|
119
125
|
for filename in os.listdir(path):
|
|
120
|
-
if filename.lower()
|
|
126
|
+
if os.path.splitext(filename)[-1][1:].lower() in EXTENSIONS_SUPPORTED:
|
|
121
127
|
count += 1
|
|
122
128
|
return count
|
|
123
129
|
|
|
@@ -195,10 +201,10 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
|
195
201
|
|
|
196
202
|
def create_form(self, layout, action):
|
|
197
203
|
super().create_form(layout, action)
|
|
198
|
-
self.tab_widget =
|
|
199
|
-
self.general_tab_layout =
|
|
204
|
+
self.tab_widget = create_tab_widget(layout)
|
|
205
|
+
self.general_tab_layout = add_tab(self.tab_widget, "General Parameters")
|
|
200
206
|
self.create_general_tab(self.general_tab_layout)
|
|
201
|
-
self.algorithm_tab_layout =
|
|
207
|
+
self.algorithm_tab_layout = add_tab(self.tab_widget, "Stacking Algorithm")
|
|
202
208
|
self.create_algorithm_tab(self.algorithm_tab_layout)
|
|
203
209
|
|
|
204
210
|
def create_general_tab(self, layout):
|
|
@@ -225,7 +231,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
|
225
231
|
default=constants.STACK_ALGO_DEFAULT)
|
|
226
232
|
q_pyramid, q_depthmap = QWidget(), QWidget()
|
|
227
233
|
for q in [q_pyramid, q_depthmap]:
|
|
228
|
-
q.setLayout(
|
|
234
|
+
q.setLayout(create_tab_layout())
|
|
229
235
|
stacked = QStackedWidget()
|
|
230
236
|
stacked.addWidget(q_pyramid)
|
|
231
237
|
stacked.addWidget(q_depthmap)
|
|
@@ -267,7 +273,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
|
|
|
267
273
|
q_pyramid.layout(), 'pyramid_memory_limit', FIELD_FLOAT,
|
|
268
274
|
'Memory limit (approx., GBytes)',
|
|
269
275
|
expert=True,
|
|
270
|
-
required=False, default=
|
|
276
|
+
required=False, default=AppConfig.get('focus_stack_params')['memory_limit'],
|
|
271
277
|
min_val=1.0, max_val=64.0)
|
|
272
278
|
max_threads = self.add_field_to_layout(
|
|
273
279
|
q_pyramid.layout(), 'pyramid_max_threads', FIELD_INT, 'Max num. of cores',
|
|
@@ -366,10 +372,6 @@ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
|
|
|
366
372
|
self.add_field_to_layout(
|
|
367
373
|
self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
|
|
368
374
|
default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
|
|
369
|
-
self.add_field_to_layout(
|
|
370
|
-
self.general_tab_layout, 'scratch_output_dir', FIELD_BOOL,
|
|
371
|
-
'Scratch output folder before run',
|
|
372
|
-
required=False, default=True)
|
|
373
375
|
self.add_field_to_layout(
|
|
374
376
|
self.general_tab_layout, 'delete_output_at_end', FIELD_BOOL,
|
|
375
377
|
'Delete output at end of job',
|
|
@@ -440,8 +442,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
440
442
|
default=0)
|
|
441
443
|
self.add_field(
|
|
442
444
|
'step_process', FIELD_BOOL, 'Step process', required=False,
|
|
443
|
-
expert=True,
|
|
444
|
-
default=True)
|
|
445
|
+
expert=True, default=constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS)
|
|
445
446
|
self.add_field(
|
|
446
447
|
'max_threads', FIELD_INT, 'Max num. of cores',
|
|
447
448
|
required=False, default=AppConfig.get('combined_actions_params')['max_threads'],
|
|
@@ -450,7 +451,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
450
451
|
self.add_field(
|
|
451
452
|
'chunk_submit', FIELD_BOOL, 'Submit in chunks',
|
|
452
453
|
expert=True,
|
|
453
|
-
required=False, default=constants.
|
|
454
|
+
required=False, default=constants.DEFAULT_FWK_CHUNK_SUBMIT)
|
|
454
455
|
|
|
455
456
|
|
|
456
457
|
class MaskNoiseConfigurator(DefaultActionConfigurator):
|
|
@@ -575,14 +576,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
575
576
|
self.detector_field = None
|
|
576
577
|
self.descriptor_field = None
|
|
577
578
|
self.matching_method_field = None
|
|
578
|
-
self.tab_widget =
|
|
579
|
-
feature_layout =
|
|
579
|
+
self.tab_widget = create_tab_widget(layout)
|
|
580
|
+
feature_layout = add_tab(self.tab_widget, "Feature extraction")
|
|
580
581
|
self.create_feature_tab(feature_layout)
|
|
581
|
-
transform_layout =
|
|
582
|
+
transform_layout = add_tab(self.tab_widget, "Transform")
|
|
582
583
|
self.create_transform_tab(transform_layout)
|
|
583
|
-
border_layout =
|
|
584
|
+
border_layout = add_tab(self.tab_widget, "Border")
|
|
584
585
|
self.create_border_tab(border_layout)
|
|
585
|
-
misc_layout =
|
|
586
|
+
misc_layout = add_tab(self.tab_widget, "Miscellanea")
|
|
586
587
|
self.create_miscellanea_tab(misc_layout)
|
|
587
588
|
|
|
588
589
|
def create_feature_tab(self, layout):
|
|
@@ -591,7 +592,6 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
591
592
|
self.change_match_config(
|
|
592
593
|
self.detector_field, self.descriptor_field,
|
|
593
594
|
self. matching_method_field, self.show_info)
|
|
594
|
-
|
|
595
595
|
self.add_bold_label_to_layout(layout, "Feature identification:")
|
|
596
596
|
self.detector_field = self.add_field_to_layout(
|
|
597
597
|
layout, 'detector', FIELD_COMBO, 'Detector', required=False,
|
|
@@ -642,9 +642,9 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
642
642
|
options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
|
|
643
643
|
default=constants.DEFAULT_TRANSFORM)
|
|
644
644
|
method = self.add_field_to_layout(
|
|
645
|
-
layout, 'align_method', FIELD_COMBO, '
|
|
646
|
-
options=self.METHOD_OPTIONS, values=constants.
|
|
647
|
-
default=constants.
|
|
645
|
+
layout, 'align_method', FIELD_COMBO, 'Estimation method', required=False,
|
|
646
|
+
options=self.METHOD_OPTIONS, values=constants.VALID_ESTIMATION_METHODS,
|
|
647
|
+
default=constants.DEFAULT_ESTIMATION_METHOD)
|
|
648
648
|
rans_threshold = self.add_field_to_layout(
|
|
649
649
|
layout, 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
|
|
650
650
|
expert=True,
|
|
@@ -689,6 +689,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
689
689
|
|
|
690
690
|
transform.currentIndexChanged.connect(change_transform)
|
|
691
691
|
change_transform()
|
|
692
|
+
phase_corr_fallback = self.add_field_to_layout(
|
|
693
|
+
layout, 'phase_corr_fallback', FIELD_BOOL, "Phase correlation as fallback",
|
|
694
|
+
required=False, expert=True, default=constants.DEFAULT_PHASE_CORR_FALLBACK)
|
|
695
|
+
phase_corr_fallback.setToolTip(
|
|
696
|
+
"Align using phase correlation algorithm if the number of matches\n"
|
|
697
|
+
"is too low to determine the transformation.\n"
|
|
698
|
+
"This algorithm is not very precise,\n"
|
|
699
|
+
"and may help only in case of blurred images.")
|
|
692
700
|
self.add_field_to_layout(
|
|
693
701
|
layout, 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
|
|
694
702
|
expert=True,
|
|
@@ -723,7 +731,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
723
731
|
self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
|
|
724
732
|
memory_limit = self.add_field_to_layout(
|
|
725
733
|
layout, 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
|
|
726
|
-
required=False, default=
|
|
734
|
+
required=False, default=AppConfig.get('align_frames_params')['memory_limit'],
|
|
727
735
|
min_val=1.0, max_val=64.0)
|
|
728
736
|
max_threads = self.add_field_to_layout(
|
|
729
737
|
layout, 'max_threads', FIELD_INT, 'Max num. of cores',
|
|
@@ -737,6 +745,10 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
737
745
|
layout, 'bw_matching', FIELD_BOOL, 'Match using black & white',
|
|
738
746
|
expert=True,
|
|
739
747
|
required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
|
|
748
|
+
delta_max = self.add_field_to_layout(
|
|
749
|
+
layout, 'delta_max', FIELD_INT, 'Max frames skip',
|
|
750
|
+
required=False, default=constants.DEFAULT_ALIGN_DELTA_MAX,
|
|
751
|
+
min_val=1, max_val=128)
|
|
740
752
|
|
|
741
753
|
def change_mode():
|
|
742
754
|
text = mode.currentText()
|
|
@@ -745,6 +757,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
745
757
|
max_threads.setEnabled(enabled)
|
|
746
758
|
chunk_submit.setEnabled(enabled)
|
|
747
759
|
bw_matching.setEnabled(enabled)
|
|
760
|
+
delta_max.setEnabled(enabled)
|
|
748
761
|
|
|
749
762
|
mode.currentIndexChanged.connect(change_mode)
|
|
750
763
|
|
|
@@ -12,6 +12,7 @@ class ConfigDialog(QDialog):
|
|
|
12
12
|
self.form_layout = create_form_layout(self)
|
|
13
13
|
scroll_area = QScrollArea()
|
|
14
14
|
scroll_area.setWidgetResizable(True)
|
|
15
|
+
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
15
16
|
container_widget = QWidget()
|
|
16
17
|
self.container_layout = QFormLayout(container_widget)
|
|
17
18
|
self.container_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
@@ -19,19 +20,19 @@ class ConfigDialog(QDialog):
|
|
|
19
20
|
self.container_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
20
21
|
self.container_layout.setLabelAlignment(Qt.AlignLeft)
|
|
21
22
|
scroll_area.setWidget(container_widget)
|
|
22
|
-
button_box = QHBoxLayout()
|
|
23
|
+
self.button_box = QHBoxLayout()
|
|
23
24
|
self.ok_button = QPushButton("OK")
|
|
24
25
|
self.ok_button.setFocus()
|
|
25
26
|
self.cancel_button = QPushButton("Cancel")
|
|
26
27
|
self.reset_button = QPushButton("Reset")
|
|
27
|
-
button_box.addWidget(self.ok_button)
|
|
28
|
-
button_box.addWidget(self.cancel_button)
|
|
29
|
-
button_box.addWidget(self.reset_button)
|
|
28
|
+
self.button_box.addWidget(self.ok_button)
|
|
29
|
+
self.button_box.addWidget(self.cancel_button)
|
|
30
|
+
self.button_box.addWidget(self.reset_button)
|
|
30
31
|
self.reset_button.clicked.connect(self.reset_to_defaults)
|
|
31
32
|
self.ok_button.clicked.connect(self.accept)
|
|
32
33
|
self.cancel_button.clicked.connect(self.reject)
|
|
33
34
|
self.form_layout.addRow(scroll_area)
|
|
34
|
-
self.form_layout.addRow(button_box)
|
|
35
|
+
self.form_layout.addRow(self.button_box)
|
|
35
36
|
QTimer.singleShot(0, self.adjust_dialog_size)
|
|
36
37
|
self.create_form_content()
|
|
37
38
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
2
|
import os
|
|
3
|
+
from PySide6.QtCore import Qt
|
|
3
4
|
from PySide6.QtWidgets import (QWidget, QRadioButton, QButtonGroup, QLineEdit,
|
|
4
5
|
QPushButton, QHBoxLayout, QVBoxLayout, QFileDialog, QMessageBox)
|
|
5
|
-
from
|
|
6
|
+
from .. algorithms.utils import EXTENSIONS_GUI_STR
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class FolderFileSelectionWidget(QWidget):
|
|
@@ -73,7 +74,7 @@ class FolderFileSelectionWidget(QWidget):
|
|
|
73
74
|
def browse_files(self):
|
|
74
75
|
files, _ = QFileDialog.getOpenFileNames(
|
|
75
76
|
self, "Select Input Files", "",
|
|
76
|
-
"Image files (
|
|
77
|
+
f"Image files ({EXTENSIONS_GUI_STR})"
|
|
77
78
|
)
|
|
78
79
|
if files:
|
|
79
80
|
parent_dir = os.path.dirname(files[0])
|
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
|
@@ -9,7 +9,7 @@ from PySide6.QtCore import Signal, Slot
|
|
|
9
9
|
from .. config.constants import constants
|
|
10
10
|
from .. config.gui_constants import gui_constants
|
|
11
11
|
from .colors import RED_BUTTON_STYLE, BLUE_BUTTON_STYLE, BLUE_COMBO_STYLE
|
|
12
|
-
from .. algorithms.utils import
|
|
12
|
+
from .. algorithms.utils import extension_supported, extension_pdf
|
|
13
13
|
from .gui_logging import LogWorker, QTextEditLogger
|
|
14
14
|
from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
|
|
15
15
|
from .colors import (
|
|
@@ -209,7 +209,7 @@ class RunWindow(QTextEditLogger):
|
|
|
209
209
|
try:
|
|
210
210
|
if extension_pdf(path):
|
|
211
211
|
image_view = GuiPdfView(path, self)
|
|
212
|
-
elif
|
|
212
|
+
elif extension_supported(path):
|
|
213
213
|
image_view = GuiImageView(path, self)
|
|
214
214
|
else:
|
|
215
215
|
raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -318,6 +318,12 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
318
318
|
edit_config_action.triggered.connect(self.edit_current_action)
|
|
319
319
|
menu.addAction(edit_config_action)
|
|
320
320
|
menu.addSeparator()
|
|
321
|
+
menu.addAction(self.menu_manager.cut_action)
|
|
322
|
+
menu.addAction(self.menu_manager.copy_action)
|
|
323
|
+
menu.addAction(self.menu_manager.paste_action)
|
|
324
|
+
menu.addAction(self.menu_manager.duplicate_action)
|
|
325
|
+
menu.addAction(self.menu_manager.delete_element_action)
|
|
326
|
+
menu.addSeparator()
|
|
321
327
|
menu.addAction(self.menu_manager.run_job_action)
|
|
322
328
|
menu.addAction(self.menu_manager.run_all_jobs_action)
|
|
323
329
|
menu.addSeparator()
|
shinestacker/gui/menu_manager.py
CHANGED
|
@@ -123,8 +123,14 @@ class MenuManager(QObject):
|
|
|
123
123
|
self.undo_action = self.action("&Undo")
|
|
124
124
|
self.undo_action.setEnabled(False)
|
|
125
125
|
menu.addAction(self.undo_action)
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
self.cut_action = self.action("&Cut", requires_file=True)
|
|
127
|
+
menu.addAction(self.cut_action)
|
|
128
|
+
self.copy_action = self.action("Cop&y", requires_file=True)
|
|
129
|
+
menu.addAction(self.copy_action)
|
|
130
|
+
self.paste_action = self.action("&Paste", requires_file=True)
|
|
131
|
+
menu.addAction(self.paste_action)
|
|
132
|
+
self.duplicate_action = self.action("Duplicate", requires_file=True)
|
|
133
|
+
menu.addAction(self.duplicate_action)
|
|
128
134
|
self.delete_element_action = self.action("Delete", requires_file=True)
|
|
129
135
|
self.delete_element_action.setEnabled(False)
|
|
130
136
|
menu.addAction(self.delete_element_action)
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -8,7 +8,7 @@ from PySide6.QtCore import Qt
|
|
|
8
8
|
from .. config.gui_constants import gui_constants
|
|
9
9
|
from .. config.constants import constants
|
|
10
10
|
from .. config.app_config import AppConfig
|
|
11
|
-
from .. algorithms.utils import read_img,
|
|
11
|
+
from .. algorithms.utils import read_img, extension_supported
|
|
12
12
|
from .. algorithms.stack import get_bunches
|
|
13
13
|
from .folder_file_selection import FolderFileSelectionWidget
|
|
14
14
|
from .base_form_dialog import BaseFormDialog
|
|
@@ -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")
|
|
@@ -197,7 +208,7 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
197
208
|
return 0
|
|
198
209
|
count = 0
|
|
199
210
|
for filename in os.listdir(path):
|
|
200
|
-
if
|
|
211
|
+
if extension_supported(filename):
|
|
201
212
|
count += 1
|
|
202
213
|
return count
|
|
203
214
|
if self.input_widget.get_selection_mode() == 'files' and \
|
|
@@ -262,7 +273,7 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
262
273
|
file_path = None
|
|
263
274
|
for filename in files:
|
|
264
275
|
full_path = os.path.join(path, filename)
|
|
265
|
-
if
|
|
276
|
+
if extension_supported(full_path):
|
|
266
277
|
file_path = full_path
|
|
267
278
|
break
|
|
268
279
|
if file_path is None:
|
|
@@ -273,8 +284,8 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
273
284
|
height, width = img.shape[:2]
|
|
274
285
|
n_bytes = 1 if img.dtype == np.uint8 else 2
|
|
275
286
|
n_bits = 8 if img.dtype == np.uint8 else 16
|
|
276
|
-
n_gbytes =
|
|
277
|
-
if n_gbytes >
|
|
287
|
+
n_gbytes = 3.0 * n_bytes * height * width * self.n_image_files / constants.ONE_GIGA
|
|
288
|
+
if n_gbytes > 4 and not self.bunch_stack.isChecked():
|
|
278
289
|
msg = QMessageBox()
|
|
279
290
|
msg.setStyleSheet("""
|
|
280
291
|
QMessageBox {
|
|
@@ -14,6 +14,9 @@ from .project_model import Project
|
|
|
14
14
|
from .project_editor import ProjectEditor
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
CURRENT_PROJECT_FILE_VERSION = 1
|
|
18
|
+
|
|
19
|
+
|
|
17
20
|
class ProjectController(QObject):
|
|
18
21
|
update_title_requested = Signal()
|
|
19
22
|
refresh_ui_requested = Signal(int, int)
|
|
@@ -162,14 +165,15 @@ class ProjectController(QObject):
|
|
|
162
165
|
if dialog.get_noise_detection():
|
|
163
166
|
job_noise = ActionConfig(
|
|
164
167
|
constants.ACTION_JOB,
|
|
165
|
-
{'name': f'{input_path}-
|
|
168
|
+
{'name': f'{input_path}-noise-job', 'working_path': working_path,
|
|
166
169
|
'input_path': input_path})
|
|
170
|
+
noise_detection_name = f'{input_path}-detect-noise'
|
|
167
171
|
noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
|
|
168
|
-
{'name':
|
|
172
|
+
{'name': noise_detection_name})
|
|
169
173
|
job_noise.add_sub_action(noise_detection)
|
|
170
174
|
self.add_job_to_project(job_noise)
|
|
171
175
|
job_params = {
|
|
172
|
-
'name': f'{input_path}-
|
|
176
|
+
'name': f'{input_path}-stack-job',
|
|
173
177
|
'working_path': working_path,
|
|
174
178
|
'input_path': input_path
|
|
175
179
|
}
|
|
@@ -184,7 +188,10 @@ class ProjectController(QObject):
|
|
|
184
188
|
constants.ACTION_COMBO, {'name': preprocess_name})
|
|
185
189
|
if dialog.get_noise_detection():
|
|
186
190
|
mask_noise = ActionConfig(
|
|
187
|
-
constants.ACTION_MASKNOISE,
|
|
191
|
+
constants.ACTION_MASKNOISE,
|
|
192
|
+
{'name': 'mask-noise',
|
|
193
|
+
'noise_mask': os.path.join(noise_detection_name,
|
|
194
|
+
constants.DEFAULT_NOISE_MAP_FILENAME)})
|
|
188
195
|
combo_action.add_sub_action(mask_noise)
|
|
189
196
|
if dialog.get_vignetting_correction():
|
|
190
197
|
vignetting = ActionConfig(
|
|
@@ -238,6 +245,7 @@ class ProjectController(QObject):
|
|
|
238
245
|
self.add_job_to_project(job)
|
|
239
246
|
self.project_editor.set_modified(True)
|
|
240
247
|
self.refresh_ui(0, -1)
|
|
248
|
+
self.set_enabled_file_open_close_actions_requested.emit(True)
|
|
241
249
|
|
|
242
250
|
def open_project(self, file_path=False):
|
|
243
251
|
if not self.check_unsaved_changes():
|
|
@@ -251,7 +259,7 @@ class ProjectController(QObject):
|
|
|
251
259
|
self.set_current_file_path(file_path)
|
|
252
260
|
with open(self.current_file_path(), 'r', encoding="utf-8") as file:
|
|
253
261
|
json_obj = json.load(file)
|
|
254
|
-
project = Project.from_dict(json_obj['project'])
|
|
262
|
+
project = Project.from_dict(json_obj['project'], json_obj['version'])
|
|
255
263
|
if project is None:
|
|
256
264
|
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
257
265
|
self.set_enabled_file_open_close_actions_requested.emit(True)
|
|
@@ -313,7 +321,7 @@ class ProjectController(QObject):
|
|
|
313
321
|
def do_save(self, file_path):
|
|
314
322
|
try:
|
|
315
323
|
json_obj = jsonpickle.encode({
|
|
316
|
-
'project': self.project().to_dict(), 'version':
|
|
324
|
+
'project': self.project().to_dict(), 'version': CURRENT_PROJECT_FILE_VERSION
|
|
317
325
|
})
|
|
318
326
|
with open(file_path, 'w', encoding="utf-8") as f:
|
|
319
327
|
f.write(json_obj)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, R0903, R0904, R1702, R0917, R0913, R0902, E0611, E1131, E1121
|
|
2
2
|
import os
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from PySide6.QtWidgets import QListWidget, QMessageBox, QDialog, QListWidgetItem, QLabel
|
|
5
|
-
|
|
4
|
+
from PySide6.QtWidgets import (QListWidget, QMessageBox, QDialog, QListWidgetItem, QLabel,
|
|
5
|
+
QSizePolicy)
|
|
6
|
+
from PySide6.QtCore import Qt, QObject, Signal, QEvent, QSize
|
|
6
7
|
from .. config.constants import constants
|
|
7
8
|
from .colors import ColorPalette
|
|
8
9
|
from .action_config_dialog import ActionConfigDialog
|
|
@@ -105,6 +106,8 @@ class HandCursorListWidget(QListWidget):
|
|
|
105
106
|
super().__init__(parent)
|
|
106
107
|
self.setMouseTracking(True)
|
|
107
108
|
self.viewport().setMouseTracking(True)
|
|
109
|
+
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
110
|
+
self.setWordWrap(False)
|
|
108
111
|
|
|
109
112
|
def event(self, event):
|
|
110
113
|
if event.type() == QEvent.HoverMove:
|
|
@@ -503,6 +506,13 @@ class ProjectEditor(QObject):
|
|
|
503
506
|
if action.enabled() \
|
|
504
507
|
else f"🚫 <span style='color:#{ColorPalette.DARK_RED.hex()};'>{text}</span>"
|
|
505
508
|
label = QLabel(html_text)
|
|
509
|
+
label.setTextFormat(Qt.RichText)
|
|
510
|
+
label.setWordWrap(False)
|
|
511
|
+
label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
|
|
512
|
+
label.adjustSize()
|
|
513
|
+
ideal_width = label.sizeHint().width()
|
|
514
|
+
widget_list.setItemWidget(item, label)
|
|
515
|
+
item.setSizeHint(QSize(ideal_width, label.sizeHint().height()))
|
|
506
516
|
widget_list.setItemWidget(item, label)
|
|
507
517
|
|
|
508
518
|
def add_sub_action(self, type_name):
|
|
@@ -62,10 +62,10 @@ class ActionConfig:
|
|
|
62
62
|
return project_dict
|
|
63
63
|
|
|
64
64
|
@classmethod
|
|
65
|
-
def from_dict(cls, data):
|
|
65
|
+
def from_dict(cls, data, version):
|
|
66
66
|
a = ActionConfig(data['type_name'], data['params'])
|
|
67
67
|
if 'sub_actions' in data.keys():
|
|
68
|
-
a.sub_actions = [ActionConfig.from_dict(s) for s in data['sub_actions']]
|
|
68
|
+
a.sub_actions = [ActionConfig.from_dict(s, version) for s in data['sub_actions']]
|
|
69
69
|
for s in a.sub_actions:
|
|
70
70
|
s.parent = a
|
|
71
71
|
return a
|
|
@@ -84,9 +84,9 @@ class Project:
|
|
|
84
84
|
return [j.to_dict() for j in self.jobs]
|
|
85
85
|
|
|
86
86
|
@classmethod
|
|
87
|
-
def from_dict(cls, data):
|
|
87
|
+
def from_dict(cls, data, version):
|
|
88
88
|
p = Project()
|
|
89
|
-
p.jobs = [ActionConfig.from_dict(j) for j in data]
|
|
89
|
+
p.jobs = [ActionConfig.from_dict(j, version) for j in data]
|
|
90
90
|
for j in p.jobs:
|
|
91
91
|
for s in j.sub_actions:
|
|
92
92
|
s.parent = j
|
|
@@ -75,6 +75,26 @@ class BrushTool:
|
|
|
75
75
|
self.hardness_slider.setValue(val)
|
|
76
76
|
self.update_brush_hardness(val)
|
|
77
77
|
|
|
78
|
+
def increase_brush_opacity(self, amount=2):
|
|
79
|
+
val = min(self.opacity_slider.value() + amount, self.opacity_slider.maximum())
|
|
80
|
+
self.opacity_slider.setValue(val)
|
|
81
|
+
self.update_brush_opacity(val)
|
|
82
|
+
|
|
83
|
+
def decrease_brush_opacity(self, amount=2):
|
|
84
|
+
val = max(self.opacity_slider.value() - amount, self.opacity_slider.minimum())
|
|
85
|
+
self.opacity_slider.setValue(val)
|
|
86
|
+
self.update_brush_opacity(val)
|
|
87
|
+
|
|
88
|
+
def increase_brush_flow(self, amount=2):
|
|
89
|
+
val = min(self.flow_slider.value() + amount, self.flow_slider.maximum())
|
|
90
|
+
self.flow_slider.setValue(val)
|
|
91
|
+
self.update_brush_flow(val)
|
|
92
|
+
|
|
93
|
+
def decrease_brush_flow(self, amount=2):
|
|
94
|
+
val = max(self.flow_slider.value() - amount, self.flow_slider.minimum())
|
|
95
|
+
self.flow_slider.setValue(val)
|
|
96
|
+
self.update_brush_flow(val)
|
|
97
|
+
|
|
78
98
|
def update_brush_hardness(self, hardness):
|
|
79
99
|
self.brush.hardness = hardness
|
|
80
100
|
self.update_brush_thumb()
|