shinestacker 1.2.1__py3-none-any.whl → 1.3.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 +126 -94
- shinestacker/algorithms/align_auto.py +64 -0
- shinestacker/algorithms/align_parallel.py +296 -0
- shinestacker/algorithms/balance.py +3 -1
- shinestacker/algorithms/base_stack_algo.py +11 -2
- shinestacker/algorithms/multilayer.py +8 -8
- shinestacker/algorithms/noise_detection.py +10 -10
- shinestacker/algorithms/pyramid.py +4 -4
- shinestacker/algorithms/pyramid_auto.py +16 -10
- shinestacker/algorithms/pyramid_tiles.py +19 -11
- shinestacker/algorithms/stack.py +21 -17
- shinestacker/algorithms/stack_framework.py +97 -46
- shinestacker/algorithms/vignetting.py +13 -10
- shinestacker/app/main.py +7 -3
- shinestacker/config/constants.py +60 -25
- shinestacker/config/gui_constants.py +1 -1
- shinestacker/core/core_utils.py +4 -0
- shinestacker/core/framework.py +104 -23
- shinestacker/gui/action_config.py +4 -5
- shinestacker/gui/action_config_dialog.py +152 -12
- shinestacker/gui/base_form_dialog.py +2 -2
- shinestacker/gui/folder_file_selection.py +101 -0
- shinestacker/gui/gui_run.py +12 -10
- shinestacker/gui/main_window.py +6 -1
- shinestacker/gui/new_project.py +171 -73
- shinestacker/gui/project_controller.py +10 -6
- shinestacker/gui/project_converter.py +4 -2
- shinestacker/gui/project_editor.py +37 -27
- shinestacker/gui/select_path_widget.py +1 -1
- shinestacker/gui/sys_mon.py +96 -0
- shinestacker/gui/time_progress_bar.py +4 -3
- shinestacker/retouch/exif_data.py +1 -1
- shinestacker/retouch/image_editor_ui.py +2 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/METADATA +6 -6
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/RECORD +40 -36
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/top_level.txt +0 -0
shinestacker/gui/new_project.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902, R0914, R0911, R0912, R0904
|
|
2
2
|
import os
|
|
3
3
|
import numpy as np
|
|
4
4
|
from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QLabel, QCheckBox, QSpinBox,
|
|
5
|
-
QMessageBox)
|
|
5
|
+
QMessageBox, QGroupBox, QVBoxLayout, QFormLayout, QSizePolicy)
|
|
6
6
|
from PySide6.QtGui import QIcon
|
|
7
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 .. algorithms.utils import read_img, extension_tif_jpg
|
|
11
11
|
from .. algorithms.stack import get_bunches
|
|
12
|
-
from .
|
|
12
|
+
from .folder_file_selection import FolderFileSelectionWidget
|
|
13
13
|
from .base_form_dialog import BaseFormDialog
|
|
14
14
|
|
|
15
15
|
DEFAULT_NO_COUNT_LABEL = " - "
|
|
@@ -17,18 +17,18 @@ DEFAULT_NO_COUNT_LABEL = " - "
|
|
|
17
17
|
|
|
18
18
|
class NewProjectDialog(BaseFormDialog):
|
|
19
19
|
def __init__(self, parent=None):
|
|
20
|
-
super().__init__("New Project", parent)
|
|
20
|
+
super().__init__("New Project", 600, parent)
|
|
21
21
|
self.create_form()
|
|
22
22
|
button_box = QHBoxLayout()
|
|
23
|
-
ok_button = QPushButton("OK")
|
|
24
|
-
ok_button.setFocus()
|
|
23
|
+
self.ok_button = QPushButton("OK")
|
|
25
24
|
cancel_button = QPushButton("Cancel")
|
|
26
|
-
button_box.addWidget(ok_button)
|
|
25
|
+
button_box.addWidget(self.ok_button)
|
|
27
26
|
button_box.addWidget(cancel_button)
|
|
28
27
|
self.add_row_to_layout(button_box)
|
|
29
|
-
ok_button.clicked.connect(self.accept)
|
|
28
|
+
self.ok_button.clicked.connect(self.accept)
|
|
30
29
|
cancel_button.clicked.connect(self.reject)
|
|
31
30
|
self.n_image_files = 0
|
|
31
|
+
self.selected_filenames = []
|
|
32
32
|
|
|
33
33
|
def expert(self):
|
|
34
34
|
return self.parent().expert_options
|
|
@@ -43,20 +43,8 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
43
43
|
self.form_layout.addRow(label)
|
|
44
44
|
|
|
45
45
|
def create_form(self):
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
icon_pixmap = app_icon.pixmap(128, 128)
|
|
49
|
-
icon_label = QLabel()
|
|
50
|
-
icon_label.setPixmap(icon_pixmap)
|
|
51
|
-
icon_label.setAlignment(Qt.AlignCenter)
|
|
52
|
-
self.form_layout.addRow(icon_label)
|
|
53
|
-
spacer = QLabel("")
|
|
54
|
-
spacer.setFixedHeight(10)
|
|
55
|
-
self.form_layout.addRow(spacer)
|
|
56
|
-
|
|
57
|
-
self.input_folder, container = create_select_file_paths_widget(
|
|
58
|
-
'', 'input files folder', 'input files folder')
|
|
59
|
-
self.input_folder.textChanged.connect(self.update_bunches_label)
|
|
46
|
+
self.input_widget = FolderFileSelectionWidget()
|
|
47
|
+
self.input_widget.text_changed_connect(self.input_submitted)
|
|
60
48
|
self.noise_detection = QCheckBox()
|
|
61
49
|
self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
|
|
62
50
|
self.vignetting_correction = QCheckBox()
|
|
@@ -65,7 +53,6 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
65
53
|
self.align_frames.setChecked(gui_constants.NEW_PROJECT_ALIGN_FRAMES)
|
|
66
54
|
self.balance_frames = QCheckBox()
|
|
67
55
|
self.balance_frames.setChecked(gui_constants.NEW_PROJECT_BALANCE_FRAMES)
|
|
68
|
-
|
|
69
56
|
self.bunch_stack = QCheckBox()
|
|
70
57
|
self.bunch_stack.setChecked(gui_constants.NEW_PROJECT_BUNCH_STACK)
|
|
71
58
|
self.bunch_frames = QSpinBox()
|
|
@@ -78,46 +65,121 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
78
65
|
self.bunch_overlap.setValue(constants.DEFAULT_OVERLAP)
|
|
79
66
|
self.bunches_label = QLabel(DEFAULT_NO_COUNT_LABEL)
|
|
80
67
|
self.frames_label = QLabel(DEFAULT_NO_COUNT_LABEL)
|
|
81
|
-
|
|
82
68
|
self.update_bunch_options(gui_constants.NEW_PROJECT_BUNCH_STACK)
|
|
83
69
|
self.bunch_stack.toggled.connect(self.update_bunch_options)
|
|
84
70
|
self.bunch_frames.valueChanged.connect(self.update_bunches_label)
|
|
85
71
|
self.bunch_overlap.valueChanged.connect(self.update_bunches_label)
|
|
86
|
-
|
|
87
72
|
self.focus_stack_pyramid = QCheckBox()
|
|
88
73
|
self.focus_stack_pyramid.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_PYRAMID)
|
|
89
74
|
self.focus_stack_depth_map = QCheckBox()
|
|
90
75
|
self.focus_stack_depth_map.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_DEPTH_MAP)
|
|
91
76
|
self.multi_layer = QCheckBox()
|
|
92
77
|
self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
78
|
+
step1_group = QGroupBox("1️⃣ Select Input")
|
|
79
|
+
step1_layout = QVBoxLayout()
|
|
80
|
+
step1_layout.setContentsMargins(15, 0, 15, 15)
|
|
81
|
+
step1_layout.addWidget(
|
|
82
|
+
QLabel("Select a folder containing "
|
|
83
|
+
"all your images, or specific image files."))
|
|
84
|
+
input_form = QFormLayout()
|
|
85
|
+
input_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
86
|
+
input_form.setFormAlignment(Qt.AlignLeft)
|
|
87
|
+
input_form.setLabelAlignment(Qt.AlignLeft)
|
|
88
|
+
self.input_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
89
|
+
self.frames_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
90
|
+
input_form.addRow("Input:", self.input_widget)
|
|
91
|
+
input_form.addRow("Number of frames: ", self.frames_label)
|
|
92
|
+
step1_layout.addLayout(input_form)
|
|
93
|
+
step1_group.setLayout(step1_layout)
|
|
94
|
+
self.form_layout.addRow(step1_group)
|
|
95
|
+
step2_group = QGroupBox("2️⃣ Basic Options")
|
|
96
|
+
step2_layout = QFormLayout()
|
|
97
|
+
step2_layout.setContentsMargins(15, 0, 15, 15)
|
|
98
|
+
step2_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
99
|
+
step2_layout.setFormAlignment(Qt.AlignLeft)
|
|
100
|
+
step2_layout.setLabelAlignment(Qt.AlignLeft)
|
|
101
|
+
for widget in [self.noise_detection, self.vignetting_correction, self.align_frames,
|
|
102
|
+
self.balance_frames, self.bunch_stack, self.bunch_frames,
|
|
103
|
+
self.bunch_overlap, self.focus_stack_pyramid,
|
|
104
|
+
self.focus_stack_depth_map, self.multi_layer]:
|
|
105
|
+
widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
99
106
|
if self.expert():
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
step2_layout.addRow("Automatic noise detection:", self.noise_detection)
|
|
108
|
+
step2_layout.addRow("Vignetting correction:", self.vignetting_correction)
|
|
109
|
+
step2_layout.addRow(
|
|
110
|
+
# f" {constants.ACTION_ICONS[constants.ACTION_ALIGNFRAMES]} "
|
|
111
|
+
"Align layers:", self.align_frames)
|
|
112
|
+
step2_layout.addRow(
|
|
113
|
+
# f" {constants.ACTION_ICONS[constants.ACTION_BALANCEFRAMES]} "
|
|
114
|
+
"Balance layers:", self.balance_frames)
|
|
115
|
+
step2_layout.addRow(
|
|
116
|
+
# f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACKBUNCH]} "
|
|
117
|
+
"Bunch stack:", self.bunch_stack)
|
|
118
|
+
step2_layout.addRow("Bunch frames:", self.bunch_frames)
|
|
119
|
+
step2_layout.addRow("Bunch overlap:", self.bunch_overlap)
|
|
120
|
+
self.bunches_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
121
|
+
step2_layout.addRow("Number of bunches: ", self.bunches_label)
|
|
108
122
|
if self.expert():
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
step2_layout.addRow(
|
|
124
|
+
f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACK]} "
|
|
125
|
+
"Focus stack (pyramid):", self.focus_stack_pyramid)
|
|
126
|
+
step2_layout.addRow(
|
|
127
|
+
f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACK]} "
|
|
128
|
+
"Focus stack (depth map):", self.focus_stack_depth_map)
|
|
111
129
|
else:
|
|
112
|
-
|
|
130
|
+
step2_layout.addRow(
|
|
131
|
+
f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACK]} "
|
|
132
|
+
"Focus stack:", self.focus_stack_pyramid)
|
|
113
133
|
if self.expert():
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
self.
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
step2_layout.addRow(
|
|
135
|
+
f" {constants.ACTION_ICONS[constants.ACTION_MULTILAYER]} "
|
|
136
|
+
"Save multi layer TIFF:", self.multi_layer)
|
|
137
|
+
step2_group.setLayout(step2_layout)
|
|
138
|
+
self.form_layout.addRow(step2_group)
|
|
139
|
+
step3_group = QGroupBox("3️⃣ Confirmation")
|
|
140
|
+
step3_layout = QVBoxLayout()
|
|
141
|
+
step3_layout.setContentsMargins(15, 0, 15, 15)
|
|
142
|
+
step3_layout.addWidget(
|
|
143
|
+
QLabel("Click 🆗 to confirm and prepare the job."))
|
|
144
|
+
step3_layout.addWidget(
|
|
145
|
+
QLabel("Select: <b>View</b> > <b>Expert options</b> for advanced configuration."))
|
|
146
|
+
step3_group.setLayout(step3_layout)
|
|
147
|
+
self.form_layout.addRow(step3_group)
|
|
148
|
+
step4_group = QGroupBox("4️⃣ Execution")
|
|
149
|
+
step4_layout = QHBoxLayout()
|
|
150
|
+
step4_layout.setContentsMargins(15, 0, 15, 15)
|
|
151
|
+
step4_layout.addWidget(QLabel("Press ▶️ to run your job."))
|
|
152
|
+
icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
|
|
153
|
+
app_icon = QIcon(icon_path)
|
|
154
|
+
icon_pixmap = app_icon.pixmap(80, 80)
|
|
155
|
+
icon_label = QLabel()
|
|
156
|
+
icon_label.setPixmap(icon_pixmap)
|
|
157
|
+
icon_label.setAlignment(Qt.AlignRight)
|
|
158
|
+
step4_layout.addWidget(icon_label)
|
|
159
|
+
step4_group.setLayout(step4_layout)
|
|
160
|
+
self.form_layout.addRow(step4_group)
|
|
161
|
+
group_style = """
|
|
162
|
+
QGroupBox {
|
|
163
|
+
font-weight: bold;
|
|
164
|
+
border: 2px solid #cccccc;
|
|
165
|
+
border-radius: 5px;
|
|
166
|
+
margin-top: 10px;
|
|
167
|
+
padding-top: 15px;
|
|
168
|
+
background-color: #f8f8f8;
|
|
169
|
+
}
|
|
170
|
+
QGroupBox::title {
|
|
171
|
+
subcontrol-origin: margin;
|
|
172
|
+
left: 10px;
|
|
173
|
+
padding: 0 5px 0 5px;
|
|
174
|
+
background-color: #f8f8f8;
|
|
175
|
+
}
|
|
176
|
+
"""
|
|
177
|
+
for group in [step1_group, step2_group, step3_group, step4_group]:
|
|
178
|
+
group.setStyleSheet(group_style)
|
|
179
|
+
group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
180
|
+
self.form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
181
|
+
self.form_layout.setFormAlignment(Qt.AlignLeft)
|
|
182
|
+
self.form_layout.setLabelAlignment(Qt.AlignLeft)
|
|
121
183
|
|
|
122
184
|
def update_bunch_options(self, checked):
|
|
123
185
|
self.bunch_frames.setEnabled(checked)
|
|
@@ -125,7 +187,7 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
125
187
|
self.update_bunches_label()
|
|
126
188
|
|
|
127
189
|
def update_bunches_label(self):
|
|
128
|
-
if not self.
|
|
190
|
+
if not self.input_widget.get_path():
|
|
129
191
|
return
|
|
130
192
|
|
|
131
193
|
def count_image_files(path):
|
|
@@ -136,8 +198,13 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
136
198
|
if extension_tif_jpg(filename):
|
|
137
199
|
count += 1
|
|
138
200
|
return count
|
|
139
|
-
|
|
140
|
-
|
|
201
|
+
if self.input_widget.get_selection_mode() == 'files' and \
|
|
202
|
+
self.input_widget.get_selected_files():
|
|
203
|
+
self.n_image_files = len(self.input_widget.get_selected_files())
|
|
204
|
+
self.selected_filenames = self.input_widget.get_selected_filenames()
|
|
205
|
+
else:
|
|
206
|
+
self.n_image_files = count_image_files(self.input_widget.get_path())
|
|
207
|
+
self.selected_filenames = []
|
|
141
208
|
if self.n_image_files == 0:
|
|
142
209
|
self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
|
|
143
210
|
self.frames_label.setText(DEFAULT_NO_COUNT_LABEL)
|
|
@@ -147,36 +214,58 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
147
214
|
bunches = get_bunches(list(range(self.n_image_files)),
|
|
148
215
|
self.bunch_frames.value(),
|
|
149
216
|
self.bunch_overlap.value())
|
|
150
|
-
self.bunches_label.setText(f"{len(bunches)}")
|
|
217
|
+
self.bunches_label.setText(f"{max(1, len(bunches))}")
|
|
151
218
|
else:
|
|
152
219
|
self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
|
|
153
220
|
|
|
221
|
+
def input_submitted(self):
|
|
222
|
+
self.update_bunches_label()
|
|
223
|
+
self.ok_button.setFocus()
|
|
224
|
+
|
|
154
225
|
def accept(self):
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
QMessageBox.warning(self, "Invalid Path", "The specified folder does not exist")
|
|
226
|
+
input_path = self.input_widget.get_path()
|
|
227
|
+
selection_mode = self.input_widget.get_selection_mode()
|
|
228
|
+
selected_files = self.input_widget.get_selected_files()
|
|
229
|
+
if not input_path:
|
|
230
|
+
QMessageBox.warning(self, "Input Required", "Please select an input folder or files")
|
|
161
231
|
return
|
|
162
|
-
if
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
232
|
+
if selection_mode == 'files':
|
|
233
|
+
if not selected_files:
|
|
234
|
+
QMessageBox.warning(self, "Invalid Selection", "No files selected")
|
|
235
|
+
return
|
|
236
|
+
for file_path in selected_files:
|
|
237
|
+
if not os.path.exists(file_path):
|
|
238
|
+
QMessageBox.warning(self, "Invalid Path",
|
|
239
|
+
f"The file {file_path} does not exist")
|
|
240
|
+
return
|
|
241
|
+
else:
|
|
242
|
+
if not os.path.exists(input_path):
|
|
243
|
+
QMessageBox.warning(self, "Invalid Path", "The specified folder does not exist")
|
|
244
|
+
return
|
|
245
|
+
if not os.path.isdir(input_path):
|
|
246
|
+
QMessageBox.warning(self, "Invalid Path", "The specified path is not a folder")
|
|
247
|
+
return
|
|
248
|
+
parent_dir = os.path.dirname(input_path)
|
|
249
|
+
if not parent_dir:
|
|
250
|
+
parent_dir = input_path
|
|
251
|
+
if len(parent_dir.split('/')) < 2:
|
|
166
252
|
QMessageBox.warning(self, "Invalid Path", "The path must have a parent folder")
|
|
167
253
|
return
|
|
168
254
|
if self.n_image_files > 0 and not self.bunch_stack.isChecked():
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
255
|
+
if selection_mode == 'files' and selected_files:
|
|
256
|
+
file_path = selected_files[0]
|
|
257
|
+
else:
|
|
258
|
+
path = self.input_widget.get_path()
|
|
259
|
+
files = os.listdir(path)
|
|
260
|
+
file_path = None
|
|
261
|
+
for filename in files:
|
|
262
|
+
full_path = os.path.join(path, filename)
|
|
263
|
+
if extension_tif_jpg(full_path):
|
|
264
|
+
file_path = full_path
|
|
265
|
+
break
|
|
177
266
|
if file_path is None:
|
|
178
267
|
QMessageBox.warning(
|
|
179
|
-
self, "Invalid input", "Could not find images
|
|
268
|
+
self, "Invalid input", "Could not find images in the selected path")
|
|
180
269
|
return
|
|
181
270
|
img = read_img(file_path)
|
|
182
271
|
height, width = img.shape[:2]
|
|
@@ -217,7 +306,16 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
217
306
|
super().accept()
|
|
218
307
|
|
|
219
308
|
def get_input_folder(self):
|
|
220
|
-
return self.
|
|
309
|
+
return self.input_widget.get_path()
|
|
310
|
+
|
|
311
|
+
def get_selected_files(self):
|
|
312
|
+
return self.input_widget.get_selected_files()
|
|
313
|
+
|
|
314
|
+
def get_selected_filenames(self):
|
|
315
|
+
return self.input_widget.get_selected_filenames()
|
|
316
|
+
|
|
317
|
+
def get_selection_mode(self):
|
|
318
|
+
return self.input_widget.get_selection_mode()
|
|
221
319
|
|
|
222
320
|
def get_noise_detection(self):
|
|
223
321
|
return self.noise_detection.isChecked()
|
|
@@ -143,7 +143,6 @@ class ProjectController(QObject):
|
|
|
143
143
|
return
|
|
144
144
|
os.chdir(get_app_base_path())
|
|
145
145
|
self.set_current_file_path('')
|
|
146
|
-
self.mark_as_modified(False)
|
|
147
146
|
self.update_title()
|
|
148
147
|
self.clear_job_list()
|
|
149
148
|
self.clear_action_list()
|
|
@@ -156,6 +155,7 @@ class ProjectController(QObject):
|
|
|
156
155
|
input_folder = dialog.get_input_folder().split('/')
|
|
157
156
|
working_path = '/'.join(input_folder[:-1])
|
|
158
157
|
input_path = input_folder[-1]
|
|
158
|
+
selected_filenames = dialog.get_selected_filenames()
|
|
159
159
|
if dialog.get_noise_detection():
|
|
160
160
|
job_noise = ActionConfig(
|
|
161
161
|
constants.ACTION_JOB,
|
|
@@ -165,10 +165,14 @@ class ProjectController(QObject):
|
|
|
165
165
|
{'name': f'{input_path}-detect-noise'})
|
|
166
166
|
job_noise.add_sub_action(noise_detection)
|
|
167
167
|
self.add_job_to_project(job_noise)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
job_params = {
|
|
169
|
+
'name': f'{input_path}-focus-stack',
|
|
170
|
+
'working_path': working_path,
|
|
171
|
+
'input_path': input_path
|
|
172
|
+
}
|
|
173
|
+
if len(selected_filenames) > 0:
|
|
174
|
+
job_params['input_filepaths'] = selected_filenames
|
|
175
|
+
job = ActionConfig(constants.ACTION_JOB, job_params)
|
|
172
176
|
preprocess_name = ''
|
|
173
177
|
if dialog.get_noise_detection() or dialog.get_vignetting_correction() or \
|
|
174
178
|
dialog.get_align_frames() or dialog.get_balance_frames():
|
|
@@ -227,7 +231,7 @@ class ProjectController(QObject):
|
|
|
227
231
|
'input_path': constants.PATH_SEPARATOR.join(multi_input_path)})
|
|
228
232
|
job.add_sub_action(multi_layer)
|
|
229
233
|
self.add_job_to_project(job)
|
|
230
|
-
self.
|
|
234
|
+
self.project_editor.set_modified(True)
|
|
231
235
|
self.refresh_ui(0, -1)
|
|
232
236
|
|
|
233
237
|
def open_project(self, file_path=False):
|
|
@@ -6,7 +6,7 @@ from .. core.exceptions import InvalidOptionError, RunStopException
|
|
|
6
6
|
from .. algorithms.stack_framework import StackJob, CombinedActions
|
|
7
7
|
from .. algorithms.noise_detection import NoiseDetection, MaskNoise
|
|
8
8
|
from .. algorithms.vignetting import Vignetting
|
|
9
|
-
from .. algorithms.
|
|
9
|
+
from .. algorithms.align_auto import AlignFramesAuto
|
|
10
10
|
from .. algorithms.balance import BalanceFrames
|
|
11
11
|
from .. algorithms.stack import FocusStack, FocusStackBunch
|
|
12
12
|
from .. algorithms.pyramid_auto import PyramidAutoStack
|
|
@@ -93,7 +93,7 @@ class ProjectConverter:
|
|
|
93
93
|
return Vignetting(**params)
|
|
94
94
|
if action_config.type_name == constants.ACTION_ALIGNFRAMES:
|
|
95
95
|
params = {k: v for k, v in action_config.params.items() if k != 'name'}
|
|
96
|
-
return
|
|
96
|
+
return AlignFramesAuto(**params)
|
|
97
97
|
if action_config.type_name == constants.ACTION_BALANCEFRAMES:
|
|
98
98
|
params = {k: v for k, v in action_config.params.items() if k != 'name'}
|
|
99
99
|
if 'intensity_interval' in params.keys():
|
|
@@ -133,7 +133,9 @@ class ProjectConverter:
|
|
|
133
133
|
enabled = action_config.params.get('enabled', True)
|
|
134
134
|
working_path = action_config.params.get('working_path', '')
|
|
135
135
|
input_path = action_config.params.get('input_path', '')
|
|
136
|
+
input_filepaths = action_config.params.get('input_filepaths', [])
|
|
136
137
|
stack_job = StackJob(name, working_path, enabled=enabled, input_path=input_path,
|
|
138
|
+
input_filepaths=input_filepaths,
|
|
137
139
|
logger_name=logger_name, callbacks=callbacks)
|
|
138
140
|
for sub in action_config.sub_actions:
|
|
139
141
|
action = self.action(sub)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, R0904, R1702, R0917, R0913, R0902, E0611, E1131, E1121
|
|
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
|
|
5
|
-
|
|
6
|
-
from PySide6.QtCore import Qt, QObject, Signal
|
|
4
|
+
from PySide6.QtWidgets import QListWidget, QMessageBox, QDialog, QListWidgetItem, QLabel
|
|
5
|
+
from PySide6.QtCore import Qt, QObject, Signal, QEvent
|
|
7
6
|
from .. config.constants import constants
|
|
8
7
|
from .colors import ColorPalette
|
|
9
8
|
from .action_config_dialog import ActionConfigDialog
|
|
@@ -101,6 +100,25 @@ class ProjectUndoManager(QObject):
|
|
|
101
100
|
self.set_enabled_undo_action_requested.emit(False, '')
|
|
102
101
|
|
|
103
102
|
|
|
103
|
+
class HandCursorListWidget(QListWidget):
|
|
104
|
+
def __init__(self, parent=None):
|
|
105
|
+
super().__init__(parent)
|
|
106
|
+
self.setMouseTracking(True)
|
|
107
|
+
self.viewport().setMouseTracking(True)
|
|
108
|
+
|
|
109
|
+
def event(self, event):
|
|
110
|
+
if event.type() == QEvent.HoverMove:
|
|
111
|
+
pos = event.position().toPoint()
|
|
112
|
+
item = self.itemAt(pos)
|
|
113
|
+
if item:
|
|
114
|
+
self.viewport().setCursor(Qt.PointingHandCursor)
|
|
115
|
+
else:
|
|
116
|
+
self.viewport().setCursor(Qt.ArrowCursor)
|
|
117
|
+
elif event.type() == QEvent.Leave:
|
|
118
|
+
self.viewport().setCursor(Qt.ArrowCursor)
|
|
119
|
+
return super().event(event)
|
|
120
|
+
|
|
121
|
+
|
|
104
122
|
class ProjectEditor(QObject):
|
|
105
123
|
INDENT_SPACE = " ↪ "
|
|
106
124
|
CLONE_POSTFIX = " (clone)"
|
|
@@ -117,8 +135,8 @@ class ProjectEditor(QObject):
|
|
|
117
135
|
self._project = None
|
|
118
136
|
self._copy_buffer = None
|
|
119
137
|
self._current_file_path = ''
|
|
120
|
-
self._job_list =
|
|
121
|
-
self._action_list =
|
|
138
|
+
self._job_list = HandCursorListWidget()
|
|
139
|
+
self._action_list = HandCursorListWidget()
|
|
122
140
|
self.dialog = None
|
|
123
141
|
|
|
124
142
|
def reset_undo(self):
|
|
@@ -246,37 +264,29 @@ class ProjectEditor(QObject):
|
|
|
246
264
|
txt = f"{job.params.get('name', '(job)')}"
|
|
247
265
|
if html:
|
|
248
266
|
txt = f"<b>{txt}</b>"
|
|
249
|
-
in_path = get_action_input_path(job)
|
|
250
|
-
|
|
267
|
+
in_path = get_action_input_path(job)[0]
|
|
268
|
+
if os.path.isabs(in_path):
|
|
269
|
+
in_path = ".../" + os.path.basename(in_path)
|
|
270
|
+
ico = constants.ACTION_ICONS[constants.ACTION_JOB]
|
|
271
|
+
return txt + (f" [{ico}Job: 📁 {in_path} → 📂 ...]" if long_name else "")
|
|
251
272
|
|
|
252
273
|
def action_text(self, action, is_sub_action=False, indent=True, long_name=False, html=False):
|
|
253
|
-
|
|
254
|
-
constants.ACTION_COMBO: '⚡',
|
|
255
|
-
constants.ACTION_NOISEDETECTION: '🌫',
|
|
256
|
-
constants.ACTION_FOCUSSTACK: '🎯',
|
|
257
|
-
constants.ACTION_FOCUSSTACKBUNCH: '🖇',
|
|
258
|
-
constants.ACTION_MULTILAYER: '🎞️',
|
|
259
|
-
constants.ACTION_MASKNOISE: '🎭',
|
|
260
|
-
constants.ACTION_VIGNETTING: '⭕️',
|
|
261
|
-
constants.ACTION_ALIGNFRAMES: '📐',
|
|
262
|
-
constants.ACTION_BALANCEFRAMES: '🌈'
|
|
263
|
-
}
|
|
264
|
-
ico = icon_map.get(action.type_name, '')
|
|
274
|
+
ico = constants.ACTION_ICONS.get(action.type_name, '')
|
|
265
275
|
if is_sub_action and indent:
|
|
266
276
|
txt = self.INDENT_SPACE
|
|
267
|
-
if ico == '':
|
|
268
|
-
ico = '🟣'
|
|
269
277
|
else:
|
|
270
278
|
txt = ''
|
|
271
|
-
if ico == '':
|
|
272
|
-
ico = '🔵'
|
|
273
279
|
if action.params.get('name', '') != '':
|
|
274
280
|
txt += f"{action.params['name']}"
|
|
275
281
|
if html:
|
|
276
282
|
txt = f"<b>{txt}</b>"
|
|
277
|
-
in_path, out_path = get_action_input_path(action), get_action_output_path(action)
|
|
278
|
-
|
|
279
|
-
|
|
283
|
+
in_path, out_path = get_action_input_path(action)[0], get_action_output_path(action)[0]
|
|
284
|
+
if os.path.isabs(in_path):
|
|
285
|
+
in_path = ".../" + os.path.basename(in_path)
|
|
286
|
+
if os.path.isabs(out_path):
|
|
287
|
+
out_path = ".../" + os.path.basename(out_path)
|
|
288
|
+
return f"{txt} [{ico}{action.type_name}" + \
|
|
289
|
+
(f": 📁 <i>{in_path}</i> → 📂 <i>{out_path}</i>]"
|
|
280
290
|
if long_name and not is_sub_action else "]")
|
|
281
291
|
|
|
282
292
|
def get_job_at(self, index):
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
|
+
import psutil
|
|
3
|
+
from PySide6.QtWidgets import QWidget, QHBoxLayout, QLabel, QProgressBar, QSizePolicy
|
|
4
|
+
from PySide6.QtCore import QTimer, Qt
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StatusBarSystemMonitor(QWidget):
|
|
8
|
+
def __init__(self, parent=None):
|
|
9
|
+
super().__init__(parent)
|
|
10
|
+
self.setup_ui()
|
|
11
|
+
self.setup_timer()
|
|
12
|
+
self.setFixedHeight(28)
|
|
13
|
+
|
|
14
|
+
def setup_ui(self):
|
|
15
|
+
bar_width = 100
|
|
16
|
+
bar_height = 20
|
|
17
|
+
layout = QHBoxLayout()
|
|
18
|
+
layout.setSpacing(10)
|
|
19
|
+
layout.setContentsMargins(0, 2, 0, 0)
|
|
20
|
+
layout.setAlignment(Qt.AlignLeft)
|
|
21
|
+
layout.setAlignment(Qt.AlignCenter)
|
|
22
|
+
cpu_widget = QWidget()
|
|
23
|
+
cpu_widget.setFixedSize(bar_width, bar_height)
|
|
24
|
+
self.cpu_bar = QProgressBar(cpu_widget)
|
|
25
|
+
self.cpu_bar.setRange(0, 100)
|
|
26
|
+
self.cpu_bar.setTextVisible(False)
|
|
27
|
+
self.cpu_bar.setGeometry(0, 0, bar_width, bar_height)
|
|
28
|
+
self.cpu_bar.setStyleSheet("""
|
|
29
|
+
QProgressBar {
|
|
30
|
+
border: 1px solid #cccccc;
|
|
31
|
+
border-radius: 5px;
|
|
32
|
+
background: #f0f0f0;
|
|
33
|
+
}
|
|
34
|
+
QProgressBar::chunk {
|
|
35
|
+
background-color: #3498db;
|
|
36
|
+
border-radius: 5px;
|
|
37
|
+
}
|
|
38
|
+
""")
|
|
39
|
+
self.cpu_label = QLabel("CPU: --%", cpu_widget)
|
|
40
|
+
self.cpu_label.setAlignment(Qt.AlignCenter)
|
|
41
|
+
self.cpu_label.setGeometry(0, 0, bar_width, bar_height)
|
|
42
|
+
self.cpu_label.setStyleSheet("""
|
|
43
|
+
QLabel {
|
|
44
|
+
color: #2c3e50;
|
|
45
|
+
font-weight: bold;
|
|
46
|
+
background: transparent;
|
|
47
|
+
font-size: 12px;
|
|
48
|
+
}
|
|
49
|
+
""")
|
|
50
|
+
mem_widget = QWidget()
|
|
51
|
+
mem_widget.setFixedSize(bar_width, bar_height)
|
|
52
|
+
self.mem_bar = QProgressBar(mem_widget)
|
|
53
|
+
self.mem_bar.setRange(0, 100)
|
|
54
|
+
self.mem_bar.setTextVisible(False)
|
|
55
|
+
self.mem_bar.setGeometry(0, 0, bar_width, bar_height)
|
|
56
|
+
self.mem_bar.setStyleSheet("""
|
|
57
|
+
QProgressBar {
|
|
58
|
+
border: 1px solid #cccccc;
|
|
59
|
+
border-radius: 5px;
|
|
60
|
+
background: #f0f0f0;
|
|
61
|
+
}
|
|
62
|
+
QProgressBar::chunk {
|
|
63
|
+
background-color: #2ecc71;
|
|
64
|
+
border-radius: 5px;
|
|
65
|
+
}
|
|
66
|
+
""")
|
|
67
|
+
self.mem_label = QLabel("MEM: --%", mem_widget)
|
|
68
|
+
self.mem_label.setAlignment(Qt.AlignCenter)
|
|
69
|
+
self.mem_label.setGeometry(0, 0, bar_width, bar_height)
|
|
70
|
+
self.mem_label.setStyleSheet("""
|
|
71
|
+
QLabel {
|
|
72
|
+
color: #2c3e50;
|
|
73
|
+
font-weight: bold;
|
|
74
|
+
background: transparent;
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
}
|
|
77
|
+
""")
|
|
78
|
+
layout.addWidget(cpu_widget)
|
|
79
|
+
layout.addWidget(mem_widget)
|
|
80
|
+
layout.addStretch()
|
|
81
|
+
self.setLayout(layout)
|
|
82
|
+
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
83
|
+
|
|
84
|
+
def setup_timer(self):
|
|
85
|
+
self.timer = QTimer()
|
|
86
|
+
self.timer.timeout.connect(self.update_stats)
|
|
87
|
+
self.timer.start(1000)
|
|
88
|
+
|
|
89
|
+
def update_stats(self):
|
|
90
|
+
cpu_percent = psutil.cpu_percent()
|
|
91
|
+
memory = psutil.virtual_memory()
|
|
92
|
+
mem_percent = memory.percent
|
|
93
|
+
self.cpu_bar.setValue(int(cpu_percent))
|
|
94
|
+
self.cpu_label.setText(f"CPU: {cpu_percent:.1f}%")
|
|
95
|
+
self.mem_bar.setValue(int(mem_percent))
|
|
96
|
+
self.mem_label.setText(f"MEM: {mem_percent:.1f}%")
|
|
@@ -39,13 +39,14 @@ class TimerProgressBar(QProgressBar):
|
|
|
39
39
|
""")
|
|
40
40
|
|
|
41
41
|
def time_str(self, secs):
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
xsecs = int(secs * 10)
|
|
43
|
+
x = xsecs % 10
|
|
44
|
+
ss = xsecs // 10
|
|
44
45
|
s = ss % 60
|
|
45
46
|
mm = ss // 60
|
|
46
47
|
m = mm % 60
|
|
47
48
|
h = mm // 60
|
|
48
|
-
t_str = f"{s:02d}
|
|
49
|
+
t_str = f"{s:02d}.{x:1d}s"
|
|
49
50
|
if m > 0:
|
|
50
51
|
t_str = f"{m:02d}:{t_str}"
|
|
51
52
|
if h > 0:
|
|
@@ -9,7 +9,7 @@ from .. gui.base_form_dialog import BaseFormDialog
|
|
|
9
9
|
|
|
10
10
|
class ExifData(BaseFormDialog):
|
|
11
11
|
def __init__(self, exif, parent=None):
|
|
12
|
-
super().__init__("EXIF data", parent)
|
|
12
|
+
super().__init__("EXIF data", parent=parent)
|
|
13
13
|
self.exif = exif
|
|
14
14
|
self.create_form()
|
|
15
15
|
button_container = QWidget()
|