shinestacker 0.3.0__py3-none-any.whl → 0.3.1__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/algorithms/utils.py +4 -0
- shinestacker/algorithms/white_balance.py +13 -0
- shinestacker/gui/new_project.py +1 -0
- shinestacker/retouch/brush_gradient.py +20 -0
- shinestacker/retouch/brush_preview.py +11 -14
- shinestacker/retouch/image_editor.py +93 -202
- shinestacker/retouch/image_editor_ui.py +15 -8
- shinestacker/retouch/image_filters.py +307 -379
- shinestacker/retouch/image_viewer.py +13 -21
- shinestacker/retouch/io_manager.py +57 -0
- shinestacker/retouch/layer_collection.py +54 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/METADATA +1 -1
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/RECORD +18 -14
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.0.dist-info → shinestacker-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -14,449 +14,377 @@ class ImageFilters(ImageEditor):
|
|
|
14
14
|
def __init__(self):
|
|
15
15
|
super().__init__()
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
class PreviewWorker(QThread):
|
|
18
|
+
finished = Signal(np.ndarray, int)
|
|
19
|
+
|
|
20
|
+
def __init__(self, func, args=(), kwargs=None, request_id=0):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.func = func
|
|
23
|
+
self.args = args
|
|
24
|
+
self.kwargs = kwargs or {}
|
|
25
|
+
self.request_id = request_id
|
|
26
|
+
|
|
27
|
+
def run(self):
|
|
28
|
+
try:
|
|
29
|
+
result = self.func(*self.args, **self.kwargs)
|
|
30
|
+
except Exception:
|
|
31
|
+
raise
|
|
32
|
+
self.finished.emit(result, self.request_id)
|
|
33
|
+
|
|
34
|
+
def connect_preview_toggle(self, preview_check, do_preview, restore_original):
|
|
35
|
+
def on_toggled(checked):
|
|
36
|
+
if checked:
|
|
37
|
+
do_preview()
|
|
38
|
+
else:
|
|
39
|
+
restore_original()
|
|
40
|
+
preview_check.toggled.connect(on_toggled)
|
|
41
|
+
|
|
42
|
+
def run_filter_with_preview(self, filter_func, get_params, setup_ui, undo_label):
|
|
43
|
+
if self.layer_collection.master_layer is None:
|
|
44
|
+
return
|
|
45
|
+
self.layer_collection.copy_master_layer()
|
|
22
46
|
dlg = QDialog(self)
|
|
23
|
-
dlg.setWindowTitle("Denoise")
|
|
24
|
-
dlg.setMinimumWidth(600)
|
|
25
47
|
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
48
|
active_worker = None
|
|
44
49
|
last_request_id = 0
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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()
|
|
51
|
+
def set_preview(img, request_id, expected_id):
|
|
52
|
+
if request_id != expected_id:
|
|
53
|
+
return
|
|
54
|
+
self.layer_collection.master_layer = img
|
|
55
|
+
self.display_master_layer()
|
|
56
|
+
try:
|
|
57
|
+
dlg.activateWindow()
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
66
60
|
|
|
67
61
|
def do_preview():
|
|
68
62
|
nonlocal active_worker, last_request_id
|
|
69
|
-
if last_preview_strength is None:
|
|
70
|
-
return
|
|
71
63
|
if active_worker and active_worker.isRunning():
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
try:
|
|
65
|
+
active_worker.quit()
|
|
66
|
+
active_worker.wait()
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
74
69
|
last_request_id += 1
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
active_worker.finished.connect(
|
|
82
|
-
lambda img, rid: set_preview(img, rid, current_request_id)
|
|
83
|
-
)
|
|
70
|
+
current_id = last_request_id
|
|
71
|
+
params = tuple(get_params() or ())
|
|
72
|
+
worker = self.PreviewWorker(filter_func, args=(self.layer_collection.master_layer_copy, *params), request_id=current_id)
|
|
73
|
+
active_worker = worker
|
|
74
|
+
active_worker.finished.connect(lambda img, rid: set_preview(img, rid, current_id))
|
|
84
75
|
active_worker.start()
|
|
85
76
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
return
|
|
89
|
-
self.master_layer = img
|
|
77
|
+
def restore_original():
|
|
78
|
+
self.layer_collection.master_layer = self.layer_collection.master_layer_copy.copy()
|
|
90
79
|
self.display_master_layer()
|
|
91
|
-
|
|
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()
|
|
80
|
+
try:
|
|
102
81
|
dlg.activateWindow()
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
self.
|
|
122
|
-
self.master_layer =
|
|
123
|
-
self.
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
setup_ui(dlg, layout, do_preview, restore_original)
|
|
86
|
+
QTimer.singleShot(0, do_preview)
|
|
87
|
+
accepted = dlg.exec_() == QDialog.Accepted
|
|
88
|
+
if accepted:
|
|
89
|
+
params = tuple(get_params() or ())
|
|
90
|
+
try:
|
|
91
|
+
h, w = self.layer_collection.master_layer.shape[:2]
|
|
92
|
+
except Exception:
|
|
93
|
+
h, w = self.layer_collection.master_layer_copy.shape[:2]
|
|
94
|
+
if hasattr(self, "undo_manager"):
|
|
95
|
+
try:
|
|
96
|
+
self.undo_manager.extend_undo_area(0, 0, w, h)
|
|
97
|
+
self.undo_manager.save_undo_state(self.layer_collection.master_layer_copy, undo_label)
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
final_img = filter_func(self.layer_collection.master_layer_copy, *params)
|
|
101
|
+
self.layer_collection.master_layer = final_img
|
|
102
|
+
self.layer_collection.copy_master_layer()
|
|
124
103
|
self.display_master_layer()
|
|
125
104
|
self.update_master_thumbnail()
|
|
126
105
|
self.mark_as_modified()
|
|
127
106
|
else:
|
|
128
|
-
|
|
129
|
-
|
|
107
|
+
restore_original()
|
|
108
|
+
|
|
109
|
+
def denoise(self):
|
|
110
|
+
max_range = 500.0
|
|
111
|
+
max_value = 10.00
|
|
112
|
+
initial_value = 2.5
|
|
113
|
+
|
|
114
|
+
def get_params():
|
|
115
|
+
return (max_value * slider.value() / max_range,)
|
|
116
|
+
|
|
117
|
+
def setup_ui(dlg, layout, do_preview, restore_original):
|
|
118
|
+
nonlocal slider
|
|
119
|
+
dlg.setWindowTitle("Denoise")
|
|
120
|
+
dlg.setMinimumWidth(600)
|
|
121
|
+
slider_layout = QHBoxLayout()
|
|
122
|
+
slider_local = QSlider(Qt.Horizontal)
|
|
123
|
+
slider_local.setRange(0, max_range)
|
|
124
|
+
slider_local.setValue(int(initial_value / max_value * max_range))
|
|
125
|
+
slider_layout.addWidget(slider_local)
|
|
126
|
+
value_label = QLabel(f"{max_value:.2f}")
|
|
127
|
+
slider_layout.addWidget(value_label)
|
|
128
|
+
layout.addLayout(slider_layout)
|
|
129
|
+
preview_check = QCheckBox("Preview")
|
|
130
|
+
preview_check.setChecked(True)
|
|
131
|
+
layout.addWidget(preview_check)
|
|
132
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
133
|
+
layout.addWidget(button_box)
|
|
134
|
+
preview_timer = QTimer()
|
|
135
|
+
preview_timer.setSingleShot(True)
|
|
136
|
+
preview_timer.setInterval(200)
|
|
137
|
+
|
|
138
|
+
def do_preview_delayed():
|
|
139
|
+
preview_timer.start()
|
|
140
|
+
|
|
141
|
+
def slider_changed(val):
|
|
142
|
+
float_val = max_value * float(val) / max_range
|
|
143
|
+
value_label.setText(f"{float_val:.2f}")
|
|
144
|
+
if preview_check.isChecked():
|
|
145
|
+
do_preview_delayed()
|
|
146
|
+
|
|
147
|
+
preview_timer.timeout.connect(do_preview)
|
|
148
|
+
slider_local.valueChanged.connect(slider_changed)
|
|
149
|
+
self.connect_preview_toggle(preview_check, do_preview_delayed, restore_original)
|
|
150
|
+
button_box.accepted.connect(dlg.accept)
|
|
151
|
+
button_box.rejected.connect(dlg.reject)
|
|
152
|
+
slider = slider_local
|
|
153
|
+
|
|
154
|
+
slider = None
|
|
155
|
+
self.run_filter_with_preview(denoise, get_params, setup_ui, 'Denoise')
|
|
130
156
|
|
|
131
157
|
def unsharp_mask(self):
|
|
132
158
|
max_range = 500.0
|
|
133
159
|
max_radius = 4.0
|
|
134
160
|
max_amount = 3.0
|
|
135
|
-
max_threshold =
|
|
161
|
+
max_threshold = 64.0
|
|
136
162
|
initial_radius = 1.0
|
|
137
163
|
initial_amount = 0.5
|
|
138
164
|
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
165
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
self.image = image
|
|
182
|
-
self.radius = radius
|
|
183
|
-
self.amount = amount
|
|
184
|
-
self.threshold = threshold
|
|
185
|
-
self.request_id = request_id
|
|
186
|
-
|
|
187
|
-
def run(self):
|
|
188
|
-
result = unsharp_mask(self.image, max(0.01, self.radius), self.amount, self.threshold)
|
|
189
|
-
self.finished.emit(result, self.request_id)
|
|
190
|
-
|
|
191
|
-
def update_param_value(name, value, max_val, fmt):
|
|
192
|
-
float_value = max_val * value / max_range
|
|
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
|
|
166
|
+
def get_params():
|
|
167
|
+
return (
|
|
168
|
+
max(0.01, max_radius * radius_slider.value() / max_range),
|
|
169
|
+
max_amount * amount_slider.value() / max_range,
|
|
170
|
+
max_threshold * threshold_slider.value() / max_range
|
|
225
171
|
)
|
|
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
172
|
|
|
237
|
-
def
|
|
238
|
-
nonlocal
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
self.
|
|
288
|
-
|
|
173
|
+
def setup_ui(dlg, layout, do_preview, restore_original):
|
|
174
|
+
nonlocal radius_slider, amount_slider, threshold_slider
|
|
175
|
+
dlg.setWindowTitle("Unsharp Mask")
|
|
176
|
+
dlg.setMinimumWidth(600)
|
|
177
|
+
params = {
|
|
178
|
+
"Radius": (max_radius, initial_radius, "{:.2f}"),
|
|
179
|
+
"Amount": (max_amount, initial_amount, "{:.1%}"),
|
|
180
|
+
"Threshold": (max_threshold, initial_threshold, "{:.2f}")
|
|
181
|
+
}
|
|
182
|
+
value_labels = {}
|
|
183
|
+
for name, (max_val, init_val, fmt) in params.items():
|
|
184
|
+
param_layout = QHBoxLayout()
|
|
185
|
+
name_label = QLabel(f"{name}:")
|
|
186
|
+
param_layout.addWidget(name_label)
|
|
187
|
+
slider = QSlider(Qt.Horizontal)
|
|
188
|
+
slider.setRange(0, max_range)
|
|
189
|
+
slider.setValue(int(init_val / max_val * max_range))
|
|
190
|
+
param_layout.addWidget(slider)
|
|
191
|
+
value_label = QLabel(fmt.format(init_val))
|
|
192
|
+
param_layout.addWidget(value_label)
|
|
193
|
+
layout.addLayout(param_layout)
|
|
194
|
+
if name == "Radius":
|
|
195
|
+
radius_slider = slider
|
|
196
|
+
elif name == "Amount":
|
|
197
|
+
amount_slider = slider
|
|
198
|
+
elif name == "Threshold":
|
|
199
|
+
threshold_slider = slider
|
|
200
|
+
value_labels[name] = value_label
|
|
201
|
+
preview_check = QCheckBox("Preview")
|
|
202
|
+
preview_check.setChecked(True)
|
|
203
|
+
layout.addWidget(preview_check)
|
|
204
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
205
|
+
layout.addWidget(button_box)
|
|
206
|
+
preview_timer = QTimer()
|
|
207
|
+
preview_timer.setSingleShot(True)
|
|
208
|
+
preview_timer.setInterval(200)
|
|
209
|
+
|
|
210
|
+
def update_value(name, value, max_val, fmt):
|
|
211
|
+
float_value = max_val * value / max_range
|
|
212
|
+
value_labels[name].setText(fmt.format(float_value))
|
|
213
|
+
if preview_check.isChecked():
|
|
214
|
+
preview_timer.start()
|
|
215
|
+
|
|
216
|
+
radius_slider.valueChanged.connect(
|
|
217
|
+
lambda v: update_value("Radius", v, max_radius, params["Radius"][2]))
|
|
218
|
+
amount_slider.valueChanged.connect(
|
|
219
|
+
lambda v: update_value("Amount", v, max_amount, params["Amount"][2]))
|
|
220
|
+
threshold_slider.valueChanged.connect(
|
|
221
|
+
lambda v: update_value("Threshold", v, max_threshold, params["Threshold"][2]))
|
|
222
|
+
preview_timer.timeout.connect(do_preview)
|
|
223
|
+
self.connect_preview_toggle(preview_check, do_preview, restore_original)
|
|
224
|
+
button_box.accepted.connect(dlg.accept)
|
|
225
|
+
button_box.rejected.connect(dlg.reject)
|
|
226
|
+
QTimer.singleShot(0, do_preview)
|
|
227
|
+
|
|
228
|
+
radius_slider = None
|
|
229
|
+
amount_slider = None
|
|
230
|
+
threshold_slider = None
|
|
231
|
+
self.run_filter_with_preview(unsharp_mask, get_params, setup_ui, 'Unsharp Mask')
|
|
232
|
+
|
|
233
|
+
def white_balance(self, init_val=False):
|
|
289
234
|
max_range = 255
|
|
290
|
-
|
|
291
|
-
|
|
235
|
+
if init_val is False:
|
|
236
|
+
init_val = (128, 128, 128)
|
|
237
|
+
initial_val = {k: v for k, v in zip(["R", "G", "B"], init_val)}
|
|
292
238
|
cursor_style = self.image_viewer.cursor_style
|
|
293
239
|
self.image_viewer.set_cursor_style('outline')
|
|
294
240
|
if self.image_viewer.brush_cursor:
|
|
295
241
|
self.image_viewer.brush_cursor.hide()
|
|
296
|
-
self.master_layer_copy = self.master_layer.copy()
|
|
297
242
|
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
243
|
|
|
352
|
-
def
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
244
|
+
def get_params():
|
|
245
|
+
return tuple(sliders[n].value() for n in ("R", "G", "B"))
|
|
246
|
+
|
|
247
|
+
def setup_ui(dlg, layout, do_preview, restore_original):
|
|
248
|
+
nonlocal sliders, value_labels, color_preview, preview_timer
|
|
249
|
+
self.wb_dialog = dlg
|
|
250
|
+
dlg.setWindowModality(Qt.ApplicationModal)
|
|
251
|
+
dlg.setWindowFlags(dlg.windowFlags() | Qt.WindowStaysOnTopHint)
|
|
252
|
+
dlg.setFocusPolicy(Qt.StrongFocus)
|
|
253
|
+
dlg.setWindowTitle("White Balance")
|
|
254
|
+
dlg.setMinimumWidth(600)
|
|
255
|
+
row_layout = QHBoxLayout()
|
|
256
|
+
color_preview = QFrame()
|
|
257
|
+
color_preview.setFixedHeight(80)
|
|
258
|
+
color_preview.setFixedWidth(80)
|
|
259
|
+
color_preview.setStyleSheet("background-color: rgb(128,128,128);")
|
|
260
|
+
row_layout.addWidget(color_preview)
|
|
261
|
+
sliders_layout = QVBoxLayout()
|
|
262
|
+
sliders = {}
|
|
263
|
+
value_labels = {}
|
|
264
|
+
for name in ("R", "G", "B"):
|
|
265
|
+
row = QHBoxLayout()
|
|
266
|
+
label = QLabel(f"{name}:")
|
|
267
|
+
row.addWidget(label)
|
|
268
|
+
slider = QSlider(Qt.Horizontal)
|
|
269
|
+
slider.setRange(0, max_range)
|
|
270
|
+
init_val = initial_val[name]
|
|
271
|
+
slider.setValue(init_val)
|
|
272
|
+
row.addWidget(slider)
|
|
273
|
+
val_label = QLabel(str(init_val))
|
|
274
|
+
row.addWidget(val_label)
|
|
275
|
+
sliders_layout.addLayout(row)
|
|
276
|
+
sliders[name] = slider
|
|
277
|
+
value_labels[name] = val_label
|
|
278
|
+
row_layout.addLayout(sliders_layout)
|
|
279
|
+
layout.addLayout(row_layout)
|
|
280
|
+
pick_button = QPushButton("Pick Color")
|
|
281
|
+
layout.addWidget(pick_button)
|
|
282
|
+
preview_check = QCheckBox("Preview")
|
|
283
|
+
preview_check.setChecked(True)
|
|
284
|
+
layout.addWidget(preview_check)
|
|
285
|
+
button_box = QDialogButtonBox(
|
|
286
|
+
QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel
|
|
287
|
+
)
|
|
288
|
+
layout.addWidget(button_box)
|
|
289
|
+
preview_timer = QTimer()
|
|
290
|
+
preview_timer.setSingleShot(True)
|
|
291
|
+
preview_timer.setInterval(200)
|
|
292
|
+
|
|
293
|
+
def update_preview_color():
|
|
294
|
+
rgb = tuple(sliders[n].value() for n in ("R", "G", "B"))
|
|
295
|
+
color_preview.setStyleSheet(f"background-color: rgb{rgb};")
|
|
296
|
+
|
|
297
|
+
def schedule_preview():
|
|
298
|
+
if preview_check.isChecked():
|
|
299
|
+
preview_timer.start()
|
|
300
|
+
|
|
301
|
+
def on_slider_change():
|
|
302
|
+
for name in ("R", "G", "B"):
|
|
303
|
+
value_labels[name].setText(str(sliders[name].value()))
|
|
304
|
+
update_preview_color()
|
|
305
|
+
schedule_preview()
|
|
306
|
+
|
|
307
|
+
for slider in sliders.values():
|
|
308
|
+
slider.valueChanged.connect(on_slider_change)
|
|
309
|
+
|
|
310
|
+
preview_timer.timeout.connect(do_preview)
|
|
311
|
+
self.connect_preview_toggle(preview_check, do_preview, restore_original)
|
|
312
|
+
|
|
313
|
+
def start_color_pick():
|
|
314
|
+
restore_original()
|
|
315
|
+
dlg.hide()
|
|
316
|
+
QApplication.setOverrideCursor(QCursor(Qt.CrossCursor))
|
|
317
|
+
self.image_viewer.setCursor(Qt.CrossCursor)
|
|
318
|
+
self._original_mouse_press = self.image_viewer.mousePressEvent
|
|
319
|
+
self.image_viewer.mousePressEvent = pick_color_from_click
|
|
320
|
+
|
|
321
|
+
def pick_color_from_click(event):
|
|
322
|
+
if event.button() == Qt.LeftButton:
|
|
323
|
+
pos = event.pos()
|
|
324
|
+
bgr = self.get_pixel_color_at(pos, radius=int(self.brush.size))
|
|
325
|
+
rgb = (bgr[2], bgr[1], bgr[0])
|
|
326
|
+
self.white_balance(rgb)
|
|
327
|
+
|
|
328
|
+
def reset_rgb():
|
|
329
|
+
for name, slider in sliders.items():
|
|
330
|
+
slider.setValue(initial_val[name])
|
|
331
|
+
|
|
332
|
+
pick_button.clicked.connect(start_color_pick)
|
|
333
|
+
button_box.accepted.connect(dlg.accept)
|
|
334
|
+
button_box.rejected.connect(dlg.reject)
|
|
335
|
+
button_box.button(QDialogButtonBox.Reset).clicked.connect(reset_rgb)
|
|
336
|
+
|
|
337
|
+
def on_finished():
|
|
338
|
+
self.image_viewer.set_cursor_style(cursor_style)
|
|
339
|
+
self.image_viewer.brush_cursor.show()
|
|
340
|
+
self.brush_preview.show()
|
|
393
341
|
if hasattr(self, "_original_mouse_press"):
|
|
342
|
+
QApplication.restoreOverrideCursor()
|
|
343
|
+
self.image_viewer.unsetCursor()
|
|
394
344
|
self.image_viewer.mousePressEvent = self._original_mouse_press
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
dlg.raise_()
|
|
345
|
+
delattr(self, "_original_mouse_press")
|
|
346
|
+
self.wb_dialog = None
|
|
398
347
|
|
|
399
|
-
|
|
400
|
-
|
|
348
|
+
dlg.finished.connect(on_finished)
|
|
349
|
+
QTimer.singleShot(0, do_preview)
|
|
401
350
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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()
|
|
351
|
+
sliders = {}
|
|
352
|
+
value_labels = {}
|
|
353
|
+
color_preview = None
|
|
354
|
+
preview_timer = None
|
|
355
|
+
self.run_filter_with_preview(lambda img, r, g, b: white_balance_from_rgb(img, (r, g, b)),
|
|
356
|
+
get_params, setup_ui, 'White Balance')
|
|
429
357
|
|
|
430
358
|
def get_pixel_color_at(self, pos, radius=None):
|
|
431
359
|
scene_pos = self.image_viewer.mapToScene(pos)
|
|
432
360
|
item_pos = self.image_viewer.pixmap_item.mapFromScene(scene_pos)
|
|
433
361
|
x = int(item_pos.x())
|
|
434
362
|
y = int(item_pos.y())
|
|
435
|
-
if (0 <= x < self.master_layer.shape[1]) and (0 <= y < self.master_layer.shape[0]):
|
|
363
|
+
if (0 <= x < self.layer_collection.master_layer.shape[1]) and (0 <= y < self.layer_collection.master_layer.shape[0]):
|
|
436
364
|
if radius is None:
|
|
437
365
|
radius = int(self.brush.size)
|
|
438
366
|
if radius > 0:
|
|
439
367
|
y_indices, x_indices = np.ogrid[-radius:radius + 1, -radius:radius + 1]
|
|
440
368
|
mask = x_indices**2 + y_indices**2 <= radius**2
|
|
441
369
|
x0 = max(0, x - radius)
|
|
442
|
-
x1 = min(self.master_layer.shape[1], x + radius + 1)
|
|
370
|
+
x1 = min(self.layer_collection.master_layer.shape[1], x + radius + 1)
|
|
443
371
|
y0 = max(0, y - radius)
|
|
444
|
-
y1 = min(self.master_layer.shape[0], y + radius + 1)
|
|
372
|
+
y1 = min(self.layer_collection.master_layer.shape[0], y + radius + 1)
|
|
445
373
|
mask = mask[radius - (y - y0): radius + (y1 - y), radius - (x - x0): radius + (x1 - x)]
|
|
446
|
-
region = self.master_layer[y0:y1, x0:x1]
|
|
374
|
+
region = self.layer_collection.master_layer[y0:y1, x0:x1]
|
|
447
375
|
if region.size == 0:
|
|
448
|
-
pixel = self.master_layer[y, x]
|
|
376
|
+
pixel = self.layer_collection.master_layer[y, x]
|
|
449
377
|
else:
|
|
450
378
|
if region.ndim == 3:
|
|
451
379
|
pixel = [region[:, :, c][mask].mean() for c in range(region.shape[2])]
|
|
452
380
|
else:
|
|
453
381
|
pixel = region[mask].mean()
|
|
454
382
|
else:
|
|
455
|
-
pixel = self.master_layer[y, x]
|
|
383
|
+
pixel = self.layer_collection.master_layer[y, x]
|
|
456
384
|
if np.isscalar(pixel):
|
|
457
385
|
pixel = [pixel, pixel, pixel]
|
|
458
386
|
pixel = [np.float32(x) for x in pixel]
|
|
459
|
-
if self.master_layer.dtype == np.uint16:
|
|
387
|
+
if self.layer_collection.master_layer.dtype == np.uint16:
|
|
460
388
|
pixel = [x / 256.0 for x in pixel]
|
|
461
389
|
return tuple(int(v) for v in pixel)
|
|
462
390
|
else:
|