shinestacker 0.5.0__py3-none-any.whl → 1.0.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/align.py +4 -12
- shinestacker/algorithms/balance.py +11 -9
- shinestacker/algorithms/depth_map.py +0 -30
- shinestacker/algorithms/utils.py +10 -0
- shinestacker/algorithms/vignetting.py +116 -70
- shinestacker/app/about_dialog.py +37 -16
- shinestacker/app/gui_utils.py +1 -1
- shinestacker/app/help_menu.py +1 -1
- shinestacker/app/main.py +2 -2
- shinestacker/app/project.py +2 -2
- shinestacker/config/constants.py +4 -1
- shinestacker/config/gui_constants.py +3 -4
- shinestacker/gui/action_config.py +5 -561
- shinestacker/gui/action_config_dialog.py +567 -0
- shinestacker/gui/base_form_dialog.py +18 -0
- shinestacker/gui/colors.py +5 -6
- shinestacker/gui/gui_logging.py +0 -1
- shinestacker/gui/gui_run.py +54 -106
- shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker/gui/ico/shinestacker.ico +0 -0
- shinestacker/gui/ico/shinestacker.png +0 -0
- shinestacker/gui/ico/shinestacker.svg +60 -0
- shinestacker/gui/main_window.py +275 -371
- shinestacker/gui/menu_manager.py +236 -0
- shinestacker/gui/new_project.py +75 -20
- shinestacker/gui/{actions_window.py → project_controller.py} +166 -79
- shinestacker/gui/project_converter.py +6 -6
- shinestacker/gui/project_editor.py +248 -165
- shinestacker/gui/project_model.py +2 -7
- shinestacker/gui/tab_widget.py +81 -0
- shinestacker/gui/time_progress_bar.py +95 -0
- shinestacker/retouch/base_filter.py +173 -40
- shinestacker/retouch/brush_preview.py +0 -10
- shinestacker/retouch/brush_tool.py +2 -5
- shinestacker/retouch/denoise_filter.py +5 -44
- shinestacker/retouch/exif_data.py +10 -13
- shinestacker/retouch/file_loader.py +1 -1
- shinestacker/retouch/filter_manager.py +1 -4
- shinestacker/retouch/image_editor_ui.py +318 -40
- shinestacker/retouch/image_viewer.py +34 -11
- shinestacker/retouch/io_gui_handler.py +34 -30
- shinestacker/retouch/layer_collection.py +2 -0
- shinestacker/retouch/shortcuts_help.py +12 -0
- shinestacker/retouch/unsharp_mask_filter.py +10 -10
- shinestacker/retouch/vignetting_filter.py +69 -0
- shinestacker/retouch/white_balance_filter.py +46 -14
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/METADATA +8 -2
- shinestacker-1.0.1.dist-info/RECORD +91 -0
- shinestacker/app/app_config.py +0 -22
- shinestacker/retouch/image_editor.py +0 -197
- shinestacker/retouch/image_filters.py +0 -69
- shinestacker-0.5.0.dist-info/RECORD +0 -87
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/WHEEL +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ from .. config.constants import constants
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ActionConfig:
|
|
7
|
-
def __init__(self, type_name: str, params
|
|
7
|
+
def __init__(self, type_name: str, params=None, parent=None):
|
|
8
8
|
self.type_name = type_name
|
|
9
9
|
self.params = params or {}
|
|
10
10
|
self.parent = parent
|
|
@@ -58,12 +58,7 @@ class ActionConfig:
|
|
|
58
58
|
|
|
59
59
|
class Project:
|
|
60
60
|
def __init__(self):
|
|
61
|
-
self.jobs
|
|
62
|
-
|
|
63
|
-
def run_all(self):
|
|
64
|
-
for job in self.jobs:
|
|
65
|
-
stack_job = job.to_stack_job()
|
|
66
|
-
stack_job.run()
|
|
61
|
+
self.jobs = []
|
|
67
62
|
|
|
68
63
|
def clone(self):
|
|
69
64
|
c = Project()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
|
+
import os
|
|
3
|
+
from PySide6.QtCore import Qt, Signal
|
|
4
|
+
from PySide6.QtGui import QPixmap
|
|
5
|
+
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QLabel, QStackedWidget
|
|
6
|
+
from .. core.core_utils import get_app_base_path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TabWidgetWithPlaceholder(QWidget):
|
|
10
|
+
currentChanged = Signal(int)
|
|
11
|
+
tabCloseRequested = Signal(int)
|
|
12
|
+
|
|
13
|
+
def __init__(self, parent=None):
|
|
14
|
+
super().__init__(parent)
|
|
15
|
+
self.layout = QVBoxLayout(self)
|
|
16
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
17
|
+
self.stacked_widget = QStackedWidget()
|
|
18
|
+
self.layout.addWidget(self.stacked_widget)
|
|
19
|
+
self.tab_widget = QTabWidget()
|
|
20
|
+
self.stacked_widget.addWidget(self.tab_widget)
|
|
21
|
+
self.placeholder = QLabel()
|
|
22
|
+
self.placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
23
|
+
rel_path = 'ico/focus_stack_bkg.png'
|
|
24
|
+
icon_path = f'{get_app_base_path()}/{rel_path}'
|
|
25
|
+
if not os.path.exists(icon_path):
|
|
26
|
+
icon_path = f'{get_app_base_path()}/../{rel_path}'
|
|
27
|
+
if os.path.exists(icon_path):
|
|
28
|
+
pixmap = QPixmap(icon_path)
|
|
29
|
+
pixmap = pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio,
|
|
30
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
31
|
+
self.placeholder.setPixmap(pixmap)
|
|
32
|
+
else:
|
|
33
|
+
self.placeholder.setText("Run logs will appear here.")
|
|
34
|
+
self.stacked_widget.addWidget(self.placeholder)
|
|
35
|
+
self.tab_widget.currentChanged.connect(self._on_current_changed)
|
|
36
|
+
self.tab_widget.tabCloseRequested.connect(self._on_tab_close_requested)
|
|
37
|
+
self.update_placeholder_visibility()
|
|
38
|
+
|
|
39
|
+
def _on_current_changed(self, index):
|
|
40
|
+
self.currentChanged.emit(index)
|
|
41
|
+
self.update_placeholder_visibility()
|
|
42
|
+
|
|
43
|
+
def _on_tab_close_requested(self, index):
|
|
44
|
+
self.tabCloseRequested.emit(index)
|
|
45
|
+
self.update_placeholder_visibility()
|
|
46
|
+
|
|
47
|
+
def update_placeholder_visibility(self):
|
|
48
|
+
if self.tab_widget.count() == 0:
|
|
49
|
+
self.stacked_widget.setCurrentIndex(1)
|
|
50
|
+
else:
|
|
51
|
+
self.stacked_widget.setCurrentIndex(0)
|
|
52
|
+
|
|
53
|
+
# pylint: disable=C0103
|
|
54
|
+
def addTab(self, widget, label):
|
|
55
|
+
result = self.tab_widget.addTab(widget, label)
|
|
56
|
+
self.update_placeholder_visibility()
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
def removeTab(self, index):
|
|
60
|
+
result = self.tab_widget.removeTab(index)
|
|
61
|
+
self.update_placeholder_visibility()
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
def count(self):
|
|
65
|
+
return self.tab_widget.count()
|
|
66
|
+
|
|
67
|
+
def setCurrentIndex(self, index):
|
|
68
|
+
return self.tab_widget.setCurrentIndex(index)
|
|
69
|
+
|
|
70
|
+
def currentIndex(self):
|
|
71
|
+
return self.tab_widget.currentIndex()
|
|
72
|
+
|
|
73
|
+
def currentWidget(self):
|
|
74
|
+
return self.tab_widget.currentWidget()
|
|
75
|
+
|
|
76
|
+
def widget(self, index):
|
|
77
|
+
return self.tab_widget.widget(index)
|
|
78
|
+
|
|
79
|
+
def indexOf(self, widget):
|
|
80
|
+
return self.tab_widget.indexOf(widget)
|
|
81
|
+
# pylint: enable=C0103
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
|
+
import time
|
|
3
|
+
from PySide6.QtWidgets import QProgressBar
|
|
4
|
+
from .colors import ColorPalette, ACTION_RUNNING_COLOR, ACTION_COMPLETED_COLOR
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TimerProgressBar(QProgressBar):
|
|
8
|
+
light_background_color = ColorPalette.LIGHT_BLUE
|
|
9
|
+
border_color = ColorPalette.DARK_BLUE
|
|
10
|
+
text_color = ColorPalette.DARK_BLUE
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__()
|
|
14
|
+
super().setRange(0, 10)
|
|
15
|
+
super().setValue(0)
|
|
16
|
+
self.set_running_style()
|
|
17
|
+
self._start_time = -1
|
|
18
|
+
self._current_time = -1
|
|
19
|
+
self.elapsed_str = ''
|
|
20
|
+
|
|
21
|
+
def set_style(self, bar_color=None):
|
|
22
|
+
if bar_color is None:
|
|
23
|
+
bar_color = ColorPalette.MEDIUM_BLUE
|
|
24
|
+
self.setStyleSheet(f"""
|
|
25
|
+
QProgressBar {{
|
|
26
|
+
border: 2px solid #{self.border_color.hex()};
|
|
27
|
+
border-radius: 8px;
|
|
28
|
+
text-align: center;
|
|
29
|
+
font-weight: bold;
|
|
30
|
+
font-size: 12px;
|
|
31
|
+
background-color: #{self.light_background_color.hex()};
|
|
32
|
+
color: #{self.text_color.hex()};
|
|
33
|
+
min-height: 1px;
|
|
34
|
+
}}
|
|
35
|
+
QProgressBar::chunk {{
|
|
36
|
+
border-radius: 6px;
|
|
37
|
+
background-color: #{bar_color.hex()};
|
|
38
|
+
}}
|
|
39
|
+
""")
|
|
40
|
+
|
|
41
|
+
def time_str(self, secs):
|
|
42
|
+
ss = int(secs)
|
|
43
|
+
x = secs - ss
|
|
44
|
+
s = ss % 60
|
|
45
|
+
mm = ss // 60
|
|
46
|
+
m = mm % 60
|
|
47
|
+
h = mm // 60
|
|
48
|
+
t_str = f"{s:02d}" + f"{x:.1f}s".lstrip('0')
|
|
49
|
+
if m > 0:
|
|
50
|
+
t_str = f"{m:02d}:{t_str}"
|
|
51
|
+
if h > 0:
|
|
52
|
+
t_str = f"{h:02d}:{t_str}"
|
|
53
|
+
if m > 0 or h > 0:
|
|
54
|
+
t_str = t_str.lstrip('0')
|
|
55
|
+
elif 0 < s < 10:
|
|
56
|
+
t_str = t_str.lstrip('0')
|
|
57
|
+
elif s == 0:
|
|
58
|
+
t_str = t_str[1:]
|
|
59
|
+
return t_str
|
|
60
|
+
|
|
61
|
+
def check_time(self, val):
|
|
62
|
+
if self._start_time < 0:
|
|
63
|
+
raise RuntimeError("Start and must be called before setValue and stop")
|
|
64
|
+
self._current_time = time.time()
|
|
65
|
+
elapsed_time = self._current_time - self._start_time
|
|
66
|
+
self.elapsed_str = self.time_str(elapsed_time)
|
|
67
|
+
fmt = f"Progress: %p% - %v of %m - elapsed: {self.elapsed_str}"
|
|
68
|
+
if 0 < val < self.maximum():
|
|
69
|
+
time_per_iter = float(elapsed_time) / float(val)
|
|
70
|
+
estimated_time = time_per_iter * self.maximum()
|
|
71
|
+
remaining_time = max(0, estimated_time - elapsed_time)
|
|
72
|
+
remaining_str = self.time_str(remaining_time)
|
|
73
|
+
fmt += f", {remaining_str} remaining"
|
|
74
|
+
self.setFormat(fmt)
|
|
75
|
+
|
|
76
|
+
def start(self, steps):
|
|
77
|
+
super().setMaximum(steps)
|
|
78
|
+
self._start_time = time.time()
|
|
79
|
+
self.setValue(0)
|
|
80
|
+
|
|
81
|
+
def stop(self):
|
|
82
|
+
self.check_time(self.maximum())
|
|
83
|
+
self.setValue(self.maximum())
|
|
84
|
+
|
|
85
|
+
# pylint: disable=C0103
|
|
86
|
+
def setValue(self, val):
|
|
87
|
+
self.check_time(val)
|
|
88
|
+
super().setValue(val)
|
|
89
|
+
# pylint: enable=C0103
|
|
90
|
+
|
|
91
|
+
def set_running_style(self):
|
|
92
|
+
self.set_style(ACTION_RUNNING_COLOR)
|
|
93
|
+
|
|
94
|
+
def set_done_style(self):
|
|
95
|
+
self.set_style(ACTION_COMPLETED_COLOR)
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0915, R0903
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0915, R0903, R0913, R0917, R0902, R0914
|
|
2
2
|
import traceback
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
import numpy as np
|
|
5
|
-
from PySide6.QtWidgets import
|
|
6
|
-
|
|
5
|
+
from PySide6.QtWidgets import (
|
|
6
|
+
QHBoxLayout, QLabel, QSlider, QDialog, QVBoxLayout, QCheckBox, QDialogButtonBox)
|
|
7
|
+
from PySide6.QtCore import Qt, Signal, QThread, QTimer
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class BaseFilter(ABC):
|
|
10
|
-
def __init__(self, editor
|
|
11
|
+
def __init__(self, name, editor, allow_partial_preview=True,
|
|
12
|
+
partial_preview_threshold=0.75, preview_at_startup=False):
|
|
11
13
|
self.editor = editor
|
|
12
|
-
self.
|
|
14
|
+
self.name = name
|
|
15
|
+
self.allow_partial_preview = allow_partial_preview
|
|
16
|
+
self.partial_preview_threshold = partial_preview_threshold
|
|
17
|
+
self.preview_at_startup = preview_at_startup
|
|
18
|
+
self.preview_check = None
|
|
19
|
+
self.button_box = None
|
|
20
|
+
self.preview_timer = None
|
|
13
21
|
|
|
14
22
|
@abstractmethod
|
|
15
23
|
def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
|
|
@@ -24,20 +32,40 @@ class BaseFilter(ABC):
|
|
|
24
32
|
pass
|
|
25
33
|
|
|
26
34
|
def run_with_preview(self, **kwargs):
|
|
27
|
-
if self.editor.
|
|
35
|
+
if self.editor.has_no_master_layer():
|
|
28
36
|
return
|
|
29
37
|
|
|
30
|
-
self.editor.
|
|
38
|
+
self.editor.copy_master_layer()
|
|
31
39
|
dlg = QDialog(self.editor)
|
|
32
40
|
layout = QVBoxLayout(dlg)
|
|
33
41
|
active_worker = None
|
|
34
42
|
last_request_id = 0
|
|
43
|
+
initial_timer = QTimer(dlg)
|
|
44
|
+
initial_timer.setSingleShot(True)
|
|
45
|
+
dialog_closed = False
|
|
35
46
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
self.editor.
|
|
47
|
+
def cleanup():
|
|
48
|
+
nonlocal active_worker, dialog_closed # noqa
|
|
49
|
+
dialog_closed = True
|
|
50
|
+
self.editor.restore_master_layer()
|
|
40
51
|
self.editor.display_manager.display_master_layer()
|
|
52
|
+
if active_worker and active_worker.isRunning():
|
|
53
|
+
active_worker.wait()
|
|
54
|
+
initial_timer.stop()
|
|
55
|
+
|
|
56
|
+
dlg.finished.connect(cleanup)
|
|
57
|
+
|
|
58
|
+
def set_preview(img, request_id, expected_id, region=None):
|
|
59
|
+
if dialog_closed or request_id != expected_id:
|
|
60
|
+
return
|
|
61
|
+
if region:
|
|
62
|
+
current_region = self.editor.image_viewer.get_visible_image_portion()[1]
|
|
63
|
+
if current_region == region:
|
|
64
|
+
self.editor.set_master_layer(img)
|
|
65
|
+
self.editor.display_manager.display_master_layer()
|
|
66
|
+
else:
|
|
67
|
+
self.editor.set_master_layer(img)
|
|
68
|
+
self.editor.display_manager.display_master_layer()
|
|
41
69
|
try:
|
|
42
70
|
dlg.activateWindow()
|
|
43
71
|
except Exception:
|
|
@@ -45,6 +73,8 @@ class BaseFilter(ABC):
|
|
|
45
73
|
|
|
46
74
|
def do_preview():
|
|
47
75
|
nonlocal active_worker, last_request_id
|
|
76
|
+
if not dlg.isVisible():
|
|
77
|
+
return
|
|
48
78
|
if active_worker and active_worker.isRunning():
|
|
49
79
|
try:
|
|
50
80
|
active_worker.quit()
|
|
@@ -53,14 +83,44 @@ class BaseFilter(ABC):
|
|
|
53
83
|
pass
|
|
54
84
|
last_request_id += 1
|
|
55
85
|
current_id = last_request_id
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
visible_region = None
|
|
87
|
+
if kwargs.get('partial_preview', self.allow_partial_preview):
|
|
88
|
+
visible_data = self.editor.image_viewer.get_visible_image_portion()
|
|
89
|
+
if visible_data:
|
|
90
|
+
visible_img, visible_region = visible_data
|
|
91
|
+
master_img = self.editor.master_layer_copy()
|
|
92
|
+
if visible_img.size < master_img.size * self.partial_preview_threshold:
|
|
93
|
+
params = tuple(self.get_params() or ())
|
|
94
|
+
worker = self.PreviewWorker(
|
|
95
|
+
self.apply,
|
|
96
|
+
args=(master_img, *params),
|
|
97
|
+
request_id=current_id,
|
|
98
|
+
region=visible_region
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
params = tuple(self.get_params() or ())
|
|
102
|
+
worker = self.PreviewWorker(
|
|
103
|
+
self.apply,
|
|
104
|
+
args=(master_img, *params),
|
|
105
|
+
request_id=current_id
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
params = tuple(self.get_params() or ())
|
|
109
|
+
worker = self.PreviewWorker(
|
|
110
|
+
self.apply,
|
|
111
|
+
args=(self.editor.master_layer_copy(), *params),
|
|
112
|
+
request_id=current_id
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
params = tuple(self.get_params() or ())
|
|
116
|
+
worker = self.PreviewWorker(
|
|
117
|
+
self.apply,
|
|
118
|
+
args=(self.editor.master_layer_copy(), *params),
|
|
119
|
+
request_id=current_id
|
|
120
|
+
)
|
|
62
121
|
active_worker = worker
|
|
63
|
-
active_worker.finished.connect(
|
|
122
|
+
active_worker.finished.connect(
|
|
123
|
+
lambda img, rid, region: set_preview(img, rid, current_id, region))
|
|
64
124
|
active_worker.start()
|
|
65
125
|
|
|
66
126
|
def restore_original():
|
|
@@ -72,57 +132,130 @@ class BaseFilter(ABC):
|
|
|
72
132
|
pass
|
|
73
133
|
|
|
74
134
|
self.setup_ui(dlg, layout, do_preview, restore_original, **kwargs)
|
|
75
|
-
|
|
135
|
+
if self.preview_check.isChecked():
|
|
136
|
+
initial_timer.timeout.connect(do_preview)
|
|
137
|
+
initial_timer.start(0)
|
|
76
138
|
accepted = dlg.exec_() == QDialog.Accepted
|
|
139
|
+
cleanup()
|
|
77
140
|
if accepted:
|
|
78
141
|
params = tuple(self.get_params() or ())
|
|
79
142
|
try:
|
|
80
|
-
h, w = self.editor.
|
|
143
|
+
h, w = self.editor.master_layer().shape[:2]
|
|
81
144
|
except Exception:
|
|
82
|
-
h, w = self.editor.
|
|
145
|
+
h, w = self.editor.master_layer_copy().shape[:2]
|
|
83
146
|
if hasattr(self.editor, "undo_manager"):
|
|
84
147
|
try:
|
|
85
148
|
self.editor.undo_manager.extend_undo_area(0, 0, w, h)
|
|
86
149
|
self.editor.undo_manager.save_undo_state(
|
|
87
|
-
self.editor.
|
|
88
|
-
self.
|
|
150
|
+
self.editor.master_layer_copy(),
|
|
151
|
+
self.name
|
|
89
152
|
)
|
|
90
153
|
except Exception:
|
|
91
154
|
pass
|
|
92
|
-
final_img = self.apply(self.editor.
|
|
93
|
-
self.editor.
|
|
94
|
-
self.editor.
|
|
155
|
+
final_img = self.apply(self.editor.master_layer_copy(), *params)
|
|
156
|
+
self.editor.set_master_layer(final_img)
|
|
157
|
+
self.editor.copy_master_layer()
|
|
95
158
|
self.editor.display_manager.display_master_layer()
|
|
96
159
|
self.editor.display_manager.update_master_thumbnail()
|
|
97
160
|
self.editor.mark_as_modified()
|
|
98
161
|
else:
|
|
99
162
|
restore_original()
|
|
100
163
|
|
|
101
|
-
def create_base_widgets(self, layout, buttons, preview_latency):
|
|
102
|
-
preview_check = QCheckBox("Preview")
|
|
103
|
-
preview_check.setChecked(
|
|
104
|
-
layout.addWidget(preview_check)
|
|
105
|
-
button_box = QDialogButtonBox(buttons)
|
|
106
|
-
layout.addWidget(button_box)
|
|
107
|
-
preview_timer = QTimer()
|
|
108
|
-
preview_timer.setSingleShot(True)
|
|
109
|
-
preview_timer.setInterval(preview_latency)
|
|
110
|
-
return preview_check, preview_timer, button_box
|
|
164
|
+
def create_base_widgets(self, layout, buttons, preview_latency, parent):
|
|
165
|
+
self.preview_check = QCheckBox("Preview")
|
|
166
|
+
self.preview_check.setChecked(self.preview_at_startup)
|
|
167
|
+
layout.addWidget(self.preview_check)
|
|
168
|
+
self.button_box = QDialogButtonBox(buttons)
|
|
169
|
+
layout.addWidget(self.button_box)
|
|
170
|
+
self.preview_timer = QTimer(parent)
|
|
171
|
+
self.preview_timer.setSingleShot(True)
|
|
172
|
+
self.preview_timer.setInterval(preview_latency)
|
|
111
173
|
|
|
112
174
|
class PreviewWorker(QThread):
|
|
113
|
-
finished = Signal(np.ndarray, int)
|
|
175
|
+
finished = Signal(np.ndarray, int, tuple)
|
|
114
176
|
|
|
115
|
-
def __init__(self, func, args=(), kwargs=None, request_id=0):
|
|
177
|
+
def __init__(self, func, args=(), kwargs=None, request_id=0, region=None):
|
|
116
178
|
super().__init__()
|
|
117
179
|
self.func = func
|
|
118
180
|
self.args = args
|
|
119
181
|
self.kwargs = kwargs or {}
|
|
120
182
|
self.request_id = request_id
|
|
183
|
+
self.region = region
|
|
121
184
|
|
|
122
185
|
def run(self):
|
|
123
186
|
try:
|
|
124
|
-
|
|
187
|
+
if self.region:
|
|
188
|
+
x, y, w, h = self.region
|
|
189
|
+
image = self.args[0]
|
|
190
|
+
region_img = image[y:y + h, x:x + w]
|
|
191
|
+
region_args = (region_img,) + self.args[1:]
|
|
192
|
+
region_result = self.func(*region_args, **self.kwargs)
|
|
193
|
+
result = image.copy()
|
|
194
|
+
result[y:y + h, x:x + w] = region_result
|
|
195
|
+
else:
|
|
196
|
+
result = self.func(*self.args, **self.kwargs)
|
|
197
|
+
self.finished.emit(result, self.request_id, self.region)
|
|
125
198
|
except Exception as e:
|
|
126
199
|
traceback.print_tb(e.__traceback__)
|
|
127
200
|
raise RuntimeError("Filter preview failed") from e
|
|
128
|
-
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class OneSliderBaseFilter(BaseFilter):
|
|
204
|
+
def __init__(self, name, editor, max_value, initial_value, title,
|
|
205
|
+
allow_partial_preview=True, partial_preview_threshold=0.5,
|
|
206
|
+
preview_at_startup=True):
|
|
207
|
+
super().__init__(name, editor, allow_partial_preview,
|
|
208
|
+
partial_preview_threshold, preview_at_startup)
|
|
209
|
+
self.max_range = 500
|
|
210
|
+
self.max_value = max_value
|
|
211
|
+
self.initial_value = initial_value
|
|
212
|
+
self.slider = None
|
|
213
|
+
self.value_label = None
|
|
214
|
+
self.title = title
|
|
215
|
+
self.format = "{:.2f}"
|
|
216
|
+
|
|
217
|
+
def add_widgets(self, layout, dlg):
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
|
|
221
|
+
dlg.setWindowTitle(self.title)
|
|
222
|
+
dlg.setMinimumWidth(600)
|
|
223
|
+
slider_layout = QHBoxLayout()
|
|
224
|
+
slider_layout.addWidget(QLabel("Amount:"))
|
|
225
|
+
slider_local = QSlider(Qt.Horizontal)
|
|
226
|
+
slider_local.setRange(0, self.max_range)
|
|
227
|
+
slider_local.setValue(int(self.initial_value / self.max_value * self.max_range))
|
|
228
|
+
slider_layout.addWidget(slider_local)
|
|
229
|
+
self.value_label = QLabel(self.format.format(self.initial_value))
|
|
230
|
+
slider_layout.addWidget(self.value_label)
|
|
231
|
+
layout.addLayout(slider_layout)
|
|
232
|
+
self.add_widgets(layout, dlg)
|
|
233
|
+
self.create_base_widgets(
|
|
234
|
+
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200, dlg)
|
|
235
|
+
|
|
236
|
+
self.preview_timer.timeout.connect(do_preview)
|
|
237
|
+
|
|
238
|
+
slider_local.valueChanged.connect(self.config_changed)
|
|
239
|
+
self.editor.connect_preview_toggle(
|
|
240
|
+
self.preview_check, self.do_preview_delayed, restore_original)
|
|
241
|
+
self.button_box.accepted.connect(dlg.accept)
|
|
242
|
+
self.button_box.rejected.connect(dlg.reject)
|
|
243
|
+
self.slider = slider_local
|
|
244
|
+
|
|
245
|
+
def param_changed(self, _val):
|
|
246
|
+
if self.preview_check.isChecked():
|
|
247
|
+
self.do_preview_delayed()
|
|
248
|
+
|
|
249
|
+
def config_changed(self, val):
|
|
250
|
+
float_val = self.max_value * float(val) / self.max_range
|
|
251
|
+
self.value_label.setText(self.format.format(float_val))
|
|
252
|
+
self.param_changed(val)
|
|
253
|
+
|
|
254
|
+
def do_preview_delayed(self):
|
|
255
|
+
self.preview_timer.start()
|
|
256
|
+
|
|
257
|
+
def get_params(self):
|
|
258
|
+
return (self.max_value * self.slider.value() / self.max_range,)
|
|
259
|
+
|
|
260
|
+
def apply(self, image, *params):
|
|
261
|
+
assert False
|
|
@@ -7,16 +7,6 @@ from PySide6.QtGui import QPixmap, QPainter, QImage
|
|
|
7
7
|
from .layer_collection import LayerCollectionHandler
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def brush_profile_lower_limited(r, hardness):
|
|
11
|
-
if hardness >= 1.0:
|
|
12
|
-
result = np.where(r < 1.0, 1.0, 0.0)
|
|
13
|
-
else:
|
|
14
|
-
r_lim = np.where(r < 1.0, r, 1.0)
|
|
15
|
-
k = 1.0 / (1.0 - hardness)
|
|
16
|
-
result = 0.5 * (np.cos(np.pi * np.power(r_lim, k)) + 1.0)
|
|
17
|
-
return result
|
|
18
|
-
|
|
19
|
-
|
|
20
10
|
def brush_profile(r, hardness):
|
|
21
11
|
h = 2.0 * hardness - 1.0
|
|
22
12
|
if h >= 1.0:
|
|
@@ -135,12 +135,12 @@ class BrushTool:
|
|
|
135
135
|
self.image_viewer.update_brush_cursor()
|
|
136
136
|
|
|
137
137
|
def apply_brush_operation(self, master_layer, source_layer, dest_layer, mask_layer,
|
|
138
|
-
view_pos
|
|
138
|
+
view_pos):
|
|
139
139
|
if master_layer is None or source_layer is None:
|
|
140
140
|
return False
|
|
141
141
|
if dest_layer is None:
|
|
142
142
|
dest_layer = master_layer
|
|
143
|
-
scene_pos = image_viewer.mapToScene(view_pos)
|
|
143
|
+
scene_pos = self.image_viewer.mapToScene(view_pos)
|
|
144
144
|
x_center = int(round(scene_pos.x()))
|
|
145
145
|
y_center = int(round(scene_pos.y()))
|
|
146
146
|
radius = int(round(self.brush.size // 2))
|
|
@@ -185,6 +185,3 @@ class BrushTool:
|
|
|
185
185
|
dest_area[:] = np.clip(
|
|
186
186
|
master_area * (1 - effective_mask) + source_area * effective_mask, 0,
|
|
187
187
|
max_px_value).astype(dtype)
|
|
188
|
-
|
|
189
|
-
def clear_cache(self):
|
|
190
|
-
self._brush_mask_cache.clear()
|
|
@@ -1,51 +1,12 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, W0221
|
|
2
|
-
from
|
|
3
|
-
from PySide6.QtCore import Qt
|
|
4
|
-
from .base_filter import BaseFilter
|
|
2
|
+
from .base_filter import OneSliderBaseFilter
|
|
5
3
|
from .. algorithms.denoise import denoise
|
|
6
4
|
|
|
7
5
|
|
|
8
|
-
class DenoiseFilter(
|
|
9
|
-
def __init__(self, editor):
|
|
10
|
-
super().__init__(editor
|
|
11
|
-
|
|
12
|
-
self.max_value = 10.00
|
|
13
|
-
self.initial_value = 2.5
|
|
14
|
-
self.slider = None
|
|
15
|
-
|
|
16
|
-
def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
|
|
17
|
-
dlg.setWindowTitle("Denoise")
|
|
18
|
-
dlg.setMinimumWidth(600)
|
|
19
|
-
slider_layout = QHBoxLayout()
|
|
20
|
-
slider_local = QSlider(Qt.Horizontal)
|
|
21
|
-
slider_local.setRange(0, self.max_range)
|
|
22
|
-
slider_local.setValue(int(self.initial_value / self.max_value * self.max_range))
|
|
23
|
-
slider_layout.addWidget(slider_local)
|
|
24
|
-
value_label = QLabel(f"{self.max_value:.2f}")
|
|
25
|
-
slider_layout.addWidget(value_label)
|
|
26
|
-
layout.addLayout(slider_layout)
|
|
27
|
-
preview_check, preview_timer, button_box = self.create_base_widgets(
|
|
28
|
-
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
|
|
29
|
-
|
|
30
|
-
def do_preview_delayed():
|
|
31
|
-
preview_timer.start()
|
|
32
|
-
|
|
33
|
-
preview_timer.timeout.connect(do_preview)
|
|
34
|
-
|
|
35
|
-
def slider_changed(val):
|
|
36
|
-
float_val = self.max_value * float(val) / self.max_range
|
|
37
|
-
value_label.setText(f"{float_val:.2f}")
|
|
38
|
-
if preview_check.isChecked():
|
|
39
|
-
do_preview_delayed()
|
|
40
|
-
|
|
41
|
-
slider_local.valueChanged.connect(slider_changed)
|
|
42
|
-
self.editor.connect_preview_toggle(preview_check, do_preview_delayed, restore_original)
|
|
43
|
-
button_box.accepted.connect(dlg.accept)
|
|
44
|
-
button_box.rejected.connect(dlg.reject)
|
|
45
|
-
self.slider = slider_local
|
|
46
|
-
|
|
47
|
-
def get_params(self):
|
|
48
|
-
return (self.max_value * self.slider.value() / self.max_range,)
|
|
6
|
+
class DenoiseFilter(OneSliderBaseFilter):
|
|
7
|
+
def __init__(self, name, editor):
|
|
8
|
+
super().__init__(name, editor, 10.0, 2.5, "Denoise",
|
|
9
|
+
allow_partial_preview=True, preview_at_startup=False)
|
|
49
10
|
|
|
50
11
|
def apply(self, image, strength):
|
|
51
12
|
return denoise(image, strength)
|
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611
|
|
2
2
|
from PIL.TiffImagePlugin import IFDRational
|
|
3
|
-
from PySide6.QtWidgets import
|
|
3
|
+
from PySide6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QLabel
|
|
4
4
|
from PySide6.QtCore import Qt
|
|
5
5
|
from .. algorithms.exif import exif_dict
|
|
6
6
|
from .icon_container import icon_container
|
|
7
|
+
from .. gui.base_form_dialog import BaseFormDialog
|
|
7
8
|
|
|
8
9
|
|
|
9
|
-
class ExifData(
|
|
10
|
+
class ExifData(BaseFormDialog):
|
|
10
11
|
def __init__(self, exif, parent=None):
|
|
11
|
-
super().__init__(parent)
|
|
12
|
+
super().__init__("EXIF data", parent)
|
|
12
13
|
self.exif = exif
|
|
13
|
-
self.setWindowTitle("EXIF data")
|
|
14
|
-
self.resize(500, self.height())
|
|
15
|
-
self.layout = QFormLayout(self)
|
|
16
|
-
self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
17
|
-
self.layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
|
18
|
-
self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
|
|
19
|
-
self.layout.setLabelAlignment(Qt.AlignLeft)
|
|
20
14
|
self.create_form()
|
|
21
|
-
|
|
15
|
+
button_container = QWidget()
|
|
16
|
+
button_layout = QHBoxLayout(button_container)
|
|
17
|
+
button_layout.setAlignment(Qt.AlignCenter)
|
|
22
18
|
ok_button = QPushButton("OK")
|
|
19
|
+
ok_button.setFixedWidth(100)
|
|
23
20
|
ok_button.setFocus()
|
|
24
|
-
|
|
25
|
-
self.
|
|
21
|
+
button_layout.addWidget(ok_button)
|
|
22
|
+
self.add_row_to_layout(button_container)
|
|
26
23
|
ok_button.clicked.connect(self.accept)
|
|
27
24
|
|
|
28
25
|
def add_bold_label(self, label):
|
|
@@ -42,7 +42,7 @@ class FileLoader(QThread):
|
|
|
42
42
|
current_labels = [f"Layer {i + 1}" for i in range(len(current_stack))]
|
|
43
43
|
self.finished.emit(current_stack, current_labels, master_layer)
|
|
44
44
|
except Exception as e:
|
|
45
|
-
traceback.print_tb(e.__traceback__)
|
|
45
|
+
# traceback.print_tb(e.__traceback__)
|
|
46
46
|
self.error.emit(f"Error loading file:\n{str(e)}")
|
|
47
47
|
|
|
48
48
|
def load_stack(self, path):
|
|
@@ -5,10 +5,7 @@ class FilterManager:
|
|
|
5
5
|
self.filters = {}
|
|
6
6
|
|
|
7
7
|
def register_filter(self, name, filter_class):
|
|
8
|
-
self.filters[name] = filter_class(self.editor)
|
|
9
|
-
|
|
10
|
-
def get_filter(self, name):
|
|
11
|
-
return self.filters.get(name)
|
|
8
|
+
self.filters[name] = filter_class(name, self.editor)
|
|
12
9
|
|
|
13
10
|
def apply(self, name, **kwargs):
|
|
14
11
|
if name in self.filters:
|