shinestacker 0.5.0__py3-none-any.whl → 1.0.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 (57) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +4 -12
  3. shinestacker/algorithms/balance.py +11 -9
  4. shinestacker/algorithms/depth_map.py +0 -30
  5. shinestacker/algorithms/utils.py +10 -0
  6. shinestacker/algorithms/vignetting.py +116 -70
  7. shinestacker/app/about_dialog.py +37 -16
  8. shinestacker/app/gui_utils.py +1 -1
  9. shinestacker/app/help_menu.py +1 -1
  10. shinestacker/app/main.py +2 -2
  11. shinestacker/app/project.py +2 -2
  12. shinestacker/config/constants.py +4 -1
  13. shinestacker/config/gui_constants.py +3 -4
  14. shinestacker/gui/action_config.py +5 -561
  15. shinestacker/gui/action_config_dialog.py +567 -0
  16. shinestacker/gui/base_form_dialog.py +18 -0
  17. shinestacker/gui/colors.py +5 -6
  18. shinestacker/gui/gui_logging.py +0 -1
  19. shinestacker/gui/gui_run.py +54 -106
  20. shinestacker/gui/ico/shinestacker.icns +0 -0
  21. shinestacker/gui/ico/shinestacker.ico +0 -0
  22. shinestacker/gui/ico/shinestacker.png +0 -0
  23. shinestacker/gui/ico/shinestacker.svg +60 -0
  24. shinestacker/gui/main_window.py +275 -371
  25. shinestacker/gui/menu_manager.py +236 -0
  26. shinestacker/gui/new_project.py +75 -20
  27. shinestacker/gui/{actions_window.py → project_controller.py} +166 -79
  28. shinestacker/gui/project_converter.py +6 -6
  29. shinestacker/gui/project_editor.py +248 -165
  30. shinestacker/gui/project_model.py +2 -7
  31. shinestacker/gui/tab_widget.py +81 -0
  32. shinestacker/gui/time_progress_bar.py +95 -0
  33. shinestacker/retouch/base_filter.py +173 -40
  34. shinestacker/retouch/brush_preview.py +0 -10
  35. shinestacker/retouch/brush_tool.py +2 -5
  36. shinestacker/retouch/denoise_filter.py +5 -44
  37. shinestacker/retouch/exif_data.py +10 -13
  38. shinestacker/retouch/file_loader.py +1 -1
  39. shinestacker/retouch/filter_manager.py +1 -4
  40. shinestacker/retouch/image_editor_ui.py +318 -40
  41. shinestacker/retouch/image_viewer.py +34 -11
  42. shinestacker/retouch/io_gui_handler.py +34 -30
  43. shinestacker/retouch/layer_collection.py +2 -0
  44. shinestacker/retouch/shortcuts_help.py +12 -0
  45. shinestacker/retouch/unsharp_mask_filter.py +10 -10
  46. shinestacker/retouch/vignetting_filter.py +69 -0
  47. shinestacker/retouch/white_balance_filter.py +46 -14
  48. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/METADATA +8 -2
  49. shinestacker-1.0.1.dist-info/RECORD +91 -0
  50. shinestacker/app/app_config.py +0 -22
  51. shinestacker/retouch/image_editor.py +0 -197
  52. shinestacker/retouch/image_filters.py +0 -69
  53. shinestacker-0.5.0.dist-info/RECORD +0 -87
  54. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/WHEEL +0 -0
  55. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/entry_points.txt +0 -0
  56. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/licenses/LICENSE +0 -0
  57. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,236 @@
