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
@@ -9,10 +9,15 @@ from .brush_gradient import create_brush_gradient
9
9
 
10
10
  class ImageViewer(QGraphicsView):
11
11
  temp_view_requested = Signal(bool)
12
+ brush_operation_started = Signal(QPoint)
13
+ brush_operation_continued = Signal(QPoint)
14
+ brush_operation_ended = Signal()
15
+ brush_size_change_requested = Signal(int) # +1 or -1
12
16
 
13
- def __init__(self, parent=None):
17
+ def __init__(self, layer_collection, parent=None):
14
18
  super().__init__(parent)
15
- self.image_editor = None
19
+ self.display_manager = None
20
+ self.layer_collection = layer_collection
16
21
  self.brush = None
17
22
  self.cursor_style = gui_constants.DEFAULT_CURSOR_STYLE
18
23
  self.scene = QGraphicsScene(self)
@@ -39,8 +44,10 @@ class ImageViewer(QGraphicsView):
39
44
  self.dragging = False
40
45
  self.last_update_time = QTime.currentTime()
41
46
  self.brush_preview = BrushPreviewItem()
47
+ self.layer_collection.add_to(self.brush_preview)
42
48
  self.scene.addItem(self.brush_preview)
43
49
  self.empty = True
50
+ self.allow_cursor_preview = True
44
51
 
45
52
  def set_image(self, qimage):
46
53
  pixmap = QPixmap.fromImage(qimage)
@@ -58,7 +65,6 @@ class ImageViewer(QGraphicsView):
58
65
  self.empty = False
59
66
  self.setFocus()
60
67
  self.activateWindow()
61
- self.brush_preview.layer_collection = self.image_editor.layer_collection
62
68
  self.brush_preview.brush = self.brush
63
69
 
64
70
  def clear_image(self):
@@ -109,7 +115,7 @@ class ImageViewer(QGraphicsView):
109
115
  def mousePressEvent(self, event):
110
116
  if self.empty:
111
117
  return
112
- if event.button() == Qt.LeftButton and self.image_editor.layer_collection.master_layer is not None:
118
+ if event.button() == Qt.LeftButton and self.layer_collection.has_master_layer():
113
119
  if self.space_pressed:
114
120
  self.scrolling = True
115
121
  self.last_mouse_pos = event.position()
@@ -118,7 +124,7 @@ class ImageViewer(QGraphicsView):
118
124
  self.brush_cursor.hide()
119
125
  else:
120
126
  self.last_brush_pos = event.position()
121
- self.image_editor.begin_copy_brush_area(event.position().toPoint())
127
+ self.brush_operation_started.emit(event.position().toPoint())
122
128
  self.dragging = True
123
129
  if self.brush_cursor:
124
130
  self.brush_cursor.show()
@@ -145,7 +151,7 @@ class ImageViewer(QGraphicsView):
145
151
  for i in range(0, n_steps + 1):
146
152
  pos = QPoint(self.last_brush_pos.x() + i * delta_x,
147
153
  self.last_brush_pos.y() + i * delta_y)
148
- self.image_editor.continue_copy_brush_area(pos)
154
+ self.brush_operation_continued.emit(pos)
149
155
  self.last_brush_pos = position
150
156
  self.last_update_time = current_time
151
157
  if self.scrolling and event.buttons() & Qt.LeftButton:
@@ -177,17 +183,14 @@ class ImageViewer(QGraphicsView):
177
183
  self.last_mouse_pos = None
178
184
  elif hasattr(self, 'dragging') and self.dragging:
179
185
  self.dragging = False
180
- self.image_editor.end_copy_brush_area()
186
+ self.brush_operation_ended.emit()
181
187
  super().mouseReleaseEvent(event)
182
188
 
183
189
  def wheelEvent(self, event):
184
190
  if self.empty:
185
191
  return
186
192
  if self.control_pressed:
187
- if event.angleDelta().y() > 0:
188
- self.image_editor.decrease_brush_size()
189
- else:
190
- self.image_editor.increase_brush_size()
193
+ self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
191
194
  else:
