shinestacker 1.5.0__py3-none-any.whl → 1.5.2__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/_version.py +1 -1
- shinestacker/app/args.py +23 -0
- shinestacker/app/main.py +17 -8
- shinestacker/app/project.py +2 -3
- shinestacker/app/retouch.py +8 -4
- shinestacker/config/gui_constants.py +2 -2
- shinestacker/gui/new_project.py +17 -14
- shinestacker/retouch/base_filter.py +59 -35
- shinestacker/retouch/brush_preview.py +29 -15
- shinestacker/retouch/denoise_filter.py +4 -3
- shinestacker/retouch/display_manager.py +14 -19
- shinestacker/retouch/filter_manager.py +11 -3
- shinestacker/retouch/image_editor_ui.py +48 -96
- shinestacker/retouch/image_viewer.py +31 -27
- shinestacker/retouch/io_gui_handler.py +0 -3
- shinestacker/retouch/overlaid_view.py +38 -17
- shinestacker/retouch/shortcuts_help.py +35 -31
- shinestacker/retouch/sidebyside_view.py +57 -76
- shinestacker/retouch/unsharp_mask_filter.py +5 -4
- shinestacker/retouch/view_strategy.py +176 -89
- shinestacker/retouch/vignetting_filter.py +4 -3
- shinestacker/retouch/white_balance_filter.py +62 -17
- {shinestacker-1.5.0.dist-info → shinestacker-1.5.2.dist-info}/METADATA +1 -1
- {shinestacker-1.5.0.dist-info → shinestacker-1.5.2.dist-info}/RECORD +28 -27
- {shinestacker-1.5.0.dist-info → shinestacker-1.5.2.dist-info}/WHEEL +0 -0
- {shinestacker-1.5.0.dist-info → shinestacker-1.5.2.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.5.0.dist-info → shinestacker-1.5.2.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.5.0.dist-info → shinestacker-1.5.2.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.5.
|
|
1
|
+
__version__ = '1.5.2'
|
shinestacker/app/args.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0116
|
|
2
|
+
|
|
3
|
+
def add_project_arguments(parser):
|
|
4
|
+
parser.add_argument('-x', '--expert', action='store_true', help='''
|
|
5
|
+
expert options are visible by default.
|
|
6
|
+
''')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def add_retouch_arguments(parser):
|
|
10
|
+
parser.add_argument('-p', '--path', nargs='?', help='''
|
|
11
|
+
import frames from one or more directories.
|
|
12
|
+
Multiple directories can be specified separated by ';'.
|
|
13
|
+
''')
|
|
14
|
+
view_group = parser.add_mutually_exclusive_group()
|
|
15
|
+
view_group.add_argument('-v1', '--view-overlaid', action='store_true', help='''
|
|
16
|
+
set overlaid view.
|
|
17
|
+
''')
|
|
18
|
+
view_group.add_argument('-v2', '--view-side-by-side', action='store_true', help='''
|
|
19
|
+
set side-by-side view.
|
|
20
|
+
''')
|
|
21
|
+
view_group.add_argument('-v3', '--view-top-bottom', action='store_true', help='''
|
|
22
|
+
set top-bottom view.
|
|
23
|
+
''')
|
shinestacker/app/main.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201, R0915, R0912
|
|
2
2
|
import sys
|
|
3
3
|
import os
|
|
4
4
|
import logging
|
|
@@ -20,6 +20,7 @@ from shinestacker.app.gui_utils import (
|
|
|
20
20
|
disable_macos_special_menu_items, fill_app_menu, set_css_style)
|
|
21
21
|
from shinestacker.app.help_menu import add_help_action
|
|
22
22
|
from shinestacker.app.open_frames import open_frames
|
|
23
|
+
from .args import add_project_arguments, add_retouch_arguments
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class SelectionDialog(QDialog):
|
|
@@ -211,19 +212,21 @@ if a single file is specified, it can be either a project or an image.
|
|
|
211
212
|
Multiple frames can be specified as a list of files.
|
|
212
213
|
Multiple files can be specified separated by ';'.
|
|
213
214
|
''')
|
|
214
|
-
parser.
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
app_group = parser.add_mutually_exclusive_group()
|
|
216
|
+
app_group.add_argument('-j', '--project', action='store_true', help='''
|
|
217
|
+
open project window at startup instead of project windows (default).
|
|
217
218
|
''')
|
|
218
|
-
|
|
219
|
+
app_group.add_argument('-r', '--retouch', action='store_true', help='''
|
|
219
220
|
open retouch window at startup instead of project windows.
|
|
220
221
|
''')
|
|
221
|
-
parser
|
|
222
|
-
|
|
223
|
-
''')
|
|
222
|
+
add_project_arguments(parser)
|
|
223
|
+
add_retouch_arguments(parser)
|
|
224
224
|
args = vars(parser.parse_args(sys.argv[1:]))
|
|
225
225
|
filename = args['filename']
|
|
226
226
|
path = args['path']
|
|
227
|
+
if filename and path:
|
|
228
|
+
print("can't specify both arguments --filename and --path", file=sys.stderr)
|
|
229
|
+
sys.exit(1)
|
|
227
230
|
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
|
|
228
231
|
app = Application(sys.argv)
|
|
229
232
|
if config.DONT_USE_NATIVE_MENU:
|
|
@@ -239,6 +242,12 @@ expert options are visible by default.
|
|
|
239
242
|
main_app.activateWindow()
|
|
240
243
|
if args['expert']:
|
|
241
244
|
main_app.project_window.set_expert_options()
|
|
245
|
+
if args['view_overlaid']:
|
|
246
|
+
main_app.retouch_window.set_strategy('overlaid')
|
|
247
|
+
elif args['view_side_by_side']:
|
|
248
|
+
main_app.retouch_window.set_strategy('sidebyside')
|
|
249
|
+
elif args['view_top_bottom']:
|
|
250
|
+
main_app.retouch_window.set_strategy('topbottom')
|
|
242
251
|
if filename:
|
|
243
252
|
filenames = filename.split(';')
|
|
244
253
|
filename = filenames[0]
|
shinestacker/app/project.py
CHANGED
|
@@ -17,6 +17,7 @@ from shinestacker.gui.main_window import MainWindow
|
|
|
17
17
|
from shinestacker.app.gui_utils import (
|
|
18
18
|
disable_macos_special_menu_items, fill_app_menu, set_css_style)
|
|
19
19
|
from shinestacker.app.help_menu import add_help_action
|
|
20
|
+
from .args import add_project_arguments
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class ProjectApp(MainWindow):
|
|
@@ -52,9 +53,7 @@ def main():
|
|
|
52
53
|
parser.add_argument('-f', '--filename', nargs='?', help='''
|
|
53
54
|
project filename.
|
|
54
55
|
''')
|
|
55
|
-
parser
|
|
56
|
-
expert options are visible by default.
|
|
57
|
-
''')
|
|
56
|
+
add_project_arguments(parser)
|
|
58
57
|
args = vars(parser.parse_args(sys.argv[1:]))
|
|
59
58
|
setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
|
|
60
59
|
app = Application(sys.argv)
|
shinestacker/app/retouch.py
CHANGED
|
@@ -13,6 +13,7 @@ from shinestacker.app.gui_utils import (
|
|
|
13
13
|
disable_macos_special_menu_items, fill_app_menu, set_css_style)
|
|
14
14
|
from shinestacker.app.help_menu import add_help_action
|
|
15
15
|
from shinestacker.app.open_frames import open_frames
|
|
16
|
+
from .args import add_retouch_arguments
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class RetouchApp(ImageEditorUI):
|
|
@@ -44,10 +45,7 @@ def main():
|
|
|
44
45
|
import frames from files.
|
|
45
46
|
Multiple files can be specified separated by ';'.
|
|
46
47
|
''')
|
|
47
|
-
parser
|
|
48
|
-
import frames from one or more directories.
|
|
49
|
-
Multiple directories can be specified separated by ';'.
|
|
50
|
-
''')
|
|
48
|
+
add_retouch_arguments(parser)
|
|
51
49
|
args = vars(parser.parse_args(sys.argv[1:]))
|
|
52
50
|
filename = args['filename']
|
|
53
51
|
path = args['path']
|
|
@@ -65,6 +63,12 @@ Multiple directories can be specified separated by ';'.
|
|
|
65
63
|
editor = RetouchApp()
|
|
66
64
|
app.editor = editor
|
|
67
65
|
editor.show()
|
|
66
|
+
if args['view_overlaid']:
|
|
67
|
+
editor.set_strategy('overlaid')
|
|
68
|
+
elif args['view_side_by_side']:
|
|
69
|
+
editor.set_strategy('sidebyside')
|
|
70
|
+
elif args['view_top_bottom']:
|
|
71
|
+
editor.set_strategy('topbottom')
|
|
68
72
|
open_frames(editor, filename, path)
|
|
69
73
|
sys.exit(app.exec())
|
|
70
74
|
|
|
@@ -26,7 +26,7 @@ class _GuiConstants:
|
|
|
26
26
|
'outer': (255, 0, 0, 200),
|
|
27
27
|
'inner': (255, 0, 0, 150),
|
|
28
28
|
'gradient_end': (255, 0, 0, 0),
|
|
29
|
-
'pen': (255, 0, 0,
|
|
29
|
+
'pen': (255, 0, 0, 200),
|
|
30
30
|
'preview': (255, 180, 180),
|
|
31
31
|
'cursor_inner': (255, 0, 0, 120),
|
|
32
32
|
'preview_inner': (255, 255, 255, 150)
|
|
@@ -55,7 +55,7 @@ class _GuiConstants:
|
|
|
55
55
|
DEFAULT_BRUSH_OPACITY = 100
|
|
56
56
|
DEFAULT_BRUSH_FLOW = 100
|
|
57
57
|
BRUSH_SIZES = {
|
|
58
|
-
'default':
|
|
58
|
+
'default': 100,
|
|
59
59
|
'min': 5,
|
|
60
60
|
'mid': 50,
|
|
61
61
|
'max': 1000
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -108,17 +108,19 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
108
108
|
step2_layout.addRow("Vignetting correction:", self.vignetting_correction)
|
|
109
109
|
step2_layout.addRow(
|
|
110
110
|
# f" {constants.ACTION_ICONS[constants.ACTION_ALIGNFRAMES]} "
|
|
111
|
-
"Align
|
|
111
|
+
"Align frames:", self.align_frames)
|
|
112
112
|
step2_layout.addRow(
|
|
113
113
|
# f" {constants.ACTION_ICONS[constants.ACTION_BALANCEFRAMES]} "
|
|
114
|
-
"Balance
|
|
114
|
+
"Balance frames:", self.balance_frames)
|
|
115
115
|
step2_layout.addRow(
|
|
116
116
|
# f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACKBUNCH]} "
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
"Create bunches:", self.bunch_stack)
|
|
118
|
+
self.bunch_stack.setToolTip("Combine multiple frames into fewer, high-quality "
|
|
119
|
+
"composite frames for easier retouching")
|
|
120
|
+
step2_layout.addRow("Frames per bunch:", self.bunch_frames)
|
|
121
|
+
step2_layout.addRow("Overlap between bunches:", self.bunch_overlap)
|
|
120
122
|
self.bunches_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
121
|
-
step2_layout.addRow("Number of bunches: ", self.bunches_label)
|
|
123
|
+
step2_layout.addRow("Number of resulting bunches: ", self.bunches_label)
|
|
122
124
|
if self.expert():
|
|
123
125
|
step2_layout.addRow(
|
|
124
126
|
f" {constants.ACTION_ICONS[constants.ACTION_FOCUSSTACK]} "
|
|
@@ -133,14 +135,14 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
133
135
|
if self.expert():
|
|
134
136
|
step2_layout.addRow(
|
|
135
137
|
f" {constants.ACTION_ICONS[constants.ACTION_MULTILAYER]} "
|
|
136
|
-
"
|
|
138
|
+
"Export as multilayer TIFF:", self.multi_layer)
|
|
137
139
|
step2_group.setLayout(step2_layout)
|
|
138
140
|
self.form_layout.addRow(step2_group)
|
|
139
141
|
step3_group = QGroupBox("3) Confirm")
|
|
140
142
|
step3_layout = QVBoxLayout()
|
|
141
143
|
step3_layout.setContentsMargins(15, 0, 15, 15)
|
|
142
144
|
step3_layout.addWidget(
|
|
143
|
-
QLabel("Click 🆗 to
|
|
145
|
+
QLabel("Click 🆗 to create project with these settings."))
|
|
144
146
|
step3_layout.addWidget(
|
|
145
147
|
QLabel("Select: <b>View</b> > <b>Expert options</b> for advanced configuration."))
|
|
146
148
|
step3_group.setLayout(step3_layout)
|
|
@@ -149,6 +151,7 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
149
151
|
step4_layout = QHBoxLayout()
|
|
150
152
|
step4_layout.setContentsMargins(15, 0, 15, 15)
|
|
151
153
|
step4_layout.addWidget(QLabel("Press ▶️ to run your job."))
|
|
154
|
+
step4_layout.addStretch()
|
|
152
155
|
icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker.png"
|
|
153
156
|
app_icon = QIcon(icon_path)
|
|
154
157
|
icon_pixmap = app_icon.pixmap(80, 80)
|
|
@@ -293,12 +296,12 @@ class NewProjectDialog(BaseFormDialog):
|
|
|
293
296
|
"Processing may require a significant amount "
|
|
294
297
|
"of memory or I/O buffering.\n\n"
|
|
295
298
|
"Continue anyway?")
|
|
296
|
-
msg.setInformativeText(
|
|
297
|
-
"
|
|
298
|
-
'✅ Check
|
|
299
|
-
"
|
|
300
|
-
|
|
301
|
-
)
|
|
299
|
+
msg.setInformativeText('You may consider creating "bunches" to reduce '
|
|
300
|
+
"the number of frames for retouching.\n\n"
|
|
301
|
+
'✅ Check "Create bunches" to combine frames '
|
|
302
|
+
"into manageable composites.\n\n"
|
|
303
|
+
"➡️ Check expert options for the stacking algorithm.\n\n"
|
|
304
|
+
'Go to "View" > "Expert Options".')
|
|
302
305
|
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
|
|
303
306
|
msg.setDefaultButton(QMessageBox.Cancel)
|
|
304
307
|
if msg.exec_() != QMessageBox.Ok:
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0915, R0903, R0913, R0917, R0902, R0914
|
|
2
2
|
import traceback
|
|
3
|
-
from abc import
|
|
3
|
+
from abc import abstractmethod
|
|
4
4
|
import numpy as np
|
|
5
|
+
from PySide6.QtCore import Qt, QThread, QTimer, QObject, Signal
|
|
5
6
|
from PySide6.QtWidgets import (
|
|
6
7
|
QHBoxLayout, QLabel, QSlider, QDialog, QVBoxLayout, QCheckBox, QDialogButtonBox)
|
|
7
|
-
from
|
|
8
|
+
from .layer_collection import LayerCollectionHandler
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class BaseFilter(
|
|
11
|
-
|
|
11
|
+
class BaseFilter(QObject, LayerCollectionHandler):
|
|
12
|
+
update_master_thumbnail_requested = Signal()
|
|
13
|
+
mark_as_modified_requested = Signal()
|
|
14
|
+
filter_gui_set_enabled_requested = Signal(bool)
|
|
15
|
+
|
|
16
|
+
def __init__(self, name, parent, image_viewer, layer_collection, undo_manager,
|
|
17
|
+
allow_partial_preview=True,
|
|
12
18
|
partial_preview_threshold=0.75, preview_at_startup=False):
|
|
13
|
-
self
|
|
19
|
+
QObject.__init__(self, parent)
|
|
20
|
+
LayerCollectionHandler.__init__(self, layer_collection)
|
|
21
|
+
self.image_viewer = image_viewer
|
|
22
|
+
self.undo_manager = undo_manager
|
|
14
23
|
self.name = name
|
|
15
24
|
self.allow_partial_preview = allow_partial_preview
|
|
16
25
|
self.partial_preview_threshold = partial_preview_threshold
|
|
@@ -31,11 +40,16 @@ class BaseFilter(ABC):
|
|
|
31
40
|
def apply(self, image, *params):
|
|
32
41
|
pass
|
|
33
42
|
|
|
43
|
+
def connect_signals(self, update_master_thumbnail, mark_as_modified, filter_gui_set_enabled):
|
|
44
|
+
self.update_master_thumbnail_requested.connect(update_master_thumbnail)
|
|
45
|
+
self.mark_as_modified_requested.connect(mark_as_modified)
|
|
46
|
+
self.filter_gui_set_enabled_requested.connect(filter_gui_set_enabled)
|
|
47
|
+
|
|
34
48
|
def run_with_preview(self, **kwargs):
|
|
35
|
-
if self.
|
|
49
|
+
if self.has_no_master_layer():
|
|
36
50
|
return
|
|
37
|
-
self.
|
|
38
|
-
dlg = QDialog(self.
|
|
51
|
+
self.copy_master_layer()
|
|
52
|
+
dlg = QDialog(self.parent())
|
|
39
53
|
layout = QVBoxLayout(dlg)
|
|
40
54
|
active_worker = None
|
|
41
55
|
last_request_id = 0
|
|
@@ -46,8 +60,8 @@ class BaseFilter(ABC):
|
|
|
46
60
|
def cleanup():
|
|
47
61
|
nonlocal active_worker, dialog_closed # noqa
|
|
48
62
|
dialog_closed = True
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
63
|
+
self.restore_master_layer()
|
|
64
|
+
self.image_viewer.update_master_display()
|
|
51
65
|
if active_worker and active_worker.isRunning():
|
|
52
66
|
active_worker.wait()
|
|
53
67
|
initial_timer.stop()
|
|
@@ -58,13 +72,13 @@ class BaseFilter(ABC):
|
|
|
58
72
|
if dialog_closed or request_id != expected_id:
|
|
59
73
|
return
|
|
60
74
|
if region:
|
|
61
|
-
current_region = self.
|
|
75
|
+
current_region = self.image_viewer.get_visible_image_portion()[1]
|
|
62
76
|
if current_region == region:
|
|
63
|
-
self.
|
|
64
|
-
self.
|
|
77
|
+
self.set_master_layer(img)
|
|
78
|
+
self.image_viewer.update_master_display()
|
|
65
79
|
else:
|
|
66
|
-
self.
|
|
67
|
-
self.
|
|
80
|
+
self.set_master_layer(img)
|
|
81
|
+
self.image_viewer.update_master_display()
|
|
68
82
|
try:
|
|
69
83
|
dlg.activateWindow()
|
|
70
84
|
except Exception:
|
|
@@ -84,10 +98,10 @@ class BaseFilter(ABC):
|
|
|
84
98
|
current_id = last_request_id
|
|
85
99
|
visible_region = None
|
|
86
100
|
if kwargs.get('partial_preview', self.allow_partial_preview):
|
|
87
|
-
visible_data = self.
|
|
101
|
+
visible_data = self.image_viewer.get_visible_image_portion()
|
|
88
102
|
if visible_data:
|
|
89
103
|
visible_img, visible_region = visible_data
|
|
90
|
-
master_img = self.
|
|
104
|
+
master_img = self.master_layer_copy()
|
|
91
105
|
if visible_img.size < master_img.size * self.partial_preview_threshold:
|
|
92
106
|
params = tuple(self.get_params() or ())
|
|
93
107
|
worker = self.PreviewWorker(
|
|
@@ -107,14 +121,14 @@ class BaseFilter(ABC):
|
|
|
107
121
|
params = tuple(self.get_params() or ())
|
|
108
122
|
worker = self.PreviewWorker(
|
|
109
123
|
self.apply,
|
|
110
|
-
args=(self.
|
|
124
|
+
args=(self.master_layer_copy(), *params),
|
|
111
125
|
request_id=current_id
|
|
112
126
|
)
|
|
113
127
|
else:
|
|
114
128
|
params = tuple(self.get_params() or ())
|
|
115
129
|
worker = self.PreviewWorker(
|
|
116
130
|
self.apply,
|
|
117
|
-
args=(self.
|
|
131
|
+
args=(self.master_layer_copy(), *params),
|
|
118
132
|
request_id=current_id
|
|
119
133
|
)
|
|
120
134
|
active_worker = worker
|
|
@@ -123,8 +137,8 @@ class BaseFilter(ABC):
|
|
|
123
137
|
active_worker.start()
|
|
124
138
|
|
|
125
139
|
def restore_original():
|
|
126
|
-
self.
|
|
127
|
-
self.
|
|
140
|
+
self.restore_master_layer()
|
|
141
|
+
self.image_viewer.update_master_display()
|
|
128
142
|
try:
|
|
129
143
|
dlg.activateWindow()
|
|
130
144
|
except Exception:
|
|
@@ -139,26 +153,34 @@ class BaseFilter(ABC):
|
|
|
139
153
|
if accepted:
|
|
140
154
|
params = tuple(self.get_params() or ())
|
|
141
155
|
try:
|
|
142
|
-
h, w = self.
|
|
156
|
+
h, w = self.master_layer().shape[:2]
|
|
143
157
|
except Exception:
|
|
144
|
-
h, w = self.
|
|
158
|
+
h, w = self.master_layer_copy().shape[:2]
|
|
145
159
|
try:
|
|
146
|
-
self.
|
|
147
|
-
self.
|
|
148
|
-
self.
|
|
160
|
+
self.undo_manager.extend_undo_area(0, 0, w, h)
|
|
161
|
+
self.undo_manager.save_undo_state(
|
|
162
|
+
self.master_layer_copy(),
|
|
149
163
|
self.name
|
|
150
164
|
)
|
|
151
165
|
except Exception:
|
|
152
166
|
pass
|
|
153
|
-
final_img = self.apply(self.
|
|
154
|
-
self.
|
|
155
|
-
self.
|
|
156
|
-
self.
|
|
157
|
-
self.
|
|
158
|
-
self.
|
|
167
|
+
final_img = self.apply(self.master_layer_copy(), *params)
|
|
168
|
+
self.set_master_layer(final_img)
|
|
169
|
+
self.copy_master_layer()
|
|
170
|
+
self.image_viewer.update_master_display()
|
|
171
|
+
self.update_master_thumbnail_requested.emit()
|
|
172
|
+
self.mark_as_modified_requested.emit()
|
|
159
173
|
else:
|
|
160
174
|
restore_original()
|
|
161
175
|
|
|
176
|
+
def connect_preview_toggle(self, preview_check, do_preview, restore_original):
|
|
177
|
+
def on_toggled(checked):
|
|
178
|
+
if checked:
|
|
179
|
+
do_preview()
|
|
180
|
+
else:
|
|
181
|
+
restore_original()
|
|
182
|
+
preview_check.toggled.connect(on_toggled)
|
|
183
|
+
|
|
162
184
|
def create_base_widgets(self, layout, buttons, preview_latency, parent):
|
|
163
185
|
self.preview_check = QCheckBox("Preview")
|
|
164
186
|
self.preview_check.setChecked(self.preview_at_startup)
|
|
@@ -199,10 +221,12 @@ class BaseFilter(ABC):
|
|
|
199
221
|
|
|
200
222
|
|
|
201
223
|
class OneSliderBaseFilter(BaseFilter):
|
|
202
|
-
def __init__(self, name,
|
|
224
|
+
def __init__(self, name, parent, image_viewer, layer_collection, undo_manager,
|
|
225
|
+
max_value, initial_value, title,
|
|
203
226
|
allow_partial_preview=True, partial_preview_threshold=0.5,
|
|
204
227
|
preview_at_startup=True):
|
|
205
|
-
super().__init__(name,
|
|
228
|
+
super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
|
|
229
|
+
allow_partial_preview,
|
|
206
230
|
partial_preview_threshold, preview_at_startup)
|
|
207
231
|
self.max_range = 500
|
|
208
232
|
self.max_value = max_value
|
|
@@ -234,7 +258,7 @@ class OneSliderBaseFilter(BaseFilter):
|
|
|
234
258
|
self.preview_timer.timeout.connect(do_preview)
|
|
235
259
|
|
|
236
260
|
slider_local.valueChanged.connect(self.config_changed)
|
|
237
|
-
self.
|
|
261
|
+
self.connect_preview_toggle(
|
|
238
262
|
self.preview_check, self.do_preview_delayed, restore_original)
|
|
239
263
|
self.button_box.accepted.connect(dlg.accept)
|
|
240
264
|
self.button_box.rejected.connect(dlg.reject)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0914, W0718
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0914, W0718, R0915
|
|
2
2
|
import traceback
|
|
3
3
|
import numpy as np
|
|
4
4
|
from PySide6.QtWidgets import QGraphicsPixmapItem
|
|
@@ -72,38 +72,52 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
|
|
|
72
72
|
self.hide()
|
|
73
73
|
return
|
|
74
74
|
radius = size // 2
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
x_center = int(scene_pos.x() + 0.5)
|
|
76
|
+
y_center = int(scene_pos.y() + 0.5)
|
|
77
|
+
x = x_center - radius
|
|
78
|
+
y = y_center - radius
|
|
77
79
|
w = h = size
|
|
78
80
|
if not self.valid_current_layer_idx():
|
|
79
81
|
self.hide()
|
|
80
82
|
return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
height, width = self.current_layer().shape[:2]
|
|
84
|
+
visible_x = max(0, x)
|
|
85
|
+
visible_y = max(0, y)
|
|
86
|
+
visible_w = min(width, x + w) - visible_x
|
|
87
|
+
visible_h = min(height, y + h) - visible_y
|
|
88
|
+
if visible_w <= 0 or visible_h <= 0:
|
|
89
|
+
self.hide()
|
|
90
|
+
return
|
|
91
|
+
layer_area = self.get_layer_area(
|
|
92
|
+
self.current_layer(), visible_x, visible_y, visible_w, visible_h)
|
|
93
|
+
master_area = self.get_layer_area(
|
|
94
|
+
self.master_layer(), visible_x, visible_y, visible_w, visible_h)
|
|
83
95
|
if layer_area is None or master_area is None:
|
|
84
96
|
self.hide()
|
|
85
97
|
return
|
|
86
|
-
height, width = self.current_layer().shape[:2]
|
|
87
98
|
full_mask = create_brush_mask(size=size, hardness_percent=self.brush.hardness,
|
|
88
99
|
opacity_percent=self.brush.opacity)[:, :, np.newaxis]
|
|
89
|
-
mask_x_start = max(0, -x)
|
|
90
|
-
mask_y_start = max(0, -y)
|
|
91
|
-
mask_x_end =
|
|
92
|
-
mask_y_end =
|
|
100
|
+
mask_x_start = max(0, -x)
|
|
101
|
+
mask_y_start = max(0, -y)
|
|
102
|
+
mask_x_end = mask_x_start + visible_w
|
|
103
|
+
mask_y_end = mask_y_start + visible_h
|
|
93
104
|
mask_area = full_mask[mask_y_start:mask_y_end, mask_x_start:mask_x_end]
|
|
94
105
|
area = (layer_area * mask_area + master_area * (1 - mask_area)) * 255.0
|
|
95
106
|
area = area.astype(np.uint8)
|
|
96
107
|
qimage = QImage(area.data, area.shape[1], area.shape[0],
|
|
97
108
|
area.strides[0], QImage.Format_RGB888)
|
|
98
|
-
mask = QPixmap(
|
|
109
|
+
mask = QPixmap(visible_w, visible_h)
|
|
99
110
|
mask.fill(Qt.transparent)
|
|
100
111
|
painter = QPainter(mask)
|
|
101
112
|
painter.setPen(Qt.NoPen)
|
|
102
113
|
painter.setBrush(Qt.black)
|
|
103
|
-
|
|
114
|
+
center_x_in_visible = x_center - visible_x
|
|
115
|
+
center_y_in_visible = y_center - visible_y
|
|
116
|
+
painter.drawEllipse(
|
|
117
|
+
center_x_in_visible - radius, center_y_in_visible - radius, size, size)
|
|
104
118
|
painter.end()
|
|
105
119
|
pixmap = QPixmap.fromImage(qimage)
|
|
106
|
-
final_pixmap = QPixmap(
|
|
120
|
+
final_pixmap = QPixmap(visible_w, visible_h)
|
|
107
121
|
final_pixmap.fill(Qt.transparent)
|
|
108
122
|
painter = QPainter(final_pixmap)
|
|
109
123
|
painter.drawPixmap(0, 0, pixmap)
|
|
@@ -111,8 +125,8 @@ class BrushPreviewItem(QGraphicsPixmapItem, LayerCollectionHandler):
|
|
|
111
125
|
painter.drawPixmap(0, 0, mask)
|
|
112
126
|
painter.end()
|
|
113
127
|
self.setPixmap(final_pixmap)
|
|
114
|
-
|
|
115
|
-
self.
|
|
128
|
+
self.setPos(visible_x, visible_y)
|
|
129
|
+
self.show()
|
|
116
130
|
except Exception:
|
|
117
131
|
traceback.print_exc()
|
|
118
132
|
self.hide()
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, W0221
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0917
|
|
2
2
|
from .base_filter import OneSliderBaseFilter
|
|
3
3
|
from .. algorithms.denoise import denoise
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class DenoiseFilter(OneSliderBaseFilter):
|
|
7
|
-
def __init__(self, name,
|
|
8
|
-
super().__init__(name,
|
|
7
|
+
def __init__(self, name, parent, image_viewer, layer_collection, undo_manager):
|
|
8
|
+
super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
|
|
9
|
+
10.0, 2.5, "Denoise",
|
|
9
10
|
allow_partial_preview=True, preview_at_startup=False)
|
|
10
11
|
|
|
11
12
|
def apply(self, image, strength):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121, R0902
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121, R0902, R0914
|
|
2
2
|
import numpy as np
|
|
3
3
|
from PySide6.QtWidgets import (QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog,
|
|
4
4
|
QAbstractItemView)
|
|
@@ -38,7 +38,6 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
38
38
|
self.update_timer = QTimer()
|
|
39
39
|
self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
|
|
40
40
|
self.update_timer.timeout.connect(self.process_pending_updates)
|
|
41
|
-
self.thumbnail_highlight = gui_constants.THUMB_LO_COLOR
|
|
42
41
|
|
|
43
42
|
def process_pending_updates(self):
|
|
44
43
|
if self.needs_update:
|
|
@@ -110,14 +109,15 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
110
109
|
self.thumbnail_list, "Rename Label", "New label name:", text=old_label)
|
|
111
110
|
if ok and new_label and new_label != old_label:
|
|
112
111
|
label_widget.setText(new_label)
|
|
113
|
-
self.
|
|
112
|
+
self.set_layer_label(i, new_label)
|
|
113
|
+
self.status_message_requested.emit("Label renamed.")
|
|
114
114
|
|
|
115
115
|
label_widget.double_clicked.connect(lambda: rename_label(label_widget, label, i))
|
|
116
116
|
content_layout.addWidget(label_widget)
|
|
117
117
|
container_layout.addWidget(content_widget)
|
|
118
118
|
if is_current:
|
|
119
119
|
container.setStyleSheet(
|
|
120
|
-
f"#thumbnailContainer{{ border: 2px solid {
|
|
120
|
+
f"#thumbnailContainer{{ border: 2px solid {gui_constants.THUMB_HI_COLOR}; }}")
|
|
121
121
|
else:
|
|
122
122
|
container.setStyleSheet("#thumbnailContainer{ border: 2px solid transparent; }")
|
|
123
123
|
item = QListWidgetItem()
|
|
@@ -128,7 +128,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
128
128
|
if is_current:
|
|
129
129
|
self.thumbnail_list.setCurrentItem(item)
|
|
130
130
|
|
|
131
|
-
def highlight_thumbnail(self, index):
|
|
131
|
+
def highlight_thumbnail(self, index, color=gui_constants.THUMB_HI_COLOR):
|
|
132
132
|
for i in range(self.thumbnail_list.count()):
|
|
133
133
|
item = self.thumbnail_list.item(i)
|
|
134
134
|
widget = self.thumbnail_list.itemWidget(item)
|
|
@@ -139,7 +139,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
139
139
|
widget = self.thumbnail_list.itemWidget(current_item)
|
|
140
140
|
if widget:
|
|
141
141
|
widget.setStyleSheet(
|
|
142
|
-
f"#thumbnailContainer{{ border: 2px solid {
|
|
142
|
+
f"#thumbnailContainer{{ border: 2px solid {color}; }}")
|
|
143
143
|
self.thumbnail_list.setCurrentRow(index)
|
|
144
144
|
self.thumbnail_list.scrollToItem(
|
|
145
145
|
self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
|
|
@@ -147,28 +147,26 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
147
147
|
def _master_refresh_and_thumb(self):
|
|
148
148
|
self.image_viewer.show_master()
|
|
149
149
|
self.refresh_master_view()
|
|
150
|
-
self.
|
|
151
|
-
self.highlight_thumbnail(self.current_layer_idx())
|
|
150
|
+
self.highlight_thumbnail(self.current_layer_idx(), gui_constants.THUMB_LO_COLOR)
|
|
152
151
|
|
|
153
152
|
def _current_refresh_and_thumb(self):
|
|
154
153
|
self.image_viewer.show_current()
|
|
155
154
|
self.refresh_current_view()
|
|
156
|
-
self.
|
|
157
|
-
self.highlight_thumbnail(self.current_layer_idx())
|
|
155
|
+
self.highlight_thumbnail(self.current_layer_idx(), gui_constants.THUMB_HI_COLOR)
|
|
158
156
|
|
|
159
157
|
def set_view_master(self):
|
|
160
158
|
if self.has_no_master_layer():
|
|
161
159
|
return
|
|
162
160
|
self.view_mode = 'master'
|
|
163
161
|
self._master_refresh_and_thumb()
|
|
164
|
-
self.status_message_requested.emit("View
|
|
162
|
+
self.status_message_requested.emit("View: Master.")
|
|
165
163
|
|
|
166
164
|
def set_view_individual(self):
|
|
167
165
|
if self.has_no_master_layer():
|
|
168
166
|
return
|
|
169
167
|
self.view_mode = 'individual'
|
|
170
168
|
self._current_refresh_and_thumb()
|
|
171
|
-
self.status_message_requested.emit("View
|
|
169
|
+
self.status_message_requested.emit("View: Individual layers.")
|
|
172
170
|
|
|
173
171
|
def refresh_master_view(self):
|
|
174
172
|
if self.has_no_master_layer():
|
|
@@ -186,19 +184,16 @@ class DisplayManager(QObject, LayerCollectionHandler):
|
|
|
186
184
|
def start_temp_view(self):
|
|
187
185
|
if self.view_mode == 'master':
|
|
188
186
|
self._current_refresh_and_thumb()
|
|
189
|
-
self.status_message_requested.emit("Temporary view: Individual layer")
|
|
187
|
+
self.status_message_requested.emit("Temporary view: Individual layer.")
|
|
190
188
|
else:
|
|
191
189
|
self._master_refresh_and_thumb()
|
|
192
190
|
self.image_viewer.strategy.brush_preview.hide()
|
|
193
|
-
self.status_message_requested.emit("Temporary view: Master")
|
|
191
|
+
self.status_message_requested.emit("Temporary view: Master.")
|
|
194
192
|
|
|
195
193
|
def end_temp_view(self):
|
|
196
194
|
if self.view_mode == 'master':
|
|
197
195
|
self._master_refresh_and_thumb()
|
|
198
|
-
self.status_message_requested.emit("View mode: Master")
|
|
196
|
+
self.status_message_requested.emit("View mode: Master.")
|
|
199
197
|
else:
|
|
200
198
|
self._current_refresh_and_thumb()
|
|
201
|
-
self.status_message_requested.emit("View: Individual layer")
|
|
202
|
-
|
|
203
|
-
def allow_cursor_preview(self):
|
|
204
|
-
return self.view_mode == 'master'
|
|
199
|
+
self.status_message_requested.emit("View: Individual layer.")
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, R0913, R0917
|
|
2
2
|
class FilterManager:
|
|
3
3
|
def __init__(self, editor):
|
|
4
4
|
self.editor = editor
|
|
5
|
+
self.image_viewer = editor.image_viewer
|
|
6
|
+
self.layer_collection = editor.layer_collection
|
|
7
|
+
self.undo_manager = editor.undo_manager
|
|
5
8
|
self.filters = {}
|
|
6
9
|
|
|
7
|
-
def register_filter(self, name, filter_class
|
|
8
|
-
|
|
10
|
+
def register_filter(self, name, filter_class,
|
|
11
|
+
update_master_thumbnail, mark_as_modified, filter_gui_set_enabled):
|
|
12
|
+
filter_obj = filter_class(
|
|
13
|
+
name, self.editor, self.image_viewer, self.layer_collection, self.undo_manager)
|
|
14
|
+
self.filters[name] = filter_obj
|
|
15
|
+
filter_obj.connect_signals(
|
|
16
|
+
update_master_thumbnail, mark_as_modified, filter_gui_set_enabled)
|
|
9
17
|
|
|
10
18
|
def apply(self, name, **kwargs):
|
|
11
19
|
if name in self.filters:
|