shinestacker 1.8.0__py3-none-any.whl → 1.8.1__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.

@@ -39,23 +39,26 @@ DEFAULT_SETTINGS = {
39
39
  'cursor_update_time': gui_constants.DEFAULT_CURSOR_UPDATE_TIME,
40
40
  'min_mouse_step_brush_fraction': gui_constants.DEFAULT_MIN_MOUSE_STEP_BRUSH_FRACTION,
41
41
  'combined_actions_params': {
42
- 'max_threads': constants.DEFAULT_MAX_FWK_THREADS
42
+ 'max_threads': constants.DEFAULT_FWK_MAX_THREADS
43
43
  },
44
44
  'align_frames_params': {
45
+ 'memory_limit': constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
45
46
  'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS,
46
47
  'detector': constants.DEFAULT_DETECTOR,
47
48
  'descriptor': constants.DEFAULT_DESCRIPTOR,
48
49
  'match_method': constants.DEFAULT_MATCHING_METHOD
49
50
  },
50
51
  'focus_stack_params': {
52
+ 'memory_limit': constants.DEFAULT_PY_MEMORY_LIMIT_GB,
51
53
  'max_threads': constants.DEFAULT_PY_MAX_THREADS
52
54
  },
53
55
  'focus_stack_bunch_params': {
56
+ 'memory_limit': constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
54
57
  'max_threads': constants.DEFAULT_PY_MAX_THREADS
55
58
  }
56
59
  }
57
60
 
58
- CURRENT_VERSION = 1
61
+ CURRENT_SETTINGS_FILE_VERSION = 1
59
62
 
60
63
 
61
64
  class Settings(StdPathFile):
@@ -66,18 +69,30 @@ class Settings(StdPathFile):
66
69
  if Settings._instance is not None:
67
70
  raise RuntimeError("Settings is a singleton.")
68
71
  super().__init__(filename)
69
- self.settings = DEFAULT_SETTINGS
72
+ self.settings = self._deep_copy_defaults()
70
73
  file_path = self.get_file_path()
71
74
  if os.path.isfile(file_path):
72
75
  try:
73
76
  with open(file_path, 'r', encoding="utf-8") as file:
74
77
  json_data = json.load(file)
75
- settings = json_data['settings']
78
+ file_settings = json_data['settings']
79
+ self._deep_merge_settings(file_settings)
76
80
  except Exception as e:
77
81
  traceback.print_tb(e.__traceback__)
78
82
  print(f"Can't read file from path {file_path}. Default settings ignored.")
79
- settings = {}
80
- self.settings = {**self.settings, **settings}
83
+
84
+ def _deep_copy_defaults(self):
85
+ return json.loads(json.dumps(DEFAULT_SETTINGS))
86
+
87
+ def _deep_merge_settings(self, file_settings):
88
+ for key, value in file_settings.items():
89
+ if key in self.settings:
90
+ if isinstance(value, dict) and isinstance(self.settings[key], dict):
91
+ for sub_key, sub_value in value.items():
92
+ if sub_key in self.settings[key]:
93
+ self.settings[key][sub_key] = sub_value
94
+ else:
95
+ self.settings[key] = value
81
96
 
82
97
  @classmethod
83
98
  def instance(cls, filename="shinestacker-settings.txt"):
@@ -99,7 +114,10 @@ class Settings(StdPathFile):
99
114
  try:
100
115
  config_dir = self.get_config_dir()
101
116
  os.makedirs(config_dir, exist_ok=True)
102
- json_data = {'version': CURRENT_VERSION, 'settings': self.settings}
117
+ json_data = {
118
+ 'version': CURRENT_SETTINGS_FILE_VERSION,
119
+ 'settings': self.settings
120
+ }
103
121
  json_obj = jsonpickle.encode(json_data)
104
122
  with open(self.get_file_path(), 'w', encoding="utf-8") as f:
105
123
  f.write(json_obj)
@@ -30,7 +30,7 @@ class AlignmentError(FocusStackError):
30
30
  def __init__(self, index, details):
31
31
  self.index = index
32
32
  self.details = details
33
- super().__init__(f"Alignment failed for image {index}: {details}")
33
+ super().__init__(f"Alignment failed for frame {index}: {details}")
34
34
 
35
35
 
36
36
  class BitDepthError(FocusStackError):
@@ -197,8 +197,8 @@ class Job(TaskBase):
197
197
 
198
198
  class SequentialTask(TaskBase):
199
199
  def __init__(self, name, enabled=True, **kwargs):
