shinestacker 1.8.0__py3-none-any.whl → 1.9.3__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 (46) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +202 -81
  3. shinestacker/algorithms/align_auto.py +13 -11
  4. shinestacker/algorithms/align_parallel.py +50 -21
  5. shinestacker/algorithms/balance.py +1 -1
  6. shinestacker/algorithms/base_stack_algo.py +1 -1
  7. shinestacker/algorithms/exif.py +848 -127
  8. shinestacker/algorithms/multilayer.py +6 -4
  9. shinestacker/algorithms/noise_detection.py +10 -8
  10. shinestacker/algorithms/pyramid_tiles.py +1 -1
  11. shinestacker/algorithms/stack.py +33 -17
  12. shinestacker/algorithms/stack_framework.py +16 -11
  13. shinestacker/algorithms/utils.py +18 -2
  14. shinestacker/algorithms/vignetting.py +16 -3
  15. shinestacker/app/main.py +1 -1
  16. shinestacker/app/settings_dialog.py +297 -173
  17. shinestacker/config/constants.py +10 -6
  18. shinestacker/config/settings.py +25 -7
  19. shinestacker/core/exceptions.py +1 -1
  20. shinestacker/core/framework.py +2 -2
  21. shinestacker/gui/action_config.py +23 -20
  22. shinestacker/gui/action_config_dialog.py +38 -25
  23. shinestacker/gui/config_dialog.py +6 -5
  24. shinestacker/gui/folder_file_selection.py +3 -2
  25. shinestacker/gui/gui_images.py +27 -3
  26. shinestacker/gui/gui_run.py +2 -2
  27. shinestacker/gui/main_window.py +6 -0
  28. shinestacker/gui/menu_manager.py +8 -2
  29. shinestacker/gui/new_project.py +23 -12
  30. shinestacker/gui/project_controller.py +14 -6
  31. shinestacker/gui/project_editor.py +12 -2
  32. shinestacker/gui/project_model.py +4 -4
  33. shinestacker/retouch/brush_tool.py +20 -0
  34. shinestacker/retouch/exif_data.py +106 -38
  35. shinestacker/retouch/file_loader.py +3 -3
  36. shinestacker/retouch/image_editor_ui.py +79 -3
  37. shinestacker/retouch/image_viewer.py +6 -1
  38. shinestacker/retouch/io_gui_handler.py +13 -16
  39. shinestacker/retouch/shortcuts_help.py +15 -8
  40. shinestacker/retouch/view_strategy.py +12 -2
  41. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/METADATA +37 -39
  42. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/RECORD +46 -46
  43. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/WHEEL +0 -0
  44. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/entry_points.txt +0 -0
  45. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/licenses/LICENSE +0 -0
  46. {shinestacker-1.8.0.dist-info → shinestacker-1.9.3.dist-info}/top_level.txt +0 -0
@@ -416,6 +416,29 @@ class FieldBuilder:
416
416
  return checkbox
417
417
 
418
418
 
419
+ def create_tab_layout():
420
+ tab_layout = QFormLayout()
421
+ tab_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
422
+ tab_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
423
+ tab_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
424
+ tab_layout.setLabelAlignment(Qt.AlignLeft)
425
+ return tab_layout
426
+
427
+
428
+ def add_tab(tab_widget, title):
429
+ tab = QWidget()
430
+ tab_layout = create_tab_layout()
431
+ tab.setLayout(tab_layout)
432
+ tab_widget.addTab(tab, title)
433
+ return tab_layout
434
+
435
+
436
+ def create_tab_widget(main_layout):
437
+ tab_widget = QTabWidget()
438
+ main_layout.addRow(tab_widget)
439
+ return tab_widget
440
+
441
+
419
442
  class NoNameActionConfigurator(ActionConfigurator):
420
443
  def __init__(self, current_wd):
421
444
  super().__init__(current_wd)
@@ -457,26 +480,6 @@ class NoNameActionConfigurator(ActionConfigurator):
457
480
  def add_labelled_row(self, label, widget):
458
481
  self.add_row(self.labelled_widget(label, widget))
459
482
 
460
- def create_tab_widget(self, main_layout):
461
- tab_widget = QTabWidget()
462
- main_layout.addRow(tab_widget)
463
- return tab_widget
464
-
465
- def create_tab_layout(self):
466
- tab_layout = QFormLayout()
467
- tab_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
468
- tab_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
469
- tab_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
470
- tab_layout.setLabelAlignment(Qt.AlignLeft)
471
- return tab_layout
472
-
473
- def add_tab(self, tab_widget, title):
474
- tab = QWidget()
475
- tab_layout = self.create_tab_layout()
476
- tab.setLayout(tab_layout)
477
- tab_widget.addTab(tab, title)
478
- return tab_layout
479
-
480
483
  def add_field_to_layout(self, main_layout, tag, field_type, label, required=False, **kwargs):
