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.
- shinestacker/__init__.py +6 -6
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/balance.py +6 -7
- shinestacker/algorithms/noise_detection.py +2 -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/retouch/brush_preview.py +5 -6
- 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 +104 -430
- shinestacker/retouch/image_editor_ui.py +32 -72
- shinestacker/retouch/image_filters.py +25 -349
- shinestacker/retouch/image_viewer.py +22 -14
- shinestacker/retouch/io_gui_handler.py +208 -0
- shinestacker/retouch/io_manager.py +9 -13
- shinestacker/retouch/layer_collection.py +65 -1
- shinestacker/retouch/unsharp_mask_filter.py +84 -0
- shinestacker/retouch/white_balance_filter.py +111 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/METADATA +3 -2
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/RECORD +38 -31
- shinestacker/retouch/brush_controller.py +0 -57
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {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,
|
|
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
|
|
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.
|
|
57
|
-
self.image_viewer.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
18
|
-
|
|
17
|
+
def denoise(self):
|
|
18
|
+
self.filter_manager.apply("denoise")
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
47
|
+
x1 = min(master_layer.shape[1], x + radius + 1)
|
|
371
48
|
y0 = max(0, y - radius)
|
|
372
|
-
y1 = min(
|
|
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 =
|
|
51
|
+
region = master_layer[y0:y1, x0:x1]
|
|
375
52
|
if region.size == 0:
|
|
376
|
-
pixel =
|
|
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.
|
|
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
|
|
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
|
-
|
|
391
|
-
return (0, 0, 0)
|
|
67
|
+
return (0, 0, 0)
|