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.

Files changed (44) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +37 -20
  3. shinestacker/algorithms/balance.py +2 -1
  4. shinestacker/algorithms/base_stack_algo.py +2 -1
  5. shinestacker/algorithms/multilayer.py +11 -8
  6. shinestacker/algorithms/noise_detection.py +13 -7
  7. shinestacker/algorithms/pyramid.py +7 -4
  8. shinestacker/algorithms/stack.py +5 -4
  9. shinestacker/algorithms/stack_framework.py +12 -10
  10. shinestacker/app/app_config.py +4 -22
  11. shinestacker/app/main.py +1 -1
  12. shinestacker/config/config.py +22 -16
  13. shinestacker/config/constants.py +8 -1
  14. shinestacker/core/framework.py +15 -10
  15. shinestacker/gui/action_config.py +20 -41
  16. shinestacker/gui/actions_window.py +18 -47
  17. shinestacker/gui/gui_logging.py +8 -7
  18. shinestacker/gui/gui_run.py +8 -8
  19. shinestacker/gui/main_window.py +4 -4
  20. shinestacker/gui/new_project.py +34 -37
  21. shinestacker/gui/project_converter.py +0 -1
  22. shinestacker/gui/project_editor.py +43 -20
  23. shinestacker/gui/select_path_widget.py +32 -0
  24. shinestacker/retouch/base_filter.py +12 -1
  25. shinestacker/retouch/denoise_filter.py +4 -10
  26. shinestacker/retouch/exif_data.py +3 -9
  27. shinestacker/retouch/icon_container.py +19 -0
  28. shinestacker/retouch/image_editor.py +1 -1
  29. shinestacker/retouch/image_editor_ui.py +2 -1
  30. shinestacker/retouch/image_viewer.py +104 -20
  31. shinestacker/retouch/io_gui_handler.py +17 -16
  32. shinestacker/retouch/io_manager.py +0 -1
  33. shinestacker/retouch/layer_collection.py +2 -1
  34. shinestacker/retouch/shortcuts_help.py +2 -13
  35. shinestacker/retouch/unsharp_mask_filter.py +3 -10
  36. shinestacker/retouch/white_balance_filter.py +5 -13
  37. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/METADATA +8 -11
  38. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/RECORD +42 -41
  39. shinestacker-0.4.0.dist-info/licenses/LICENSE +165 -0
  40. shinestacker/algorithms/core_utils.py +0 -22
  41. shinestacker-0.3.5.dist-info/licenses/LICENSE +0 -1
  42. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/WHEEL +0 -0
  43. {shinestacker-0.3.5.dist-info → shinestacker-0.4.0.dist-info}/entry_points.txt +0 -0
  44. {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
- value = self.action.params.get(tag, '')
204
- edit = QLineEdit(value)
205
- edit.setPlaceholderText(kwargs.get('placeholder', ''))
206
- button = QPushButton("Browse...")
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.setContentsMargins(0, 0, 0, 0)
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, placeholder="_stack")
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
- self.builder.add_field('input_path', FIELD_REL_PATH,
645
- f'Input path (separate by {constants.PATH_SEPARATOR})',
646
- required=False, multiple_entries=True,
647
- placeholder='relative to working path')
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
- if self._current_file:
24
- title += f" - {os.path.basename(self._current_file)}"
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._current_file = None
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._current_file = None
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._current_file = file_path
128
- self._current_file_wd = '' if os.path.isabs(file_path) \
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
- if self._current_file:
181
- self.do_save(self._current_file)
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._current_file = file_path
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
- path = f"{self._current_file_wd}/{file_path}" \
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
- action_counter = -1
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(
@@ -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:#{FF}{MM}{MM}">',
36
- '\x1b[92m': f'<span style="color:#{MM}{FF}{MM}">',
37
- '\x1b[93m': f'<span style="color:#{FF}{FF}{MM}">',
38
- '\x1b[94m': f'<span style="color:#{MM}{MM}{FF}">',
39
- '\x1b[95m': f'<span style="color:#{FF}{MM}{FF}">',
40
- '\x1b[96m': f'<span style="color:#{MM}{FF}{FF}">',
41
- '\x1b[97m': f'<span style="color:#{FF}{FF}{FF}">',
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}">',
@@ -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(action_running_color)
126
+ self.set_style(ACTION_RUNNING_COLOR)
127
127
 
128
128
  def set_done_style(self):
129
- self.set_style(action_done_color)
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(*action_running_color.tuple())
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(*action_done_color.tuple())
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
@@ -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("Jobs"))
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("Actions"))
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: " + job_name,
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: " + project_name,
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)
@@ -1,13 +1,15 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0915, R0902
2
2
  import os
3
- from PySide6.QtWidgets import (QWidget, QLineEdit, QFormLayout, QHBoxLayout, QPushButton,
4
- QDialog, QSizePolicy, QFileDialog, QLabel, QCheckBox,
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
- def count_image_files(path):
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._current_file = None
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._current_file_wd, 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
- action_counter = -1
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