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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +37 -20
- shinestacker/algorithms/balance.py +2 -1
- shinestacker/algorithms/base_stack_algo.py +2 -1
- shinestacker/algorithms/multilayer.py +11 -8
- shinestacker/algorithms/noise_detection.py +13 -7
- shinestacker/algorithms/stack.py +5 -4
- shinestacker/algorithms/stack_framework.py +12 -10
- shinestacker/app/main.py +1 -1
- shinestacker/config/config.py +1 -0
- shinestacker/config/constants.py +8 -1
- shinestacker/core/framework.py +15 -10
- shinestacker/gui/action_config.py +11 -7
- shinestacker/gui/gui_logging.py +8 -7
- shinestacker/gui/gui_run.py +8 -8
- shinestacker/gui/main_window.py +4 -4
- shinestacker/gui/new_project.py +31 -17
- shinestacker/gui/project_converter.py +0 -1
- shinestacker/gui/select_path_widget.py +3 -1
- shinestacker/retouch/image_editor.py +1 -1
- shinestacker/retouch/image_editor_ui.py +2 -1
- shinestacker/retouch/image_viewer.py +104 -20
- shinestacker/retouch/io_gui_handler.py +17 -16
- shinestacker/retouch/io_manager.py +0 -1
- shinestacker/retouch/layer_collection.py +2 -1
- {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/METADATA +5 -4
- {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/RECORD +31 -31
- shinestacker-0.4.0.dist-info/licenses/LICENSE +165 -0
- shinestacker-0.3.6.dist-info/licenses/LICENSE +0 -1
- {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.6.dist-info → shinestacker-0.4.0.dist-info}/top_level.txt +0 -0
shinestacker/gui/new_project.py
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
self.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
self.
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
146
|
-
extension = self.
|
|
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.
|
|
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.
|
|
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.
|
|
176
|
-
self.save_master_to_path(self.
|
|
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.
|
|
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.
|
|
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()
|
|
@@ -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
|
-
|
|
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
|
+
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
|
[](https://pypi.org/project/shinestacker/)
|
|
38
38
|
[](https://pypi.org/project/shinestacker/)
|
|
39
39
|
[](https://www.qt.io/qt-for-python)
|
|
40
|
-
[](https://github.com/lucalista/shinestacker/blob/main/.github/workflows/pylint.yml)
|
|
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
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
|
-
|
|
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=
|
|
2
|
+
shinestacker/_version.py,sha256=DObMj8zITWgJRRICOQXNFEgLDtZ9uQZUVwbNAU-P3oc,21
|
|
3
3
|
shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
|
|
4
|
-
shinestacker/algorithms/align.py,sha256=
|
|
5
|
-
shinestacker/algorithms/balance.py,sha256=
|
|
6
|
-
shinestacker/algorithms/base_stack_algo.py,sha256=
|
|
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=
|
|
11
|
-
shinestacker/algorithms/noise_detection.py,sha256=
|
|
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=
|
|
15
|
-
shinestacker/algorithms/stack_framework.py,sha256=
|
|
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=
|
|
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=
|
|
30
|
-
shinestacker/config/constants.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
44
|
-
shinestacker/gui/gui_run.py,sha256=
|
|
45
|
-
shinestacker/gui/main_window.py,sha256=
|
|
46
|
-
shinestacker/gui/new_project.py,sha256=
|
|
47
|
-
shinestacker/gui/project_converter.py,sha256=
|
|
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=
|
|
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=
|
|
72
|
-
shinestacker/retouch/image_editor_ui.py,sha256=
|
|
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=
|
|
75
|
-
shinestacker/retouch/io_gui_handler.py,sha256=
|
|
76
|
-
shinestacker/retouch/io_manager.py,sha256=
|
|
77
|
-
shinestacker/retouch/layer_collection.py,sha256=
|
|
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.
|
|
83
|
-
shinestacker-0.
|
|
84
|
-
shinestacker-0.
|
|
85
|
-
shinestacker-0.
|
|
86
|
-
shinestacker-0.
|
|
87
|
-
shinestacker-0.
|
|
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,,
|