shinestacker 0.4.0__py3-none-any.whl → 1.0.0__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 (59) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +4 -12
  3. shinestacker/algorithms/balance.py +11 -9
  4. shinestacker/algorithms/depth_map.py +0 -30
  5. shinestacker/algorithms/utils.py +10 -0
  6. shinestacker/algorithms/vignetting.py +116 -70
  7. shinestacker/app/about_dialog.py +101 -12
  8. shinestacker/app/gui_utils.py +1 -1
  9. shinestacker/app/help_menu.py +1 -1
  10. shinestacker/app/main.py +2 -2
  11. shinestacker/app/project.py +2 -2
  12. shinestacker/config/constants.py +4 -1
  13. shinestacker/config/gui_constants.py +10 -9
  14. shinestacker/gui/action_config.py +5 -561
  15. shinestacker/gui/action_config_dialog.py +567 -0
  16. shinestacker/gui/base_form_dialog.py +18 -0
  17. shinestacker/gui/colors.py +5 -6
  18. shinestacker/gui/gui_logging.py +0 -1
  19. shinestacker/gui/gui_run.py +54 -106
  20. shinestacker/gui/ico/shinestacker.icns +0 -0
  21. shinestacker/gui/ico/shinestacker.ico +0 -0
  22. shinestacker/gui/ico/shinestacker.png +0 -0
  23. shinestacker/gui/ico/shinestacker.svg +60 -0
  24. shinestacker/gui/main_window.py +276 -367
  25. shinestacker/gui/menu_manager.py +236 -0
  26. shinestacker/gui/new_project.py +75 -20
  27. shinestacker/gui/project_converter.py +6 -6
  28. shinestacker/gui/project_editor.py +248 -165
  29. shinestacker/gui/project_model.py +2 -7
  30. shinestacker/gui/tab_widget.py +81 -0
  31. shinestacker/gui/time_progress_bar.py +95 -0
  32. shinestacker/retouch/base_filter.py +173 -40
  33. shinestacker/retouch/brush_preview.py +0 -10
  34. shinestacker/retouch/brush_tool.py +25 -11
  35. shinestacker/retouch/denoise_filter.py +5 -44
  36. shinestacker/retouch/display_manager.py +57 -20
  37. shinestacker/retouch/exif_data.py +10 -13
  38. shinestacker/retouch/file_loader.py +1 -1
  39. shinestacker/retouch/filter_manager.py +1 -4
  40. shinestacker/retouch/image_editor_ui.py +365 -49
  41. shinestacker/retouch/image_viewer.py +34 -11
  42. shinestacker/retouch/io_gui_handler.py +96 -43
  43. shinestacker/retouch/io_manager.py +23 -7
  44. shinestacker/retouch/layer_collection.py +2 -0
  45. shinestacker/retouch/shortcuts_help.py +12 -0
  46. shinestacker/retouch/unsharp_mask_filter.py +10 -10
  47. shinestacker/retouch/vignetting_filter.py +69 -0
  48. shinestacker/retouch/white_balance_filter.py +46 -14
  49. {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/METADATA +14 -2
  50. shinestacker-1.0.0.dist-info/RECORD +90 -0
  51. shinestacker/app/app_config.py +0 -22
  52. shinestacker/gui/actions_window.py +0 -258
  53. shinestacker/retouch/image_editor.py +0 -201
  54. shinestacker/retouch/image_filters.py +0 -69
  55. shinestacker-0.4.0.dist-info/RECORD +0 -87
  56. {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/WHEEL +0 -0
  57. {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/entry_points.txt +0 -0
  58. {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/licenses/LICENSE +0 -0
  59. {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.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: dict=None, parent=None): # noqa
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: list[ActionConfig] = []
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 QDialog, QVBoxLayout, QCheckBox, QDialogButtonBox
6
- from PySide6.QtCore import Signal, QThread, QTimer
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.undo_label = self.__class__.__name__
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.layer_collection.master_layer is None:
35
+ if self.editor.has_no_master_layer():
28
36
  return
29
37
 
30
- self.editor.layer_collection.copy_master_layer()
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 set_preview(img, request_id, expected_id):
37
- if request_id != expected_id:
38
- return
39
- self.editor.layer_collection.master_layer = img
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
- params = tuple(self.get_params() or ())
57
- worker = self.PreviewWorker(
58
- self.apply,
59
- args=(self.editor.layer_collection.master_layer_copy, *params),
60
- request_id=current_id
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(lambda img, rid: set_preview(img, rid, current_id))
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
- QTimer.singleShot(0, do_preview)
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.layer_collection.master_layer.shape[:2]
143
+ h, w = self.editor.master_layer().shape[:2]
81
144
  except Exception:
82
- h, w = self.editor.layer_collection.master_layer_copy.shape[:2]
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.layer_collection.master_layer_copy,
88
- self.undo_label
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.layer_collection.master_layer_copy, *params)
93
- self.editor.layer_collection.master_layer = final_img
94
- self.editor.layer_collection.copy_master_layer()
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(True)
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
- result = self.func(*self.args, **self.kwargs)
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
- self.finished.emit(result, self.request_id)
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:
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902, R0913, R0917, R0914
2
2
  import numpy as np
3
- from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush
3
+ from PySide6.QtWidgets import QApplication, QLabel
4
+ from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QFont
4
5
  from PySide6.QtCore import Qt, QPoint
5
6
  from .brush_gradient import create_default_brush_gradient
6
7
  from .. config.gui_constants import gui_constants
@@ -18,11 +19,16 @@ class BrushTool:
18
19
  self.opacity_slider = None
19
20
  self.flow_slider = None
20
21
  self._brush_mask_cache = {}
22
+ self.brush_text = None
21
23
 
22
24
  def setup_ui(self, brush, brush_preview, image_viewer, size_slider, hardness_slider,
23
25
  opacity_slider, flow_slider):
24
26
  self.brush = brush
25
27
  self.brush_preview = brush_preview
28
+ self.brush_text = QLabel(brush_preview.parent())
29
+ self.brush_text.setStyleSheet("color: navy; background: transparent;")
30
+ self.brush_text.setAlignment(Qt.AlignLeft | Qt.AlignTop)
31
+ self.brush_text.raise_()
26
32
  self.image_viewer = image_viewer
27
33
  self.size_slider = size_slider
28
34
  self.hardness_slider = hardness_slider
@@ -86,7 +92,7 @@ class BrushTool:
86
92
  pixmap = QPixmap(width, height)
87
93
  pixmap.fill(Qt.transparent)
88
94
  painter = QPainter(pixmap)
89
- painter.setRenderHint(QPainter.Antialiasing)
95
+ painter.setRenderHint(QPainter.TextAntialiasing, True)
90
96
  preview_size = min(self.brush.size, width + 30, height + 30)
91
97
  center_x, center_y = width // 2, height // 2
92
98
  radius = preview_size // 2
@@ -109,21 +115,32 @@ class BrushTool:
109
115
  painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
110
116
  if self.image_viewer.cursor_style == 'preview':
111
117
  painter.setPen(QPen(QColor(0, 0, 160)))
112
- painter.drawText(0, 10, f"Size: {int(self.brush.size)}px")
113
- painter.drawText(0, 25, f"Hardness: {self.brush.hardness}%")
114
- painter.drawText(0, 40, f"Opacity: {self.brush.opacity}%")
115
- painter.drawText(0, 55, f"Flow: {self.brush.flow}%")
118
+ font = QApplication.font()
119
+ painter.setFont(font)
120
+ font.setHintingPreference(QFont.PreferFullHinting)
121
+ painter.setFont(font)
122
+ self.brush_text.setText(
123
+ f"Size: {int(self.brush.size)}px\n"
124
+ f"Hardness: {self.brush.hardness}%\n"
125
+ f"Opacity: {self.brush.opacity}%\n"
126
+ f"Flow: {self.brush.flow}%"
127
+ )
128
+ self.brush_text.adjustSize()
129
+ self.brush_text.move(10, self.brush_preview.height() // 2 + 125)
130
+ self.brush_text.show()
131
+ else:
132
+ self.brush_text.hide()
116
133
  painter.end()
117
134
  self.brush_preview.setPixmap(pixmap)
118
135
  self.image_viewer.update_brush_cursor()
119
136
 
120
137
  def apply_brush_operation(self, master_layer, source_layer, dest_layer, mask_layer,
121
- view_pos, image_viewer):
138
+ view_pos):
122
139
  if master_layer is None or source_layer is None:
123
140
  return False
124
141
  if dest_layer is None:
125
142
  dest_layer = master_layer
126
- scene_pos = image_viewer.mapToScene(view_pos)
143
+ scene_pos = self.image_viewer.mapToScene(view_pos)
127
144
  x_center = int(round(scene_pos.x()))
128
145
  y_center = int(round(scene_pos.y()))
129
146
  radius = int(round(self.brush.size // 2))
@@ -168,6 +185,3 @@ class BrushTool:
168
185
  dest_area[:] = np.clip(
169
186
  master_area * (1 - effective_mask) + source_area * effective_mask, 0,
170
187
  max_px_value).astype(dtype)
171
-
172
- def clear_cache(self):
173
- self._brush_mask_cache.clear()
@@ -1,51 +1,12 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0221
2
- from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QDialogButtonBox
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(BaseFilter):
9
- def __init__(self, editor):
10
- super().__init__(editor)
11
- self.max_range = 500.0
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)