shinestacker 0.3.3__py3-none-any.whl → 0.3.5__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 (71) hide show
  1. shinestacker/__init__.py +2 -1
  2. shinestacker/_version.py +1 -1
  3. shinestacker/algorithms/__init__.py +3 -2
  4. shinestacker/algorithms/align.py +102 -64
  5. shinestacker/algorithms/balance.py +89 -42
  6. shinestacker/algorithms/base_stack_algo.py +41 -0
  7. shinestacker/algorithms/core_utils.py +6 -6
  8. shinestacker/algorithms/denoise.py +4 -1
  9. shinestacker/algorithms/depth_map.py +28 -39
  10. shinestacker/algorithms/exif.py +43 -38
  11. shinestacker/algorithms/multilayer.py +48 -28
  12. shinestacker/algorithms/noise_detection.py +34 -26
  13. shinestacker/algorithms/pyramid.py +42 -42
  14. shinestacker/algorithms/sharpen.py +1 -0
  15. shinestacker/algorithms/stack.py +42 -42
  16. shinestacker/algorithms/stack_framework.py +118 -66
  17. shinestacker/algorithms/utils.py +12 -11
  18. shinestacker/algorithms/vignetting.py +52 -25
  19. shinestacker/algorithms/white_balance.py +1 -0
  20. shinestacker/app/about_dialog.py +6 -2
  21. shinestacker/app/app_config.py +1 -0
  22. shinestacker/app/gui_utils.py +20 -0
  23. shinestacker/app/help_menu.py +2 -1
  24. shinestacker/app/main.py +9 -18
  25. shinestacker/app/open_frames.py +5 -4
  26. shinestacker/app/project.py +5 -16
  27. shinestacker/app/retouch.py +5 -17
  28. shinestacker/core/colors.py +4 -4
  29. shinestacker/core/core_utils.py +1 -1
  30. shinestacker/core/exceptions.py +2 -1
  31. shinestacker/core/framework.py +46 -33
  32. shinestacker/core/logging.py +9 -10
  33. shinestacker/gui/action_config.py +253 -197
  34. shinestacker/gui/actions_window.py +36 -35
  35. shinestacker/gui/colors.py +1 -0
  36. shinestacker/gui/gui_images.py +7 -3
  37. shinestacker/gui/gui_logging.py +3 -2
  38. shinestacker/gui/gui_run.py +53 -38
  39. shinestacker/gui/main_window.py +69 -25
  40. shinestacker/gui/new_project.py +35 -2
  41. shinestacker/gui/project_converter.py +21 -20
  42. shinestacker/gui/project_editor.py +51 -52
  43. shinestacker/gui/project_model.py +15 -23
  44. shinestacker/retouch/{filter_base.py → base_filter.py} +7 -4
  45. shinestacker/retouch/brush.py +1 -0
  46. shinestacker/retouch/brush_gradient.py +17 -3
  47. shinestacker/retouch/brush_preview.py +14 -10
  48. shinestacker/retouch/brush_tool.py +28 -19
  49. shinestacker/retouch/denoise_filter.py +3 -2
  50. shinestacker/retouch/display_manager.py +11 -5
  51. shinestacker/retouch/exif_data.py +1 -0
  52. shinestacker/retouch/file_loader.py +13 -9
  53. shinestacker/retouch/filter_manager.py +1 -0
  54. shinestacker/retouch/image_editor.py +14 -48
  55. shinestacker/retouch/image_editor_ui.py +10 -5
  56. shinestacker/retouch/image_filters.py +4 -2
  57. shinestacker/retouch/image_viewer.py +33 -31
  58. shinestacker/retouch/io_gui_handler.py +25 -13
  59. shinestacker/retouch/io_manager.py +3 -2
  60. shinestacker/retouch/layer_collection.py +79 -23
  61. shinestacker/retouch/shortcuts_help.py +1 -0
  62. shinestacker/retouch/undo_manager.py +7 -0
  63. shinestacker/retouch/unsharp_mask_filter.py +3 -2
  64. shinestacker/retouch/white_balance_filter.py +11 -6
  65. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/METADATA +18 -6
  66. shinestacker-0.3.5.dist-info/RECORD +86 -0
  67. shinestacker-0.3.3.dist-info/RECORD +0 -85
  68. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/WHEEL +0 -0
  69. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/entry_points.txt +0 -0
  70. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/licenses/LICENSE +0 -0
  71. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902
1
2
  import os