192
195
  zoom_in_factor = 1.10
193
196
  zoom_out_factor = 1 / zoom_in_factor
@@ -227,7 +230,7 @@ class ImageViewer(QGraphicsView):
227
230
  center_y = scene_pos.y()
228
231
  radius = size / 2
229
232
  self.brush_cursor.setRect(center_x - radius, center_y - radius, size, size)
230
- allow_cursor_preview = self.image_editor.allow_cursor_preview()
233
+ allow_cursor_preview = self.display_manager.allow_cursor_preview()
231
234
  if self.cursor_style == 'preview' and allow_cursor_preview:
232
235
  self._setup_outline_style()
233
236
  self.brush_cursor.hide()
@@ -235,8 +238,8 @@ class ImageViewer(QGraphicsView):
235
238
  if isinstance(pos, QPointF):
236
239
  scene_pos = pos
237
240
  else:
238
- cursor_pos = self.image_editor.image_viewer.mapFromGlobal(pos)
239
- scene_pos = self.image_editor.image_viewer.mapToScene(cursor_pos)
241
+ cursor_pos = self.mapFromGlobal(pos)
242
+ scene_pos = self.mapToScene(cursor_pos)
240
243
  self.brush_preview.update(scene_pos, int(size))
241
244
  else:
242
245
  self.brush_preview.hide()
@@ -346,3 +349,8 @@ class ImageViewer(QGraphicsView):
346
349
  self.cursor_style = style
347
350
  if self.brush_cursor:
348
351
  self.update_brush_cursor()
