shinestacker 0.5.0__py3-none-any.whl → 1.0.1__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 +4 -12
- shinestacker/algorithms/balance.py +11 -9
- shinestacker/algorithms/depth_map.py +0 -30
- shinestacker/algorithms/utils.py +10 -0
- shinestacker/algorithms/vignetting.py +116 -70
- shinestacker/app/about_dialog.py +37 -16
- shinestacker/app/gui_utils.py +1 -1
- shinestacker/app/help_menu.py +1 -1
- shinestacker/app/main.py +2 -2
- shinestacker/app/project.py +2 -2
- shinestacker/config/constants.py +4 -1
- shinestacker/config/gui_constants.py +3 -4
- shinestacker/gui/action_config.py +5 -561
- shinestacker/gui/action_config_dialog.py +567 -0
- shinestacker/gui/base_form_dialog.py +18 -0
- shinestacker/gui/colors.py +5 -6
- shinestacker/gui/gui_logging.py +0 -1
- shinestacker/gui/gui_run.py +54 -106
- shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker/gui/ico/shinestacker.ico +0 -0
- shinestacker/gui/ico/shinestacker.png +0 -0
- shinestacker/gui/ico/shinestacker.svg +60 -0
- shinestacker/gui/main_window.py +275 -371
- shinestacker/gui/menu_manager.py +236 -0
- shinestacker/gui/new_project.py +75 -20
- shinestacker/gui/{actions_window.py → project_controller.py} +166 -79
- shinestacker/gui/project_converter.py +6 -6
- shinestacker/gui/project_editor.py +248 -165
- shinestacker/gui/project_model.py +2 -7
- shinestacker/gui/tab_widget.py +81 -0
- shinestacker/gui/time_progress_bar.py +95 -0
- shinestacker/retouch/base_filter.py +173 -40
- shinestacker/retouch/brush_preview.py +0 -10
- shinestacker/retouch/brush_tool.py +2 -5
- shinestacker/retouch/denoise_filter.py +5 -44
- shinestacker/retouch/exif_data.py +10 -13
- shinestacker/retouch/file_loader.py +1 -1
- shinestacker/retouch/filter_manager.py +1 -4
- shinestacker/retouch/image_editor_ui.py +318 -40
- shinestacker/retouch/image_viewer.py +34 -11
- shinestacker/retouch/io_gui_handler.py +34 -30
- shinestacker/retouch/layer_collection.py +2 -0
- shinestacker/retouch/shortcuts_help.py +12 -0
- shinestacker/retouch/unsharp_mask_filter.py +10 -10
- shinestacker/retouch/vignetting_filter.py +69 -0
- shinestacker/retouch/white_balance_filter.py +46 -14
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/METADATA +8 -2
- shinestacker-1.0.1.dist-info/RECORD +91 -0
- shinestacker/app/app_config.py +0 -22
- shinestacker/retouch/image_editor.py +0 -197
- shinestacker/retouch/image_filters.py +0 -69
- shinestacker-0.5.0.dist-info/RECORD +0 -87
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/WHEEL +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -6,8 +6,8 @@ from .base_filter import BaseFilter
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class UnsharpMaskFilter(BaseFilter):
|
|
9
|
-
def __init__(self, editor):
|
|
10
|
-
super().__init__(editor)
|
|
9
|
+
def __init__(self, name, editor):
|
|
10
|
+
super().__init__(name, editor, preview_at_startup=True)
|
|
11
11
|
self.max_range = 500.0
|
|
12
12
|
self.max_radius = 4.0
|
|
13
13
|
self.max_amount = 3.0
|
|
@@ -46,14 +46,14 @@ class UnsharpMaskFilter(BaseFilter):
|
|
|
46
46
|
elif name == "Threshold":
|
|
47
47
|
self.threshold_slider = slider
|
|
48
48
|
value_labels[name] = value_label
|
|
49
|
-
|
|
50
|
-
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200)
|
|
49
|
+
self.create_base_widgets(
|
|
50
|
+
layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200, dlg)
|
|
51
51
|
|
|
52
52
|
def update_value(name, value, max_val, fmt):
|
|
53
53
|
float_value = max_val * value / self.max_range
|
|
54
54
|
value_labels[name].setText(fmt.format(float_value))
|
|
55
|
-
if preview_check.isChecked():
|
|
56
|
-
preview_timer.start()
|
|
55
|
+
if self.preview_check.isChecked():
|
|
56
|
+
self.preview_timer.start()
|
|
57
57
|
|
|
58
58
|
self.radius_slider.valueChanged.connect(
|
|
59
59
|
lambda v: update_value("Radius", v, self.max_radius, params["Radius"][2]))
|
|
@@ -61,10 +61,10 @@ class UnsharpMaskFilter(BaseFilter):
|
|
|
61
61
|
lambda v: update_value("Amount", v, self.max_amount, params["Amount"][2]))
|
|
62
62
|
self.threshold_slider.valueChanged.connect(
|
|
63
63
|
lambda v: update_value("Threshold", v, self.max_threshold, params["Threshold"][2]))
|
|
64
|
-
preview_timer.timeout.connect(do_preview)
|
|
65
|
-
self.editor.connect_preview_toggle(preview_check, do_preview, restore_original)
|
|
66
|
-
button_box.accepted.connect(dlg.accept)
|
|
67
|
-
button_box.rejected.connect(dlg.reject)
|
|
64
|
+
self.preview_timer.timeout.connect(do_preview)
|
|
65
|
+
self.editor.connect_preview_toggle(self.preview_check, do_preview, restore_original)
|
|
66
|
+
self.button_box.accepted.connect(dlg.accept)
|
|
67
|
+
self.button_box.rejected.connect(dlg.reject)
|
|
68
68
|
QTimer.singleShot(0, do_preview)
|
|
69
69
|
|
|
70
70
|
def get_params(self):
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0902
|
|
2
|
+
from PySide6.QtCore import Qt
|
|
3
|
+
from PySide6.QtWidgets import QSpinBox, QCheckBox, QLabel, QHBoxLayout, QSlider
|
|
4
|
+
from .. config.constants import constants
|
|
5
|
+
from .. algorithms.vignetting import correct_vignetting
|
|
6
|
+
from .base_filter import OneSliderBaseFilter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VignettingFilter(OneSliderBaseFilter):
|
|
10
|
+
def __init__(self, name, editor):
|
|
11
|
+
super().__init__(name, editor, 1.0, 0.90, "Vignetting correction",
|
|
12
|
+
allow_partial_preview=False, preview_at_startup=False)
|
|
13
|
+
self.subsample_box = None
|
|
14
|
+
self.fast_subsampling_check = None
|
|
15
|
+
self.r_steps_box = None
|
|
16
|
+
self.threshold_slider = None
|
|
17
|
+
self.threshold_label = None
|
|
18
|
+
self.threshold_max_range = 500
|
|
19
|
+
self.threshold_max_value = 128.0
|
|
20
|
+
self.threshold_initial_value = constants.DEFAULT_BLACK_THRESHOLD
|
|
21
|
+
self.threshold_format = "{:.1f}"
|
|
22
|
+
|
|
23
|
+
def apply(self, image, strength):
|
|
24
|
+
return correct_vignetting(image, max_correction=strength,
|
|
25
|
+
black_threshold=self.threshold_slider.value(),
|
|
26
|
+
r_steps=self.r_steps_box.value(),
|
|
27
|
+
subsample=self.subsample_box.value(),
|
|
28
|
+
fast_subsampling=True)
|
|
29
|
+
|
|
30
|
+
def add_widgets(self, layout, dlg):
|
|
31
|
+
threshold_layout = QHBoxLayout()
|
|
32
|
+
threshold_layout.addWidget(QLabel("Threshold:"))
|
|
33
|
+
self.threshold_slider = QSlider(Qt.Horizontal)
|
|
34
|
+
self.threshold_slider.setRange(0, self.threshold_max_range)
|
|
35
|
+
self.threshold_slider.setValue(
|
|
36
|
+
int(self.threshold_initial_value /
|
|
37
|
+
self.threshold_max_value * self.threshold_max_range))
|
|
38
|
+
self.threshold_slider.valueChanged.connect(self.threshold_changed)
|
|
39
|
+
self.threshold_label = QLabel(self.threshold_format.format(self.threshold_initial_value))
|
|
40
|
+
threshold_layout.addWidget(self.threshold_slider)
|
|
41
|
+
threshold_layout.addWidget(self.threshold_label)
|
|
42
|
+
layout.addLayout(threshold_layout)
|
|
43
|
+
subsample_layout = QHBoxLayout()
|
|
44
|
+
subsample_label = QLabel("Subsample:")
|
|
45
|
+
self.subsample_box = QSpinBox()
|
|
46
|
+
self.subsample_box.setFixedWidth(50)
|
|
47
|
+
self.subsample_box.setRange(1, 50)
|
|
48
|
+
self.subsample_box.setValue(constants.DEFAULT_VIGN_SUBSAMPLE)
|
|
49
|
+
self.subsample_box.valueChanged.connect(self.threshold_changed)
|
|
50
|
+
self.fast_subsampling_check = QCheckBox("Fast subsampling")
|
|
51
|
+
self.fast_subsampling_check.setChecked(constants.DEFAULT_VIGN_FAST_SUBSAMPLING)
|
|
52
|
+
r_steps_label = QLabel("Radial steps:")
|
|
53
|
+
self.r_steps_box = QSpinBox()
|
|
54
|
+
self.r_steps_box.setFixedWidth(50)
|
|
55
|
+
self.r_steps_box.setRange(1, 200)
|
|
56
|
+
self.r_steps_box.setValue(constants.DEFAULT_R_STEPS)
|
|
57
|
+
self.r_steps_box.valueChanged.connect(self.param_changed)
|
|
58
|
+
subsample_layout.addWidget(subsample_label)
|
|
59
|
+
subsample_layout.addWidget(self.subsample_box)
|
|
60
|
+
subsample_layout.addWidget(r_steps_label)
|
|
61
|
+
subsample_layout.addWidget(self.r_steps_box)
|
|
62
|
+
subsample_layout.addStretch(1)
|
|
63
|
+
layout.addLayout(subsample_layout)
|
|
64
|
+
layout.addWidget(self.fast_subsampling_check)
|
|
65
|
+
|
|
66
|
+
def threshold_changed(self, val):
|
|
67
|
+
float_val = self.threshold_max_value * float(val) / self.threshold_max_range
|
|
68
|
+
self.threshold_label.setText(self.threshold_format.format(float_val))
|
|
69
|
+
self.param_changed(val)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0914, R0917, R0902
|
|
2
2
|
from PySide6.QtWidgets import (QHBoxLayout, QPushButton, QFrame, QVBoxLayout, QLabel, QDialog,
|
|
3
|
-
QApplication, QSlider, QDialogButtonBox)
|
|
3
|
+
QApplication, QSlider, QDialogButtonBox, QLineEdit)
|
|
4
4
|
from PySide6.QtCore import Qt, QTimer
|
|
5
5
|
from PySide6.QtGui import QCursor
|
|
6
6
|
from .. algorithms.white_balance import white_balance_from_rgb
|
|
@@ -8,12 +8,13 @@ from .base_filter import BaseFilter
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class WhiteBalanceFilter(BaseFilter):
|
|
11
|
-
def __init__(self, editor):
|
|
12
|
-
super().__init__(editor)
|
|
11
|
+
def __init__(self, name, editor):
|
|
12
|
+
super().__init__(name, editor, preview_at_startup=True)
|
|
13
13
|
self.max_range = 255
|
|
14
14
|
self.initial_val = (128, 128, 128)
|
|
15
15
|
self.sliders = {}
|
|
16
16
|
self.value_labels = {}
|
|
17
|
+
self.rgb_hex = None
|
|
17
18
|
self.color_preview = None
|
|
18
19
|
self.preview_timer = None
|
|
19
20
|
self.original_mouse_press = None
|
|
@@ -45,29 +46,60 @@ class WhiteBalanceFilter(BaseFilter):
|
|
|
45
46
|
self.value_labels[name] = val_label
|
|
46
47
|
row_layout.addLayout(sliders_layout)
|
|
47
48
|
layout.addLayout(row_layout)
|
|
49
|
+
|
|
50
|
+
rbg_layout = QHBoxLayout()
|
|
51
|
+
rbg_layout.addWidget(QLabel("RBG hex:"))
|
|
52
|
+
self.rgb_hex = QLineEdit(self.hex_color(self.initial_val))
|
|
53
|
+
self.rgb_hex.setFixedWidth(60)
|
|
54
|
+
self.rgb_hex.textChanged.connect(self.on_rgb_change)
|
|
55
|
+
rbg_layout.addWidget(self.rgb_hex)
|
|
56
|
+
rbg_layout.addStretch(1)
|
|
57
|
+
layout.addLayout(rbg_layout)
|
|
58
|
+
|
|
48
59
|
pick_button = QPushButton("Pick Color")
|
|
49
60
|
layout.addWidget(pick_button)
|
|
50
|
-
|
|
61
|
+
self.create_base_widgets(
|
|
51
62
|
layout,
|
|
52
63
|
QDialogButtonBox.Ok | QDialogButtonBox.Reset | QDialogButtonBox.Cancel,
|
|
53
|
-
200)
|
|
64
|
+
200, dlg)
|
|
54
65
|
for slider in self.sliders.values():
|
|
55
66
|
slider.valueChanged.connect(self.on_slider_change)
|
|
56
67
|
self.preview_timer.timeout.connect(do_preview)
|
|
57
|
-
self.editor.connect_preview_toggle(preview_check, do_preview, restore_original)
|
|
68
|
+
self.editor.connect_preview_toggle(self.preview_check, do_preview, restore_original)
|
|
58
69
|
pick_button.clicked.connect(self.start_color_pick)
|
|
59
|
-
button_box.accepted.connect(dlg.accept)
|
|
60
|
-
button_box.rejected.connect(dlg.reject)
|
|
61
|
-
button_box.button(QDialogButtonBox.Reset).clicked.connect(self.reset_rgb)
|
|
70
|
+
self.button_box.accepted.connect(dlg.accept)
|
|
71
|
+
self.button_box.rejected.connect(dlg.reject)
|
|
72
|
+
self.button_box.button(QDialogButtonBox.Reset).clicked.connect(self.reset_rgb)
|
|
62
73
|
QTimer.singleShot(0, do_preview)
|
|
63
74
|
|
|
75
|
+
def hex_color(self, val):
|
|
76
|
+
return "".join([f"{int(c):0>2X}" for c in val])
|
|
77
|
+
|
|
78
|
+
def apply_preview(self, rgb):
|
|
79
|
+
self.color_preview.setStyleSheet(f"background-color: rgb{tuple(rgb)};")
|
|
80
|
+
if self.preview_timer:
|
|
81
|
+
self.preview_timer.start()
|
|
82
|
+
|
|
64
83
|
def on_slider_change(self):
|
|
65
84
|
for name in ("R", "G", "B"):
|
|
66
85
|
self.value_labels[name].setText(str(self.sliders[name].value()))
|
|
67
86
|
rgb = tuple(self.sliders[n].value() for n in ("R", "G", "B"))
|
|
68
|
-
self.
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
self.rgb_hex.blockSignals(True)
|
|
88
|
+
self.rgb_hex.setText(self.hex_color(rgb))
|
|
89
|
+
self.rgb_hex.blockSignals(False)
|
|
90
|
+
self.apply_preview(rgb)
|
|
91
|
+
|
|
92
|
+
def on_rgb_change(self):
|
|
93
|
+
txt = self.rgb_hex.text()
|
|
94
|
+
if len(txt) != 6:
|
|
95
|
+
return
|
|
96
|
+
rgb = [int(txt[i:i + 2], 16) for i in range(0, 6, 2)]
|
|
97
|
+
for name, c in zip(("R", "G", "B"), rgb):
|
|
98
|
+
self.sliders[name].blockSignals(True)
|
|
99
|
+
self.sliders[name].setValue(c)
|
|
100
|
+
self.sliders[name].blockSignals(False)
|
|
101
|
+
self.value_labels[name].setText(str(c))
|
|
102
|
+
self.apply_preview(rgb)
|
|
71
103
|
|
|
72
104
|
def start_color_pick(self):
|
|
73
105
|
for widget in QApplication.topLevelWidgets():
|
|
@@ -94,7 +126,7 @@ class WhiteBalanceFilter(BaseFilter):
|
|
|
94
126
|
self.editor.image_viewer.mousePressEvent = self.original_mouse_press
|
|
95
127
|
self.editor.image_viewer.brush_cursor.show()
|
|
96
128
|
self.editor.brush_preview.show()
|
|
97
|
-
new_filter = WhiteBalanceFilter(self.editor)
|
|
129
|
+
new_filter = WhiteBalanceFilter(self.name, self.editor)
|
|
98
130
|
new_filter.run_with_preview(init_val=rgb)
|
|
99
131
|
|
|
100
132
|
def reset_rgb(self):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shinestacker
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: ShineStacker
|
|
5
5
|
Author-email: Luca Lista <luka.lista@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0
|
|
@@ -29,6 +29,8 @@ Provides-Extra: dev
|
|
|
29
29
|
Requires-Dist: pytest; extra == "dev"
|
|
30
30
|
Dynamic: license-file
|
|
31
31
|
|
|
32
|
+
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="150" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
|
|
33
|
+
|
|
32
34
|
# Shine Stacker
|
|
33
35
|
|
|
34
36
|
## Focus Stacking Processing Framework and GUI
|
|
@@ -41,6 +43,7 @@ Dynamic: license-file
|
|
|
41
43
|
[](https://codecov.io/github/lucalista/shinestacker)
|
|
42
44
|
[](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
|
|
43
45
|
[](https://www.gnu.org/licenses/lgpl-3.0)
|
|
46
|
+
[](https://pepy.tech/projects/shinestacker)
|
|
44
47
|
|
|
45
48
|
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies.gif' width="400" referrerpolicy="no-referrer"> <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/flies_stack.jpg' width="400" referrerpolicy="no-referrer">
|
|
46
49
|
|
|
@@ -84,7 +87,10 @@ Pyramid methods in image processing
|
|
|
84
87
|
# License
|
|
85
88
|
|
|
86
89
|
<img src="https://www.gnu.org/graphics/lgplv3-147x51.png" alt="LGPL 3 logo">
|
|
87
|
-
The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
|
|
90
|
+
- **Code**: The software is provided as is under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html). See [LICENSE](https://github.com/lucalista/shinestacker/blob/main/LICENSE) for details.
|
|
91
|
+
<img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/src/shinestacker/gui/ico/shinestacker.png' width="150" referrerpolicy="no-referrer" alt="Shine Stacker Logo">
|
|
92
|
+
|
|
93
|
+
- **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.
|
|
88
94
|
|
|
89
95
|
# Attribution request
|
|
90
96
|
📸 If you publish images created with Shine Stacker, please consider adding a note such as:
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
|
|
2
|
+
shinestacker/_version.py,sha256=bMIenWosteoeUs51RbaWVZetIuzRhWymuyy-n0rfK0I,21
|
|
3
|
+
shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
|
|
4
|
+
shinestacker/algorithms/align.py,sha256=XT4DJoD5ZvpkC1-J3W3GWmWRsXJg3qJ-3zr9erT8oW0,17514
|
|
5
|
+
shinestacker/algorithms/balance.py,sha256=iSjO-pl0vQv58iEQ077EUcDTAExMKDBdtXmJXbMhazk,16721
|
|
6
|
+
shinestacker/algorithms/base_stack_algo.py,sha256=AFV2QkcFNaTcnISpsWHuAVy2De9hhaPcBNjE1O0h50I,1430
|
|
7
|
+
shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
|
|
8
|
+
shinestacker/algorithms/depth_map.py,sha256=FOR5M0brO5-9NnXDY7TWpc3OtKKSuzrOSoBMe0cP6Ho,6076
|
|
9
|
+
shinestacker/algorithms/exif.py,sha256=gY9s6Cd4g4swo5qEjSbzuVIvl1GImCYu6ytOO9WrV0I,9435
|
|
10
|
+
shinestacker/algorithms/multilayer.py,sha256=5JA6TW8oO_R3mu6cOvPno9by4md8q5sXUb8ZfsRRpmY,9259
|
|
11
|
+
shinestacker/algorithms/noise_detection.py,sha256=CDnN8pglxufY5Y-dT3mVooD4zPySdSq9CMgtDGMXBnA,8970
|
|
12
|
+
shinestacker/algorithms/pyramid.py,sha256=_Pk19lRQ21b3W3aHQ6DgAe9VVOfbsi2a9jrynF0qFVw,8610
|
|
13
|
+
shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
|
|
14
|
+
shinestacker/algorithms/stack.py,sha256=FCU89Of-s6C_DuMleG06c8V6fnIm9MFInvkkKtTsGBo,4906
|
|
15
|
+
shinestacker/algorithms/stack_framework.py,sha256=frw7sbc9qOfVBYP3ZOFZEaIn9O27Wms8j_mxSW79uI0,12460
|
|
16
|
+
shinestacker/algorithms/utils.py,sha256=2XFa16Q8JVq4C2iXZFOvv98GVKqxceFG73yFZNHJSqI,2475
|
|
17
|
+
shinestacker/algorithms/vignetting.py,sha256=yW-1TF4tesLWfKQOS0XxRkOEN82U-YDmMaj09C9cH4M,9552
|
|
18
|
+
shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
|
|
19
|
+
shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
|
|
21
|
+
shinestacker/app/gui_utils.py,sha256=08TrCj2gFGsNsF6hG7ySO2y7wcQakM5PzERkeplqNFs,2344
|
|
22
|
+
shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
|
|
23
|
+
shinestacker/app/main.py,sha256=geZwxShTDg8dDOnsGu9TvWMw2Wru6MaX9CvlGHMnRvQ,6474
|
|
24
|
+
shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
|
|
25
|
+
shinestacker/app/project.py,sha256=W0u715LZne_PNJvg9msSy27ybIjgDXiEAQdJ7_6BjYI,2774
|
|
26
|
+
shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
|
|
27
|
+
shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
|
|
28
|
+
shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
|
|
29
|
+
shinestacker/config/constants.py,sha256=l7RcRxXlIThkBXcy2GVRBWFZQRXNgqYShNEVfDpgjEU,6096
|
|
30
|
+
shinestacker/config/gui_constants.py,sha256=5DR-ET1oeMMD7lIsjvAwSuln89A7I9wy9VuAeRo2G64,2575
|
|
31
|
+
shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
|
|
32
|
+
shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
|
|
33
|
+
shinestacker/core/core_utils.py,sha256=ulJhzen5McAb5n6wWNA_KB4U_PdTEr-H2TCQkVKUaOw,1421
|
|
34
|
+
shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk2c,1618
|
|
35
|
+
shinestacker/core/framework.py,sha256=zCnJuQrHNpwEgJW23_BgS7iQrLolRWTAMB1oRp_a7Kk,7447
|
|
36
|
+
shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
|
|
37
|
+
shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
+
shinestacker/gui/action_config.py,sha256=yXNDv0MyONbHk4iUrkvMkLKKaDvpJyzA5Yr0Eikgo0c,16986
|
|
39
|
+
shinestacker/gui/action_config_dialog.py,sha256=6Xjbtj7oHGXBNiogcnPoFHqcuTOnZFlMzWCZXv8eBAI,32623
|
|
40
|
+
shinestacker/gui/base_form_dialog.py,sha256=yYqMee1mzw9VBx8siBS0jDk1qqsTIKJUgdjh92aprQk,687
|
|
41
|
+
shinestacker/gui/colors.py,sha256=m0pQQ-uvtIN1xmb_-N06BvC7pZYZZnq59ZSEJwutHuk,1432
|
|
42
|
+
shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
|
|
43
|
+
shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
|
|
44
|
+
shinestacker/gui/gui_run.py,sha256=jGrtXNT3Gn53qMPSU74aEY6PP7UQfc85SD3XIesWLuY,15147
|
|
45
|
+
shinestacker/gui/main_window.py,sha256=6zGFZkgBh3_UNWjCfNzciPeiCvBsZnjJiPy62U4ldAw,23988
|
|
46
|
+
shinestacker/gui/menu_manager.py,sha256=_L6LOikB3impEYqilqwXc0WJuunishjz57ozZlrBn7Q,9616
|
|
47
|
+
shinestacker/gui/new_project.py,sha256=zHmGrT27L7I6YHM1L8wjt7DzukLFPddFsbVyGVHfJoc,11004
|
|
48
|
+
shinestacker/gui/project_controller.py,sha256=jjM8JuPlIsQohmiqzuV1EndGVvqO4tOEdoMEUJ37SNU,15049
|
|
49
|
+
shinestacker/gui/project_converter.py,sha256=_AFfU2HYKPX78l6iX6bXJrlKpdjSl63pmKzrc6kQpn8,7348
|
|
50
|
+
shinestacker/gui/project_editor.py,sha256=uouzmUkrqouQlq-dqPOgSO16r1WOnGNV2v8jTcZlRXU,23749
|
|
51
|
+
shinestacker/gui/project_model.py,sha256=eRUmH3QmRzDtPtZoxgT6amKzN8_5XzwjHgEJeL-_JOE,4263
|
|
52
|
+
shinestacker/gui/select_path_widget.py,sha256=OfQImOmkzbvl5BBshmb7ePWrSGDJQ8VvyaAOypHAGd4,1023
|
|
53
|
+
shinestacker/gui/tab_widget.py,sha256=6iUifK-wu0EzjVFccKHirhA2fENglVi6xREKiD96aaY,2950
|
|
54
|
+
shinestacker/gui/time_progress_bar.py,sha256=Ty7pNTfbKU44Y_0YQNYtgEcxpOD-Bbi4lC8g-u9bno0,3012
|
|
55
|
+
shinestacker/gui/ico/focus_stack_bkg.png,sha256=Q86TgqvKEi_IzKI8m6aZB2a3T40UkDtexf2PdeBM9XE,163151
|
|
56
|
+
shinestacker/gui/ico/shinestacker.icns,sha256=3IshIOv0uFexYsAEPkE9xiyuw8mB5X5gffekOUhFlt0,45278
|
|
57
|
+
shinestacker/gui/ico/shinestacker.ico,sha256=8IMRk-toObWUz8iDXA-zHBWQ8Ps3vXN5u5ZEyw7sP3c,109613
|
|
58
|
+
shinestacker/gui/ico/shinestacker.png,sha256=VybGY-nig_-wMW8g_uImGxRYvcnltWcAVEPSX6AZUHM,22448
|
|
59
|
+
shinestacker/gui/ico/shinestacker.svg,sha256=r8jx5aiIT9K70MRP0ANWniFE0ctRCqH7LMQ7vcGJ8ss,6571
|
|
60
|
+
shinestacker/gui/img/close-round-line-icon.png,sha256=9HZwCjgni1s_JGUPUb_MoOfoe4tRZgM5OWzk92XFZlE,8019
|
|
61
|
+
shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzdsHvmDAjlbE18Pgo,4788
|
|
62
|
+
shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
|
|
63
|
+
shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
|
|
64
|
+
shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
|
+
shinestacker/retouch/base_filter.py,sha256=7UfuhZV3wEZefQbExVIHJ6IqKnXlMF2MelxswTMaqOo,10356
|
|
66
|
+
shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
|
|
67
|
+
shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
|
|
68
|
+
shinestacker/retouch/brush_preview.py,sha256=QKD3pL7n7YJbIibinUFYKv7lkyq_AWLpt6oyqnltrwQ,4893
|
|
69
|
+
shinestacker/retouch/brush_tool.py,sha256=nxnEuvTioPNw1WeWsT20X1zl-LNZ8i-1ExOcihikEjk,8618
|
|
70
|
+
shinestacker/retouch/denoise_filter.py,sha256=TDUHzhRKlKvCa3D5SCYCZKTpjcl81kGwmONsgSDtO1k,440
|
|
71
|
+
shinestacker/retouch/display_manager.py,sha256=XPbOBmoYc_jNA791WkWkOSaFHb0ztCZechl2p2KSlwQ,9597
|
|
72
|
+
shinestacker/retouch/exif_data.py,sha256=giqoIaMlhN6H3x8BAd73ghVHODmWIcD_QbSoDymQycU,1864
|
|
73
|
+
shinestacker/retouch/file_loader.py,sha256=x8lzKShlgElcJYLFiDoeszeVEToUUiUbUrozAeF5SMU,4812
|
|
74
|
+
shinestacker/retouch/filter_manager.py,sha256=SdYIZkZBUvuB6wDG0moGWav5sfEvIcB9ioUJR5wJFts,388
|
|
75
|
+
shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
|
|
76
|
+
shinestacker/retouch/image_editor_ui.py,sha256=GNozK_P7orgl8EioemJeyR4e_LLMbKVZi4xhYW9m38U,29893
|
|
77
|
+
shinestacker/retouch/image_viewer.py,sha256=3ebrLHTDtGd_EbIT2nNFRUjH836rblmmK7jZ62YcJ2U,19564
|
|
78
|
+
shinestacker/retouch/io_gui_handler.py,sha256=M9rlfqiIaP1wWCm6p-Ew15w0_pH8-DoBXJdEkTUeVCU,11525
|
|
79
|
+
shinestacker/retouch/io_manager.py,sha256=JUAA--AK0mVa1PTErJTnBFjaXIle5Qs7Ow0Wkd8at0o,2437
|
|
80
|
+
shinestacker/retouch/layer_collection.py,sha256=Q7zoCYRn__jYkfrEC2lY1uKHWfOUbsJ27xaYHIoKVxo,5730
|
|
81
|
+
shinestacker/retouch/shortcuts_help.py,sha256=SN4vNa_6yRAFaWxt5HpWn8FHgwmHrIs_wYwjl4iyDmg,3769
|
|
82
|
+
shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-KqMlQRo,3257
|
|
83
|
+
shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
|
|
84
|
+
shinestacker/retouch/vignetting_filter.py,sha256=3WuoF38lQOIaU1MWmqviItuQn8NnbMN0nwV7pM9IJqU,3453
|
|
85
|
+
shinestacker/retouch/white_balance_filter.py,sha256=glMBYlmrF-i_OrB3sGUpjZE6X4FQdyLC4GBy2bWtaFc,6056
|
|
86
|
+
shinestacker-1.0.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
87
|
+
shinestacker-1.0.1.dist-info/METADATA,sha256=txWeQBvdot0jMY-eCpwlZTqMataYiQn8MpSZECcxaUw,5902
|
|
88
|
+
shinestacker-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
89
|
+
shinestacker-1.0.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
|
|
90
|
+
shinestacker-1.0.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
|
|
91
|
+
shinestacker-1.0.1.dist-info/RECORD,,
|
shinestacker/app/app_config.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, C0103, W0201
|
|
2
|
-
from .. config.config import _ConfigBase
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class _AppConfig(_ConfigBase):
|
|
6
|
-
def __new__(cls):
|
|
7
|
-
return _ConfigBase.__new__(cls)
|
|
8
|
-
|
|
9
|
-
def _init_defaults(self):
|
|
10
|
-
self._DONT_USE_NATIVE_MENU = True
|
|
11
|
-
self._COMBINED_APP = False
|
|
12
|
-
|
|
13
|
-
@property
|
|
14
|
-
def DONT_USE_NATIVE_MENU(self):
|
|
15
|
-
return self._DONT_USE_NATIVE_MENU
|
|
16
|
-
|
|
17
|
-
@property
|
|
18
|
-
def COMBINED_APP(self):
|
|
19
|
-
return self._COMBINED_APP
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
app_config = _AppConfig()
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E0611, R0902
|
|
2
|
-
from PySide6.QtWidgets import QMainWindow, QMessageBox
|
|
3
|
-
from .. config.constants import constants
|
|
4
|
-
from .undo_manager import UndoManager
|
|
5
|
-
from .layer_collection import LayerCollection
|
|
6
|
-
from .io_gui_handler import IOGuiHandler
|
|
7
|
-
from .display_manager import DisplayManager
|
|
8
|
-
from .brush_tool import BrushTool
|
|
9
|
-
from .layer_collection import LayerCollectionHandler
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ImageEditor(QMainWindow, LayerCollectionHandler):
|
|
13
|
-
def __init__(self):
|
|
14
|
-
QMainWindow.__init__(self)
|
|
15
|
-
LayerCollectionHandler.__init__(self, LayerCollection())
|
|
16
|
-
self.undo_manager = UndoManager()
|
|
17
|
-
self.undo_action = None
|
|
18
|
-
self.redo_action = None
|
|
19
|
-
self.undo_manager.stack_changed.connect(self.update_undo_redo_actions)
|
|
20
|
-
self.io_gui_handler = None
|
|
21
|
-
self.display_manager = None
|
|
22
|
-
self.brush_tool = BrushTool()
|
|
23
|
-
self.modified = False
|
|
24
|
-
self.installEventFilter(self)
|
|
25
|
-
self.mask_layer = None
|
|
26
|
-
|
|
27
|
-
def setup_ui(self):
|
|
28
|
-
self.display_manager = DisplayManager(
|
|
29
|
-
self.layer_collection, self.image_viewer,
|
|
30
|
-
self.master_thumbnail_label, self.thumbnail_list, parent=self)
|
|
31
|
-
self.io_gui_handler = IOGuiHandler(self.layer_collection, self.undo_manager, parent=self)
|
|
32
|
-
self.display_manager.status_message_requested.connect(self.show_status_message)
|
|
33
|
-
self.display_manager.cursor_preview_state_changed.connect(
|
|
34
|
-
lambda state: setattr(self.image_viewer, 'allow_cursor_preview', state))
|
|
35
|
-
self.io_gui_handler.status_message_requested.connect(self.show_status_message)
|
|
36
|
-
self.io_gui_handler.update_title_requested.connect(self.update_title)
|
|
37
|
-
self.brush_tool.setup_ui(self.brush, self.brush_preview, self.image_viewer,
|
|
38
|
-
self.brush_size_slider, self.hardness_slider, self.opacity_slider,
|
|
39
|
-
self.flow_slider)
|
|
40
|
-
self.image_viewer.brush = self.brush_tool.brush
|
|
41
|
-
self.brush_tool.update_brush_thumb()
|
|
42
|
-
self.io_gui_handler.setup_ui(self.display_manager, self.image_viewer)
|
|
43
|
-
self.image_viewer.display_manager = self.display_manager
|
|
44
|
-
|
|
45
|
-
def show_status_message(self, message):
|
|
46
|
-
self.statusBar().showMessage(message)
|
|
47
|
-
|
|
48
|
-
# pylint: disable=C0103
|
|
49
|
-
def keyPressEvent(self, event):
|
|
50
|
-
if self.image_viewer.empty:
|
|
51
|
-
return
|
|
52
|
-
if event.text() == '[':
|
|
53
|
-
self.brush_tool.decrease_brush_size()
|
|
54
|
-
return
|
|
55
|
-
if event.text() == ']':
|
|
56
|
-
self.brush_tool.increase_brush_size()
|
|
57
|
-
return
|
|
58
|
-
if event.text() == '{':
|
|
59
|
-
self.brush_tool.decrease_brush_hardness()
|
|
60
|
-
return
|
|
61
|
-
if event.text() == '}':
|
|
62
|
-
self.brush_tool.increase_brush_hardness()
|
|
63
|
-
return
|
|
64
|
-
super().keyPressEvent(event)
|
|
65
|
-
# pylint: enable=C0103
|
|
66
|
-
|
|
67
|
-
def check_unsaved_changes(self) -> bool:
|
|
68
|
-
if self.modified:
|
|
69
|
-
reply = QMessageBox.question(
|
|
70
|
-
self, "Unsaved Changes",
|
|
71
|
-
"The image stack has unsaved changes. Do you want to continue?",
|
|
72
|
-
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
|
|
73
|
-
)
|
|
74
|
-
if reply == QMessageBox.Save:
|
|
75
|
-
self.save_file()
|
|
76
|
-
return True
|
|
77
|
-
if reply == QMessageBox.Discard:
|
|
78
|
-
return True
|
|
79
|
-
return False
|
|
80
|
-
return True
|
|
81
|
-
|
|
82
|
-
def sort_layers(self, order):
|
|
83
|
-
self.sort_layers(order)
|
|
84
|
-
self.display_manager.update_thumbnails()
|
|
85
|
-
self.change_layer(self.current_layer())
|
|
86
|
-
|
|
87
|
-
def update_title(self):
|
|
88
|
-
title = constants.APP_TITLE
|
|
89
|
-
if self.io_gui_handler is not None:
|
|
90
|
-
path = self.io_gui_handler.current_file_path()
|
|
91
|
-
if path != '':
|
|
92
|
-
title += f" - {path.split('/')[-1]}"
|
|
93
|
-
if self.modified:
|
|
94
|
-
title += " *"
|
|
95
|
-
self.window().setWindowTitle(title)
|
|
96
|
-
|
|
97
|
-
def mark_as_modified(self):
|
|
98
|
-
self.modified = True
|
|
99
|
-
self.save_actions_set_enabled(True)
|
|
100
|
-
self.update_title()
|
|
101
|
-
|
|
102
|
-
def change_layer(self, layer_idx):
|
|
103
|
-
if 0 <= layer_idx < self.number_of_layers():
|
|
104
|
-
view_state = self.image_viewer.get_view_state()
|
|
105
|
-
self.set_current_layer_idx(layer_idx)
|
|
106
|
-
self.display_manager.display_current_view()
|
|
107
|
-
self.image_viewer.set_view_state(view_state)
|
|
108
|
-
self.thumbnail_list.setCurrentRow(layer_idx)
|
|
109
|
-
self.thumbnail_list.setFocus()
|
|
110
|
-
self.image_viewer.update_brush_cursor()
|
|
111
|
-
self.image_viewer.setFocus()
|
|
112
|
-
|
|
113
|
-
def prev_layer(self):
|
|
114
|
-
if self.layer_stack() is not None:
|
|
115
|
-
new_idx = max(0, self.current_layer_idx() - 1)
|
|
116
|
-
if new_idx != self.current_layer_idx():
|
|
117
|
-
self.change_layer(new_idx)
|
|
118
|
-
self.display_manager.highlight_thumbnail(new_idx)
|
|
119
|
-
|
|
120
|
-
def next_layer(self):
|
|
121
|
-
if self.layer_stack() is not None:
|
|
122
|
-
new_idx = min(self.number_of_layers() - 1, self.current_layer_idx() + 1)
|
|
123
|
-
if new_idx != self.current_layer_idx():
|
|
124
|
-
self.change_layer(new_idx)
|
|
125
|
-
self.display_manager.highlight_thumbnail(new_idx)
|
|
126
|
-
|
|
127
|
-
def copy_layer_to_master(self):
|
|
128
|
-
if self.layer_stack() is None or self.master_layer() is None:
|
|
129
|
-
return
|
|
130
|
-
reply = QMessageBox.question(
|
|
131
|
-
self,
|
|
132
|
-
"Confirm Copy",
|
|
133
|
-
"Warning: the current master layer will be erased\n\nDo you want to continue?",
|
|
134
|
-
QMessageBox.Yes | QMessageBox.No,
|
|
135
|
-
QMessageBox.No
|
|
136
|
-
)
|
|
137
|
-
if reply == QMessageBox.Yes:
|
|
138
|
-
self.set_master_layer(self.current_layer().copy())
|
|
139
|
-
self.master_layer().setflags(write=True)
|
|
140
|
-
self.display_manager.display_current_view()
|
|
141
|
-
self.display_manager.update_thumbnails()
|
|
142
|
-
self.mark_as_modified()
|
|
143
|
-
self.statusBar().showMessage(f"Copied layer {self.current_layer_idx() + 1} to master")
|
|
144
|
-
|
|
145
|
-
def copy_brush_area_to_master(self, view_pos):
|
|
146
|
-
if self.layer_stack() is None or self.number_of_layers() == 0 \
|
|
147
|
-
or not self.display_manager.allow_cursor_preview():
|
|
148
|
-
return
|
|
149
|
-
area = self.brush_tool.apply_brush_operation(
|
|
150
|
-
self.master_layer_copy(),
|
|
151
|
-
self.current_layer(),
|
|
152
|
-
self.master_layer(), self.mask_layer,
|
|
153
|
-
view_pos, self.image_viewer)
|
|
154
|
-
self.undo_manager.extend_undo_area(*area)
|
|
155
|
-
|
|
156
|
-
def begin_copy_brush_area(self, pos):
|
|
157
|
-
if self.display_manager.allow_cursor_preview():
|
|
158
|
-
self.mask_layer = self.io_gui_handler.blank_layer.copy()
|
|
159
|
-
self.copy_master_layer()
|
|
160
|
-
self.undo_manager.reset_undo_area()
|
|
161
|
-
self.copy_brush_area_to_master(pos)
|
|
162
|
-
self.display_manager.needs_update = True
|
|
163
|
-
if not self.display_manager.update_timer.isActive():
|
|
164
|
-
self.display_manager.update_timer.start()
|
|
165
|
-
self.mark_as_modified()
|
|
166
|
-
|
|
167
|
-
def continue_copy_brush_area(self, pos):
|
|
168
|
-
if self.display_manager.allow_cursor_preview():
|
|
169
|
-
self.copy_brush_area_to_master(pos)
|
|
170
|
-
self.display_manager.needs_update = True
|
|
171
|
-
if not self.display_manager.update_timer.isActive():
|
|
172
|
-
self.display_manager.update_timer.start()
|
|
173
|
-
self.mark_as_modified()
|
|
174
|
-
|
|
175
|
-
def end_copy_brush_area(self):
|
|
176
|
-
if self.display_manager.update_timer.isActive():
|
|
177
|
-
self.display_manager.display_master_layer()
|
|
178
|
-
self.display_manager.update_master_thumbnail()
|
|
179
|
-
self.undo_manager.save_undo_state(self.master_layer_copy(), 'Brush Stroke')
|
|
180
|
-
self.display_manager.update_timer.stop()
|
|
181
|
-
self.mark_as_modified()
|
|
182
|
-
|
|
183
|
-
def update_undo_redo_actions(self, has_undo, undo_desc, has_redo, redo_desc):
|
|
184
|
-
if self.undo_action:
|
|
185
|
-
if has_undo:
|
|
186
|
-
self.undo_action.setText(f"Undo {undo_desc}")
|
|
187
|
-
self.undo_action.setEnabled(True)
|
|
188
|
-
else:
|
|
189
|
-
self.undo_action.setText("Undo")
|
|
190
|
-
self.undo_action.setEnabled(False)
|
|
191
|
-
if self.redo_action:
|
|
192
|
-
if has_redo:
|
|
193
|
-
self.redo_action.setText(f"Redo {redo_desc}")
|
|
194
|
-
self.redo_action.setEnabled(True)
|
|
195
|
-
else:
|
|
196
|
-
self.redo_action.setText("Redo")
|
|
197
|
-
self.redo_action.setEnabled(False)
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, R0914
|
|
2
|
-
import numpy as np
|
|
3
|
-
from .image_editor import ImageEditor
|
|
4
|
-
from .filter_manager import FilterManager
|
|
5
|
-
from .denoise_filter import DenoiseFilter
|
|
6
|
-
from .unsharp_mask_filter import UnsharpMaskFilter
|
|
7
|
-
from .white_balance_filter import WhiteBalanceFilter
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class ImageFilters(ImageEditor):
|
|
11
|
-
def __init__(self):
|
|
12
|
-
super().__init__()
|
|
13
|
-
self.filter_manager = FilterManager(self)
|
|
14
|
-
self.filter_manager.register_filter("denoise", DenoiseFilter)
|
|
15
|
-
self.filter_manager.register_filter("unsharp_mask", UnsharpMaskFilter)
|
|
16
|
-
self.filter_manager.register_filter("white_balance", WhiteBalanceFilter)
|
|
17
|
-
|
|
18
|
-
def denoise_filter(self):
|
|
19
|
-
self.filter_manager.apply("denoise")
|
|
20
|
-
|
|
21
|
-
def unsharp_mask(self):
|
|
22
|
-
self.filter_manager.apply("unsharp_mask")
|
|
23
|
-
|
|
24
|
-
def white_balance(self, init_val=None):
|
|
25
|
-
self.filter_manager.apply("white_balance", init_val=init_val or (128, 128, 128))
|
|
26
|
-
|
|
27
|
-
def connect_preview_toggle(self, preview_check, do_preview, restore_original):
|
|
28
|
-
def on_toggled(checked):
|
|
29
|
-
if checked:
|
|
30
|
-
do_preview()
|
|
31
|
-
else:
|
|
32
|
-
restore_original()
|
|
33
|
-
preview_check.toggled.connect(on_toggled)
|
|
34
|
-
|
|
35
|
-
def get_pixel_color_at(self, pos, radius=None):
|
|
36
|
-
item_pos = self.image_viewer.position_on_image(pos)
|
|
37
|
-
x = int(item_pos.x())
|
|
38
|
-
y = int(item_pos.y())
|
|
39
|
-
master_layer = self.master_layer()
|
|
40
|
-
if (0 <= x < self.master_layer().shape[1]) and \
|
|
41
|
-
(0 <= y < self.master_layer().shape[0]):
|
|
42
|
-
if radius is None:
|
|
43
|
-
radius = int(self.brush.size)
|
|
44
|
-
if radius > 0:
|
|
45
|
-
y_indices, x_indices = np.ogrid[-radius:radius + 1, -radius:radius + 1]
|
|
46
|
-
mask = x_indices**2 + y_indices**2 <= radius**2
|
|
47
|
-
x0 = max(0, x - radius)
|
|
48
|
-
x1 = min(master_layer.shape[1], x + radius + 1)
|
|
49
|
-
y0 = max(0, y - radius)
|
|
50
|
-
y1 = min(master_layer.shape[0], y + radius + 1)
|
|
51
|
-
mask = mask[radius - (y - y0): radius + (y1 - y),
|
|
52
|
-
radius - (x - x0): radius + (x1 - x)]
|
|
53
|
-
region = master_layer[y0:y1, x0:x1]
|
|
54
|
-
if region.size == 0:
|
|
55
|
-
pixel = master_layer[y, x]
|
|
56
|
-
else:
|
|
57
|
-
if region.ndim == 3:
|
|
58
|
-
pixel = [region[:, :, c][mask].mean() for c in range(region.shape[2])]
|
|
59
|
-
else:
|
|
60
|
-
pixel = region[mask].mean()
|
|
61
|
-
else:
|
|
62
|
-
pixel = self.master_layer()[y, x]
|
|
63
|
-
if np.isscalar(pixel):
|
|
64
|
-
pixel = [pixel, pixel, pixel]
|
|
65
|
-
pixel = [np.float32(x) for x in pixel]
|
|
66
|
-
if master_layer.dtype == np.uint16:
|
|
67
|
-
pixel = [x / 256.0 for x in pixel]
|
|
68
|
-
return tuple(int(v) for v in pixel)
|
|
69
|
-
return (0, 0, 0)
|