2
3
  from PySide6.QtWidgets import (QWidget, QLineEdit, QFormLayout, QHBoxLayout, QPushButton,
3
4
  QDialog, QSizePolicy, QFileDialog, QLabel, QCheckBox,
@@ -6,6 +7,7 @@ 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.stack import get_bunches
9
11
 
10
12
 
11
13
  class NewProjectDialog(QDialog):
@@ -50,12 +52,14 @@ class NewProjectDialog(QDialog):
50
52
  self.layout.addRow(spacer)
51
53
  self.input_folder = QLineEdit()
52
54
  self.input_folder .setPlaceholderText('input files folder')
55
+ self.input_folder.textChanged.connect(self.update_bunches_label)
53
56
  button = QPushButton("Browse...")
54
57
 
55
58
  def browse():
56
59
  path = QFileDialog.getExistingDirectory(None, "Select input files folder")
57
60
  if path:
58
61
  self.input_folder.setText(path)
62
+
59
63
  button.clicked.connect(browse)
60
64
  button.setAutoDefault(False)
61
65
  layout = QHBoxLayout()
@@ -74,9 +78,9 @@ class NewProjectDialog(QDialog):
74
78
  self.align_frames.setChecked(gui_constants.NEW_PROJECT_ALIGN_FRAMES)
75
79
  self.balance_frames = QCheckBox()
76
80
  self.balance_frames.setChecked(gui_constants.NEW_PROJECT_BALANCE_FRAMES)
81
+
77
82
  self.bunch_stack = QCheckBox()
78
83
  self.bunch_stack.setChecked(gui_constants.NEW_PROJECT_BUNCH_STACK)
79
- self.bunch_stack.toggled.connect(self.update_bunch_options)
80
84
  self.bunch_frames = QSpinBox()
81
85
  bunch_frames_range = gui_constants.NEW_PROJECT_BUNCH_FRAMES
82
86
  self.bunch_frames.setRange(bunch_frames_range['min'], bunch_frames_range['max'])
@@ -85,7 +89,13 @@ class NewProjectDialog(QDialog):
85
89
  bunch_overlap_range = gui_constants.NEW_PROJECT_BUNCH_OVERLAP
86
90
  self.bunch_overlap.setRange(bunch_overlap_range['min'], bunch_overlap_range['max'])
87
91
  self.bunch_overlap.setValue(constants.DEFAULT_OVERLAP)
92
+ self.bunches_label = QLabel("")
93
+
88
94
  self.update_bunch_options(gui_constants.NEW_PROJECT_BUNCH_STACK)
95
+ self.bunch_stack.toggled.connect(self.update_bunch_options)
96
+ self.bunch_frames.valueChanged.connect(self.update_bunches_label)
97
+ self.bunch_overlap.valueChanged.connect(self.update_bunches_label)
98
+
89
99
  self.focus_stack_pyramid = QCheckBox()
90
100
  self.focus_stack_pyramid.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_PYRAMID)
91
101
  self.focus_stack_depth_map = QCheckBox()
@@ -103,7 +113,8 @@ class NewProjectDialog(QDialog):
103
113
  self.layout.addRow("Balance layers:", self.balance_frames)
104
114
  self.layout.addRow("Bunch stack:", self.bunch_stack)
105
115
  self.layout.addRow("Bunch frames:", self.bunch_frames)
106
- self.layout.addRow("Bunch frames:", self.bunch_overlap)
116
+ self.layout.addRow("Bunch overlap:", self.bunch_overlap)
117
+ self.layout.addRow("Number of bunches: ", self.bunches_label)
107
118
  if self.expert():
108
119
  self.layout.addRow("Focus stack (pyramid):", self.focus_stack_pyramid)
109
120
  self.layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
@@ -114,6 +125,28 @@ class NewProjectDialog(QDialog):
114
125
  def update_bunch_options(self, checked):
115
126
  self.bunch_frames.setEnabled(checked)
116
127
  self.bunch_overlap.setEnabled(checked)
128
+ self.update_bunches_label()
129
+
130
+ def update_bunches_label(self):
131
+ if self.bunch_stack.isChecked():
132
+ def count_image_files(path):
133
+ if path == '' or not os.path.isdir(path):
134
+ return 0
135
+ extensions = ['jpg', 'jpeg', 'tif', 'tiff']
136
+ count = 0
137
+ for filename in os.listdir(path):
138
+ if '.' in filename:
139
+ ext = filename.lower().split('.')[-1]
140
+ if ext in extensions:
141
+ count += 1
142
+ return count
143
+
144
+ bunches = get_bunches(list(range(count_image_files(self.input_folder.text()))),
145
+ self.bunch_frames.value(),
146
+ self.bunch_overlap.value())
147
+ self.bunches_label.setText(f"{len(bunches)}")
148
+ else:
149
+ self.bunches_label.setText(" - ")
117
150
 
118
151
  def accept(self):
119
152
  input_folder = self.input_folder.text()
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0115, C0116, R0912, R0911, E1101, W0718
1
2
  import logging
