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.

Files changed (46) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +152 -112
  3. shinestacker/algorithms/align_auto.py +76 -0
  4. shinestacker/algorithms/align_parallel.py +336 -0
  5. shinestacker/algorithms/balance.py +3 -1
  6. shinestacker/algorithms/base_stack_algo.py +25 -22
  7. shinestacker/algorithms/depth_map.py +9 -14
  8. shinestacker/algorithms/multilayer.py +8 -8
  9. shinestacker/algorithms/noise_detection.py +10 -10
  10. shinestacker/algorithms/pyramid.py +10 -24
  11. shinestacker/algorithms/pyramid_auto.py +21 -24
  12. shinestacker/algorithms/pyramid_tiles.py +31 -25
  13. shinestacker/algorithms/stack.py +21 -17
  14. shinestacker/algorithms/stack_framework.py +98 -47
  15. shinestacker/algorithms/utils.py +16 -0
  16. shinestacker/algorithms/vignetting.py +13 -10
  17. shinestacker/app/gui_utils.py +10 -0
  18. shinestacker/app/main.py +10 -4
  19. shinestacker/app/project.py +3 -1
  20. shinestacker/app/retouch.py +3 -1
  21. shinestacker/config/constants.py +60 -25
  22. shinestacker/config/gui_constants.py +1 -1
  23. shinestacker/core/core_utils.py +4 -0
  24. shinestacker/core/framework.py +104 -23
  25. shinestacker/gui/action_config.py +4 -5
  26. shinestacker/gui/action_config_dialog.py +409 -239
  27. shinestacker/gui/base_form_dialog.py +2 -2
  28. shinestacker/gui/colors.py +1 -0
  29. shinestacker/gui/folder_file_selection.py +106 -0
  30. shinestacker/gui/gui_run.py +12 -10
  31. shinestacker/gui/main_window.py +10 -5
  32. shinestacker/gui/new_project.py +171 -73
  33. shinestacker/gui/project_controller.py +10 -6
  34. shinestacker/gui/project_converter.py +4 -2
  35. shinestacker/gui/project_editor.py +40 -28
  36. shinestacker/gui/select_path_widget.py +1 -1
  37. shinestacker/gui/sys_mon.py +97 -0
  38. shinestacker/gui/time_progress_bar.py +4 -3
  39. shinestacker/retouch/exif_data.py +1 -1
  40. shinestacker/retouch/image_editor_ui.py +2 -0
  41. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/METADATA +6 -6
  42. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/RECORD +46 -42
  43. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/WHEEL +0 -0
  44. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/entry_points.txt +0 -0
  45. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/licenses/LICENSE +0 -0
  46. {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(500, self.height())
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):
@@ -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)
@@ -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
- 'before_action': self.before_action,
288
- 'after_action': self.after_action,
289
- 'step_counts': self.step_counts,
290
- 'begin_steps': self.begin_steps,
291
- 'end_steps': self.end_steps,
292
- 'after_step': self.after_step,
293
- 'save_plot': self.save_plot,
294
- 'check_running': self.check_running,
295
- 'open_app': self.open_app
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
 
@@ -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)
@@ -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 .select_path_widget import create_select_file_paths_widget
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
- icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
47
- app_icon = QIcon(icon_path)
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
- self.add_bold_label("1️⃣ Select input folder, all images therein will be merged. ")
95
- self.form_layout.addRow("Input folder:", container)
96
- self.form_layout.addRow("Number of frames: ", self.frames_label)
97
- self.add_label("")
98
- self.add_bold_label("2️⃣ Select basic options.")
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
- self.form_layout.addRow("Automatic noise detection:", self.noise_detection)
101
- self.form_layout.addRow("Vignetting correction:", self.vignetting_correction)
102
- self.form_layout.addRow("Align layers:", self.align_frames)
103
- self.form_layout.addRow("Balance layers:", self.balance_frames)
104
- self.form_layout.addRow("Bunch stack:", self.bunch_stack)
105
- self.form_layout.addRow("Bunch frames:", self.bunch_frames)
106
- self.form_layout.addRow("Bunch overlap:", self.bunch_overlap)
107
- self.form_layout.addRow("Number of bunches: ", self.bunches_label)
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
- self.form_layout.addRow("Focus stack (pyramid):", self.focus_stack_pyramid)
110
- self.form_layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
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
- self.form_layout.addRow("Focus stack:", self.focus_stack_pyramid)
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
- self.form_layout.addRow("Save multi layer TIFF:", self.multi_layer)
115
- self.add_label("")
116
- self.add_bold_label("3️⃣ Push 🆗 for further options, then press ▶️ to run.")
117
- self.add_label("")
118
- self.add_label("4️⃣ "
119
- "Select: <b>View</b> > <b>Expert options</b> "
120
- "to unlock advanced configuration.")
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.input_folder.text():
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
- self.n_image_files = count_image_files(self.input_folder.text())
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
- input_folder = self.input_folder.text()
156
- if not input_folder:
157
- QMessageBox.warning(self, "Input Required", "Please select an input folder")
158
- return
159
- if not os.path.exists(input_folder):
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 not os.path.isdir(input_folder):
163
- QMessageBox.warning(self, "Invalid Path", "The specified path is not a folder")
164
- return
165
- if len(input_folder.split('/')) < 2:
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
- path = self.input_folder.text()
170
- files = os.listdir(path)
171
- file_path = None
172
- for filename in files:
173
- full_path = os.path.join(path, filename)
174
- if extension_tif_jpg(full_path):
175
- file_path = full_path
176
- break
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 now in the selected path")
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.input_folder.text()
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
- job = ActionConfig(constants.ACTION_JOB,
169
- {'name': f'{input_path}-focus-stack',
170
- 'working_path': working_path,
171
- 'input_path': input_path})
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.mark_as_modified(True, "New Project")
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.align import AlignFrames
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 AlignFrames(**params)
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)