shinestacker 1.8.0__py3-none-any.whl → 1.9.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 (36) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +184 -80
  3. shinestacker/algorithms/align_auto.py +13 -11
  4. shinestacker/algorithms/align_parallel.py +41 -16
  5. shinestacker/algorithms/base_stack_algo.py +1 -1
  6. shinestacker/algorithms/exif.py +252 -6
  7. shinestacker/algorithms/multilayer.py +6 -4
  8. shinestacker/algorithms/noise_detection.py +10 -8
  9. shinestacker/algorithms/pyramid_tiles.py +1 -1
  10. shinestacker/algorithms/stack.py +25 -13
  11. shinestacker/algorithms/stack_framework.py +16 -11
  12. shinestacker/algorithms/utils.py +18 -2
  13. shinestacker/algorithms/vignetting.py +16 -3
  14. shinestacker/app/settings_dialog.py +297 -173
  15. shinestacker/config/constants.py +10 -6
  16. shinestacker/config/settings.py +25 -7
  17. shinestacker/core/exceptions.py +1 -1
  18. shinestacker/core/framework.py +2 -2
  19. shinestacker/gui/action_config.py +23 -20
  20. shinestacker/gui/action_config_dialog.py +38 -21
  21. shinestacker/gui/folder_file_selection.py +3 -2
  22. shinestacker/gui/gui_images.py +27 -3
  23. shinestacker/gui/gui_run.py +2 -2
  24. shinestacker/gui/new_project.py +23 -12
  25. shinestacker/gui/project_controller.py +13 -6
  26. shinestacker/gui/project_editor.py +12 -2
  27. shinestacker/gui/project_model.py +4 -4
  28. shinestacker/retouch/exif_data.py +3 -0
  29. shinestacker/retouch/file_loader.py +3 -3
  30. shinestacker/retouch/io_gui_handler.py +4 -4
  31. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/METADATA +37 -39
  32. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/RECORD +36 -36
  33. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/WHEEL +0 -0
  34. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/entry_points.txt +0 -0
  35. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.dist-info}/licenses/LICENSE +0 -0
  36. {shinestacker-1.8.0.dist-info → shinestacker-1.9.0.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',
@@ -440,8 +446,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
440
446
  default=0)
441
447
  self.add_field(
442
448
  'step_process', FIELD_BOOL, 'Step process', required=False,
443
- expert=True,
444
- default=True)
449
+ expert=True, default=constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS)
445
450
  self.add_field(
446
451
  'max_threads', FIELD_INT, 'Max num. of cores',
447
452
  required=False, default=AppConfig.get('combined_actions_params')['max_threads'],
@@ -450,7 +455,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
450
455
  self.add_field(
451
456
  'chunk_submit', FIELD_BOOL, 'Submit in chunks',
452
457
  expert=True,
453
- required=False, default=constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
458
+ required=False, default=constants.DEFAULT_FWK_CHUNK_SUBMIT)
454
459
 
455
460
 
456
461
  class MaskNoiseConfigurator(DefaultActionConfigurator):
@@ -575,14 +580,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
575
580
  self.detector_field = None
576
581
  self.descriptor_field = None
577
582
  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")
583
+ self.tab_widget = create_tab_widget(layout)
584
+ feature_layout = add_tab(self.tab_widget, "Feature extraction")
580
585
  self.create_feature_tab(feature_layout)
581
- transform_layout = self.add_tab(self.tab_widget, "Transform")
586
+ transform_layout = add_tab(self.tab_widget, "Transform")
582
587
  self.create_transform_tab(transform_layout)
583
- border_layout = self.add_tab(self.tab_widget, "Border")
588
+ border_layout = add_tab(self.tab_widget, "Border")
584
589
  self.create_border_tab(border_layout)
585
- misc_layout = self.add_tab(self.tab_widget, "Miscellanea")
590
+ misc_layout = add_tab(self.tab_widget, "Miscellanea")
586
591
  self.create_miscellanea_tab(misc_layout)
587
592
 
588
593
  def create_feature_tab(self, layout):
@@ -591,7 +596,6 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
591
596
  self.change_match_config(
592
597
  self.detector_field, self.descriptor_field,
593
598
  self. matching_method_field, self.show_info)
594
-
595
599
  self.add_bold_label_to_layout(layout, "Feature identification:")
596
600
  self.detector_field = self.add_field_to_layout(
597
601
  layout, 'detector', FIELD_COMBO, 'Detector', required=False,
@@ -642,9 +646,9 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
642
646
  options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
643
647
  default=constants.DEFAULT_TRANSFORM)
644
648
  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)