2
3
  import traceback
3
4
  from .. config.constants import constants
@@ -64,20 +65,19 @@ class ProjectConverter:
64
65
  for j in project.jobs:
65
66
  job = self.job(j, logger_name, callbacks)
66
67
  if job is None:
67
- raise Exception("Job instantiation failed.")
68
- else:
69
- jobs.append(job)
68
+ raise RuntimeError("Job instantiation failed.")
69
+ jobs.append(job)
70
70
  return jobs
71
71
 
72
- def filter_dict_keys(self, dict, prefix):
73
- dict_with = {k.replace(prefix, ''): v for (k, v) in dict.items() if k.startswith(prefix)}
74
- dict_without = {k: v for (k, v) in dict.items() if not k.startswith(prefix)}
72
+ def filter_dict_keys(self, k_dict, prefix):
73
+ dict_with = {k.replace(prefix, ''): v for (k, v) in k_dict.items() if k.startswith(prefix)}
74
+ dict_without = {k: v for (k, v) in k_dict.items() if not k.startswith(prefix)}
75
75
  return dict_with, dict_without
76
76
 
77
77
  def action(self, action_config):
78
78
  if action_config.type_name == constants.ACTION_NOISEDETECTION:
79
79
  return NoiseDetection(**action_config.params)
80
- elif action_config.type_name == constants.ACTION_COMBO:
80
+ if action_config.type_name == constants.ACTION_COMBO:
81
81
  sub_actions = []
82
82
  for sa in action_config.sub_actions:
83
83
  a = self.action(sa)
@@ -85,22 +85,23 @@ class ProjectConverter:
85
85
  sub_actions.append(a)
86
86
  a = CombinedActions(**action_config.params, actions=sub_actions)
87
87
  return a
88
- elif action_config.type_name == constants.ACTION_MASKNOISE:
88
+ if action_config.type_name == constants.ACTION_MASKNOISE:
89
89
  params = {k: v for k, v in action_config.params.items() if k != 'name'}
90
90
  return MaskNoise(**params)
91
- elif action_config.type_name == constants.ACTION_VIGNETTING:
91
+ if action_config.type_name == constants.ACTION_VIGNETTING:
92
92
  params = {k: v for k, v in action_config.params.items() if k != 'name'}
93
93
  return Vignetting(**params)
94
- elif action_config.type_name == constants.ACTION_ALIGNFRAMES:
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
96
  return AlignFrames(**params)
97
- elif action_config.type_name == constants.ACTION_BALANCEFRAMES:
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():
100
100
  i = params['intensity_interval']
101
101
  params['intensity_interval'] = {'min': i[0], 'max': i[1]}
102
102
  return BalanceFrames(**params)
103
- elif action_config.type_name == constants.ACTION_FOCUSSTACK or action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
103
+ if action_config.type_name in (constants.ACTION_FOCUSSTACK,
104
+ constants.ACTION_FOCUSSTACKBUNCH):
104
105
  stacker = action_config.params.get('stacker', constants.STACK_ALGO_DEFAULT)
105
106
  if stacker == constants.STACK_ALGO_PYRAMID:
106
107
  algo_dict, module_dict = self.filter_dict_keys(action_config.params, 'pyramid_')
@@ -115,17 +116,17 @@ class ProjectConverter:
115
116
  f"{constants.STACK_ALGO_DEPTH_MAP}")
116
117
  if action_config.type_name == constants.ACTION_FOCUSSTACK:
117
118
  return FocusStack(**module_dict, stack_algo=stack_algo)
118
- elif action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
119
+ if action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
119
120
  return FocusStackBunch(**module_dict, stack_algo=stack_algo)
120
- else:
121
- raise InvalidOptionError("stracker", stacker, details="valid values are: Pyramid, Depth map.")
122
- elif action_config.type_name == constants.ACTION_MULTILAYER:
123
- input_path = list(filter(lambda p: p != '', action_config.params.get('input_path', '').split(";")))
121
+ raise InvalidOptionError(
122
+ "stracker", stacker, details="valid values are: Pyramid, Depth map.")
123
+ if action_config.type_name == constants.ACTION_MULTILAYER:
124
+ input_path = list(filter(lambda p: p != '',
125
+ action_config.params.get('input_path', '').split(";")))
124
126
  params = {k: v for k, v in action_config.params.items() if k != 'imput_path'}
125
127
  params['input_path'] = [i.strip() for i in input_path]
126
128
  return MultiLayer(**params)
127
- else:
128
- raise Exception(f"Cannot convert action of type {action_config.type_name}.")
129
+ raise RuntimeError(f"Cannot convert action of type {action_config.type_name}.")
129
130
 