1
+ # pylint: disable=C0114, C0115, C0116, R0904, E0611, R0902, W0201
2
+ import os
3
+ from PySide6.QtGui import QAction, QIcon
4
+ from PySide6.QtWidgets import QMenu, QComboBox
5
+ from .. config.constants import constants
6
+
7
+
8
+ class MenuManager:
9
+ def __init__(self, menubar, actions, project_editor, parent):
10
+ self.script_dir = os.path.dirname(__file__)
11
+ self.project_editor = project_editor
12
+ self.parent = parent
13
+ self.menubar = menubar
14
+ self.actions = actions
15
+ self.action_selector = None
16
+ self.sub_action_selector = None
17
+ self.shortcuts = {
18
+ "&New...": "Ctrl+N",
19
+ "&Open...": "Ctrl+O",
20
+ "&Close": "Ctrl+W",
21
+ "&Save": "Ctrl+S",
22
+ "Save &As...": "Ctrl+Shift+S",
23
+ "&Undo": "Ctrl+Z",
24
+ "&Cut": "Ctrl+X",
25
+ "Cop&y": "Ctrl+C",
26
+ "&Paste": "Ctrl+V",
27
+ "Duplicate": "Ctrl+D",
28
+ "Delete": "Del",
29
+ "Move &Up": "Ctrl+Up",
30
+ "Move &Down": "Ctrl+Down",
31
+ "E&nable": "Ctrl+E",
32
+ "Di&sable": "Ctrl+B",
33
+ "Enable All": "Ctrl+Shift+E",
34
+ "Disable All": "Ctrl+Shift+B",
35
+ "Expert Options": "Ctrl+Shift+X",
36
+ "Add Job": "Ctrl+P",
37
+ "Run Job": "Ctrl+J",
38
+ "Run All Jobs": "Ctrl+Shift+J"
39
+ }
40
+ self.icons = {
41
+ "Delete": "close-round-line-icon",
42
+ "Add Job": "plus-round-line-icon",
43
+ "Run Job": "play-button-round-icon",
44
+ "Run All Jobs": "forward-button-icon",
45
+ }
46
+ self.tooltips = {
47
+ "Delete": "Delete",
48
+ "Add Job": "Add job",
49
+ "Run Job": "Run job",
50
+ "Run All Jobs": "Run all jobs",
51
+ }
52
+
53
+ def get_icon(self, icon):
54
+ return QIcon(os.path.join(self.script_dir, f"img/{icon}.png"))
55
+
56
+ def action(self, name):
57
+ action = QAction(name, self.parent)
58
+ shortcut = self.shortcuts.get(name, '')
59
+ if shortcut:
60
+ action.setShortcut(shortcut)
61
+ icon = self.icons.get(name, '')
62
+ if icon:
63
+ action.setIcon(self.get_icon(icon))
64
+ tooltip = self.tooltips.get(name, '')
65
+ if tooltip:
66
+ action.setToolTip(tooltip)
67
+ action_fun = self.actions.get(name, None)
68
+ if action_fun is not None:
69
+ action.triggered.connect(action_fun)
70
+ return action
71
+
72
+ def add_file_menu(self):
73
+ menu = self.menubar.addMenu("&File")
74
+ for name in ["&New...", "&Open...", "&Close"]:
75
+ menu.addAction(self.action(name))
76
+ menu.addSeparator()
77
+ self.save_action = self.action("&Save")
78
+ menu.addAction(self.save_action)
79
+ self.save_as_action = self.action("Save &As...")
80
+ menu.addAction(self.save_as_action)
81
+ self.save_actions_set_enabled(False)
82
+
83
+ def add_edit_menu(self):
84
+ menu = self.menubar.addMenu("&Edit")
85
+ for name in ["&Undo", "&Cut", "Cop&y", "&Paste", "Duplicate"]:
86
+ menu.addAction(self.action(name))
87
+ self.delete_element_action = self.action("Delete")
88
+ self.delete_element_action.setEnabled(False)
89
+ menu.addAction(self.delete_element_action)
90
+ menu.addSeparator()
91
+ for name in ["Move &Up", "Move &Down"]:
92
+ menu.addAction(self.action(name))
93
+ menu.addSeparator()
94
+ self.enable_action = self.action("E&nable")
95
+ menu.addAction(self.enable_action)
96
+ self.disable_action = self.action("Di&sable")
97
+ menu.addAction(self.disable_action)
98
+ for name in ["Enable All", "Disable All"]:
99
+ menu.addAction(self.action(name))
100
+
101
+ def add_view_menu(self):
102
+ menu = self.menubar.addMenu("&View")
103
+ self.expert_options_action = self.action("Expert Options")
104
+ self.expert_options_action.setCheckable(True)
105
+ menu.addAction(self.expert_options_action)
106
+
107
+ def add_job_menu(self):
108
+ menu = self.menubar.addMenu("&Jobs")
109
+ self.add_job_action = self.action("Add Job")
110
+ menu.addAction(self.add_job_action)
111
+ menu.addSeparator()
112
+ self.run_job_action = self.action("Run Job")
113
+ self.run_job_action.setEnabled(False)
114
+ menu.addAction(self.run_job_action)
115
+ self.run_all_jobs_action = self.action("Run All Jobs")
116
+ self.run_all_jobs_action.setEnabled(False)
117
+ menu.addAction(self.run_all_jobs_action)
118
+
119
+ def add_actions_menu(self):
120
+ menu = self.menubar.addMenu("&Actions")
121
+ add_action_menu = QMenu("Add Action", self.parent)
122
+ for action in constants.ACTION_TYPES:
123
+ entry_action = QAction(action, self.parent)
124
+ entry_action.triggered.connect({
125
+ constants.ACTION_COMBO: self.add_action_combined_actions,
126
+ constants.ACTION_NOISEDETECTION: self.add_action_noise_detection,
127
+ constants.ACTION_FOCUSSTACK: self.add_action_focus_stack,
128
+ constants.ACTION_FOCUSSTACKBUNCH: self.add_action_focus_stack_bunch,
129
+ constants.ACTION_MULTILAYER: self.add_action_multilayer
130
+ }[action])
131
+ add_action_menu.addAction(entry_action)
132
+ menu.addMenu(add_action_menu)
133
+ add_sub_action_menu = QMenu("Add Sub Action", self.parent)
134
+ self.sub_action_menu_entries = []
135
+ for action in constants.SUB_ACTION_TYPES:
136
+ entry_action = QAction(action, self.parent)
137
+ entry_action.triggered.connect({
138
+ constants.ACTION_MASKNOISE: self.add_sub_action_make_noise,
139
+ constants.ACTION_VIGNETTING: self.add_sub_action_vignetting,
140
+ constants.ACTION_ALIGNFRAMES: self.add_sub_action_align_frames,
141
+ constants.ACTION_BALANCEFRAMES: self.add_sub_action_balance_frames
142
+ }[action])
143
+ entry_action.setEnabled(False)
144
+ self.sub_action_menu_entries.append(entry_action)
145
+ add_sub_action_menu.addAction(entry_action)
146
+ menu.addMenu(add_sub_action_menu)
147
+
148
+ def add_help_menu(self):
149
+ menu = self.menubar.addMenu("&Help")
150
+ menu.setObjectName("Help")
151
+
152
+ def add_menus(self):
153
+ self.add_file_menu()
154
+ self.add_edit_menu()
155
+ self.add_view_menu()
156
+ self.add_job_menu()
157
+ self.add_actions_menu()
158
+ self.add_help_menu()
159
+
160
+ def add_action(self, type_name=False):
161
+ if type_name is False:
162
+ type_name = self.action_selector.currentText()
163
+ self.project_editor.add_action(type_name)
164
+
165
+ def add_sub_action(self, type_name=False):
166
+ if type_name is False:
167
+ type_name = self.sub_action_selector.currentText()
168
+ self.project_editor.add_sub_action(type_name)
169
+
170
+ def save_actions_set_enabled(self, enabled):
171
+ self.save_action.setEnabled(enabled)
172
+ self.save_as_action.setEnabled(enabled)
173
+
174
+ def add_action_combined_actions(self):
175
+ self.add_action(constants.ACTION_COMBO)
176
+
177
+ def add_action_noise_detection(self):
178
+ self.add_action(constants.ACTION_NOISEDETECTION)
179
+
180
+ def add_action_focus_stack(self):
181
+ self.add_action(constants.ACTION_FOCUSSTACK)
182
+
183
+ def add_action_focus_stack_bunch(self):
184
+ self.add_action(constants.ACTION_FOCUSSTACKBUNCH)
185
+
186
+ def add_action_multilayer(self):
187
+ self.add_action(constants.ACTION_MULTILAYER)
188
+
189
+ def add_sub_action_make_noise(self):
190
+ self.add_sub_action(constants.ACTION_MASKNOISE)
191
+
192
+ def add_sub_action_vignetting(self):
193
+ self.add_sub_action(constants.ACTION_VIGNETTING)
194
+
195
+ def add_sub_action_align_frames(self):
196
+ self.add_sub_action(constants.ACTION_ALIGNFRAMES)
197
+
198
+ def add_sub_action_balance_frames(self):
199
+ self.add_sub_action(constants.ACTION_BALANCEFRAMES)
200
+
201
+ def fill_toolbar(self, toolbar):
202
+ toolbar.addAction(self.add_job_action)
203
+ toolbar.addSeparator()
204
+ self.action_selector = QComboBox()
205
+ self.action_selector.addItems(constants.ACTION_TYPES)
206
+ self.action_selector.setEnabled(False)
207
+ toolbar.addWidget(self.action_selector)
208
+ self.add_action_entry_action = QAction("Add Action", self.parent)
209
+ self.add_action_entry_action.setIcon(
210
+ QIcon(os.path.join(self.script_dir, "img/plus-round-line-icon.png")))
211
+ self.add_action_entry_action.setToolTip("Add action")
212
+ self.add_action_entry_action.triggered.connect(self.add_action)
213
+ self.add_action_entry_action.setEnabled(False)
214
+ toolbar.addAction(self.add_action_entry_action)
215
+ self.sub_action_selector = QComboBox()
216
+ self.sub_action_selector.addItems(constants.SUB_ACTION_TYPES)
217
+ self.sub_action_selector.setEnabled(False)
218
+ toolbar.addWidget(self.sub_action_selector)
219
+ self.add_sub_action_entry_action = QAction("Add Sub Action", self.parent)
220
+ self.add_sub_action_entry_action.setIcon(
221
+ QIcon(os.path.join(self.script_dir, "img/plus-round-line-icon.png")))
222
+ self.add_sub_action_entry_action.setToolTip("Add sub action")
223
+ self.add_sub_action_entry_action.triggered.connect(self.add_sub_action)
224
+ self.add_sub_action_entry_action.setEnabled(False)
225
+ toolbar.addAction(self.add_sub_action_entry_action)
226
+ toolbar.addSeparator()
227
+ toolbar.addAction(self.delete_element_action)
228
+ toolbar.addSeparator()
229
+ toolbar.addAction(self.run_job_action)
230
+ toolbar.addAction(self.run_all_jobs_action)
231
+
232
+ def set_enabled_sub_actions_gui(self, enabled):
233
+ self.add_sub_action_entry_action.setEnabled(enabled)
234
+ self.sub_action_selector.setEnabled(enabled)
235
+ for a in self.sub_action_menu_entries:
236
+ a.setEnabled(enabled)
@@ -1,27 +1,24 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902
2
2
  import os