481
484
  return self.add_field(tag, field_type, label, required, add_to_layout=main_layout, **kwargs)
482
485
 
@@ -6,9 +6,10 @@ from PySide6.QtCore import QTimer
6
6
  from PySide6.QtWidgets import QWidget, QLabel, QMessageBox, QStackedWidget
7
7
  from .. config.constants import constants
8
8
  from .. config.app_config import AppConfig
9
+ from .. algorithms.utils import EXTENSIONS_SUPPORTED
9
10
  from .. algorithms.align import validate_align_config
10
11
  from . action_config import (
11
- DefaultActionConfigurator,
12
+ DefaultActionConfigurator, add_tab, create_tab_layout, create_tab_widget,
12
13
  FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
13
14
  FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO, FIELD_REF_IDX
14
15
  )
@@ -76,10 +77,15 @@ class JobConfigurator(DefaultActionConfigurator):
76
77
  input_filepaths = input_filepaths.split(constants.PATH_SEPARATOR)
77
78
  self.working_path_label = QLabel(working_path or "Not set")
78
79
  self.input_path_label = QLabel(input_path or "Not set")
79
- self.input_widget.path_edit.setText('')
80
80
  if input_filepaths:
81
+ full_input_dir = os.path.join(working_path, input_path)
82
+ self.input_widget.selected_files = [os.path.join(full_input_dir, f)
83
+ for f in input_filepaths]
84
+ self.input_widget.path_edit.setText(full_input_dir)
81
85
  self.input_widget.files_mode_radio.setChecked(True)
82
86
  else:
87
+ full_input_dir = os.path.join(working_path, input_path)
88
+ self.input_widget.path_edit.setText(full_input_dir)
83
89
  self.input_widget.folder_mode_radio.setChecked(False)
84
90
  self.input_widget.text_changed_connect(self.update_paths_and_frames)
85
91
  self.input_widget.folder_mode_radio.toggled.connect(self.update_paths_and_frames)
@@ -117,7 +123,7 @@ class JobConfigurator(DefaultActionConfigurator):
117
123
  return 0
118
124
  count = 0
119
125
  for filename in os.listdir(path):
120
- if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff')):
126
+ if os.path.splitext(filename)[-1][1:].lower() in EXTENSIONS_SUPPORTED:
121
127
  count += 1
122
128
  return count
123
129
 
@@ -195,10 +201,10 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
195
201
 
196
202
  def create_form(self, layout, action):
197
203
  super().create_form(layout, action)
198
- self.tab_widget = self.create_tab_widget(layout)
199
- self.general_tab_layout = self.add_tab(self.tab_widget, "General Parameters")
204
+ self.tab_widget = create_tab_widget(layout)
205
+ self.general_tab_layout = add_tab(self.tab_widget, "General Parameters")
200
206
  self.create_general_tab(self.general_tab_layout)
201
- self.algorithm_tab_layout = self.add_tab(self.tab_widget, "Stacking Algorithm")
207
+ self.algorithm_tab_layout = add_tab(self.tab_widget, "Stacking Algorithm")
202
208
  self.create_algorithm_tab(self.algorithm_tab_layout)
203
209
 
204
210
  def create_general_tab(self, layout):
@@ -225,7 +231,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
225
231
  default=constants.STACK_ALGO_DEFAULT)
226
232
  q_pyramid, q_depthmap = QWidget(), QWidget()
227
233
  for q in [q_pyramid, q_depthmap]:
228
- q.setLayout(self.create_tab_layout())
234
+ q.setLayout(create_tab_layout())
229
235
  stacked = QStackedWidget()
230
236
  stacked.addWidget(q_pyramid)
231
237
  stacked.addWidget(q_depthmap)
@@ -267,7 +273,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
267
273
  q_pyramid.layout(), 'pyramid_memory_limit', FIELD_FLOAT,
268
274
  'Memory limit (approx., GBytes)',
269
275
  expert=True,
270
- required=False, default=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
276
+ required=False, default=AppConfig.get('focus_stack_params')['memory_limit'],
271
277
  min_val=1.0, max_val=64.0)
