shinestacker 0.3.5__py3-none-any.whl → 0.4.0__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 +37 -20
- shinestacker/algorithms/balance.py +2 -1
- shinestacker/algorithms/base_stack_algo.py +2 -1
- shinestacker/algorithms/multilayer.py +11 -8
- shinestacker/algorithms/noise_detection.py +13 -7
- shinestacker/algorithms/pyramid.py +7 -4
- shinestacker/algorithms/stack.py +5 -4
- shinestacker/algorithms/stack_framework.py +12 -10
- shinestacker/app/app_config.py +4 -22
- shinestacker/app/main.py +1 -1
- shinestacker/config/config.py +22 -16
- shinestacker/config/constants.py +8 -1
- shinestacker/core/framework.py +15 -10
- shinestacker/gui/action_config.py +20 -41
- shinestacker/gui/actions_window.py +18 -47
- shinestacker/gui/gui_logging.py +8 -7
- shinestacker/gui/gui_run.py +8 -8
- shinestacker/gui/main_window.py +4 -4
- shinestacker/gui/new_project.py +34 -37
- shinestacker/gui/project_converter.py +0 -1
- shinestacker/gui/project_editor.py +43 -20
- shinestacker/gui/select_path_widget.py +32 -0
- shinestacker/retouch/base_filter.py +12 -1
- shinestacker/retouch/denoise_filter.py +4 -10
- shinestacker/retouch/exif_data.py +3 -9
- shinestacker/retouch/icon_container.py +19 -0
- shinestacker/retouch/image_editor.py +1 -1
- shinestacker/retouch/image_editor_ui.py +2 -1
- shinestacker/retouch/image_viewer.py +104 -20
- shinestacker/retouch/io_gui_handler.py +17 -16
- shinestacker/retouch/io_manager.py +0 -1
- shinestacker/retouch/layer_collection.py +2 -1
- shinestacker/retouch/shortcuts_help.py +2 -13
- shinestacker/retouch/unsharp_mask_filter.py +3 -10
- shinestacker/retouch/white_balance_filter.py +5 -13
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/METADATA +8 -11
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/RECORD +42 -41
- shinestacker-0.4.0.dist-info/licenses/LICENSE +165 -0
- shinestacker/algorithms/core_utils.py +0 -22
- shinestacker-0.3.5.dist-info/licenses/LICENSE +0 -1
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -10,8 +10,10 @@ from PySide6.QtWidgets import (QWidget, QPushButton, QHBoxLayout, QFileDialog, Q
|
|
|
10
10
|
QAbstractItemView, QListView)
|
|
11
11
|
from PySide6.QtCore import Qt, QTimer
|
|
12
12
|
from .. config.constants import constants
|
|
13
|
-
from .project_model import ActionConfig
|
|
14
13
|
from .. algorithms.align import validate_align_config
|
|
14
|
+
from .project_model import ActionConfig
|
|
15
|
+
from .select_path_widget import (create_select_file_paths_widget, create_layout_widget_no_margins,
|
|
16
|
+
create_layout_widget_and_connect)
|
|
15
17
|
|
|
16
18
|
FIELD_TEXT = 'text'
|
|
17
19
|
FIELD_ABS_PATH = 'abs_path'
|
|
@@ -200,25 +202,11 @@ class FieldBuilder:
|
|
|
200
202
|
return edit
|
|
201
203
|
|
|
202
204
|
def create_abs_path_field(self, tag, **kwargs):
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def browse():
|
|
209
|
-
path = QFileDialog.getExistingDirectory(None, f"Select {tag.replace('_', ' ')}")
|
|
210
|
-
if path:
|
|
211
|
-
edit.setText(path)
|
|
212
|
-
button.clicked.connect(browse)
|
|
213
|
-
button.setAutoDefault(False)
|
|
214
|
-
layout = QHBoxLayout()
|
|
215
|
-
layout.addWidget(edit)
|
|
216
|
-
layout.addWidget(button)
|
|
217
|
-
layout.setContentsMargins(0, 0, 0, 0)
|
|
218
|
-
container = QWidget()
|
|
219
|
-
container.setLayout(layout)
|
|
220
|
-
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
221
|
-
return container
|
|
205
|
+
return create_select_file_paths_widget(
|
|
206
|
+
self.action.params.get(tag, ''),
|
|
207
|
+
kwargs.get('placeholder', ''),
|
|
208
|
+
tag.replace('_', ' ')
|
|
209
|
+
)[1]
|
|
222
210
|
|
|
223
211
|
def create_rel_path_field(self, tag, **kwargs):
|
|
224
212
|
value = self.action.params.get(tag, kwargs.get('default', ''))
|
|
@@ -326,17 +314,7 @@ class FieldBuilder:
|
|
|
326
314
|
except ValueError as e:
|
|
327
315
|
traceback.print_tb(e.__traceback__)
|
|
328
316
|
QMessageBox.warning(None, "Error", "Could not compute relative path")
|
|
329
|
-
|
|
330
|
-
button.clicked.connect(browse)
|
|
331
|
-
button.setAutoDefault(False)
|
|
332
|
-
layout = QHBoxLayout()
|
|
333
|
-
layout.addWidget(edit)
|
|
334
|
-
layout.addWidget(button)
|
|
335
|
-
layout.setContentsMargins(0, 0, 0, 0)
|
|
336
|
-
container = QWidget()
|
|
337
|
-
container.setLayout(layout)
|
|
338
|
-
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
339
|
-
return container
|
|
317
|
+
return create_layout_widget_and_connect(button, edit, browse)
|
|
340
318
|
|
|
341
319
|
def create_float_field(self, tag, default=0.0, min_val=0.0, max_val=1.0,
|
|
342
320
|
step=0.1, decimals=2):
|
|
@@ -369,11 +347,7 @@ class FieldBuilder:
|
|
|
369
347
|
layout.addWidget(label)
|
|
370
348
|
layout.addWidget(spin)
|
|
371
349
|
layout.setStretch(layout.count() - 1, 1)
|
|
372
|
-
layout
|
|
373
|
-
container = QWidget()
|
|
374
|
-
container.setLayout(layout)
|
|
375
|
-
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
376
|
-
return container
|
|
350
|
+
return create_layout_widget_no_margins(layout)
|
|
377
351
|
|
|
378
352
|
def create_combo_field(self, tag, options=None, default=None, **kwargs):
|
|
379
353
|
options = options or []
|
|
@@ -618,7 +592,8 @@ class FocusStackConfigurator(FocusStackBaseConfigurator):
|
|
|
618
592
|
self.builder.add_field('exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
|
|
619
593
|
placeholder='relative to working path')
|
|
620
594
|
self.builder.add_field('prefix', FIELD_TEXT, 'Ouptut filename prefix', required=False,
|
|
621
|
-
default=constants.DEFAULT_STACK_PREFIX,
|
|
595
|
+
default=constants.DEFAULT_STACK_PREFIX,
|
|
596
|
+
placeholder=constants.DEFAULT_STACK_PREFIX)
|
|
622
597
|
self.builder.add_field('plot_stack', FIELD_BOOL, 'Plot stack', required=False,
|
|
623
598
|
default=constants.DEFAULT_PLOT_STACK)
|
|
624
599
|
super().common_fields(layout)
|
|
@@ -641,10 +616,11 @@ class MultiLayerConfigurator(DefaultActionConfigurator):
|
|
|
641
616
|
super().create_form(layout, action)
|
|
642
617
|
if self.expert:
|
|
643
618
|
self.builder.add_field('working_path', FIELD_ABS_PATH, 'Working path', required=False)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
619
|
+
self.builder.add_field('input_path', FIELD_REL_PATH,
|
|
620
|
+
f'Input path (separate by {constants.PATH_SEPARATOR})',
|
|
621
|
+
required=False, multiple_entries=True,
|
|
622
|
+
placeholder='relative to working path')
|
|
623
|
+
if self.expert:
|
|
648
624
|
self.builder.add_field('output_path', FIELD_REL_PATH, 'Output path', required=False,
|
|
649
625
|
placeholder='relative to working path')
|
|
650
626
|
self.builder.add_field('exif_path', FIELD_REL_PATH, 'Exif data path', required=False,
|
|
@@ -830,6 +806,9 @@ class AlignFramesConfigurator(DefaultActionConfigurator):
|
|
|
830
806
|
rans_threshold = self.builder.add_field(
|
|
831
807
|
'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
|
|
832
808
|
default=constants.DEFAULT_RANS_THRESHOLD, min_val=0, max_val=20, step=0.1)
|
|
809
|
+
self.builder.add_field(
|
|
810
|
+
'min_good_matches', FIELD_INT, "Min. good matches", required=False,
|
|
811
|
+
default=constants.DEFAULT_ALIGN_MIN_GOOD_MATCHES, min_val=0, max_val=500)
|
|
833
812
|
|
|
834
813
|
def change_method():
|
|
835
814
|
text = method.currentText()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0914, R0912, R0915, W0718
|
|
2
2
|
import os.path
|
|
3
3
|
import os
|
|
4
|
-
import traceback
|
|
4
|
+
# import traceback
|
|
5
5
|
import json
|
|
6
6
|
import jsonpickle
|
|
7
7
|
from PySide6.QtWidgets import QMessageBox, QFileDialog, QDialog
|
|
@@ -20,8 +20,9 @@ class ActionsWindow(ProjectEditor):
|
|
|
20
20
|
|
|
21
21
|
def update_title(self):
|
|
22
22
|
title = constants.APP_TITLE
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
file_name = self.current_file_name()
|
|
24
|
+
if file_name:
|
|
25
|
+
title += f" - {file_name}"
|
|
25
26
|
if self._modified_project:
|
|
26
27
|
title += " *"
|
|
27
28
|
self.window().setWindowTitle(title)
|
|
@@ -34,7 +35,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
34
35
|
def close_project(self):
|
|
35
36
|
if self._check_unsaved_changes():
|
|
36
37
|
self.set_project(Project())
|
|
37
|
-
self.
|
|
38
|
+
self.set_current_file_path('')
|
|
38
39
|
self.update_title()
|
|
39
40
|
self.job_list.clear()
|
|
40
41
|
self.action_list.clear()
|
|
@@ -44,17 +45,17 @@ class ActionsWindow(ProjectEditor):
|
|
|
44
45
|
if not self._check_unsaved_changes():
|
|
45
46
|
return
|
|
46
47
|
os.chdir(get_app_base_path())
|
|
47
|
-
self.
|
|
48
|
+
self.set_current_file_path('')
|
|
48
49
|
self._modified_project = False
|
|
49
50
|
self.update_title()
|
|
50
51
|
self.job_list.clear()
|
|
51
52
|
self.action_list.clear()
|
|
53
|
+
self.set_project(Project())
|
|
52
54
|
dialog = NewProjectDialog(self)
|
|
53
55
|
if dialog.exec() == QDialog.Accepted:
|
|
54
56
|
input_folder = dialog.get_input_folder().split('/')
|
|
55
57
|
working_path = '/'.join(input_folder[:-1])
|
|
56
58
|
input_path = input_folder[-1]
|
|
57
|
-
project = Project()
|
|
58
59
|
if dialog.get_noise_detection():
|
|
59
60
|
job_noise = ActionConfig(constants.ACTION_JOB,
|
|
60
61
|
{'name': 'detect-noise', 'working_path': working_path,
|
|
@@ -62,7 +63,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
62
63
|
noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
|
|
63
64
|
{'name': 'detect-noise'})
|
|
64
65
|
job_noise.add_sub_action(noise_detection)
|
|
65
|
-
project.jobs.append(job_noise)
|
|
66
|
+
self.project.jobs.append(job_noise)
|
|
66
67
|
job = ActionConfig(constants.ACTION_JOB,
|
|
67
68
|
{'name': 'focus-stack', 'working_path': working_path,
|
|
68
69
|
'input_path': input_path})
|
|
@@ -111,8 +112,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
111
112
|
{'name': 'multi-layer',
|
|
112
113
|
'input_path': ','.join(input_path)})
|
|
113
114
|
job.add_sub_action(multi_layer)
|
|
114
|
-
project.jobs.append(job)
|
|
115
|
-
self.set_project(project)
|
|
115
|
+
self.project.jobs.append(job)
|
|
116
116
|
self._modified_project = True
|
|
117
117
|
self.refresh_ui(0, -1)
|
|
118
118
|
|
|
@@ -124,17 +124,9 @@ class ActionsWindow(ProjectEditor):
|
|
|
124
124
|
self, "Open Project", "", "Project Files (*.fsp);;All Files (*)")
|
|
125
125
|
if file_path:
|
|
126
126
|
try:
|
|
127
|
-
self.
|
|
128
|
-
self.
|
|
129
|
-
else os.path.dirname(file_path)
|
|
130
|
-
if not os.path.isabs(self._current_file_wd):
|
|
131
|
-
self._current_file_wd = os.path.abspath(self._current_file_wd)
|
|
132
|
-
self._current_file = os.path.basename(self._current_file)
|
|
133
|
-
with open(file_path, 'r', encoding="utf-8") as file:
|
|
127
|
+
self.set_current_file_path(file_path)
|
|
128
|
+
with open(self.current_file_path(), 'r', encoding="utf-8") as file:
|
|
134
129
|
json_obj = json.load(file)
|
|
135
|
-
pp = file_path.split('/')
|
|
136
|
-
if len(pp) > 1:
|
|
137
|
-
os.chdir('/'.join(pp[:-1]))
|
|
138
130
|
project = Project.from_dict(json_obj['project'])
|
|
139
131
|
if project is None:
|
|
140
132
|
raise RuntimeError(f"Project from file {file_path} produced a null project.")
|
|
@@ -145,7 +137,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
145
137
|
if self.job_list.count() > 0:
|
|
146
138
|
self.job_list.setCurrentRow(0)
|
|
147
139
|
except Exception as e:
|
|
148
|
-
traceback.print_tb(e.__traceback__)
|
|
140
|
+
# traceback.print_tb(e.__traceback__)
|
|
149
141
|
QMessageBox.critical(self, "Error", f"Cannot open file {file_path}:\n{str(e)}")
|
|
150
142
|
if len(self.project.jobs) > 0:
|
|
151
143
|
self.job_list.setCurrentRow(0)
|
|
@@ -173,12 +165,10 @@ class ActionsWindow(ProjectEditor):
|
|
|
173
165
|
Please, select a valid working path.''')
|
|
174
166
|
self.edit_action(action)
|
|
175
167
|
|
|
176
|
-
def current_file_name(self):
|
|
177
|
-
return os.path.basename(self._current_file) if self._current_file else ''
|
|
178
|
-
|
|
179
168
|
def save_project(self):
|
|
180
|
-
|
|
181
|
-
|
|
169
|
+
path = self.current_file_path()
|
|
170
|
+
if path:
|
|
171
|
+
self.do_save(path)
|
|
182
172
|
else:
|
|
183
173
|
self.save_project_as()
|
|
184
174
|
|
|
@@ -188,9 +178,8 @@ class ActionsWindow(ProjectEditor):
|
|
|
188
178
|
if file_path:
|
|
189
179
|
if not file_path.endswith('.fsp'):
|
|
190
180
|
file_path += '.fsp'
|
|
191
|
-
self._current_file_wd = ''
|
|
192
181
|
self.do_save(file_path)
|
|
193
|
-
self.
|
|
182
|
+
self.set_current_file_path(file_path)
|
|
194
183
|
self._modified_project = False
|
|
195
184
|
self.update_title()
|
|
196
185
|
os.chdir(os.path.dirname(file_path))
|
|
@@ -201,9 +190,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
201
190
|
'project': self.project.to_dict(),
|
|
202
191
|
'version': 1
|
|
203
192
|
})
|
|
204
|
-
|
|
205
|
-
if self._current_file_wd != '' else file_path
|
|
206
|
-
with open(path, 'w', encoding="utf-8") as f:
|
|
193
|
+
with open(file_path, 'w', encoding="utf-8") as f:
|
|
207
194
|
f.write(json_obj)
|
|
208
195
|
self._modified_project = False
|
|
209
196
|
except Exception as e:
|
|
@@ -238,23 +225,7 @@ class ActionsWindow(ProjectEditor):
|
|
|
238
225
|
if 0 <= job_index < len(self.project.jobs):
|
|
239
226
|
job = self.project.jobs[job_index]
|
|
240
227
|
action_index = self.action_list.row(item)
|
|
241
|
-
|
|
242
|
-
current_action = None
|
|
243
|
-
is_sub_action = False
|
|
244
|
-
for action in job.sub_actions:
|
|
245
|
-
action_counter += 1
|
|
246
|
-
if action_counter == action_index:
|
|
247
|
-
current_action = action
|
|
248
|
-
break
|
|
249
|
-
if len(action.type_name) > 0:
|
|
250
|
-
for sub_action in action.sub_actions:
|
|
251
|
-
action_counter += 1
|
|
252
|
-
if action_counter == action_index:
|
|
253
|
-
current_action = sub_action
|
|
254
|
-
is_sub_action = True
|
|
255
|
-
break
|
|
256
|
-
if current_action:
|
|
257
|
-
break
|
|
228
|
+
current_action, is_sub_action = self.get_current_action_at(job, action_index)
|
|
258
229
|
if current_action:
|
|
259
230
|
if not is_sub_action:
|
|
260
231
|
self.set_enabled_sub_actions_gui(
|
shinestacker/gui/gui_logging.py
CHANGED
|
@@ -17,6 +17,7 @@ class SimpleHtmlFormatter(logging.Formatter):
|
|
|
17
17
|
FF = '80'
|
|
18
18
|
OO = '00'
|
|
19
19
|
MM = '40'
|
|
20
|
+
GG = 'FF'
|
|
20
21
|
ANSI_COLORS = {
|
|
21
22
|
# Reset
|
|
22
23
|
'\x1b[0m': '</span>',
|
|
@@ -32,13 +33,13 @@ class SimpleHtmlFormatter(logging.Formatter):
|
|
|
32
33
|
'\x1b[37m': f'<span style="color:#{FF}{FF}{FF}">', # white
|
|
33
34
|
# Brilliant colors (90-97)
|
|
34
35
|
'\x1b[90m': f'<span style="color:#{MM}{MM}{MM}">',
|
|
35
|
-
'\x1b[91m': f'<span style="color:#{
|
|
36
|
-
'\x1b[92m': f'<span style="color:#{MM}{
|
|
37
|
-
'\x1b[93m': f'<span style="color:#{
|
|
38
|
-
'\x1b[94m': f'<span style="color:#{MM}{MM}{
|
|
39
|
-
'\x1b[95m': f'<span style="color:#{
|
|
40
|
-
'\x1b[96m': f'<span style="color:#{MM}{
|
|
41
|
-
'\x1b[97m': f'<span style="color:#{
|
|
36
|
+
'\x1b[91m': f'<span style="color:#{GG}{MM}{MM}">',
|
|
37
|
+
'\x1b[92m': f'<span style="color:#{MM}{GG}{MM}">',
|
|
38
|
+
'\x1b[93m': f'<span style="color:#{GG}{GG}{MM}">',
|
|
39
|
+
'\x1b[94m': f'<span style="color:#{MM}{MM}{GG}">',
|
|
40
|
+
'\x1b[95m': f'<span style="color:#{GG}{MM}{GG}">',
|
|
41
|
+
'\x1b[96m': f'<span style="color:#{MM}{GG}{GG}">',
|
|
42
|
+
'\x1b[97m': f'<span style="color:#{GG}{GG}{GG}">',
|
|
42
43
|
# Background (40-47)
|
|
43
44
|
'\x1b[40m': f'<span style="background-color:#{OO}{OO}{OO}">',
|
|
44
45
|
'\x1b[41m': f'<span style="background-color:#{FF}{OO}{OO}">',
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -13,6 +13,10 @@ from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
|
|
|
13
13
|
from .colors import ColorPalette
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
ACTION_RUNNING_COLOR = ColorPalette.MEDIUM_BLUE
|
|
17
|
+
ACTION_DONE_COLOR = ColorPalette.MEDIUM_GREEN
|
|
18
|
+
|
|
19
|
+
|
|
16
20
|
class ColorButton(QPushButton):
|
|
17
21
|
def __init__(self, text, enabled, parent=None):
|
|
18
22
|
super().__init__(text.replace(gui_constants.DISABLED_TAG, ''), parent)
|
|
@@ -36,10 +40,6 @@ class ColorButton(QPushButton):
|
|
|
36
40
|
""")
|
|
37
41
|
|
|
38
42
|
|
|
39
|
-
action_running_color = ColorPalette.MEDIUM_BLUE
|
|
40
|
-
action_done_color = ColorPalette.MEDIUM_GREEN
|
|
41
|
-
|
|
42
|
-
|
|
43
43
|
class TimerProgressBar(QProgressBar):
|
|
44
44
|
light_background_color = ColorPalette.LIGHT_BLUE
|
|
45
45
|
border_color = ColorPalette.DARK_BLUE
|
|
@@ -123,10 +123,10 @@ class TimerProgressBar(QProgressBar):
|
|
|
123
123
|
# pylint: enable=C0103
|
|
124
124
|
|
|
125
125
|
def set_running_style(self):
|
|
126
|
-
self.set_style(
|
|
126
|
+
self.set_style(ACTION_RUNNING_COLOR)
|
|
127
127
|
|
|
128
128
|
def set_done_style(self):
|
|
129
|
-
self.set_style(
|
|
129
|
+
self.set_style(ACTION_DONE_COLOR)
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
class RunWindow(QTextEditLogger):
|
|
@@ -246,7 +246,7 @@ class RunWindow(QTextEditLogger):
|
|
|
246
246
|
@Slot(int, str)
|
|
247
247
|
def handle_before_action(self, run_id, _name):
|
|
248
248
|
if 0 <= run_id < len(self.color_widgets[self.row_widget_id]):
|
|
249
|
-
self.color_widgets[self.row_widget_id][run_id].set_color(*
|
|
249
|
+
self.color_widgets[self.row_widget_id][run_id].set_color(*ACTION_RUNNING_COLOR.tuple())
|
|
250
250
|
self.progress_bar.start(1)
|
|
251
251
|
if run_id == -1:
|
|
252
252
|
self.progress_bar.set_running_style()
|
|
@@ -254,7 +254,7 @@ class RunWindow(QTextEditLogger):
|
|
|
254
254
|
@Slot(int, str)
|
|
255
255
|
def handle_after_action(self, run_id, _name):
|
|
256
256
|
if 0 <= run_id < len(self.color_widgets[self.row_widget_id]):
|
|
257
|
-
self.color_widgets[self.row_widget_id][run_id].set_color(*
|
|
257
|
+
self.color_widgets[self.row_widget_id][run_id].set_color(*ACTION_DONE_COLOR.tuple())
|
|
258
258
|
self.progress_bar.stop()
|
|
259
259
|
if run_id == -1:
|
|
260
260
|
self.row_widget_id += 1
|
shinestacker/gui/main_window.py
CHANGED
|
@@ -163,11 +163,11 @@ class MainWindow(ActionsWindow, LogManager):
|
|
|
163
163
|
self.action_list.itemDoubleClicked.connect(self.on_action_edit)
|
|
164
164
|
vbox_left = QVBoxLayout()
|
|
165
165
|
vbox_left.setSpacing(4)
|
|
166
|
-
vbox_left.addWidget(QLabel("
|
|
166
|
+
vbox_left.addWidget(QLabel("Job"))
|
|
167
167
|
vbox_left.addWidget(self.job_list)
|
|
168
168
|
vbox_right = QVBoxLayout()
|
|
169
169
|
vbox_right.setSpacing(4)
|
|
170
|
-
vbox_right.addWidget(QLabel("
|
|
170
|
+
vbox_right.addWidget(QLabel("Action"))
|
|
171
171
|
vbox_right.addWidget(self.action_list)
|
|
172
172
|
self.job_list.itemSelectionChanged.connect(self.update_delete_action_state)
|
|
173
173
|
self.action_list.itemSelectionChanged.connect(self.update_delete_action_state)
|
|
@@ -615,7 +615,7 @@ class MainWindow(ActionsWindow, LogManager):
|
|
|
615
615
|
labels = [[(self.action_text(a), a.enabled()) for a in job.sub_actions]]
|
|
616
616
|
r = self.get_retouch_path(job)
|
|
617
617
|
retouch_paths = [] if len(r) == 0 else [(job_name, r)]
|
|
618
|
-
new_window, id_str = self.create_new_window("Job
|
|
618
|
+
new_window, id_str = self.create_new_window(f"{job_name} [⚙️ Job]",
|
|
619
619
|
labels, retouch_paths)
|
|
620
620
|
worker = JobLogWorker(job, id_str)
|
|
621
621
|
self.connect_signals(worker, new_window)
|
|
@@ -638,7 +638,7 @@ class MainWindow(ActionsWindow, LogManager):
|
|
|
638
638
|
r = self.get_retouch_path(job)
|
|
639
639
|
if len(r) > 0:
|
|
640
640
|
retouch_paths.append((job.params["name"], r))
|
|
641
|
-
new_window, id_str = self.create_new_window("Project
|
|
641
|
+
new_window, id_str = self.create_new_window(f"{project_name} [Project 📚]",
|
|
642
642
|
labels, retouch_paths)
|
|
643
643
|
worker = ProjectLogWorker(self.project, id_str)
|
|
644
644
|
self.connect_signals(worker, new_window)
|
shinestacker/gui/new_project.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902
|
|
2
2
|
import os
|
|
3
|
-
from PySide6.QtWidgets import (
|
|
4
|
-
QDialog,
|
|
5
|
-
QSpinBox, QMessageBox)
|
|
3
|
+
from PySide6.QtWidgets import (QFormLayout, QHBoxLayout, QPushButton,
|
|
4
|
+
QDialog, QLabel, QCheckBox, QSpinBox, QMessageBox)
|
|
6
5
|
from PySide6.QtGui import QIcon
|
|
7
6
|
from PySide6.QtCore import Qt
|
|
8
7
|
from .. config.gui_constants import gui_constants
|
|
9
8
|
from .. config.constants import constants
|
|
10
9
|
from .. algorithms.stack import get_bunches
|
|
10
|
+
from .select_path_widget import create_select_file_paths_widget
|
|
11
|
+
|
|
12
|
+
DEFAULT_NO_COUNT_LABEL = " - "
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class NewProjectDialog(QDialog):
|
|
@@ -50,26 +52,10 @@ class NewProjectDialog(QDialog):
|
|
|
50
52
|
spacer = QLabel("")
|
|
51
53
|
spacer.setFixedHeight(10)
|
|
52
54
|
self.layout.addRow(spacer)
|
|
53
|
-
self.input_folder = QLineEdit()
|
|
54
|
-
self.input_folder .setPlaceholderText('input files folder')
|
|
55
|
-
self.input_folder.textChanged.connect(self.update_bunches_label)
|
|
56
|
-
button = QPushButton("Browse...")
|
|
57
|
-
|
|
58
|
-
def browse():
|
|
59
|
-
path = QFileDialog.getExistingDirectory(None, "Select input files folder")
|
|
60
|
-
if path:
|
|
61
|
-
self.input_folder.setText(path)
|
|
62
|
-
|
|
63
|
-
button.clicked.connect(browse)
|
|
64
|
-
button.setAutoDefault(False)
|
|
65
|
-
layout = QHBoxLayout()
|
|
66
|
-
layout.addWidget(self.input_folder)
|
|
67
|
-
layout.addWidget(button)
|
|
68
|
-
layout.setContentsMargins(0, 0, 0, 0)
|
|
69
|
-
container = QWidget()
|
|
70
|
-
container.setLayout(layout)
|
|
71
|
-
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
72
55
|
|
|
56
|
+
self.input_folder, container = create_select_file_paths_widget(
|
|
57
|
+
'', 'input files folder', 'input files folder')
|
|
58
|
+
self.input_folder.textChanged.connect(self.update_bunches_label)
|
|
73
59
|
self.noise_detection = QCheckBox()
|
|
74
60
|
self.noise_detection.setChecked(gui_constants.NEW_PROJECT_NOISE_DETECTION)
|
|
75
61
|
self.vignetting_correction = QCheckBox()
|
|
@@ -89,7 +75,8 @@ class NewProjectDialog(QDialog):
|
|
|
89
75
|
bunch_overlap_range = gui_constants.NEW_PROJECT_BUNCH_OVERLAP
|
|
90
76
|
self.bunch_overlap.setRange(bunch_overlap_range['min'], bunch_overlap_range['max'])
|
|
91
77
|
self.bunch_overlap.setValue(constants.DEFAULT_OVERLAP)
|
|
92
|
-
self.bunches_label = QLabel(
|
|
78
|
+
self.bunches_label = QLabel(DEFAULT_NO_COUNT_LABEL)
|
|
79
|
+
self.frames_label = QLabel(DEFAULT_NO_COUNT_LABEL)
|
|
93
80
|
|
|
94
81
|
self.update_bunch_options(gui_constants.NEW_PROJECT_BUNCH_STACK)
|
|
95
82
|
self.bunch_stack.toggled.connect(self.update_bunch_options)
|
|
@@ -105,6 +92,7 @@ class NewProjectDialog(QDialog):
|
|
|
105
92
|
|
|
106
93
|
self.add_bold_label("Select input:")
|
|
107
94
|
self.layout.addRow("Input folder:", container)
|
|
95
|
+
self.layout.addRow("Number of frames: ", self.frames_label)
|
|
108
96
|
self.add_bold_label("Select actions:")
|
|
109
97
|
if self.expert():
|
|
110
98
|
self.layout.addRow("Automatic noise detection:", self.noise_detection)
|
|
@@ -128,25 +116,34 @@ class NewProjectDialog(QDialog):
|
|
|
128
116
|
self.update_bunches_label()
|
|
129
117
|
|
|
130
118
|
def update_bunches_label(self):
|
|
119
|
+
if not self.input_folder.text():
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
def count_image_files(path):
|
|
123
|
+
if path == '' or not os.path.isdir(path):
|
|
124
|
+
return 0
|
|
125
|
+
extensions = ['jpg', 'jpeg', 'tif', 'tiff']
|
|
126
|
+
count = 0
|
|
127
|
+
for filename in os.listdir(path):
|
|
128
|
+
if '.' in filename:
|
|
129
|
+
ext = filename.lower().split('.')[-1]
|
|
130
|
+
if ext in extensions:
|
|
131
|
+
count += 1
|
|
132
|
+
return count
|
|
133
|
+
|
|
134
|
+
n_image_files = count_image_files(self.input_folder.text())
|
|
135
|
+
if n_image_files == 0:
|
|
136
|
+
self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
|
|
137
|
+
self.frames_label.setText(DEFAULT_NO_COUNT_LABEL)
|
|
138
|
+
return
|
|
139
|
+
self.frames_label.setText(f"{n_image_files}")
|
|
131
140
|
if self.bunch_stack.isChecked():
|
|
132
|
-
|
|
133
|
-
if path == '' or not os.path.isdir(path):
|
|
134
|
-
return 0
|
|
135
|
-
extensions = ['jpg', 'jpeg', 'tif', 'tiff']
|
|
136
|
-
count = 0
|
|
137
|
-
for filename in os.listdir(path):
|
|
138
|
-
if '.' in filename:
|
|
139
|
-
ext = filename.lower().split('.')[-1]
|
|
140
|
-
if ext in extensions:
|
|
141
|
-
count += 1
|
|
142
|
-
return count
|
|
143
|
-
|
|
144
|
-
bunches = get_bunches(list(range(count_image_files(self.input_folder.text()))),
|
|
141
|
+
bunches = get_bunches(list(range(n_image_files)),
|
|
145
142
|
self.bunch_frames.value(),
|
|
146
143
|
self.bunch_overlap.value())
|
|
147
144
|
self.bunches_label.setText(f"{len(bunches)}")
|
|
148
145
|
else:
|
|
149
|
-
self.bunches_label.setText(
|
|
146
|
+
self.bunches_label.setText(DEFAULT_NO_COUNT_LABEL)
|
|
150
147
|
|
|
151
148
|
def accept(self):
|
|
152
149
|
input_folder = self.input_folder.text()
|
|
@@ -112,7 +112,6 @@ class ProjectConverter:
|
|
|
112
112
|
else:
|
|
113
113
|
raise InvalidOptionError('stacker', stacker, f"valid options are: "
|
|
114
114
|
f"{constants.STACK_ALGO_PYRAMID}, "
|
|
115
|
-
f"{constants.STACK_ALGO_PYRAMID_BLOCK}, "
|
|
116
115
|
f"{constants.STACK_ALGO_DEPTH_MAP}")
|
|
117
116
|
if action_config.type_name == constants.ACTION_FOCUSSTACK:
|
|
118
117
|
return FocusStack(**module_dict, stack_algo=stack_algo)
|
|
@@ -89,10 +89,28 @@ class ProjectEditor(QMainWindow):
|
|
|
89
89
|
self.expert_options = False
|
|
90
90
|
self.script_dir = os.path.dirname(__file__)
|
|
91
91
|
self.dialog = None
|
|
92
|
-
self.
|
|
93
|
-
self._current_file_wd = ''
|
|
92
|
+
self._current_file_path = ''
|
|
94
93
|
self._modified_project = False
|
|
95
94
|
|
|
95
|
+
def current_file_path(self):
|
|
96
|
+
return self._current_file_path
|
|
97
|
+
|
|
98
|
+
def current_file_directory(self):
|
|
99
|
+
if os.path.isdir(self._current_file_path):
|
|
100
|
+
return self._current_file_path
|
|
101
|
+
return os.path.dirname(self._current_file_path)
|
|
102
|
+
|
|
103
|
+
def current_file_name(self):
|
|
104
|
+
if os.path.isfile(self._current_file_path):
|
|
105
|
+
return os.path.basename(self._current_file_path)
|
|
106
|
+
return ''
|
|
107
|
+
|
|
108
|
+
def set_current_file_path(self, path):
|
|
109
|
+
if path and not os.path.exists(path):
|
|
110
|
+
raise RuntimeError(f"Path: {path} does not exist.")
|
|
111
|
+
self._current_file_path = os.path.abspath(path)
|
|
112
|
+
os.chdir(self.current_file_directory())
|
|
113
|
+
|
|
96
114
|
def set_project(self, project):
|
|
97
115
|
self.project = project
|
|
98
116
|
|
|
@@ -304,7 +322,7 @@ class ProjectEditor(QMainWindow):
|
|
|
304
322
|
return element
|
|
305
323
|
|
|
306
324
|
def action_config_dialog(self, action):
|
|
307
|
-
return ActionConfigDialog(action, self.
|
|
325
|
+
return ActionConfigDialog(action, self.current_file_directory(), self)
|
|
308
326
|
|
|
309
327
|
def add_job(self):
|
|
310
328
|
job_action = ActionConfig("Job")
|
|
@@ -499,6 +517,27 @@ class ProjectEditor(QMainWindow):
|
|
|
499
517
|
self.add_list_item(self.action_list, sub_action, True)
|
|
500
518
|
self.update_delete_action_state()
|
|
501
519
|
|
|
520
|
+
def get_current_action_at(self, job, action_index):
|
|
521
|
+
action_counter = -1
|
|
522
|
+
current_action = None
|
|
523
|
+
is_sub_action = False
|
|
524
|
+
for action in job.sub_actions:
|
|
525
|
+
action_counter += 1
|
|
526
|
+
if action_counter == action_index:
|
|
527
|
+
current_action = action
|
|
528
|
+
break
|
|
529
|
+
if len(action.sub_actions) > 0:
|
|
530
|
+
for sub_action in action.sub_actions:
|
|
531
|
+
action_counter += 1
|
|
532
|
+
if action_counter == action_index:
|
|
533
|
+
current_action = sub_action
|
|
534
|
+
is_sub_action = True
|
|
535
|
+
break
|
|
536
|
+
if current_action:
|
|
537
|
+
break
|
|
538
|
+
|
|
539
|
+
return current_action, is_sub_action
|
|
540
|
+
|
|
502
541
|
def update_delete_action_state(self):
|
|
503
542
|
has_job_selected = len(self.job_list.selectedItems()) > 0
|
|
504
543
|
has_action_selected = len(self.action_list.selectedItems()) > 0
|
|
@@ -510,23 +549,7 @@ class ProjectEditor(QMainWindow):
|
|
|
510
549
|
action_index = self.action_list.currentRow()
|
|
511
550
|
if job_index >= 0:
|
|
512
551
|
job = self.project.jobs[job_index]
|
|
513
|
-
|
|
514
|
-
current_action = None
|
|
515
|
-
is_sub_action = False
|
|
516
|
-
for action in job.sub_actions:
|
|
517
|
-
action_counter += 1
|
|
518
|
-
if action_counter == action_index:
|
|
519
|
-
current_action = action
|
|
520
|
-
break
|
|
521
|
-
if len(action.sub_actions) > 0:
|
|
522
|
-
for sub_action in action.sub_actions:
|
|
523
|
-
action_counter += 1
|
|
524
|
-
if action_counter == action_index:
|
|
525
|
-
current_action = sub_action
|
|
526
|
-
is_sub_action = True
|
|
527
|
-
break
|
|
528
|
-
if current_action:
|
|
529
|
-
break
|
|
552
|
+
current_action, is_sub_action = self.get_current_action_at(job, action_index)
|
|
530
553
|
enable_sub_actions = current_action is not None and \
|
|
531
554
|
not is_sub_action and current_action.type_name == constants.ACTION_COMBO
|
|
532
555
|
self.set_enabled_sub_actions_gui(enable_sub_actions)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0116, E0611
|
|
2
|
+
from PySide6.QtWidgets import QWidget, QPushButton, QHBoxLayout, QFileDialog, QSizePolicy, QLineEdit
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def create_layout_widget_no_margins(layout):
|
|
6
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
7
|
+
container = QWidget()
|
|
8
|
+
container.setLayout(layout)
|
|
9
|
+
container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
10
|
+
return container
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_layout_widget_and_connect(button, edit, browse):
|
|
14
|
+
button.clicked.connect(browse)
|
|
15
|
+
button.setAutoDefault(False)
|
|
16
|
+
layout = QHBoxLayout()
|
|
17
|
+
layout.addWidget(edit)
|
|
18
|
+
layout.addWidget(button)
|
|
19
|
+
return create_layout_widget_no_margins(layout)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_select_file_paths_widget(value, placeholder, tag):
|
|
23
|
+
edit = QLineEdit(value)
|
|
24
|
+
edit.setPlaceholderText(placeholder)
|
|
25
|
+
button = QPushButton("Browse...")
|
|
26
|
+
|
|
27
|
+
def browse():
|
|
28
|
+
path = QFileDialog.getExistingDirectory(None, f"Select {tag}")
|
|
29
|
+
if path:
|
|
30
|
+
edit.setText(path)
|
|
31
|
+
|
|
32
|
+
return edit, create_layout_widget_and_connect(button, edit, browse)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import traceback
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
import numpy as np
|
|
5
|
-
from PySide6.QtWidgets import QDialog, QVBoxLayout
|
|
5
|
+
from PySide6.QtWidgets import QDialog, QVBoxLayout, QCheckBox, QDialogButtonBox
|
|
6
6
|
from PySide6.QtCore import Signal, QThread, QTimer
|
|
7
7
|
|
|
8
8
|
|
|
@@ -98,6 +98,17 @@ class BaseFilter(ABC):
|
|
|
98
98
|
else:
|
|
99
99
|
restore_original()
|
|
100
100
|
|
|
101
|
+
def create_base_widgets(self, layout, buttons, preview_latency):
|
|
102
|
+
preview_check = QCheckBox("Preview")
|
|
103
|
+
preview_check.setChecked(True)
|
|
104
|
+
layout.addWidget(preview_check)
|
|
105
|
+
button_box = QDialogButtonBox(buttons)
|
|
106
|
+
layout.addWidget(button_box)
|
|
107
|
+
preview_timer = QTimer()
|
|
108
|
+
preview_timer.setSingleShot(True)
|
|
109
|
+
preview_timer.setInterval(preview_latency)
|
|
110
|
+
return preview_check, preview_timer, button_box
|
|
111
|
+
|
|
101
112
|
class PreviewWorker(QThread):
|
|
102
113
|
finished = Signal(np.ndarray, int)
|
|
103
114
|
|