3
- from PySide6.QtWidgets import (QFormLayout, QHBoxLayout, QPushButton,
4
- QDialog, QLabel, QCheckBox, QSpinBox, QMessageBox)
3
+ import numpy as np
4
+ from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QLabel, QCheckBox, QSpinBox,
5
+ QMessageBox)
5
6
  from PySide6.QtGui import QIcon
6
7
  from PySide6.QtCore import Qt
7
8
  from .. config.gui_constants import gui_constants
8
9
  from .. config.constants import constants
10
+ from .. algorithms.utils import read_img
9
11
  from .. algorithms.stack import get_bunches
10
12
  from .select_path_widget import create_select_file_paths_widget
13
+ from .base_form_dialog import BaseFormDialog
11
14
 
12
15
  DEFAULT_NO_COUNT_LABEL = " - "
16
+ EXTENSIONS = ['jpg', 'jpeg', 'tif', 'tiff']
13
17
 
14
18
 
15
- class NewProjectDialog(QDialog):
19
+ class NewProjectDialog(BaseFormDialog):
16
20
  def __init__(self, parent=None):
17
- super().__init__(parent)
18
- self.setWindowTitle("New Project")
19
- self.resize(500, self.height())
20
- self.layout = QFormLayout(self)
21
- self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
22
- self.layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
23
- self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
24
- self.layout.setLabelAlignment(Qt.AlignLeft)
21
+ super().__init__("New Project", parent)
25
22
  self.create_form()
