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.
- 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 +101 -12
- 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 +10 -9
- 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 +276 -367
- shinestacker/gui/menu_manager.py +236 -0
- shinestacker/gui/new_project.py +75 -20
- 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 +25 -11
- shinestacker/retouch/denoise_filter.py +5 -44
- shinestacker/retouch/display_manager.py +57 -20
- 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 +365 -49
- shinestacker/retouch/image_viewer.py +34 -11
- shinestacker/retouch/io_gui_handler.py +96 -43
- shinestacker/retouch/io_manager.py +23 -7
- 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.4.0.dist-info → shinestacker-1.0.0.dist-info}/METADATA +14 -2
- shinestacker-1.0.0.dist-info/RECORD +90 -0
- shinestacker/app/app_config.py +0 -22
- shinestacker/gui/actions_window.py +0 -258
- shinestacker/retouch/image_editor.py +0 -201
- shinestacker/retouch/image_filters.py +0 -69
- shinestacker-0.4.0.dist-info/RECORD +0 -87
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.4.0.dist-info → shinestacker-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -7,13 +7,15 @@ from PySide6.QtGui import QGuiApplication, QCursor
|
|
|
7
7
|
from PySide6.QtCore import Qt, QObject, QTimer, Signal
|
|
8
8
|
from .file_loader import FileLoader
|
|
9
9
|
from .exif_data import ExifData
|
|
10
|
-
from .io_manager import IOManager
|
|
10
|
+
from .io_manager import IOManager, FileMultilayerSaver
|
|
11
11
|
from .layer_collection import LayerCollectionHandler
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
15
15
|
status_message_requested = Signal(str)
|
|
16
16
|
update_title_requested = Signal()
|
|
17
|
+
mark_as_modified_requested = Signal(bool)
|
|
18
|
+
change_layer_requested = Signal(int)
|
|
17
19
|
|
|
18
20
|
def __init__(self, layer_collection, undo_manager, parent):
|
|
19
21
|
QObject.__init__(self, parent)
|
|
@@ -28,7 +30,15 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
28
30
|
self.loading_dialog = None
|
|
29
31
|
self.loading_timer = None
|
|
30
32
|
self.exif_dialog = None
|
|
31
|
-
self.
|
|
33
|
+
self.saver_thread = None
|
|
34
|
+
self.saving_dialog = None
|
|
35
|
+
self.saving_timer = None
|
|
36
|
+
self.current_file_path_master = ''
|
|
37
|
+
self.current_file_path_multi = ''
|
|
38
|
+
|
|
39
|
+
def current_file_path(self):
|
|
40
|
+
return self.current_file_path_master if self.save_master_only.isChecked() \
|
|
41
|
+
else self.current_file_path_multi
|
|
32
42
|
|
|
33
43
|
def setup_ui(self, display_manager, image_viewer):
|
|
34
44
|
self.display_manager = display_manager
|
|
@@ -44,16 +54,10 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
44
54
|
else:
|
|
45
55
|
self.set_layer_labels(labels)
|
|
46
56
|
self.set_master_layer(master_layer)
|
|
47
|
-
self.parent().modified = False
|
|
48
57
|
self.undo_manager.reset()
|
|
49
58
|
self.blank_layer = np.zeros(master_layer.shape[:2])
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
self.parent().change_layer(0)
|
|
53
|
-
self.image_viewer.reset_zoom()
|
|
54
|
-
self.status_message_requested.emit(f"Loaded: {self.current_file_path}")
|
|
55
|
-
self.parent().thumbnail_list.setFocus()
|
|
56
|
-
self.update_title_requested.emit()
|
|
59
|
+
self.finish_loading_setup(stack, None, master_layer,
|
|
60
|
+
f"Loaded: {self.current_file_path()}")
|
|
57
61
|
|
|
58
62
|
def on_file_error(self, error_msg):
|
|
59
63
|
QApplication.restoreOverrideCursor()
|
|
@@ -61,7 +65,25 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
61
65
|
self.loading_dialog.accept()
|
|
62
66
|
self.loading_dialog.deleteLater()
|
|
63
67
|
QMessageBox.critical(self.parent(), "Error", error_msg)
|
|
64
|
-
self.
|
|
68
|
+
self.current_file_path_master = ''
|
|
69
|
+
self.current_file_path_multi = ''
|
|
70
|
+
self.status_message_requested.emit(f"Error loading: {self.current_file_path()}")
|
|
71
|
+
|
|
72
|
+
def on_multilayer_save_success(self):
|
|
73
|
+
QApplication.restoreOverrideCursor()
|
|
74
|
+
self.saving_timer.stop()
|
|
75
|
+
self.saving_dialog.hide()
|
|
76
|
+
self.saving_dialog.deleteLater()
|
|
77
|
+
self.mark_as_modified_requested.emit(False)
|
|
78
|
+
self.update_title_requested.emit()
|
|
79
|
+
self.status_message_requested.emit(f"Saved multilayer to: {self.current_file_path_multi}")
|
|
80
|
+
|
|
81
|
+
def on_multilayer_save_error(self, error_msg):
|
|
82
|
+
QApplication.restoreOverrideCursor()
|
|
83
|
+
self.saving_timer.stop()
|
|
84
|
+
self.saving_dialog.hide()
|
|
85
|
+
self.saving_dialog.deleteLater()
|
|
86
|
+
QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {error_msg}")
|
|
65
87
|
|
|
66
88
|
def open_file(self, file_paths=None):
|
|
67
89
|
if file_paths is None:
|
|
@@ -77,7 +99,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
77
99
|
self.import_frames_from_files(file_paths)
|
|
78
100
|
return
|
|
79
101
|
path = file_paths[0] if isinstance(file_paths, list) else file_paths
|
|
80
|
-
self.
|
|
102
|
+
self.current_file_path_master = os.path.abspath(path)
|
|
103
|
+
self.current_file_path_multi = os.path.abspath(path)
|
|
81
104
|
QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
|
|
82
105
|
self.loading_dialog = QDialog(self.parent())
|
|
83
106
|
self.loading_dialog.setWindowTitle("Loading")
|
|
@@ -113,29 +136,39 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
113
136
|
msg.setText(str(e))
|
|
114
137
|
msg.exec()
|
|
115
138
|
return
|
|
139
|
+
self.finish_loading_setup(stack, labels, master, "Selected frames imported")
|
|
140
|
+
|
|
141
|
+
def finish_loading_setup(self, stack, labels, master, message):
|
|
116
142
|
if self.layer_stack() is None and len(stack) > 0:
|
|
117
143
|
self.set_layer_stack(np.array(stack))
|
|
118
|
-
|
|
144
|
+
if labels is None:
|
|
145
|
+
labels = self.layer_labels()
|
|
146
|
+
else:
|
|
147
|
+
self.set_layer_labels(labels)
|
|
119
148
|
self.set_master_layer(master)
|
|
120
149
|
self.blank_layer = np.zeros(master.shape[:2])
|
|
121
150
|
else:
|
|
151
|
+
if labels is None:
|
|
152
|
+
labels = self.layer_labels()
|
|
122
153
|
for img, label in zip(stack, labels):
|
|
123
154
|
self.add_layer_label(label)
|
|
124
155
|
self.add_layer(img)
|
|
125
|
-
self.parent().mark_as_modified()
|
|
126
|
-
self.parent().change_layer(0)
|
|
127
|
-
self.image_viewer.reset_zoom()
|
|
128
|
-
self.parent().thumbnail_list.setFocus()
|
|
129
156
|
self.display_manager.update_thumbnails()
|
|
157
|
+
self.mark_as_modified_requested.emit(True)
|
|
158
|
+
self.change_layer_requested.emit(0)
|
|
159
|
+
self.image_viewer.setup_brush_cursor()
|
|
160
|
+
self.image_viewer.reset_zoom()
|
|
161
|
+
self.status_message_requested.emit(message)
|
|
162
|
+
self.update_title_requested.emit()
|
|
130
163
|
|
|
131
164
|
def save_file(self):
|
|
132
|
-
if self.
|
|
165
|
+
if self.save_master_only.isChecked():
|
|
133
166
|
self.save_master()
|
|
134
167
|
else:
|
|
135
168
|
self.save_multilayer()
|
|
136
169
|
|
|
137
170
|
def save_file_as(self):
|
|
138
|
-
if self.
|
|
171
|
+
if self.save_master_only.isChecked():
|
|
139
172
|
self.save_master_as()
|
|
140
173
|
else:
|
|
141
174
|
self.save_multilayer_as()
|
|
@@ -143,11 +176,13 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
143
176
|
def save_multilayer(self):
|
|
144
177
|
if self.layer_stack() is None:
|
|
145
178
|
return
|
|
146
|
-
if self.
|
|
147
|
-
extension = self.
|
|
179
|
+
if self.current_file_path_multi != '':
|
|
180
|
+
extension = self.current_file_path_multi.split('.')[-1]
|
|
148
181
|
if extension in ['tif', 'tiff']:
|
|
149
|
-
self.save_multilayer_to_path(self.
|
|
182
|
+
self.save_multilayer_to_path(self.current_file_path_multi)
|
|
150
183
|
return
|
|
184
|
+
else:
|
|
185
|
+
self.save_multilayer_as()
|
|
151
186
|
|
|
152
187
|
def save_multilayer_as(self):
|
|
153
188
|
if self.layer_stack() is None:
|
|
@@ -161,11 +196,30 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
161
196
|
|
|
162
197
|
def save_multilayer_to_path(self, path):
|
|
163
198
|
try:
|
|
164
|
-
self.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
199
|
+
master_layer = {'Master': self.master_layer().copy()}
|
|
200
|
+
individual_layers = dict(zip(
|
|
201
|
+
self.layer_labels(),
|
|
202
|
+
[layer.copy() for layer in self.layer_stack()]
|
|
203
|
+
))
|
|
204
|
+
images_dict = {**master_layer, **individual_layers}
|
|
205
|
+
self.saver_thread = FileMultilayerSaver(
|
|
206
|
+
images_dict, path, exif_path=self.io_manager.exif_path)
|
|
207
|
+
self.saver_thread.finished.connect(self.on_multilayer_save_success)
|
|
208
|
+
self.saver_thread.error.connect(self.on_multilayer_save_error)
|
|
209
|
+
QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
|
|
210
|
+
self.saving_dialog = QDialog(self.parent())
|
|
211
|
+
self.saving_dialog.setWindowTitle("Saving")
|
|
212
|
+
self.saving_dialog.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
|
|
213
|
+
self.saving_dialog.setModal(True)
|
|
214
|
+
layout = QVBoxLayout()
|
|
215
|
+
layout.addWidget(QLabel("Saving file..."))
|
|
216
|
+
self.saving_dialog.setLayout(layout)
|
|
217
|
+
self.saving_timer = QTimer()
|
|
218
|
+
self.saving_timer.setSingleShot(True)
|
|
219
|
+
self.saving_timer.timeout.connect(self.saving_dialog.show)
|
|
220
|
+
self.saving_timer.start(100)
|
|
221
|
+
self.saver_thread.start()
|
|
222
|
+
|
|
169
223
|
except Exception as e:
|
|
170
224
|
traceback.print_tb(e.__traceback__)
|
|
171
225
|
QMessageBox.critical(self.parent(), "Save Error", f"Could not save file: {str(e)}")
|
|
@@ -173,8 +227,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
173
227
|
def save_master(self):
|
|
174
228
|
if self.master_layer() is None:
|
|
175
229
|
return
|
|
176
|
-
if self.
|
|
177
|
-
self.save_master_to_path(self.
|
|
230
|
+
if self.current_file_path_master != '':
|
|
231
|
+
self.save_master_to_path(self.current_file_path_master)
|
|
178
232
|
return
|
|
179
233
|
self.save_master_as()
|
|
180
234
|
|
|
@@ -190,8 +244,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
190
244
|
def save_master_to_path(self, path):
|
|
191
245
|
try:
|
|
192
246
|
self.io_manager.save_master(path)
|
|
193
|
-
self.
|
|
194
|
-
self.
|
|
247
|
+
self.current_file_path_master = os.path.abspath(path)
|
|
248
|
+
self.mark_as_modified_requested.emit(False)
|
|
195
249
|
self.update_title_requested.emit()
|
|
196
250
|
self.status_message_requested.emit(f"Saved master layer to: {path}")
|
|
197
251
|
except Exception as e:
|
|
@@ -207,15 +261,14 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
|
207
261
|
self.exif_dialog.exec()
|
|
208
262
|
|
|
209
263
|
def close_file(self):
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
self.status_message_requested.emit("File closed")
|
|
264
|
+
self.mark_as_modified_requested.emit(False)
|
|
265
|
+
self.blank_layer = None
|
|
266
|
+
self.layer_collection.reset()
|
|
267
|
+
self.current_file_path_master = ''
|
|
268
|
+
self.current_file_path_multi = ''
|
|
269
|
+
self.undo_manager.reset()
|
|
270
|
+
self.image_viewer.clear_image()
|
|
271
|
+
self.display_manager.thumbnail_list.clear()
|
|
272
|
+
self.display_manager.update_thumbnails()
|
|
273
|
+
self.update_title_requested.emit()
|
|
274
|
+
self.status_message_requested.emit("File closed")
|
|
@@ -1,11 +1,33 @@
|
|
|
1
|
-
# pylint: disable=E1101, C0114, C0115, C0116
|
|
1
|
+
# pylint: disable=E1101, C0114, C0115, C0116, E0611, W0718, R0903
|
|
2
|
+
import traceback
|
|
2
3
|
import cv2
|
|
4
|
+
from PySide6.QtCore import QThread, Signal
|
|
3
5
|
from .. algorithms.utils import read_img, validate_image, get_img_metadata
|
|
4
6
|
from .. algorithms.exif import get_exif, write_image_with_exif_data
|
|
5
7
|
from .. algorithms.multilayer import write_multilayer_tiff_from_images
|
|
6
8
|
from .layer_collection import LayerCollectionHandler
|
|
7
9
|
|
|
8
10
|
|
|
11
|
+
class FileMultilayerSaver(QThread):
|
|
12
|
+
finished = Signal()
|
|
13
|
+
error = Signal(str)
|
|
14
|
+
|
|
15
|
+
def __init__(self, images_dict, path, exif_path=None):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.images_dict = images_dict
|
|
18
|
+
self.path = path
|
|
19
|
+
self.exif_path = exif_path
|
|
20
|
+
|
|
21
|
+
def run(self):
|
|
22
|
+
try:
|
|
23
|
+
write_multilayer_tiff_from_images(
|
|
24
|
+
self.images_dict, self.path, exif_path=self.exif_path)
|
|
25
|
+
self.finished.emit()
|
|
26
|
+
except Exception as e:
|
|
27
|
+
traceback.print_tb(e.__traceback__)
|
|
28
|
+
self.error.emit(str(e))
|
|
29
|
+
|
|
30
|
+
|
|
9
31
|
class IOManager(LayerCollectionHandler):
|
|
10
32
|
def __init__(self, layer_collection):
|
|
11
33
|
super().__init__(layer_collection)
|
|
@@ -38,12 +60,6 @@ class IOManager(LayerCollectionHandler):
|
|
|
38
60
|
raise RuntimeError(f"Error loading file: {path}.\n{str(e)}") from e
|
|
39
61
|
return stack, labels, master
|
|
40
62
|
|
|
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
63
|
def save_master(self, path):
|
|
48
64
|
img = cv2.cvtColor(self.master_layer(), cv2.COLOR_RGB2BGR)
|
|
49
65
|
write_image_with_exif_data(self.exif_data, img, path)
|
|
@@ -32,6 +32,7 @@ class ShortcutsHelp(QDialog):
|
|
|
32
32
|
self.create_form(left_layout, right_layout)
|
|
33
33
|
button_box = QHBoxLayout()
|
|
34
34
|
ok_button = QPushButton("OK")
|
|
35
|
+
ok_button.setFixedWidth(100)
|
|
35
36
|
ok_button.setFocus()
|
|
36
37
|
button_box.addWidget(ok_button)
|
|
37
38
|
self.layout.addLayout(button_box)
|
|
@@ -48,6 +49,7 @@ class ShortcutsHelp(QDialog):
|
|
|
48
49
|
shortcuts = {
|
|
49
50
|
"M": "show master layer",
|
|
50
51
|
"L": "show selected layer",
|
|
52
|
+
"T": "toggle master/selected layer",
|
|
51
53
|
"X": "temp. toggle between master and source layer",
|
|
52
54
|
"↑": "select one layer up",
|
|
53
55
|
"↓": "selcet one layer down",
|
|
@@ -80,3 +82,13 @@ class ShortcutsHelp(QDialog):
|
|
|
80
82
|
self.add_bold_label(right_layout, "Mouse Controls")
|
|
81
83
|
for k, v in mouse_controls.items():
|
|
82
84
|
right_layout.addRow(f"<b>{k}</b>", QLabel(v))
|
|
85
|
+
|
|
86
|
+
touchpad_controls = {
|
|
87
|
+
"Two fingers": "pan",
|
|
88
|
+
"Pinch": "zoom in/out",
|
|
89
|
+
"Ctrl + two fingers": "zoom in/out",
|
|
90
|
+
}
|
|
91
|
+
self.add_bold_label(right_layout, " ")
|
|
92
|
+
self.add_bold_label(right_layout, "Touchpad Controls")
|
|
93
|
+
for k, v in touchpad_controls.items():
|
|
94
|
+
right_layout.addRow(f"<b>{k}</b>", QLabel(v))
|
|
@@ -6,8 +6,8 @@ from .base_filter import BaseFilter
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class UnsharpMaskFilter(BaseFilter):
|
|
9
|
-
def __init__(self, editor):
|
|
10
|
-
super().__init__(editor)
|
|
9
|
+
def __init__(self, name, editor):
|
|
10
|
+
super().__init__(name, editor, preview_at_startup=True)
|
|
11
11
|
self.max_range = 500.0
|
|
12
12
|
self.max_radius = 4.0
|
|
13
13
|
self.max_amount = 3.0
|
|
@@ -46,14 +46,14 @@ class UnsharpMaskFilter(BaseFilter):
|
|
|
46
46
|
elif name == "Threshold":
|
|
47
47
|
self.threshold_slider = slider
|
|
48
48
|
value_labels[name] = value_label
|
|
49
|
-
|
|
50
|
-
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
|
|
49
|
+
self.create_base_widgets(
|
|
50
|
+
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200, dlg)
|
|
51
51
|
|
|
52
52
|
def update_value(name, value, max_val, fmt):
|
|
53
53
|
float_value = max_val * value / self.max_range
|
|
54
54
|
value_labels[name].setText(fmt.format(float_value))
|
|
55
|
-
if preview_check.isChecked():
|
|
56
|
-
preview_timer.start()
|
|
55
|
+
if self.preview_check.isChecked():
|
|
56
|
+
self.preview_timer.start()
|
|
57
57
|
|
|
58
58
|
self.radius_slider.valueChanged.connect(
|
|
59
59
|
lambda v: update_value("Radius", v, self.max_radius, params["Radius"][2]))
|
|
@@ -61,10 +61,10 @@ class UnsharpMaskFilter(BaseFilter):
|
|
|
61
61
|
lambda v: update_value("Amount", v, self.max_amount, params["Amount"][2]))
|
|
62
62
|
self.threshold_slider.valueChanged.connect(
|
|
63
63
|
lambda v: update_value("Threshold", v, self.max_threshold, params["Threshold"][2]))
|
|
64
|
-
preview_timer.timeout.connect(do_preview)
|
|
65
|
-
self.editor.connect_preview_toggle(preview_check, do_preview, restore_original)
|
|
66
|
-
button_box.accepted.connect(dlg.accept)
|
|
67
|
-
button_box.rejected.connect(dlg.reject)
|
|
64
|
+
self.preview_timer.timeout.connect(do_preview)
|
|
65
|
+
self.editor.connect_preview_toggle(self.preview_check, do_preview, restore_original)
|
|
66
|
+
self.button_box.accepted.connect(dlg.accept)
|
|
67
|
+
self.button_box.rejected.connect(dlg.reject)
|
|
68
68
|
QTimer.singleShot(0, do_preview)
|
|
69
69
|
|
|
70
70
|
def get_params(self):
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0902
|
|
2
|
+
from PySide6.QtCore import Qt
|
|
3
|
+
from PySide6.QtWidgets import QSpinBox, QCheckBox, QLabel, QHBoxLayout, QSlider
|
|
4
|
+
from .. config.constants import constants
|
|
5
|
+
from .. algorithms.vignetting import correct_vignetting
|
|
6
|
+
from .base_filter import OneSliderBaseFilter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VignettingFilter(OneSliderBaseFilter):
|
|
10
|
+
def __init__(self, name, editor):
|
|
11
|
+
super().__init__(name, editor, 1.0, 0.90, "Vignetting correction",
|
|
12
|
+
allow_partial_preview=False, preview_at_startup=False)
|
|
13
|
+
self.subsample_box = None
|
|
14
|
+
self.fast_subsampling_check = None
|
|
15
|
+
self.r_steps_box = None
|
|
16
|
+
self.threshold_slider = None
|
|
17
|
+
self.threshold_label = None
|
|
18
|
+
self.threshold_max_range = 500
|
|
19
|
+
self.threshold_max_value = 128.0
|
|
20
|
+
self.threshold_initial_value = constants.DEFAULT_BLACK_THRESHOLD
|
|
21
|
+
self.threshold_format = "{:.1f}"
|
|
22
|
+
|
|
23
|
+
def apply(self, image, strength):
|
|
24
|
+
return correct_vignetting(image, max_correction=strength,
|
|
25
|
+
black_threshold=self.threshold_slider.value(),
|
|
26
|
+
r_steps=self.r_steps_box.value(),
|
|
27
|
+
subsample=self.subsample_box.value(),
|
|
28
|
+
fast_subsampling=True)
|
|
29
|
+
|
|
30
|
+
def add_widgets(self, layout, dlg):
|
|
31
|
+
threshold_layout = QHBoxLayout()
|
|
32
|
+
threshold_layout.addWidget(QLabel("Threshold:"))
|
|
33
|
+
self.threshold_slider = QSlider(Qt.Horizontal)
|
|
34
|
+
self.threshold_slider.setRange(0, self.threshold_max_range)
|
|
35
|
+
self.threshold_slider.setValue(
|
|
36
|
+
int(self.threshold_initial_value /
|
|
37
|
+
self.threshold_max_value * self.threshold_max_range))
|
|
38
|
+
self.threshold_slider.valueChanged.connect(self.threshold_changed)
|
|
39
|
+
self.threshold_label = QLabel(self.threshold_format.format(self.threshold_initial_value))
|
|
40
|
+
threshold_layout.addWidget(self.threshold_slider)
|
|
41
|
+
threshold_layout.addWidget(self.threshold_label)
|
|
42
|
+
layout.addLayout(threshold_layout)
|
|
43
|
+
subsample_layout = QHBoxLayout()
|
|
44
|
+
subsample_label = QLabel("Subsample:")
|
|
45
|
+
self.subsample_box = QSpinBox()
|
|
46
|
+
self.subsample_box.setFixedWidth(50)
|
|
47
|
+
self.subsample_box.setRange(1, 50)
|
|
48
|
+
self.subsample_box.setValue(constants.DEFAULT_VIGN_SUBSAMPLE)
|
|
49
|
+
self.subsample_box.valueChanged.connect(self.threshold_changed)
|
|
50
|
+
self.fast_subsampling_check = QCheckBox("Fast subsampling")
|
|
51
|
+
self.fast_subsampling_check.setChecked(constants.DEFAULT_VIGN_FAST_SUBSAMPLING)
|
|
52
|
+
r_steps_label = QLabel("Radial steps:")
|
|
53
|
+
self.r_steps_box = QSpinBox()
|
|
54
|
+
self.r_steps_box.setFixedWidth(50)
|
|
55
|
+
self.r_steps_box.setRange(1, 200)
|
|
56
|
+
self.r_steps_box.setValue(constants.DEFAULT_R_STEPS)
|
|
57
|
+
self.r_steps_box.valueChanged.connect(self.param_changed)
|
|
58
|
+
subsample_layout.addWidget(subsample_label)
|
|
59
|
+
subsample_layout.addWidget(self.subsample_box)
|
|
60
|
+
subsample_layout.addWidget(r_steps_label)
|
|
61
|
+
subsample_layout.addWidget(self.r_steps_box)
|
|
62
|
+
subsample_layout.addStretch(1)
|
|
63
|
+
layout.addLayout(subsample_layout)
|
|
64
|
+
layout.addWidget(self.fast_subsampling_check)
|
|
65
|
+
|
|
66
|
+
def threshold_changed(self, val):
|
|
67
|
+
float_val = self.threshold_max_value * float(val) / self.threshold_max_range
|
|
68
|
+
self.threshold_label.setText(self.threshold_format.format(float_val))
|
|
69
|
+
self.param_changed(val)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917, R0902
|
|
2
2
|
from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QFrame, QVBoxLayout, QLabel, QDialog,
|
|
3
|
-
QApplication, QSlider, QDialogButtonBox)
|
|
3
|
+
QApplication, QSlider, QDialogButtonBox, QLineEdit)
|
|
4
4
|
from PySide6.QtCore import Qt, QTimer
|
|
5
5
|
from PySide6.QtGui import QCursor
|
|
6
6
|
from .. algorithms.white_balance import white_balance_from_rgb
|
|
@@ -8,12 +8,13 @@ from .base_filter import BaseFilter
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class WhiteBalanceFilter(BaseFilter):
|
|
11
|
-
def __init__(self, editor):
|
|
12
|
-
super().__init__(editor)
|
|
11
|
+
def __init__(self, name, editor):
|
|
12
|
+
super().__init__(name, editor, preview_at_startup=True)
|
|
13
13
|
self.max_range = 255
|
|
14
14
|
self.initial_val = (128, 128, 128)
|
|
15
15
|
self.sliders = {}
|
|
16
16
|
self.value_labels = {}
|
|
17
|
+
self.rgb_hex = None
|
|
17
18
|
self.color_preview = None
|
|
18
19
|
self.preview_timer = None
|
|
19
20
|
self.original_mouse_press = None
|
|
@@ -45,29 +46,60 @@ class WhiteBalanceFilter(BaseFilter):
|
|
|
45
46
|
self.value_labels[name] = val_label
|
|
46
47
|
row_layout.addLayout(sliders_layout)
|
|
47
48
|
layout.addLayout(row_layout)
|
|
49
|
+
|
|
50
|
+
rbg_layout = QHBoxLayout()
|
|
51
|
+
rbg_layout.addWidget(QLabel("RBG hex:"))
|
|
52
|
+
self.rgb_hex = QLineEdit(self.hex_color(self.initial_val))
|
|
53
|
+
self.rgb_hex.setFixedWidth(60)
|
|
54
|
+
self.rgb_hex.textChanged.connect(self.on_rgb_change)
|
|
55
|
+
rbg_layout.addWidget(self.rgb_hex)
|
|
56
|
+
rbg_layout.addStretch(1)
|
|
57
|
+
layout.addLayout(rbg_layout)
|
|
58
|
+
|
|
48
59
|
pick_button = QPushButton("Pick Color")
|
|
49
60
|
layout.addWidget(pick_button)
|
|
50
|
-
|
|
61
|
+
self.create_base_widgets(
|
|
51
62
|
layout,
|
|
52
63
|
QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel,
|
|
53
|
-
200)
|
|
64
|
+
200, dlg)
|
|
54
65
|
for slider in self.sliders.values():
|
|
55
66
|
slider.valueChanged.connect(self.on_slider_change)
|
|
56
67
|
self.preview_timer.timeout.connect(do_preview)
|
|
57
|
-
self.editor.connect_preview_toggle(preview_check, do_preview, restore_original)
|
|
68
|
+
self.editor.connect_preview_toggle(self.preview_check, do_preview, restore_original)
|
|
58
69
|
pick_button.clicked.connect(self.start_color_pick)
|
|
59
|
-
button_box.accepted.connect(dlg.accept)
|
|
60
|
-
button_box.rejected.connect(dlg.reject)
|
|
61
|
-
button_box.button(QDialogButtonBox.Reset).clicked.connect(self.reset_rgb)
|
|
70
|
+
self.button_box.accepted.connect(dlg.accept)
|
|
71
|
+
self.button_box.rejected.connect(dlg.reject)
|
|
72
|
+
self.button_box.button(QDialogButtonBox.Reset).clicked.connect(self.reset_rgb)
|
|
62
73
|
QTimer.singleShot(0, do_preview)
|
|
63
74
|
|
|
75
|
+
def hex_color(self, val):
|
|
76
|
+
return "".join([f"{int(c):0>2X}" for c in val])
|
|
77
|
+
|
|
78
|
+
def apply_preview(self, rgb):
|
|
79
|
+
self.color_preview.setStyleSheet(f"background-color: rgb{tuple(rgb)};")
|
|
80
|
+
if self.preview_timer:
|
|
81
|
+
self.preview_timer.start()
|
|
82
|
+
|
|
64
83
|
def on_slider_change(self):
|
|
65
84
|
for name in ("R", "G", "B"):
|
|
66
85
|
self.value_labels[name].setText(str(self.sliders[name].value()))
|
|
67
86
|
rgb = tuple(self.sliders[n].value() for n in ("R", "G", "B"))
|
|
68
|
-
self.
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
self.rgb_hex.blockSignals(True)
|
|
88
|
+
self.rgb_hex.setText(self.hex_color(rgb))
|
|
89
|
+
self.rgb_hex.blockSignals(False)
|
|
90
|
+
self.apply_preview(rgb)
|
|
91
|
+
|
|
92
|
+
def on_rgb_change(self):
|
|
93
|
+
txt = self.rgb_hex.text()
|
|
94
|
+
if len(txt) != 6:
|
|
95
|
+
return
|
|
96
|
+
rgb = [int(txt[i:i + 2], 16) for i in range(0, 6, 2)]
|
|
97
|
+
for name, c in zip(("R", "G", "B"), rgb):
|
|
98
|
+
self.sliders[name].blockSignals(True)
|
|
99
|
+
self.sliders[name].setValue(c)
|
|
100
|
+
self.sliders[name].blockSignals(False)
|
|
101
|
+
self.value_labels[name].setText(str(c))
|
|
102
|
+
self.apply_preview(rgb)
|
|
71
103
|
|
|
72
104
|
def start_color_pick(self):
|
|
73
105
|
for widget in QApplication.topLevelWidgets():
|
|
@@ -94,7 +126,7 @@ class WhiteBalanceFilter(BaseFilter):
|
|
|
94
126
|
self.editor.image_viewer.mousePressEvent = self.original_mouse_press
|
|
95
127
|
self.editor.image_viewer.brush_cursor.show()
|
|
96
128
|
self.editor.brush_preview.show()
|
|
97
|
-
new_filter = WhiteBalanceFilter(self.editor)
|
|
129
|
+
new_filter = WhiteBalanceFilter(self.name, self.editor)
|
|
98
130
|
new_filter.run_with_preview(init_val=rgb)
|
|
99
131
|
|
|
100
132
|
def reset_rgb(self):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
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
|
[](https://codecov.io/github/lucalista/shinestacker)
|
|
42
42
|
[](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
|
|
43
43
|
[](https://www.gnu.org/licenses/lgpl-3.0)
|
|
44
|
+
[](https://pepy.tech/projects/shinestacker)
|
|
44
45
|
|
|
45
46
|
<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">
|
|
46
47
|
|
|
@@ -75,6 +76,14 @@ The GUI has two main working areas:
|
|
|
75
76
|
|
|
76
77
|
The first version of the core focus stack algorithm was initially inspired by the [Laplacian pyramids method](https://github.com/sjawhar/focus-stacking) implementation by Sami Jawhar that was used under permission of the author. The implementation in the latest releases was rewritten from the original code.
|
|
77
78
|
|
|
79
|
+
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="256" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
|
|
80
|
+
|
|
81
|
+
## Logo attribution
|
|
82
|
+
|
|
83
|
+
The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista).
|
|
84
|
+
Copyright © Alessandro Lista. All rights reserved.
|
|
85
|
+
The logo is not covered by the LGPL-3.0 license of this project.
|
|
86
|
+
|
|
78
87
|
# Resources
|
|
79
88
|
|
|
80
89
|
* [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
|
|
@@ -83,8 +92,11 @@ Pyramid methods in image processing
|
|
|
83
92
|
|
|
84
93
|
# License
|
|
85
94
|
|
|
86
|
-
<img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
95
|
+
- **Code**: <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
87
96
|
The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
|
|
97
|
+
- **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista).
|
|
98
|
+
Copyright © Alessandro Lista. All rights reserved.
|
|
99
|
+
The logo is not covered by the LGPL-3.0 license of this project.
|
|
88
100
|
|
|
89
101
|
# Attribution request
|
|
90
102
|
📸 If you publish images created with Shine Stacker, please consider adding a note such as:
|