shinestacker 0.3.5__py3-none-any.whl → 0.4.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 (44) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +37 -20
  3. shinestacker/algorithms/balance.py +2 -1
  4. shinestacker/algorithms/base_stack_algo.py +2 -1
  5. shinestacker/algorithms/multilayer.py +11 -8
  6. shinestacker/algorithms/noise_detection.py +13 -7
  7. shinestacker/algorithms/pyramid.py +7 -4
  8. shinestacker/algorithms/stack.py +5 -4
  9. shinestacker/algorithms/stack_framework.py +12 -10
  10. shinestacker/app/app_config.py +4 -22
  11. shinestacker/app/main.py +1 -1
  12. shinestacker/config/config.py +22 -16
  13. shinestacker/config/constants.py +8 -1
  14. shinestacker/core/framework.py +15 -10
  15. shinestacker/gui/action_config.py +20 -41
  16. shinestacker/gui/actions_window.py +18 -47
  17. shinestacker/gui/gui_logging.py +8 -7
  18. shinestacker/gui/gui_run.py +8 -8
  19. shinestacker/gui/main_window.py +4 -4
  20. shinestacker/gui/new_project.py +34 -37
  21. shinestacker/gui/project_converter.py +0 -1
  22. shinestacker/gui/project_editor.py +43 -20
  23. shinestacker/gui/select_path_widget.py +32 -0
  24. shinestacker/retouch/base_filter.py +12 -1
  25. shinestacker/retouch/denoise_filter.py +4 -10
  26. shinestacker/retouch/exif_data.py +3 -9
  27. shinestacker/retouch/icon_container.py +19 -0
  28. shinestacker/retouch/image_editor.py +1 -1
  29. shinestacker/retouch/image_editor_ui.py +2 -1
  30. shinestacker/retouch/image_viewer.py +104 -20
  31. shinestacker/retouch/io_gui_handler.py +17 -16
  32. shinestacker/retouch/io_manager.py +0 -1
  33. shinestacker/retouch/layer_collection.py +2 -1
  34. shinestacker/retouch/shortcuts_help.py +2 -13
  35. shinestacker/retouch/unsharp_mask_filter.py +3 -10
  36. shinestacker/retouch/white_balance_filter.py +5 -13
  37. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/METADATA +8 -11
  38. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/RECORD +42 -41
  39. shinestacker-0.4.0.dist-info/licenses/LICENSE +165 -0
  40. shinestacker/algorithms/core_utils.py +0 -22
  41. shinestacker-0.3.5.dist-info/licenses/LICENSE +0 -1
  42. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/WHEEL +0 -0
  43. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/entry_points.txt +0 -0
  44. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0221
2
- from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QCheckBox, QDialogButtonBox
3
- from PySide6.QtCore import Qt, QTimer
2
+ from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QDialogButtonBox
3
+ from PySide6.QtCore import Qt
4
4
  from .base_filter import BaseFilter
5
5
  from .. algorithms.denoise import denoise
6
6
 
@@ -24,14 +24,8 @@ class DenoiseFilter(BaseFilter):
24
24
  value_label = QLabel(f"{self.max_value:.2f}")
25
25
  slider_layout.addWidget(value_label)
26
26
  layout.addLayout(slider_layout)