200
- self.max_threads = kwargs.pop('max_threads', constants.DEFAULT_MAX_FWK_THREADS)
201
- self.chunk_submit = kwargs.pop('chunk_submit', constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
200
+ self.max_threads = kwargs.pop('max_threads', constants.DEFAULT_FWK_MAX_THREADS)
201
+ self.chunk_submit = kwargs.pop('chunk_submit', constants.DEFAULT_FWK_CHUNK_SUBMIT)
202
202
  TaskBase.__init__(self, name, enabled, **kwargs)
203
203
  self.total_action_counts = None
204
204
  self.current_action_count = None
@@ -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
 
@@ -8,7 +8,7 @@ from .. config.constants import constants
8
8
  from .. config.app_config import AppConfig
9
9
  from .. algorithms.align import validate_align_config
10
10
  from . action_config import (
11
- DefaultActionConfigurator,
11
+ DefaultActionConfigurator, add_tab, create_tab_layout, create_tab_widget,
12
12
  FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
13
13
  FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO, FIELD_REF_IDX
14
14
  )
@@ -76,10 +76,15 @@ class JobConfigurator(DefaultActionConfigurator):
76
76
  input_filepaths = input_filepaths.split(constants.PATH_SEPARATOR)
77
77
  self.working_path_label = QLabel(working_path or "Not set")
78
78
  self.input_path_label = QLabel(input_path or "Not set")
79
- self.input_widget.path_edit.setText('')
80
79
  if input_filepaths:
80
+ full_input_dir = os.path.join(working_path, input_path)
81
+ self.input_widget.selected_files = [os.path.join(full_input_dir, f)
82
+ for f in input_filepaths]
83
+ self.input_widget.path_edit.setText(full_input_dir)
81
84
  self.input_widget.files_mode_radio.setChecked(True)
82
85
  else:
86
+ full_input_dir = os.path.join(working_path, input_path)
87
+ self.input_widget.path_edit.setText(full_input_dir)
83
88
  self.input_widget.folder_mode_radio.setChecked(False)
84
89
  self.input_widget.text_changed_connect(self.update_paths_and_frames)
85
90
  self.input_widget.folder_mode_radio.toggled.connect(self.update_paths_and_frames)
@@ -195,10 +200,10 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
195
200
 
196
201
  def create_form(self, layout, action):
197
202
  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")
203
+ self.tab_widget = create_tab_widget(layout)
204
+ self.general_tab_layout = add_tab(self.tab_widget, "General Parameters")
200
205
  self.create_general_tab(self.general_tab_layout)
201
- self.algorithm_tab_layout = self.add_tab(self.tab_widget, "Stacking Algorithm")
206
+ self.algorithm_tab_layout = add_tab(self.tab_widget, "Stacking Algorithm")
202
207
  self.create_algorithm_tab(self.algorithm_tab_layout)
203
208
 
204
209
  def create_general_tab(self, layout):
@@ -225,7 +230,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
225
230
  default=constants.STACK_ALGO_DEFAULT)
226
231
  q_pyramid, q_depthmap = QWidget(), QWidget()
227
232
  for q in [q_pyramid, q_depthmap]:
228
- q.setLayout(self.create_tab_layout())
233
+ q.setLayout(create_tab_layout())
229
234
  stacked = QStackedWidget()
230
235
  stacked.addWidget(q_pyramid)
231
236
  stacked.addWidget(q_depthmap)
@@ -267,7 +272,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
267
272
  q_pyramid.layout(), 'pyramid_memory_limit', FIELD_FLOAT,
268
273
  'Memory limit (approx., GBytes)',
269
274
  expert=True,
270
- required=False, default=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
275
+ required=False, default=AppConfig.get('focus_stack_params')['memory_limit'],
271
276
  min_val=1.0, max_val=64.0)
272
277
  max_threads = self.add_field_to_layout(
273
278
  q_pyramid.layout(), 'pyramid_max_threads', FIELD_INT, 'Max num. of cores',
@@ -440,8 +445,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
440
445
  default=0)
