shinestacker 0.2.0.post1.dev1__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 (67) hide show
  1. shinestacker/__init__.py +3 -0
  2. shinestacker/_version.py +1 -0
  3. shinestacker/algorithms/__init__.py +14 -0
  4. shinestacker/algorithms/align.py +307 -0
  5. shinestacker/algorithms/balance.py +367 -0
  6. shinestacker/algorithms/core_utils.py +22 -0
  7. shinestacker/algorithms/depth_map.py +164 -0
  8. shinestacker/algorithms/exif.py +238 -0
  9. shinestacker/algorithms/multilayer.py +187 -0
  10. shinestacker/algorithms/noise_detection.py +182 -0
  11. shinestacker/algorithms/pyramid.py +176 -0
  12. shinestacker/algorithms/stack.py +112 -0
  13. shinestacker/algorithms/stack_framework.py +248 -0
  14. shinestacker/algorithms/utils.py +71 -0
  15. shinestacker/algorithms/vignetting.py +137 -0
  16. shinestacker/app/__init__.py +0 -0
  17. shinestacker/app/about_dialog.py +24 -0
  18. shinestacker/app/app_config.py +39 -0
  19. shinestacker/app/gui_utils.py +35 -0
  20. shinestacker/app/help_menu.py +16 -0
  21. shinestacker/app/main.py +176 -0
  22. shinestacker/app/open_frames.py +39 -0
  23. shinestacker/app/project.py +91 -0
  24. shinestacker/app/retouch.py +82 -0
  25. shinestacker/config/__init__.py +4 -0
  26. shinestacker/config/config.py +53 -0
  27. shinestacker/config/constants.py +174 -0
  28. shinestacker/config/gui_constants.py +85 -0
  29. shinestacker/core/__init__.py +5 -0
  30. shinestacker/core/colors.py +60 -0
  31. shinestacker/core/core_utils.py +52 -0
  32. shinestacker/core/exceptions.py +50 -0
  33. shinestacker/core/framework.py +210 -0
  34. shinestacker/core/logging.py +89 -0
  35. shinestacker/gui/__init__.py +0 -0
  36. shinestacker/gui/action_config.py +879 -0
  37. shinestacker/gui/actions_window.py +283 -0
  38. shinestacker/gui/colors.py +57 -0
  39. shinestacker/gui/gui_images.py +152 -0
  40. shinestacker/gui/gui_logging.py +213 -0
  41. shinestacker/gui/gui_run.py +393 -0
  42. shinestacker/gui/img/close-round-line-icon.png +0 -0
  43. shinestacker/gui/img/forward-button-icon.png +0 -0
  44. shinestacker/gui/img/play-button-round-icon.png +0 -0
  45. shinestacker/gui/img/plus-round-line-icon.png +0 -0
  46. shinestacker/gui/main_window.py +599 -0
  47. shinestacker/gui/new_project.py +170 -0
  48. shinestacker/gui/project_converter.py +148 -0
  49. shinestacker/gui/project_editor.py +539 -0
  50. shinestacker/gui/project_model.py +138 -0
  51. shinestacker/retouch/__init__.py +0 -0
  52. shinestacker/retouch/brush.py +9 -0
  53. shinestacker/retouch/brush_controller.py +57 -0
  54. shinestacker/retouch/brush_preview.py +126 -0
  55. shinestacker/retouch/exif_data.py +65 -0
  56. shinestacker/retouch/file_loader.py +104 -0
  57. shinestacker/retouch/image_editor.py +651 -0
  58. shinestacker/retouch/image_editor_ui.py +380 -0
  59. shinestacker/retouch/image_viewer.py +356 -0
  60. shinestacker/retouch/shortcuts_help.py +98 -0
  61. shinestacker/retouch/undo_manager.py +38 -0
  62. shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
  63. shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
  64. shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
  65. shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
  66. shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
  67. shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,170 @@
