shinestacker 1.8.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_framework.py +14 -9
- shinestacker/algorithms/vignetting.py +16 -3
- shinestacker/app/settings_dialog.py +297 -173
- shinestacker/config/constants.py +10 -5
- 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 +36 -20
- shinestacker/gui/gui_images.py +27 -3
- shinestacker/gui/new_project.py +18 -7
- shinestacker/gui/project_controller.py +13 -6
- shinestacker/gui/project_editor.py +12 -2
- shinestacker/gui/project_model.py +4 -4
- {shinestacker-1.8.0.dist-info → shinestacker-1.8.1.dist-info}/METADATA +35 -39
- {shinestacker-1.8.0.dist-info → shinestacker-1.8.1.dist-info}/RECORD +27 -27
- {shinestacker-1.8.0.dist-info → shinestacker-1.8.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.8.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.8.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.8.1.dist-info}/top_level.txt +0 -0
shinestacker/config/settings.py
CHANGED
|
@@ -39,23 +39,26 @@ DEFAULT_SETTINGS = {
|
|
|
39
39
|
'cursor_update_time': gui_constants.DEFAULT_CURSOR_UPDATE_TIME,
|
|
40
40
|
'min_mouse_step_brush_fraction': gui_constants.DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION,
|
|
41
41
|
'combined_actions_params': {
|
|
42
|
-
'max_threads': constants.
|
|
42
|
+
'max_threads': constants.DEFAULT_FWK_MAX_THREADS
|
|
43
43
|
},
|
|
44
44
|
'align_frames_params': {
|
|
45
|
+
'memory_limit': constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
|
|
45
46
|
'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS,
|
|
46
47
|
'detector': constants.DEFAULT_DETECTOR,
|
|
47
48
|
'descriptor': constants.DEFAULT_DESCRIPTOR,
|
|
48
49
|
'match_method': constants.DEFAULT_MATCHING_METHOD
|
|
49
50
|
},
|
|
50
51
|
'focus_stack_params': {
|
|
52
|
+
'memory_limit': constants.DEFAULT_PY_MEMORY_LIMIT_GB,
|
|
51
53
|
'max_threads': constants.DEFAULT_PY_MAX_THREADS
|
|
52
54
|
},
|
|
53
55
|
'focus_stack_bunch_params': {
|
|
56
|
+
'memory_limit': constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
|
|
54
57
|
'max_threads': constants.DEFAULT_PY_MAX_THREADS
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
CURRENT_SETTINGS_FILE_VERSION = 1
|
|
59
62
|
|
|
60
63
|
|
|
61
64
|
class Settings(StdPathFile):
|
|
@@ -66,18 +69,30 @@ class Settings(StdPathFile):
|
|
|
66
69
|
if Settings._instance is not None:
|
|
67
70
|
raise RuntimeError("Settings is a singleton.")
|
|
68
71
|
super().__init__(filename)
|
|
69
|
-
self.settings =
|
|
72
|
+
self.settings = self._deep_copy_defaults()
|
|
70
73
|
file_path = self.get_file_path()
|
|
71
74
|
if os.path.isfile(file_path):
|
|
72
75
|
try:
|
|
73
76
|
with open(file_path, 'r', encoding="utf-8") as file:
|
|
74
77
|
json_data = json.load(file)
|
|
75
|
-
|
|
78
|
+
file_settings = json_data['settings']
|
|
79
|
+
self._deep_merge_settings(file_settings)
|
|
76
80
|
except Exception as e:
|
|
77
81
|
traceback.print_tb(e.__traceback__)
|
|
78
82
|
print(f"Can't read file from path {file_path}. Default settings ignored.")
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
|
|
84
|
+
def _deep_copy_defaults(self):
|
|
85
|
+
return json.loads(json.dumps(DEFAULT_SETTINGS))
|
|
86
|
+
|
|
87
|
+
def _deep_merge_settings(self, file_settings):
|
|
88
|
+
for key, value in file_settings.items():
|
|
89
|
+
if key in self.settings:
|
|
90
|
+
if isinstance(value, dict) and isinstance(self.settings[key], dict):
|
|
91
|
+
for sub_key, sub_value in value.items():
|
|
92
|
+
if sub_key in self.settings[key]:
|
|
93
|
+
self.settings[key][sub_key] = sub_value
|
|
94
|
+
else:
|
|
95
|
+
self.settings[key] = value
|
|
81
96
|
|
|
82
97
|
@classmethod
|
|
83
98
|
def instance(cls, filename="shinestacker-settings.txt"):
|
|
@@ -99,7 +114,10 @@ class Settings(StdPathFile):
|
|
|
99
114
|
try:
|
|
100
115
|
config_dir = self.get_config_dir()
|
|
101
116
|
os.makedirs(config_dir, exist_ok=True)
|
|
102
|
-
json_data = {
|
|
117
|
+
json_data = {
|
|
118
|
+
'version': CURRENT_SETTINGS_FILE_VERSION,
|
|
119
|
+
'settings': self.settings
|
|
120
|
+
}
|
|
103
121
|
json_obj = jsonpickle.encode(json_data)
|
|
104
122
|
with open(self.get_file_path(), 'w', encoding="utf-8") as f:
|
|
105
123
|
f.write(json_obj)
|
shinestacker/core/exceptions.py
CHANGED
|
@@ -30,7 +30,7 @@ class AlignmentError(FocusStackError):
|
|
|
30
30
|
def __init__(self, index, details):
|
|
31
31
|
self.index = index
|
|
32
32
|
self.details = details
|
|
33
|
-
super().__init__(f"Alignment failed for
|
|
33
|
+
super().__init__(f"Alignment failed for frame {index}: {details}")
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class BitDepthError(FocusStackError):
|
shinestacker/core/framework.py
CHANGED
|
@@ -197,8 +197,8 @@ class Job(TaskBase):
|
|
|
197
197
|
|
|
198
198
|
class SequentialTask(TaskBase):
|
|
199
199
|
def __init__(self, name, enabled=True, **kwargs):
|
|
200
|
-
self.max_threads = kwargs.pop('max_threads', constants.
|
|
201
|
-
self.chunk_submit = kwargs.pop('chunk_submit', constants.
|
|
200
|
+
self.max_threads = kwargs.pop('max_threads', constants.DEFAULT_FWK_MAX_THREADS)
|
|
201
|
+
self.chunk_submit = kwargs.pop('chunk_submit', constants.DEFAULT_FWK_CHUNK_SUBMIT)
|
|
202
202
|
TaskBase.__init__(self, name, enabled, **kwargs)
|
|
203
203
|
self.total_action_counts = None
|
|
204
204
|
self.current_action_count = None
|
|
@@ -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
|
|
|
@@ -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):
|
|
@@ -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',
|
|
@@ -440,8 +445,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
440
445
|
default=0)
|
|
441
446
|
self.add_field(
|
|
442
447
|
'step_process', FIELD_BOOL, 'Step process', required=False,
|
|
443
|
-
expert=True,
|
|
444
|
-
default=True)
|
|
448
|
+
expert=True, default=constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS)
|
|
445
449
|
self.add_field(
|
|
446
450
|
'max_threads', FIELD_INT, 'Max num. of cores',
|
|
447
451
|
required=False, default=AppConfig.get('combined_actions_params')['max_threads'],
|
|
@@ -450,7 +454,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
450
454
|
self.add_field(
|
|
451
455
|
'chunk_submit', FIELD_BOOL, 'Submit in chunks',
|
|
452
456
|
expert=True,
|
|
453
|
-
required=False, default=constants.
|
|
457
|
+
required=False, default=constants.DEFAULT_FWK_CHUNK_SUBMIT)
|
|
454
458
|
|
|
455
459
|
|
|
456
460
|
class MaskNoiseConfigurator(DefaultActionConfigurator):
|
|
@@ -575,14 +579,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
575
579
|
self.detector_field = None
|
|
576
580
|
self.descriptor_field = None
|
|
577
581
|
self.matching_method_field = None
|
|
578
|
-
self.tab_widget =
|
|
579
|
-
feature_layout =
|
|
582
|
+
self.tab_widget = create_tab_widget(layout)
|
|
583
|
+
feature_layout = add_tab(self.tab_widget, "Feature extraction")
|
|
580
584
|
self.create_feature_tab(feature_layout)
|
|
581
|
-
transform_layout =
|
|
585
|
+
transform_layout = add_tab(self.tab_widget, "Transform")
|
|
582
586
|
self.create_transform_tab(transform_layout)
|
|
583
|
-
border_layout =
|
|
587
|
+
border_layout = add_tab(self.tab_widget, "Border")
|
|
584
588
|
self.create_border_tab(border_layout)
|
|
585
|
-
misc_layout =
|
|
589
|
+
misc_layout = add_tab(self.tab_widget, "Miscellanea")
|
|
586
590
|
self.create_miscellanea_tab(misc_layout)
|
|
587
591
|
|
|
588
592
|
def create_feature_tab(self, layout):
|
|
@@ -591,7 +595,6 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
591
595
|
self.change_match_config(
|
|
592
596
|
self.detector_field, self.descriptor_field,
|
|
593
597
|
self. matching_method_field, self.show_info)
|
|
594
|
-
|
|
595
598
|
self.add_bold_label_to_layout(layout, "Feature identification:")
|
|
596
599
|
self.detector_field = self.add_field_to_layout(
|
|
597
600
|
layout, 'detector', FIELD_COMBO, 'Detector', required=False,
|
|
@@ -642,9 +645,9 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
642
645
|
options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
|
|
643
646
|
default=constants.DEFAULT_TRANSFORM)
|
|
644
647
|
method = self.add_field_to_layout(
|
|
645
|
-
layout, 'align_method', FIELD_COMBO, '
|
|
646
|
-
options=self.METHOD_OPTIONS, values=constants.
|
|
647
|
-
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)
|
|
648
651
|
rans_threshold = self.add_field_to_layout(
|
|
649
652
|
layout, 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
|
|
650
653
|
expert=True,
|
|
@@ -689,6 +692,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
689
692
|
|
|
690
693
|
transform.currentIndexChanged.connect(change_transform)
|
|
691
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.")
|
|
692
703
|
self.add_field_to_layout(
|
|
693
704
|
layout, 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
|
|
694
705
|
expert=True,
|
|
@@ -723,7 +734,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
723
734
|
self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
|
|
724
735
|
memory_limit = self.add_field_to_layout(
|
|
725
736
|
layout, 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
|
|
726
|
-
required=False, default=
|
|
737
|
+
required=False, default=AppConfig.get('align_frames_params')['memory_limit'],
|
|
727
738
|
min_val=1.0, max_val=64.0)
|
|
728
739
|
max_threads = self.add_field_to_layout(
|
|
729
740
|
layout, 'max_threads', FIELD_INT, 'Max num. of cores',
|
|
@@ -737,6 +748,10 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
737
748
|
layout, 'bw_matching', FIELD_BOOL, 'Match using black & white',
|
|
738
749
|
expert=True,
|
|
739
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)
|
|
740
755
|
|
|
741
756
|
def change_mode():
|
|
742
757
|
text = mode.currentText()
|
|
@@ -745,6 +760,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
745
760
|
max_threads.setEnabled(enabled)
|
|
746
761
|
chunk_submit.setEnabled(enabled)
|
|
747
762
|
bw_matching.setEnabled(enabled)
|
|
763
|
+
delta_max.setEnabled(enabled)
|
|
748
764
|
|
|
749
765
|
mode.currentIndexChanged.connect(change_mode)
|
|
750
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/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")
|
|
@@ -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(
|
|
@@ -251,7 +258,7 @@ class ProjectController(QObject):
|
|
|
251
258
|
self.set_current_file_path(file_path)
|
|
252
259
|
with open(self.current_file_path(), 'r', encoding="utf-8") as file:
|
|
253
260
|
json_obj = json.load(file)
|
|
254
|
-
project = Project.from_dict(json_obj['project'])
|
|
261
|
+
project = Project.from_dict(json_obj['project'], json_obj['version'])
|
|
255
262
|
if project is None:
|
|
256
263
|
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
257
264
|
self.set_enabled_file_open_close_actions_requested.emit(True)
|
|
@@ -313,7 +320,7 @@ class ProjectController(QObject):
|
|
|
313
320
|
def do_save(self, file_path):
|
|
314
321
|
try:
|
|
315
322
|
json_obj = jsonpickle.encode({
|
|
316
|
-
'project': self.project().to_dict(), 'version':
|
|
323
|
+
'project': self.project().to_dict(), 'version': CURRENT_PROJECT_FILE_VERSION
|
|
317
324
|
})
|
|
318
325
|
with open(file_path, 'w', encoding="utf-8") as f:
|
|
319
326
|
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
|