shinestacker 1.2.1__py3-none-any.whl → 1.3.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 +152 -112
- shinestacker/algorithms/align_auto.py +76 -0
- shinestacker/algorithms/align_parallel.py +336 -0
- shinestacker/algorithms/balance.py +3 -1
- shinestacker/algorithms/base_stack_algo.py +25 -22
- shinestacker/algorithms/depth_map.py +9 -14
- shinestacker/algorithms/multilayer.py +8 -8
- shinestacker/algorithms/noise_detection.py +10 -10
- shinestacker/algorithms/pyramid.py +10 -24
- shinestacker/algorithms/pyramid_auto.py +21 -24
- shinestacker/algorithms/pyramid_tiles.py +31 -25
- shinestacker/algorithms/stack.py +21 -17
- shinestacker/algorithms/stack_framework.py +98 -47
- shinestacker/algorithms/utils.py +16 -0
- shinestacker/algorithms/vignetting.py +13 -10
- shinestacker/app/gui_utils.py +10 -0
- shinestacker/app/main.py +10 -4
- shinestacker/app/project.py +3 -1
- shinestacker/app/retouch.py +3 -1
- 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 +409 -239
- shinestacker/gui/base_form_dialog.py +2 -2
- shinestacker/gui/colors.py +1 -0
- shinestacker/gui/folder_file_selection.py +106 -0
- shinestacker/gui/gui_run.py +12 -10
- shinestacker/gui/main_window.py +10 -5
- 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 +40 -28
- shinestacker/gui/select_path_widget.py +1 -1
- shinestacker/gui/sys_mon.py +97 -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.1.dist-info}/METADATA +6 -6
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/RECORD +46 -42
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/top_level.txt +0 -0
|
@@ -13,10 +13,10 @@ def create_form_layout(parent):
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class BaseFormDialog(QDialog):
|
|
16
|
-
def __init__(self, title, parent=None):
|
|
16
|
+
def __init__(self, title, width=500, parent=None):
|
|
17
17
|
super().__init__(parent)
|
|
18
18
|
self.setWindowTitle(title)
|
|
19
|
-
self.resize(
|
|
19
|
+
self.resize(width, self.height())
|
|
20
20
|
self.form_layout = create_form_layout(self)
|
|
21
21
|
|
|
22
22
|
def add_row_to_layout(self, item):
|
shinestacker/gui/colors.py
CHANGED
|
@@ -16,6 +16,7 @@ class ColorPalette:
|
|
|
16
16
|
BLACK = ColorEntry(0, 0, 0)
|
|
17
17
|
WHITE = ColorEntry(255, 255, 255)
|
|
18
18
|
LIGHT_BLUE = ColorEntry(210, 210, 240)
|
|
19
|
+
LIGHT_GREEN = ColorEntry(210, 240, 210)
|
|
19
20
|
LIGHT_RED = ColorEntry(240, 210, 210)
|
|
20
21
|
DARK_BLUE = ColorEntry(0, 0, 160)
|
|
21
22
|
DARK_RED = ColorEntry(160, 0, 0)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
|
+
import os
|
|
3
|
+
from PySide6.QtWidgets import (QWidget, QRadioButton, QButtonGroup, QLineEdit,
|
|
4
|
+
QPushButton, QHBoxLayout, QVBoxLayout, QFileDialog, QMessageBox)
|
|
5
|
+
from PySide6.QtCore import Qt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FolderFileSelectionWidget(QWidget):
|
|
9
|
+
def __init__(self, parent=None):
|
|
10
|
+
super().__init__(parent)
|
|
11
|
+
self.selection_mode = 'folder' # 'folder' or 'files'
|
|
12
|
+
self.selected_files = []
|
|
13
|
+
self.setup_ui()
|
|
14
|
+
|
|
15
|
+
def setup_ui(self):
|
|
16
|
+
self.mode_group = QButtonGroup(self)
|
|
17
|
+
self.folder_mode_radio = QRadioButton("Select Folder")
|
|
18
|
+
self.folder_mode_radio.setMaximumWidth(100)
|
|
19
|
+
self.files_mode_radio = QRadioButton("Select Files")
|
|
20
|
+
self.files_mode_radio.setMaximumWidth(100)
|
|
21
|
+
self.folder_mode_radio.setChecked(True)
|
|
22
|
+
self.mode_group.addButton(self.folder_mode_radio)
|
|
23
|
+
self.mode_group.addButton(self.files_mode_radio)
|
|
24
|
+
self.path_edit = QLineEdit()
|
|
25
|
+
self.path_edit.setPlaceholderText("input files folder")
|
|
26
|
+
self.browse_button = QPushButton("Browse Folder...")
|
|
27
|
+
self.browse_button.setFixedWidth(120)
|
|
28
|
+
main_layout = QVBoxLayout()
|
|
29
|
+
main_layout.setSpacing(10)
|
|
30
|
+
main_layout.setAlignment(Qt.AlignLeft)
|
|
31
|
+
mode_layout = QHBoxLayout()
|
|
32
|
+
mode_layout.setContentsMargins(2, 2, 2, 2)
|
|
33
|
+
mode_layout.setSpacing(20)
|
|
34
|
+
mode_layout.addWidget(self.folder_mode_radio)
|
|
35
|
+
mode_layout.addWidget(self.files_mode_radio)
|
|
36
|
+
mode_layout.addStretch()
|
|
37
|
+
main_layout.addLayout(mode_layout)
|
|
38
|
+
input_layout = QHBoxLayout()
|
|
39
|
+
input_layout.setContentsMargins(2, 2, 2, 2)
|
|
40
|
+
input_layout.setSpacing(8)
|
|
41
|
+
input_layout.setAlignment(Qt.AlignLeft)
|
|
42
|
+
input_layout.addWidget(self.path_edit)
|
|
43
|
+
input_layout.addWidget(self.browse_button)
|
|
44
|
+
main_layout.addLayout(input_layout)
|
|
45
|
+
self.setLayout(main_layout)
|
|
46
|
+
self.folder_mode_radio.toggled.connect(self.update_selection_mode)
|
|
47
|
+
self.files_mode_radio.toggled.connect(self.update_selection_mode)
|
|
48
|
+
self.browse_button.clicked.connect(self.handle_browse)
|
|
49
|
+
|
|
50
|
+
def update_selection_mode(self):
|
|
51
|
+
if self.folder_mode_radio.isChecked():
|
|
52
|
+
self.selection_mode = 'folder'
|
|
53
|
+
self.browse_button.setText("Browse Folder...")
|
|
54
|
+
# self.path_edit.setPlaceholderText("input files folder")
|
|
55
|
+
else:
|
|
56
|
+
self.selection_mode = 'files'
|
|
57
|
+
self.browse_button.setText("Browse Files...")
|
|
58
|
+
# self.path_edit.setPlaceholderText("input files")
|
|
59
|
+
|
|
60
|
+
def handle_browse(self):
|
|
61
|
+
self.path_edit.setText('')
|
|
62
|
+
if self.selection_mode == 'folder':
|
|
63
|
+
self.browse_folder()
|
|
64
|
+
else:
|
|
65
|
+
self.browse_files()
|
|
66
|
+
|
|
67
|
+
def browse_folder(self):
|
|
68
|
+
path = QFileDialog.getExistingDirectory(self, "Select Input Folder")
|
|
69
|
+
if path:
|
|
70
|
+
self.selected_files = []
|
|
71
|
+
self.path_edit.setText(path)
|
|
72
|
+
|
|
73
|
+
def browse_files(self):
|
|
74
|
+
files, _ = QFileDialog.getOpenFileNames(
|
|
75
|
+
self, "Select Input Files", "",
|
|
76
|
+
"Image files (*.png *.jpg *.jpeg *.tif *.tiff)"
|
|
77
|
+
)
|
|
78
|
+
if files:
|
|
79
|
+
parent_dir = os.path.dirname(files[0])
|
|
80
|
+
if all(os.path.dirname(f) == parent_dir for f in files):
|
|
81
|
+
self.selected_files = files
|
|
82
|
+
self.path_edit.setText(parent_dir)
|
|
83
|
+
else:
|
|
84
|
+
self.selected_files = []
|
|
85
|
+
QMessageBox.warning(
|
|
86
|
+
self, "Invalid Selection",
|
|
87
|
+
"All files must be in the same directory."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def get_selection_mode(self):
|
|
91
|
+
return self.selection_mode
|
|
92
|
+
|
|
93
|
+
def get_selected_files(self):
|
|
94
|
+
return self.selected_files
|
|
95
|
+
|
|
96
|
+
def num_selected_files(self):
|
|
97
|
+
return len(self.selected_files)
|
|
98
|
+
|
|
99
|
+
def get_selected_filenames(self):
|
|
100
|
+
return [os.path.basename(file_path) for file_path in self.selected_files]
|
|
101
|
+
|
|
102
|
+
def get_path(self):
|
|
103
|
+
return self.path_edit.text()
|
|
104
|
+
|
|
105
|
+
def text_changed_connect(self, callback):
|
|
106
|
+
self.path_edit.textChanged.connect(callback)
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -18,6 +18,7 @@ from .colors import (
|
|
|
18
18
|
ACTION_STOPPED_COLOR, ACTION_FAILED_COLOR)
|
|
19
19
|
from .time_progress_bar import TimerProgressBar
|
|
20
20
|
from .flow_layout import FlowLayout
|
|
21
|
+
from .sys_mon import StatusBarSystemMonitor
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class ColorButton(QPushButton):
|
|
@@ -96,7 +97,8 @@ class RunWindow(QTextEditLogger):
|
|
|
96
97
|
self.right_area.setMaximumWidth(0)
|
|
97
98
|
self.image_area_widget.setFixedWidth(0)
|
|
98
99
|
layout.addLayout(output_layout)
|
|
99
|
-
|
|
100
|
+
self.system_monitor = StatusBarSystemMonitor(self)
|
|
101
|
+
self.status_bar.addPermanentWidget(self.system_monitor)
|
|
100
102
|
n_paths = len(self.retouch_paths) if self.retouch_paths else 0
|
|
101
103
|
if n_paths == 1:
|
|
102
104
|
self.retouch_widget = QPushButton(f"Retouch {self.retouch_paths[0][0]}")
|
|
@@ -284,15 +286,15 @@ class RunWorker(LogWorker):
|
|
|
284
286
|
self.id_str = id_str
|
|
285
287
|
self.status = constants.STATUS_RUNNING
|
|
286
288
|
self.callbacks = {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
289
|
+
constants.CALLBACK_BEFORE_ACTION: self.before_action,
|
|
290
|
+
constants.CALLBACK_AFTER_ACTION: self.after_action,
|
|
291
|
+
constants.CALLBACK_STEP_COUNTS: self.step_counts,
|
|
292
|
+
constants.CALLBACK_BEGIN_STEPS: self.begin_steps,
|
|
293
|
+
constants.CALLBACK_END_STEPS: self.end_steps,
|
|
294
|
+
constants.CALLBACK_AFTER_STEP: self.after_step,
|
|
295
|
+
constants.CALLBACK_CHECK_RUNNING: self.check_running,
|
|
296
|
+
constants.CALLBACK_SAVE_PLOT: self.save_plot,
|
|
297
|
+
constants.CALLBACK_OPEN_APP: self.open_app
|
|
296
298
|
}
|
|
297
299
|
self.tag = ""
|
|
298
300
|
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -43,7 +43,10 @@ class ProjectLogWorker(RunWorker):
|
|
|
43
43
|
|
|
44
44
|
LIST_STYLE_SHEET = f"""
|
|
45
45
|
QListWidget::item:selected {{
|
|
46
|
-
background-color: #{ColorPalette.LIGHT_BLUE.hex()}
|
|
46
|
+
background-color: #{ColorPalette.LIGHT_BLUE.hex()};
|
|
47
|
+
}}
|
|
48
|
+
QListWidget::item:hover {{
|
|
49
|
+
background-color: #F0F0F0;
|
|
47
50
|
}}
|
|
48
51
|
"""
|
|
49
52
|
|
|
@@ -298,6 +301,9 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
298
301
|
edit_config_action.triggered.connect(self.edit_current_action)
|
|
299
302
|
menu.addAction(edit_config_action)
|
|
300
303
|
menu.addSeparator()
|
|
304
|
+
menu.addAction(self.menu_manager.run_job_action)
|
|
305
|
+
menu.addAction(self.menu_manager.run_all_jobs_action)
|
|
306
|
+
menu.addSeparator()
|
|
301
307
|
self.current_action_working_path, name = get_action_working_path(current_action)
|
|
302
308
|
if self.current_action_working_path != '' and \
|
|
303
309
|
os.path.exists(self.current_action_working_path):
|
|
@@ -336,9 +342,6 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
336
342
|
self.browse_output_path_action = QAction(action_name)
|
|
337
343
|
self.browse_output_path_action.triggered.connect(self.browse_output_path)
|
|
338
344
|
menu.addAction(self.browse_output_path_action)
|
|
339
|
-
menu.addSeparator()
|
|
340
|
-
menu.addAction(self.menu_manager.run_job_action)
|
|
341
|
-
menu.addAction(self.menu_manager.run_all_jobs_action)
|
|
342
345
|
if current_action.type_name == constants.ACTION_JOB:
|
|
343
346
|
retouch_path = self.get_retouch_path(current_action)
|
|
344
347
|
if len(retouch_path) > 0:
|
|
@@ -421,6 +424,8 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
421
424
|
for worker in self._workers:
|
|
422
425
|
worker.stop()
|
|
423
426
|
self.close()
|
|
427
|
+
return True
|
|
428
|
+
return False
|
|
424
429
|
|
|
425
430
|
def toggle_expert_options(self):
|
|
426
431
|
self.expert_options = self.menu_manager.expert_options_action.isChecked()
|
|
@@ -528,7 +533,7 @@ class MainWindow(QMainWindow, LogManager):
|
|
|
528
533
|
r = self.get_retouch_path(job)
|
|
529
534
|
if len(r) > 0:
|
|
530
535
|
retouch_paths.append((job.params["name"], r))
|
|
531
|
-
new_window, id_str = self.create_new_window(f"{project_name} [Project
|
|
536
|
+
new_window, id_str = self.create_new_window(f"{project_name} [Project]",
|
|
532
537
|
labels, retouch_paths)
|
|
533
538
|
worker = ProjectLogWorker(self.project(), id_str)
|
|
534
539
|
self.connect_worker_signals(worker, new_window)
|
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) Confirm")
|
|
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) Execute")
|
|
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 = self.input_widget.num_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)
|