shinestacker 0.3.2__py3-none-any.whl → 0.3.4__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 +42 -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 -23
  13. shinestacker/algorithms/pyramid.py +42 -42
  14. shinestacker/algorithms/sharpen.py +1 -0
  15. shinestacker/algorithms/stack.py +42 -41
  16. shinestacker/algorithms/stack_framework.py +111 -65
  17. shinestacker/algorithms/utils.py +12 -11
  18. shinestacker/algorithms/vignetting.py +48 -22
  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 +1 -0
  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 +32 -28
  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 +45 -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.2.dist-info → shinestacker-0.3.4.dist-info}/METADATA +10 -4
  66. shinestacker-0.3.4.dist-info/RECORD +86 -0
  67. shinestacker-0.3.2.dist-info/RECORD +0 -85
  68. {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/WHEEL +0 -0
  69. {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/entry_points.txt +0 -0
  70. {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/licenses/LICENSE +0 -0
  71. {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.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,22 @@ 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
84
92
 
85
93
  def set_project(self, project):
86
94
  self.project = project
@@ -105,7 +113,7 @@ class ProjectEditor(QMainWindow):
105
113
  constants.ACTION_BALANCEFRAMES: '🌈'
106
114
  }
107
115
  ico = icon_map.get(action.type_name, '')
108
- if is_sub_action:
116
+ if is_sub_action and indent:
109
117
  txt = INDENT_SPACE
110
118
  if ico == '':
111
119
  ico = '🟣'
@@ -118,7 +126,9 @@ class ProjectEditor(QMainWindow):
118
126
  if html:
119
127
  txt = f"<b>{txt}</b>"
120
128
  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 "]")
129
+ return f"{txt} [{ico} {action.type_name}" + \
130
+ (f": 📁 <i>{in_path[0]}</i> → 📂 <i>{out_path[0]}</i>]"
131
+ if long_name and not is_sub_action else "]")
122
132
 
123
133
  def get_job_at(self, index):
124
134
  return None if index < 0 else self.project.jobs[index]
@@ -138,9 +148,11 @@ class ProjectEditor(QMainWindow):
138
148
  return (job_row, action_row, None)
139
149
  job = self.project.jobs[job_row]
140
150
  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)))
151
+ return (job_row, action_row,
152
+ ActionPosition(job.sub_actions, action.sub_actions,
153
+ job.sub_actions.index(action), sub_action_index))
154
+ return (job_row, action_row,
155
+ ActionPosition(job.sub_actions, None, job.sub_actions.index(action)))
144
156
 
145
157
  def find_action_position(self, job_index, ui_index):
146
158
  if not 0 <= job_index < len(self.project.jobs):
@@ -237,9 +249,12 @@ class ProjectEditor(QMainWindow):
237
249
  if confirm:
238
250
  reply = QMessageBox.question(
239
251
  self, "Confirm Delete",
240
- f"Are you sure you want to delete job '{self.project.jobs[current_index].params.get('name', '')}'?",
252
+ "Are you sure you want to delete job "
253
+ f"'{self.project.jobs[current_index].params.get('name', '')}'?",
241
254
  QMessageBox.Yes | QMessageBox.No
242
255
  )
256
+ else:
257
+ reply = None
243
258
  if not confirm or reply == QMessageBox.Yes:
244
259
  self.job_list.takeItem(current_index)
245
260
  self.mark_as_modified()
@@ -257,9 +272,12 @@ class ProjectEditor(QMainWindow):
257
272
  reply = QMessageBox.question(
258
273
  self,
259
274
  "Confirm Delete",
260
- f"Are you sure you want to delete action '{self.action_text(current_action, pos.is_sub_action, indent=False)}'?",
275
+ "Are you sure you want to delete action "
276
+ f"'{self.action_text(current_action, pos.is_sub_action, indent=False)}'?",
261
277
  QMessageBox.Yes | QMessageBox.No
262
278
  )
279
+ else:
280
+ reply = None
263
281
  if not confirm or reply == QMessageBox.Yes:
264
282
  self.mark_as_modified()
265
283
  if pos.is_sub_action:
@@ -284,8 +302,8 @@ class ProjectEditor(QMainWindow):
284
302
 
285
303
  def add_job(self):
286
304
  job_action = ActionConfig("Job")
287
- dialog = ActionConfigDialog(job_action, self)
288
- if dialog.exec() == QDialog.Accepted:
305
+ self.dialog = ActionConfigDialog(job_action, self)
306
+ if self.dialog.exec() == QDialog.Accepted:
289
307
  self.mark_as_modified()