272
278
  max_threads = self.add_field_to_layout(
273
279
  q_pyramid.layout(), 'pyramid_max_threads', FIELD_INT, 'Max num. of cores',
@@ -366,10 +372,6 @@ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
366
372
  self.add_field_to_layout(
367
373
  self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
368
374
  default=constants.DEFAULT_OVERLAP, min_val=0, max_val=100)
369
- self.add_field_to_layout(
370
- self.general_tab_layout, 'scratch_output_dir', FIELD_BOOL,
371
- 'Scratch output folder before run',
372
- required=False, default=True)
373
375
  self.add_field_to_layout(
374
376
  self.general_tab_layout, 'delete_output_at_end', FIELD_BOOL,
375
377
  'Delete output at end of job',
@@ -440,8 +442,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
440
442
  default=0)
441
443
  self.add_field(
442
444
  'step_process', FIELD_BOOL, 'Step process', required=False,
443
- expert=True,
444
- default=True)
445
+ expert=True, default=constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS)
445
446
  self.add_field(
446
447
  'max_threads', FIELD_INT, 'Max num. of cores',
447
448
  required=False, default=AppConfig.get('combined_actions_params')['max_threads'],
@@ -450,7 +451,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
450
451
  self.add_field(
451
452
  'chunk_submit', FIELD_BOOL, 'Submit in chunks',
452
453
  expert=True,
453
- required=False, default=constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
454
+ required=False, default=constants.DEFAULT_FWK_CHUNK_SUBMIT)
454
455
 
455
456
 
456
457
  class MaskNoiseConfigurator(DefaultActionConfigurator):
@@ -575,14 +576,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
575
576
  self.detector_field = None
576
577
  self.descriptor_field = None
577
578
  self.matching_method_field = None
578
- self.tab_widget = self.create_tab_widget(layout)
579
- feature_layout = self.add_tab(self.tab_widget, "Feature extraction")
579
+ self.tab_widget = create_tab_widget(layout)
580
+ feature_layout = add_tab(self.tab_widget, "Feature extraction")
580
581
  self.create_feature_tab(feature_layout)
581
- transform_layout = self.add_tab(self.tab_widget, "Transform")
582
+ transform_layout = add_tab(self.tab_widget, "Transform")
582
583
  self.create_transform_tab(transform_layout)
583
- border_layout = self.add_tab(self.tab_widget, "Border")
584
+ border_layout = add_tab(self.tab_widget, "Border")
584
585
  self.create_border_tab(border_layout)
585
- misc_layout = self.add_tab(self.tab_widget, "Miscellanea")
586
+ misc_layout = add_tab(self.tab_widget, "Miscellanea")
586
587
  self.create_miscellanea_tab(misc_layout)
587
588
 
588
589
  def create_feature_tab(self, layout):
@@ -591,7 +592,6 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
591
592
  self.change_match_config(
592
593
  self.detector_field, self.descriptor_field,
593
594
  self. matching_method_field, self.show_info)
594
-
595
595
  self.add_bold_label_to_layout(layout, "Feature identification:")
596
596
  self.detector_field = self.add_field_to_layout(
597
597
  layout, 'detector', FIELD_COMBO, 'Detector', required=False,
@@ -642,9 +642,9 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
642
642
  options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
643
643
  default=constants.DEFAULT_TRANSFORM)
644
644
  method = self.add_field_to_layout(
645
- layout, 'align_method', FIELD_COMBO, 'Align method', required=False,
646
- options=self.METHOD_OPTIONS, values=constants.VALID_ALIGN_METHODS,
647
- default=constants.DEFAULT_ALIGN_METHOD)
645
+ layout, 'align_method', FIELD_COMBO, 'Estimation method', required=False,
646
+ options=self.METHOD_OPTIONS, values=constants.VALID_ESTIMATION_METHODS,
647
+ default=constants.DEFAULT_ESTIMATION_METHOD)
648
648
  rans_threshold = self.add_field_to_layout(
649
649
  layout, 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
650
650
  expert=True,
@@ -689,6 +689,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
689
689
 
690
690
  transform.currentIndexChanged.connect(change_transform)
691
691
  change_transform()
692
+ phase_corr_fallback = self.add_field_to_layout(
693
+ layout, 'phase_corr_fallback', FIELD_BOOL, "Phase correlation as fallback",
694
+ required=False, expert=True, default=constants.DEFAULT_PHASE_CORR_FALLBACK)
695
+ phase_corr_fallback.setToolTip(
696
+ "Align using phase correlation algorithm if the number of matches\n"
697
+ "is too low to determine the transformation.\n"
698
+ "This algorithm is not very precise,\n"
699
+ "and may help only in case of blurred images.")
692
700
  self.add_field_to_layout(
693
701
  layout, 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
694
702
  expert=True,
@@ -723,7 +731,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
723
731
  self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
724
732
  memory_limit = self.add_field_to_layout(
725
733
  layout, 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
726
- required=False, default=constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
734
+ required=False, default=AppConfig.get('align_frames_params')['memory_limit'],
727
735
  min_val=1.0, max_val=64.0)
728
736
  max_threads = self.add_field_to_layout(
729
737
  layout, 'max_threads', FIELD_INT, 'Max num. of cores',
@@ -737,6 +745,10 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
737
745
  layout, 'bw_matching', FIELD_BOOL, 'Match using black & white',
738
746
  expert=True,
739
747
  required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
748
+ delta_max = self.add_field_to_layout(
749
+ layout, 'delta_max', FIELD_INT, 'Max frames skip',
750
+ required=False, default=constants.DEFAULT_ALIGN_DELTA_MAX,
751
+ min_val=1, max_val=128)
740
752
 
741
753
  def change_mode():
742
754
  text = mode.currentText()
@@ -745,6 +757,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
745
757
  max_threads.setEnabled(enabled)
746
758
  chunk_submit.setEnabled(enabled)
747
759
  bw_matching.setEnabled(enabled)
760
+ delta_max.setEnabled(enabled)
748
761
 
749
762
  mode.currentIndexChanged.connect(change_mode)
750
763
 
@@ -12,6 +12,7 @@ class ConfigDialog(QDialog):
12
12
  self.form_layout = create_form_layout(self)
13
13
  scroll_area = QScrollArea()
14
14
  scroll_area.setWidgetResizable(True)
15
+ scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
15
16
  container_widget = QWidget()
16
17
  self.container_layout = QFormLayout(container_widget)
17
18
  self.container_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
@@ -19,19 +20,19 @@ class ConfigDialog(QDialog):
19
20
  self.container_layout.setFormAlignment(Qt.AlignLeft | Qt.AlignTop)
20
21
  self.container_layout.setLabelAlignment(Qt.AlignLeft)
21
22
  scroll_area.setWidget(container_widget)
22
- button_box = QHBoxLayout()
23
+ self.button_box = QHBoxLayout()
23
24
  self.ok_button = QPushButton("OK")
24
25
  self.ok_button.setFocus()
25
26
  self.cancel_button = QPushButton("Cancel")
26
27
  self.reset_button = QPushButton("Reset")
27
- button_box.addWidget(self.ok_button)
28
- button_box.addWidget(self.cancel_button)
29
- button_box.addWidget(self.reset_button)
28
+ self.button_box.addWidget(self.ok_button)
29
+ self.button_box.addWidget(self.cancel_button)
30
+ self.button_box.addWidget(self.reset_button)
30
31
  self.reset_button.clicked.connect(self.reset_to_defaults)
31
32
  self.ok_button.clicked.connect(self.accept)
32
33
  self.cancel_button.clicked.connect(self.reject)
33
34
  self.form_layout.addRow(scroll_area)
34
- self.form_layout.addRow(button_box)
35
+ self.form_layout.addRow(self.button_box)
35
36
  QTimer.singleShot(0, self.adjust_dialog_size)
36
37
  self.create_form_content()
37
38
 
@@ -1,8 +1,9 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611
2
2
  import os
3
+ from PySide6.QtCore import Qt
3
4
  from PySide6.QtWidgets import (QWidget, QRadioButton, QButtonGroup, QLineEdit,
4
5
  QPushButton, QHBoxLayout, QVBoxLayout, QFileDialog, QMessageBox)
5
- from PySide6.QtCore import Qt
6
+ from .. algorithms.utils import EXTENSIONS_GUI_STR
6
7
 
7
8
 
8
9
  class FolderFileSelectionWidget(QWidget):
@@ -73,7 +74,7 @@ class FolderFileSelectionWidget(QWidget):
73
74
  def browse_files(self):
74
75
  files, _ = QFileDialog.getOpenFileNames(
75
76
  self, "Select Input Files", "",
76
- "Image files (*.png *.jpg *.jpeg *.tif *.tiff)"
77
+ f"Image files ({EXTENSIONS_GUI_STR})"
77
78
  )
78
79
  if files:
79
80
  parent_dir = os.path.dirname(files[0])
@@ -1,14 +1,17 @@
1
- # pylint: disable=C0114, C0115, C0116, E0611, W0718, E1101, C0103
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0718, E1101, C0103, R0914
2
2
  import webbrowser
3
3
  import subprocess
4
4
  import os
5
+ import numpy as np
6
+ import cv2
5
7
  from PySide6.QtWidgets import QSizePolicy, QVBoxLayout, QWidget, QLabel, QStackedWidget
6
8
  from PySide6.QtPdf import QPdfDocument
7
9
  from PySide6.QtPdfWidgets import QPdfView
8
10
  from PySide6.QtCore import Qt, QMargins
9
- from PySide6.QtGui import QPixmap
11
+ from PySide6.QtGui import QPixmap, QImage
10
12
  from .. config.gui_constants import gui_constants
11
13
  from .. core.core_utils import running_under_windows, running_under_macos
14
+ from .. algorithms.utils import read_img
12
15
 
13
16
 
14
17
  def open_file(file_path):
@@ -74,7 +77,28 @@ class GuiImageView(QWidget):
74
77
  self.image_label.setAlignment(Qt.AlignCenter)
75
78
  self.main_layout.addWidget(self.image_label)
76
79
  self.setLayout(self.main_layout)
77
- pixmap = QPixmap(file_path)
80
+ try:
81
+ img = read_img(file_path)
82
+ height, width = img.shape[:2]
83
+ scale_factor = gui_constants.GUI_IMG_WIDTH / width
84
+ new_height = int(height * scale_factor)
85
+ img = cv2.resize(img, (gui_constants.GUI_IMG_WIDTH, new_height),
86
+ interpolation=cv2.INTER_LINEAR)
87
+ except Exception as e:
88
+ raise RuntimeError(f"Can't load file: {file_path}.") from e
89
+ if img.dtype == np.uint16:
90
+ img = (img // 256).astype(np.uint8)
91
+ if len(img.shape) == 3:
92
+ h, w, ch = img.shape
93
+ bytes_per_line = ch * w
94
+ rgb_image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
95
+ q_img = QImage(rgb_image.data, w, h, bytes_per_line, QImage.Format_RGB888)
96
+ else:
97
+ h, w = img.shape
98
+ bytes_per_line = w
99
+ q_img = QImage(img.data, w, h, bytes_per_line, QImage.Format_Grayscale8)
100
+ pixmap = QPixmap.fromImage(q_img)
101
+ self.image_label.setPixmap(pixmap)
78
102
  if pixmap:
79
103
  scaled_pixmap = pixmap.scaledToWidth(
80
104
  gui_constants.GUI_IMG_WIDTH, Qt.SmoothTransformation)
@@ -9,7 +9,7 @@ from PySide6.QtCore import Signal, Slot
9
9
  from .. config.constants import constants
10
10
  from .. config.gui_constants import gui_constants
11
11
  from .colors import RED_BUTTON_STYLE, BLUE_BUTTON_STYLE, BLUE_COMBO_STYLE
12
- from .. algorithms.utils import extension_jpg_tif_png, extension_pdf
12
+ from .. algorithms.utils import extension_supported, extension_pdf
13
13
  from .gui_logging import LogWorker, QTextEditLogger
14
14
  from .gui_images import GuiPdfView, GuiImageView, GuiOpenApp
15
15
  from .colors import (
@@ -209,7 +209,7 @@ class RunWindow(QTextEditLogger):
209
209
  try:
210
210
  if extension_pdf(path):
211
211
  image_view = GuiPdfView(path, self)
212
- elif extension_jpg_tif_png(path):
212
+ elif extension_supported(path):
213
213
  image_view = GuiImageView(path, self)
214
214
  else:
215
215
  raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
@@ -318,6 +318,12 @@ class MainWindow(QMainWindow, LogManager):
318
318
  edit_config_action.triggered.connect(self.edit_current_action)
319
319
  menu.addAction(edit_config_action)
320
320
  menu.addSeparator()
321
+ menu.addAction(self.menu_manager.cut_action)
322
+ menu.addAction(self.menu_manager.copy_action)
323
+ menu.addAction(self.menu_manager.paste_action)
324
+ menu.addAction(self.menu_manager.duplicate_action)
325
+ menu.addAction(self.menu_manager.delete_element_action)
326
+ menu.addSeparator()
321
327
  menu.addAction(self.menu_manager.run_job_action)
322
328
  menu.addAction(self.menu_manager.run_all_jobs_action)
323
329
  menu.addSeparator()
@@ -123,8 +123,14 @@ class MenuManager(QObject):
123
123
  self.undo_action = self.action("&Undo")
124
124
  self.undo_action.setEnabled(False)
125
125
  menu.addAction(self.undo_action)
126
- for name in ["&Cut", "Cop&y", "&Paste", "Duplicate"]:
127
- menu.addAction(self.action(name, requires_file=True))
126
+ self.cut_action = self.action("&Cut", requires_file=True)
127
+ menu.addAction(self.cut_action)
128
+ self.copy_action = self.action("Cop&y", requires_file=True)
129
+ menu.addAction(self.copy_action)
130
+ self.paste_action = self.action("&Paste", requires_file=True)
131
+ menu.addAction(self.paste_action)
132
+ self.duplicate_action = self.action("Duplicate", requires_file=True)
133
+ menu.addAction(self.duplicate_action)
128
134
  self.delete_element_action = self.action("Delete", requires_file=True)
129
135
  self.delete_element_action.setEnabled(False)
130
136
  menu.addAction(self.delete_element_action)
@@ -8,7 +8,7 @@ from PySide6.QtCore import Qt
8
8
  from .. config.gui_constants import gui_constants
9
9
  from .. config.constants import constants
10
10
  from .. config.app_config import AppConfig
11
- from .. algorithms.utils import read_img, extension_tif_jpg
11
+ from .. algorithms.utils import read_img, extension_supported
12
12
  from .. algorithms.stack import get_bunches
13
13
  from .folder_file_selection import FolderFileSelectionWidget
14
14
  from .base_form_dialog import BaseFormDialog
@@ -76,21 +76,32 @@ class NewProjectDialog(BaseFormDialog):
76
76
  self.focus_stack_depth_map.setChecked(gui_constants.NEW_PROJECT_FOCUS_STACK_DEPTH_MAP)
77
77
  self.multi_layer = QCheckBox()
78
78
  self.multi_layer.setChecked(gui_constants.NEW_PROJECT_MULTI_LAYER)
79
+
79
80
  step1_group = QGroupBox("1) Select Input")
80
81
  step1_layout = QVBoxLayout()
81
82
  step1_layout.setContentsMargins(15, 0, 15, 15)
82
83
  step1_layout.addWidget(
83
84
  QLabel("Select a folder containing "
84
85
  "all your images, or specific image files."))
85
- input_form = QFormLayout()
86
- input_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
87
- input_form.setFormAlignment(Qt.AlignLeft)
88
- input_form.setLabelAlignment(Qt.AlignLeft)
86
+ input_layout = QHBoxLayout()
87
+ input_layout.setContentsMargins(0, 0, 0, 0)
88
+ input_layout.setSpacing(10)
89
+ input_label = QLabel("Input:")
90
+ input_label.setFixedWidth(60)
89
91
  self.input_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
92
+ input_layout.addWidget(input_label)
93
+ input_layout.addWidget(self.input_widget)
94
+ frames_layout = QHBoxLayout()
95
+ frames_layout.setContentsMargins(0, 0, 0, 0)
96
+ frames_layout.setSpacing(10)
97
+ frames_label = QLabel("Number of selected frames:")
98
+ frames_label.setFixedWidth(180)
90
99
  self.frames_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
91
- input_form.addRow("Input:", self.input_widget)
92
- input_form.addRow("Number of frames: ", self.frames_label)
93
- step1_layout.addLayout(input_form)
100
+ frames_layout.addWidget(frames_label)
101
+ frames_layout.addWidget(self.frames_label)
102
+ frames_layout.addStretch()
103
+ step1_layout.addLayout(input_layout)
104
+ step1_layout.addLayout(frames_layout)
94
105
  step1_group.setLayout(step1_layout)
95
106
  self.form_layout.addRow(step1_group)
96
107
  step2_group = QGroupBox("2) Basic Options")
@@ -197,7 +208,7 @@ class NewProjectDialog(BaseFormDialog):
197
208
  return 0
198
209
  count = 0
199
210
  for filename in os.listdir(path):
200
- if extension_tif_jpg(filename):
211
+ if extension_supported(filename):
201
212
  count += 1
202
213
  return count
203
214
  if self.input_widget.get_selection_mode() == 'files' and \
@@ -262,7 +273,7 @@ class NewProjectDialog(BaseFormDialog):
262
273
  file_path = None
263
274
  for filename in files:
264
275
  full_path = os.path.join(path, filename)
265
- if extension_tif_jpg(full_path):
276
+ if extension_supported(full_path):
266
277
  file_path = full_path
267
278
  break
268
279
  if file_path is None:
@@ -273,8 +284,8 @@ class NewProjectDialog(BaseFormDialog):
273
284
  height, width = img.shape[:2]
274
285
  n_bytes = 1 if img.dtype == np.uint8 else 2
275
286
  n_bits = 8 if img.dtype == np.uint8 else 16
276
- n_gbytes = float(n_bytes * height * width * self.n_image_files) / constants.ONE_GIGA
277
- if n_gbytes > 1 and not self.bunch_stack.isChecked():
287
+ n_gbytes = 3.0 * n_bytes * height * width * self.n_image_files / constants.ONE_GIGA
288
+ if n_gbytes > 4 and not self.bunch_stack.isChecked():
278
289
  msg = QMessageBox()
279
290
  msg.setStyleSheet("""
280
291
  QMessageBox {
@@ -14,6 +14,9 @@ from .project_model import Project
14
14
  from .project_editor import ProjectEditor
15
15
 
16
16
 
17
+ CURRENT_PROJECT_FILE_VERSION = 1
18
+
19
+
17
20
  class ProjectController(QObject):
18
21
  update_title_requested = Signal()
19
22
  refresh_ui_requested = Signal(int, int)
@@ -162,14 +165,15 @@ class ProjectController(QObject):
162
165
  if dialog.get_noise_detection():
163
166
  job_noise = ActionConfig(
164
167
  constants.ACTION_JOB,
165
- {'name': f'{input_path}-detect-noise', 'working_path': working_path,
168
+ {'name': f'{input_path}-noise-job', 'working_path': working_path,
166
169
  'input_path': input_path})
170
+ noise_detection_name = f'{input_path}-detect-noise'
167
171
  noise_detection = ActionConfig(constants.ACTION_NOISEDETECTION,
168
- {'name': f'{input_path}-detect-noise'})
172
+ {'name': noise_detection_name})
169
173
  job_noise.add_sub_action(noise_detection)
170
174
  self.add_job_to_project(job_noise)
171
175
  job_params = {
172
- 'name': f'{input_path}-focus-stack',
176
+ 'name': f'{input_path}-stack-job',
173
177
  'working_path': working_path,
174
178
  'input_path': input_path
175
179
  }
@@ -184,7 +188,10 @@ class ProjectController(QObject):
184
188
  constants.ACTION_COMBO, {'name': preprocess_name})
185
189
  if dialog.get_noise_detection():
186
190
  mask_noise = ActionConfig(
187
- constants.ACTION_MASKNOISE, {'name': 'mask-noise'})
191
+ constants.ACTION_MASKNOISE,
192
+ {'name': 'mask-noise',
193
+ 'noise_mask': os.path.join(noise_detection_name,
194
+ constants.DEFAULT_NOISE_MAP_FILENAME)})
188
195
  combo_action.add_sub_action(mask_noise)
189
196
  if dialog.get_vignetting_correction():
190
197
  vignetting = ActionConfig(
@@ -238,6 +245,7 @@ class ProjectController(QObject):
238
245
  self.add_job_to_project(job)
239
246
  self.project_editor.set_modified(True)
240
247
  self.refresh_ui(0, -1)
248
+ self.set_enabled_file_open_close_actions_requested.emit(True)
241
249
 
242
250
  def open_project(self, file_path=False):
243
251
  if not self.check_unsaved_changes():
@@ -251,7 +259,7 @@ class ProjectController(QObject):
251
259
  self.set_current_file_path(file_path)
252
260
  with open(self.current_file_path(), 'r', encoding="utf-8") as file:
253
261
  json_obj = json.load(file)
254
- project = Project.from_dict(json_obj['project'])
262
+ project = Project.from_dict(json_obj['project'], json_obj['version'])
255
263
  if project is None:
256
264
  raise RuntimeError(f"Project from file {file_path} produced a null project.")
257
265
  self.set_enabled_file_open_close_actions_requested.emit(True)
@@ -313,7 +321,7 @@ class ProjectController(QObject):
313
321
  def do_save(self, file_path):
314
322
  try:
315
323
  json_obj = jsonpickle.encode({
316
- 'project': self.project().to_dict(), 'version': 1
324
+ 'project': self.project().to_dict(), 'version': CURRENT_PROJECT_FILE_VERSION
317
325
  })
318
326
  with open(file_path, 'w', encoding="utf-8") as f:
319
327
  f.write(json_obj)
@@ -1,8 +1,9 @@
1
1
  # pylint: disable=C0114, C0115, C0116, R0903, R0904, R1702, R0917, R0913, R0902, E0611, E1131, E1121
2
2
  import os
3
3
  from dataclasses import dataclass
4
- from PySide6.QtWidgets import QListWidget, QMessageBox, QDialog, QListWidgetItem, QLabel
5
- from PySide6.QtCore import Qt, QObject, Signal, QEvent
4
+ from PySide6.QtWidgets import (QListWidget, QMessageBox, QDialog, QListWidgetItem, QLabel,
5
+ QSizePolicy)
6
+ from PySide6.QtCore import Qt, QObject, Signal, QEvent, QSize
6
7
  from .. config.constants import constants
7
8
  from .colors import ColorPalette
8
9
  from .action_config_dialog import ActionConfigDialog
@@ -105,6 +106,8 @@ class HandCursorListWidget(QListWidget):
105
106
  super().__init__(parent)
106
107
  self.setMouseTracking(True)
107
108
  self.viewport().setMouseTracking(True)
109
+ self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
110
+ self.setWordWrap(False)
108
111
 
109
112
  def event(self, event):
110
113
  if event.type() == QEvent.HoverMove:
@@ -503,6 +506,13 @@ class ProjectEditor(QObject):
503
506
  if action.enabled() \
504
507
  else f"🚫 <span style='color:#{ColorPalette.DARK_RED.hex()};'>{text}</span>"
505
508
  label = QLabel(html_text)
509
+ label.setTextFormat(Qt.RichText)
510
+ label.setWordWrap(False)
511
+ label.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
512
+ label.adjustSize()
513
+ ideal_width = label.sizeHint().width()
514
+ widget_list.setItemWidget(item, label)
515
+ item.setSizeHint(QSize(ideal_width, label.sizeHint().height()))
506
516
  widget_list.setItemWidget(item, label)
507
517
 
508
518
  def add_sub_action(self, type_name):
@@ -62,10 +62,10 @@ class ActionConfig:
62
62
  return project_dict
63
63
 
64
64
  @classmethod
65
- def from_dict(cls, data):
65
+ def from_dict(cls, data, version):
66
66
  a = ActionConfig(data['type_name'], data['params'])
67
67
  if 'sub_actions' in data.keys():
68
- a.sub_actions = [ActionConfig.from_dict(s) for s in data['sub_actions']]
68
+ a.sub_actions = [ActionConfig.from_dict(s, version) for s in data['sub_actions']]
69
69
  for s in a.sub_actions:
70
70
  s.parent = a
71
71
  return a
@@ -84,9 +84,9 @@ class Project:
84
84
  return [j.to_dict() for j in self.jobs]
85
85
 
86
86
  @classmethod
87
- def from_dict(cls, data):
87
+ def from_dict(cls, data, version):
88
88
  p = Project()
89
- p.jobs = [ActionConfig.from_dict(j) for j in data]
89
+ p.jobs = [ActionConfig.from_dict(j, version) for j in data]
90
90
  for j in p.jobs:
91
91
  for s in j.sub_actions:
92
92
  s.parent = j
@@ -75,6 +75,26 @@ class BrushTool:
75
75
  self.hardness_slider.setValue(val)
76
76
  self.update_brush_hardness(val)
77
77
 
78
+ def increase_brush_opacity(self, amount=2):
79
+ val = min(self.opacity_slider.value() + amount, self.opacity_slider.maximum())
80
+ self.opacity_slider.setValue(val)
81
+ self.update_brush_opacity(val)
82
+
83
+ def decrease_brush_opacity(self, amount=2):
84
+ val = max(self.opacity_slider.value() - amount, self.opacity_slider.minimum())
85
+ self.opacity_slider.setValue(val)
86
+ self.update_brush_opacity(val)
87
+
88
+ def increase_brush_flow(self, amount=2):
89
+ val = min(self.flow_slider.value() + amount, self.flow_slider.maximum())
90
+ self.flow_slider.setValue(val)
91
+ self.update_brush_flow(val)
92
+
93
+ def decrease_brush_flow(self, amount=2):
94
+ val = max(self.flow_slider.value() - amount, self.flow_slider.minimum())
95
+ self.flow_slider.setValue(val)
96
+ self.update_brush_flow(val)
97
+
78
98
  def update_brush_hardness(self, hardness):
79
99
  self.brush.hardness = hardness
80
100
  self.update_brush_thumb()