shinestacker 0.3.3__py3-none-any.whl → 0.3.5__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/__init__.py +2 -1
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/__init__.py +3 -2
- shinestacker/algorithms/align.py +102 -64
- shinestacker/algorithms/balance.py +89 -42
- shinestacker/algorithms/base_stack_algo.py +41 -0
- shinestacker/algorithms/core_utils.py +6 -6
- shinestacker/algorithms/denoise.py +4 -1
- shinestacker/algorithms/depth_map.py +28 -39
- shinestacker/algorithms/exif.py +43 -38
- shinestacker/algorithms/multilayer.py +48 -28
- shinestacker/algorithms/noise_detection.py +34 -26
- shinestacker/algorithms/pyramid.py +42 -42
- shinestacker/algorithms/sharpen.py +1 -0
- shinestacker/algorithms/stack.py +42 -42
- shinestacker/algorithms/stack_framework.py +118 -66
- shinestacker/algorithms/utils.py +12 -11
- shinestacker/algorithms/vignetting.py +52 -25
- shinestacker/algorithms/white_balance.py +1 -0
- shinestacker/app/about_dialog.py +6 -2
- shinestacker/app/app_config.py +1 -0
- shinestacker/app/gui_utils.py +20 -0
- shinestacker/app/help_menu.py +2 -1
- shinestacker/app/main.py +9 -18
- shinestacker/app/open_frames.py +5 -4
- shinestacker/app/project.py +5 -16
- shinestacker/app/retouch.py +5 -17
- shinestacker/core/colors.py +4 -4
- shinestacker/core/core_utils.py +1 -1
- shinestacker/core/exceptions.py +2 -1
- shinestacker/core/framework.py +46 -33
- shinestacker/core/logging.py +9 -10
- shinestacker/gui/action_config.py +253 -197
- shinestacker/gui/actions_window.py +36 -35
- shinestacker/gui/colors.py +1 -0
- shinestacker/gui/gui_images.py +7 -3
- shinestacker/gui/gui_logging.py +3 -2
- shinestacker/gui/gui_run.py +53 -38
- shinestacker/gui/main_window.py +69 -25
- shinestacker/gui/new_project.py +35 -2
- shinestacker/gui/project_converter.py +21 -20
- shinestacker/gui/project_editor.py +51 -52
- shinestacker/gui/project_model.py +15 -23
- shinestacker/retouch/{filter_base.py → base_filter.py} +7 -4
- shinestacker/retouch/brush.py +1 -0
- shinestacker/retouch/brush_gradient.py +17 -3
- shinestacker/retouch/brush_preview.py +14 -10
- shinestacker/retouch/brush_tool.py +28 -19
- shinestacker/retouch/denoise_filter.py +3 -2
- shinestacker/retouch/display_manager.py +11 -5
- shinestacker/retouch/exif_data.py +1 -0
- shinestacker/retouch/file_loader.py +13 -9
- shinestacker/retouch/filter_manager.py +1 -0
- shinestacker/retouch/image_editor.py +14 -48
- shinestacker/retouch/image_editor_ui.py +10 -5
- shinestacker/retouch/image_filters.py +4 -2
- shinestacker/retouch/image_viewer.py +33 -31
- shinestacker/retouch/io_gui_handler.py +25 -13
- shinestacker/retouch/io_manager.py +3 -2
- shinestacker/retouch/layer_collection.py +79 -23
- shinestacker/retouch/shortcuts_help.py +1 -0
- shinestacker/retouch/undo_manager.py +7 -0
- shinestacker/retouch/unsharp_mask_filter.py +3 -2
- shinestacker/retouch/white_balance_filter.py +11 -6
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/METADATA +18 -6
- shinestacker-0.3.5.dist-info/RECORD +86 -0
- shinestacker-0.3.3.dist-info/RECORD +0 -85
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0902, R0913, R0917, R0914
|
|
1
2
|
import numpy as np
|
|
2
3
|
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush
|
|
3
4
|
from PySide6.QtCore import Qt, QPoint
|
|
4
|
-
from .brush_gradient import
|
|
5
|
+
from .brush_gradient import create_default_brush_gradient
|
|
5
6
|
from .. config.gui_constants import gui_constants
|
|
6
7
|
from .. config.constants import constants
|
|
7
8
|
from .brush_preview import create_brush_mask
|
|
@@ -42,7 +43,8 @@ class BrushTool:
|
|
|
42
43
|
normalized = slider_val / gui_constants.BRUSH_SIZE_SLIDER_MAX
|
|
43
44
|
size = gui_constants.BRUSH_SIZES['min'] + \
|
|
44
45
|
gui_constants.BRUSH_SIZES['max'] * (normalized ** gui_constants.BRUSH_GAMMA)
|
|
45
|
-
return max(gui_constants.BRUSH_SIZES['min'],
|
|
46
|
+
return max(gui_constants.BRUSH_SIZES['min'],
|
|
47
|
+
min(gui_constants.BRUSH_SIZES['max'], size))
|
|
46
48
|
|
|
47
49
|
self.brush.size = slider_to_brush_size(slider_val)
|
|
48
50
|
self.update_brush_thumb()
|
|
@@ -89,21 +91,21 @@ class BrushTool:
|
|
|
89
91
|
center_x, center_y = width // 2, height // 2
|
|
90
92
|
radius = preview_size // 2
|
|
91
93
|
if self.image_viewer.cursor_style == 'preview':
|
|
92
|
-
gradient =
|
|
93
|
-
center_x, center_y, radius,
|
|
94
|
-
self.brush.hardness,
|
|
95
|
-
inner_color=QColor(*gui_constants.BRUSH_COLORS['inner']),
|
|
96
|
-
outer_color=QColor(*gui_constants.BRUSH_COLORS['gradient_end']),
|
|
97
|
-
opacity=self.brush.opacity
|
|
98
|
-
)
|
|
94
|
+
gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
|
|
99
95
|
painter.setBrush(QBrush(gradient))
|
|
100
|
-
painter.setPen(
|
|
96
|
+
painter.setPen(
|
|
97
|
+
QPen(QColor(*gui_constants.BRUSH_COLORS['outer']),
|
|
98
|
+
gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
|
|
101
99
|
elif self.image_viewer.cursor_style == 'outline':
|
|
102
100
|
painter.setBrush(Qt.NoBrush)
|
|
103
|
-
painter.setPen(
|
|
101
|
+
painter.setPen(
|
|
102
|
+
QPen(QColor(*gui_constants.BRUSH_COLORS['outer']),
|
|
103
|
+
gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
|
|
104
104
|
else:
|
|
105
105
|
painter.setBrush(QBrush(QColor(*gui_constants.BRUSH_COLORS['cursor_inner'])))
|
|
106
|
-
painter.setPen(
|
|
106
|
+
painter.setPen(
|
|
107
|
+
QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
|
|
108
|
+
gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
|
|
107
109
|
painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
|
|
108
110
|
if self.image_viewer.cursor_style == 'preview':
|
|
109
111
|
painter.setPen(QPen(QColor(0, 0, 160)))
|
|
@@ -115,7 +117,8 @@ class BrushTool:
|
|
|
115
117
|
self.brush_preview.setPixmap(pixmap)
|
|
116
118
|
self.image_viewer.update_brush_cursor()
|
|
117
119
|
|
|
118
|
-
def apply_brush_operation(self, master_layer, source_layer, dest_layer, mask_layer,
|
|
120
|
+
def apply_brush_operation(self, master_layer, source_layer, dest_layer, mask_layer,
|
|
121
|
+
view_pos, image_viewer):
|
|
119
122
|
if master_layer is None or source_layer is None:
|
|
120
123
|
return False
|
|
121
124
|
if dest_layer is None:
|
|
@@ -136,14 +139,17 @@ class BrushTool:
|
|
|
136
139
|
source_area = source_layer[y_start:y_end, x_start:x_end]
|
|
137
140
|
dest_area = dest_layer[y_start:y_end, x_start:x_end]
|
|
138
141
|
mask_layer_area = mask_layer[y_start:y_end, x_start:x_end]
|
|
139
|
-
mask_area = mask[y_start - (y_center - radius):y_end - (y_center - radius),
|
|
140
|
-
|
|
142
|
+
mask_area = mask[y_start - (y_center - radius):y_end - (y_center - radius),
|
|
143
|
+
x_start - (x_center - radius):x_end - (x_center - radius)]
|
|
144
|
+
mask_layer_area[:] = np.clip(
|
|
145
|
+
mask_layer_area + mask_area * self.brush.flow / 100.0, 0.0,
|
|
146
|
+
1.0)
|
|
141
147
|
self.apply_mask(master_area, source_area, mask_layer_area, dest_area)
|
|
142
148
|
return x_start, y_start, x_end, y_end
|
|
143
149
|
|
|
144
150
|
def get_brush_mask(self, radius):
|
|
145
151
|
mask_key = (radius, self.brush.hardness)
|
|
146
|
-
if mask_key not in self._brush_mask_cache
|
|
152
|
+
if mask_key not in self._brush_mask_cache:
|
|
147
153
|
full_mask = create_brush_mask(size=radius * 2 + 1, hardness_percent=self.brush.hardness,
|
|
148
154
|
opacity_percent=self.brush.opacity)
|
|
149
155
|
self._brush_mask_cache[mask_key] = full_mask
|
|
@@ -155,10 +161,13 @@ class BrushTool:
|
|
|
155
161
|
dtype = master_area.dtype
|
|
156
162
|
max_px_value = constants.MAX_UINT16 if dtype == np.uint16 else constants.MAX_UINT8
|
|
157
163
|
if master_area.ndim == 3:
|
|
158
|
-
dest_area[:] = np.clip(
|
|
159
|
-
|
|
164
|
+
dest_area[:] = np.clip(
|
|
165
|
+
master_area * (1 - effective_mask[..., np.newaxis]) +
|
|
166
|
+
source_area * effective_mask[..., np.newaxis], 0, max_px_value).astype(dtype)
|
|
160
167
|
else:
|
|
161
|
-
dest_area[:] = np.clip(
|
|
168
|
+
dest_area[:] = np.clip(
|
|
169
|
+
master_area * (1 - effective_mask) + source_area * effective_mask, 0,
|
|
170
|
+
max_px_value).astype(dtype)
|
|
162
171
|
|
|
163
172
|
def clear_cache(self):
|
|
164
173
|
self._brush_mask_cache.clear()
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0221
|
|
1
2
|
from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QCheckBox, QDialogButtonBox
|
|
2
3
|
from PySide6.QtCore import Qt, QTimer
|
|
3
|
-
from .
|
|
4
|
+
from .base_filter import BaseFilter
|
|
5
|
+
from .. algorithms.denoise import denoise
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class DenoiseFilter(BaseFilter):
|
|
@@ -52,5 +54,4 @@ class DenoiseFilter(BaseFilter):
|
|
|
52
54
|
return (self.max_value * self.slider.value() / self.max_range,)
|
|
53
55
|
|
|
54
56
|
def apply(self, image, strength):
|
|
55
|
-
from .. algorithms.denoise import denoise
|
|
56
57
|
return denoise(image, strength)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0903, R0913, R0917, E1121
|
|
1
2
|
import numpy as np
|
|
2
3
|
from PySide6.QtWidgets import QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog
|
|
3
4
|
from PySide6.QtGui import QPixmap, QImage
|
|
4
5
|
from PySide6.QtCore import Qt, QObject, QTimer, QSize, Signal
|
|
5
6
|
from .. config.gui_constants import gui_constants
|
|
7
|
+
from .layer_collection import LayerCollectionHandler
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class ClickableLabel(QLabel):
|
|
@@ -12,20 +14,22 @@ class ClickableLabel(QLabel):
|
|
|
12
14
|
super().__init__(text, parent)
|
|
13
15
|
self.setMouseTracking(True)
|
|
14
16
|
|
|
17
|
+
# pylint: disable=C0103
|
|
15
18
|
def mouseDoubleClickEvent(self, event):
|
|
16
19
|
if event.button() == Qt.LeftButton:
|
|
17
20
|
self.double_clicked.emit()
|
|
18
21
|
super().mouseDoubleClickEvent(event)
|
|
22
|
+
# pylint: enable=C0103
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
class DisplayManager(QObject):
|
|
25
|
+
class DisplayManager(QObject, LayerCollectionHandler):
|
|
22
26
|
status_message_requested = Signal(str)
|
|
23
27
|
cursor_preview_state_changed = Signal(bool)
|
|
24
28
|
|
|
25
29
|
def __init__(self, layer_collection, image_viewer, master_thumbnail_label,
|
|
26
30
|
thumbnail_list, parent=None):
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
QObject.__init__(self, parent)
|
|
32
|
+
LayerCollectionHandler.__init__(self, layer_collection)
|
|
29
33
|
self.image_viewer = image_viewer
|
|
30
34
|
self.master_thumbnail_label = master_thumbnail_label
|
|
31
35
|
self.thumbnail_list = thumbnail_list
|
|
@@ -67,7 +71,8 @@ class DisplayManager(QObject):
|
|
|
67
71
|
qimg = QImage(layer.data, width, height, 3 * width, QImage.Format_RGB888)
|
|
68
72
|
else:
|
|
69
73
|
qimg = QImage(layer.data, width, height, width, QImage.Format_Grayscale8)
|
|
70
|
-
return QPixmap.fromImage(
|
|
74
|
+
return QPixmap.fromImage(
|
|
75
|
+
qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
|
|
71
76
|
|
|
72
77
|
def update_thumbnails(self):
|
|
73
78
|
self.update_master_thumbnail()
|
|
@@ -112,7 +117,8 @@ class DisplayManager(QObject):
|
|
|
112
117
|
label_widget.setAlignment(Qt.AlignCenter)
|
|
113
118
|
|
|
114
119
|
def rename_label(label_widget, old_label, i):
|
|
115
|
-
new_label, ok = QInputDialog.getText(
|
|
120
|
+
new_label, ok = QInputDialog.getText(
|
|
121
|
+
self.thumbnail_list, "Rename Label", "New label name:", text=old_label)
|
|
116
122
|
if ok and new_label and new_label != old_label:
|
|
117
123
|
label_widget.setText(new_label)
|
|
118
124
|
self.set_layer_labels(i, new_label)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0914, E1101, R0911, R0912
|
|
2
|
+
import os
|
|
2
3
|
import traceback
|
|
4
|
+
import numpy as np
|
|
3
5
|
import cv2
|
|
4
|
-
import os
|
|
5
6
|
from psdtags import PsdChannelId
|
|
6
7
|
from PySide6.QtCore import QThread, Signal
|
|
7
8
|
from .. algorithms.utils import read_img
|
|
@@ -20,10 +21,11 @@ class FileLoader(QThread):
|
|
|
20
21
|
try:
|
|
21
22
|
current_stack, current_labels = self.load_stack(self.path)
|
|
22
23
|
if current_stack is None or len(current_stack) == 0:
|
|
23
|
-
self.error.emit("
|
|
24
|
+
self.error.emit(f"The file {self.path} does not contain a valid image.")
|
|
24
25
|
return
|
|
25
26
|
if current_labels:
|
|
26
|
-
master_indices = [i for i, label in enumerate(current_labels)
|
|
27
|
+
master_indices = [i for i, label in enumerate(current_labels)
|
|
28
|
+
if label.lower() == "master"]
|
|
27
29
|
else:
|
|
28
30
|
master_indices = []
|
|
29
31
|
master_index = -1 if len(master_indices) == 0 else master_indices[0]
|
|
@@ -76,7 +78,8 @@ class FileLoader(QThread):
|
|
|
76
78
|
if layers:
|
|
77
79
|
stack = np.array(layers)
|
|
78
80
|
if labels:
|
|
79
|
-
master_indices = [i for i, label in enumerate(labels)
|
|
81
|
+
master_indices = [i for i, label in enumerate(labels)
|
|
82
|
+
if label.lower() == "master"]
|
|
80
83
|
if master_indices:
|
|
81
84
|
master_index = master_indices[0]
|
|
82
85
|
master_label = labels.pop(master_index)
|
|
@@ -86,8 +89,9 @@ class FileLoader(QThread):
|
|
|
86
89
|
stack = np.insert(stack, 0, master_layer, axis=0)
|
|
87
90
|
return stack, labels
|
|
88
91
|
return stack, labels
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
return None, None
|
|
93
|
+
except ValueError as val_err:
|
|
94
|
+
if str(val_err) == "TIFF file contains no ImageSourceData tag":
|
|
91
95
|
try:
|
|
92
96
|
stack = np.array([cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)])
|
|
93
97
|
return stack, [path.split('/')[-1].split('.')[0]]
|
|
@@ -95,8 +99,8 @@ class FileLoader(QThread):
|
|
|
95
99
|
traceback.print_tb(e.__traceback__)
|
|
96
100
|
return None, None
|
|
97
101
|
else:
|
|
98
|
-
traceback.print_tb(
|
|
99
|
-
raise
|
|
102
|
+
traceback.print_tb(val_err.__traceback__)
|
|
103
|
+
raise val_err
|
|
100
104
|
except Exception as e:
|
|
101
105
|
traceback.print_tb(e.__traceback__)
|
|
102
106
|
return None, None
|
|
@@ -1,21 +1,18 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0902
|
|
1
2
|
from PySide6.QtWidgets import QMainWindow, QMessageBox, QAbstractItemView
|
|
2
|
-
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush
|
|
3
|
-
from PySide6.QtCore import Qt, QPoint
|
|
4
3
|
from .. config.constants import constants
|
|
5
|
-
from .. config.gui_constants import gui_constants
|
|
6
4
|
from .undo_manager import UndoManager
|
|
7
5
|
from .layer_collection import LayerCollection
|
|
8
6
|
from .io_gui_handler import IOGuiHandler
|
|
9
|
-
from .brush_gradient import create_brush_gradient
|
|
10
7
|
from .display_manager import DisplayManager
|
|
11
8
|
from .brush_tool import BrushTool
|
|
9
|
+
from .layer_collection import LayerCollectionHandler
|
|
12
10
|
|
|
13
11
|
|
|
14
|
-
class ImageEditor(QMainWindow):
|
|
12
|
+
class ImageEditor(QMainWindow, LayerCollectionHandler):
|
|
15
13
|
def __init__(self):
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
layer_collection.add_to(self)
|
|
14
|
+
QMainWindow.__init__(self)
|
|
15
|
+
LayerCollectionHandler.__init__(self, LayerCollection())
|
|
19
16
|
self.undo_manager = UndoManager()
|
|
20
17
|
self.undo_action = None
|
|
21
18
|
self.redo_action = None
|
|
@@ -25,10 +22,12 @@ class ImageEditor(QMainWindow):
|
|
|
25
22
|
self.brush_tool = BrushTool()
|
|
26
23
|
self.modified = False
|
|
27
24
|
self.installEventFilter(self)
|
|
25
|
+
self.mask_layer = None
|
|
28
26
|
|
|
29
27
|
def setup_ui(self):
|
|
30
|
-
self.display_manager = DisplayManager(
|
|
31
|
-
|
|
28
|
+
self.display_manager = DisplayManager(
|
|
29
|
+
self.layer_collection, self.image_viewer,
|
|
30
|
+
self.master_thumbnail_label, self.thumbnail_list, parent=self)
|
|
32
31
|
self.io_gui_handler = IOGuiHandler(self.layer_collection, self.undo_manager, parent=self)
|
|
33
32
|
self.display_manager.status_message_requested.connect(self.show_status_message)
|
|
34
33
|
self.display_manager.cursor_preview_state_changed.connect(
|
|
@@ -46,6 +45,7 @@ class ImageEditor(QMainWindow):
|
|
|
46
45
|
def show_status_message(self, message):
|
|
47
46
|
self.statusBar().showMessage(message)
|
|
48
47
|
|
|
48
|
+
# pylint: disable=C0103
|
|
49
49
|
def keyPressEvent(self, event):
|
|
50
50
|
if self.image_viewer.empty:
|
|
51
51
|
return
|
|
@@ -62,8 +62,9 @@ class ImageEditor(QMainWindow):
|
|
|
62
62
|
self.brush_tool.increase_brush_hardness()
|
|
63
63
|
return
|
|
64
64
|
super().keyPressEvent(event)
|
|
65
|
+
# pylint: enable=C0103
|
|
65
66
|
|
|
66
|
-
def
|
|
67
|
+
def check_unsaved_changes(self) -> bool:
|
|
67
68
|
if self.modified:
|
|
68
69
|
reply = QMessageBox.question(
|
|
69
70
|
self, "Unsaved Changes",
|
|
@@ -124,7 +125,8 @@ class ImageEditor(QMainWindow):
|
|
|
124
125
|
|
|
125
126
|
def highlight_thumbnail(self, index):
|
|
126
127
|
self.thumbnail_list.setCurrentRow(index)
|
|
127
|
-
self.thumbnail_list.scrollToItem(
|
|
128
|
+
self.thumbnail_list.scrollToItem(
|
|
129
|
+
self.thumbnail_list.item(index), QAbstractItemView.PositionAtCenter)
|
|
128
130
|
|
|
129
131
|
def copy_layer_to_master(self):
|
|
130
132
|
if self.layer_stack() is None or self.master_layer() is None:
|
|
@@ -155,42 +157,6 @@ class ImageEditor(QMainWindow):
|
|
|
155
157
|
view_pos, self.image_viewer)
|
|
156
158
|
self.undo_manager.extend_undo_area(*area)
|
|
157
159
|
|
|
158
|
-
def update_brush_thumb(self):
|
|
159
|
-
width, height = gui_constants.UI_SIZES['brush_preview']
|
|
160
|
-
pixmap = QPixmap(width, height)
|
|
161
|
-
pixmap.fill(Qt.transparent)
|
|
162
|
-
painter = QPainter(pixmap)
|
|
163
|
-
painter.setRenderHint(QPainter.Antialiasing)
|
|
164
|
-
preview_size = min(self.brush.size, width + 30, height + 30)
|
|
165
|
-
center_x, center_y = width // 2, height // 2
|
|
166
|
-
radius = preview_size // 2
|
|
167
|
-
if self.image_viewer.cursor_style == 'preview':
|
|
168
|
-
gradient = create_brush_gradient(
|
|
169
|
-
center_x, center_y, radius,
|
|
170
|
-
self.brush.hardness,
|
|
171
|
-
inner_color=QColor(*gui_constants.BRUSH_COLORS['inner']),
|
|
172
|
-
outer_color=QColor(*gui_constants.BRUSH_COLORS['gradient_end']),
|
|
173
|
-
opacity=self.brush.opacity
|
|
174
|
-
)
|
|
175
|
-
painter.setBrush(QBrush(gradient))
|
|
176
|
-
painter.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['outer']), gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
|
|
177
|
-
elif self.image_viewer.cursor_style == 'outline':
|
|
178
|
-
painter.setBrush(Qt.NoBrush)
|
|
179
|
-
painter.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['outer']), gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
|
|
180
|
-
else:
|
|
181
|
-
painter.setBrush(QBrush(QColor(*gui_constants.BRUSH_COLORS['cursor_inner'])))
|
|
182
|
-
painter.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), gui_constants.BRUSH_PREVIEW_LINE_WIDTH))
|
|
183
|
-
painter.drawEllipse(QPoint(center_x, center_y), radius, radius)
|
|
184
|
-
if self.image_viewer.cursor_style == 'preview':
|
|
185
|
-
painter.setPen(QPen(QColor(0, 0, 160)))
|
|
186
|
-
painter.drawText(0, 10, f"Size: {int(self.brush.size)}px")
|
|
187
|
-
painter.drawText(0, 25, f"Hardness: {self.brush.hardness}%")
|
|
188
|
-
painter.drawText(0, 40, f"Opacity: {self.brush.opacity}%")
|
|
189
|
-
painter.drawText(0, 55, f"Flow: {self.brush.flow}%")
|
|
190
|
-
painter.end()
|
|
191
|
-
self.brush_preview.setPixmap(pixmap)
|
|
192
|
-
self.image_viewer.update_brush_cursor()
|
|
193
|
-
|
|
194
160
|
def begin_copy_brush_area(self, pos):
|
|
195
161
|
if self.display_manager.allow_cursor_preview():
|
|
196
162
|
self.mask_layer = self.io_gui_handler.blank_layer.copy()
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0902, R0914, R0915
|
|
2
|
+
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel,
|
|
3
|
+
QListWidget, QSlider)
|
|
2
4
|
from PySide6.QtGui import QShortcut, QKeySequence, QAction, QActionGroup
|
|
3
5
|
from PySide6.QtCore import Qt
|
|
4
6
|
from PySide6.QtGui import QGuiApplication
|
|
@@ -14,7 +16,8 @@ def brush_size_to_slider(size):
|
|
|
14
16
|
return 0
|
|
15
17
|
if size >= gui_constants.BRUSH_SIZES['max']:
|
|
16
18
|
return gui_constants.BRUSH_SIZE_SLIDER_MAX
|
|
17
|
-
normalized = ((size - gui_constants.BRUSH_SIZES['min']) /
|
|
19
|
+
normalized = ((size - gui_constants.BRUSH_SIZES['min']) /
|
|
20
|
+
gui_constants.BRUSH_SIZES['max']) ** (1 / gui_constants.BRUSH_GAMMA)
|
|
18
21
|
return int(normalized * gui_constants.BRUSH_SIZE_SLIDER_MAX)
|
|
19
22
|
|
|
20
23
|
|
|
@@ -25,6 +28,7 @@ class ImageEditorUI(ImageFilters):
|
|
|
25
28
|
self.setup_ui()
|
|
26
29
|
self.setup_menu()
|
|
27
30
|
self.setup_shortcuts()
|
|
31
|
+
self._dialog = None
|
|
28
32
|
|
|
29
33
|
def setup_shortcuts(self):
|
|
30
34
|
prev_layer = QShortcut(QKeySequence(Qt.Key_Up), self, context=Qt.ApplicationShortcut)
|
|
@@ -129,7 +133,8 @@ class ImageEditorUI(ImageFilters):
|
|
|
129
133
|
master_thumbnail_layout.setContentsMargins(2, 2, 2, 2)
|
|
130
134
|
self.master_thumbnail_label = QLabel()
|
|
131
135
|
self.master_thumbnail_label.setAlignment(Qt.AlignCenter)
|
|
132
|
-
self.master_thumbnail_label.setFixedSize(
|
|
136
|
+
self.master_thumbnail_label.setFixedSize(
|
|
137
|
+
gui_constants.THUMB_WIDTH, gui_constants.THUMB_HEIGHT)
|
|
133
138
|
self.master_thumbnail_label.mousePressEvent = lambda e: self.set_view_master()
|
|
134
139
|
master_thumbnail_layout.addWidget(self.master_thumbnail_label)
|
|
135
140
|
side_layout.addWidget(self.master_thumbnail_frame)
|
|
@@ -313,7 +318,7 @@ class ImageEditorUI(ImageFilters):
|
|
|
313
318
|
filter_menu = menubar.addMenu("&Filter")
|
|
314
319
|
filter_menu.setObjectName("Filter")
|
|
315
320
|
denoise_action = QAction("Denoise", self)
|
|
316
|
-
denoise_action.triggered.connect(self.
|
|
321
|
+
denoise_action.triggered.connect(self.denoise_filter)
|
|
317
322
|
filter_menu.addAction(denoise_action)
|
|
318
323
|
unsharp_mask_action = QAction("Unsharp Mask", self)
|
|
319
324
|
unsharp_mask_action.triggered.connect(self.unsharp_mask)
|
|
@@ -339,7 +344,7 @@ class ImageEditorUI(ImageFilters):
|
|
|
339
344
|
self.window().showNormal()
|
|
340
345
|
|
|
341
346
|
def quit(self):
|
|
342
|
-
if self.
|
|
347
|
+
if self.check_unsaved_changes():
|
|
343
348
|
self.close()
|
|
344
349
|
|
|
345
350
|
def undo(self):
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, R0914
|
|
1
2
|
import numpy as np
|
|
2
3
|
from .image_editor import ImageEditor
|
|
3
4
|
from .filter_manager import FilterManager
|
|
@@ -14,7 +15,7 @@ class ImageFilters(ImageEditor):
|
|
|
14
15
|
self.filter_manager.register_filter("unsharp_mask", UnsharpMaskFilter)
|
|
15
16
|
self.filter_manager.register_filter("white_balance", WhiteBalanceFilter)
|
|
16
17
|
|
|
17
|
-
def
|
|
18
|
+
def denoise_filter(self):
|
|
18
19
|
self.filter_manager.apply("denoise")
|
|
19
20
|
|
|
20
21
|
def unsharp_mask(self):
|
|
@@ -47,7 +48,8 @@ class ImageFilters(ImageEditor):
|
|
|
47
48
|
x1 = min(master_layer.shape[1], x + radius + 1)
|
|
48
49
|
y0 = max(0, y - radius)
|
|
49
50
|
y1 = min(master_layer.shape[0], y + radius + 1)
|
|
50
|
-
mask = mask[radius - (y - y0): radius + (y1 - y),
|
|
51
|
+
mask = mask[radius - (y - y0): radius + (y1 - y),
|
|
52
|
+
radius - (x - x0): radius + (x1 - x)]
|
|
51
53
|
region = master_layer[y0:y1, x0:x1]
|
|
52
54
|
if region.size == 0:
|
|
53
55
|
pixel = master_layer[y, x]
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0904, R0902, R0914
|
|
1
2
|
import math
|
|
2
3
|
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
|
|
3
4
|
from PySide6.QtGui import QPixmap, QPainter, QColor, QPen, QBrush, QCursor, QShortcut, QKeySequence
|
|
4
5
|
from PySide6.QtCore import Qt, QRectF, QTime, QPoint, QPointF, Signal
|
|
5
6
|
from .. config.gui_constants import gui_constants
|
|
6
7
|
from .brush_preview import BrushPreviewItem
|
|
7
|
-
from .brush_gradient import
|
|
8
|
+
from .brush_gradient import create_default_brush_gradient
|
|
9
|
+
from .layer_collection import LayerCollectionHandler
|
|
8
10
|
|
|
9
11
|
|
|
10
|
-
class ImageViewer(QGraphicsView):
|
|
12
|
+
class ImageViewer(QGraphicsView, LayerCollectionHandler):
|
|
11
13
|
temp_view_requested = Signal(bool)
|
|
12
14
|
brush_operation_started = Signal(QPoint)
|
|
13
15
|
brush_operation_continued = Signal(QPoint)
|
|
@@ -15,7 +17,8 @@ class ImageViewer(QGraphicsView):
|
|
|
15
17
|
brush_size_change_requested = Signal(int) # +1 or -1
|
|
16
18
|
|
|
17
19
|
def __init__(self, layer_collection, parent=None):
|
|
18
|
-
|
|
20
|
+
QGraphicsView.__init__(self, parent)
|
|
21
|
+
LayerCollectionHandler.__init__(self)
|
|
19
22
|
self.display_manager = None
|
|
20
23
|
self.layer_collection = layer_collection
|
|
21
24
|
self.brush = None
|
|
@@ -43,11 +46,12 @@ class ImageViewer(QGraphicsView):
|
|
|
43
46
|
self.scrolling = False
|
|
44
47
|
self.dragging = False
|
|
45
48
|
self.last_update_time = QTime.currentTime()
|
|
46
|
-
self.
|
|
47
|
-
self.
|
|
49
|
+
self.set_layer_collection(layer_collection)
|
|
50
|
+
self.brush_preview = BrushPreviewItem(self.layer_collection)
|
|
48
51
|
self.scene.addItem(self.brush_preview)
|
|
49
52
|
self.empty = True
|
|
50
53
|
self.allow_cursor_preview = True
|
|
54
|
+
self.last_brush_pos = None
|
|
51
55
|
|
|
52
56
|
def set_image(self, qimage):
|
|
53
57
|
pixmap = QPixmap.fromImage(qimage)
|
|
@@ -73,12 +77,13 @@ class ImageViewer(QGraphicsView):
|
|
|
73
77
|
self.scene.addItem(self.pixmap_item)
|
|
74
78
|
self.zoom_factor = 1.0
|
|
75
79
|
self.setup_brush_cursor()
|
|
76
|
-
self.brush_preview = BrushPreviewItem()
|
|
80
|
+
self.brush_preview = BrushPreviewItem(self.layer_collection)
|
|
77
81
|
self.scene.addItem(self.brush_preview)
|
|
78
82
|
self.setCursor(Qt.ArrowCursor)
|
|
79
83
|
self.brush_cursor.hide()
|
|
80
84
|
self.empty = True
|
|
81
85
|
|
|
86
|
+
# pylint: disable=C0103
|
|
82
87
|
def keyPressEvent(self, event):
|
|
83
88
|
if self.empty:
|
|
84
89
|
return
|
|
@@ -140,7 +145,8 @@ class ImageViewer(QGraphicsView):
|
|
|
140
145
|
if self.dragging and event.buttons() & Qt.LeftButton:
|
|
141
146
|
current_time = QTime.currentTime()
|
|
142
147
|
if self.last_update_time.msecsTo(current_time) >= gui_constants.PAINT_REFRESH_TIMER:
|
|
143
|
-
min_step = brush_size *
|
|
148
|
+
min_step = brush_size * \
|
|
149
|
+
gui_constants.MIN_MOUSE_STEP_BRUSH_FRACTION * self.zoom_factor
|
|
144
150
|
x, y = position.x(), position.y()
|
|
145
151
|
xp, yp = self.last_brush_pos.x(), self.last_brush_pos.y()
|
|
146
152
|
distance = math.sqrt((x - xp)**2 + (y - yp)**2)
|
|
@@ -207,11 +213,29 @@ class ImageViewer(QGraphicsView):
|
|
|
207
213
|
self.zoom_factor = new_scale
|
|
208
214
|
self.update_brush_cursor()
|
|
209
215
|
|
|
216
|
+
def enterEvent(self, event):
|
|
217
|
+
self.activateWindow()
|
|
218
|
+
self.setFocus()
|
|
219
|
+
if not self.empty:
|
|
220
|
+
self.setCursor(Qt.BlankCursor)
|
|
221
|
+
if self.brush_cursor:
|
|
222
|
+
self.brush_cursor.show()
|
|
223
|
+
super().enterEvent(event)
|
|
224
|
+
|
|
225
|
+
def leaveEvent(self, event):
|
|
226
|
+
if not self.empty:
|
|
227
|
+
self.setCursor(Qt.ArrowCursor)
|
|
228
|
+
if self.brush_cursor:
|
|
229
|
+
self.brush_cursor.hide()
|
|
230
|
+
super().leaveEvent(event)
|
|
231
|
+
# pylint: enable=C0103
|
|
232
|
+
|
|
210
233
|
def setup_brush_cursor(self):
|
|
211
234
|
self.setCursor(Qt.BlankCursor)
|
|
212
235
|
pen = QPen(QColor(*gui_constants.BRUSH_COLORS['pen']), 1)
|
|
213
236
|
brush = QBrush(QColor(*gui_constants.BRUSH_COLORS['cursor_inner']))
|
|
214
|
-
self.brush_cursor = self.scene.addEllipse(
|
|
237
|
+
self.brush_cursor = self.scene.addEllipse(
|
|
238
|
+
0, 0, self.brush.size, self.brush.size, pen, brush)
|
|
215
239
|
self.brush_cursor.setZValue(1000)
|
|
216
240
|
self.brush_cursor.hide()
|
|
217
241
|
|
|
@@ -256,33 +280,11 @@ class ImageViewer(QGraphicsView):
|
|
|
256
280
|
self.brush_cursor.setBrush(Qt.NoBrush)
|
|
257
281
|
|
|
258
282
|
def _setup_simple_brush_style(self, center_x, center_y, radius):
|
|
259
|
-
gradient =
|
|
260
|
-
center_x, center_y, radius,
|
|
261
|
-
self.brush.hardness,
|
|
262
|
-
inner_color=QColor(*gui_constants.BRUSH_COLORS['inner']),
|
|
263
|
-
outer_color=QColor(*gui_constants.BRUSH_COLORS['gradient_end']),
|
|
264
|
-
opacity=self.brush.opacity
|
|
265
|
-
)
|
|
283
|
+
gradient = create_default_brush_gradient(center_x, center_y, radius, self.brush)
|
|
266
284
|
self.brush_cursor.setPen(QPen(QColor(*gui_constants.BRUSH_COLORS['pen']),
|
|
267
285
|
gui_constants.BRUSH_LINE_WIDTH / self.zoom_factor))
|
|
268
286
|
self.brush_cursor.setBrush(QBrush(gradient))
|
|
269
287
|
|
|
270
|
-
def enterEvent(self, event):
|
|
271
|
-
self.activateWindow()
|
|
272
|
-
self.setFocus()
|
|
273
|
-
if not self.empty:
|
|
274
|
-
self.setCursor(Qt.BlankCursor)
|
|
275
|
-
if self.brush_cursor:
|
|
276
|
-
self.brush_cursor.show()
|
|
277
|
-
super().enterEvent(event)
|
|
278
|
-
|
|
279
|
-
def leaveEvent(self, event):
|
|
280
|
-
if not self.empty:
|
|
281
|
-
self.setCursor(Qt.ArrowCursor)
|
|
282
|
-
if self.brush_cursor:
|
|
283
|
-
self.brush_cursor.hide()
|
|
284
|
-
super().leaveEvent(event)
|
|
285
|
-
|
|
286
288
|
def setup_shortcuts(self):
|
|
287
289
|
prev_layer = QShortcut(QKeySequence(Qt.Key_Up), self, context=Qt.ApplicationShortcut)
|
|
288
290
|
prev_layer.activated.connect(self.prev_layer)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, R0902, W0718
|
|
1
2
|
import traceback
|
|
2
3
|
import numpy as np
|
|
3
4
|
from PySide6.QtWidgets import QFileDialog, QMessageBox, QVBoxLayout, QLabel, QDialog, QApplication
|
|
@@ -6,18 +7,27 @@ from PySide6.QtCore import Qt, QObject, QTimer, Signal
|
|
|
6
7
|
from .file_loader import FileLoader
|
|
7
8
|
from .exif_data import ExifData
|
|
8
9
|
from .io_manager import IOManager
|
|
10
|
+
from .layer_collection import LayerCollectionHandler
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class IOGuiHandler(QObject):
|
|
13
|
+
class IOGuiHandler(QObject, LayerCollectionHandler):
|
|
12
14
|
status_message_requested = Signal(str)
|
|
13
15
|
update_title_requested = Signal()
|
|
14
16
|
|
|
15
17
|
def __init__(self, layer_collection, undo_manager, parent):
|
|
16
|
-
|
|
18
|
+
QObject.__init__(self, parent)
|
|
19
|
+
LayerCollectionHandler.__init__(self)
|
|
17
20
|
self.io_manager = IOManager(layer_collection)
|
|
18
21
|
self.undo_manager = undo_manager
|
|
19
|
-
|
|
22
|
+
self.set_layer_collection(layer_collection)
|
|
20
23
|
self.loader_thread = None
|
|
24
|
+
self.display_manager = None
|
|
25
|
+
self.image_viewer = None
|
|
26
|
+
self.modified = None
|
|
27
|
+
self.blank_layer = None
|
|
28
|
+
self.loading_dialog = None
|
|
29
|
+
self.loading_timer = None
|
|
30
|
+
self.exif_dialog = None
|
|
21
31
|
|
|
22
32
|
def setup_ui(self, display_manager, image_viewer):
|
|
23
33
|
self.display_manager = display_manager
|
|
@@ -49,13 +59,14 @@ class IOGuiHandler(QObject):
|
|
|
49
59
|
self.loading_timer.stop()
|
|
50
60
|
self.loading_dialog.accept()
|
|
51
61
|
self.loading_dialog.deleteLater()
|
|
52
|
-
QMessageBox.critical(self, "Error", error_msg)
|
|
62
|
+
QMessageBox.critical(self.parent(), "Error", error_msg)
|
|
53
63
|
self.status_message_requested.emit(f"Error loading: {self.io_manager.current_file_path}")
|
|
54
64
|
|
|
55
65
|
def open_file(self, file_paths=None):
|
|
56
66
|
if file_paths is None:
|
|
57
67
|
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
58
|
-
self.parent(), "Open Image", "",
|
|
68
|
+
self.parent(), "Open Image", "",
|
|
69
|
+
"Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
|
|
59
70
|
if not file_paths:
|
|
60
71
|
return
|
|
61
72
|
if self.loader_thread and self.loader_thread.isRunning():
|
|
@@ -84,8 +95,9 @@ class IOGuiHandler(QObject):
|
|
|
84
95
|
self.loader_thread.start()
|
|
85
96
|
|
|
86
97
|
def import_frames(self):
|
|
87
|
-
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
88
|
-
|
|
98
|
+
file_paths, _ = QFileDialog.getOpenFileNames(
|
|
99
|
+
self.parent(), "Select frames", "",
|
|
100
|
+
"Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
|
|
89
101
|
if file_paths:
|
|
90
102
|
self.import_frames_from_files(file_paths)
|
|
91
103
|
self.status_message_requested.emit("Imported selected frames")
|
|
@@ -168,8 +180,9 @@ class IOGuiHandler(QObject):
|
|
|
168
180
|
def save_master_as(self):
|
|
169
181
|
if self.layer_stack() is None:
|
|
170
182
|
return
|
|
171
|
-
path, _ = QFileDialog.getSaveFileName(
|
|
172
|
-
|
|
183
|
+
path, _ = QFileDialog.getSaveFileName(
|
|
184
|
+
self.parent(), "Save Image", "",
|
|
185
|
+
"TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
|
|
173
186
|
if path:
|
|
174
187
|
self.save_master_to_path(path)
|
|
175
188
|
|
|
@@ -189,14 +202,13 @@ class IOGuiHandler(QObject):
|
|
|
189
202
|
if path:
|
|
190
203
|
self.io_manager.set_exif_data(path)
|
|
191
204
|
self.status_message_requested.emit(f"EXIF data extracted from {path}.")
|
|
192
|
-
self.
|
|
193
|
-
self.
|
|
205
|
+
self.exif_dialog = ExifData(self.io_manager.exif_data, self.parent())
|
|
206
|
+
self.exif_dialog.exec()
|
|
194
207
|
|
|
195
208
|
def close_file(self):
|
|
196
|
-
if self.parent().
|
|
209
|
+
if self.parent().check_unsaved_changes():
|
|
197
210
|
self.set_master_layer(None)
|
|
198
211
|
self.blank_layer = None
|
|
199
|
-
self.current_stack = None
|
|
200
212
|
self.layer_collection.reset()
|
|
201
213
|
self.io_manager.current_file_path = ''
|
|
202
214
|
self.modified = False
|