130
131
  def job(self, action_config: ActionConfig, logger_name=None, callbacks=None):
131
132
  try:
@@ -143,6 +144,6 @@ class ProjectConverter:
143
144
  except Exception as e:
144
145
  msg = str(e)
145
146
  logger = self.get_logger(logger_name)
146
- logger.error(f"=== can't instantiate job: {name}: {msg} ===")
147
+ logger.error(msg=f"=== can't instantiate job: {name}: {msg} ===")
147
148
  traceback.print_tb(e.__traceback__)
148
149
  raise e
@@ -1,6 +1,8 @@
1
+ # pylint: disable=C0114, C0115, C0116, R0904, R1702, R0917, R0913, R0902, E0611, E1131
1
2
  import os
2
3
  from dataclasses import dataclass
3
- from PySide6.QtWidgets import QMainWindow, QListWidget, QMessageBox, QDialog, QListWidgetItem, QLabel
4
+ from PySide6.QtWidgets import (QMainWindow, QListWidget, QMessageBox,
5
+ QDialog, QListWidgetItem, QLabel)
4
6
  from PySide6.QtCore import Qt
5
7
  from .. config.constants import constants
6
8
  from .colors import ColorPalette
@@ -28,7 +30,9 @@ class ActionPosition:
28
30
 
29
31
  @property
30
32
  def sub_action(self):
31
- return None if self.sub_actions is None or self.sub_action_index == -1 else self.sub_actions[self.sub_action_index]
33
+ return None if self.sub_actions is None or \
34
+ self.sub_action_index == -1 \
35
+ else self.sub_actions[self.sub_action_index]
32
36
 
33
37
 
34
38
  def new_row_after_delete(action_row, pos: ActionPosition):
@@ -41,6 +45,8 @@ def new_row_after_delete(action_row, pos: ActionPosition):
41
45
  new_row = action_row
42
46
  elif pos.action_index == len(pos.actions):
43
47
  new_row = action_row - len(pos.actions[pos.action_index - 1].sub_actions) - 1
48
+ else:
49
+ new_row = None
44
50
  return new_row
45
51
 
46
52
 
@@ -67,20 +73,25 @@ def new_row_after_paste(action_row, pos: ActionPosition):
67
73
 
68
74
  def new_row_after_clone(job, action_row, is_sub_action, cloned):
69
75
  return action_row + 1 if is_sub_action else \
70
- sum(1 + len(action.sub_actions) for action in job.sub_actions[:job.sub_actions.index(cloned)])
76
+ sum(1 + len(action.sub_actions)
77
+ for action in job.sub_actions[:job.sub_actions.index(cloned)])
71
78
 
72
79
 
73
80
  class ProjectEditor(QMainWindow):
74
81
  def __init__(self):
75
82
  super().__init__()
76
83
  self._copy_buffer = None
77
- self._project_buffer = []
84
+ self.project_buffer = []
78
85
  self.job_list = QListWidget()
79
86
  self.action_list = QListWidget()
80
87
  self.project = None
81
88
  self.job_list_model = None
82
89
  self.expert_options = False
83
90
  self.script_dir = os.path.dirname(__file__)
91
+ self.dialog = None
92
+ self._current_file = None
93
+ self._current_file_wd = ''
94
+ self._modified_project = False
84
95
 
85
96
  def set_project(self, project):
86
97
  self.project = project
@@ -105,7 +116,7 @@ class ProjectEditor(QMainWindow):
105
116
  constants.ACTION_BALANCEFRAMES: '🌈'
106
117
  }
107
118
  ico = icon_map.get(action.type_name, '')
108
- if is_sub_action:
119
+ if is_sub_action and indent:
109
120
  txt = INDENT_SPACE
110
121
  if ico == '':
111
122
  ico = '🟣'
@@ -118,7 +129,9 @@ class ProjectEditor(QMainWindow):
118
129
  if html:
119
130
  txt = f"<b>{txt}</b>"
120
131
  in_path, out_path = get_action_input_path(action), get_action_output_path(action)
121
- return f"{txt} [{ico} {action.type_name}" + (f": 📁 <i>{in_path[0]}</i> → 📂 <i>{out_path[0]}</i>]" if long_name and not is_sub_action else "]")
132
+ return f"{txt} [{ico} {action.type_name}" + \
133
+ (f": 📁 <i>{in_path[0]}</i> → 📂 <i>{out_path[0]}</i>]"
134
+ if long_name and not is_sub_action else "]")
122
135
 
123
136
  def get_job_at(self, index):
124
137
  return None if index < 0 else self.project.jobs[index]
@@ -138,9 +151,11 @@ class ProjectEditor(QMainWindow):
138
151
  return (job_row, action_row, None)