27
- preview_check = QCheckBox("Preview")
28
- preview_check.setChecked(True)
29
- layout.addWidget(preview_check)
30
- button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
31
- layout.addWidget(button_box)
32
- preview_timer = QTimer()
33
- preview_timer.setSingleShot(True)
34
- preview_timer.setInterval(200)
27
+ preview_check, preview_timer, button_box = self.create_base_widgets(
28
+ layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
35
29
 
36
30
  def do_preview_delayed():
37
31
  preview_timer.start()
@@ -1,10 +1,9 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611
2
- import os
3
2
  from PIL.TiffImagePlugin import IFDRational
4
3
  from PySide6.QtWidgets import QFormLayout, QHBoxLayout, QPushButton, QDialog, QLabel
5
- from PySide6.QtGui import QIcon
6
4
  from PySide6.QtCore import Qt
7
5
  from .. algorithms.exif import exif_dict
6
+ from .icon_container import icon_container
8
7
 
9
8
 
10
9
  class ExifData(QDialog):
@@ -32,13 +31,8 @@ class ExifData(QDialog):
32
31
  self.layout.addRow(label)
33
32
 
34
33
  def create_form(self):
35
- icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
36
- app_icon = QIcon(icon_path)
37
- icon_pixmap = app_icon.pixmap(128, 128)
38
- icon_label = QLabel()
39
- icon_label.setPixmap(icon_pixmap)
40
- icon_label.setAlignment(Qt.AlignCenter)
41
- self.layout.addRow(icon_label)
34
+ self.layout.addRow(icon_container())
35
+
42
36
  spacer = QLabel("")
43
37
  spacer.setFixedHeight(10)
44
38
  self.layout.addRow(spacer)
@@ -0,0 +1,19 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611
2
+ import os
3
+ from PySide6.QtWidgets import QHBoxLayout, QLabel, QWidget
4
+ from PySide6.QtGui import QIcon
5
+ from PySide6.QtCore import Qt
6
+
7
+
8
+ def icon_container():
9
+ icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
10
+ app_icon = QIcon(icon_path)
11
+ pixmap = app_icon.pixmap(128, 128)
12
+ label = QLabel()
13
+ label.setPixmap(pixmap)
14
+ label.setAlignment(Qt.AlignCenter)
15
+ container = QWidget()
16
+ layout = QHBoxLayout(container)
17
+ layout.addWidget(label)
18
+ layout.setAlignment(Qt.AlignCenter)
19
+ return container
@@ -87,7 +87,7 @@ class ImageEditor(QMainWindow, LayerCollectionHandler):
87
87
  def update_title(self):
88
88
  title = constants.APP_TITLE
89
89
  if self.io_gui_handler is not None:
90
- path = self.io_gui_handler.io_manager.current_file_path
90
+ path = self.io_gui_handler.current_file_path
91
91
  if path != '':
92
92
  title += f" - {path.split('/')[-1]}"
93
93
  if self.modified:
@@ -135,7 +135,8 @@ class ImageEditorUI(ImageFilters):
135
135
  self.master_thumbnail_label.setAlignment(Qt.AlignCenter)
136
136
  self.master_thumbnail_label.setFixedSize(
137
137
  gui_constants.THUMB_WIDTH, gui_constants.THUMB_HEIGHT)
138
- self.master_thumbnail_label.mousePressEvent = lambda e: self.set_view_master()
138
+ self.master_thumbnail_label.mousePressEvent = \
139
+ lambda e: self.display_manager.set_view_master()
139
140
  master_thumbnail_layout.addWidget(self.master_thumbnail_label)
140
141
  side_layout.addWidget(self.master_thumbnail_frame)
141
142
  side_layout.addSpacing(10)
@@ -1,8 +1,8 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, R0904, R0902, R0914
1
+ # pylint: disable=C0114, C0115, C0116, E0611, R0904, R0902, R0914, R0912
2
2
  import math
3
3
  from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
4
4
  from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor, QShortcut, QKeySequence
5
- from PySide6.QtCore import Qt, QRectF, QTime, QPoint, QPointF, Signal
5
+ from PySide6.QtCore import Qt, QRectF, QTime, QPoint, QPointF, Signal, QEvent
6
6
  from .. config.gui_constants import gui_constants
7
7
  from .brush_preview import BrushPreviewItem
8
8
  from .brush_gradient import create_default_brush_gradient
@@ -52,6 +52,13 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
52
52
  self.empty = True
53
53
  self.allow_cursor_preview = True
54
54
  self.last_brush_pos = None
55
+ self.grabGesture(Qt.PanGesture)
56
+ self.grabGesture(Qt.PinchGesture)
57
+ self.pinch_start_scale = 1.0
58
+ self.last_scroll_pos = QPointF()
59
+ self.gesture_active = False
60
+ self.pinch_center_view = None
61
+ self.pinch_center_scene = None
55
62
 
56
63
  def set_image(self, qimage):
57
64
  pixmap = QPixmap.fromImage(qimage)
@@ -193,25 +200,44 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
193
200
  super().mouseReleaseEvent(event)
194
201
 
195
202
  def wheelEvent(self, event):
196
- if self.empty:
203
+ if self.empty or self.gesture_active:
197
204
  return
198
- if self.control_pressed:
199
- self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
200
- else:
201
- zoom_in_factor = 1.10
202
- zoom_out_factor = 1 / zoom_in_factor
203
- current_scale = self.get_current_scale()
204
- if event.angleDelta().y() > 0: # Zoom in
205
- new_scale = current_scale * zoom_in_factor
206
- if new_scale <= self.max_scale:
207
- self.scale(zoom_in_factor, zoom_in_factor)
208
- self.zoom_factor = new_scale
209
- else: # Zoom out
210
- new_scale = current_scale * zoom_out_factor
211
- if new_scale >= self.min_scale:
212
- self.scale(zoom_out_factor, zoom_out_factor)
213
- self.zoom_factor = new_scale
214
- self.update_brush_cursor()
205
+ if event.source() == Qt.MouseEventNotSynthesized: # Physical mouse
206
+ if self.control_pressed:
207
+ self.brush_size_change_requested.emit(1 if event.angleDelta().y() > 0 else -1)
208
+ else:
209
+ zoom_in_factor = 1.10
210
+ zoom_out_factor = 1 / zoom_in_factor
211
+ current_scale = self.get_current_scale()
212
+ if event.angleDelta().y() > 0: # Zoom in
213
+ new_scale = current_scale * zoom_in_factor
214
+ if new_scale <= self.max_scale:
215
+ self.scale(zoom_in_factor, zoom_in_factor)
216
+ self.zoom_factor = new_scale
217
+ else: # Zoom out
218
+ new_scale = current_scale * zoom_out_factor
219
+ if new_scale >= self.min_scale:
220
+ self.scale(zoom_out_factor, zoom_out_factor)
221
+ self.zoom_factor = new_scale
222
+ self.update_brush_cursor()
223
+ else: # Touchpad event - fallback for systems without gesture recognition
224
+ # Handle touchpad panning (two-finger scroll)
225
+ if not self.control_pressed:
226
+ delta = event.pixelDelta() or event.angleDelta() / 8
227
+ if delta:
228
+ self.horizontalScrollBar().setValue(
229
+ self.horizontalScrollBar().value() - delta.x()
230
+ )
231
+ self.verticalScrollBar().setValue(
232
+ self.verticalScrollBar().value() - delta.y()
233
+ )
234
+ else: # Control + touchpad scroll for zoom
235
+ zoom_in = event.angleDelta().y() > 0
236
+ if zoom_in:
237
+ self.zoom_in()
238
+ else:
239
+ self.zoom_out()
240
+ event.accept()
215
241
 
216
242
  def enterEvent(self, event):
217
243
  self.activateWindow()
@@ -230,6 +256,64 @@ class ImageViewer(QGraphicsView, LayerCollectionHandler):
230
256
  super().leaveEvent(event)
231
257
  # pylint: enable=C0103
232
258
 
259
+ def event(self, event):
260
+ if event.type() == QEvent.Gesture:
261
+ return self.handle_gesture_event(event)
262
+ return super().event(event)
263
+
264
+ def handle_gesture_event(self, event):
265
+ handled = False
266
+ pan_gesture = event.gesture(Qt.PanGesture)
267
+ if pan_gesture:
268
+ self.handle_pan_gesture(pan_gesture)
269
+ handled = True
270
+ pinch_gesture = event.gesture(Qt.PinchGesture)
271
+ if pinch_gesture:
272
+ self.handle_pinch_gesture(pinch_gesture)
273
+ handled = True
274
+ if handled:
275
+ event.accept()
276
+ return True
277
+ return False
278
+
279
+ def handle_pan_gesture(self, pan_gesture):
280
+ if pan_gesture.state() == Qt.GestureStarted:
281
+ self.last_scroll_pos = pan_gesture.delta()
282
+ self.gesture_active = True
283
+ elif pan_gesture.state() == Qt.GestureUpdated:
284
+ delta = pan_gesture.delta() - self.last_scroll_pos
285
+ self.last_scroll_pos = pan_gesture.delta()
286
+ zoom_factor = self.get_current_scale()
287
+ scaled_delta = delta * (1.0 / zoom_factor)
288
+ self.horizontalScrollBar().setValue(
289
+ self.horizontalScrollBar().value() - int(scaled_delta.x())
290
+ )
291
+ self.verticalScrollBar().setValue(
292
+ self.verticalScrollBar().value() - int(scaled_delta.y())
293
+ )
294
+ elif pan_gesture.state() == Qt.GestureFinished:
295
+ self.gesture_active = False
296
+
297
+ def handle_pinch_gesture(self, pinch):
298
+ if pinch.state() == Qt.GestureStarted:
299
+ self.pinch_start_scale = self.get_current_scale()
300
+ self.pinch_center_view = pinch.centerPoint()
301
+ self.pinch_center_scene = self.mapToScene(self.pinch_center_view.toPoint())
302
+ self.gesture_active = True
303
+ elif pinch.state() == Qt.GestureUpdated:
304
+ new_scale = self.pinch_start_scale * pinch.totalScaleFactor()
305
+ new_scale = max(self.min_scale, min(new_scale, self.max_scale))
306
+ if abs(new_scale - self.get_current_scale()) > 0.01:
307
+ self.resetTransform()
308
+ self.scale(new_scale, new_scale)
309
+ self.zoom_factor = new_scale
310
+ new_center = self.mapToScene(self.pinch_center_view.toPoint())
311
+ delta = self.pinch_center_scene - new_center
312
+ self.translate(delta.x(), delta.y())
313
+ self.update_brush_cursor()
314
+ elif pinch.state() in (Qt.GestureFinished, Qt.GestureCanceled):
315
+ self.gesture_active = False
316
+
233
317
  def setup_brush_cursor(self):
234
318
  self.setCursor(Qt.BlankCursor)
235
319
  pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), 1)
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0902, W0718
2
+ import os
2
3
  import traceback
