shinestacker 1.8.0__py3-none-any.whl → 1.9.0__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/exif.py +252 -6
- shinestacker/algorithms/multilayer.py +6 -4
- shinestacker/algorithms/noise_detection.py +10 -8
- shinestacker/algorithms/pyramid_tiles.py +1 -1
- shinestacker/algorithms/stack.py +25 -13
- shinestacker/algorithms/stack_framework.py +16 -11
- shinestacker/algorithms/utils.py +18 -2
- shinestacker/algorithms/vignetting.py +16 -3
- 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 -21
- shinestacker/gui/folder_file_selection.py +3 -2
- shinestacker/gui/gui_images.py +27 -3
- shinestacker/gui/gui_run.py +2 -2
- shinestacker/gui/new_project.py +23 -12
- shinestacker/gui/project_controller.py +13 -6
- shinestacker/gui/project_editor.py +12 -2
- shinestacker/gui/project_model.py +4 -4
- shinestacker/retouch/exif_data.py +3 -0
- shinestacker/retouch/file_loader.py +3 -3
- shinestacker/retouch/io_gui_handler.py +4 -4
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/METADATA +37 -39
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/RECORD +36 -36
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.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',
|
|
@@ -440,8 +446,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
440
446
|
default=0)
|
|
441
447
|
self.add_field(
|
|
442
448
|
'step_process', FIELD_BOOL, 'Step process', required=False,
|
|
443
|
-
expert=True,
|
|
444
|
-
default=True)
|
|
449
|
+
expert=True, default=constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS)
|
|
445
450
|
self.add_field(
|
|
446
451
|
'max_threads', FIELD_INT, 'Max num. of cores',
|
|
447
452
|
required=False, default=AppConfig.get('combined_actions_params')['max_threads'],
|
|
@@ -450,7 +455,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
450
455
|
self.add_field(
|
|
451
456
|
'chunk_submit', FIELD_BOOL, 'Submit in chunks',
|
|
452
457
|
expert=True,
|
|
453
|
-
required=False, default=constants.
|
|
458
|
+
required=False, default=constants.DEFAULT_FWK_CHUNK_SUBMIT)
|
|
454
459
|
|
|
455
460
|
|
|
456
461
|
class MaskNoiseConfigurator(DefaultActionConfigurator):
|
|
@@ -575,14 +580,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
575
580
|
self.detector_field = None
|
|
576
581
|
self.descriptor_field = None
|
|
577
582
|
self.matching_method_field = None
|
|
578
|
-
self.tab_widget =
|
|
579
|
-
feature_layout =
|
|
583
|
+
self.tab_widget = create_tab_widget(layout)
|
|
584
|
+
feature_layout = add_tab(self.tab_widget, "Feature extraction")
|
|
580
585
|
self.create_feature_tab(feature_layout)
|
|
581
|
-
transform_layout =
|
|
586
|
+
transform_layout = add_tab(self.tab_widget, "Transform")
|
|
582
587
|
self.create_transform_tab(transform_layout)
|
|
583
|
-
border_layout =
|
|
588
|
+
border_layout = add_tab(self.tab_widget, "Border")
|
|
584
589
|
self.create_border_tab(border_layout)
|
|
585
|
-
misc_layout =
|
|
590
|
+
misc_layout = add_tab(self.tab_widget, "Miscellanea")
|
|
586
591
|
self.create_miscellanea_tab(misc_layout)
|
|
587
592
|
|
|
588
593
|
def create_feature_tab(self, layout):
|
|
@@ -591,7 +596,6 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
591
596
|
self.change_match_config(
|
|
592
597
|
self.detector_field, self.descriptor_field,
|
|
593
598
|
self. matching_method_field, self.show_info)
|
|
594
|
-
|
|
595
599
|
self.add_bold_label_to_layout(layout, "Feature identification:")
|
|
596
600
|
self.detector_field = self.add_field_to_layout(
|
|
597
601
|
layout, 'detector', FIELD_COMBO, 'Detector', required=False,
|
|
@@ -642,9 +646,9 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
642
646
|
options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
|
|
643
647
|
default=constants.DEFAULT_TRANSFORM)
|
|
644
648
|
method = self.add_field_to_layout(
|
|
645
|
-
layout, 'align_method', FIELD_COMBO, '
|
|
646
|
-
options=self.METHOD_OPTIONS, values=constants.
|
|
647
|
-
default=constants.
|
|
649
|
+
layout, 'align_method', FIELD_COMBO, 'Estimation method', required=False,
|
|
650
|
+
options=self.METHOD_OPTIONS, values=constants.VALID_ESTIMATION_METHODS,
|
|
651
|
+
default=constants.DEFAULT_ESTIMATION_METHOD)
|
|
648
652
|
rans_threshold = self.add_field_to_layout(
|
|
649
653
|
layout, 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
|
|
650
654
|
expert=True,
|
|
@@ -689,6 +693,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
689
693
|
|
|
690
694
|
transform.currentIndexChanged.connect(change_transform)
|
|
691
695
|
change_transform()
|
|
696
|
+
phase_corr_fallback = self.add_field_to_layout(
|
|
697
|
+
layout, 'phase_corr_fallback', FIELD_BOOL, "Phase correlation as fallback",
|
|
698
|
+
required=False, expert=True, default=constants.DEFAULT_PHASE_CORR_FALLBACK)
|
|
699
|
+
phase_corr_fallback.setToolTip(
|
|
700
|
+
"Align using phase correlation algorithm if the number of matches\n"
|
|
701
|
+
"is too low to determine the transformation.\n"
|
|
702
|
+
"This algorithm is not very precise,\n"
|
|
703
|
+
"and may help only in case of blurred images.")
|
|
692
704
|
self.add_field_to_layout(
|
|
693
705
|
layout, 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
|
|
694
706
|
expert=True,
|
|
@@ -723,7 +735,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
723
735
|
self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
|
|
724
736
|
memory_limit = self.add_field_to_layout(
|
|
725
737
|
layout, 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
|
|
726
|
-
required=False, default=
|
|
738
|
+
required=False, default=AppConfig.get('align_frames_params')['memory_limit'],
|
|
727
739
|
min_val=1.0, max_val=64.0)
|
|
728
740
|
max_threads = self.add_field_to_layout(
|
|
729
741
|
layout, 'max_threads', FIELD_INT, 'Max num. of cores',
|
|
@@ -737,6 +749,10 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
737
749
|
layout, 'bw_matching', FIELD_BOOL, 'Match using black & white',
|
|
738
750
|
expert=True,
|
|
739
751
|
required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
|
|
752
|
+
delta_max = self.add_field_to_layout(
|
|
753
|
+
layout, 'delta_max', FIELD_INT, 'Max frames skip',
|
|
754
|
+
required=False, default=constants.DEFAULT_ALIGN_DELTA_MAX,
|
|
755
|
+
min_val=1, max_val=128)
|
|
740
756
|
|
|
741
757
|
def change_mode():
|
|
742
758
|
text = mode.currentText()
|
|
@@ -745,6 +761,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
|
|
|
745
761
|
max_threads.setEnabled(enabled)
|
|
746
762
|
chunk_submit.setEnabled(enabled)
|
|
747
763
|
bw_matching.setEnabled(enabled)
|
|
764
|
+
delta_max.setEnabled(enabled)
|
|
748
765
|
|
|
749
766
|
mode.currentIndexChanged.connect(change_mode)
|
|
750
767
|
|
|
@@ -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/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(
|
|
@@ -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
|
|
@@ -42,8 +42,11 @@ class ExifData(BaseFormDialog):
|
|
|
42
42
|
data = exif_dict(self.exif)
|
|
43
43
|
if len(data) > 0:
|
|
44
44
|
for k, (_, d) in data.items():
|
|
45
|
+
print(k, type(d))
|
|
45
46
|
if isinstance(d, IFDRational):
|
|
46
47
|
d = f"{d.numerator}/{d.denominator}"
|
|
48
|
+
elif len(str(d)) > 40:
|
|
49
|
+
d = f"{str(d):.40}..."
|
|
47
50
|
else:
|
|
48
51
|
d = f"{d}"
|
|
49
52
|
if "<<<" not in d and k != 'IPTCNAA':
|
|
@@ -5,7 +5,7 @@ import numpy as np
|
|
|
5
5
|
import cv2
|
|
6
6
|
from psdtags import PsdChannelId
|
|
7
7
|
from PySide6.QtCore import QThread, Signal
|
|
8
|
-
from .. algorithms.utils import read_img, extension_tif, extension_jpg
|
|
8
|
+
from .. algorithms.utils import read_img, extension_tif, extension_jpg, extension_png
|
|
9
9
|
from .. algorithms.multilayer import read_multilayer_tiff
|
|
10
10
|
|
|
11
11
|
|
|
@@ -50,10 +50,10 @@ class FileLoader(QThread):
|
|
|
50
50
|
raise RuntimeError(f"Path {path} does not exist.")
|
|
51
51
|
if not os.path.isfile(path):
|
|
52
52
|
raise RuntimeError(f"Path {path} is not a file.")
|
|
53
|
-
if extension_jpg(path):
|
|
53
|
+
if extension_jpg(path) or extension_png(path):
|
|
54
54
|
try:
|
|
55
55
|
stack = np.array([cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)])
|
|
56
|
-
return stack, [path.
|
|
56
|
+
return stack, [os.path.splitext(os.path.basename(path))[0]]
|
|
57
57
|
except Exception as e:
|
|
58
58
|
traceback.print_tb(e.__traceback__)
|
|
59
59
|
return None, None
|
|
@@ -7,6 +7,7 @@ from PySide6.QtWidgets import (QFileDialog, QMessageBox, QVBoxLayout, QLabel, QD
|
|
|
7
7
|
QApplication, QProgressBar)
|
|
8
8
|
from PySide6.QtGui import QGuiApplication, QCursor
|
|
9
9
|
from PySide6.QtCore import Qt, QObject, QTimer, Signal
|
|
10
|
+
from .. algorithms.utils import EXTENSIONS_GUI_STR, EXTENSIONS_GUI_SAVE_STR
|
|
10
11
|
from .. algorithms.exif import get_exif, write_image_with_exif_data
|
|
11
12
|
from .file_loader import FileLoader
|
|
12
13
|
from .exif_data import ExifData
|
|
@@ -135,7 +136,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
135
136
|
if file_paths is None:
|
|
136
137
|
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
137
138
|
self.parent(), "Open Image", "",
|
|
138
|
-
"Images (
|
|
139
|
+
F"Images ({EXTENSIONS_GUI_STR});;All Files (*)")
|
|
139
140
|
if not file_paths:
|
|
140
141
|
return
|
|
141
142
|
if self.loader_thread and self.loader_thread.isRunning():
|
|
@@ -167,7 +168,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
167
168
|
def import_frames(self):
|
|
168
169
|
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
169
170
|
self.parent(), "Select frames", "",
|
|
170
|
-
"Images Images (
|
|
171
|
+
f"Images Images ({EXTENSIONS_GUI_STR});;All Files (*)")
|
|
171
172
|
if file_paths:
|
|
172
173
|
self.import_frames_from_files(file_paths)
|
|
173
174
|
|
|
@@ -286,8 +287,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
286
287
|
if self.layer_stack() is None:
|
|
287
288
|
return
|
|
288
289
|
path, _ = QFileDialog.getSaveFileName(
|
|
289
|
-
self.parent(), "Save Image", "",
|
|
290
|
-
"TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
|
|
290
|
+
self.parent(), "Save Image", "", EXTENSIONS_GUI_SAVE_STR)
|
|
291
291
|
if path:
|
|
292
292
|
self.save_master_to_path(path)
|
|
293
293
|
|