139
152
  job = self.project.jobs[job_row]
140
153
  if sub_action:
141
- return (job_row, action_row, ActionPosition(job.sub_actions, action.sub_actions, job.sub_actions.index(action), sub_action_index))
142
- else:
143
- return (job_row, action_row, ActionPosition(job.sub_actions, None, job.sub_actions.index(action)))
154
+ return (job_row, action_row,
155
+ ActionPosition(job.sub_actions, action.sub_actions,
156
+ job.sub_actions.index(action), sub_action_index))
157
+ return (job_row, action_row,
158
+ ActionPosition(job.sub_actions, None, job.sub_actions.index(action)))
144
159
 
145
160
  def find_action_position(self, job_index, ui_index):
146
161
  if not 0 <= job_index < len(self.project.jobs):
@@ -237,9 +252,12 @@ class ProjectEditor(QMainWindow):
237
252
  if confirm:
238
253
  reply = QMessageBox.question(
239
254
  self, "Confirm Delete",
240
- f"Are you sure you want to delete job '{self.project.jobs[current_index].params.get('name', '')}'?",
255
+ "Are you sure you want to delete job "
256
+ f"'{self.project.jobs[current_index].params.get('name', '')}'?",
241
257
  QMessageBox.Yes | QMessageBox.No
242
258
  )
259
+ else:
260
+ reply = None
243
261
  if not confirm or reply == QMessageBox.Yes:
244
262
  self.job_list.takeItem(current_index)
245
263
  self.mark_as_modified()
@@ -257,9 +275,12 @@ class ProjectEditor(QMainWindow):
257
275
  reply = QMessageBox.question(
258
276
  self,
259
277
  "Confirm Delete",
260
- f"Are you sure you want to delete action '{self.action_text(current_action, pos.is_sub_action, indent=False)}'?",
278
+ "Are you sure you want to delete action "
279
+ f"'{self.action_text(current_action, pos.is_sub_action, indent=False)}'?",
261
280
  QMessageBox.Yes | QMessageBox.No
262
281
  )
282
+ else:
283
+ reply = None
263
284
  if not confirm or reply == QMessageBox.Yes:
264
285
  self.mark_as_modified()
265
286
  if pos.is_sub_action:
@@ -282,10 +303,13 @@ class ProjectEditor(QMainWindow):
282
303
  self.delete_element_action.setEnabled(True)
283
304
  return element
284
305
 
306
+ def action_config_dialog(self, action):
307
+ return ActionConfigDialog(action, self._current_file_wd, self)
308
+
285
309
  def add_job(self):
286
310
  job_action = ActionConfig("Job")
287
- dialog = ActionConfigDialog(job_action, self)
288
- if dialog.exec() == QDialog.Accepted:
311
+ self.dialog = self.action_config_dialog(job_action)
312
+ if self.dialog.exec() == QDialog.Accepted:
289
313
  self.mark_as_modified()
290
314
  self.project.jobs.append(job_action)
291
315
  self.add_list_item(self.job_list, job_action, False)
@@ -305,8 +329,8 @@ class ProjectEditor(QMainWindow):
305
329
  type_name = self.action_selector.currentText()
306
330
  action = ActionConfig(type_name)
307
331
  action.parent = self.get_current_job()
308
- dialog = ActionConfigDialog(action, self)
309
- if dialog.exec() == QDialog.Accepted:
332
+ self.dialog = self.action_config_dialog(action)
333
+ if self.dialog.exec() == QDialog.Accepted:
310
334
  self.mark_as_modified()
311
335
  self.project.jobs[current_index].add_sub_action(action)
312
336
  self.add_list_item(self.action_list, action, False)
@@ -327,30 +351,16 @@ class ProjectEditor(QMainWindow):
327
351
  label = QLabel(html_text)
328
352
  widget_list.setItemWidget(item, label)
329
353
 
330
- def add_action_CombinedActions(self):
331
- self.add_action(constants.ACTION_COMBO)
332
-
333
- def add_action_NoiseDetection(self):
334
- self.add_action(constants.ACTION_NOISEDETECTION)
335
-
336
- def add_action_FocusStack(self):
337
- self.add_action(constants.ACTION_FOCUSSTACK)
338
-
339
- def add_action_FocusStackBunch(self):
340
- self.add_action(constants.ACTION_FOCUSSTACKBUNCH)
341
-
342
- def add_action_MultiLayer(self):
343
- self.add_action(constants.ACTION_MULTILAYER)
344
-
345
354
  def add_sub_action(self, type_name=False):
346
355
  current_job_index = self.job_list.currentRow()
347
356
  current_action_index = self.action_list.currentRow()