1
+ import os
2
+ from PySide6.QtWidgets import (QWidget, QLineEdit, QFormLayout, QHBoxLayout, QPushButton,
3
+ QDialog, QSizePolicy, QFileDialog, QLabel, QCheckBox,
4
+ QSpinBox, QMessageBox)
5
+ from PySide6.QtGui import QIcon
6
+ from PySide6.QtCore import Qt
7
+ from .. config.gui_constants import gui_constants
8
+ from .. config.constants import constants
9
+ from .. core.core_utils import get_app_base_path
10
+
11
+
12
+ class NewProjectDialog(QDialog):
13
+ def __init__(self, parent=None):
14
+ super().__init__(parent)
15
+ self.setWindowTitle("New Project")
16
+ self.resize(500, self.height())
17
+ self.layout = QFormLayout(self)
18
+ self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
19
+ self.layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
20
+ self.layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
21
+ self.layout.setLabelAlignment(Qt.AlignLeft)
22
+ self.create_form()
23
+ button_box = QHBoxLayout()
24
+ ok_button = QPushButton("OK")
25
+ ok_button.setFocus()
26
+ cancel_button = QPushButton("Cancel")
27
+ button_box.addWidget(ok_button)
28
+ button_box.addWidget(cancel_button)
29
+ self.layout.addRow(button_box)
30
+ ok_button.clicked.connect(self.accept)
31
+ cancel_button.clicked.connect(self.reject)
32
+
33
+ def expert(self):
34
+ return self.parent().expert_options
35
+
36
+ def add_bold_label(self, label):
37
+ label = QLabel(label)
38
+ label.setStyleSheet("font-weight: bold")
39
+ self.layout.addRow(label)
40
+
41
+ def create_form(self):
42
+ icon_path = f'{get_app_base_path()}'
43
+ if os.path.exists(f'{icon_path}/ico'):
44
+ icon_path = f'{icon_path}/ico'
45
+ else:
46
+ icon_path = f'{icon_path}/../ico'
47
+ icon_path = f'{icon_path}/shinestacker.png'
48
+ app_icon = QIcon(icon_path)
49
+ icon_pixmap = app_icon.pixmap(128, 128)
50
+ icon_label = QLabel()
51
+ icon_label.setPixmap(icon_pixmap)
52
+ icon_label.setAlignment(Qt.AlignCenter)
53
+ self.layout.addRow(icon_label)
54
+ spacer = QLabel("")
55
+ spacer.setFixedHeight(10)
56
+ self.layout.addRow(spacer)
57
+ self.input_folder = QLineEdit()
58
+ self.input_folder .setPlaceholderText('input files folder')
59
+ button = QPushButton("Browse...")
60
+
61
+ def browse():
62
+ path = QFileDialog.getExistingDirectory(None, "Select input files folder")
63
+ if path:
64
+ self.input_folder.setText(path)
65
+ button.clicked.connect(browse)
66
+ button.setAutoDefault(False)
67
+ layout = QHBoxLayout()
68
+ layout.addWidget(self.input_folder)
69
+ layout.addWidget(button)
70
+ layout.setContentsMargins(0, 0, 0, 0)
71
+ container = QWidget()
72
+ container.setLayout(layout)
73
+ container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
74
+
75
+ self.noise_detection = QCheckBox()
76
+ self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
77
+ self.vignetting_correction = QCheckBox()
78
+ self.vignetting_correction.setChecked(gui_constants.NEW_PROJECT_VIGNETTING_CORRECTION)
79
+ self.align_frames = QCheckBox()
80
+ self.align_frames.setChecked(gui_constants.NEW_PROJECT_ALIGN_FRAMES)
81
+ self.balance_frames = QCheckBox()
82
+ self.balance_frames.setChecked(gui_constants.NEW_PROJECT_BALANCE_FRAMES)
83
+ self.bunch_stack = QCheckBox()
84
+ self.bunch_stack.setChecked(gui_constants.NEW_PROJECT_BUNCH_STACK)
85
+ self.bunch_stack.toggled.connect(self.update_bunch_options)
86
+ self.bunch_frames = QSpinBox()
87
+ bunch_frames_range = gui_constants.NEW_PROJECT_BUNCH_FRAMES
88
+ self.bunch_frames.setRange(bunch_frames_range['min'], bunch_frames_range['max'])
89
+ self.bunch_frames.setValue(constants.DEFAULT_FRAMES)
90
+ self.bunch_overlap = QSpinBox()
91
+ bunch_overlap_range = gui_constants.NEW_PROJECT_BUNCH_OVERLAP
92
+ self.bunch_overlap.setRange(bunch_overlap_range['min'], bunch_overlap_range['max'])
93
+ self.bunch_overlap.setValue(constants.DEFAULT_OVERLAP)
94
+ self.focus_stack_pyramid = QCheckBox()
95
+ self.focus_stack_pyramid.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_PYRAMID)
96
+ self.focus_stack_depth_map = QCheckBox()
97
+ self.focus_stack_depth_map.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_DEPTH_MAP)
98
+ self.multi_layer = QCheckBox()
99
+ self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
100
+
101
+ self.add_bold_label("Select input:")
102
+ self.layout.addRow("Input folder:", container)
103
+ self.add_bold_label("Select actions:")
104
+ if self.expert():
105
+ self.layout.addRow("Automatic noise detection:", self.noise_detection)
106
+ self.layout.addRow("Vignetting correction:", self.vignetting_correction)
107
+ self.layout.addRow("Align layers:", self.align_frames)
108
+ self.layout.addRow("Balance layers:", self.balance_frames)
109
+ self.layout.addRow("Bunch stack:", self.bunch_stack)
110
+ self.layout.addRow("Bunch frames:", self.bunch_frames)
111
+ self.layout.addRow("Bunch frames:", self.bunch_overlap)
112
+ if self.expert():
113
+ self.layout.addRow("Focus stack (pyramid):", self.focus_stack_pyramid)
114
+ self.layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
115
+ else:
116
+ self.layout.addRow("Focus stack:", self.focus_stack_pyramid)
117
+ self.layout.addRow("Save multi layer TIFF:", self.multi_layer)
118
+
119
+ def update_bunch_options(self, checked):
120
+ self.bunch_frames.setEnabled(checked)
121
+ self.bunch_overlap.setEnabled(checked)
122
+
123
+ def accept(self):
124
+ input_folder = self.input_folder.text()
125
+ if not input_folder:
126
+ QMessageBox.warning(self, "Input Required", "Please select an input folder")
127
+ return
128
+ if not os.path.exists(input_folder):
129
+ QMessageBox.warning(self, "Invalid Path", "The specified folder does not exist")
130
+ return
131
+ if not os.path.isdir(input_folder):
132
+ QMessageBox.warning(self, "Invalid Path", "The specified path is not a folder")
133
+ return
134
+ if len(input_folder.split('/')) < 2:
135
+ QMessageBox.warning(self, "Invalid Path", "The path must have a parent folder")
136
+ return
137
+ super().accept()
138
+
139
+ def get_input_folder(self):
140
+ return self.input_folder.text()
141
+
142
+ def get_noise_detection(self):
143
+ return self.noise_detection.isChecked()
144
+
145
+ def get_vignetting_correction(self):
146
+ return self.vignetting_correction.isChecked()
147
+
148
+ def get_align_frames(self):
149
+ return self.align_frames.isChecked()
150
+
151
+ def get_balance_frames(self):
152
+ return self.balance_frames.isChecked()
153
+
154
+ def get_bunch_stack(self):
155
+ return self.bunch_stack.isChecked()
156
+
157
+ def get_bunch_frames(self):
158
+ return self.bunch_frames.value()
159
+
160
+ def get_bunch_overlap(self):
161
+ return self.bunch_overlap.value()
162
+
163
+ def get_focus_stack_pyramid(self):
164
+ return self.focus_stack_pyramid.isChecked()
165
+
166
+ def get_focus_stack_depth_map(self):
167
+ return self.focus_stack_depth_map.isChecked()
168
+
169
+ def get_multi_layer(self):
170
+ return self.multi_layer.isChecked()
@@ -0,0 +1,148 @@
1
+ import logging
2
+ import traceback
3
+ from .. config.constants import constants
4
+ from .. core.exceptions import InvalidOptionError, RunStopException
5
+ from .. algorithms.stack_framework import StackJob, CombinedActions
6
+ from .. algorithms.noise_detection import NoiseDetection, MaskNoise
7
+ from .. algorithms.vignetting import Vignetting
8
+ from .. algorithms.align import AlignFrames
9
+ from .. algorithms.balance import BalanceFrames
10
+ from .. algorithms.stack import FocusStack, FocusStackBunch
11
+ from .. algorithms.pyramid import PyramidStack
12
+ from .. algorithms.depth_map import DepthMapStack
13
+ from .. algorithms.multilayer import MultiLayer
14
+ from .project_model import Project, ActionConfig
15
+
16
+
17
+ class ProjectConverter:
18
+ def get_logger(self, logger_name=None):
19
+ return logging.getLogger(__name__ if logger_name is None else logger_name)
20
+
21
+ def run(self, job, logger):
22
+ if job.enabled:
23
+ logger.info(f"=== run job: {job.name} ===")
24
+ else:
25
+ logger.warning(f"=== job: {job.name} disabled ===")
26
+ try:
27
+ job.run()
28
+ return constants.RUN_COMPLETED, ''
29
+ except RunStopException:
30
+ logger.warning(f"=== job: {job.name} stopped ===")
31
+ return constants.RUN_STOPPED, ''
32
+ except Exception as e:
33
+ traceback.print_tb(e.__traceback__)
34
+ msg = str(e)
35
+ logger.error(f"=== job: {job.name} failed: {msg} ===")
36
+ return constants.RUN_FAILED, msg
37
+
38
+ def run_project(self, project: Project, logger_name=None, callbacks=None):
39
+ logger = self.get_logger(logger_name)
40
+ try:
41
+ jobs = self.project(project, logger_name, callbacks)
42
+ except Exception as e:
43
+ traceback.print_tb(e.__traceback__)
44
+ return constants.RUN_FAILED, str(e)
45
+ status = constants.RUN_COMPLETED, ''
46
+ for job in jobs:
47
+ job_status, message = self.run(job, logger)
48
+ if job_status in [constants.RUN_STOPPED, constants.RUN_FAILED]:
49
+ return job_status, message
50
+ return status
51
+
52
+ def run_job(self, job: ActionConfig, logger_name=None, callbacks=None):
53
+ logger = self.get_logger(logger_name)
54
+ try:
55
+ job = self.job(job, logger_name, callbacks)
56
+ except Exception as e:
57
+ traceback.print_tb(e.__traceback__)
58
+ return constants.RUN_FAILED, str(e)
59
+ status = self.run(job, logger)
60
+ return status
61
+
62
+ def project(self, project: Project, logger_name=None, callbacks=None):
63
+ jobs = []
64
+ for j in project.jobs:
65
+ job = self.job(j, logger_name, callbacks)
66
+ if job is None:
67
+ raise Exception("Job instantiation failed.")
68
+ else:
69
+ jobs.append(job)
70
+ return jobs
71
+
72
+ def filter_dict_keys(self, dict, prefix):
73
+ dict_with = {k.replace(prefix, ''): v for (k, v) in dict.items() if k.startswith(prefix)}
74
+ dict_without = {k: v for (k, v) in dict.items() if not k.startswith(prefix)}
75
+ return dict_with, dict_without
76
+
77
+ def action(self, action_config):
78
+ if action_config.type_name == constants.ACTION_NOISEDETECTION:
79
+ return NoiseDetection(**action_config.params)
80
+ elif action_config.type_name == constants.ACTION_COMBO:
81
+ sub_actions = []
82
+ for sa in action_config.sub_actions:
83
+ a = self.action(sa)
84
+ if a is not None:
85
+ sub_actions.append(a)
86
+ a = CombinedActions(**action_config.params, actions=sub_actions)
87
+ return a
88
+ elif action_config.type_name == constants.ACTION_MASKNOISE:
89
+ params = {k: v for k, v in action_config.params.items() if k != 'name'}
90
+ return MaskNoise(**params)
91
+ elif action_config.type_name == constants.ACTION_VIGNETTING:
92
+ params = {k: v for k, v in action_config.params.items() if k != 'name'}
93
+ return Vignetting(**params)
94
+ elif action_config.type_name == constants.ACTION_ALIGNFRAMES:
95
+ params = {k: v for k, v in action_config.params.items() if k != 'name'}
96
+ return AlignFrames(**params)
97
+ elif action_config.type_name == constants.ACTION_BALANCEFRAMES:
98
+ params = {k: v for k, v in action_config.params.items() if k != 'name'}
99
+ if 'intensity_interval' in params.keys():
100
+ i = params['intensity_interval']
101
+ params['intensity_interval'] = {'min': i[0], 'max': i[1]}
102
+ return BalanceFrames(**params)
103
+ elif action_config.type_name == constants.ACTION_FOCUSSTACK or action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
104
+ stacker = action_config.params.get('stacker', constants.STACK_ALGO_DEFAULT)
105
+ if stacker == constants.STACK_ALGO_PYRAMID:
106
+ algo_dict, module_dict = self.filter_dict_keys(action_config.params, 'pyramid_')
107
+ stack_algo = PyramidStack(**algo_dict)
108
+ elif stacker == constants.STACK_ALGO_DEPTH_MAP:
109
+ algo_dict, module_dict = self.filter_dict_keys(action_config.params, 'depthmap_')
110
+ stack_algo = DepthMapStack(**algo_dict)
111
+ else:
112
+ raise InvalidOptionError('stacker', stacker, f"valid options are: "
113
+ f"{constants.STACK_ALGO_PYRAMID}, "
114
+ f"{constants.STACK_ALGO_PYRAMID_BLOCK}, "
115
+ f"{constants.STACK_ALGO_DEPTH_MAP}")
116
+ if action_config.type_name == constants.ACTION_FOCUSSTACK:
117
+ return FocusStack(**module_dict, stack_algo=stack_algo)
118
+ elif action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
119
+ return FocusStackBunch(**module_dict, stack_algo=stack_algo)
120
+ else:
121
+ raise InvalidOptionError("stracker", stacker, details="valid values are: Pyramid, Depth map.")
122
+ elif action_config.type_name == constants.ACTION_MULTILAYER:
123
+ input_path = list(filter(lambda p: p != '', action_config.params.get('input_path', '').split(";")))
124
+ params = {k: v for k, v in action_config.params.items() if k != 'imput_path'}
125
+ params['input_path'] = [i.strip() for i in input_path]
126
+ return MultiLayer(**params)
127
+ else:
128
+ raise Exception(f"Cannot convert action of type {action_config.type_name}.")
129
+
130
+ def job(self, action_config: ActionConfig, logger_name=None, callbacks=None):
131
+ try:
132
+ name = action_config.params.get('name', '')
133
+ enabled = action_config.params.get('enabled', True)
134
+ working_path = action_config.params.get('working_path', '')
135
+ input_path = action_config.params.get('input_path', '')
136
+ stack_job = StackJob(name, working_path, enabled=enabled, input_path=input_path,
137
+ logger_name=logger_name, callbacks=callbacks)
138
+ for sub in action_config.sub_actions:
139
+ action = self.action(sub)
140
+ if action is not None:
141
+ stack_job.add_action(action)
142
+ return stack_job
143
+ except Exception as e:
144
+ msg = str(e)
145
+ logger = self.get_logger(logger_name)
146
+ logger.error(f"=== can't instantiate job: {name}: {msg} ===")
147
+ traceback.print_tb(e.__traceback__)
148
+ raise e