shinestacker 0.3.6__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 (32) 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/stack.py +5 -4
  8. shinestacker/algorithms/stack_framework.py +12 -10
  9. shinestacker/app/main.py +1 -1
  10. shinestacker/config/config.py +1 -0
  11. shinestacker/config/constants.py +8 -1
  12. shinestacker/core/framework.py +15 -10
  13. shinestacker/gui/action_config.py +11 -7
  14. shinestacker/gui/gui_logging.py +8 -7
  15. shinestacker/gui/gui_run.py +8 -8
  16. shinestacker/gui/main_window.py +4 -4
  17. shinestacker/gui/new_project.py +31 -17
  18. shinestacker/gui/project_converter.py +0 -1
  19. shinestacker/gui/select_path_widget.py +3 -1
  20. shinestacker/retouch/image_editor.py +1 -1
  21. shinestacker/retouch/image_editor_ui.py +2 -1
  22. shinestacker/retouch/image_viewer.py +104 -20
  23. shinestacker/retouch/io_gui_handler.py +17 -16
  24. shinestacker/retouch/io_manager.py +0 -1
  25. shinestacker/retouch/layer_collection.py +2 -1
  26. {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/METADATA +5 -4
  27. {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/RECORD +31 -31
  28. shinestacker-0.4.0.dist-info/licenses/LICENSE +165 -0
  29. shinestacker-0.3.6.dist-info/licenses/LICENSE +0 -1
  30. {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/WHEEL +0 -0
  31. {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/entry_points.txt +0 -0
  32. {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,8 @@ from .. config.constants import constants
9
9
  from .. algorithms.stack import get_bunches
10
10
  from .select_path_widget import create_select_file_paths_widget
11
11
 
12
+ DEFAULT_NO_COUNT_LABEL = " - "
13
+
12
14
 
13
15
  class NewProjectDialog(QDialog):
14
16
  def __init__(self, parent=None):
@@ -51,8 +53,9 @@ class NewProjectDialog(QDialog):
51
53
  spacer.setFixedHeight(10)
52
54
  self.layout.addRow(spacer)
53
55
 
54
- container = create_select_file_paths_widget('', 'input files folder', 'input files folder')
55
-
56
+ self.input_folder, container = create_select_file_paths_widget(
57
+ '', 'input files folder', 'input files folder')
58
+ self.input_folder.textChanged.connect(self.update_bunches_label)
56
59
  self.noise_detection = QCheckBox()
57
60
  self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
58
61
  self.vignetting_correction = QCheckBox()
@@ -72,7 +75,8 @@ class NewProjectDialog(QDialog):
72
75
  bunch_overlap_range = gui_constants.NEW_PROJECT_BUNCH_OVERLAP
73
76
  self.bunch_overlap.setRange(bunch_overlap_range['min'], bunch_overlap_range['max'])
74
77
  self.bunch_overlap.setValue(constants.DEFAULT_OVERLAP)
75
- self.bunches_label = QLabel("")
78
+ self.bunches_label = QLabel(DEFAULT_NO_COUNT_LABEL)
79
+ self.frames_label = QLabel(DEFAULT_NO_COUNT_LABEL)
76
80
 
77
81
  self.update_bunch_options(gui_constants.NEW_PROJECT_BUNCH_STACK)
78
82
  self.bunch_stack.toggled.connect(self.update_bunch_options)
@@ -88,6 +92,7 @@ class NewProjectDialog(QDialog):
88
92
 
89
93
  self.add_bold_label("Select input:")
90
94
  self.layout.addRow("Input folder:", container)
95
+ self.layout.addRow("Number of frames: ", self.frames_label)
91
96
  self.add_bold_label("Select actions:")
92
97
  if self.expert():
93
98
  self.layout.addRow("Automatic noise detection:", self.noise_detection)
@@ -111,25 +116,34 @@ class NewProjectDialog(QDialog):
111
116
  self.update_bunches_label()
112
117
 
113
118
  def update_bunches_label(self):
119
+ if not self.input_folder.text():
120
+ return
121
+
122
+ def count_image_files(path):
123
+ if path == '' or not os.path.isdir(path):
124
+ return 0
125
+ extensions = ['jpg', 'jpeg', 'tif', 'tiff']
126
+ count = 0
127
+ for filename in os.listdir(path):
128
+ if '.' in filename:
129
+ ext = filename.lower().split('.')[-1]
130
+ if ext in extensions:
131
+ count += 1
132
+ return count
133
+
134
+ n_image_files = count_image_files(self.input_folder.text())
135
+ if n_image_files == 0:
136
+ self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
137
+ self.frames_label.setText(DEFAULT_NO_COUNT_LABEL)
138
+ return
139
+ self.frames_label.setText(f"{n_image_files}")
114
140
  if self.bunch_stack.isChecked():
115
- def count_image_files(path):
116
- if path == '' or not os.path.isdir(path):
117
- return 0
118
- extensions = ['jpg', 'jpeg', 'tif', 'tiff']
119
- count = 0
120
- for filename in os.listdir(path):
121
- if '.' in filename:
122
- ext = filename.lower().split('.')[-1]
123
- if ext in extensions:
124
- count += 1
125
- return count
126
-
127
- bunches = get_bunches(list(range(count_image_files(self.input_folder.text()))),
141
+ bunches = get_bunches(list(range(n_image_files)),
128
142
  self.bunch_frames.value(),
129
143
  self.bunch_overlap.value())
130
144
  self.bunches_label.setText(f"{len(bunches)}")
131
145
  else:
132
- self.bunches_label.setText(" - ")
146
+ self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
133
147
 
134
148
  def accept(self):
135
149
  input_folder = self.input_folder.text()
@@ -112,7 +112,6 @@ class ProjectConverter:
112
112
  else:
113
113
  raise InvalidOptionError('stacker', stacker, f"valid options are: "
114
114
  f"{constants.STACK_ALGO_PYRAMID}, "
115
- f"{constants.STACK_ALGO_PYRAMID_BLOCK}, "
116
115
  f"{constants.STACK_ALGO_DEPTH_MAP}")
117
116
  if action_config.type_name == constants.ACTION_FOCUSSTACK:
118
117
  return FocusStack(**module_dict, stack_algo=stack_algo)
@@ -9,6 +9,7 @@ def create_layout_widget_no_margins(layout):
9
9
  container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
10
10
  return container
11
11
 
12
+
12
13
  def create_layout_widget_and_connect(button, edit, browse):
13
14
  button.clicked.connect(browse)
14
15
  button.setAutoDefault(False)
@@ -17,6 +18,7 @@ def create_layout_widget_and_connect(button, edit, browse):
17
18
  layout.addWidget(button)
18
19
  return create_layout_widget_no_margins(layout)
19
20
 
21
+
20
22
  def create_select_file_paths_widget(value, placeholder, tag):
21
23
  edit = QLineEdit(value)
22
24
  edit.setPlaceholderText(placeholder)
@@ -27,4 +29,4 @@ def create_select_file_paths_widget(value, placeholder, tag):
27
29
  if path:
28
30
  edit.setText(path)
29
31
 
30
- return create_layout_widget_and_connect(button, edit, browse)
32
+ return edit, create_layout_widget_and_connect(button, edit, browse)
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 0.3.6
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,10 +37,10 @@ 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)
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)
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
44
 
45
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">
46
46
 
@@ -83,7 +83,8 @@ Pyramid methods in image processing
83
83
 
84
84
  # License
85
85
 
86
- 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.
87
88
 
88
89
  # Attribution request
89
90
  📸 If you publish images created with Shine Stacker, please consider adding a note such as:
@@ -1,18 +1,18 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=IbpUPwvtjLOqowcOFsWQ6LKq-FH6cI19IpvfQlxufq0,21
2
+ shinestacker/_version.py,sha256=DObMj8zITWgJRRICOQXNFEgLDtZ9uQZUVwbNAU-P3oc,21
3
3
  shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
4
- shinestacker/algorithms/align.py,sha256=1CAnVhxaYO-SUd86Mmj7lTmaqlrmUWlF-HEM5341gcs,17166
5
- shinestacker/algorithms/balance.py,sha256=ZBcw2Ck-CfuobIG1FxFsGVjnLvD1rtVNrTO-GrFbi3Q,16441
6
- shinestacker/algorithms/base_stack_algo.py,sha256=EAdVcO2UDq6UwqWZwGZFF7XXJvysyxqqVoswz4PLCdo,1353
4
+ shinestacker/algorithms/align.py,sha256=FKGcDrp20jubY-LWA1OBqO-V781AoP8jCLH5xZm9nhk,17902
5
+ shinestacker/algorithms/balance.py,sha256=y68L5h8yuIuGZI3g5Zhj-jLXXOPsxHVGEhNTbCc2tlI,16518
6
+ shinestacker/algorithms/base_stack_algo.py,sha256=AFV2QkcFNaTcnISpsWHuAVy2De9hhaPcBNjE1O0h50I,1430
7
7
  shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
8
8
  shinestacker/algorithms/depth_map.py,sha256=b88GqbRXEU3wCXBxMcStlgZ4sFJicoiZfJMD30Z4b98,7364
9
9
  shinestacker/algorithms/exif.py,sha256=gY9s6Cd4g4swo5qEjSbzuVIvl1GImCYu6ytOO9WrV0I,9435
10
- shinestacker/algorithms/multilayer.py,sha256=4Y6XlNJHFW74iNDFIeq_zdVtwLBnrieeMd708zJX-lo,8994
11
- shinestacker/algorithms/noise_detection.py,sha256=PmscQWi2v3ERTSf8SejkkSZXmTixKvh4NV9CtfuoUfM,8564
10
+ shinestacker/algorithms/multilayer.py,sha256=5JA6TW8oO_R3mu6cOvPno9by4md8q5sXUb8ZfsRRpmY,9259
11
+ shinestacker/algorithms/noise_detection.py,sha256=CDnN8pglxufY5Y-dT3mVooD4zPySdSq9CMgtDGMXBnA,8970
12
12
  shinestacker/algorithms/pyramid.py,sha256=_Pk19lRQ21b3W3aHQ6DgAe9VVOfbsi2a9jrynF0qFVw,8610
13
13
  shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
14
- shinestacker/algorithms/stack.py,sha256=IAa24rPMXl7F5yfcy0nw-fjsgGPpUkxqeKMqLHqvee8,4796
15
- shinestacker/algorithms/stack_framework.py,sha256=WZLudhjk6piQz2JULShxcoC3-3mSD-Bgh4_VT7JeG7c,12293
14
+ shinestacker/algorithms/stack.py,sha256=FCU89Of-s6C_DuMleG06c8V6fnIm9MFInvkkKtTsGBo,4906
15
+ shinestacker/algorithms/stack_framework.py,sha256=frw7sbc9qOfVBYP3ZOFZEaIn9O27Wms8j_mxSW79uI0,12460
16
16
  shinestacker/algorithms/utils.py,sha256=VLm6eZmcAk2QPvomT4d1q56laJSYfbCQmiwI2Rmuu_s,2171
17
17
  shinestacker/algorithms/vignetting.py,sha256=wFwi20ob1O3Memav1XQrtrOHgOtKRiK1RV4E-ex69r8,7470
18
18
  shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
@@ -21,33 +21,33 @@ shinestacker/app/about_dialog.py,sha256=QzZgTcLvkSP3_FhmPOUnwQ_YSxwJdeFrU2IAVYKD
21
21
  shinestacker/app/app_config.py,sha256=eTIRxp0t7Wic46jMTe_oY3kz7ktZbdM43C3bjshVDKg,494
22
22
  shinestacker/app/gui_utils.py,sha256=ptbUKjv5atbx5vW912_j8BVmDZpovAqZDEC48d0R2vA,2331
23
23
  shinestacker/app/help_menu.py,sha256=UOlabEY_EKV2Q1BoiU2JAM1udSSBAwXlL7d58bqxKe0,516
24
- shinestacker/app/main.py,sha256=RAf9WiCipYLK1rrwnXyL1sWq_28zDl9Z_eipfrdtSuY,6421
24
+ shinestacker/app/main.py,sha256=tUb9aatktRJ1_oXSR3WPRubp7GDt-77mIjsHR7-0euE,6436
25
25
  shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
26
26
  shinestacker/app/project.py,sha256=ir98-zogYmvx2QYvFbAaBUqLL03qWYkoMOIvLvmQy_w,2736
27
27
  shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
28
28
  shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
29
- shinestacker/config/config.py,sha256=BshNb20Dx5HqdlpsTQbx4p-LnQ5uBP2q-h9v3pl84ss,1635
30
- shinestacker/config/constants.py,sha256=79bOcE44MZ0WuAVPjDwwhvNrsQTlHGyIOwmqwlLOfMU,5776
29
+ shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
30
+ shinestacker/config/constants.py,sha256=MeZ15b7xIYJoN6EeiuR_OKi4sP-7_E7OtzrETzIowZI,5976
31
31
  shinestacker/config/gui_constants.py,sha256=002r96jtxV4Acel7q5NgECrcsDJzW-kOStEHqam-5Gg,2492
32
32
  shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
33
33
  shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
34
34
  shinestacker/core/core_utils.py,sha256=ulJhzen5McAb5n6wWNA_KB4U_PdTEr-H2TCQkVKUaOw,1421
35
35
  shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk2c,1618
36
- shinestacker/core/framework.py,sha256=3Q3zalyZeCiUHXYbBiYadWNdtyD_3j3dcymk5_3NajM,7063
36
+ shinestacker/core/framework.py,sha256=zCnJuQrHNpwEgJW23_BgS7iQrLolRWTAMB1oRp_a7Kk,7447
37
37
  shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
38
38
  shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- shinestacker/gui/action_config.py,sha256=RpUHjq8lmiGJQPnp55O1yd3ZPiLQG3R7jZ52m1VmiQc,48678
39
+ shinestacker/gui/action_config.py,sha256=zWzTVkySEYfODJ620wQk6B2dr1C-YSxtDicJ0XXrU_M,48955
40
40
  shinestacker/gui/actions_window.py,sha256=-ehMkGshsH22HSnn33ThAMXy7tR_cqWr14mEnXDTfXk,12025
41
41
  shinestacker/gui/colors.py,sha256=zgLRcC3fAzklx7zzyjLEsMX2i64YTxGUmQM2woYBZuw,1344
42
42
  shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
43
- shinestacker/gui/gui_logging.py,sha256=ESlk1EAQMdoT8pCZDFsvtU1UtF4h2GKv3wAhxJYxNis,8213
44
- shinestacker/gui/gui_run.py,sha256=bYXX4N__Ez7JMIJtVcTmLF2PJ3y9bCd-uvlOHsV-4gg,16230
45
- shinestacker/gui/main_window.py,sha256=z14PWRVDRbuySM05YCCFrn1DPU3U96xwQHy730oiLkw,28577
46
- shinestacker/gui/new_project.py,sha256=cs3641RW3Uiy2VfwxeM-k254rH4BNykJyojmwxzrEi8,7758
47
- shinestacker/gui/project_converter.py,sha256=v-oldaw77VLsywhQcl5uhTtPD7GbGFeJo33JJRl3aG4,7453
43
+ shinestacker/gui/gui_logging.py,sha256=ciuwZU-_5TicPpjC83iZmcwuDWiBO17onYJRGyF0FaY,8227
44
+ shinestacker/gui/gui_run.py,sha256=n0OaPZn9C4SVpopMNKUpksMwV27xT0TGFn1inhmCFIk,16230
45
+ shinestacker/gui/main_window.py,sha256=Na3TVSWwzIJ5lpCAf1vjkCIo8tKfhYzFO_nxB8ekVJI,28589
46
+ shinestacker/gui/new_project.py,sha256=GSqYfonv-jdOEb4veXU6LxiDsAevr1TulzT0vsVazAk,8342
47
+ shinestacker/gui/project_converter.py,sha256=zZfXZg2h-PHh2Prr450B1UFADbZPzMBVkYhcpZkqPuk,7370
48
48
  shinestacker/gui/project_editor.py,sha256=zwmj7PFs7X06GY4tkoDBcOL4Tl0IGo4Mf13n2qGwaJY,22245
49
49
  shinestacker/gui/project_model.py,sha256=89L0IDSAqRK2mvU1EVIrcsJas8CU-aTzUIjdL1Cv0mw,4421
50
- shinestacker/gui/select_path_widget.py,sha256=JAxAkbQukPwBc27-EdeobxxJBG4IBfooiV-JZq3ttsY,1015
50
+ shinestacker/gui/select_path_widget.py,sha256=OfQImOmkzbvl5BBshmb7ePWrSGDJQ8VvyaAOypHAGd4,1023
51
51
  shinestacker/gui/ico/focus_stack_bkg.png,sha256=Q86TgqvKEi_IzKI8m6aZB2a3T40UkDtexf2PdeBM9XE,163151
52
52
  shinestacker/gui/ico/shinestacker.icns,sha256=m_6WQBx8sE9jQKwIRa_B5oa7_VcNn6e2TyijeQXPjwM,337563
53
53
  shinestacker/gui/ico/shinestacker.ico,sha256=yO0NaBWA0uFov_GqHuHQbymoqLtQKt5DPWpGGmRKie0,186277
@@ -68,20 +68,20 @@ shinestacker/retouch/exif_data.py,sha256=uA9ck9skp8ztSUdX1SFrApgtqmxrHtfWW3vsry8
68
68
  shinestacker/retouch/file_loader.py,sha256=723A_2w3cjn4rhvAzCq-__SWFelDRsMhkazgnb2h7Ig,4810
69
69
  shinestacker/retouch/filter_manager.py,sha256=SkioWTr6iFFpugUgZLg0a3m5b9EHdZAeyNFy39qk0z8,453
70
70
  shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
71
- shinestacker/retouch/image_editor.py,sha256=co5zUufgeb1WrD3aF1RVPh1MbcC9-92HSUa2iROnKk4,8503
72
- shinestacker/retouch/image_editor_ui.py,sha256=vfBALDuHtqSWIPmyfUirkygM1guwQG-gHo0AH0x8_jU,15712
71
+ shinestacker/retouch/image_editor.py,sha256=PxVLyXRoZVT9GmVIbiYDdfSUKqtQossmoMj7bYv97FE,8492
72
+ shinestacker/retouch/image_editor_ui.py,sha256=eacZAnU7Gh4Mri0DCLzBll6kCO4hkl89xysuMyndOQ0,15742
73
73
  shinestacker/retouch/image_filters.py,sha256=JF2a7VATO3CGQr5_OOIPi2k7b9HvHzrhhWS73x32t-A,2883
74
- shinestacker/retouch/image_viewer.py,sha256=oqBgaanPXWjzIaox5KSRhYOHoGvoYnWm7sqW3cLoY24,14803
75
- shinestacker/retouch/io_gui_handler.py,sha256=2jkbPXew95rMKO2aC9hwZJGtZRg3wCCtXI0SFFiNHUI,9089
76
- shinestacker/retouch/io_manager.py,sha256=sNcZVEttiVdxNBVs39ZvexqOcvtjl2CvJs6BVqmGvOM,2148
77
- shinestacker/retouch/layer_collection.py,sha256=j1NiGGtLZ3OwrftBVNT4rb0Kq0CfWAB3t2bUrqHx1Sk,5608
74
+ shinestacker/retouch/image_viewer.py,sha256=4kovjI8s5MWWA98oqNiQfe4xZvNRL_-UwnmSVK9r-gE,18639
75
+ shinestacker/retouch/io_gui_handler.py,sha256=hAgjCiYU1lopmzrIXHQggowj1D5yGncNG2EMdLroORc,9072
76
+ shinestacker/retouch/io_manager.py,sha256=QdxR6fbfx_J7uM-Yoptdp17jlmk45R30wmDE9ACRm_8,2112
77
+ shinestacker/retouch/layer_collection.py,sha256=cvAW6nbG-KdhbN6XI4SrhGwvqTYGPyrZBLWz-uZkIJ0,5672
78
78
  shinestacker/retouch/shortcuts_help.py,sha256=dlt7OSAr9thYuoEPlirTU_YRzv5xP9vy2-9mZO7GVAA,3308
79
79
  shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-KqMlQRo,3257
80
80
  shinestacker/retouch/unsharp_mask_filter.py,sha256=hNJlqXYjf9Nd8KlVy09fd4TxrHa9Ofef0ZLSMHjLL6I,3481
81
81
  shinestacker/retouch/white_balance_filter.py,sha256=2krwdz0X6qLWuCIEQcPtSQA_txfAsl7QUzfdsOLBrBU,4878
82
- shinestacker-0.3.6.dist-info/licenses/LICENSE,sha256=cBN0P3F6BWFkfOabkhuTxwJnK1B0v50jmmzZJjGGous,80
83
- shinestacker-0.3.6.dist-info/METADATA,sha256=1xiC2Bgn2sR8bqwuapC0QNodFy9fOO9p8ybGpqvHOwc,4915
84
- shinestacker-0.3.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
- shinestacker-0.3.6.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
86
- shinestacker-0.3.6.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
87
- shinestacker-0.3.6.dist-info/RECORD,,
82
+ shinestacker-0.4.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
83
+ shinestacker-0.4.0.dist-info/METADATA,sha256=SV1_0dnrc1f9gXLgqyE7AqTMLnxGDsodLfU-u9cbmak,5202
84
+ shinestacker-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
+ shinestacker-0.4.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
86
+ shinestacker-0.4.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
87
+ shinestacker-0.4.0.dist-info/RECORD,,