348
- if (current_job_index < 0 or current_action_index < 0 or current_job_index >= len(self.project.jobs)):
357
+ if current_job_index < 0 or current_action_index < 0 or \
358
+ current_job_index >= len(self.project.jobs):
349
359
  return
350
360
  job = self.project.jobs[current_job_index]
351
361
  action = None
352
362
  action_counter = -1
353
- for i, act in enumerate(job.sub_actions):
363
+ for act in job.sub_actions:
354
364
  action_counter += 1
355
365
  if action_counter == current_action_index:
356
366
  action = act
@@ -361,32 +371,20 @@ class ProjectEditor(QMainWindow):
361
371
  if type_name is False:
362
372
  type_name = self.sub_action_selector.currentText()
363
373
  sub_action = ActionConfig(type_name)
364
- dialog = ActionConfigDialog(sub_action, self)
365
- if dialog.exec() == QDialog.Accepted:
374
+ self.dialog = self.action_config_dialog(sub_action)
375
+ if self.dialog.exec() == QDialog.Accepted:
366
376
  self.mark_as_modified()
367
377
  action.add_sub_action(sub_action)
368
378
  self.on_job_selected(current_job_index)
369
379
  self.action_list.setCurrentRow(current_action_index)
370
380
 
371
- def add_sub_action_MakeNoise(self):
372
- self.add_sub_action(constants.ACTION_MASKNOISE)
373
-
374
- def add_sub_action_Vignetting(self):
375
- self.add_sub_action(constants.ACTION_VIGNETTING)
376
-
377
- def add_sub_action_AlignFrames(self):
378
- self.add_sub_action(constants.ACTION_ALIGNFRAMES)
379
-
380
- def add_sub_action_BalanceFrames(self):
381
- self.add_sub_action(constants.ACTION_BALANCEFRAMES)
382
-
383
381
  def copy_job(self):
384
382
  current_index = self.job_list.currentRow()
385
383
  if 0 <= current_index < len(self.project.jobs):
386
384
  self._copy_buffer = self.project.jobs[current_index].clone()
387
385
 
388
386
  def copy_action(self):
389
- job_row, action_row, pos = self.get_current_action()
387
+ _job_row, _action_row, pos = self.get_current_action()
390
388
  if pos.actions is not None:
391
389
  self._copy_buffer = pos.sub_action.clone() if pos.is_sub_action else pos.action.clone()
392
390
 
@@ -439,8 +437,8 @@ class ProjectEditor(QMainWindow):
439
437
  def undo(self):
440
438
  job_row = self.job_list.currentRow()
441
439
  action_row = self.action_list.currentRow()
442
- if len(self._project_buffer) > 0:
443
- self.set_project(self._project_buffer.pop())
440
+ if len(self.project_buffer) > 0:
441
+ self.set_project(self.project_buffer.pop())
444
442
  self.refresh_ui()
445
443
  len_jobs = len(self.project.jobs)
446
444
  if len_jobs > 0:
@@ -449,8 +447,7 @@ class ProjectEditor(QMainWindow):
449
447
  self.job_list.setCurrentRow(job_row)
450
448
  len_actions = self.action_list.count()
451
449
  if len_actions > 0:
452
- if action_row >= len_actions:
453
- action_row = len_actions
450
+ action_row = min(action_row, len_actions)
454
451
  self.action_list.setCurrentRow(action_row)
455
452
 
456
453
  def set_enabled(self, enabled):
@@ -459,10 +456,12 @@ class ProjectEditor(QMainWindow):
459
456
  job_row = self.job_list.currentRow()
460
457
  if 0 <= job_row < len(self.project.jobs):
461
458
  current_action = self.project.jobs[job_row]
462
- action_row = -1
459
+ action_row = -1
463
460
  elif self.action_list.hasFocus():
464
461
  job_row, action_row, pos = self.get_current_action()
465
462
  current_action = pos.sub_action if pos.is_sub_action else pos.action
463
+ else:
464
+ action_row = -1
466
465
  if current_action:
467
466
  if current_action.enabled() != enabled:
468
467
  self.mark_as_modified()
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0115, C0116, R0911
1
2
  from copy import deepcopy
2
3
  from .. config.constants import constants
3
4
 
@@ -28,7 +29,7 @@ class ActionConfig:
28
29
  if index < len(self.sub_actions):
29
30
  self.sub_actions.pop(index)
30
31
  else:
31
- raise Exception(f"can't pop sub-action {index}, lenght is {len(self.sub_actions)}")
32
+ raise RuntimeError(f"can't pop sub-action {index}, lenght is {len(self.sub_actions)}")
32
33
 
33
34
  def clone(self, name_postfix=''):
34
35
  c = ActionConfig(self.type_name, deepcopy(self.params))
