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
@@ -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.current_file_path = ''
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.display_manager.update_thumbnails()
51
- self.image_viewer.setup_brush_cursor()
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.status_message_requested.emit(f"Error loading: {self.current_file_path}")
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.current_file_path = os.path.abspath(path)
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
- self.set_layer_labels(labels)
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.parent().save_master_only.isChecked():
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.parent().save_master_only.isChecked():
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.current_file_path != '':
147
- extension = self.current_file_path.split('.')[-1]
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.current_file_path)
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.io_manager.save_multilayer(path)
165
- self.current_file_path = os.path.abspath(path)
166
- self.parent().modified = False
167
- self.update_title_requested.emit()
168
- self.status_message_requested.emit(f"Saved multilayer to: {path}")
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.current_file_path != '':
177
- self.save_master_to_path(self.current_file_path)
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.current_file_path = os.path.abspath(path)
194
- self.parent().modified = False
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
- if self.parent().check_unsaved_changes():
211
- self.set_master_layer(None)
212
- self.blank_layer = None
213
- self.layer_collection.reset()
214
- self.current_file_path = ''
215
- self.parent().modified = False
216
- self.undo_manager.reset()
217
- self.image_viewer.clear_image()
218
- self.display_manager.thumbnail_list.clear()
219
- self.display_manager.update_thumbnails()
220
- self.update_title_requested.emit()
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,8 @@ class LayerCollection:
32
32
  return self.master_layer_copy is None
33
33
 
34
34
  def number_of_layers(self):
35
+ if self.layer_stack is None:
36
+ return 0
35
37
  return len(self.layer_stack)
36
38
 
37
39
  def layer_label(self, i):
@@ -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
- preview_check, preview_timer, button_box = self.create_base_widgets(
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
- preview_check, self.preview_timer, button_box = self.create_base_widgets(
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.color_preview.setStyleSheet(f"background-color: rgb{rgb};")
69
- if self.preview_timer:
70
- self.preview_timer.start()
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.4.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
  [![codecov](https://codecov.io/github/lucalista/shinestacker/graph/badge.svg?token=Y5NKW6VH5G)](https://codecov.io/github/lucalista/shinestacker)
42
42
  [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
43
43
  [![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
44
+ [![PyPI Downloads](https://static.pepy.tech/badge/shinestacker)](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: