shinestacker 0.3.1__py3-none-any.whl → 0.3.2__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 +6 -6
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/balance.py +6 -7
- shinestacker/algorithms/noise_detection.py +2 -0
- shinestacker/app/open_frames.py +6 -4
- shinestacker/config/__init__.py +2 -1
- shinestacker/config/config.py +1 -0
- shinestacker/config/constants.py +1 -0
- shinestacker/config/gui_constants.py +1 -0
- shinestacker/core/__init__.py +4 -3
- shinestacker/core/colors.py +1 -0
- shinestacker/core/core_utils.py +6 -6
- shinestacker/core/exceptions.py +1 -0
- shinestacker/core/framework.py +2 -1
- shinestacker/gui/action_config.py +47 -42
- shinestacker/gui/actions_window.py +8 -5
- shinestacker/retouch/brush_preview.py +5 -6
- shinestacker/retouch/brush_tool.py +164 -0
- shinestacker/retouch/denoise_filter.py +56 -0
- shinestacker/retouch/display_manager.py +177 -0
- shinestacker/retouch/exif_data.py +2 -1
- shinestacker/retouch/filter_base.py +114 -0
- shinestacker/retouch/filter_manager.py +14 -0
- shinestacker/retouch/image_editor.py +104 -430
- shinestacker/retouch/image_editor_ui.py +32 -72
- shinestacker/retouch/image_filters.py +25 -349
- shinestacker/retouch/image_viewer.py +22 -14
- shinestacker/retouch/io_gui_handler.py +208 -0
- shinestacker/retouch/io_manager.py +9 -13
- shinestacker/retouch/layer_collection.py +65 -1
- shinestacker/retouch/unsharp_mask_filter.py +84 -0
- shinestacker/retouch/white_balance_filter.py +111 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.2.dist-info}/METADATA +3 -2
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.2.dist-info}/RECORD +38 -31
- shinestacker/retouch/brush_controller.py +0 -57
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.2.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.2.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.1.dist-info → shinestacker-0.3.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QCheckBox, QDialogButtonBox
|
|
2
|
+
from PySide6.QtCore import Qt, QTimer
|
|
3
|
+
from .filter_base import BaseFilter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DenoiseFilter(BaseFilter):
|
|
7
|
+
def __init__(self, editor):
|
|
8
|
+
super().__init__(editor)
|
|
9
|
+
self.max_range = 500.0
|
|
10
|
+
self.max_value = 10.00
|
|
11
|
+
self.initial_value = 2.5
|
|
12
|
+
self.slider = None
|
|
13
|
+
|
|
14
|
+
def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
|
|
15
|
+
dlg.setWindowTitle("Denoise")
|
|
16
|
+
dlg.setMinimumWidth(600)
|
|
17
|
+
slider_layout = QHBoxLayout()
|
|
18
|
+
slider_local = QSlider(Qt.Horizontal)
|
|
19
|
+
slider_local.setRange(0, self.max_range)
|
|
20
|
+
slider_local.setValue(int(self.initial_value / self.max_value * self.max_range))
|
|
21
|
+
slider_layout.addWidget(slider_local)
|
|
22
|
+
value_label = QLabel(f"{self.max_value:.2f}")
|
|
23
|
+
slider_layout.addWidget(value_label)
|
|
24
|
+
layout.addLayout(slider_layout)
|
|
25
|
+
preview_check = QCheckBox("Preview")
|
|
26
|
+
preview_check.setChecked(True)
|
|
27
|
+
layout.addWidget(preview_check)
|
|
28
|
+
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
|
29
|
+
layout.addWidget(button_box)
|
|
30
|
+
preview_timer = QTimer()
|
|
31
|
+
preview_timer.setSingleShot(True)
|
|
32
|
+
preview_timer.setInterval(200)
|
|
33
|
+
|
|
34
|
+
def do_preview_delayed():
|
|
35
|
+
preview_timer.start()
|
|
36
|
+
|
|
37
|
+
preview_timer.timeout.connect(do_preview)
|
|
38
|
+
|
|
39
|
+
def slider_changed(val):
|
|
40
|
+
float_val = self.max_value * float(val) / self.max_range
|
|
41
|
+
value_label.setText(f"{float_val:.2f}")
|
|
42
|
+
if preview_check.isChecked():
|
|
43
|
+
do_preview_delayed()
|
|
44
|
+
|
|
45
|
+
slider_local.valueChanged.connect(slider_changed)
|
|
46
|
+
self.editor.connect_preview_toggle(preview_check, do_preview_delayed, restore_original)
|
|
47
|
+
button_box.accepted.connect(dlg.accept)
|
|
48
|
+
button_box.rejected.connect(dlg.reject)
|
|
49
|
+
self.slider = slider_local
|
|
50
|
+
|
|
51
|
+
def get_params(self):
|
|
52
|
+
return (self.max_value * self.slider.value() / self.max_range,)
|
|
53
|
+
|
|
54
|
+
def apply(self, image, strength):
|
|
55
|
+
from .. algorithms.denoise import denoise
|
|
56
|
+
return denoise(image, strength)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from PySide6.QtWidgets import QWidget, QListWidgetItem, QVBoxLayout, QLabel, QInputDialog
|
|
3
|
+
from PySide6.QtGui import QPixmap, QImage
|
|
4
|
+
from PySide6.QtCore import Qt, QObject, QTimer, QSize, Signal
|
|
5
|
+
from .. config.gui_constants import gui_constants
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClickableLabel(QLabel):
|
|
9
|
+
double_clicked = Signal()
|
|
10
|
+
|
|
11
|
+
def __init__(self, text, parent=None):
|
|
12
|
+
super().__init__(text, parent)
|
|
13
|
+
self.setMouseTracking(True)
|
|
14
|
+
|
|
15
|
+
def mouseDoubleClickEvent(self, event):
|
|
16
|
+
if event.button() == Qt.LeftButton:
|
|
17
|
+
self.double_clicked.emit()
|
|
18
|
+
super().mouseDoubleClickEvent(event)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DisplayManager(QObject):
|
|
22
|
+
status_message_requested = Signal(str)
|
|
23
|
+
cursor_preview_state_changed = Signal(bool)
|
|
24
|
+
|
|
25
|
+
def __init__(self, layer_collection, image_viewer, master_thumbnail_label,
|
|
26
|
+
thumbnail_list, parent=None):
|
|
27
|
+
super().__init__(parent)
|
|
28
|
+
layer_collection.add_to(self)
|
|
29
|
+
self.image_viewer = image_viewer
|
|
30
|
+
self.master_thumbnail_label = master_thumbnail_label
|
|
31
|
+
self.thumbnail_list = thumbnail_list
|
|
32
|
+
self.view_mode = 'master'
|
|
33
|
+
self.temp_view_individual = False
|
|
34
|
+
self.needs_update = False
|
|
35
|
+
self.update_timer = QTimer()
|
|
36
|
+
self.update_timer.setInterval(gui_constants.PAINT_REFRESH_TIMER)
|
|
37
|
+
self.update_timer.timeout.connect(self.process_pending_updates)
|
|
38
|
+
|
|
39
|
+
def process_pending_updates(self):
|
|
40
|
+
if self.needs_update:
|
|
41
|
+
self.display_master_layer()
|
|
42
|
+
self.needs_update = False
|
|
43
|
+
|
|
44
|
+
def display_image(self, img):
|
|
45
|
+
if img is None:
|
|
46
|
+
self.image_viewer.clear_image()
|
|
47
|
+
else:
|
|
48
|
+
self.image_viewer.set_image(self.numpy_to_qimage(img))
|
|
49
|
+
|
|
50
|
+
def display_current_layer(self):
|
|
51
|
+
self.display_image(self.current_layer())
|
|
52
|
+
|
|
53
|
+
def display_master_layer(self):
|
|
54
|
+
self.display_image(self.master_layer())
|
|
55
|
+
|
|
56
|
+
def display_current_view(self):
|
|
57
|
+
if self.temp_view_individual or self.view_mode == 'individual':
|
|
58
|
+
self.display_current_layer()
|
|
59
|
+
else:
|
|
60
|
+
self.display_master_layer()
|
|
61
|
+
|
|
62
|
+
def create_thumbnail(self, layer):
|
|
63
|
+
if layer.dtype == np.uint16:
|
|
64
|
+
layer = (layer // 256).astype(np.uint8)
|
|
65
|
+
height, width = layer.shape[:2]
|
|
66
|
+
if layer.ndim == 3 and layer.shape[-1] == 3:
|
|
67
|
+
qimg = QImage(layer.data, width, height, 3 * width, QImage.Format_RGB888)
|
|
68
|
+
else:
|
|
69
|
+
qimg = QImage(layer.data, width, height, width, QImage.Format_Grayscale8)
|
|
70
|
+
return QPixmap.fromImage(qimg.scaled(*gui_constants.UI_SIZES['thumbnail'], Qt.KeepAspectRatio))
|
|
71
|
+
|
|
72
|
+
def update_thumbnails(self):
|
|
73
|
+
self.update_master_thumbnail()
|
|
74
|
+
thumbnails = []
|
|
75
|
+
if self.layer_stack() is None:
|
|
76
|
+
return
|
|
77
|
+
for i, (layer, label) in enumerate(zip(self.layer_stack(), self.layer_labels())):
|
|
78
|
+
thumbnail = self.create_thumbnail(layer)
|
|
79
|
+
thumbnails.append((thumbnail, label, i, i == self.current_layer_idx()))
|
|
80
|
+
self._update_thumbnail_list(thumbnails)
|
|
81
|
+
|
|
82
|
+
def _update_thumbnail_list(self, thumbnails):
|
|
83
|
+
self.thumbnail_list.clear()
|
|
84
|
+
for thumb_data in thumbnails:
|
|
85
|
+
thumbnail, label, index, is_current = thumb_data
|
|
86
|
+
self.add_thumbnail_item(thumbnail, label, index, is_current)
|
|
87
|
+
|
|
88
|
+
def update_master_thumbnail(self):
|
|
89
|
+
if self.has_no_master_layer():
|
|
90
|
+
self._clear_master_thumbnail()
|
|
91
|
+
else:
|
|
92
|
+
self._set_master_thumbnail(self.create_thumbnail(self.master_layer()))
|
|
93
|
+
|
|
94
|
+
def _clear_master_thumbnail(self):
|
|
95
|
+
self.master_thumbnail_label.clear()
|
|
96
|
+
|
|
97
|
+
def _set_master_thumbnail(self, pixmap):
|
|
98
|
+
self.master_thumbnail_label.setPixmap(pixmap)
|
|
99
|
+
|
|
100
|
+
def add_thumbnail_item(self, thumbnail, label, i, is_current):
|
|
101
|
+
item_widget = QWidget()
|
|
102
|
+
layout = QVBoxLayout(item_widget)
|
|
103
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
104
|
+
layout.setSpacing(0)
|
|
105
|
+
|
|
106
|
+
thumbnail_label = QLabel()
|
|
107
|
+
thumbnail_label.setPixmap(thumbnail)
|
|
108
|
+
thumbnail_label.setAlignment(Qt.AlignCenter)
|
|
109
|
+
layout.addWidget(thumbnail_label)
|
|
110
|
+
|
|
111
|
+
label_widget = ClickableLabel(label)
|
|
112
|
+
label_widget.setAlignment(Qt.AlignCenter)
|
|
113
|
+
|
|
114
|
+
def rename_label(label_widget, old_label, i):
|
|
115
|
+
new_label, ok = QInputDialog.getText(self.thumbnail_list, "Rename Label", "New label name:", text=old_label)
|
|
116
|
+
if ok and new_label and new_label != old_label:
|
|
117
|
+
label_widget.setText(new_label)
|
|
118
|
+
self.set_layer_labels(i, new_label)
|
|
119
|
+
|
|
120
|
+
label_widget.double_clicked.connect(lambda: rename_label(label_widget, label, i))
|
|
121
|
+
layout.addWidget(label_widget)
|
|
122
|
+
item = QListWidgetItem()
|
|
123
|
+
item.setSizeHint(QSize(gui_constants.IMG_WIDTH, gui_constants.IMG_HEIGHT))
|
|
124
|
+
self.thumbnail_list.addItem(item)
|
|
125
|
+
self.thumbnail_list.setItemWidget(item, item_widget)
|
|
126
|
+
|
|
127
|
+
if is_current:
|
|
128
|
+
self.thumbnail_list.setCurrentItem(item)
|
|
129
|
+
|
|
130
|
+
def set_view_master(self):
|
|
131
|
+
if self.has_no_master_layer():
|
|
132
|
+
return
|
|
133
|
+
self.view_mode = 'master'
|
|
134
|
+
self.temp_view_individual = False
|
|
135
|
+
self.display_master_layer()
|
|
136
|
+
self.status_message_requested.emit("View mode: Master")
|
|
137
|
+
self.cursor_preview_state_changed.emit(True) # True = allow preview
|
|
138
|
+
|
|
139
|
+
def set_view_individual(self):
|
|
140
|
+
if self.has_no_master_layer():
|
|
141
|
+
return
|
|
142
|
+
self.view_mode = 'individual'
|
|
143
|
+
self.temp_view_individual = False
|
|
144
|
+
self.display_current_layer()
|
|
145
|
+
self.status_message_requested.emit("View mode: Individual layers")
|
|
146
|
+
self.cursor_preview_state_changed.emit(False) # False = no preview
|
|
147
|
+
|
|
148
|
+
def start_temp_view(self):
|
|
149
|
+
if not self.temp_view_individual and self.view_mode == 'master':
|
|
150
|
+
self.temp_view_individual = True
|
|
151
|
+
self.image_viewer.update_brush_cursor()
|
|
152
|
+
self.display_current_layer()
|
|
153
|
+
self.status_message_requested.emit("Temporary view: Individual layer (hold X)")
|
|
154
|
+
|
|
155
|
+
def end_temp_view(self):
|
|
156
|
+
if self.temp_view_individual:
|
|
157
|
+
self.temp_view_individual = False
|
|
158
|
+
self.image_viewer.update_brush_cursor()
|
|
159
|
+
self.display_master_layer()
|
|
160
|
+
self.status_message_requested.emit("View mode: Master")
|
|
161
|
+
self.cursor_preview_state_changed.emit(True) # Restore preview
|
|
162
|
+
|
|
163
|
+
def numpy_to_qimage(self, array):
|
|
164
|
+
if array.dtype == np.uint16:
|
|
165
|
+
array = np.right_shift(array, 8).astype(np.uint8)
|
|
166
|
+
if array.ndim == 2:
|
|
167
|
+
height, width = array.shape
|
|
168
|
+
return QImage(memoryview(array), width, height, width, QImage.Format_Grayscale8)
|
|
169
|
+
if array.ndim == 3:
|
|
170
|
+
height, width, _ = array.shape
|
|
171
|
+
if not array.flags['C_CONTIGUOUS']:
|
|
172
|
+
array = np.ascontiguousarray(array)
|
|
173
|
+
return QImage(memoryview(array), width, height, 3 * width, QImage.Format_RGB888)
|
|
174
|
+
return QImage()
|
|
175
|
+
|
|
176
|
+
def allow_cursor_preview(self):
|
|
177
|
+
return self.view_mode == 'master' and not self.temp_view_individual
|
|
@@ -45,10 +45,11 @@ class ExifData(QDialog):
|
|
|
45
45
|
shortcuts = {}
|
|
46
46
|
if self.exif is None:
|
|
47
47
|
shortcuts['Warning:'] = 'no EXIF data found'
|
|
48
|
+
data = {}
|
|
48
49
|
else:
|
|
49
50
|
data = exif_dict(self.exif)
|
|
50
51
|
if len(data) > 0:
|
|
51
|
-
for k, (
|
|
52
|
+
for k, (_, d) in data.items():
|
|
52
53
|
if isinstance(d, IFDRational):
|
|
53
54
|
d = f"{d.numerator}/{d.denominator}"
|
|
54
55
|
else:
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from PySide6.QtWidgets import QDialog, QVBoxLayout
|
|
4
|
+
from PySide6.QtCore import Signal, QThread, QTimer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseFilter(ABC):
|
|
8
|
+
def __init__(self, editor):
|
|
9
|
+
self.editor = editor
|
|
10
|
+
self.undo_label = self.__class__.__name__
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def get_params(self):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def apply(self, image, *params):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def run_with_preview(self, **kwargs):
|
|
25
|
+
if self.editor.layer_collection.master_layer is None:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
self.editor.layer_collection.copy_master_layer()
|
|
29
|
+
dlg = QDialog(self.editor)
|
|
30
|
+
layout = QVBoxLayout(dlg)
|
|
31
|
+
active_worker = None
|
|
32
|
+
last_request_id = 0
|
|
33
|
+
|
|
34
|
+
def set_preview(img, request_id, expected_id):
|
|
35
|
+
if request_id != expected_id:
|
|
36
|
+
return
|
|
37
|
+
self.editor.layer_collection.master_layer = img
|
|
38
|
+
self.editor.display_manager.display_master_layer()
|
|
39
|
+
try:
|
|
40
|
+
dlg.activateWindow()
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def do_preview():
|
|
45
|
+
nonlocal active_worker, last_request_id
|
|
46
|
+
if active_worker and active_worker.isRunning():
|
|
47
|
+
try:
|
|
48
|
+
active_worker.quit()
|
|
49
|
+
active_worker.wait()
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
last_request_id += 1
|
|
53
|
+
current_id = last_request_id
|
|
54
|
+
params = tuple(self.get_params() or ())
|
|
55
|
+
worker = self.PreviewWorker(
|
|
56
|
+
self.apply,
|
|
57
|
+
args=(self.editor.layer_collection.master_layer_copy, *params),
|
|
58
|
+
request_id=current_id
|
|
59
|
+
)
|
|
60
|
+
active_worker = worker
|
|
61
|
+
active_worker.finished.connect(lambda img, rid: set_preview(img, rid, current_id))
|
|
62
|
+
active_worker.start()
|
|
63
|
+
|
|
64
|
+
def restore_original():
|
|
65
|
+
self.editor.layer_collection.master_layer = self.editor.layer_collection.master_layer_copy.copy()
|
|
66
|
+
self.editor.display_manager.display_master_layer()
|
|
67
|
+
try:
|
|
68
|
+
dlg.activateWindow()
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
self.setup_ui(dlg, layout, do_preview, restore_original, **kwargs)
|
|
73
|
+
QTimer.singleShot(0, do_preview)
|
|
74
|
+
accepted = dlg.exec_() == QDialog.Accepted
|
|
75
|
+
if accepted:
|
|
76
|
+
params = tuple(self.get_params() or ())
|
|
77
|
+
try:
|
|
78
|
+
h, w = self.editor.layer_collection.master_layer.shape[:2]
|
|
79
|
+
except Exception:
|
|
80
|
+
h, w = self.editor.layer_collection.master_layer_copy.shape[:2]
|
|
81
|
+
if hasattr(self.editor, "undo_manager"):
|
|
82
|
+
try:
|
|
83
|
+
self.editor.undo_manager.extend_undo_area(0, 0, w, h)
|
|
84
|
+
self.editor.undo_manager.save_undo_state(
|
|
85
|
+
self.editor.layer_collection.master_layer_copy,
|
|
86
|
+
self.undo_label
|
|
87
|
+
)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
final_img = self.apply(self.editor.layer_collection.master_layer_copy, *params)
|
|
91
|
+
self.editor.layer_collection.master_layer = final_img
|
|
92
|
+
self.editor.layer_collection.copy_master_layer()
|
|
93
|
+
self.editor.display_manager.display_master_layer()
|
|
94
|
+
self.editor.display_manager.update_master_thumbnail()
|
|
95
|
+
self.editor.mark_as_modified()
|
|
96
|
+
else:
|
|
97
|
+
restore_original()
|
|
98
|
+
|
|
99
|
+
class PreviewWorker(QThread):
|
|
100
|
+
finished = Signal(np.ndarray, int)
|
|
101
|
+
|
|
102
|
+
def __init__(self, func, args=(), kwargs=None, request_id=0):
|
|
103
|
+
super().__init__()
|
|
104
|
+
self.func = func
|
|
105
|
+
self.args = args
|
|
106
|
+
self.kwargs = kwargs or {}
|
|
107
|
+
self.request_id = request_id
|
|
108
|
+
|
|
109
|
+
def run(self):
|
|
110
|
+
try:
|
|
111
|
+
result = self.func(*self.args, **self.kwargs)
|
|
112
|
+
except Exception:
|
|
113
|
+
raise
|
|
114
|
+
self.finished.emit(result, self.request_id)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class FilterManager:
|
|
2
|
+
def __init__(self, editor):
|
|
3
|
+
self.editor = editor
|
|
4
|
+
self.filters = {}
|
|
5
|
+
|
|
6
|
+
def register_filter(self, name, filter_class):
|
|
7
|
+
self.filters[name] = filter_class(self.editor)
|
|
8
|
+
|
|
9
|
+
def get_filter(self, name):
|
|
10
|
+
return self.filters.get(name)
|
|
11
|
+
|
|
12
|
+
def apply(self, name, **kwargs):
|
|
13
|
+
if name in self.filters:
|
|
14
|
+
self.filters[name].run_with_preview(**kwargs)
|