shinestacker 1.3.1__py3-none-any.whl → 1.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 (34) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +198 -18
  3. shinestacker/algorithms/align_parallel.py +17 -1
  4. shinestacker/algorithms/balance.py +23 -13
  5. shinestacker/algorithms/noise_detection.py +3 -1
  6. shinestacker/algorithms/utils.py +21 -10
  7. shinestacker/algorithms/vignetting.py +2 -0
  8. shinestacker/config/gui_constants.py +2 -2
  9. shinestacker/core/core_utils.py +10 -1
  10. shinestacker/gui/action_config.py +172 -7
  11. shinestacker/gui/action_config_dialog.py +246 -285
  12. shinestacker/gui/gui_run.py +2 -2
  13. shinestacker/gui/main_window.py +14 -5
  14. shinestacker/gui/menu_manager.py +26 -2
  15. shinestacker/gui/project_controller.py +4 -0
  16. shinestacker/gui/recent_file_manager.py +93 -0
  17. shinestacker/retouch/base_filter.py +5 -5
  18. shinestacker/retouch/brush_preview.py +3 -0
  19. shinestacker/retouch/brush_tool.py +11 -11
  20. shinestacker/retouch/display_manager.py +21 -37
  21. shinestacker/retouch/image_editor_ui.py +129 -71
  22. shinestacker/retouch/image_view_status.py +61 -0
  23. shinestacker/retouch/image_viewer.py +89 -431
  24. shinestacker/retouch/io_gui_handler.py +12 -2
  25. shinestacker/retouch/overlaid_view.py +212 -0
  26. shinestacker/retouch/shortcuts_help.py +13 -3
  27. shinestacker/retouch/sidebyside_view.py +479 -0
  28. shinestacker/retouch/view_strategy.py +466 -0
  29. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/METADATA +1 -1
  30. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/RECORD +34 -29
  31. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/WHEEL +0 -0
  32. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/entry_points.txt +0 -0
  33. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/licenses/LICENSE +0 -0
  34. {shinestacker-1.3.1.dist-info → shinestacker-1.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,479 @@
1
+ # pylint: disable=C0114, C0115, C0116, R0904, R0915, E0611, R0902, R0911, R0914, E1003
2
+ from PySide6.QtCore import Qt, Signal, QEvent, QRectF
3
+ from PySide6.QtGui import QPixmap, QPen, QColor, QCursor
4
+ from PySide6.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QFrame, QGraphicsEllipseItem
5
+ from .. config.gui_constants import gui_constants
6
+ from .view_strategy import ViewStrategy, ImageGraphicsViewBase, ViewSignals
7
+
8
+
9
+ class ImageGraphicsView(ImageGraphicsViewBase):
10
+ mouse_pressed = Signal(QEvent)
11
+ mouse_moved = Signal(QEvent)
12
+ mouse_released = Signal(QEvent)
13
+ gesture_event = Signal(QEvent)
14
+
15
+ # pylint: disable=C0103
16
+ def event(self, event):
17
+ if event.type() == QEvent.Gesture:
18
+ self.gesture_event.emit(event)
19
+ return True
20
+ return super().event(event)
21
+
22
+ def mousePressEvent(self, event):
23
+ self.mouse_pressed.emit(event)
24
+ super().mousePressEvent(event)
25
+
26
+ def mouseMoveEvent(self, event):
27
+ self.mouse_moved.emit(event)
28
+
29
+ def mouseReleaseEvent(self, event):
30
+ self.mouse_released.emit(event)
31
+ super().mouseReleaseEvent(event)
32
+ # pylint: enable=C0103
33
+
34
+
35
+ class DoubleViewBase(ViewStrategy, QWidget, ViewSignals):
36
+ def __init__(self, layer_collection, status, parent):
37
+ ViewStrategy.__init__(self, layer_collection, status)
38
+ QWidget.__init__(self, parent)
39
+ self.current_view = ImageGraphicsView(parent)
40
+ self.master_view = ImageGraphicsView(parent)
41
+ self.current_scene = self.create_scene(self.current_view)
42
+ self.master_scene = self.create_scene(self.master_view)
43
+ self.create_pixmaps()
44
+ self.master_scene.addItem(self.brush_preview)
45
+ self.setup_layout()
46
+ self._connect_signals()
47
+ self.panning_current = False
48
+ self.brush_cursor = None
49
+ self.setup_brush_cursor()
50
+ self.setFocusPolicy(Qt.StrongFocus)
51
+ self.pan_start = None
52
+ self.pinch_start_scale = None
53
+ self.current_view.installEventFilter(self)
54
+ self.master_view.installEventFilter(self)
55
+ self.current_view.setFocusPolicy(Qt.NoFocus)
56
+ self.master_view.setFocusPolicy(Qt.NoFocus)
57
+ self.current_brush_cursor = None
58
+ self.setup_current_brush_cursor()
59
+
60
+ def setup_layout(self):
61
+ """To be implemented by subclasses - creates layout and adds widgets"""
62
+ raise NotImplementedError("Subclasses must implement setup_layout")
63
+
64
+ def create_pixmaps(self):
65
+ self.current_pixmap_item = self.create_pixmap(self.current_scene)
66
+ self.master_pixmap_item = self.create_pixmap(self.master_scene)
67
+
68
+ def _connect_signals(self):
69
+ self.current_view.mouse_pressed.connect(self.handle_current_mouse_press)
70
+ self.current_view.mouse_moved.connect(self.handle_current_mouse_move)
71
+ self.current_view.mouse_released.connect(self.handle_current_mouse_release)
72
+ self.current_view.gesture_event.connect(self.handle_gesture_event)
73
+ self.master_view.mouse_pressed.connect(self.handle_master_mouse_press)
74
+ self.master_view.mouse_moved.connect(self.handle_master_mouse_move)
75
+ self.master_view.mouse_released.connect(self.handle_master_mouse_release)
76
+ self.master_view.gesture_event.connect(self.handle_gesture_event)
77
+ self.current_view.horizontalScrollBar().valueChanged.connect(
78
+ self.master_view.horizontalScrollBar().setValue)
79
+ self.current_view.verticalScrollBar().valueChanged.connect(
80
+ self.master_view.verticalScrollBar().setValue)
81
+ self.master_view.horizontalScrollBar().valueChanged.connect(
82
+ self.current_view.horizontalScrollBar().setValue)
83
+ self.master_view.verticalScrollBar().valueChanged.connect(
84
+ self.current_view.verticalScrollBar().setValue)
85
+ # pylint: disable=C0103, W0201
86
+ self.current_view.enterEvent = self.current_view_enter_event
87
+ self.current_view.leaveEvent = self.current_view_leave_event
88
+ self.master_view.enterEvent = self.master_view_enter_event
89
+ self.master_view.leaveEvent = self.master_view_leave_event
90
+ # pylint: enable=C0103, W0201
91
+
92
+ def current_view_enter_event(self, event):
93
+ self.activateWindow()
94
+ self.setFocus()
95
+ self.update_brush_cursor()
96
+ super(ImageGraphicsView, self.current_view).enterEvent(event)
97
+
98
+ def current_view_leave_event(self, event):
99
+ self.update_brush_cursor()
100
+ super(ImageGraphicsView, self.current_view).leaveEvent(event)
101
+
102
+ def master_view_enter_event(self, event):
103
+ self.activateWindow()
104
+ self.setFocus()
105
+ self.update_brush_cursor()
106
+ super(ImageGraphicsView, self.master_view).enterEvent(event)
107
+
108
+ def master_view_leave_event(self, event):
109
+ self.update_brush_cursor()
110
+ super(ImageGraphicsView, self.master_view).leaveEvent(event)
111
+
112
+ def get_master_view(self):
113
+ return self.master_view
114
+
115
+ def get_master_scene(self):
116
+ return self.master_scene
117
+
118
+ def get_master_pixmap(self):
119
+ return self.master_pixmap_item
120
+
121
+ def get_views(self):
122
+ return [self.master_view, self.current_view]
123
+
124
+ def get_scenes(self):
125
+ return [self.master_scene, self.current_scene]
126
+
127
+ def get_pixmaps(self):
128
+ return {
129
+ self.master_pixmap_item: self.master_view,
130
+ self.current_pixmap_item: self.current_view
131
+ }
132
+
133
+ # pylint: disable=C0103
134
+ def focusInEvent(self, event):
135
+ super().focusInEvent(event)
136
+ self.activateWindow()
137
+ self.setFocus()
138
+
139
+ def eventFilter(self, obj, event):
140
+ if obj in [self.current_view, self.master_view]:
141
+ if event.type() == QEvent.KeyPress:
142
+ self.keyPressEvent(event)
143
+ return True
144
+ if event.type() == QEvent.KeyRelease:
145
+ self.keyReleaseEvent(event)
146
+ return True
147
+ return super().eventFilter(obj, event)
148
+
149
+ def showEvent(self, event):
150
+ super().showEvent(event)
151
+ self.update_brush_cursor()
152
+
153
+ def enterEvent(self, event):
154
+ self.activateWindow()
155
+ self.setFocus()
156
+ if not self.empty():
157
+ if self.space_pressed:
158
+ self.master_view.setCursor(Qt.OpenHandCursor)
159
+ else:
160
+ self.master_view.setCursor(Qt.BlankCursor)
161
+ if self.brush_cursor:
162
+ self.brush_cursor.show()
163
+ super().enterEvent(event)
164
+
165
+ def leaveEvent(self, event):
166
+ if self.brush_cursor:
167
+ self.brush_cursor.hide()
168
+ if self.current_brush_cursor:
169
+ self.current_brush_cursor.hide()
170
+ self.master_view.setCursor(Qt.ArrowCursor)
171
+ self.current_view.setCursor(Qt.ArrowCursor)
172
+ super().leaveEvent(event)
173
+
174
+ def keyPressEvent(self, event):
175
+ super().keyPressEvent(event)
176
+ if event.key() == Qt.Key_Space:
177
+ self.update_brush_cursor()
178
+
179
+ def keyReleaseEvent(self, event):
180
+ super().keyReleaseEvent(event)
181
+ if event.key() == Qt.Key_Space:
182
+ self.update_brush_cursor()
183
+ # pylint: enable=C0103
184
+
185
+ def setup_brush_cursor(self):
186
+ super().setup_brush_cursor()
187
+ self.setup_current_brush_cursor()
188
+ self.update_cursor_pen_width()
189
+
190
+ def setup_current_brush_cursor(self):
191
+ if not self.brush:
192
+ return
193
+ for item in self.current_scene.items():
194
+ if isinstance(item, QGraphicsEllipseItem) and item != self.brush_preview:
195
+ self.current_scene.removeItem(item)
196
+ pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
197
+ pen = QPen(QColor(255, 0, 0), pen_width)
198
+ brush = Qt.NoBrush
199
+ self.current_brush_cursor = self.current_scene.addEllipse(
200
+ 0, 0, self.brush.size, self.brush.size, pen, brush)
201
+ self.current_brush_cursor.setZValue(1000)
202
+ self.current_brush_cursor.hide()
203
+
204
+ def update_current_brush_cursor(self, scene_pos):
205
+ if not self.current_brush_cursor or not self.isVisible():
206
+ return
207
+ size = self.brush.size
208
+ radius = size / 2
209
+ self.current_brush_cursor.setRect(
210
+ scene_pos.x() - radius, scene_pos.y() - radius, size, size)
211
+ if self.brush_cursor and self.brush_cursor.isVisible():
212
+ self.current_brush_cursor.show()
213
+ else:
214
+ self.current_brush_cursor.hide()
215
+
216
+ def update_cursor_pen_width(self):
217
+ if not self.brush_cursor or not self.current_brush_cursor:
218
+ return
219
+ pen_width = gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor()
220
+ master_pen = self.brush_cursor.pen()
221
+ master_pen.setWidthF(pen_width)
222
+ self.brush_cursor.setPen(master_pen)
223
+ current_pen = self.current_brush_cursor.pen()
224
+ current_pen.setWidthF(pen_width)
225
+ self.current_brush_cursor.setPen(current_pen)
226
+
227
+ def update_brush_cursor(self):
228
+ if self.empty():
229
+ return
230
+ self.update_cursor_pen_width()
231
+ mouse_pos_global = QCursor.pos()
232
+ mouse_pos_current = self.current_view.mapFromGlobal(mouse_pos_global)
233
+ mouse_pos_master = self.master_view.mapFromGlobal(mouse_pos_global)
234
+ current_has_mouse = self.current_view.rect().contains(mouse_pos_current)
235
+ master_has_mouse = self.master_view.rect().contains(mouse_pos_master)
236
+ if master_has_mouse:
237
+ super().update_brush_cursor()
238
+ self.sync_current_cursor_with_master()
239
+ if self.space_pressed:
240
+ cursor_style = Qt.OpenHandCursor if not self.scrolling else Qt.ClosedHandCursor
241
+ self.master_view.setCursor(cursor_style)
242
+ self.current_view.setCursor(cursor_style)
243
+ else:
244
+ self.master_view.setCursor(Qt.BlankCursor)
245
+ self.current_view.setCursor(Qt.BlankCursor)
246
+ elif current_has_mouse:
247
+ scene_pos = self.current_view.mapToScene(mouse_pos_current)
248
+ size = self.brush.size
249
+ radius = size / 2
250
+ self.current_brush_cursor.setRect(
251
+ scene_pos.x() - radius,
252
+ scene_pos.y() - radius,
253
+ size, size
254
+ )
255
+ self.current_brush_cursor.show()
256
+ if self.brush_cursor:
257
+ self.brush_cursor.setRect(
258
+ scene_pos.x() - radius,
259
+ scene_pos.y() - radius,
260
+ size, size
261
+ )
262
+ self.brush_cursor.show()
263
+ if self.space_pressed:
264
+ cursor_style = Qt.OpenHandCursor \
265
+ if not self.panning_current else Qt.ClosedHandCursor
266
+ self.current_view.setCursor(cursor_style)
267
+ self.master_view.setCursor(cursor_style)
268
+ else:
269
+ self.current_view.setCursor(Qt.BlankCursor)
270
+ self.master_view.setCursor(Qt.BlankCursor)
271
+ else:
272
+ if self.brush_cursor:
273
+ self.brush_cursor.hide()
274
+ if self.current_brush_cursor:
275
+ self.current_brush_cursor.hide()
276
+ self.master_view.setCursor(Qt.ArrowCursor)
277
+ self.current_view.setCursor(Qt.ArrowCursor)
278
+
279
+ def handle_master_mouse_press(self, event):
280
+ self.setFocus()
281
+ self.mouse_press_event(event)
282
+
283
+ def handle_master_mouse_move(self, event):
284
+ self.mouse_move_event(event)
285
+ self.update_brush_cursor()
286
+
287
+ def handle_master_mouse_release(self, event):
288
+ self.mouse_release_event(event)
289
+
290
+ def handle_current_mouse_press(self, event):
291
+ position = event.position()
292
+ if self.space_pressed:
293
+ self.pan_start = position
294
+ self.panning_current = True
295
+ self.update_brush_cursor()
296
+
297
+ def handle_current_mouse_move(self, event):
298
+ position = event.position()
299
+ if self.panning_current and self.space_pressed:
300
+ delta = position - self.pan_start
301
+ self.pan_start = position
302
+ self.scroll_view(self.current_view, delta.x(), delta.y())
303
+ else:
304
+ self.update_brush_cursor()
305
+
306
+ def handle_current_mouse_release(self, _event):
307
+ if self.panning_current:
308
+ self.panning_current = False
309
+ self.update_brush_cursor()
310
+
311
+ def handle_gesture_event(self, event):
312
+ if self.empty():
313
+ return
314
+ pinch_gesture = event.gesture(Qt.PinchGesture)
315
+ if pinch_gesture:
316
+ self.handle_pinch_gesture(pinch_gesture)
317
+ event.accept()
318
+
319
+ def handle_pinch_gesture(self, pinch):
320
+ if pinch.state() == Qt.GestureStarted:
321
+ self.pinch_start_scale = self.zoom_factor()
322
+ self.pinch_center_view = pinch.centerPoint()
323
+ self.pinch_center_scene = self.master_view.mapToScene(self.pinch_center_view.toPoint())
324
+ elif pinch.state() == Qt.GestureUpdated:
325
+ new_scale = self.pinch_start_scale * pinch.totalScaleFactor()
326
+ new_scale = max(self.min_scale(), min(new_scale, self.max_scale()))
327
+ if abs(new_scale - self.zoom_factor()) > 0.01:
328
+ self.set_zoom_factor(new_scale)
329
+ self._apply_zoom()
330
+ new_center = self.master_view.mapToScene(self.pinch_center_view.toPoint())
331
+ delta = self.pinch_center_scene - new_center
332
+ h_scroll = self.master_view.horizontalScrollBar().value()
333
+ v_scroll = self.master_view.verticalScrollBar().value()
334
+ self.master_view.horizontalScrollBar().setValue(
335
+ h_scroll + int(delta.x() * self.zoom_factor()))
336
+ self.master_view.verticalScrollBar().setValue(
337
+ v_scroll + int(delta.y() * self.zoom_factor()))
338
+
339
+ def set_master_image(self, qimage):
340
+ self.status.set_master_image(qimage)
341
+ pixmap = self.status.pixmap_master
342
+ img_width, img_height = pixmap.width(), pixmap.height()
343
+ self.master_view.setSceneRect(QRectF(pixmap.rect()))
344
+ self.master_pixmap_item.setPixmap(pixmap)
345
+ self.set_min_scale(min(gui_constants.MIN_ZOOMED_IMG_WIDTH / img_width,
346
+ gui_constants.MIN_ZOOMED_IMG_HEIGHT / img_height))
347
+ self.set_max_scale(gui_constants.MAX_ZOOMED_IMG_PX_SIZE)
348
+ self.set_zoom_factor(1.0)
349
+ self.master_view.fitInView(self.master_pixmap_item, Qt.KeepAspectRatio)
350
+ self.set_zoom_factor(self.get_current_scale())
351
+ self.set_zoom_factor(max(self.min_scale(), min(self.max_scale(), self.zoom_factor())))
352
+ self.master_view.resetTransform()
353
+ self.master_scene.scale(self.zoom_factor(), self.zoom_factor())
354
+ self.master_view.centerOn(self.master_pixmap_item)
355
+ center = self.master_scene.sceneRect().center()
356
+ self.brush_preview.setPos(max(0, min(center.x(), img_width)),
357
+ max(0, min(center.y(), img_height)))
358
+ self.master_scene.setSceneRect(QRectF(self.master_pixmap_item.boundingRect()))
359
+
360
+ def set_current_image(self, qimage):
361
+ self.status.set_current_image(qimage)
362
+ pixmap = self.status.pixmap_current
363
+ self.current_scene.setSceneRect(QRectF(pixmap.rect()))
364
+ self.current_pixmap_item.setPixmap(pixmap)
365
+ self.current_view.resetTransform()
366
+ self.current_scene.scale(self.zoom_factor(), self.zoom_factor())
367
+ # self.current_view.centerOn(self.current_pixmap_item)
368
+ self.current_scene.setSceneRect(QRectF(self.current_pixmap_item.boundingRect()))
369
+
370
+ def _arrange_images(self):
371
+ if self.status.empty():
372
+ return
373
+ if self.master_pixmap_item.pixmap().height() == 0:
374
+ self.update_master_display()
375
+ self.update_current_display()
376
+ self.reset_zoom()
377
+ self._apply_zoom()
378
+
379
+ def update_master_display(self):
380
+ if not self.status.empty():
381
+ master_qimage = self.numpy_to_qimage(self.master_layer())
382
+ if master_qimage:
383
+ pixmap = QPixmap.fromImage(master_qimage)
384
+ self.master_pixmap_item.setPixmap(pixmap)
385
+ self.master_scene.setSceneRect(QRectF(pixmap.rect()))
386
+ self._arrange_images()
387
+
388
+ def update_current_display(self):
389
+ if not self.status.empty() and self.number_of_layers() > 0:
390
+ current_qimage = self.numpy_to_qimage(self.current_layer())
391
+ if current_qimage:
392
+ pixmap = QPixmap.fromImage(current_qimage)
393
+ self.current_pixmap_item.setPixmap(pixmap)
394
+ self.current_scene.setSceneRect(QRectF(pixmap.rect()))
395
+ self._arrange_images()
396
+
397
+ def _apply_zoom(self):
398
+ if not self.current_pixmap_item.pixmap().isNull():
399
+ self.current_view.resetTransform()
400
+ self.current_view.scale(self.zoom_factor(), self.zoom_factor())
401
+ # self.current_view.centerOn(self.current_pixmap_item)
402
+ if not self.master_pixmap_item.pixmap().isNull():
403
+ self.master_view.resetTransform()
404
+ self.master_view.scale(self.zoom_factor(), self.zoom_factor())
405
+ # self.master_view.centerOn(self.master_pixmap_item)
406
+
407
+ def set_brush(self, brush):
408
+ super().set_brush(brush)
409
+ if self.brush_cursor:
410
+ self.master_scene.removeItem(self.brush_cursor)
411
+ self.setup_brush_cursor()
412
+ self.setup_current_brush_cursor()
413
+
414
+ def clear_image(self):
415
+ super().clear_image()
416
+ if self.current_brush_cursor:
417
+ self.current_scene.removeItem(self.current_brush_cursor)
418
+ self.current_brush_cursor = None
419
+
420
+ def sync_current_cursor_with_master(self):
421
+ if not self.brush_cursor or not self.current_brush_cursor:
422
+ return
423
+ master_rect = self.brush_cursor.rect()
424
+ scene_pos = master_rect.center()
425
+ size = self.brush.size
426
+ radius = size / 2
427
+ self.current_brush_cursor.setRect(
428
+ scene_pos.x() - radius,
429
+ scene_pos.y() - radius,
430
+ size, size
431
+ )
432
+ if self.brush_cursor.isVisible():
433
+ self.current_brush_cursor.show()
434
+ else:
435
+ self.current_brush_cursor.hide()
436
+
437
+ def zoom_in(self):
438
+ super().zoom_in()
439
+ self.update_cursor_pen_width()
440
+
441
+ def zoom_out(self):
442
+ super().zoom_out()
443
+ self.update_cursor_pen_width()
444
+
445
+ def reset_zoom(self):
446
+ super().reset_zoom()
447
+ self.update_cursor_pen_width()
448
+
449
+ def actual_size(self):
450
+ super().actual_size()
451
+ self.update_cursor_pen_width()
452
+
453
+
454
+ class SideBySideView(DoubleViewBase):
455
+ def setup_layout(self):
456
+ layout = QHBoxLayout(self)
457
+ layout.setContentsMargins(0, 0, 0, 0)
458
+ layout.setSpacing(0)
459
+ layout.addWidget(self.current_view, 1)
460
+ separator = QFrame()
461
+ separator.setFrameShape(QFrame.VLine)
462
+ separator.setFrameShadow(QFrame.Sunken)
463
+ separator.setLineWidth(2)
464
+ layout.addWidget(separator, 0)
465
+ layout.addWidget(self.master_view, 1)
466
+
467
+
468
+ class TopBottomView(DoubleViewBase):
469
+ def setup_layout(self):
470
+ layout = QVBoxLayout(self)
471
+ layout.setContentsMargins(0, 0, 0, 0)
472
+ layout.setSpacing(0)
473
+ layout.addWidget(self.current_view, 1)
474
+ separator = QFrame()
475
+ separator.setFrameShape(QFrame.HLine)
476
+ separator.setFrameShadow(QFrame.Sunken)
477
+ separator.setLineWidth(2)
478
+ layout.addWidget(separator, 0)
479
+ layout.addWidget(self.master_view, 1)