3
4
  import numpy as np
4
5
  from PySide6.QtWidgets import QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog, QApplication
@@ -23,11 +24,11 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
23
24
  self.loader_thread = None
24
25
  self.display_manager = None
25
26
  self.image_viewer = None
26
- self.modified = None
27
27
  self.blank_layer = None
28
28
  self.loading_dialog = None
29
29
  self.loading_timer = None
30
30
  self.exif_dialog = None
31
+ self.current_file_path = ''
31
32
 
32
33
  def setup_ui(self, display_manager, image_viewer):
33
34
  self.display_manager = display_manager
@@ -43,14 +44,14 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
43
44
  else:
44
45
  self.set_layer_labels(labels)
45
46
  self.set_master_layer(master_layer)
46
- self.modified = False
47
+ self.parent().modified = False
47
48
  self.undo_manager.reset()
48
49
  self.blank_layer = np.zeros(master_layer.shape[:2])
49
50
  self.display_manager.update_thumbnails()
50
51
  self.image_viewer.setup_brush_cursor()
51
52
  self.parent().change_layer(0)
52
53
  self.image_viewer.reset_zoom()
53
- self.status_message_requested.emit(f"Loaded: {self.io_manager.current_file_path}")
54
+ self.status_message_requested.emit(f"Loaded: {self.current_file_path}")
54
55
  self.parent().thumbnail_list.setFocus()