649
+ layout, 'align_method', FIELD_COMBO, 'Estimation method', required=False,
650
+ options=self.METHOD_OPTIONS, values=constants.VALID_ESTIMATION_METHODS,
651
+ default=constants.DEFAULT_ESTIMATION_METHOD)
648
652
  rans_threshold = self.add_field_to_layout(
649
653
  layout, 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
650
654
  expert=True,
@@ -689,6 +693,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
689
693
 
690
694
  transform.currentIndexChanged.connect(change_transform)
691
695
  change_transform()
696
+ phase_corr_fallback = self.add_field_to_layout(
697
+ layout, 'phase_corr_fallback', FIELD_BOOL, "Phase correlation as fallback",
698
+ required=False, expert=True, default=constants.DEFAULT_PHASE_CORR_FALLBACK)
699
+ phase_corr_fallback.setToolTip(
700
+ "Align using phase correlation algorithm if the number of matches\n"
701
+ "is too low to determine the transformation.\n"
702
+ "This algorithm is not very precise,\n"
703
+ "and may help only in case of blurred images.")
692
704
  self.add_field_to_layout(
693
705
  layout, 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
694
706
  expert=True,
@@ -723,7 +735,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
723
735
  self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
724
736
  memory_limit = self.add_field_to_layout(
725
737
  layout, 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
726
- required=False, default=constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
738
+ required=False, default=AppConfig.get('align_frames_params')['memory_limit'],
727
739
  min_val=1.0, max_val=64.0)
728
740
  max_threads = self.add_field_to_layout(
729
741
  layout, 'max_threads', FIELD_INT, 'Max num. of cores',
@@ -737,6 +749,10 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
737
749
  layout, 'bw_matching', FIELD_BOOL, 'Match using black & white',
738
750
  expert=True,
739
751
  required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
752
+ delta_max = self.add_field_to_layout(
753
+ layout, 'delta_max', FIELD_INT, 'Max frames skip',
754
+ required=False, default=constants.DEFAULT_ALIGN_DELTA_MAX,
755
+ min_val=1, max_val=128)
740
756
 
741
757
  def change_mode():
742
758
  text = mode.currentText()
@@ -745,6 +761,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
745
761
  max_threads.setEnabled(enabled)
746
762
  chunk_submit.setEnabled(enabled)
747
763
  bw_matching.setEnabled(enabled)
764
+ delta_max.setEnabled(enabled)
748
765
 
749
766
  mode.currentIndexChanged.connect(change_mode)
750
767
 
@@ -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]}.")
@@ -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(
@@ -251,7 +258,7 @@ class ProjectController(QObject):
251
258
  self.set_current_file_path(file_path)
252
259
  with open(self.current_file_path(), 'r', encoding="utf-8") as file:
253
260
  json_obj = json.load(file)
254
- project = Project.from_dict(json_obj['project'])
261
+ project = Project.from_dict(json_obj['project'], json_obj['version'])
255
262
  if project is None:
256
263
  raise RuntimeError(f"Project from file {file_path} produced a null project.")
257
264
  self.set_enabled_file_open_close_actions_requested.emit(True)
@@ -313,7 +320,7 @@ class ProjectController(QObject):
313
320
  def do_save(self, file_path):
314
321
  try:
315
322
  json_obj = jsonpickle.encode({
316
- 'project': self.project().to_dict(), 'version': 1
323
+ 'project': self.project().to_dict(), 'version': CURRENT_PROJECT_FILE_VERSION
317
324
  })
318
325
  with open(file_path, 'w', encoding="utf-8") as f:
319
326
  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
@@ -42,8 +42,11 @@ class ExifData(BaseFormDialog):
42
42
  data = exif_dict(self.exif)
43
43
  if len(data) > 0:
44
44
  for k, (_, d) in data.items():
45
+ print(k, type(d))
45
46
  if isinstance(d, IFDRational):
46
47
  d = f"{d.numerator}/{d.denominator}"
48
+ elif len(str(d)) > 40:
49
+ d = f"{str(d):.40}..."
47
50
  else:
48
51
  d = f"{d}"
49
52
  if "<<<" not in d and k != 'IPTCNAA':
@@ -5,7 +5,7 @@ import numpy as np
5
5
  import cv2
6
6
  from psdtags import PsdChannelId
7
7
  from PySide6.QtCore import QThread, Signal
8
- from .. algorithms.utils import read_img, extension_tif, extension_jpg
8
+ from .. algorithms.utils import read_img, extension_tif, extension_jpg, extension_png
9
9
  from .. algorithms.multilayer import read_multilayer_tiff
10
10
 
11
11
 
@@ -50,10 +50,10 @@ class FileLoader(QThread):
50
50
  raise RuntimeError(f"Path {path} does not exist.")
51
51
  if not os.path.isfile(path):
52
52
  raise RuntimeError(f"Path {path} is not a file.")
53
- if extension_jpg(path):
53
+ if extension_jpg(path) or extension_png(path):
54
54
  try:
55
55
  stack = np.array([cv2.cvtColor(read_img(path), cv2.COLOR_BGR2RGB)])
56
- return stack, [path.split('/')[-1].split('.')[0]]
56
+ return stack, [os.path.splitext(os.path.basename(path))[0]]
57
57
  except Exception as e:
58
58
  traceback.print_tb(e.__traceback__)
59
59
  return None, None
@@ -7,6 +7,7 @@ from PySide6.QtWidgets import (QFileDialog, QMessageBox, QVBoxLayout, QLabel, QD
7
7
  QApplication, QProgressBar)
8
8
  from PySide6.QtGui import QGuiApplication, QCursor
9
9
  from PySide6.QtCore import Qt, QObject, QTimer, Signal
10
+ from .. algorithms.utils import EXTENSIONS_GUI_STR, EXTENSIONS_GUI_SAVE_STR
10
11
  from .. algorithms.exif import get_exif, write_image_with_exif_data
11
12
  from .file_loader import FileLoader
12
13
  from .exif_data import ExifData
@@ -135,7 +136,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
135
136
  if file_paths is None:
136
137
  file_paths, _ = QFileDialog.getOpenFileNames(
137
138
  self.parent(), "Open Image", "",
138
- "Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
139
+ F"Images ({EXTENSIONS_GUI_STR});;All Files (*)")
139
140
  if not file_paths:
140
141
  return
141
142
  if self.loader_thread and self.loader_thread.isRunning():
@@ -167,7 +168,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
167
168
  def import_frames(self):
168
169
  file_paths, _ = QFileDialog.getOpenFileNames(
169
170
  self.parent(), "Select frames", "",
170
- "Images Images (*.tif *.tiff *.jpg *.jpeg);;All Files (*)")
171
+ f"Images Images ({EXTENSIONS_GUI_STR});;All Files (*)")
171
172
  if file_paths:
172
173
  self.import_frames_from_files(file_paths)
173
174
 
@@ -286,8 +287,7 @@ class IOGuiHandler(QObject, LayerCollectionHandler):
286
287
  if self.layer_stack() is None:
287
288
  return
288
289
  path, _ = QFileDialog.getSaveFileName(
289
- self.parent(), "Save Image", "",
290
- "TIFF Files (*.tif *.tiff);;JPEG Files (*.jpg *.jpeg);;All Files (*)")
290
+ self.parent(), "Save Image", "", EXTENSIONS_GUI_SAVE_STR)
291
291
  if path:
292
292
  self.save_master_to_path(path)
293
293