shinestacker 0.3.0__py3-none-any.whl → 0.3.2__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 (43) 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/algorithms/utils.py +4 -0
  6. shinestacker/algorithms/white_balance.py +13 -0
  7. shinestacker/app/open_frames.py +6 -4
  8. shinestacker/config/__init__.py +2 -1
  9. shinestacker/config/config.py +1 -0
  10. shinestacker/config/constants.py +1 -0
  11. shinestacker/config/gui_constants.py +1 -0
  12. shinestacker/core/__init__.py +4 -3
  13. shinestacker/core/colors.py +1 -0
  14. shinestacker/core/core_utils.py +6 -6
  15. shinestacker/core/exceptions.py +1 -0
  16. shinestacker/core/framework.py +2 -1
  17. shinestacker/gui/action_config.py +47 -42
  18. shinestacker/gui/actions_window.py +8 -5
  19. shinestacker/gui/new_project.py +1 -0
  20. shinestacker/retouch/brush_gradient.py +20 -0
  21. shinestacker/retouch/brush_preview.py +10 -14
  22. shinestacker/retouch/brush_tool.py +164 -0
  23. shinestacker/retouch/denoise_filter.py +56 -0
  24. shinestacker/retouch/display_manager.py +177 -0
  25. shinestacker/retouch/exif_data.py +2 -1
  26. shinestacker/retouch/filter_base.py +114 -0
  27. shinestacker/retouch/filter_manager.py +14 -0
  28. shinestacker/retouch/image_editor.py +108 -543
  29. shinestacker/retouch/image_editor_ui.py +42 -75
  30. shinestacker/retouch/image_filters.py +27 -423
  31. shinestacker/retouch/image_viewer.py +31 -31
  32. shinestacker/retouch/io_gui_handler.py +208 -0
  33. shinestacker/retouch/io_manager.py +53 -0
  34. shinestacker/retouch/layer_collection.py +118 -0
  35. shinestacker/retouch/unsharp_mask_filter.py +84 -0
  36. shinestacker/retouch/white_balance_filter.py +111 -0
  37. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/METADATA +3 -2
  38. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/RECORD +42 -31
  39. shinestacker/retouch/brush_controller.py +0 -57
  40. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/WHEEL +0 -0
  41. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/entry_points.txt +0 -0
  42. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/licenses/LICENSE +0 -0
  43. {shinestacker-0.3.0.dist-info → shinestacker-0.3.2.dist-info}/top_level.txt +0 -0
@@ -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")
@@ -0,0 +1,53 @@
1
+ # pylint: disable=E1101, C0114, C0115, C0116
2
+ import cv2
3
+ from .. algorithms.utils import read_img, validate_image, get_img_metadata
4
+ from .. algorithms.exif import get_exif, write_image_with_exif_data
5
+ from .. algorithms.multilayer import write_multilayer_tiff_from_images
6
+
7
+
8
+ class IOManager:
9
+ def __init__(self, layer_collection):
10
+ layer_collection.add_to(self)
11
+ self.current_file_path = ''
12
+ self.exif_path = ''
13
+ self.exif_data = None
14
+
15
+ def import_frames(self, file_paths):
16
+ stack = []
17
+ labels = []
18
+ master = None
19
+ shape, dtype = get_img_metadata(self.master_layer())
20
+ for path in file_paths:
21
+ try:
22
+ label = path.split("/")[-1].split(".")[0]
23
+ img = cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)
24
+ if shape is not None and dtype is not None:
25
+ validate_image(img, shape, dtype)
26
+ else:
27
+ shape, dtype = get_img_metadata(img)
28
+ label_x = label
29
+ i = 0
30
+ while label_x in labels:
31
+ i += 1
32
+ label_x = f"{label} ({i})"
33
+ labels.append(label_x)
34
+ stack.append(img)
35
+ if master is None:
36
+ master = img.copy()
37
+ except Exception as e:
38
+ raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
39
+ return stack, labels, master
40
+
41
+ def save_multilayer(self, 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)
46
+
47
+ def save_master(self, path):
48
+ img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
49
+ write_image_with_exif_data(self.exif_data, img, path)
50
+
51
+ def set_exif_data(self, path):
52
+ self.exif_path = path
53
+ self.exif_data = get_exif(path)
@@ -0,0 +1,118 @@
1
+ import numpy as np
2
+
3
+
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
+
27
+ def __init__(self):
28
+ self.reset()
29
+
30
+ def reset(self):
31
+ self.master_layer = None
32
+ self.master_layer_copy = None
33
+ self.layer_stack = None
34
+ self.layer_labels = []
35
+ self.current_layer_idx = 0
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
+
49
+ def number_of_layers(self):
50
+ return len(self.layer_stack)
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
+
67
+ def valid_current_layer_idx(self):
68
+ return 0 <= self.current_layer_idx < self.number_of_layers()
69
+
70
+ def current_layer(self):
71
+ if self.layer_stack is not None and self.valid_current_layer_idx():
72
+ return self.layer_stack[self.current_layer_idx]
73
+ return None
74
+
75
+ def set_master_layer(self, img):
76
+ self.master_layer = img
77
+
78
+ def copy_master_layer(self):
79
+ self.master_layer_copy = self.master_layer.copy()
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
+
90
+ def sort_layers(self, order):
91
+ master_index = -1
92
+ master_label = None
93
+ master_layer = None
94
+ for i, label in enumerate(self.layer_labels):
95
+ if label.lower() == "master":
96
+ master_index = i
97
+ master_label = self.layer_labels.pop(i)
98
+ master_layer = self.layer_stack[i]
99
+ self.layer_stack = np.delete(self.layer_stack, i, axis=0)
100
+ break
101
+ if order == 'asc':
102
+ self.sorted_indices = sorted(range(len(self.layer_labels)),
103
+ key=lambda i: self.layer_labels[i].lower())
104
+ elif order == 'desc':
105
+ self.sorted_indices = sorted(range(len(self.layer_labels)),
106
+ key=lambda i: self.layer_labels[i].lower(),
107
+ reverse=True)
108
+ else:
109
+ raise ValueError(f"Invalid sorting order: {order}")
110
+ self.layer_labels = [self.layer_labels[i] for i in self.sorted_indices]
111
+ self.layer_stack = self.layer_stack[self.sorted_indices]
112
+ if master_index != -1:
113
+ self.layer_labels.insert(0, master_label)
114
+ self.layer_stack = np.insert(self.layer_stack, 0, master_layer, axis=0)
115
+ self.master_layer = master_layer.copy()
116
+ self.master_layer.setflags(write=True)
117
+ if self.current_layer_idx >= self.number_of_layers():
118
+ self.current_layer_idx = self.number_of_layers() - 1
@@ -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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -41,6 +41,7 @@ Dynamic: license-file
41
41
 
42
42
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400" referrerpolicy="no-referrer">
43
43
 
44
+ <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coins_stack.jpg' width="400" referrerpolicy="no-referrer">
44
45
  > **Focus stacking** for microscopy, macro photography, and computational imaging
45
46
 
46
47
  ## Key Features
@@ -58,7 +59,7 @@ The GUI has two main working areas:
58
59
 
59
60
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-project-run.png' width="600" referrerpolicy="no-referrer">
60
61
 
61
- * *Retouch*: interactive final refinements to final blended image from individual frames.
62
+ * *Retouch*: select interactively details from individual frames and apply final filters to the blended image.
62
63
 
63
64
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
64
65