441
446
  self.add_field(
442
447
  'step_process', FIELD_BOOL, 'Step process', required=False,
443
- expert=True,
444
- default=True)
448
+ expert=True, default=constants.DEFAULT_COMBINED_ACTIONS_STEP_PROCESS)
445
449
  self.add_field(
446
450
  'max_threads', FIELD_INT, 'Max num. of cores',
447
451
  required=False, default=AppConfig.get('combined_actions_params')['max_threads'],
@@ -450,7 +454,7 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
450
454
  self.add_field(
451
455
  'chunk_submit', FIELD_BOOL, 'Submit in chunks',
452
456
  expert=True,
453
- required=False, default=constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
457
+ required=False, default=constants.DEFAULT_FWK_CHUNK_SUBMIT)
454
458
 
455
459
 
456
460
  class MaskNoiseConfigurator(DefaultActionConfigurator):
@@ -575,14 +579,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
575
579
  self.detector_field = None
576
580
  self.descriptor_field = None
577
581
  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")
582
+ self.tab_widget = create_tab_widget(layout)
583
+ feature_layout = add_tab(self.tab_widget, "Feature extraction")
580
584
  self.create_feature_tab(feature_layout)
581
- transform_layout = self.add_tab(self.tab_widget, "Transform")
585
+ transform_layout = add_tab(self.tab_widget, "Transform")
582
586
  self.create_transform_tab(transform_layout)
583
- border_layout = self.add_tab(self.tab_widget, "Border")
587
+ border_layout = add_tab(self.tab_widget, "Border")
584
588
  self.create_border_tab(border_layout)
585
- misc_layout = self.add_tab(self.tab_widget, "Miscellanea")
589
+ misc_layout = add_tab(self.tab_widget, "Miscellanea")
586
590
  self.create_miscellanea_tab(misc_layout)
587
591
 
588
592
  def create_feature_tab(self, layout):
@@ -591,7 +595,6 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
591
595
  self.change_match_config(
592
596
  self.detector_field, self.descriptor_field,
593
597
  self. matching_method_field, self.show_info)
594
-
595
598
  self.add_bold_label_to_layout(layout, "Feature identification:")
596
599
  self.detector_field = self.add_field_to_layout(
597
600
  layout, 'detector', FIELD_COMBO, 'Detector', required=False,
@@ -642,9 +645,9 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
642
645
  options=self.TRANSFORM_OPTIONS, values=constants.VALID_TRANSFORMS,
643
646
  default=constants.DEFAULT_TRANSFORM)
644
647
  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)
648
+ layout, 'align_method', FIELD_COMBO, 'Estimation method', required=False,
649
+ options=self.METHOD_OPTIONS, values=constants.VALID_ESTIMATION_METHODS,
650
+ default=constants.DEFAULT_ESTIMATION_METHOD)
648
651
  rans_threshold = self.add_field_to_layout(
649
652
  layout, 'rans_threshold', FIELD_FLOAT, 'RANSAC threshold (px)', required=False,
650
653
  expert=True,
@@ -689,6 +692,14 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
689
692
 
690
693
  transform.currentIndexChanged.connect(change_transform)
691
694
  change_transform()
695
+ phase_corr_fallback = self.add_field_to_layout(
696
+ layout, 'phase_corr_fallback', FIELD_BOOL, "Phase correlation as fallback",
697
+ required=False, expert=True, default=constants.DEFAULT_PHASE_CORR_FALLBACK)
698
+ phase_corr_fallback.setToolTip(
699
+ "Align using phase correlation algorithm if the number of matches\n"
700
+ "is too low to determine the transformation.\n"
701
+ "This algorithm is not very precise,\n"
702
+ "and may help only in case of blurred images.")
692
703
  self.add_field_to_layout(
693
704
  layout, 'abort_abnormal', FIELD_BOOL, 'Abort on abnormal transf.',
694
705
  expert=True,
@@ -723,7 +734,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
723
734
  self.MODE_OPTIONS))[constants.DEFAULT_ALIGN_MODE])
724
735
  memory_limit = self.add_field_to_layout(
725
736
  layout, 'memory_limit', FIELD_FLOAT, 'Memory limit (approx., GBytes)',
726
- required=False, default=constants.DEFAULT_ALIGN_MEMORY_LIMIT_GB,
737
+ required=False, default=AppConfig.get('align_frames_params')['memory_limit'],
727
738
  min_val=1.0, max_val=64.0)
728
739
  max_threads = self.add_field_to_layout(
729
740
  layout, 'max_threads', FIELD_INT, 'Max num. of cores',
@@ -737,6 +748,10 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
737
748
  layout, 'bw_matching', FIELD_BOOL, 'Match using black & white',
738
749
  expert=True,
739
750
  required=False, default=constants.DEFAULT_ALIGN_BW_MATCHING)
751
+ delta_max = self.add_field_to_layout(
752
+ layout, 'delta_max', FIELD_INT, 'Max frames skip',
753
+ required=False, default=constants.DEFAULT_ALIGN_DELTA_MAX,
754
+ min_val=1, max_val=128)
740
755
 
741
756
  def change_mode():
742
757
  text = mode.currentText()
@@ -745,6 +760,7 @@ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase
745
760
  max_threads.setEnabled(enabled)
746
761
  chunk_submit.setEnabled(enabled)
747
762
  bw_matching.setEnabled(enabled)
763
+ delta_max.setEnabled(enabled)
748
764
 
749
765
  mode.currentIndexChanged.connect(change_mode)
750
766
 
@@ -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)
@@ -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")
@@ -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