55
56
  self.update_title_requested.emit()
56
57
 
@@ -60,7 +61,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
60
61
  self.loading_dialog.accept()
61
62
  self.loading_dialog.deleteLater()
62
63
  QMessageBox.critical(self.parent(), "Error", error_msg)
63
- self.status_message_requested.emit(f"Error loading: {self.io_manager.current_file_path}")
64
+ self.status_message_requested.emit(f"Error loading: {self.current_file_path}")
64
65
 
65
66
  def open_file(self, file_paths=None):
66
67
  if file_paths is None:
@@ -76,7 +77,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
76
77
  self.import_frames_from_files(file_paths)
77
78
  return
78
79
  path = file_paths[0] if isinstance(file_paths, list) else file_paths
79
- self.io_manager.current_file_path = path
80
+ self.current_file_path = os.path.abspath(path)
80
81
  QGuiApplication.setOverrideCursor(QCursor(Qt.BusyCursor))
81
82
  self.loading_dialog = QDialog(self.parent())
82
83
  self.loading_dialog.setWindowTitle("Loading")
@@ -142,10 +143,10 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
142
143
  def save_multilayer(self):
143
144
  if self.layer_stack() is None:
144
145
  return
145
- if self.io_manager.current_file_path != '':
146
- extension = self.io_manager.current_file_path.split('.')[-1]
146
+ if self.current_file_path != '':
147
+ extension = self.current_file_path.split('.')[-1]
147
148
  if extension in ['tif', 'tiff']:
148
- self.save_multilayer_to_path(self.io_manager.current_file_path)
149
+ self.save_multilayer_to_path(self.current_file_path)
149
150
  return
150
151
 
151
152
  def save_multilayer_as(self):
@@ -161,8 +162,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
161
162
  def save_multilayer_to_path(self, path):
162
163
  try:
163
164
  self.io_manager.save_multilayer(path)
164
- self.io_manager.current_file_path = path
165
- self.modified = False
165
+ self.current_file_path = os.path.abspath(path)
166
+ self.parent().modified = False
166
167
  self.update_title_requested.emit()
167
168
  self.status_message_requested.emit(f"Saved multilayer to: {path}")
