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.
- 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 +42 -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 -23
- shinestacker/algorithms/pyramid.py +42 -42
- shinestacker/algorithms/sharpen.py +1 -0
- shinestacker/algorithms/stack.py +42 -41
- shinestacker/algorithms/stack_framework.py +111 -65
- shinestacker/algorithms/utils.py +12 -11
- shinestacker/algorithms/vignetting.py +48 -22
- 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 +1 -0
- 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 +32 -28
- 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 +45 -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.2.dist-info → shinestacker-0.3.4.dist-info}/METADATA +10 -4
- shinestacker-0.3.4.dist-info/RECORD +86 -0
- shinestacker-0.3.2.dist-info/RECORD +0 -85
- {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.2.dist-info → shinestacker-0.3.4.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,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)
|
|
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
|
|
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}" +
|
|
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,
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
443
|
-
self.set_project(self.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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()
|