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.
- shinestacker/__init__.py +2 -1
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/__init__.py +3 -2
- shinestacker/algorithms/align.py +102 -64
- shinestacker/algorithms/balance.py +89 -42
- shinestacker/algorithms/base_stack_algo.py +41 -0
- shinestacker/algorithms/core_utils.py +6 -6
- shinestacker/algorithms/denoise.py +4 -1
- shinestacker/algorithms/depth_map.py +28 -39
- shinestacker/algorithms/exif.py +43 -38
- shinestacker/algorithms/multilayer.py +48 -28
- shinestacker/algorithms/noise_detection.py +34 -26
- shinestacker/algorithms/pyramid.py +42 -42
- shinestacker/algorithms/sharpen.py +1 -0
- shinestacker/algorithms/stack.py +42 -42
- shinestacker/algorithms/stack_framework.py +118 -66
- shinestacker/algorithms/utils.py +12 -11
- shinestacker/algorithms/vignetting.py +52 -25
- shinestacker/algorithms/white_balance.py +1 -0
- shinestacker/app/about_dialog.py +6 -2
- shinestacker/app/app_config.py +1 -0
- shinestacker/app/gui_utils.py +20 -0
- shinestacker/app/help_menu.py +2 -1
- shinestacker/app/main.py +9 -18
- shinestacker/app/open_frames.py +5 -4
- shinestacker/app/project.py +5 -16
- shinestacker/app/retouch.py +5 -17
- shinestacker/core/colors.py +4 -4
- shinestacker/core/core_utils.py +1 -1
- shinestacker/core/exceptions.py +2 -1
- shinestacker/core/framework.py +46 -33
- shinestacker/core/logging.py +9 -10
- shinestacker/gui/action_config.py +253 -197
- shinestacker/gui/actions_window.py +36 -35
- shinestacker/gui/colors.py +1 -0
- shinestacker/gui/gui_images.py +7 -3
- shinestacker/gui/gui_logging.py +3 -2
- shinestacker/gui/gui_run.py +53 -38
- shinestacker/gui/main_window.py +69 -25
- shinestacker/gui/new_project.py +35 -2
- shinestacker/gui/project_converter.py +21 -20
- shinestacker/gui/project_editor.py +51 -52
- shinestacker/gui/project_model.py +15 -23
- shinestacker/retouch/{filter_base.py → base_filter.py} +7 -4
- shinestacker/retouch/brush.py +1 -0
- shinestacker/retouch/brush_gradient.py +17 -3
- shinestacker/retouch/brush_preview.py +14 -10
- shinestacker/retouch/brush_tool.py +28 -19
- shinestacker/retouch/denoise_filter.py +3 -2
- shinestacker/retouch/display_manager.py +11 -5
- shinestacker/retouch/exif_data.py +1 -0
- shinestacker/retouch/file_loader.py +13 -9
- shinestacker/retouch/filter_manager.py +1 -0
- shinestacker/retouch/image_editor.py +14 -48
- shinestacker/retouch/image_editor_ui.py +10 -5
- shinestacker/retouch/image_filters.py +4 -2
- shinestacker/retouch/image_viewer.py +33 -31
- shinestacker/retouch/io_gui_handler.py +25 -13
- shinestacker/retouch/io_manager.py +3 -2
- shinestacker/retouch/layer_collection.py +79 -23
- shinestacker/retouch/shortcuts_help.py +1 -0
- shinestacker/retouch/undo_manager.py +7 -0
- shinestacker/retouch/unsharp_mask_filter.py +3 -2
- shinestacker/retouch/white_balance_filter.py +11 -6
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/METADATA +18 -6
- shinestacker-0.3.5.dist-info/RECORD +86 -0
- shinestacker-0.3.3.dist-info/RECORD +0 -85
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/top_level.txt +0 -0
shinestacker/gui/new_project.py
CHANGED
|
@@ -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
|
|
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
|
|
68
|
-
|
|
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,
|
|
73
|
-
dict_with = {k.replace(prefix, ''): v for (k, v) in
|
|
74
|
-
dict_without = {k: v for (k, v) in
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
+
if action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
|
|
119
120
|
return FocusStackBunch(**module_dict, stack_algo=stack_algo)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
input_path = list(filter(lambda p: p != '',
|
|
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
|
-
|
|
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,
|
|
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
|
|
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)
|
|
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.
|
|
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}" +
|
|
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,
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
443
|
-
self.set_project(self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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)
|
shinestacker/retouch/brush.py
CHANGED
|
@@ -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,
|
|
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
|
|
9
|
-
|
|
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(
|
|
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
|
-
|
|
45
|
-
self
|
|
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
|
-
|
|
72
|
+
if area.dtype == np.uint16:
|
|
68
73
|
return area.astype(np.float32) / 65536.0
|
|
69
|
-
|
|
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],
|
|
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()
|