@@ -40,13 +41,10 @@ class ActionConfig:
40
41
  return c
41
42
 
42
43
  def to_dict(self):
43
- dict = {
44
- 'type_name': self.type_name,
45
- 'params': self.params,
46
- }
44
+ project_dict = {'type_name': self.type_name, 'params': self.params}
47
45
  if len(self.sub_actions) > 0:
48
- dict['sub_actions'] = [a.to_dict() for a in self.sub_actions]
49
- return dict
46
+ project_dict['sub_actions'] = [a.to_dict() for a in self.sub_actions]
47
+ return project_dict
50
48
 
51
49
  @classmethod
52
50
  def from_dict(cls, data):
@@ -93,8 +91,7 @@ def get_action_working_path(action, get_name=False):
93
91
  wp = action.params.get('working_path', '')
94
92
  if wp != '':
95
93
  return wp, (f" {action.params.get('name', '')} [{action.type_name}]" if get_name else '')
96
- else:
97
- return get_action_working_path(action.parent, True)
94
+ return get_action_working_path(action.parent, True)
98
95
 
99
96
 
100
97
  def get_action_output_path(action, get_name=False):
@@ -122,17 +119,12 @@ def get_action_input_path(action, get_name=False):
122
119
  action = action.sub_actions[0]
123
120
  path = action.params.get('input_path', '')
124
121
  return path, f" {action.params.get('name', '')} [{action.type_name}]"
125
- else:
126
- return '', ''
127
- else:
128
- actions = action.parent.sub_actions
129
- if action in actions:
130
- i = actions.index(action)
131
- if i == 0:
132
- return get_action_input_path(action.parent, True)
133
- else:
134
- return get_action_output_path(actions[i - 1], True)
135
- else:
136
- return '', ''
137
- else:
138
- return path, (f" {action.params.get('name', '')} [{action.type_name}]" if get_name else '')
122
+ return '', ''
123
+ actions = action.parent.sub_actions
124
+ if action in actions:
125
+ i = actions.index(action)
126
+ if i == 0:
127
+ return get_action_input_path(action.parent, True)
128
+ return get_action_output_path(actions[i - 1], True)
129
+ return '', ''
130
+ return path, (f" {action.params.get('name', '')} [{action.type_name}]" if get_name else '')
@@ -1,5 +1,7 @@
1
- import numpy as np
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0718, R0915, R0903
2
+ import traceback
2
3
  from abc import ABC, abstractmethod
4
+ import numpy as np
3
5
  from PySide6.QtWidgets import QDialog, QVBoxLayout
4
6
  from PySide6.QtCore import Signal, QThread, QTimer
5
7
 
@@ -62,7 +64,7 @@ class BaseFilter(ABC):
62
64
  active_worker.start()
63
65
 
64
66
  def restore_original():
65
- self.editor.layer_collection.master_layer = self.editor.layer_collection.master_layer_copy.copy()
67
+ self.editor.restore_master_layer()
66
68
  self.editor.display_manager.display_master_layer()
67
69
  try:
68
70
  dlg.activateWindow()
@@ -109,6 +111,7 @@ class BaseFilter(ABC):
109
111
  def run(self):
110
112
  try:
111
113
  result = self.func(*self.args, **self.kwargs)
112
- except Exception:
113
- raise
114
+ except Exception as e:
115
+ traceback.print_tb(e.__traceback__)
116
+ raise RuntimeError("Filter preview failed") from e
114
117
  self.finished.emit(result, self.request_id)
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0115, R0903
1
2
  from .. config.gui_constants import gui_constants
2
3
 
3
4
 
@@ -1,12 +1,16 @@
1
+ # pylint: disable=C0114, C0116, R0913, R0917, E0611
1
2
  from PySide6.QtGui import QRadialGradient
2
3
  from PySide6.QtGui import QColor
3
4
  from .. config.gui_constants import gui_constants
4
5
 
5
6
 
6
- def create_brush_gradient(center_x, center_y, radius, hardness, inner_color=None, outer_color=None, opacity=100):
7
+ def create_brush_gradient(center_x, center_y, radius, hardness,
8
+ inner_color=None, outer_color=None, opacity=100):
7
9
  gradient = QRadialGradient(center_x, center_y, float(radius))
8
- inner = inner_color if inner_color is not None else QColor(*gui_constants.BRUSH_COLORS['inner'])
9
- outer = outer_color if outer_color is not None else QColor(*gui_constants.BRUSH_COLORS['gradient_end'])
10
+ inner = inner_color if inner_color is not None else \
11
+ QColor(*gui_constants.BRUSH_COLORS['inner'])
12
+ outer = outer_color if outer_color is not None else \
13
+ QColor(*gui_constants.BRUSH_COLORS['gradient_end'])
10
14
  inner_with_opacity = QColor(inner)
11
15
  inner_with_opacity.setAlpha(int(float(inner.alpha()) * float(opacity) / 100.0))
12
16
  if hardness < 100:
@@ -18,3 +22,13 @@ def create_brush_gradient(center_x, center_y, radius, hardness, inner_color=None
18
22
  gradient.setColorAt(0.0, inner_with_opacity)
19
23
  gradient.setColorAt(1.0, inner_with_opacity)
20
24
  return gradient
25
+
26
+
27
+ def create_default_brush_gradient(center_x, center_y, radius, brush):
28
+ return create_brush_gradient(
29
+ center_x, center_y, radius,
30
+ brush.hardness,
31
+ inner_color=QColor(*gui_constants.BRUSH_COLORS['inner']),
32
+ outer_color=QColor(*gui_constants.BRUSH_COLORS['gradient_end']),
33
+ opacity=brush.opacity
34
+ )
@@ -1,7 +1,10 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0914, W0718
2
+ import traceback
1
3
  import numpy as np
2
4
  from PySide6.QtWidgets import QGraphicsPixmapItem
3
5
  from PySide6.QtCore import Qt
4
6
  from PySide6.QtGui import QPixmap, QPainter, QImage
7
+ from .layer_collection import LayerCollectionHandler
5
8
 
6
9
 
7
10
  def brush_profile_lower_limited(r, hardness):
@@ -23,7 +26,9 @@ def brush_profile(r, hardness):
23
26
  result = 0.5 * (np.cos(np.pi * np.power(np.where(r < 1.0, r, 1.0), k)) + 1.0)
24
27
  elif h < 0:
25
28
  k = 1.0 / (1.0 + hardness)
26
- result = np.where(r < 1.0, 0.5 * (1.0 - np.cos(np.pi * np.power(1.0 - np.where(r < 1.0, r, 1.0), k))), 0.0)
29
+ result = np.where(
30
+ r < 1.0,
31
+ 0.5 * (1.0 - np.cos(np.pi * np.power(1.0 - np.where(r < 1.0, r, 1.0), k))), 0.0)
27
32
  else:
28
33
  result = np.zeros_like(r)
29
34
  return result
@@ -39,10 +44,10 @@ def create_brush_mask(size, hardness_percent, opacity_percent):
39
44
  return mask
40
45
 
41
46
 
42
- class BrushPreviewItem(QGraphicsPixmapItem):
43
- def __init__(self):
44
- super().__init__()
45
- self.layer_collection = None
47
+ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
48
+ def __init__(self, layer_collection):
49
+ QGraphicsPixmapItem.__init__(self)
50
+ LayerCollectionHandler.__init__(self, layer_collection)
46
51
  self.setVisible(False)
47
52
  self.setZValue(500)
48
53
  self.setTransformationMode(Qt.SmoothTransformation)
@@ -64,10 +69,9 @@ class BrushPreviewItem(QGraphicsPixmapItem):
64
69
  area = np.ascontiguousarray(area[..., :3]) # RGB
65
70
  if area.dtype == np.uint8:
66
71
  return area.astype(np.float32) / 256.0
67
- elif area.dtype == np.uint16:
72
+ if area.dtype == np.uint16:
68
73
  return area.astype(np.float32) / 65536.0
69
- else:
70
- raise Exception("Bitmas is neither 8 bit nor 16, but of type " + area.dtype)
74
+ raise RuntimeError("Bitmas is neither 8 bit nor 16, but of type " + area.dtype)
71
75
 
72
76
  def update(self, scene_pos, size):
73
77
  try:
@@ -96,7 +100,8 @@ class BrushPreviewItem(QGraphicsPixmapItem):
96
100
  mask_area = full_mask[mask_y_start:mask_y_end, mask_x_start:mask_x_end]
97
101
  area = (layer_area * mask_area + master_area * (1 - mask_area)) * 255.0
98
102
  area = area.astype(np.uint8)
99
- qimage = QImage(area.data, area.shape[1], area.shape[0], area.strides[0], QImage.Format_RGB888)
103
+ qimage = QImage(area.data, area.shape[1], area.shape[0],
104
+ area.strides[0], QImage.Format_RGB888)
100
105
  mask = QPixmap(w, h)
101
106
  mask.fill(Qt.transparent)
102
107
  painter = QPainter(mask)
@@ -117,6 +122,5 @@ class BrushPreviewItem(QGraphicsPixmapItem):
117
122
  self.setPos(x_start, y_start)
118
123
  self.show()
119
124
  except Exception:
120
- import traceback
121
125
  traceback.print_exc()
122
126
  self.hide()