352
+
353
+ def position_on_image(self, pos):
354
+ scene_pos = self.mapToScene(pos)
355
+ item_pos = self.pixmap_item.mapFromScene(scene_pos)
356
+ return item_pos
@@ -0,0 +1,208 @@
1
+ import traceback
2
+ import numpy as np
3
+ from PySide6.QtWidgets import QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog, QApplication
4
+ from PySide6.QtGui import QGuiApplication, QCursor
5
+ from PySide6.QtCore import Qt, QObject, QTimer, Signal
6
+ from .file_loader import FileLoader
7
+ from .exif_data import ExifData
8
+ from .io_manager import IOManager
9
+
10
+
11
+ class IOGuiHandler(QObject):
12
+ status_message_requested = Signal(str)
13
+ update_title_requested = Signal()
14
+
15
+ def __init__(self, layer_collection, undo_manager, parent):
16
+ super().__init__(parent)
17
+ self.io_manager = IOManager(layer_collection)
18
+ self.undo_manager = undo_manager
19
+ layer_collection.add_to(self)
20
+ self.loader_thread = None
21
+
22
+ def setup_ui(self, display_manager, image_viewer):
23
+ self.display_manager = display_manager
24
+ self.image_viewer = image_viewer
25
+
26
+ def on_file_loaded(self, stack, labels, master_layer):
27
+ QApplication.restoreOverrideCursor()
28
+ self.loading_timer.stop()
29
+ self.loading_dialog.hide()
30
+ self.set_layer_stack(stack)
31
+ if labels is None:
32
+ self.set_layer_labels([f'Layer {i:03d}' for i in range(len(stack))])
33
+ else:
34
+ self.set_layer_labels(labels)
35
+ self.set_master_layer(master_layer)
36
+ self.modified = False
37
+ self.undo_manager.reset()
38
+ self.blank_layer = np.zeros(master_layer.shape[:2])
39
+ self.display_manager.update_thumbnails()
40
+ self.image_viewer.setup_brush_cursor()
41
+ self.parent().change_layer(0)
42
+ self.image_viewer.reset_zoom()
43
+ self.status_message_requested.emit(f"Loaded: {self.io_manager.current_file_path}")
44
+ self.parent().thumbnail_list.setFocus()
45
+ self.update_title_requested.emit()
46
+
47
+ def on_file_error(self, error_msg):
48
+ QApplication.restoreOverrideCursor()
49
+ self.loading_timer.stop()
50
+ self.loading_dialog.accept()
51
+ self.loading_dialog.deleteLater()
52
+ QMessageBox.critical(self, "Error", error_msg)
53
+ self.status_message_requested.emit(f"Error loading: {self.io_manager.current_file_path}")
54
+
55
+ def open_file(self, file_paths=None):
56
+ if file_paths is None:
57
+ file_paths, _ = QFileDialog.getOpenFileNames(
58
+ self.parent(), "Open Image", "", "Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
59
+ if not file_paths:
60
+ return
61
+ if self.loader_thread and self.loader_thread.isRunning():
62
+ if not self.loader_thread.wait(10000):
63
+ raise RuntimeError("Loading timeout error.")
64
+ if isinstance(file_paths, list) and len(file_paths) > 1:
65
+ self.import_frames_from_files(file_paths)
66
+ return
67
+ path = file_paths[0] if isinstance(file_paths, list) else file_paths
68
+ self.io_manager.current_file_path = path
69
+ QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
70
+ self.loading_dialog = QDialog(self.parent())
71
+ self.loading_dialog.setWindowTitle("Loading")
72
+ self.loading_dialog.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
73
+ self.loading_dialog.setModal(True)
74
+ layout = QVBoxLayout()
75
+ layout.addWidget(QLabel("File loading..."))
76
+ self.loading_dialog.setLayout(layout)
77
+ self.loading_timer = QTimer()
78
+ self.loading_timer.setSingleShot(True)
79
+ self.loading_timer.timeout.connect(self.loading_dialog.show)
80
+ self.loading_timer.start(100)
81
+ self.loader_thread = FileLoader(path)
82
+ self.loader_thread.finished.connect(self.on_file_loaded)
83
+ self.loader_thread.error.connect(self.on_file_error)
84
+ self.loader_thread.start()
85
+
86
+ def import_frames(self):
87
+ file_paths, _ = QFileDialog.getOpenFileNames(self.parent(), "Select frames", "",
88
+ "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
89
+ if file_paths:
90
+ self.import_frames_from_files(file_paths)
91
+ self.status_message_requested.emit("Imported selected frames")
92
+
93
+ def import_frames_from_files(self, file_paths):
94
+ try:
95
+ stack, labels, master = self.io_manager.import_frames(file_paths)
96
+ except Exception as e:
97
+ msg = QMessageBox()
98
+ msg.setIcon(QMessageBox.Critical)
99
+ msg.setWindowTitle("Import error")
100
+ msg.setText(str(e))
101
+ msg.exec()
102
+ return
103
+ if self.layer_stack() is None and len(stack) > 0:
104
+ self.set_layer_stack(np.array(stack))
105
+ self.set_layer_labels(labels)
106
+ self.set_master_layer(master)
107
+ self.blank_layer = np.zeros(master.shape[:2])
108
+ else:
109
+ for img, label in zip(stack, labels):
110
+ self.add_layer_label(label)
111
+ self.add_layer(img)
112
+ self.parent().mark_as_modified()
113
+ self.parent().change_layer(0)
114
+ self.image_viewer.reset_zoom()
115
+ self.parent().thumbnail_list.setFocus()
116
+ self.display_manager.update_thumbnails()
117
+
118
+ def save_file(self):
119
+ if self.parent().save_master_only.isChecked():
120
+ self.save_master()
121
+ else:
122
+ self.save_multilayer()
123
+
124
+ def save_file_as(self):
125
+ if self.parent().save_master_only.isChecked():
126
+ self.save_master_as()
127
+ else:
128
+ self.save_multilayer_as()
129
+
130
+ def save_multilayer(self):
131
+ if self.layer_stack() is None:
132
+ return
133
+ if self.io_manager.current_file_path != '':
134
+ extension = self.io_manager.current_file_path.split('.')[-1]
135
+ if extension in ['tif', 'tiff']:
136
+ self.save_multilayer_to_path(self.io_manager.current_file_path)
137
+ return
138
+
139
+ def save_multilayer_as(self):
140
+ if self.layer_stack() is None:
141
+ return
142
+ path, _ = QFileDialog.getSaveFileName(self.parent(), "Save Image", "",
143
+ "TIFF Files (*.tif *.tiff);;All Files (*)")
144
+ if path:
145
+ if not path.lower().endswith(('.tif', '.tiff')):
146
+ path += '.tif'
147
+ self.save_multilayer_to_path(path)
148
+
149
+ def save_multilayer_to_path(self, path):
150
+ try:
151
+ self.io_manager.save_multilayer(path)
152
+ self.io_manager.current_file_path = path
153
+ self.modified = False
154
+ self.update_title_requested.emit()
155
+ self.status_message_requested.emit(f"Saved multilayer to: {path}")
156
+ except Exception as e:
157
+ traceback.print_tb(e.__traceback__)
158
+ QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
159
+
160
+ def save_master(self):
161
+ if self.master_layer() is None:
162
+ return
163
+ if self.io_manager.current_file_path != '':
164
+ self.save_master_to_path(self.io_manager.current_file_path)
165
+ return
166
+ self.save_master_as()
167
+
168
+ def save_master_as(self):
169
+ if self.layer_stack() is None:
170
+ return
171
+ path, _ = QFileDialog.getSaveFileName(self.parent(), "Save Image", "",
172
+ "TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
173
+ if path:
174
+ self.save_master_to_path(path)
175
+
176
+ def save_master_to_path(self, path):
177
+ try:
178
+ self.io_manager.save_master(path)
179
+ self.io_manager.current_file_path = path
180
+ self.modified = False
181
+ self.update_title_requested.emit()
182
+ self.status_message_requested.emit(f"Saved master layer to: {path}")
183
+ except Exception as e:
184
+ traceback.print_tb(e.__traceback__)
185
+ QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
186
+
187
+ def select_exif_path(self):
188
+ path, _ = QFileDialog.getOpenFileName(None, "Select file with exif data")
189
+ if path:
190
+ self.io_manager.set_exif_data(path)
191
+ self.status_message_requested.emit(f"EXIF data extracted from {path}.")
192
+ self._exif_dialog = ExifData(self.io_manager.exif_data, self.parent())
193
+ self._exif_dialog.exec()
194
+
195
+ def close_file(self):
196
+ if self.parent()._check_unsaved_changes():
197
+ self.set_master_layer(None)
198
+ self.blank_layer = None
199
+ self.current_stack = None
200
+ self.layer_collection.reset()
201
+ self.io_manager.current_file_path = ''
202
+ self.modified = False
203
+ self.undo_manager.reset()
204
+ self.image_viewer.clear_image()
205
+ self.display_manager.thumbnail_list.clear()
206
+ self.display_manager.update_thumbnails()
207
+ self.update_title_requested.emit()
208
+ self.status_message_requested.emit("File closed")
@@ -1,5 +1,5 @@
1
+ # pylint: disable=E1101, C0114, C0115, C0116
1
2
  import cv2
2
- from .. core.exceptions import ShapeError, BitDepthError
3
3
  from .. algorithms.utils import read_img, validate_image, get_img_metadata
4
4
  from .. algorithms.exif import get_exif, write_image_with_exif_data
5
5
  from .. algorithms.multilayer import write_multilayer_tiff_from_images
@@ -7,7 +7,7 @@ from .. algorithms.multilayer import write_multilayer_tiff_from_images
7
7
 
8
8
  class IOManager:
9
9
  def __init__(self, layer_collection):
10
- self.layer_collection = layer_collection
10
+ layer_collection.add_to(self)
11
11
  self.current_file_path = ''
12
12
  self.exif_path = ''
13
13
  self.exif_data = None
@@ -16,7 +16,7 @@ class IOManager:
16
16
  stack = []
17
17
  labels = []
18
18
  master = None
19
- shape, dtype = get_img_metadata(self.layer_collection.master_layer)
19
+ shape, dtype = get_img_metadata(self.master_layer())
20
20
  for path in file_paths:
21
21
  try:
22
22
  label = path.split("/")[-1].split(".")[0]
@@ -34,22 +34,18 @@ class IOManager:
34
34
  stack.append(img)
35
35
  if master is None:
36
36
  master = img.copy()
37
- except ShapeError as e:
38
- raise ShapeError(f"All files must have the same shape.\n{str(e)}")
39
- except BitDepthError as e:
40
- raise BitDepthError(f"All files must have the same bit depth.\n{str(e)}")
41
37
  except Exception as e:
42
- raise RuntimeError(f"Error loading file: {path}.\n{str(e)}")
38
+ raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
43
39
  return stack, labels, master
44
40
 
45
41
  def save_multilayer(self, path):
46
- master_layer = {'Master': self.layer_collection.master_layer}
47
- individual_layers = {label: image for label, image in zip(
48
- self.layer_collection.layer_labels, self.layer_collection.layer_stack)}
49
- write_multilayer_tiff_from_images({**master_layer, **individual_layers}, path, exif_path=self.exif_path)
42
+ master_layer = {'Master': self.master_layer()}
43
+ individual_layers = dict(zip(self.layer_labels(), self.layer_stack()))
44
+ write_multilayer_tiff_from_images({**master_layer, **individual_layers},
45
+ path, exif_path=self.exif_path)
50
46
 
51
47
  def save_master(self, path):
52
- img = cv2.cvtColor(self.layer_collection.master_layer, cv2.COLOR_RGB2BGR)
48
+ img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
53
49
  write_image_with_exif_data(self.exif_data, img, path)
54
50
 
55
51
  def set_exif_data(self, path):
@@ -2,16 +2,68 @@ import numpy as np
2
2
 
3
3
 
4
4
  class LayerCollection:
5
+ def add_to(self, obj):
6
+ obj.layer_collection = self
7
+ obj.master_layer = lambda: obj.layer_collection.master_layer
8
+ obj.current_layer = lambda: obj.layer_collection.current_layer()
9
+ obj.layer_stack = lambda: obj.layer_collection.layer_stack
10
+ obj.layer_labels = lambda: obj.layer_collection.layer_labels
11
+ obj.set_layer_label = lambda i, val: obj.layer_collection.set_layer_label(i, val)
12
+ obj.set_layer_labels = lambda labels: obj.layer_collection.set_layer_labels(labels)
13
+ obj.current_layer_idx = lambda: obj.layer_collection.current_layer_idx
14
+ obj.has_no_master_layer = lambda: obj.layer_collection.has_no_master_layer()
15
+ obj.has_master_layer = lambda: obj.layer_collection.has_master_layer()
16
+ obj.set_layer_stack = lambda stk: obj.layer_collection.set_layer_stack(stk)
17
+ obj.set_master_layer = lambda img: obj.layer_collection.set_master_layer(img)
18
+ obj.add_layer_label = lambda label: obj.layer_collection.add_layer_label(label)
19
+ obj.add_layer = lambda img: obj.layer_collection.add_layer(img)
20
+ obj.master_layer_copy = lambda: obj.layer_collection.master_layer_copy
21
+ obj.copy_master_layer = lambda: obj.layer_collection.copy_master_layer()
22
+ obj.set_current_layer_idx = lambda idx: obj.layer_collection.set_current_layer_idx(idx)
23
+ obj.sort_layers = lambda order: obj.layer_collection.sort_layers(order)
24
+ obj.number_of_layers = lambda: obj.layer_collection.number_of_layers()
25
+ obj.valid_current_layer_idx = lambda: obj.layer_collection.valid_current_layer_idx()
26
+
5
27
  def __init__(self):
28
+ self.reset()
29
+
30
+ def reset(self):
6
31
  self.master_layer = None
7
32
  self.master_layer_copy = None
8
33
  self.layer_stack = None
9
- self.layer_labels = None
34
+ self.layer_labels = []
10
35
  self.current_layer_idx = 0
11
36
 
37
+ def has_master_layer(self):
38
+ return self.master_layer is not None
39
+
40
+ def has_no_master_layer(self):
41
+ return self.master_layer is None
42
+
43
+ def has_master_layer_copy(self):
44
+ return self.master_layer_copy is not None
45
+
46
+ def has_no_master_layer_copy(self):
47
+ return self.master_layer_copy is None
48
+
12
49
  def number_of_layers(self):
13
50
  return len(self.layer_stack)
14
51
 
52
+ def layer_label(self, i):
53
+ return self.layer_labels[i]
54
+
55
+ def set_layer_label(self, i, val):
56
+ self.layer_labels[i] = val
57
+
58
+ def set_layer_labels(self, labels):
59
+ self.layer_labels = labels
60
+
61
+ def set_layer_stack(self, stk):
62
+ self.layer_stack = stk
63
+
64
+ def set_current_layer_idx(self, idx):
65
+ self.current_layer_idx = idx
66
+
15
67
  def valid_current_layer_idx(self):
16
68
  return 0 <= self.current_layer_idx < self.number_of_layers()
17
69
 
@@ -20,9 +72,21 @@ class LayerCollection:
20
72
  return self.layer_stack[self.current_layer_idx]
21
73
  return None
22
74
 
75
+ def set_master_layer(self, img):
76
+ self.master_layer = img
77
+
23
78
  def copy_master_layer(self):
24
79
  self.master_layer_copy = self.master_layer.copy()
25
80
 
81
+ def add_layer_label(self, label):
82
+ if self.layer_labels is None:
83
+ self.layer_labels = [label]
84
+ else:
85
+ self.layer_labels.append(label)
86
+
87
+ def add_layer(self, img):
88
+ self.layer_stack = np.append(self.layer_stack, [img], axis=0)
89
+
26
90
  def sort_layers(self, order):
27
91
  master_index = -1
28
92
  master_label = None
@@ -0,0 +1,84 @@
1
+ from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QCheckBox, QDialogButtonBox
2
+ from PySide6.QtCore import Qt, QTimer
3
+ from .filter_base import BaseFilter
4
+
5
+
6
+ class UnsharpMaskFilter(BaseFilter):
7
+ def __init__(self, editor):
8
+ super().__init__(editor)
9
+ self.max_range = 500.0
10
+ self.max_radius = 4.0
11
+ self.max_amount = 3.0
12
+ self.max_threshold = 64.0
13
+ self.initial_radius = 1.0
14
+ self.initial_amount = 0.5
15
+ self.initial_threshold = 0.0
16
+ self.radius_slider = None
17
+ self.amount_slider = None
18
+ self.threshold_slider = None
19
+
20
+ def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
21
+ dlg.setWindowTitle("Unsharp Mask")
22
+ dlg.setMinimumWidth(600)
23
+ params = {
24
+ "Radius": (self.max_radius, self.initial_radius, "{:.2f}"),
25
+ "Amount": (self.max_amount, self.initial_amount, "{:.1%}"),
26
+ "Threshold": (self.max_threshold, self.initial_threshold, "{:.2f}")
27
+ }
28
+ value_labels = {}
29
+ for name, (max_val, init_val, fmt) in params.items():
30
+ param_layout = QHBoxLayout()
31
+ name_label = QLabel(f"{name}:")
32
+ param_layout.addWidget(name_label)
33
+ slider = QSlider(Qt.Horizontal)
34
+ slider.setRange(0, self.max_range)
35
+ slider.setValue(int(init_val / max_val * self.max_range))
36
+ param_layout.addWidget(slider)
37
+ value_label = QLabel(fmt.format(init_val))
38
+ param_layout.addWidget(value_label)
39
+ layout.addLayout(param_layout)
40
+ if name == "Radius":
41
+ self.radius_slider = slider
42
+ elif name == "Amount":
43
+ self.amount_slider = slider
44
+ elif name == "Threshold":
45
+ self.threshold_slider = slider
46
+ value_labels[name] = value_label
47
+
48
+ preview_check = QCheckBox("Preview")
49
+ preview_check.setChecked(True)
50
+ layout.addWidget(preview_check)
51
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
52
+ layout.addWidget(button_box)
53
+ preview_timer = QTimer()
54
+ preview_timer.setSingleShot(True)
55
+ preview_timer.setInterval(200)
56
+
57
+ def update_value(name, value, max_val, fmt):
58
+ float_value = max_val * value / self.max_range
59
+ value_labels[name].setText(fmt.format(float_value))
60
+ if preview_check.isChecked():
61
+ preview_timer.start()
62
+
63
+ self.radius_slider.valueChanged.connect(
64
+ lambda v: update_value("Radius", v, self.max_radius, params["Radius"][2]))
65
+ self.amount_slider.valueChanged.connect(
66
+ lambda v: update_value("Amount", v, self.max_amount, params["Amount"][2]))
67
+ self.threshold_slider.valueChanged.connect(
68
+ lambda v: update_value("Threshold", v, self.max_threshold, params["Threshold"][2]))
69
+ preview_timer.timeout.connect(do_preview)
70
+ self.editor.connect_preview_toggle(preview_check, do_preview, restore_original)
71
+ button_box.accepted.connect(dlg.accept)
72
+ button_box.rejected.connect(dlg.reject)
73
+ QTimer.singleShot(0, do_preview)
74
+
75
+ def get_params(self):
76
+ return (
77
+ max(0.01, self.max_radius * self.radius_slider.value() / self.max_range),
78
+ self.max_amount * self.amount_slider.value() / self.max_range,
79
+ self.max_threshold * self.threshold_slider.value() / self.max_range
80
+ )
81
+
82
+ def apply(self, image, radius, amount, threshold):
83
+ from .. algorithms.sharpen import unsharp_mask
84
+ return unsharp_mask(image, radius, amount, threshold)
@@ -0,0 +1,111 @@
1
+ from PySide6.QtWidgets import (QHBoxLayout,
2
+ QPushButton, QFrame, QVBoxLayout, QLabel, QDialog, QApplication, QSlider,
3
+ QCheckBox, QDialogButtonBox)
4
+ from PySide6.QtCore import Qt, QTimer
5
+ from PySide6.QtGui import QCursor
6
+ from .filter_base import BaseFilter
7
+
8
+
9
+ class WhiteBalanceFilter(BaseFilter):
10
+ def __init__(self, editor):
11
+ super().__init__(editor)
12
+ self.max_range = 255
13
+ self.initial_val = (128, 128, 128)
14
+ self.sliders = {}
15
+ self.value_labels = {}
16
+ self.color_preview = None
17
+ self.preview_timer = None
18
+
19
+ def setup_ui(self, dlg, layout, do_preview, restore_original, init_val=None):
20
+ if init_val:
21
+ self.initial_val = init_val
22
+ dlg.setWindowTitle("White Balance")
23
+ dlg.setMinimumWidth(600)
24
+ row_layout = QHBoxLayout()
25
+ self.color_preview = QFrame()
26
+ self.color_preview.setFixedHeight(80)
27
+ self.color_preview.setFixedWidth(80)
28
+ self.color_preview.setStyleSheet(f"background-color: rgb{self.initial_val};")
29
+ row_layout.addWidget(self.color_preview)
30
+ sliders_layout = QVBoxLayout()
31
+ for name in ("R", "G", "B"):
32
+ row = QHBoxLayout()
33
+ label = QLabel(f"{name}:")
34
+ row.addWidget(label)
35
+ slider = QSlider(Qt.Horizontal)
36
+ slider.setRange(0, self.max_range)
37
+ slider.setValue(self.initial_val[["R", "G", "B"].index(name)])
38
+ row.addWidget(slider)
39
+ val_label = QLabel(str(self.initial_val[["R", "G", "B"].index(name)]))
40
+ row.addWidget(val_label)
41
+ sliders_layout.addLayout(row)
42
+ self.sliders[name] = slider
43
+ self.value_labels[name] = val_label
44
+ row_layout.addLayout(sliders_layout)
45
+ layout.addLayout(row_layout)
46
+ pick_button = QPushButton("Pick Color")
47
+ layout.addWidget(pick_button)
48
+ preview_check = QCheckBox("Preview")
49
+ preview_check.setChecked(True)
50
+ layout.addWidget(preview_check)
51
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel)
52
+ layout.addWidget(button_box)
53
+ self.preview_timer = QTimer()
54
+ self.preview_timer.setSingleShot(True)
55
+ self.preview_timer.setInterval(200)
56
+ for slider in self.sliders.values():
57
+ slider.valueChanged.connect(self.on_slider_change)
58
+ self.preview_timer.timeout.connect(do_preview)
59
+ self.editor.connect_preview_toggle(preview_check, do_preview, restore_original)
60
+ pick_button.clicked.connect(self.start_color_pick)
61
+ button_box.accepted.connect(dlg.accept)
62
+ button_box.rejected.connect(dlg.reject)
63
+ button_box.button(QDialogButtonBox.Reset).clicked.connect(self.reset_rgb)
64
+ QTimer.singleShot(0, do_preview)
65
+
66
+ def on_slider_change(self):
67
+ for name in ("R", "G", "B"):
68
+ self.value_labels[name].setText(str(self.sliders[name].value()))
69
+ rgb = tuple(self.sliders[n].value() for n in ("R", "G", "B"))
70
+ self.color_preview.setStyleSheet(f"background-color: rgb{rgb};")
71
+ if self.preview_timer:
72
+ self.preview_timer.start()
73
+
74
+ def start_color_pick(self):
75
+ for widget in QApplication.topLevelWidgets():
76
+ if isinstance(widget, QDialog) and widget.isVisible():
77
+ widget.hide()
78
+ widget.reject()
79
+ break
80
+ self.editor.image_viewer.set_cursor_style('outline')
81
+ if self.editor.image_viewer.brush_cursor:
82
+ self.editor.image_viewer.brush_cursor.hide()
83
+ self.editor.brush_preview.hide()
84
+ QApplication.setOverrideCursor(QCursor(Qt.CrossCursor))
85
+ self.editor.image_viewer.setCursor(Qt.CrossCursor)
86
+ self.original_mouse_press = self.editor.image_viewer.mousePressEvent
87
+ self.editor.image_viewer.mousePressEvent = self.pick_color_from_click
88
+
89
+ def pick_color_from_click(self, event):
90
+ if event.button() == Qt.LeftButton:
91
+ pos = event.pos()
92
+ bgr = self.editor.get_pixel_color_at(pos, radius=int(self.editor.brush.size))
93
+ rgb = (bgr[2], bgr[1], bgr[0])
94
+ QApplication.restoreOverrideCursor()
95
+ self.editor.image_viewer.unsetCursor()
96
+ self.editor.image_viewer.mousePressEvent = self.original_mouse_press
97
+ self.editor.image_viewer.brush_cursor.show()
98
+ self.editor.brush_preview.show()
99
+ new_filter = WhiteBalanceFilter(self.editor)
100
+ new_filter.run_with_preview(init_val=rgb)
101
+
102
+ def reset_rgb(self):
103
+ for name, slider in self.sliders.items():
104
+ slider.setValue(self.initial_val[["R", "G", "B"].index(name)])
105
+
106
+ def get_params(self):
107
+ return tuple(self.sliders[n].value() for n in ("R", "G", "B"))
108
+
109
+ def apply(self, image, r, g, b):
110
+ from .. algorithms.white_balance import white_balance_from_rgb
111
+ return white_balance_from_rgb(image, (r, g, b))