290
308
  self.project.jobs.append(job_action)
291
309
  self.add_list_item(self.job_list, job_action, False)
@@ -305,8 +323,8 @@ class ProjectEditor(QMainWindow):
305
323
  type_name = self.action_selector.currentText()
306
324
  action = ActionConfig(type_name)
307
325
  action.parent = self.get_current_job()
308
- dialog = ActionConfigDialog(action, self)
309
- if dialog.exec() == QDialog.Accepted:
326
+ self.dialog = ActionConfigDialog(action, self)
327
+ if self.dialog.exec() == QDialog.Accepted:
310
328
  self.mark_as_modified()
311
329
  self.project.jobs[current_index].add_sub_action(action)
312
330
  self.add_list_item(self.action_list, action, False)
@@ -327,30 +345,16 @@ class ProjectEditor(QMainWindow):
327
345
  label = QLabel(html_text)
328
346
  widget_list.setItemWidget(item, label)
329
347
 
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
348
  def add_sub_action(self, type_name=False):
346
349
  current_job_index = self.job_list.currentRow()
347
350
  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)):
351
+ if current_job_index < 0 or current_action_index < 0 or \
352
+ current_job_index >= len(self.project.jobs):
349
353
  return
350
354
  job = self.project.jobs[current_job_index]
351
355
  action = None
352
356
  action_counter = -1
353
- for i, act in enumerate(job.sub_actions):
357
+ for act in job.sub_actions:
354
358
  action_counter += 1
355
359
  if action_counter == current_action_index:
356
360
  action = act
@@ -361,32 +365,20 @@ class ProjectEditor(QMainWindow):
361
365
  if type_name is False:
362
366
  type_name = self.sub_action_selector.currentText()
363
367
  sub_action = ActionConfig(type_name)
364
- dialog = ActionConfigDialog(sub_action, self)
365
- if dialog.exec() == QDialog.Accepted:
368
+ self.dialog = ActionConfigDialog(sub_action, self)
369
+ if self.dialog.exec() == QDialog.Accepted:
366
370
  self.mark_as_modified()
367
371
  action.add_sub_action(sub_action)
368
372
  self.on_job_selected(current_job_index)
369
373
  self.action_list.setCurrentRow(current_action_index)
370
374
 
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
375
  def copy_job(self):
384
376
  current_index = self.job_list.currentRow()
385
377
  if 0 <= current_index < len(self.project.jobs):
386
378
  self._copy_buffer = self.project.jobs[current_index].clone()
387
379
 
388
380
  def copy_action(self):
389
- job_row, action_row, pos = self.get_current_action()
381
+ _job_row, _action_row, pos = self.get_current_action()
390
382
  if pos.actions is not None:
391
383
  self._copy_buffer = pos.sub_action.clone() if pos.is_sub_action else pos.action.clone()
392
384
 
@@ -439,8 +431,8 @@ class ProjectEditor(QMainWindow):
439
431
  def undo(self):
440
432
  job_row = self.job_list.currentRow()
441
433
  action_row = self.action_list.currentRow()
442
- if len(self._project_buffer) > 0:
443
- self.set_project(self._project_buffer.pop())
434
+ if len(self.project_buffer) > 0:
435
+ self.set_project(self.project_buffer.pop())
444
436
  self.refresh_ui()
445
437
  len_jobs = len(self.project.jobs)
446
438
  if len_jobs > 0:
@@ -449,8 +441,7 @@ class ProjectEditor(QMainWindow):
449
441
  self.job_list.setCurrentRow(job_row)
450
442
  len_actions = self.action_list.count()
451
443
  if len_actions > 0:
452
- if action_row >= len_actions:
453
- action_row = len_actions
444
+ action_row = min(action_row, len_actions)
454
445
  self.action_list.setCurrentRow(action_row)
455
446
 
456
447
  def set_enabled(self, enabled):
@@ -459,10 +450,12 @@ class ProjectEditor(QMainWindow):
459
450
  job_row = self.job_list.currentRow()
460
451
  if 0 <= job_row < len(self.project.jobs):
461
452
  current_action = self.project.jobs[job_row]
462
- action_row = -1
453
+ action_row = -1
463
454
  elif self.action_list.hasFocus():
464
455
  job_row, action_row, pos = self.get_current_action()
465
456
  current_action = pos.sub_action if pos.is_sub_action else pos.action
457
+ else:
458
+ action_row = -1
466
459
  if current_action:
467
460
  if current_action.enabled() != enabled:
468
461
  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()