168
169
  except Exception as e:
@@ -172,8 +173,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
172
173
  def save_master(self):
173
174
  if self.master_layer() is None:
174
175
  return
175
- if self.io_manager.current_file_path != '':
176
- self.save_master_to_path(self.io_manager.current_file_path)
176
+ if self.current_file_path != '':
177
+ self.save_master_to_path(self.current_file_path)
177
178
  return
178
179
  self.save_master_as()
179
180
 
@@ -189,8 +190,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
189
190
  def save_master_to_path(self, path):
190
191
  try:
191
192
  self.io_manager.save_master(path)
192
- self.io_manager.current_file_path = path
193
- self.modified = False
193
+ self.current_file_path = os.path.abspath(path)
194
+ self.parent().modified = False
194
195
  self.update_title_requested.emit()
195
196
  self.status_message_requested.emit(f"Saved master layer to: {path}")
196
197
  except Exception as e:
@@ -210,8 +211,8 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
210
211
  self.set_master_layer(None)
211
212
  self.blank_layer = None
212
213
  self.layer_collection.reset()
213
- self.io_manager.current_file_path = ''
214
- self.modified = False
214
+ self.current_file_path = ''
215
+ self.parent().modified = False
215
216
  self.undo_manager.reset()
216
217
  self.image_viewer.clear_image()
217
218
  self.display_manager.thumbnail_list.clear()
@@ -9,7 +9,6 @@ from .layer_collection import LayerCollectionHandler
9
9
  class IOManager(LayerCollectionHandler):
10
10
  def __init__(self, layer_collection):
11
11
  super().__init__(layer_collection)
12
- self.current_file_path = ''
13
12
  self.exif_path = ''
14
13
  self.exif_data = None
15
14
 
@@ -80,7 +80,8 @@ class LayerCollection:
80
80
  master_label = None
81
81
  master_layer = None
82
82
  for i, label in enumerate(self.layer_labels):
83
- if label.lower() == "master":
83
+ label_lower = label.lower()
84
+ if "master" in label_lower or "stack" in label_lower:
84
85
  master_index = i
85
86
  master_label = self.layer_labels.pop(i)
86
87
  master_layer = self.layer_stack[i]
@@ -1,9 +1,8 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611
2
- import os
3
2
  from PySide6.QtWidgets import (QFormLayout, QHBoxLayout, QPushButton, QDialog,
4
3
  QLabel, QVBoxLayout, QWidget)
5
- from PySide6.QtGui import QIcon
6
4
  from PySide6.QtCore import Qt
5
+ from .icon_container import icon_container
7
6
 
8
7
 
9
8
  class ShortcutsHelp(QDialog):
@@ -44,17 +43,7 @@ class ShortcutsHelp(QDialog):
44
43
  layout.addRow(label)
45
44
 
46
45
  def create_form(self, left_layout, right_layout):
47
- icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
48
- app_icon = QIcon(icon_path)
49
- icon_pixmap = app_icon.pixmap(128, 128)
50
- icon_label = QLabel()
51
- icon_label.setPixmap(icon_pixmap)
52
- icon_label.setAlignment(Qt.AlignCenter)
53
- icon_container = QWidget()
54
- icon_container_layout = QHBoxLayout(icon_container)
55
- icon_container_layout.addWidget(icon_label)
56
- icon_container_layout.setAlignment(Qt.AlignCenter)
57
- self.layout.insertWidget(0, icon_container)
46
+ self.layout.insertWidget(0, icon_container())
58
47
 
59
48
  shortcuts = {
60
49
  "M": "show master layer",
@@ -1,5 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0902, R0914
2
- from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QCheckBox, QDialogButtonBox
2
+ from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QDialogButtonBox
3
3
  from PySide6.QtCore import Qt, QTimer
4
4
  from .. algorithms.sharpen import unsharp_mask
5
5
  from .base_filter import BaseFilter
@@ -46,15 +46,8 @@ class UnsharpMaskFilter(BaseFilter):
46
46
  elif name == "Threshold":
47
47
  self.threshold_slider = slider
48
48
  value_labels[name] = value_label
49
-
50
- preview_check = QCheckBox("Preview")
51
- preview_check.setChecked(True)
52
- layout.addWidget(preview_check)
53
- button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
54
- layout.addWidget(button_box)
55
- preview_timer = QTimer()
56
- preview_timer.setSingleShot(True)
57
- preview_timer.setInterval(200)
49
+ preview_check, preview_timer, button_box = self.create_base_widgets(
50
+ layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
58
51
 
59
52
  def update_value(name, value, max_val, fmt):
60
53
  float_value = max_val * value / self.max_range
@@ -1,6 +1,6 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917
2
2
  from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QFrame, QVBoxLayout, QLabel, QDialog,
3
- QApplication, QSlider, QCheckBox, QDialogButtonBox)
3
+ QApplication, QSlider, QDialogButtonBox)
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
@@ -47,18 +47,10 @@ class WhiteBalanceFilter(BaseFilter):
47
47
  layout.addLayout(row_layout)
48
48
  pick_button = QPushButton("Pick Color")
49
49
  layout.addWidget(pick_button)
50
- preview_check = QCheckBox("Preview")
51
- preview_check.setChecked(True)
52
- layout.addWidget(preview_check)
53
- button_box = QDialogButtonBox(
54
- QDialogButtonBox.Ok |
55
- QDialogButtonBox.Reset |
56
- QDialogButtonBox.Cancel
57
- )
58
- layout.addWidget(button_box)
59
- self.preview_timer = QTimer()
60
- self.preview_timer.setSingleShot(True)
61
- self.preview_timer.setInterval(200)
50
+ preview_check, self.preview_timer, button_box = self.create_base_widgets(
51
+ layout,
52
+ QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel,
53
+ 200)
62
54
  for slider in self.sliders.values():
63
55
  slider.valueChanged.connect(self.on_slider_change)
64
56
  self.preview_timer.timeout.connect(do_preview)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 0.3.5
3
+ Version: 0.4.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -37,26 +37,22 @@ Dynamic: license-file
37
37
  [![PyPI version](https://img.shields.io/pypi/v/shinestacker?color=success)](https://pypi.org/project/shinestacker/)
38
38
  [![Python Versions](https://img.shields.io/pypi/pyversions/shinestacker)](https://pypi.org/project/shinestacker/)
39
39
  [![Qt Versions](https://img.shields.io/badge/Qt-6-blue.svg?&logo=Qt&logoWidth=18&logoColor=white)](https://www.qt.io/qt-for-python)
40
- [![pylint](https://img.shields.io/badge/PyLint-9.98-yellow?logo=python&logoColor=white)](https://github.com/lucalista/shinestacker/blob/main/.github/workflows/pylint.yml)
41
- [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
42
- <!--
40
+ [![pylint](https://img.shields.io/badge/PyLint-10.00-brightgreen?logo=python&logoColor=white)](https://github.com/lucalista/shinestacker/blob/main/.github/workflows/pylint.yml)
43
41
  [![codecov](https://codecov.io/github/lucalista/shinestacker/graph/badge.svg?token=Y5NKW6VH5G)](https://codecov.io/github/lucalista/shinestacker)
44
- -->
45
-
42
+ [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
43
+ [![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
46
44
 
47
45
  <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">
48
46
 
49
47
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/coffee_stack.jpg' width="400" referrerpolicy="no-referrer">
50
48
 
51
- <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">
52
49
  > **Focus stacking** for microscopy, macro photography, and computational imaging
53
50
 
54
51
  ## Key Features
55
52
  - 🚀 **Batch Processing**: Align, balance, and stack hundreds of images
56
- - 🎨 **Hybrid Workflows**: Combine Python scripting with GUI refinement
57
53
  - 🧩 **Modular Architecture**: Mix-and-match processing modules
58
54
  - 🖌️ **Retouch Editing**: Final interactive retouch of stacked image from individual frames
59
- - 📊 **Jupyter Integration**: Reproducible research notebooks
55
+ - 📊 **Jupyter Integration**: Image processing python notebooks
60
56
 
61
57
  ## Interactive GUI
62
58
 
@@ -77,7 +73,7 @@ The GUI has two main working areas:
77
73
 
78
74
  # Credits
79
75
 
80
- The main pyramid 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 for initial versions of this package. The implementation in the latest releases was rewritten from the original code.
76
+ 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.
81
77
 
82
78
  # Resources
83
79
 
@@ -87,7 +83,8 @@ Pyramid methods in image processing
87
83
 
88
84
  # License
89
85
 
90
- The software is provided as is under the [GNU Lesser General Public License v3.0](https://choosealicense.com/licenses/lgpl-3.0/).
86
+ <img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
87
+ 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.
91
88
 
92
89
  # Attribution request
93
90
  📸 If you publish images created with Shine Stacker, please consider adding a note such as: