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.
- 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/pyramid.py +7 -4
- shinestacker/algorithms/stack.py +5 -4
- shinestacker/algorithms/stack_framework.py +12 -10
- shinestacker/app/app_config.py +4 -22
- shinestacker/app/main.py +1 -1
- shinestacker/config/config.py +22 -16
- shinestacker/config/constants.py +8 -1
- shinestacker/core/framework.py +15 -10
- shinestacker/gui/action_config.py +20 -41
- shinestacker/gui/actions_window.py +18 -47
- 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 +34 -37
- shinestacker/gui/project_converter.py +0 -1
- shinestacker/gui/project_editor.py +43 -20
- shinestacker/gui/select_path_widget.py +32 -0
- shinestacker/retouch/base_filter.py +12 -1
- shinestacker/retouch/denoise_filter.py +4 -10
- shinestacker/retouch/exif_data.py +3 -9
- shinestacker/retouch/icon_container.py +19 -0
- 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/retouch/shortcuts_help.py +2 -13
- shinestacker/retouch/unsharp_mask_filter.py +3 -10
- shinestacker/retouch/white_balance_filter.py +5 -13
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/METADATA +8 -11
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/RECORD +42 -41
- shinestacker-0.4.0.dist-info/licenses/LICENSE +165 -0
- shinestacker/algorithms/core_utils.py +0 -22
- shinestacker-0.3.5.dist-info/licenses/LICENSE +0 -1
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/entry_points.txt +0 -0
- {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,
|
|
3
|
-
from PySide6.QtCore import Qt
|
|
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 =
|
|
28
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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.
|
|
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,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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
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
|
[](https://pypi.org/project/shinestacker/)
|
|
38
38
|
[](https://pypi.org/project/shinestacker/)
|
|
39
39
|
[](https://www.qt.io/qt-for-python)
|
|
40
|
-
[](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
|
|
42
|
-
<!--
|
|
40
|
+
[](https://github.com/lucalista/shinestacker/blob/main/.github/workflows/pylint.yml)
|
|
43
41
|
[](https://codecov.io/github/lucalista/shinestacker)
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
[](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
|
|
43
|
+
[](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**:
|
|
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
|
|
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
|
-
|
|
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:
|