26
23
  button_box = QHBoxLayout()
27
24
  ok_button = QPushButton("OK")
@@ -29,9 +26,10 @@ class NewProjectDialog(QDialog):
29
26
  cancel_button = QPushButton("Cancel")
30
27
  button_box.addWidget(ok_button)
31
28
  button_box.addWidget(cancel_button)
32
- self.layout.addRow(button_box)
29
+ self.add_row_to_layout(button_box)
33
30
  ok_button.clicked.connect(self.accept)
34
31
  cancel_button.clicked.connect(self.reject)
32
+ self.n_image_files = 0
35
33
 
36
34
  def expert(self):
37
35
  return self.parent().expert_options
@@ -41,6 +39,10 @@ class NewProjectDialog(QDialog):
41
39
  label.setStyleSheet("font-weight: bold")
42
40
  self.layout.addRow(label)
43
41
 
42
+ def add_label(self, label):
43
+ label = QLabel(label)
44
+ self.layout.addRow(label)
45
+
44
46
  def create_form(self):
45
47
  icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
46
48
  app_icon = QIcon(icon_path)
@@ -90,10 +92,11 @@ class NewProjectDialog(QDialog):
90
92
  self.multi_layer = QCheckBox()
91
93
  self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
92
94
 
93
- self.add_bold_label("Select input:")
95
+ self.add_bold_label("1️⃣ Select input folder, all images therein will be merged. ")
94
96
  self.layout.addRow("Input folder:", container)
95
97
  self.layout.addRow("Number of frames: ", self.frames_label)
96
- self.add_bold_label("Select actions:")
98
+ self.add_label("")
99
+ self.add_bold_label("2️⃣ Select basic options.")
97
100
  if self.expert():
98
101
  self.layout.addRow("Automatic noise detection:", self.noise_detection)
99
102
  self.layout.addRow("Vignetting correction:", self.vignetting_correction)
@@ -109,6 +112,12 @@ class NewProjectDialog(QDialog):
109
112
  else:
110
113
  self.layout.addRow("Focus stack:", self.focus_stack_pyramid)
111
114
  self.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.")
112
121
 
113
122
  def update_bunch_options(self, checked):
114
123
  self.bunch_frames.setEnabled(checked)
@@ -122,23 +131,22 @@ class NewProjectDialog(QDialog):
122
131
  def count_image_files(path):
123
132
  if path == '' or not os.path.isdir(path):
124
133
  return 0
125
- extensions = ['jpg', 'jpeg', 'tif', 'tiff']
126
134
  count = 0
127
135
  for filename in os.listdir(path):
128
136
  if '.' in filename:
129
137
  ext = filename.lower().split('.')[-1]
130
- if ext in extensions:
138
+ if ext in EXTENSIONS:
131
139
  count += 1
132
140
  return count
133
141
 
134
- n_image_files = count_image_files(self.input_folder.text())
135
- if n_image_files == 0:
142
+ self.n_image_files = count_image_files(self.input_folder.text())
143
+ if self.n_image_files == 0:
136
144
  self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
137
145
  self.frames_label.setText(DEFAULT_NO_COUNT_LABEL)
138
146
  return
139
- self.frames_label.setText(f"{n_image_files}")
147
+ self.frames_label.setText(f"{self.n_image_files}")
140
148
  if self.bunch_stack.isChecked():
141
- bunches = get_bunches(list(range(n_image_files)),
149
+ bunches = get_bunches(list(range(self.n_image_files)),
142
150
  self.bunch_frames.value(),
143
151
  self.bunch_overlap.value())
144
152
  self.bunches_label.setText(f"{len(bunches)}")
@@ -159,6 +167,53 @@ class NewProjectDialog(QDialog):
159
167
  if len(input_folder.split('/')) < 2:
160
168
  QMessageBox.warning(self, "Invalid Path", "The path must have a parent folder")
161
169
  return
170
+ if self.n_image_files > 0 and not self.bunch_stack.isChecked():
171
+ path = self.input_folder.text()
172
+ files = os.listdir(path)
173
+ file_path = None
174
+ for filename in files:
175
+ full_path = os.path.join(path, filename)
176
+ if os.path.isfile(full_path):
177
+ ext = full_path.split(".")[-1].lower()
178
+ if ext in EXTENSIONS:
179
+ file_path = full_path
180
+ break
181
+ if file_path is None:
182
+ QMessageBox.warning(
183
+ self, "Invalid input", "Could not find images now in the selected path")
184
+ return
185
+ img = read_img(file_path)
186
+ height, width = img.shape[:2]
187
+ n_bytes = 1 if img.dtype == np.uint8 else 2
188
+ n_bits = 8 if img.dtype == np.uint8 else 16
189
+ n_mbytes = float(n_bytes * height * width * self.n_image_files) / 10**6
190
+ if n_mbytes > 500 and not self.bunch_stack.isChecked():
191
+ msg = QMessageBox()
192
+ msg.setStyleSheet("""
193
+ QMessageBox {
194
+ min-width: 600px;
195
+ font-weight: bold;
196
+ font-size: 14px;
197
+ }
198
+ QMessageBox QLabel#qt_msgbox_informativelabel {
199
+ font-weight: normal;
200
+ font-size: 14px;
201
+ color: #555555;
202
+ }
203
+ """)
204
+ msg.setIcon(QMessageBox.Warning)
205
+ msg.setWindowTitle("Too many frames")
206
+ msg.setText(f"You selected {self.n_image_files} images "
207
+ f"with resolution {width}×{height} pixels, {n_bits} bits depth. "
208
+ "Processing may require a significant amount of memory.\n\n"
209
+ "Continue anyway?")
210
+ msg.setInformativeText("You may consider to split the processing "
211
+ " using a bunch stack to reduce memory usage.\n\n"
212
+ '✅ Check the option "Bunch stack".')
213
+ msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
214
+ msg.setDefaultButton(QMessageBox.Cancel)
215
+ if msg.exec_() != QMessageBox.Ok:
216
+ return
162
217
  super().accept()
163
218
 
164
219
  def get_input_folder(self):