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.

@@ -14,449 +14,377 @@ class ImageFilters(ImageEditor):
14
14
  def __init__(self):
15
15
  super().__init__()
16
16
 
17
- def denoise(self):
18
- max_range = 500.0
19
- max_value = 5.00
20
- initial_value = 2.5
21
- self.master_layer_copy = self.master_layer.copy()
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
- 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()
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
- active_worker.quit()
73
- active_worker.wait()
64
+ try:
65
+ active_worker.quit()
66
+ active_worker.wait()
67
+ except Exception:
68
+ pass
74
69
  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
- )
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 set_preview(img, request_id, expected_id):
87
- if request_id != expected_id:
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
- 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()
80
+ try:
102
81
  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()
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
- self.master_layer = self.master_layer_copy.copy()
129
- self.display_master_layer()
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 = 100.0
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
- 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
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 on_preview_toggled(checked):
238
- nonlocal last_preview_params
239
- 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
- do_preview()
246
- else:
247
- self.master_layer = self.master_layer_copy.copy()
248
- self.display_master_layer()
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
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
- initial_val = 128
291
- initial_rgb = (initial_val, initial_val, initial_val)
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 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()
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
- dlg.show()
396
- dlg.activateWindow()
397
- dlg.raise_()
345
+ delattr(self, "_original_mouse_press")
346
+ self.wb_dialog = None
398
347
 
399
- pick_button.clicked.connect(start_color_pick)
400
- button_box.accepted.connect(dlg.accept)
348
+ dlg.finished.connect(on_finished)
349
+ QTimer.singleShot(0, do_preview)
401
350
 
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()
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: