shinestacker 0.3.1__py3-none-any.whl → 0.3.3__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.

Files changed (39) hide show
  1. shinestacker/__init__.py +6 -6
  2. shinestacker/_version.py +1 -1
  3. shinestacker/algorithms/balance.py +6 -7
  4. shinestacker/algorithms/noise_detection.py +2 -0
  5. shinestacker/app/open_frames.py +6 -4
  6. shinestacker/config/__init__.py +2 -1
  7. shinestacker/config/config.py +1 -0
  8. shinestacker/config/constants.py +1 -0
  9. shinestacker/config/gui_constants.py +1 -0
  10. shinestacker/core/__init__.py +4 -3
  11. shinestacker/core/colors.py +1 -0
  12. shinestacker/core/core_utils.py +6 -6
  13. shinestacker/core/exceptions.py +1 -0
  14. shinestacker/core/framework.py +2 -1
  15. shinestacker/gui/action_config.py +47 -42
  16. shinestacker/gui/actions_window.py +8 -5
  17. shinestacker/retouch/brush_preview.py +5 -6
  18. shinestacker/retouch/brush_tool.py +164 -0
  19. shinestacker/retouch/denoise_filter.py +56 -0
  20. shinestacker/retouch/display_manager.py +177 -0
  21. shinestacker/retouch/exif_data.py +2 -1
  22. shinestacker/retouch/filter_base.py +114 -0
  23. shinestacker/retouch/filter_manager.py +14 -0
  24. shinestacker/retouch/image_editor.py +104 -430
  25. shinestacker/retouch/image_editor_ui.py +32 -72
  26. shinestacker/retouch/image_filters.py +25 -349
  27. shinestacker/retouch/image_viewer.py +22 -14
  28. shinestacker/retouch/io_gui_handler.py +208 -0
  29. shinestacker/retouch/io_manager.py +9 -13
  30. shinestacker/retouch/layer_collection.py +65 -1
  31. shinestacker/retouch/unsharp_mask_filter.py +84 -0
  32. shinestacker/retouch/white_balance_filter.py +111 -0
  33. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/METADATA +3 -2
  34. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/RECORD +38 -31
  35. shinestacker/retouch/brush_controller.py +0 -57
  36. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/WHEEL +0 -0
  37. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/entry_points.txt +0 -0
  38. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/licenses/LICENSE +0 -0
  39. {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,12 @@
1
- from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QListWidget, QListWidgetItem, QSlider, QInputDialog
1
+ from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QListWidget, QSlider
2
2
  from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
3
- from PySide6.QtCore import Qt, QSize, Signal
3
+ from PySide6.QtCore import Qt
4
4
  from PySide6.QtGui import QGuiApplication
5
5
  from .. config.gui_constants import gui_constants
6
6
  from .image_filters import ImageFilters
7
7
  from .image_viewer import ImageViewer
8
8
  from .shortcuts_help import ShortcutsHelp
9
+ from .brush import Brush
9
10
 
10
11
 
11
12
  def brush_size_to_slider(size):
@@ -17,22 +18,10 @@ def brush_size_to_slider(size):
17
18
  return int(normalized * gui_constants.BRUSH_SIZE_SLIDER_MAX)
18
19
 
19
20
 
20
- class ClickableLabel(QLabel):
21
- doubleClicked = Signal()
22
-
23
- def __init__(self, text, parent=None):
24
- super().__init__(text, parent)
25
- self.setMouseTracking(True)
26
-
27
- def mouseDoubleClickEvent(self, event):
28
- if event.button() == Qt.LeftButton:
29
- self.doubleClicked.emit()
30
- super().mouseDoubleClickEvent(event)
31
-
32
-
33
21
  class ImageEditorUI(ImageFilters):
34
22
  def __init__(self):
35
23
  super().__init__()
24
+ self.brush = Brush()
36
25
  self.setup_ui()
37
26
  self.setup_menu()
38
27
  self.setup_shortcuts()
@@ -51,10 +40,12 @@ class ImageEditorUI(ImageFilters):
51
40
  central_widget = QWidget()
52
41
  self.setCentralWidget(central_widget)
53
42
  layout = QHBoxLayout(central_widget)
54
- self.image_viewer = ImageViewer()
43
+ self.image_viewer = ImageViewer(self.layer_collection)
55
44
  self.image_viewer.temp_view_requested.connect(self.handle_temp_view)
56
- self.image_viewer.image_editor = self
57
- self.image_viewer.brush = self.brush_controller.brush
45
+ self.image_viewer.brush_operation_started.connect(self.begin_copy_brush_area)
46
+ self.image_viewer.brush_operation_continued.connect(self.continue_copy_brush_area)
47
+ self.image_viewer.brush_operation_ended.connect(self.end_copy_brush_area)
48
+ self.image_viewer.brush_size_change_requested.connect(self.handle_brush_size_change)
58
49
  self.image_viewer.setFocusPolicy(Qt.StrongFocus)
59
50
  side_panel = QWidget()
60
51
  side_layout = QVBoxLayout(side_panel)
@@ -75,7 +66,6 @@ class ImageEditorUI(ImageFilters):
75
66
  self.brush_size_slider = QSlider(Qt.Horizontal)
76
67
  self.brush_size_slider.setRange(0, gui_constants.BRUSH_SIZE_SLIDER_MAX)
77
68
  self.brush_size_slider.setValue(brush_size_to_slider(self.brush.size))
78
- self.brush_size_slider.valueChanged.connect(self.update_brush_size)
79
69
  brush_layout.addWidget(self.brush_size_slider)
80
70
 
81
71
  hardness_label = QLabel("Brush Hardness")
@@ -84,7 +74,6 @@ class ImageEditorUI(ImageFilters):
84
74
  self.hardness_slider = QSlider(Qt.Horizontal)
85
75
  self.hardness_slider.setRange(0, 100)
86
76
  self.hardness_slider.setValue(self.brush.hardness)
87
- self.hardness_slider.valueChanged.connect(self.update_brush_hardness)
88
77
  brush_layout.addWidget(self.hardness_slider)
89
78
 
90
79
  opacity_label = QLabel("Brush Opacity")
@@ -93,7 +82,6 @@ class ImageEditorUI(ImageFilters):
93
82
  self.opacity_slider = QSlider(Qt.Horizontal)
94
83
  self.opacity_slider.setRange(0, 100)
95
84
  self.opacity_slider.setValue(self.brush.opacity)
96
- self.opacity_slider.valueChanged.connect(self.update_brush_opacity)
97
85
  brush_layout.addWidget(self.opacity_slider)
98
86
 
99
87
  flow_label = QLabel("Brush Flow")
@@ -102,7 +90,6 @@ class ImageEditorUI(ImageFilters):
102
90
  self.flow_slider = QSlider(Qt.Horizontal)
103
91
  self.flow_slider.setRange(1, 100)
104
92
  self.flow_slider.setValue(self.brush.flow)
105
- self.flow_slider.valueChanged.connect(self.update_brush_flow)
106
93
  brush_layout.addWidget(self.flow_slider)
107
94
 
108
95
  side_layout.addWidget(brush_panel)
@@ -119,7 +106,6 @@ class ImageEditorUI(ImageFilters):
119
106
  """)
120
107
  self.brush_preview.setAlignment(Qt.AlignCenter)
121
108
  self.brush_preview.setFixedHeight(100)
122
- self.update_brush_thumb()
123
109
  brush_layout.addWidget(self.brush_preview)
124
110
  side_layout.addWidget(brush_panel)
125
111
 
@@ -210,22 +196,23 @@ class ImageEditorUI(ImageFilters):
210
196
  layout.addWidget(control_panel, 0)
211
197
  layout.setContentsMargins(0, 0, 0, 0)
212
198
  layout.setSpacing(2)
199
+ super().setup_ui()
213
200
 
214
201
  def setup_menu(self):
215
202
  menubar = self.menuBar()
216
203
  file_menu = menubar.addMenu("&File")
217
- file_menu.addAction("&Open...", self.open_file, "Ctrl+O")
218
- file_menu.addAction("&Save", self.save_file, "Ctrl+S")
219
- file_menu.addAction("Save &As...", self.save_file_as, "Ctrl+Shift+S")
204
+ file_menu.addAction("&Open...", self.io_gui_handler.open_file, "Ctrl+O")
205
+ file_menu.addAction("&Save", self.io_gui_handler.save_file, "Ctrl+S")
206
+ file_menu.addAction("Save &As...", self.io_gui_handler.save_file_as, "Ctrl+Shift+S")
220
207
  self.save_master_only = QAction("Save Master &Only", self)
221
208
  self.save_master_only.setCheckable(True)
222
209
  self.save_master_only.setChecked(True)
223
210
  file_menu.addAction(self.save_master_only)
224
211
 
225
- file_menu.addAction("&Close", self.close_file, "Ctrl+W")
212
+ file_menu.addAction("&Close", self.io_gui_handler.close_file, "Ctrl+W")
226
213
  file_menu.addSeparator()
227
- file_menu.addAction("&Import frames", self.import_frames)
228
- file_menu.addAction("Import &EXIF data", self.select_exif_path)
214
+ file_menu.addAction("&Import frames", self.io_gui_handler.import_frames)
215
+ file_menu.addAction("Import &EXIF data", self.io_gui_handler.select_exif_path)
229
216
 
230
217
  edit_menu = menubar.addMenu("&Edit")
231
218
  self.undo_action = QAction("Undo", self)
@@ -278,12 +265,12 @@ class ImageEditorUI(ImageFilters):
278
265
 
279
266
  view_master_action = QAction("View Master", self)
280
267
  view_master_action.setShortcut("M")
281
- view_master_action.triggered.connect(self.set_view_master)
268
+ view_master_action.triggered.connect(self.display_manager.set_view_master)
282
269
  view_menu.addAction(view_master_action)
283
270
 
284
271
  view_individual_action = QAction("View Individual", self)
285
272
  view_individual_action.setShortcut("L")
286
- view_individual_action.triggered.connect(self.set_view_individual)
273
+ view_individual_action.triggered.connect(self.display_manager.set_view_individual)
287
274
  view_menu.addAction(view_individual_action)
288
275
  view_menu.addSeparator()
289
276
 
@@ -355,55 +342,28 @@ class ImageEditorUI(ImageFilters):
355
342
  if self._check_unsaved_changes():
356
343
  self.close()
357
344
 
358
- def _add_thumbnail_item(self, thumbnail, label, i, is_current):
359
- item_widget = QWidget()
360
- layout = QVBoxLayout(item_widget)
361
- layout.setContentsMargins(0, 0, 0, 0)
362
- layout.setSpacing(0)
363
-
364
- thumbnail_label = QLabel()
365
- thumbnail_label.setPixmap(thumbnail)
366
- thumbnail_label.setAlignment(Qt.AlignCenter)
367
- layout.addWidget(thumbnail_label)
368
-
369
- label_widget = ClickableLabel(label)
370
- label_widget.setAlignment(Qt.AlignCenter)
371
- label_widget.doubleClicked.connect(lambda: self._rename_label(label_widget, label, i))
372
- layout.addWidget(label_widget)
373
-
374
- item = QListWidgetItem()
375
- item.setSizeHint(QSize(gui_constants.IMG_WIDTH, gui_constants.IMG_HEIGHT))
376
- self.thumbnail_list.addItem(item)
377
- self.thumbnail_list.setItemWidget(item, item_widget)
378
-
379
- if is_current:
380
- self.thumbnail_list.setCurrentItem(item)
381
-
382
- def _rename_label(self, label_widget, old_label, i):
383
- new_label, ok = QInputDialog.getText(self.thumbnail_list, "Rename Label", "New label name:", text=old_label)
384
- if ok and new_label and new_label != old_label:
385
- label_widget.setText(new_label)
386
- self._update_label_in_data(old_label, new_label, i)
387
-
388
- def _update_label_in_data(self, old_label, new_label, i):
389
- self.layer_collection.layer_labels[i] = new_label
390
-
391
345
  def undo(self):
392
- if self.undo_manager.undo(self.layer_collection.master_layer):
393
- self.display_current_view()
394
- self.update_master_thumbnail()
346
+ if self.undo_manager.undo(self.master_layer()):
347
+ self.display_manager.display_current_view()
348
+ self.display_manager.update_master_thumbnail()
395
349
  self.mark_as_modified()
396
350
  self.statusBar().showMessage("Undo applied", 2000)
397
351
 
398
352
  def redo(self):
399
- if self.undo_manager.redo(self.layer_collection.master_layer):
400
- self.display_current_view()
401
- self.update_master_thumbnail()
353
+ if self.undo_manager.redo(self.master_layer()):
354
+ self.display_manager.display_current_view()
355
+ self.display_manager.update_master_thumbnail()
402
356
  self.mark_as_modified()
403
357
  self.statusBar().showMessage("Redo applied", 2000)
404
358
 
405
359
  def handle_temp_view(self, start):
406
360
  if start:
407
- self.start_temp_view()
361
+ self.display_manager.start_temp_view()
362
+ else:
363
+ self.display_manager.end_temp_view()
364
+
365
+ def handle_brush_size_change(self, delta):
366
+ if delta > 0:
367
+ self.brush_tool.increase_brush_size()
408
368
  else:
409
- self.end_temp_view()
369
+ self.brush_tool.decrease_brush_size()
@@ -1,35 +1,27 @@
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
- class PreviewWorker(QThread):
18
- finished = Signal(np.ndarray, int)
17
+ def denoise(self):
18
+ self.filter_manager.apply("denoise")
19
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
20
+ def unsharp_mask(self):
21
+ self.filter_manager.apply("unsharp_mask")
26
22
 
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)
23
+ def white_balance(self, init_val=None):
24
+ self.filter_manager.apply("white_balance", init_val=init_val or (128, 128, 128))
33
25
 
34
26
  def connect_preview_toggle(self, preview_check, do_preview, restore_original):
35
27
  def on_toggled(checked):
@@ -39,353 +31,37 @@ class ImageFilters(ImageEditor):
39
31
  restore_original()
40
32
  preview_check.toggled.connect(on_toggled)
41
33
 
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()
46
- dlg = QDialog(self)
47
- layout = QVBoxLayout(dlg)
48
- active_worker = None
49
- last_request_id = 0
50
-
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
60
-
61
- def do_preview():
62
- nonlocal active_worker, last_request_id
63
- if active_worker and active_worker.isRunning():
64
- try:
65
- active_worker.quit()
66
- active_worker.wait()
67
- except Exception:
68
- pass
69
- last_request_id += 1
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))
75
- active_worker.start()
76
-
77
- def restore_original():
78
- self.layer_collection.master_layer = self.layer_collection.master_layer_copy.copy()
79
- self.display_master_layer()
80
- try:
81
- dlg.activateWindow()
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()
103
- self.display_master_layer()
104
- self.update_master_thumbnail()
105
- self.mark_as_modified()
106
- else:
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')
156
-
157
- def unsharp_mask(self):
158
- max_range = 500.0
159
- max_radius = 4.0
160
- max_amount = 3.0
161
- max_threshold = 64.0
162
- initial_radius = 1.0
163
- initial_amount = 0.5
164
- initial_threshold = 0.0
165
-
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
171
- )
172
-
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):
234
- max_range = 255
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)}
238
- cursor_style = self.image_viewer.cursor_style
239
- self.image_viewer.set_cursor_style('outline')
240
- if self.image_viewer.brush_cursor:
241
- self.image_viewer.brush_cursor.hide()
242
- self.brush_preview.hide()
243
-
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()
341
- if hasattr(self, "_original_mouse_press"):
342
- QApplication.restoreOverrideCursor()
343
- self.image_viewer.unsetCursor()
344
- self.image_viewer.mousePressEvent = self._original_mouse_press
345
- delattr(self, "_original_mouse_press")
346
- self.wb_dialog = None
347
-
348
- dlg.finished.connect(on_finished)
349
- QTimer.singleShot(0, do_preview)
350
-
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')
357
-
358
34
  def get_pixel_color_at(self, pos, radius=None):
359
- scene_pos = self.image_viewer.mapToScene(pos)
360
- item_pos = self.image_viewer.pixmap_item.mapFromScene(scene_pos)
35
+ item_pos = self.image_viewer.position_on_image(pos)
361
36
  x = int(item_pos.x())
362
37
  y = int(item_pos.y())
363
- if (0 <= x < self.layer_collection.master_layer.shape[1]) and (0 <= y < self.layer_collection.master_layer.shape[0]):
38
+ master_layer = self.master_layer()
39
+ if (0 <= x < self.master_layer().shape[1]) and \
40
+ (0 <= y < self.master_layer().shape[0]):
364
41
  if radius is None:
365
42
  radius = int(self.brush.size)
366
43
  if radius > 0:
367
44
  y_indices, x_indices = np.ogrid[-radius:radius + 1, -radius:radius + 1]
368
45
  mask = x_indices**2 + y_indices**2 <= radius**2
369
46
  x0 = max(0, x - radius)
370
- x1 = min(self.layer_collection.master_layer.shape[1], x + radius + 1)
47
+ x1 = min(master_layer.shape[1], x + radius + 1)
371
48
  y0 = max(0, y - radius)
372
- y1 = min(self.layer_collection.master_layer.shape[0], y + radius + 1)
49
+ y1 = min(master_layer.shape[0], y + radius + 1)
373
50
  mask = mask[radius - (y - y0): radius + (y1 - y), radius - (x - x0): radius + (x1 - x)]
374
- region = self.layer_collection.master_layer[y0:y1, x0:x1]
51
+ region = master_layer[y0:y1, x0:x1]
375
52
  if region.size == 0:
376
- pixel = self.layer_collection.master_layer[y, x]
53
+ pixel = master_layer[y, x]
377
54
  else:
378
55
  if region.ndim == 3:
379
56
  pixel = [region[:, :, c][mask].mean() for c in range(region.shape[2])]
380
57
  else:
381
58
  pixel = region[mask].mean()
382
59
  else:
383
- pixel = self.layer_collection.master_layer[y, x]
60
+ pixel = self.master_layer()[y, x]
384
61
  if np.isscalar(pixel):
385
62
  pixel = [pixel, pixel, pixel]
386
63
  pixel = [np.float32(x) for x in pixel]
387
- if self.layer_collection.master_layer.dtype == np.uint16:
64
+ if master_layer.dtype == np.uint16:
388
65
  pixel = [x / 256.0 for x in pixel]
389
66
  return tuple(int(v) for v in pixel)
390
- else:
391
- return (0, 0, 0)
67
+ return (0, 0, 0)