shinestacker 0.3.0__py3-none-any.whl → 0.3.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/__init__.py +6 -6
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/balance.py +6 -7
- shinestacker/algorithms/noise_detection.py +2 -0
- shinestacker/algorithms/utils.py +4 -0
- shinestacker/algorithms/white_balance.py +13 -0
- shinestacker/app/open_frames.py +6 -4
- shinestacker/config/__init__.py +2 -1
- shinestacker/config/config.py +1 -0
- shinestacker/config/constants.py +1 -0
- shinestacker/config/gui_constants.py +1 -0
- shinestacker/core/__init__.py +4 -3
- shinestacker/core/colors.py +1 -0
- shinestacker/core/core_utils.py +6 -6
- shinestacker/core/exceptions.py +1 -0
- shinestacker/core/framework.py +2 -1
- shinestacker/gui/action_config.py +47 -42
- shinestacker/gui/actions_window.py +8 -5
- shinestacker/gui/new_project.py +1 -0
- shinestacker/retouch/brush_gradient.py +20 -0
- shinestacker/retouch/brush_preview.py +10 -14
- shinestacker/retouch/brush_tool.py +164 -0
- shinestacker/retouch/denoise_filter.py +56 -0
- shinestacker/retouch/display_manager.py +177 -0
- shinestacker/retouch/exif_data.py +2 -1
- shinestacker/retouch/filter_base.py +114 -0
- shinestacker/retouch/filter_manager.py +14 -0
- shinestacker/retouch/image_editor.py +108 -543
- shinestacker/retouch/image_editor_ui.py +42 -75
- shinestacker/retouch/image_filters.py +27 -423
- shinestacker/retouch/image_viewer.py +31 -31
- shinestacker/retouch/io_gui_handler.py +208 -0
- shinestacker/retouch/io_manager.py +53 -0
- shinestacker/retouch/layer_collection.py +118 -0
- shinestacker/retouch/unsharp_mask_filter.py +84 -0
- shinestacker/retouch/white_balance_filter.py +111 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/METADATA +3 -2
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/RECORD +42 -31
- shinestacker/retouch/brush_controller.py +0 -57
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/top_level.txt +0 -0
|
@@ -1,463 +1,67 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
-
from PySide6.QtWidgets import (QHBoxLayout,
|
|
3
|
-
QPushButton, QFrame, QVBoxLayout, QLabel, QDialog, QApplication, QSlider,
|
|
4
|
-
QCheckBox, QDialogButtonBox)
|
|
5
|
-
from PySide6.QtGui import QCursor
|
|
6
|
-
from PySide6.QtCore import Qt, QTimer, QThread, Signal
|
|
7
|
-
from .. algorithms.denoise import denoise
|
|
8
|
-
from .. algorithms.sharpen import unsharp_mask
|
|
9
|
-
from .. algorithms.white_balance import white_balance_from_rgb
|
|
10
2
|
from .image_editor import ImageEditor
|
|
3
|
+
from .filter_manager import FilterManager
|
|
4
|
+
from .denoise_filter import DenoiseFilter
|
|
5
|
+
from .unsharp_mask_filter import UnsharpMaskFilter
|
|
6
|
+
from .white_balance_filter import WhiteBalanceFilter
|
|
11
7
|
|
|
12
8
|
|
|
13
9
|
class ImageFilters(ImageEditor):
|
|
14
10
|
def __init__(self):
|
|
15
11
|
super().__init__()
|
|
12
|
+
self.filter_manager = FilterManager(self)
|
|
13
|
+
self.filter_manager.register_filter("denoise", DenoiseFilter)
|
|
14
|
+
self.filter_manager.register_filter("unsharp_mask", UnsharpMaskFilter)
|
|
15
|
+
self.filter_manager.register_filter("white_balance", WhiteBalanceFilter)
|
|
16
16
|
|
|
17
17
|
def denoise(self):
|
|
18
|
-
|
|
19
|
-
max_value = 5.00
|
|
20
|
-
initial_value = 2.5
|
|
21
|
-
self.master_layer_copy = self.master_layer.copy()
|
|
22
|
-
dlg = QDialog(self)
|
|
23
|
-
dlg.setWindowTitle("Denoise")
|
|
24
|
-
dlg.setMinimumWidth(600)
|
|
25
|
-
layout = QVBoxLayout(dlg)
|
|
26
|
-
slider_layout = QHBoxLayout()
|
|
27
|
-
slider = QSlider(Qt.Horizontal)
|
|
28
|
-
slider.setRange(0, max_range)
|
|
29
|
-
slider.setValue(initial_value / max_value * max_range)
|
|
30
|
-
slider_layout.addWidget(slider)
|
|
31
|
-
value_label = QLabel(f"{max_value:.2f}")
|
|
32
|
-
slider_layout.addWidget(value_label)
|
|
33
|
-
layout.addLayout(slider_layout)
|
|
34
|
-
preview_check = QCheckBox("Preview")
|
|
35
|
-
preview_check.setChecked(True)
|
|
36
|
-
layout.addWidget(preview_check)
|
|
37
|
-
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
38
|
-
layout.addWidget(button_box)
|
|
39
|
-
last_preview_strength = None
|
|
40
|
-
preview_timer = QTimer()
|
|
41
|
-
preview_timer.setSingleShot(True)
|
|
42
|
-
preview_timer.setInterval(200)
|
|
43
|
-
active_worker = None
|
|
44
|
-
last_request_id = 0
|
|
45
|
-
|
|
46
|
-
class PreviewWorker(QThread):
|
|
47
|
-
finished = Signal(np.ndarray, int)
|
|
48
|
-
|
|
49
|
-
def __init__(self, image, strength, request_id):
|
|
50
|
-
super().__init__()
|
|
51
|
-
self.image = image
|
|
52
|
-
self.strength = strength
|
|
53
|
-
self.request_id = request_id
|
|
54
|
-
|
|
55
|
-
def run(self):
|
|
56
|
-
result = denoise(self.image, self.strength)
|
|
57
|
-
self.finished.emit(result, self.request_id)
|
|
58
|
-
|
|
59
|
-
def slider_changed(value):
|
|
60
|
-
float_value = max_value * value / max_range
|
|
61
|
-
value_label.setText(f"{float_value:.2f}")
|
|
62
|
-
if preview_check.isChecked():
|
|
63
|
-
nonlocal last_preview_strength
|
|
64
|
-
last_preview_strength = float_value
|
|
65
|
-
preview_timer.start()
|
|
66
|
-
|
|
67
|
-
def do_preview():
|
|
68
|
-
nonlocal active_worker, last_request_id
|
|
69
|
-
if last_preview_strength is None:
|
|
70
|
-
return
|
|
71
|
-
if active_worker and active_worker.isRunning():
|
|
72
|
-
active_worker.quit()
|
|
73
|
-
active_worker.wait()
|
|
74
|
-
last_request_id += 1
|
|
75
|
-
current_request_id = last_request_id
|
|
76
|
-
active_worker = PreviewWorker(
|
|
77
|
-
self.master_layer_copy.copy(),
|
|
78
|
-
last_preview_strength,
|
|
79
|
-
current_request_id
|
|
80
|
-
)
|
|
81
|
-
active_worker.finished.connect(
|
|
82
|
-
lambda img, rid: set_preview(img, rid, current_request_id)
|
|
83
|
-
)
|
|
84
|
-
active_worker.start()
|
|
85
|
-
|
|
86
|
-
def set_preview(img, request_id, expected_id):
|
|
87
|
-
if request_id != expected_id:
|
|
88
|
-
return
|
|
89
|
-
self.master_layer = img
|
|
90
|
-
self.display_master_layer()
|
|
91
|
-
dlg.activateWindow()
|
|
92
|
-
slider.setFocus()
|
|
93
|
-
|
|
94
|
-
def on_preview_toggled(checked):
|
|
95
|
-
nonlocal last_preview_strength
|
|
96
|
-
if checked:
|
|
97
|
-
last_preview_strength = max_value * slider.value() / max_range
|
|
98
|
-
do_preview()
|
|
99
|
-
else:
|
|
100
|
-
self.master_layer = self.master_layer_copy.copy()
|
|
101
|
-
self.display_master_layer()
|
|
102
|
-
dlg.activateWindow()
|
|
103
|
-
slider.setFocus()
|
|
104
|
-
button_box.setFocus()
|
|
105
|
-
|
|
106
|
-
slider.valueChanged.connect(slider_changed)
|
|
107
|
-
preview_timer.timeout.connect(do_preview)
|
|
108
|
-
preview_check.stateChanged.connect(on_preview_toggled)
|
|
109
|
-
button_box.accepted.connect(dlg.accept)
|
|
110
|
-
button_box.rejected.connect(dlg.reject)
|
|
111
|
-
|
|
112
|
-
def run_initial_preview():
|
|
113
|
-
slider_changed(slider.value())
|
|
114
|
-
|
|
115
|
-
QTimer.singleShot(0, run_initial_preview)
|
|
116
|
-
slider.setFocus()
|
|
117
|
-
if dlg.exec_() == QDialog.Accepted:
|
|
118
|
-
strength = max_value * float(slider.value()) / max_range
|
|
119
|
-
h, w = self.master_layer.shape[:2]
|
|
120
|
-
self.undo_manager.extend_undo_area(0, 0, w, h)
|
|
121
|
-
self.undo_manager.save_undo_state(self.master_layer_copy, 'Denoise')
|
|
122
|
-
self.master_layer = denoise(self.master_layer_copy, strength)
|
|
123
|
-
self.master_layer_copy = self.master_layer.copy()
|
|
124
|
-
self.display_master_layer()
|
|
125
|
-
self.update_master_thumbnail()
|
|
126
|
-
self.mark_as_modified()
|
|
127
|
-
else:
|
|
128
|
-
self.master_layer = self.master_layer_copy.copy()
|
|
129
|
-
self.display_master_layer()
|
|
18
|
+
self.filter_manager.apply("denoise")
|
|
130
19
|
|
|
131
20
|
def unsharp_mask(self):
|
|
132
|
-
|
|
133
|
-
max_radius = 4.0
|
|
134
|
-
max_amount = 3.0
|
|
135
|
-
max_threshold = 100.0
|
|
136
|
-
initial_radius = 1.0
|
|
137
|
-
initial_amount = 0.5
|
|
138
|
-
initial_threshold = 0.0
|
|
139
|
-
self.master_layer_copy = self.master_layer.copy()
|
|
140
|
-
dlg = QDialog(self)
|
|
141
|
-
dlg.setWindowTitle("Unsharp Mask")
|
|
142
|
-
dlg.setMinimumWidth(600)
|
|
143
|
-
layout = QVBoxLayout(dlg)
|
|
144
|
-
params = {
|
|
145
|
-
"Radius": (max_radius, initial_radius, "{:.2f}"),
|
|
146
|
-
"Amount": (max_amount, initial_amount, "{:.2%}"),
|
|
147
|
-
"Threshold": (max_threshold, initial_threshold, "{:.2f}")
|
|
148
|
-
}
|
|
149
|
-
sliders = {}
|
|
150
|
-
value_labels = {}
|
|
151
|
-
for name, (max_val, init_val, fmt) in params.items():
|
|
152
|
-
param_layout = QHBoxLayout()
|
|
153
|
-
name_label = QLabel(f"{name}:")
|
|
154
|
-
param_layout.addWidget(name_label)
|
|
155
|
-
slider = QSlider(Qt.Horizontal)
|
|
156
|
-
slider.setRange(0, max_range)
|
|
157
|
-
slider.setValue(init_val / max_val * max_range)
|
|
158
|
-
param_layout.addWidget(slider)
|
|
159
|
-
value_label = QLabel(fmt.format(init_val))
|
|
160
|
-
param_layout.addWidget(value_label)
|
|
161
|
-
layout.addLayout(param_layout)
|
|
162
|
-
sliders[name] = slider
|
|
163
|
-
value_labels[name] = value_label
|
|
164
|
-
preview_check = QCheckBox("Preview")
|
|
165
|
-
preview_check.setChecked(True)
|
|
166
|
-
layout.addWidget(preview_check)
|
|
167
|
-
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
168
|
-
layout.addWidget(button_box)
|
|
169
|
-
last_preview_params = None
|
|
170
|
-
preview_timer = QTimer()
|
|
171
|
-
preview_timer.setSingleShot(True)
|
|
172
|
-
preview_timer.setInterval(200)
|
|
173
|
-
active_worker = None
|
|
174
|
-
last_request_id = 0
|
|
175
|
-
|
|
176
|
-
class UnsharpWorker(QThread):
|
|
177
|
-
finished = Signal(np.ndarray, int)
|
|
178
|
-
|
|
179
|
-
def __init__(self, image, radius, amount, threshold, request_id):
|
|
180
|
-
super().__init__()
|
|
181
|
-
self.image = image
|
|
182
|
-
self.radius = radius
|
|
183
|
-
self.amount = amount
|
|
184
|
-
self.threshold = threshold
|
|
185
|
-
self.request_id = request_id
|
|
21
|
+
self.filter_manager.apply("unsharp_mask")
|
|
186
22
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
self.finished.emit(result, self.request_id)
|
|
23
|
+
def white_balance(self, init_val=None):
|
|
24
|
+
self.filter_manager.apply("white_balance", init_val=init_val or (128, 128, 128))
|
|
190
25
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
value_labels[name].setText(fmt.format(float_value))
|
|
194
|
-
if preview_check.isChecked():
|
|
195
|
-
nonlocal last_preview_params
|
|
196
|
-
last_preview_params = (
|
|
197
|
-
max_radius * sliders["Radius"].value() / max_range,
|
|
198
|
-
max_amount * sliders["Amount"].value() / max_range,
|
|
199
|
-
max_threshold * sliders["Threshold"].value() / max_range
|
|
200
|
-
)
|
|
201
|
-
preview_timer.start()
|
|
202
|
-
sliders["Radius"].valueChanged.connect(
|
|
203
|
-
lambda v: update_param_value("Radius", v, params["Radius"][0], params["Radius"][2]))
|
|
204
|
-
sliders["Amount"].valueChanged.connect(
|
|
205
|
-
lambda v: update_param_value("Amount", v, params["Amount"][0], params["Amount"][2]))
|
|
206
|
-
sliders["Threshold"].valueChanged.connect(
|
|
207
|
-
lambda v: update_param_value("Threshold", v, params["Threshold"][0], params["Threshold"][2]))
|
|
208
|
-
|
|
209
|
-
def do_preview():
|
|
210
|
-
nonlocal active_worker, last_request_id
|
|
211
|
-
if last_preview_params is None:
|
|
212
|
-
return
|
|
213
|
-
if active_worker and active_worker.isRunning():
|
|
214
|
-
active_worker.quit()
|
|
215
|
-
active_worker.wait()
|
|
216
|
-
last_request_id += 1
|
|
217
|
-
current_request_id = last_request_id
|
|
218
|
-
radius, amount, threshold = last_preview_params
|
|
219
|
-
active_worker = UnsharpWorker(
|
|
220
|
-
self.master_layer_copy.copy(),
|
|
221
|
-
radius,
|
|
222
|
-
amount,
|
|
223
|
-
threshold,
|
|
224
|
-
current_request_id
|
|
225
|
-
)
|
|
226
|
-
active_worker.finished.connect(lambda img, rid: set_preview(img, rid, current_request_id))
|
|
227
|
-
active_worker.start()
|
|
228
|
-
|
|
229
|
-
def set_preview(img, request_id, expected_id):
|
|
230
|
-
if request_id != expected_id:
|
|
231
|
-
return
|
|
232
|
-
self.master_layer = img
|
|
233
|
-
self.display_master_layer()
|
|
234
|
-
dlg.activateWindow()
|
|
235
|
-
sliders["Radius"].setFocus()
|
|
236
|
-
|
|
237
|
-
def on_preview_toggled(checked):
|
|
238
|
-
nonlocal last_preview_params
|
|
26
|
+
def connect_preview_toggle(self, preview_check, do_preview, restore_original):
|
|
27
|
+
def on_toggled(checked):
|
|
239
28
|
if checked:
|
|
240
|
-
last_preview_params = (
|
|
241
|
-
max_radius * sliders["Radius"].value() / max_range,
|
|
242
|
-
max_amount * sliders["Amount"].value() / max_range,
|
|
243
|
-
max_threshold * sliders["Threshold"].value() / max_range
|
|
244
|
-
)
|
|
245
29
|
do_preview()
|
|
246
30
|
else:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
dlg.activateWindow()
|
|
250
|
-
sliders["Radius"].setFocus()
|
|
251
|
-
|
|
252
|
-
preview_timer.timeout.connect(do_preview)
|
|
253
|
-
preview_check.stateChanged.connect(on_preview_toggled)
|
|
254
|
-
button_box.accepted.connect(dlg.accept)
|
|
255
|
-
button_box.rejected.connect(dlg.reject)
|
|
256
|
-
|
|
257
|
-
def run_initial_preview():
|
|
258
|
-
nonlocal last_preview_params
|
|
259
|
-
last_preview_params = (
|
|
260
|
-
initial_radius,
|
|
261
|
-
initial_amount,
|
|
262
|
-
initial_threshold
|
|
263
|
-
)
|
|
264
|
-
do_preview()
|
|
265
|
-
|
|
266
|
-
QTimer.singleShot(0, run_initial_preview)
|
|
267
|
-
sliders["Radius"].setFocus()
|
|
268
|
-
if dlg.exec_() == QDialog.Accepted:
|
|
269
|
-
radius = max_radius * sliders["Radius"].value() / max_range
|
|
270
|
-
amount = max_amount * sliders["Amount"].value() / max_range
|
|
271
|
-
threshold = max_threshold * sliders["Threshold"].value() / max_range
|
|
272
|
-
h, w = self.master_layer.shape[:2]
|
|
273
|
-
self.undo_manager.extend_undo_area(0, 0, w, h)
|
|
274
|
-
self.undo_manager.save_undo_state(self.master_layer_copy, 'Unsharp Mask')
|
|
275
|
-
self.master_layer = unsharp_mask(self.master_layer_copy, max(0.01, radius), amount, threshold)
|
|
276
|
-
self.master_layer_copy = self.master_layer.copy()
|
|
277
|
-
self.display_master_layer()
|
|
278
|
-
self.update_master_thumbnail()
|
|
279
|
-
self.mark_as_modified()
|
|
280
|
-
else:
|
|
281
|
-
self.master_layer = self.master_layer_copy.copy()
|
|
282
|
-
self.display_master_layer()
|
|
283
|
-
|
|
284
|
-
def white_balance(self):
|
|
285
|
-
if hasattr(self, 'wb_dialog') and self.wb_dialog:
|
|
286
|
-
self.wb_dialog.activateWindow()
|
|
287
|
-
self.wb_dialog.raise_()
|
|
288
|
-
return
|
|
289
|
-
max_range = 255
|
|
290
|
-
initial_val = 128
|
|
291
|
-
initial_rgb = (initial_val, initial_val, initial_val)
|
|
292
|
-
cursor_style = self.image_viewer.cursor_style
|
|
293
|
-
self.image_viewer.set_cursor_style('outline')
|
|
294
|
-
if self.image_viewer.brush_cursor:
|
|
295
|
-
self.image_viewer.brush_cursor.hide()
|
|
296
|
-
self.master_layer_copy = self.master_layer.copy()
|
|
297
|
-
self.brush_preview.hide()
|
|
298
|
-
self.wb_dialog = dlg = QDialog(self)
|
|
299
|
-
dlg.setWindowTitle("White Balance")
|
|
300
|
-
dlg.setMinimumWidth(600)
|
|
301
|
-
layout = QVBoxLayout(dlg)
|
|
302
|
-
row_layout = QHBoxLayout()
|
|
303
|
-
color_preview = QFrame()
|
|
304
|
-
color_preview.setFixedHeight(80)
|
|
305
|
-
color_preview.setFixedWidth(80)
|
|
306
|
-
color_preview.setStyleSheet("background-color: rgb(128,128,128);")
|
|
307
|
-
row_layout.addWidget(color_preview)
|
|
308
|
-
sliders_layout = QVBoxLayout()
|
|
309
|
-
sliders = {}
|
|
310
|
-
value_labels = {}
|
|
311
|
-
rgb_layouts = {}
|
|
312
|
-
for name, init_val in zip(("R", "G", "B"), initial_rgb):
|
|
313
|
-
row = QHBoxLayout()
|
|
314
|
-
label = QLabel(f"{name}:")
|
|
315
|
-
row.addWidget(label)
|
|
316
|
-
slider = QSlider(Qt.Horizontal)
|
|
317
|
-
slider.setRange(0, max_range)
|
|
318
|
-
slider.setValue(init_val)
|
|
319
|
-
row.addWidget(slider)
|
|
320
|
-
val_label = QLabel(str(init_val))
|
|
321
|
-
row.addWidget(val_label)
|
|
322
|
-
sliders_layout.addLayout(row)
|
|
323
|
-
sliders[name] = slider
|
|
324
|
-
value_labels[name] = val_label
|
|
325
|
-
rgb_layouts[name] = row
|
|
326
|
-
row_layout.addLayout(sliders_layout)
|
|
327
|
-
layout.addLayout(row_layout)
|
|
328
|
-
pick_button = QPushButton("Pick Color")
|
|
329
|
-
layout.addWidget(pick_button)
|
|
330
|
-
|
|
331
|
-
def update_preview_color():
|
|
332
|
-
rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
|
|
333
|
-
color_preview.setStyleSheet(f"background-color: rgb{rgb};")
|
|
334
|
-
|
|
335
|
-
def schedule_preview():
|
|
336
|
-
nonlocal last_preview_rgb
|
|
337
|
-
rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
|
|
338
|
-
for n in ("R", "G", "B"):
|
|
339
|
-
value_labels[n].setText(str(sliders[n].value()))
|
|
340
|
-
update_preview_color()
|
|
341
|
-
if preview_check.isChecked() and rgb != last_preview_rgb:
|
|
342
|
-
last_preview_rgb = rgb
|
|
343
|
-
preview_timer.start(100)
|
|
344
|
-
|
|
345
|
-
def apply_preview():
|
|
346
|
-
rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
|
|
347
|
-
processed = white_balance_from_rgb(self.master_layer_copy, rgb)
|
|
348
|
-
self.master_layer = processed
|
|
349
|
-
self.display_master_layer()
|
|
350
|
-
dlg.activateWindow()
|
|
351
|
-
|
|
352
|
-
def on_preview_toggled(checked):
|
|
353
|
-
nonlocal last_preview_rgb
|
|
354
|
-
if checked:
|
|
355
|
-
last_preview_rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
|
|
356
|
-
preview_timer.start(100)
|
|
357
|
-
else:
|
|
358
|
-
self.master_layer = self.master_layer_copy.copy()
|
|
359
|
-
self.display_master_layer()
|
|
360
|
-
dlg.activateWindow()
|
|
361
|
-
|
|
362
|
-
preview_check = QCheckBox("Preview")
|
|
363
|
-
preview_check.setChecked(True)
|
|
364
|
-
preview_check.stateChanged.connect(on_preview_toggled)
|
|
365
|
-
layout.addWidget(preview_check)
|
|
366
|
-
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel)
|
|
367
|
-
layout.addWidget(button_box)
|
|
368
|
-
last_preview_rgb = None
|
|
369
|
-
preview_timer = QTimer()
|
|
370
|
-
preview_timer.setSingleShot(True)
|
|
371
|
-
preview_timer.timeout.connect(apply_preview)
|
|
372
|
-
for slider in sliders.values():
|
|
373
|
-
slider.valueChanged.connect(schedule_preview)
|
|
374
|
-
|
|
375
|
-
def start_color_pick():
|
|
376
|
-
dlg.hide()
|
|
377
|
-
QApplication.setOverrideCursor(QCursor(Qt.CrossCursor))
|
|
378
|
-
self.image_viewer.setCursor(Qt.CrossCursor)
|
|
379
|
-
self.master_layer = self.master_layer_copy
|
|
380
|
-
self.display_master_layer()
|
|
381
|
-
self._original_mouse_press = self.image_viewer.mousePressEvent
|
|
382
|
-
self.image_viewer.mousePressEvent = pick_color_from_click
|
|
383
|
-
|
|
384
|
-
def pick_color_from_click(event):
|
|
385
|
-
if event.button() == Qt.LeftButton:
|
|
386
|
-
pos = event.pos()
|
|
387
|
-
bgr = self.get_pixel_color_at(pos)
|
|
388
|
-
rgb = (bgr[2], bgr[1], bgr[0])
|
|
389
|
-
for name, val in zip(("R", "G", "B"), rgb):
|
|
390
|
-
sliders[name].setValue(val)
|
|
391
|
-
QApplication.restoreOverrideCursor()
|
|
392
|
-
self.image_viewer.unsetCursor()
|
|
393
|
-
if hasattr(self, "_original_mouse_press"):
|
|
394
|
-
self.image_viewer.mousePressEvent = self._original_mouse_press
|
|
395
|
-
dlg.show()
|
|
396
|
-
dlg.activateWindow()
|
|
397
|
-
dlg.raise_()
|
|
398
|
-
|
|
399
|
-
pick_button.clicked.connect(start_color_pick)
|
|
400
|
-
button_box.accepted.connect(dlg.accept)
|
|
401
|
-
|
|
402
|
-
def cancel_changes():
|
|
403
|
-
self.master_layer = self.master_layer_copy
|
|
404
|
-
self.display_master_layer()
|
|
405
|
-
dlg.reject()
|
|
406
|
-
|
|
407
|
-
def reset_rgb():
|
|
408
|
-
for k, s in sliders.items():
|
|
409
|
-
s.setValue(initial_val)
|
|
410
|
-
|
|
411
|
-
button_box.rejected.connect(cancel_changes)
|
|
412
|
-
button_box.button(QDialogButtonBox.Reset).clicked.connect(reset_rgb)
|
|
413
|
-
|
|
414
|
-
def finish_white_balance(result):
|
|
415
|
-
if result == QDialog.Accepted:
|
|
416
|
-
apply_preview()
|
|
417
|
-
h, w = self.master_layer.shape[:2]
|
|
418
|
-
self.undo_manager.extend_undo_area(0, 0, w, h)
|
|
419
|
-
self.undo_manager.save_undo_state(self.master_layer_copy, 'White Balance')
|
|
420
|
-
self.master_layer_copy = self.master_layer.copy()
|
|
421
|
-
self.display_master_layer()
|
|
422
|
-
self.update_master_thumbnail()
|
|
423
|
-
self.mark_as_modified()
|
|
424
|
-
self.image_viewer.set_cursor_style(cursor_style)
|
|
425
|
-
self.wb_dialog = None
|
|
426
|
-
|
|
427
|
-
dlg.finished.connect(finish_white_balance)
|
|
428
|
-
dlg.show()
|
|
31
|
+
restore_original()
|
|
32
|
+
preview_check.toggled.connect(on_toggled)
|
|
429
33
|
|
|
430
34
|
def get_pixel_color_at(self, pos, radius=None):
|
|
431
|
-
|
|
432
|
-
item_pos = self.image_viewer.pixmap_item.mapFromScene(scene_pos)
|
|
35
|
+
item_pos = self.image_viewer.position_on_image(pos)
|
|
433
36
|
x = int(item_pos.x())
|
|
434
37
|
y = int(item_pos.y())
|
|
435
|
-
|
|
38
|
+
master_layer = self.master_layer()
|
|
39
|
+
if (0 <= x < self.master_layer().shape[1]) and \
|
|
40
|
+
(0 <= y < self.master_layer().shape[0]):
|
|
436
41
|
if radius is None:
|
|
437
42
|
radius = int(self.brush.size)
|
|
438
43
|
if radius > 0:
|
|
439
44
|
y_indices, x_indices = np.ogrid[-radius:radius + 1, -radius:radius + 1]
|
|
440
45
|
mask = x_indices**2 + y_indices**2 <= radius**2
|
|
441
46
|
x0 = max(0, x - radius)
|
|
442
|
-
x1 = min(
|
|
47
|
+
x1 = min(master_layer.shape[1], x + radius + 1)
|
|
443
48
|
y0 = max(0, y - radius)
|
|
444
|
-
y1 = min(
|
|
49
|
+
y1 = min(master_layer.shape[0], y + radius + 1)
|
|
445
50
|
mask = mask[radius - (y - y0): radius + (y1 - y), radius - (x - x0): radius + (x1 - x)]
|
|
446
|
-
region =
|
|
51
|
+
region = master_layer[y0:y1, x0:x1]
|
|
447
52
|
if region.size == 0:
|
|
448
|
-
pixel =
|
|
53
|
+
pixel = master_layer[y, x]
|
|
449
54
|
else:
|
|
450
55
|
if region.ndim == 3:
|
|
451
56
|
pixel = [region[:, :, c][mask].mean() for c in range(region.shape[2])]
|
|
452
57
|
else:
|
|
453
58
|
pixel = region[mask].mean()
|
|
454
59
|
else:
|
|
455
|
-
pixel = self.master_layer[y, x]
|
|
60
|
+
pixel = self.master_layer()[y, x]
|
|
456
61
|
if np.isscalar(pixel):
|
|
457
62
|
pixel = [pixel, pixel, pixel]
|
|
458
63
|
pixel = [np.float32(x) for x in pixel]
|
|
459
|
-
if
|
|
64
|
+
if master_layer.dtype == np.uint16:
|
|
460
65
|
pixel = [x / 256.0 for x in pixel]
|
|
461
66
|
return tuple(int(v) for v in pixel)
|
|
462
|
-
|
|
463
|
-
return (0, 0, 0)
|
|
67
|
+
return (0, 0, 0)
|
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
import math
|
|
2
2
|
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
|
|
3
|
-
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor, QShortcut, QKeySequence
|
|
4
|
-
from PySide6.QtCore import Qt, QRectF, QTime, QPoint, Signal
|
|
3
|
+
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor, QShortcut, QKeySequence
|
|
4
|
+
from PySide6.QtCore import Qt, QRectF, QTime, QPoint, QPointF, Signal
|
|
5
5
|
from .. config.gui_constants import gui_constants
|
|
6
6
|
from .brush_preview import BrushPreviewItem
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def create_brush_gradient(center_x, center_y, radius, hardness, inner_color=None, outer_color=None, opacity=100):
|
|
10
|
-
gradient = QRadialGradient(center_x, center_y, float(radius))
|
|
11
|
-
inner = inner_color if inner_color is not None else QColor(*gui_constants.BRUSH_COLORS['inner'])
|
|
12
|
-
outer = outer_color if outer_color is not None else QColor(*gui_constants.BRUSH_COLORS['gradient_end'])
|
|
13
|
-
inner_with_opacity = QColor(inner)
|
|
14
|
-
inner_with_opacity.setAlpha(int(float(inner.alpha()) * float(opacity) / 100.0))
|
|
15
|
-
if hardness < 100:
|
|
16
|
-
hardness_normalized = float(hardness) / 100.0
|
|
17
|
-
gradient.setColorAt(0.0, inner_with_opacity)
|
|
18
|
-
gradient.setColorAt(hardness_normalized, inner_with_opacity)
|
|
19
|
-
gradient.setColorAt(1.0, outer)
|
|
20
|
-
else:
|
|
21
|
-
gradient.setColorAt(0.0, inner_with_opacity)
|
|
22
|
-
gradient.setColorAt(1.0, inner_with_opacity)
|
|
23
|
-
return gradient
|
|
7
|
+
from .brush_gradient import create_brush_gradient
|
|
24
8
|
|
|
25
9
|
|
|
26
10
|
class ImageViewer(QGraphicsView):
|
|
27
11
|
temp_view_requested = Signal(bool)
|
|
12
|
+
brush_operation_started = Signal(QPoint)
|
|
13
|
+
brush_operation_continued = Signal(QPoint)
|
|
14
|
+
brush_operation_ended = Signal()
|
|
15
|
+
brush_size_change_requested = Signal(int) # +1 or -1
|
|
28
16
|
|
|
29
|
-
def __init__(self, parent=None):
|
|
17
|
+
def __init__(self, layer_collection, parent=None):
|
|
30
18
|
super().__init__(parent)
|
|
31
|
-
self.
|
|
19
|
+
self.display_manager = None
|
|
20
|
+
self.layer_collection = layer_collection
|
|
32
21
|
self.brush = None
|
|
33
22
|
self.cursor_style = gui_constants.DEFAULT_CURSOR_STYLE
|
|
34
23
|
self.scene = QGraphicsScene(self)
|
|
@@ -55,8 +44,10 @@ class ImageViewer(QGraphicsView):
|
|
|
55
44
|
self.dragging = False
|
|
56
45
|
self.last_update_time = QTime.currentTime()
|
|
57
46
|
self.brush_preview = BrushPreviewItem()
|
|
47
|
+
self.layer_collection.add_to(self.brush_preview)
|
|
58
48
|
self.scene.addItem(self.brush_preview)
|
|
59
49
|
self.empty = True
|
|
50
|
+
self.allow_cursor_preview = True
|
|
60
51
|
|
|
61
52
|
def set_image(self, qimage):
|
|
62
53
|
pixmap = QPixmap.fromImage(qimage)
|
|
@@ -74,6 +65,7 @@ class ImageViewer(QGraphicsView):
|
|
|
74
65
|
self.empty = False
|
|
75
66
|
self.setFocus()
|
|
76
67
|
self.activateWindow()
|
|
68
|
+
self.brush_preview.brush = self.brush
|
|
77
69
|
|
|
78
70
|
def clear_image(self):
|
|
79
71
|
self.scene.clear()
|
|
@@ -123,7 +115,7 @@ class ImageViewer(QGraphicsView):
|
|
|
123
115
|
def mousePressEvent(self, event):
|
|
124
116
|
if self.empty:
|
|
125
117
|
return
|
|
126
|
-
if event.button() == Qt.LeftButton and self.
|
|
118
|
+
if event.button() == Qt.LeftButton and self.layer_collection.has_master_layer():
|
|
127
119
|
if self.space_pressed:
|
|
128
120
|
self.scrolling = True
|
|
129
121
|
self.last_mouse_pos = event.position()
|
|
@@ -132,7 +124,7 @@ class ImageViewer(QGraphicsView):
|
|
|
132
124
|
self.brush_cursor.hide()
|
|
133
125
|
else:
|
|
134
126
|
self.last_brush_pos = event.position()
|
|
135
|
-
self.
|
|
127
|
+
self.brush_operation_started.emit(event.position().toPoint())
|
|
136
128
|
self.dragging = True
|
|
137
129
|
if self.brush_cursor:
|
|
138
130
|
self.brush_cursor.show()
|
|
@@ -159,7 +151,7 @@ class ImageViewer(QGraphicsView):
|
|
|
159
151
|
for i in range(0, n_steps + 1):
|
|
160
152
|
pos = QPoint(self.last_brush_pos.x() + i * delta_x,
|
|
161
153
|
self.last_brush_pos.y() + i * delta_y)
|
|
162
|
-
self.
|
|
154
|
+
self.brush_operation_continued.emit(pos)
|
|
163
155
|
self.last_brush_pos = position
|
|
164
156
|
self.last_update_time = current_time
|
|
165
157
|
if self.scrolling and event.buttons() & Qt.LeftButton:
|
|
@@ -191,17 +183,14 @@ class ImageViewer(QGraphicsView):
|
|
|
191
183
|
self.last_mouse_pos = None
|
|
192
184
|
elif hasattr(self, 'dragging') and self.dragging:
|
|
193
185
|
self.dragging = False
|
|
194
|
-
self.
|
|
186
|
+
self.brush_operation_ended.emit()
|
|
195
187
|
super().mouseReleaseEvent(event)
|
|
196
188
|
|
|
197
189
|
def wheelEvent(self, event):
|
|
198
190
|
if self.empty:
|
|
199
191
|
return
|
|
200
192
|
if self.control_pressed:
|
|
201
|
-
if event.angleDelta().y() > 0
|
|
202
|
-
self.image_editor.decrease_brush_size()
|
|
203
|
-
else:
|
|
204
|
-
self.image_editor.increase_brush_size()
|
|
193
|
+
self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
|
|
205
194
|
else:
|
|
206
195
|
zoom_in_factor = 1.10
|
|
207
196
|
zoom_out_factor = 1 / zoom_in_factor
|
|
@@ -241,11 +230,17 @@ class ImageViewer(QGraphicsView):
|
|
|
241
230
|
center_y = scene_pos.y()
|
|
242
231
|
radius = size / 2
|
|
243
232
|
self.brush_cursor.setRect(center_x - radius, center_y - radius, size, size)
|
|
244
|
-
allow_cursor_preview = self.
|
|
233
|
+
allow_cursor_preview = self.display_manager.allow_cursor_preview()
|
|
245
234
|
if self.cursor_style == 'preview' and allow_cursor_preview:
|
|
246
235
|
self._setup_outline_style()
|
|
247
236
|
self.brush_cursor.hide()
|
|
248
|
-
|
|
237
|
+
pos = QCursor.pos()
|
|
238
|
+
if isinstance(pos, QPointF):
|
|
239
|
+
scene_pos = pos
|
|
240
|
+
else:
|
|
241
|
+
cursor_pos = self.mapFromGlobal(pos)
|
|
242
|
+
scene_pos = self.mapToScene(cursor_pos)
|
|
243
|
+
self.brush_preview.update(scene_pos, int(size))
|
|
249
244
|
else:
|
|
250
245
|
self.brush_preview.hide()
|
|
251
246
|
if self.cursor_style == 'outline' or not allow_cursor_preview:
|
|
@@ -354,3 +349,8 @@ class ImageViewer(QGraphicsView):
|
|
|
354
349
|
self.cursor_style = style
|
|
355
350
|
if self.brush_cursor:
|
|
356
351
|
self.update_brush_cursor()
|
|
352
|
+
|
|
353
|
+
def position_on_image(self, pos):
|
|
354
|
+
scene_pos = self.mapToScene(pos)
|
|
355
|
+
item_pos = self.pixmap_item.mapFromScene(scene_pos)
|
|
356
|
+
return item_pos
|