shinestacker 1.3.1__py3-none-any.whl → 1.5.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 +198 -18
- shinestacker/algorithms/align_parallel.py +17 -1
- shinestacker/algorithms/balance.py +23 -13
- shinestacker/algorithms/noise_detection.py +3 -1
- shinestacker/algorithms/utils.py +21 -10
- shinestacker/algorithms/vignetting.py +2 -0
- shinestacker/app/main.py +1 -1
- shinestacker/config/gui_constants.py +7 -2
- shinestacker/core/core_utils.py +10 -1
- shinestacker/gui/action_config.py +172 -7
- shinestacker/gui/action_config_dialog.py +246 -285
- shinestacker/gui/gui_run.py +2 -2
- shinestacker/gui/main_window.py +14 -5
- shinestacker/gui/menu_manager.py +26 -2
- shinestacker/gui/project_controller.py +4 -0
- shinestacker/gui/recent_file_manager.py +93 -0
- shinestacker/retouch/base_filter.py +13 -15
- shinestacker/retouch/brush_preview.py +3 -1
- shinestacker/retouch/brush_tool.py +11 -11
- shinestacker/retouch/display_manager.py +43 -59
- shinestacker/retouch/image_editor_ui.py +161 -82
- shinestacker/retouch/image_view_status.py +65 -0
- shinestacker/retouch/image_viewer.py +95 -431
- shinestacker/retouch/io_gui_handler.py +12 -2
- shinestacker/retouch/layer_collection.py +3 -0
- shinestacker/retouch/overlaid_view.py +215 -0
- shinestacker/retouch/shortcuts_help.py +13 -3
- shinestacker/retouch/sidebyside_view.py +477 -0
- shinestacker/retouch/transformation_manager.py +43 -0
- shinestacker/retouch/undo_manager.py +22 -3
- shinestacker/retouch/view_strategy.py +557 -0
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/METADATA +7 -7
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/RECORD +38 -32
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.3.1.dist-info → shinestacker-1.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0904, R0903, R0902, E1101, R0914
|
|
2
|
+
import math
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
import numpy as np
|
|
5
|
+
from PySide6.QtCore import Qt, QPointF, QTime, QPoint, Signal, QRectF
|
|
6
|
+
from PySide6.QtGui import QImage, QPainter, QColor, QBrush, QPen, QCursor, QPixmap
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QGraphicsEllipseItem, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem)
|
|
9
|
+
from .. config.gui_constants import gui_constants
|
|
10
|
+
from .layer_collection import LayerCollectionHandler
|
|
11
|
+
from .brush_gradient import create_default_brush_gradient
|
|
12
|
+
from .brush_preview import BrushPreviewItem
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ViewSignals:
|
|
16
|
+
temp_view_requested = Signal(bool)
|
|
17
|
+
brush_operation_started = Signal(QPoint)
|
|
18
|
+
brush_operation_continued = Signal(QPoint)
|
|
19
|
+
brush_operation_ended = Signal()
|
|
20
|
+
brush_size_change_requested = Signal(int) # +1 or -1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ImageGraphicsViewBase(QGraphicsView):
|
|
24
|
+
def __init__(self, parent=None):
|
|
25
|
+
super().__init__(parent)
|
|
26
|
+
self.setTransformationAnchor(QGraphicsView.AnchorViewCenter)
|
|
27
|
+
self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
|
|
28
|
+
self.setInteractive(False)
|
|
29
|
+
self.grabGesture(Qt.PinchGesture)
|
|
30
|
+
self.grabGesture(Qt.PanGesture)
|
|
31
|
+
self.setMouseTracking(True)
|
|
32
|
+
self.setDragMode(QGraphicsView.NoDrag)
|
|
33
|
+
self.setRenderHint(QPainter.Antialiasing)
|
|
34
|
+
self.setRenderHint(QPainter.SmoothPixmapTransform)
|
|
35
|
+
self.setCursor(Qt.BlankCursor)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ViewStrategy(LayerCollectionHandler):
|
|
39
|
+
def __init__(self, layer_collection, status):
|
|
40
|
+
LayerCollectionHandler.__init__(self, layer_collection)
|
|
41
|
+
self.display_manager = None
|
|
42
|
+
self.status = status
|
|
43
|
+
self.brush = None
|
|
44
|
+
self.brush_cursor = None
|
|
45
|
+
self.display_manager = None
|
|
46
|
+
self.brush_preview = BrushPreviewItem(layer_collection)
|
|
47
|
+
self.cursor_style = gui_constants.DEFAULT_CURSOR_STYLE
|
|
48
|
+
self.allow_cursor_preview = True
|
|
49
|
+
self.control_pressed = False
|
|
50
|
+
self.space_pressed = False
|
|
51
|
+
self.gesture_active = False
|
|
52
|
+
self.pinch_center_view = None
|
|
53
|
+
self.pinch_center_scene = None
|
|
54
|
+
self.pinch_start_scale = None
|
|
55
|
+
self.scrolling = False
|
|
56
|
+
self.dragging = False
|
|
57
|
+
self.last_brush_pos = None
|
|
58
|
+
self.last_mouse_pos = None
|
|
59
|
+
self.last_update_time = QTime.currentTime()
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def create_pixmaps(self):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def set_master_image(self, qimage):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def set_current_image(self, qimage):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def get_master_view(self):
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def get_current_view(self):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def get_master_scene(self):
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def get_current_scene(self):
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def get_views(self):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def get_scenes(self):
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def get_pixmaps(self):
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def get_master_pixmap(self):
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def get_current_pixmap(self):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def show_master(self):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
@abstractmethod
|
|
114
|
+
def show_current(self):
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def arrange_images(self):
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
def update_master_display(self):
|
|
122
|
+
if not self.empty():
|
|
123
|
+
master_qimage = self.numpy_to_qimage(self.master_layer())
|
|
124
|
+
if master_qimage:
|
|
125
|
+
pixmap = QPixmap.fromImage(master_qimage)
|
|
126
|
+
self.get_master_pixmap().setPixmap(pixmap)
|
|
127
|
+
self.get_master_scene().setSceneRect(QRectF(pixmap.rect()))
|
|
128
|
+
self.get_master_view().horizontalScrollBar().setValue(self.status.h_scroll)
|
|
129
|
+
self.get_master_view().verticalScrollBar().setValue(self.status.v_scroll)
|
|
130
|
+
self.arrange_images()
|
|
131
|
+
|
|
132
|
+
def update_current_display(self):
|
|
133
|
+
if not self.empty() and self.number_of_layers() > 0:
|
|
134
|
+
current_qimage = self.numpy_to_qimage(self.current_layer())
|
|
135
|
+
if current_qimage:
|
|
136
|
+
pixmap = QPixmap.fromImage(current_qimage)
|
|
137
|
+
self.get_current_pixmap().setPixmap(pixmap)
|
|
138
|
+
self.get_current_scene().setSceneRect(QRectF(pixmap.rect()))
|
|
139
|
+
self.get_current_view().horizontalScrollBar().setValue(self.status.h_scroll)
|
|
140
|
+
self.get_current_view().verticalScrollBar().setValue(self.status.v_scroll)
|
|
141
|
+
self.arrange_images()
|
|
142
|
+
|
|
143
|
+
def update_cursor_pen_width(self):
|
|
144
|
+
pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
|
|
145
|
+
if self.brush_cursor is not None:
|
|
146
|
+
master_pen = self.brush_cursor.pen()
|
|
147
|
+
master_pen.setWidthF(pen_width)
|
|
148
|
+
self.brush_cursor.setPen(master_pen)
|
|
149
|
+
return pen_width
|
|
150
|
+
|
|
151
|
+
def set_allow_cursor_preview(self, state):
|
|
152
|
+
self.allow_cursor_preview = state
|
|
153
|
+
|
|
154
|
+
def zoom_factor(self):
|
|
155
|
+
return self.status.zoom_factor
|
|
156
|
+
|
|
157
|
+
def set_zoom_factor(self, zoom_factor):
|
|
158
|
+
self.status.set_zoom_factor(zoom_factor)
|
|
159
|
+
|
|
160
|
+
def get_current_scale(self):
|
|
161
|
+
return self.get_master_view().transform().m11()
|
|
162
|
+
|
|
163
|
+
def min_scale(self):
|
|
164
|
+
return self.status.min_scale
|
|
165
|
+
|
|
166
|
+
def max_scale(self):
|
|
167
|
+
return self.status.max_scale
|
|
168
|
+
|
|
169
|
+
def set_min_scale(self, scale):
|
|
170
|
+
self.status.set_min_scale(scale)
|
|
171
|
+
|
|
172
|
+
def set_max_scale(self, scale):
|
|
173
|
+
self.status.set_max_scale(scale)
|
|
174
|
+
|
|
175
|
+
def empty(self):
|
|
176
|
+
return self.status.empty()
|
|
177
|
+
|
|
178
|
+
def set_brush(self, brush):
|
|
179
|
+
self.brush = brush
|
|
180
|
+
|
|
181
|
+
def set_preview_brush(self, brush):
|
|
182
|
+
self.brush_preview.brush = brush
|
|
183
|
+
|
|
184
|
+
def set_display_manager(self, dm):
|
|
185
|
+
self.display_manager = dm
|
|
186
|
+
|
|
187
|
+
def set_cursor_style(self, style):
|
|
188
|
+
self.cursor_style = style
|
|
189
|
+
if self.brush_cursor:
|
|
190
|
+
self.update_brush_cursor()
|
|
191
|
+
|
|
192
|
+
def get_cursor_style(self):
|
|
193
|
+
return self.cursor_style
|
|
194
|
+
|
|
195
|
+
def handle_key_press_event(self, _event):
|
|
196
|
+
return True
|
|
197
|
+
|
|
198
|
+
def handle_key_release_event(self, _event):
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
def clear_image(self):
|
|
202
|
+
for scene in self.get_scenes():
|
|
203
|
+
scene.clear()
|
|
204
|
+
self.create_pixmaps()
|
|
205
|
+
self.status.clear()
|
|
206
|
+
self.setup_brush_cursor()
|
|
207
|
+
self.brush_preview = BrushPreviewItem(self.layer_collection)
|
|
208
|
+
self.get_master_scene().addItem(self.brush_preview)
|
|
209
|
+
self.setCursor(Qt.ArrowCursor)
|
|
210
|
+
if self.brush_cursor:
|
|
211
|
+
self.brush_cursor.hide()
|
|
212
|
+
|
|
213
|
+
def cleanup_brush_preview(self):
|
|
214
|
+
if self.brush_cursor:
|
|
215
|
+
self.brush_cursor.hide()
|
|
216
|
+
self.brush_preview.hide()
|
|
217
|
+
|
|
218
|
+
def set_master_image_np(self, img):
|
|
219
|
+
self.set_master_image(self.numpy_to_qimage(img))
|
|
220
|
+
|
|
221
|
+
def numpy_to_qimage(self, array):
|
|
222
|
+
if array is None:
|
|
223
|
+
return None
|
|
224
|
+
if array.dtype == np.uint16:
|
|
225
|
+
array = np.right_shift(array, 8).astype(np.uint8)
|
|
226
|
+
if array.ndim == 2:
|
|
227
|
+
height, width = array.shape
|
|
228
|
+
return QImage(memoryview(array), width, height, width, QImage.Format_Grayscale8)
|
|
229
|
+
if array.ndim == 3:
|
|
230
|
+
height, width, _ = array.shape
|
|
231
|
+
if not array.flags['C_CONTIGUOUS']:
|
|
232
|
+
array = np.ascontiguousarray(array)
|
|
233
|
+
return QImage(memoryview(array), width, height, 3 * width, QImage.Format_RGB888)
|
|
234
|
+
return QImage()
|
|
235
|
+
|
|
236
|
+
def create_scene(self, view):
|
|
237
|
+
scene = QGraphicsScene()
|
|
238
|
+
view.setScene(scene)
|
|
239
|
+
scene.setBackgroundBrush(QBrush(QColor(120, 120, 120)))
|
|
240
|
+
return scene
|
|
241
|
+
|
|
242
|
+
def create_pixmap(self, scene):
|
|
243
|
+
pixmap_item = QGraphicsPixmapItem()
|
|
244
|
+
scene.addItem(pixmap_item)
|
|
245
|
+
return pixmap_item
|
|
246
|
+
|
|
247
|
+
def refresh_display(self):
|
|
248
|
+
for scene in self.get_scenes():
|
|
249
|
+
scene.update()
|
|
250
|
+
self.update_brush_cursor()
|
|
251
|
+
|
|
252
|
+
def set_max_min_scales(self, img_width, img_height):
|
|
253
|
+
self.set_min_scale(min(gui_constants.MIN_ZOOMED_IMG_WIDTH / img_width,
|
|
254
|
+
gui_constants.MIN_ZOOMED_IMG_HEIGHT / img_height))
|
|
255
|
+
self.set_max_scale(gui_constants.MAX_ZOOMED_IMG_PX_SIZE)
|
|
256
|
+
|
|
257
|
+
def zoom_in(self):
|
|
258
|
+
if self.empty():
|
|
259
|
+
return
|
|
260
|
+
master_view = self.get_master_view()
|
|
261
|
+
old_center = master_view.mapToScene(master_view.viewport().rect().center())
|
|
262
|
+
current_scale = self.get_current_scale()
|
|
263
|
+
new_scale = current_scale * gui_constants.ZOOM_IN_FACTOR
|
|
264
|
+
if new_scale <= self.max_scale():
|
|
265
|
+
for view in self.get_views():
|
|
266
|
+
view.scale(gui_constants.ZOOM_IN_FACTOR, gui_constants.ZOOM_IN_FACTOR)
|
|
267
|
+
self.set_zoom_factor(new_scale)
|
|
268
|
+
master_view.centerOn(old_center)
|
|
269
|
+
self.update_brush_cursor()
|
|
270
|
+
self.update_cursor_pen_width()
|
|
271
|
+
|
|
272
|
+
def apply_zoom(self):
|
|
273
|
+
if self.empty():
|
|
274
|
+
return
|
|
275
|
+
for view in self.get_views():
|
|
276
|
+
current_scale = view.transform().m11()
|
|
277
|
+
scale_factor = self.zoom_factor() / current_scale
|
|
278
|
+
view.scale(scale_factor, scale_factor)
|
|
279
|
+
|
|
280
|
+
def zoom_out(self):
|
|
281
|
+
if self.empty():
|
|
282
|
+
return
|
|
283
|
+
master_view = self.get_master_view()
|
|
284
|
+
old_center = master_view.mapToScene(master_view.viewport().rect().center())
|
|
285
|
+
current_scale = self.get_current_scale()
|
|
286
|
+
new_scale = current_scale * gui_constants.ZOOM_OUT_FACTOR
|
|
287
|
+
if new_scale >= self.min_scale():
|
|
288
|
+
for view in self.get_views():
|
|
289
|
+
view.scale(gui_constants.ZOOM_OUT_FACTOR, gui_constants.ZOOM_OUT_FACTOR)
|
|
290
|
+
self.set_zoom_factor(new_scale)
|
|
291
|
+
master_view.centerOn(old_center)
|
|
292
|
+
self.update_brush_cursor()
|
|
293
|
+
self.update_cursor_pen_width()
|
|
294
|
+
|
|
295
|
+
def reset_zoom(self):
|
|
296
|
+
if self.empty():
|
|
297
|
+
return
|
|
298
|
+
self.pinch_start_scale = 1.0
|
|
299
|
+
self.gesture_active = False
|
|
300
|
+
self.pinch_center_view = None
|
|
301
|
+
self.pinch_center_scene = None
|
|
302
|
+
for pixmap, view in self.get_pixmaps().items():
|
|
303
|
+
view.fitInView(pixmap, Qt.KeepAspectRatio)
|
|
304
|
+
self.set_zoom_factor(self.get_current_scale())
|
|
305
|
+
self.set_zoom_factor(max(self.min_scale(), min(self.max_scale(), self.zoom_factor())))
|
|
306
|
+
for view in self.get_views():
|
|
307
|
+
view.resetTransform()
|
|
308
|
+
view.scale(self.zoom_factor(), self.zoom_factor())
|
|
309
|
+
self.update_brush_cursor()
|
|
310
|
+
self.update_cursor_pen_width()
|
|
311
|
+
|
|
312
|
+
def actual_size(self):
|
|
313
|
+
if self.empty():
|
|
314
|
+
return
|
|
315
|
+
self.set_zoom_factor(max(self.min_scale(), min(self.max_scale(), 1.0)))
|
|
316
|
+
for view in self.get_views():
|
|
317
|
+
view.resetTransform()
|
|
318
|
+
view.scale(self.zoom_factor(), self.zoom_factor())
|
|
319
|
+
self.update_brush_cursor()
|
|
320
|
+
self.update_cursor_pen_width()
|
|
321
|
+
|
|
322
|
+
def setup_outline_style(self):
|
|
323
|
+
self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
|
|
324
|
+
gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()))
|
|
325
|
+
self.brush_cursor.setBrush(Qt.NoBrush)
|
|
326
|
+
|
|
327
|
+
def setup_simple_brush_style(self, center_x, center_y, radius):
|
|
328
|
+
gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
|
|
329
|
+
self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
|
|
330
|
+
gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()))
|
|
331
|
+
self.brush_cursor.setBrush(QBrush(gradient))
|
|
332
|
+
|
|
333
|
+
def setup_brush_cursor(self):
|
|
334
|
+
if not self.brush:
|
|
335
|
+
return
|
|
336
|
+
scene = self.get_master_scene()
|
|
337
|
+
for item in scene.items():
|
|
338
|
+
if isinstance(item, QGraphicsEllipseItem) and item != self.brush_preview:
|
|
339
|
+
scene.removeItem(item)
|
|
340
|
+
pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), 1)
|
|
341
|
+
brush = Qt.NoBrush
|
|
342
|
+
self.brush_cursor = scene.addEllipse(
|
|
343
|
+
0, 0, self.brush.size, self.brush.size, pen, brush)
|
|
344
|
+
self.brush_cursor.setZValue(1000)
|
|
345
|
+
self.brush_cursor.hide()
|
|
346
|
+
|
|
347
|
+
def update_brush_cursor(self):
|
|
348
|
+
if self.empty() or not self.brush_cursor or not self.isVisible():
|
|
349
|
+
return
|
|
350
|
+
self.update_cursor_pen_width()
|
|
351
|
+
master_view = self.get_master_view()
|
|
352
|
+
mouse_pos = master_view.mapFromGlobal(QCursor.pos())
|
|
353
|
+
if not master_view.rect().contains(mouse_pos):
|
|
354
|
+
self.brush_cursor.hide()
|
|
355
|
+
return
|
|
356
|
+
scene_pos = master_view.mapToScene(mouse_pos)
|
|
357
|
+
size = self.brush.size
|
|
358
|
+
radius = size / 2
|
|
359
|
+
self.brush_cursor.setRect(scene_pos.x() - radius, scene_pos.y() - radius, size, size)
|
|
360
|
+
allow_cursor_preview = self.display_manager.allow_cursor_preview()
|
|
361
|
+
if self.cursor_style == 'preview':
|
|
362
|
+
self.setup_outline_style()
|
|
363
|
+
if allow_cursor_preview:
|
|
364
|
+
self.brush_cursor.hide()
|
|
365
|
+
pos = QCursor.pos()
|
|
366
|
+
if isinstance(pos, QPointF):
|
|
367
|
+
scene_pos = pos
|
|
368
|
+
else:
|
|
369
|
+
cursor_pos = master_view.mapFromGlobal(pos)
|
|
370
|
+
scene_pos = master_view.mapToScene(cursor_pos)
|
|
371
|
+
self.brush_preview.update(scene_pos, int(size))
|
|
372
|
+
else:
|
|
373
|
+
self.brush_preview.hide()
|
|
374
|
+
if self.cursor_style == 'outline':
|
|
375
|
+
self.setup_outline_style()
|
|
376
|
+
else:
|
|
377
|
+
self.setup_simple_brush_style(scene_pos.x(), scene_pos.y(), radius)
|
|
378
|
+
if not self.brush_cursor.isVisible():
|
|
379
|
+
self.brush_cursor.show()
|
|
380
|
+
|
|
381
|
+
def position_on_image(self, pos):
|
|
382
|
+
master_view = self.get_master_view()
|
|
383
|
+
pixmap = self.get_master_pixmap()
|
|
384
|
+
scene_pos = master_view.mapToScene(pos)
|
|
385
|
+
item_pos = pixmap.mapFromScene(scene_pos)
|
|
386
|
+
return item_pos
|
|
387
|
+
|
|
388
|
+
def get_visible_image_region(self):
|
|
389
|
+
if self.empty():
|
|
390
|
+
return None
|
|
391
|
+
master_view = self.get_master_view()
|
|
392
|
+
master_pixmap = self.get_master_pixmap()
|
|
393
|
+
pixmap = self.get_master_pixmap()
|
|
394
|
+
view_rect = master_view.viewport().rect()
|
|
395
|
+
scene_rect = master_view.mapToScene(view_rect).boundingRect()
|
|
396
|
+
image_rect = master_pixmap.mapFromScene(scene_rect).boundingRect().toRect()
|
|
397
|
+
return image_rect.intersected(pixmap.boundingRect().toRect())
|
|
398
|
+
|
|
399
|
+
def get_visible_image_portion(self):
|
|
400
|
+
if self.has_no_master_layer():
|
|
401
|
+
return None
|
|
402
|
+
visible_rect = self.get_visible_image_region()
|
|
403
|
+
if not visible_rect:
|
|
404
|
+
return self.master_layer()
|
|
405
|
+
x, y = int(visible_rect.x()), int(visible_rect.y())
|
|
406
|
+
w, h = int(visible_rect.width()), int(visible_rect.height())
|
|
407
|
+
master_img = self.master_layer()
|
|
408
|
+
return master_img[y:y + h, x:x + w], (x, y, w, h)
|
|
409
|
+
|
|
410
|
+
def map_to_scene(self, pos):
|
|
411
|
+
return self.get_master_view().mapToScene(pos)
|
|
412
|
+
|
|
413
|
+
# pylint: disable=C0103
|
|
414
|
+
def keyPressEvent(self, event):
|
|
415
|
+
if self.empty():
|
|
416
|
+
return
|
|
417
|
+
if event.key() == Qt.Key_Space and not self.scrolling:
|
|
418
|
+
self.space_pressed = True
|
|
419
|
+
self.get_master_view().setCursor(Qt.OpenHandCursor)
|
|
420
|
+
if self.brush_cursor:
|
|
421
|
+
self.brush_cursor.hide()
|
|
422
|
+
if self.handle_key_press_event(event):
|
|
423
|
+
if event.key() == Qt.Key_Control and not self.scrolling:
|
|
424
|
+
self.control_pressed = True
|
|
425
|
+
super().keyPressEvent(event)
|
|
426
|
+
|
|
427
|
+
def keyReleaseEvent(self, event):
|
|
428
|
+
if self.empty():
|
|
429
|
+
return
|
|
430
|
+
self.update_brush_cursor()
|
|
431
|
+
if event.key() == Qt.Key_Space:
|
|
432
|
+
self.space_pressed = False
|
|
433
|
+
if not self.scrolling:
|
|
434
|
+
self.get_master_view().setCursor(Qt.BlankCursor)
|
|
435
|
+
if self.brush_cursor:
|
|
436
|
+
self.brush_cursor.show()
|
|
437
|
+
if self.handle_key_release_event(event):
|
|
438
|
+
if event.key() == Qt.Key_Control:
|
|
439
|
+
self.control_pressed = False
|
|
440
|
+
super().keyReleaseEvent(event)
|
|
441
|
+
|
|
442
|
+
def leaveEvent(self, event):
|
|
443
|
+
if self.empty():
|
|
444
|
+
self.setCursor(Qt.ArrowCursor)
|
|
445
|
+
else:
|
|
446
|
+
self.get_master_view().setCursor(Qt.ArrowCursor)
|
|
447
|
+
if self.brush_cursor:
|
|
448
|
+
self.brush_cursor.hide()
|
|
449
|
+
super().leaveEvent(event)
|
|
450
|
+
# pylint: enable=C0103
|
|
451
|
+
|
|
452
|
+
def scroll_view(self, view, delta_x, delta_y):
|
|
453
|
+
view.horizontalScrollBar().setValue(
|
|
454
|
+
view.horizontalScrollBar().value() - delta_x)
|
|
455
|
+
view.verticalScrollBar().setValue(
|
|
456
|
+
view.verticalScrollBar().value() - delta_y)
|
|
457
|
+
self.status.set_scroll(view.horizontalScrollBar().value(),
|
|
458
|
+
view.verticalScrollBar().value())
|
|
459
|
+
|
|
460
|
+
def center_image(self, view):
|
|
461
|
+
view.horizontalScrollBar().setValue(self.status.h_scroll)
|
|
462
|
+
view.verticalScrollBar().setValue(self.status.v_scroll)
|
|
463
|
+
|
|
464
|
+
def mouse_move_event(self, event):
|
|
465
|
+
if self.empty():
|
|
466
|
+
return
|
|
467
|
+
position = event.position()
|
|
468
|
+
brush_size = self.brush.size
|
|
469
|
+
if not self.space_pressed:
|
|
470
|
+
self.update_brush_cursor()
|
|
471
|
+
if self.dragging and event.buttons() & Qt.LeftButton:
|
|
472
|
+
current_time = QTime.currentTime()
|
|
473
|
+
if self.last_update_time.msecsTo(current_time) >= gui_constants.PAINT_REFRESH_TIMER:
|
|
474
|
+
min_step = brush_size * \
|
|
475
|
+
gui_constants.MIN_MOUSE_STEP_BRUSH_FRACTION * self.zoom_factor()
|
|
476
|
+
x, y = position.x(), position.y()
|
|
477
|
+
xp, yp = self.last_brush_pos.x(), self.last_brush_pos.y()
|
|
478
|
+
distance = math.sqrt((x - xp)**2 + (y - yp)**2)
|
|
479
|
+
n_steps = int(float(distance) / min_step)
|
|
480
|
+
if n_steps > 0:
|
|
481
|
+
delta_x = (position.x() - self.last_brush_pos.x()) / n_steps
|
|
482
|
+
delta_y = (position.y() - self.last_brush_pos.y()) / n_steps
|
|
483
|
+
for i in range(0, n_steps + 1):
|
|
484
|
+
pos = QPoint(self.last_brush_pos.x() + i * delta_x,
|
|
485
|
+
self.last_brush_pos.y() + i * delta_y)
|
|
486
|
+
self.brush_operation_continued.emit(pos)
|
|
487
|
+
self.last_brush_pos = position
|
|
488
|
+
self.last_update_time = current_time
|
|
489
|
+
if self.scrolling and event.buttons() & Qt.LeftButton:
|
|
490
|
+
master_view = self.get_master_view()
|
|
491
|
+
if self.space_pressed:
|
|
492
|
+
master_view.setCursor(Qt.ClosedHandCursor)
|
|
493
|
+
if self.brush_cursor:
|
|
494
|
+
self.brush_cursor.hide()
|
|
495
|
+
delta = position - self.last_mouse_pos
|
|
496
|
+
self.last_mouse_pos = position
|
|
497
|
+
self.scroll_view(master_view, delta.x(), delta.y())
|
|
498
|
+
|
|
499
|
+
def mouse_press_event(self, event):
|
|
500
|
+
if self.empty():
|
|
501
|
+
return
|
|
502
|
+
if event.button() == Qt.LeftButton and self.has_master_layer():
|
|
503
|
+
if self.space_pressed:
|
|
504
|
+
self.scrolling = True
|
|
505
|
+
self.last_mouse_pos = event.position()
|
|
506
|
+
self.setCursor(Qt.ClosedHandCursor)
|
|
507
|
+
else:
|
|
508
|
+
self.last_brush_pos = event.position()
|
|
509
|
+
self.brush_operation_started.emit(event.position().toPoint())
|
|
510
|
+
self.dragging = True
|
|
511
|
+
if self.brush_cursor:
|
|
512
|
+
self.brush_cursor.show()
|
|
513
|
+
|
|
514
|
+
def mouse_release_event(self, event):
|
|
515
|
+
if self.empty():
|
|
516
|
+
return
|
|
517
|
+
master_view = self.get_master_view()
|
|
518
|
+
if self.space_pressed:
|
|
519
|
+
master_view.setCursor(Qt.OpenHandCursor)
|
|
520
|
+
if self.brush_cursor:
|
|
521
|
+
self.brush_cursor.hide()
|
|
522
|
+
else:
|
|
523
|
+
master_view.setCursor(Qt.BlankCursor)
|
|
524
|
+
if self.brush_cursor:
|
|
525
|
+
self.brush_cursor.show()
|
|
526
|
+
if event.button() == Qt.LeftButton:
|
|
527
|
+
if self.scrolling:
|
|
528
|
+
self.scrolling = False
|
|
529
|
+
self.last_mouse_pos = None
|
|
530
|
+
elif self.dragging:
|
|
531
|
+
self.dragging = False
|
|
532
|
+
self.brush_operation_ended.emit()
|
|
533
|
+
|
|
534
|
+
def handle_pinch_gesture(self, pinch):
|
|
535
|
+
master_view = self.get_master_view()
|
|
536
|
+
if pinch.state() == Qt.GestureStarted:
|
|
537
|
+
self.pinch_start_scale = self.zoom_factor()
|
|
538
|
+
self.pinch_center_view = pinch.centerPoint()
|
|
539
|
+
self.pinch_center_scene = master_view.mapToScene(self.pinch_center_view.toPoint())
|
|
540
|
+
self.gesture_active = True
|
|
541
|
+
elif pinch.state() == Qt.GestureUpdated:
|
|
542
|
+
new_scale = self.pinch_start_scale * pinch.totalScaleFactor()
|
|
543
|
+
new_scale = max(self.min_scale(), min(new_scale, self.max_scale()))
|
|
544
|
+
if abs(new_scale - self.zoom_factor()) > 0.01:
|
|
545
|
+
self.set_zoom_factor(new_scale)
|
|
546
|
+
self.apply_zoom()
|
|
547
|
+
new_center = master_view.mapToScene(self.pinch_center_view.toPoint())
|
|
548
|
+
delta = self.pinch_center_scene - new_center
|
|
549
|
+
h_scroll = master_view.horizontalScrollBar().value() + \
|
|
550
|
+
int(delta.x() * self.zoom_factor())
|
|
551
|
+
v_scroll = master_view.verticalScrollBar().value() + \
|
|
552
|
+
int(delta.y() * self.zoom_factor())
|
|
553
|
+
self.status.set_scroll(h_scroll, v_scroll)
|
|
554
|
+
self.center_image(master_view)
|
|
555
|
+
elif pinch.state() in (Qt.GestureFinished, Qt.GestureCanceled):
|
|
556
|
+
self.gesture_active = False
|
|
557
|
+
self.update_cursor_pen_width()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: ShineStacker
|
|
5
5
|
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0
|
|
@@ -70,11 +70,11 @@ The GUI has two main working areas:
|
|
|
70
70
|
|
|
71
71
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
## Resources
|
|
74
74
|
|
|
75
75
|
🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
## Note for macOS users
|
|
78
78
|
|
|
79
79
|
**The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
|
|
80
80
|
|
|
@@ -93,17 +93,17 @@ xattr -cr ~/Downloads/shinestacker/shinestacker.app
|
|
|
93
93
|
|
|
94
94
|
macOS adds a quarantine flag to all files downloaded from the internet. The above command removes that flag while preserving all other application functionality.
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
## Credits
|
|
97
97
|
|
|
98
98
|
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.
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
## Resources
|
|
101
101
|
|
|
102
102
|
* [Pyramid Methods in Image Processing](https://www.researchgate.net/publication/246727904_Pyramid_Methods_in_Image_Processing), E. H. Adelson, C. H. Anderson, J. R. Bergen, P. J. Burt, J. M. Ogden, RCA Engineer, 29-6, Nov/Dec 1984
|
|
103
103
|
Pyramid methods in image processing
|
|
104
104
|
* [A Multi-focus Image Fusion Method Based on Laplacian Pyramid](http://www.jcomputers.us/vol6/jcp0612-07.pdf), Wencheng Wang, Faliang Chang, Journal of Computers 6 (12), 2559, December 2011
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
## License
|
|
107
107
|
|
|
108
108
|
<img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
109
109
|
|
|
@@ -112,7 +112,7 @@ Pyramid methods in image processing
|
|
|
112
112
|
|
|
113
113
|
- **Logo**: The Shine Stacker logo was designed by [Alessandro Lista](https://linktr.ee/alelista). Copyright © Alessandro Lista. All rights reserved. The logo is not covered by the LGPL-3.0 license of this project.
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
## Attribution request
|
|
116
116
|
📸 If you publish images created with Shine Stacker, please consider adding a note such as:
|
|
117
117
|
|
|
118
118
|
*Created with Shine Stacker – https://github.com/lucalista/shinestacker*
|