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.

Files changed (57) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +4 -12
  3. shinestacker/algorithms/balance.py +11 -9
  4. shinestacker/algorithms/depth_map.py +0 -30
  5. shinestacker/algorithms/utils.py +10 -0
  6. shinestacker/algorithms/vignetting.py +116 -70
  7. shinestacker/app/about_dialog.py +37 -16
  8. shinestacker/app/gui_utils.py +1 -1
  9. shinestacker/app/help_menu.py +1 -1
  10. shinestacker/app/main.py +2 -2
  11. shinestacker/app/project.py +2 -2
  12. shinestacker/config/constants.py +4 -1
  13. shinestacker/config/gui_constants.py +3 -4
  14. shinestacker/gui/action_config.py +5 -561
  15. shinestacker/gui/action_config_dialog.py +567 -0
  16. shinestacker/gui/base_form_dialog.py +18 -0
  17. shinestacker/gui/colors.py +5 -6
  18. shinestacker/gui/gui_logging.py +0 -1
  19. shinestacker/gui/gui_run.py +54 -106
  20. shinestacker/gui/ico/shinestacker.icns +0 -0
  21. shinestacker/gui/ico/shinestacker.ico +0 -0
  22. shinestacker/gui/ico/shinestacker.png +0 -0
  23. shinestacker/gui/ico/shinestacker.svg +60 -0
  24. shinestacker/gui/main_window.py +275 -371
  25. shinestacker/gui/menu_manager.py +236 -0
  26. shinestacker/gui/new_project.py +75 -20
  27. shinestacker/gui/{actions_window.py → project_controller.py} +166 -79
  28. shinestacker/gui/project_converter.py +6 -6
  29. shinestacker/gui/project_editor.py +248 -165
  30. shinestacker/gui/project_model.py +2 -7
  31. shinestacker/gui/tab_widget.py +81 -0
  32. shinestacker/gui/time_progress_bar.py +95 -0
  33. shinestacker/retouch/base_filter.py +173 -40
  34. shinestacker/retouch/brush_preview.py +0 -10
  35. shinestacker/retouch/brush_tool.py +2 -5
  36. shinestacker/retouch/denoise_filter.py +5 -44
  37. shinestacker/retouch/exif_data.py +10 -13
  38. shinestacker/retouch/file_loader.py +1 -1
  39. shinestacker/retouch/filter_manager.py +1 -4
  40. shinestacker/retouch/image_editor_ui.py +318 -40
  41. shinestacker/retouch/image_viewer.py +34 -11
  42. shinestacker/retouch/io_gui_handler.py +34 -30
  43. shinestacker/retouch/layer_collection.py +2 -0
  44. shinestacker/retouch/shortcuts_help.py +12 -0
  45. shinestacker/retouch/unsharp_mask_filter.py +10 -10
  46. shinestacker/retouch/vignetting_filter.py +69 -0
  47. shinestacker/retouch/white_balance_filter.py +46 -14
  48. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/METADATA +8 -2
  49. shinestacker-1.0.1.dist-info/RECORD +91 -0
  50. shinestacker/app/app_config.py +0 -22
  51. shinestacker/retouch/image_editor.py +0 -197
  52. shinestacker/retouch/image_filters.py +0 -69
  53. shinestacker-0.5.0.dist-info/RECORD +0 -87
  54. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/WHEEL +0 -0
  55. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/entry_points.txt +0 -0
  56. {shinestacker-0.5.0.dist-info → shinestacker-1.0.1.dist-info}/licenses/LICENSE +0 -0
  57. {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
- preview_check, preview_timer, button_box = self.create_base_widgets(
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
- preview_check, self.preview_timer, button_box = self.create_base_widgets(
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.color_preview.setStyleSheet(f"background-color: rgb{rgb};")
69
- if self.preview_timer:
70
- self.preview_timer.start()
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.5.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
  [![codecov](https://codecov.io/github/lucalista/shinestacker/graph/badge.svg?token=Y5NKW6VH5G)](https://codecov.io/github/lucalista/shinestacker)
42
44
  [![Documentation Status](https://readthedocs.org/projects/shinestacker/badge/?version=latest)](https://shinestacker.readthedocs.io/en/latest/?badge=latest)
43
45
  [![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
46
+ [![PyPI Downloads](https://static.pepy.tech/badge/shinestacker)](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,,
@@ -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)