shinestacker 1.7.0__py3-none-any.whl → 1.8.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 (31) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/stack.py +9 -0
  3. shinestacker/algorithms/stack_framework.py +35 -16
  4. shinestacker/algorithms/utils.py +5 -1
  5. shinestacker/app/settings_dialog.py +46 -3
  6. shinestacker/config/settings.py +4 -1
  7. shinestacker/core/core_utils.py +1 -0
  8. shinestacker/core/framework.py +7 -2
  9. shinestacker/gui/action_config_dialog.py +72 -45
  10. shinestacker/gui/gui_run.py +1 -2
  11. shinestacker/gui/img/dark/close-round-line-icon.png +0 -0
  12. shinestacker/gui/img/dark/forward-button-icon.png +0 -0
  13. shinestacker/gui/img/dark/play-button-round-icon.png +0 -0
  14. shinestacker/gui/img/dark/plus-round-line-icon.png +0 -0
  15. shinestacker/gui/img/dark/shinestacker_bkg.png +0 -0
  16. shinestacker/gui/main_window.py +20 -7
  17. shinestacker/gui/menu_manager.py +18 -7
  18. shinestacker/gui/new_project.py +0 -2
  19. shinestacker/gui/tab_widget.py +16 -6
  20. shinestacker/retouch/adjustments.py +5 -0
  21. {shinestacker-1.7.0.dist-info → shinestacker-1.8.0.dist-info}/METADATA +4 -4
  22. {shinestacker-1.7.0.dist-info → shinestacker-1.8.0.dist-info}/RECORD +31 -26
  23. /shinestacker/gui/img/{close-round-line-icon.png → light/close-round-line-icon.png} +0 -0
  24. /shinestacker/gui/img/{forward-button-icon.png → light/forward-button-icon.png} +0 -0
  25. /shinestacker/gui/img/{play-button-round-icon.png → light/play-button-round-icon.png} +0 -0
  26. /shinestacker/gui/img/{plus-round-line-icon.png → light/plus-round-line-icon.png} +0 -0
  27. /shinestacker/gui/{ico → img/light}/shinestacker_bkg.png +0 -0
  28. {shinestacker-1.7.0.dist-info → shinestacker-1.8.0.dist-info}/WHEEL +0 -0
  29. {shinestacker-1.7.0.dist-info → shinestacker-1.8.0.dist-info}/entry_points.txt +0 -0
  30. {shinestacker-1.7.0.dist-info → shinestacker-1.8.0.dist-info}/licenses/LICENSE +0 -0
  31. {shinestacker-1.7.0.dist-info → shinestacker-1.8.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.7.0'
1
+ __version__ = '1.8.0'
@@ -65,6 +65,9 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
65
65
  if self.exif_path != '':
66
66
  self.exif_path = os.path.join(working_path, self.exif_path)
67
67
 
68
+ def end_job(self):
69
+ ImageSequenceManager.end_job(self)
70
+
68
71
 
69
72
  def get_bunches(collection, n_frames, n_overlap):
70
73
  bunches = [collection[x:x + n_frames]
@@ -100,6 +103,9 @@ class FocusStackBunch(SequentialTask, FocusStackBase):
100
103
  def end(self):
101
104
  SequentialTask.end(self)
102
105
 
106
+ def end_job(self):
107
+ FocusStackBase.end_job(self)
108
+
103
109
  def run_step(self, action_count=-1):
104
110
  self.print_message(
105
111
  color_str(f"fusing bunch: {action_count + 1}/{self.total_action_counts}",
@@ -126,3 +132,6 @@ class FocusStack(FocusStackBase):
126
132
 
127
133
  def init(self, job, _working_path=''):
128
134
  FocusStackBase.init(self, job, self.working_path)
135
+
136
+ def end_job(self):
137
+ FocusStackBase.end_job(self)
@@ -46,7 +46,8 @@ class StackJob(Job):
46
46
  class ImageSequenceManager:
47
47
  def __init__(self, name, input_path='', output_path='', working_path='',
48
48
  plot_path=constants.DEFAULT_PLOTS_PATH,
49
- scratch_output_dir=True, resample=1,
49
+ scratch_output_dir=True, delete_output_at_end=False,
50
+ resample=1,
50
51
  reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **_kwargs):
51
52
  self.name = name
52
53
  self.working_path = working_path
@@ -56,6 +57,7 @@ class ImageSequenceManager:
56
57
  self._resample = resample
57
58
  self.reverse_order = reverse_order
58
59
  self.scratch_output_dir = scratch_output_dir
60
+ self.delete_output_at_end = delete_output_at_end
59
61
  self.enabled = None
60
62
  self.base_message = ''
61
63
  self._input_full_path = None
@@ -122,6 +124,24 @@ class ImageSequenceManager:
122
124
  constants.LOG_COLOR_LEVEL_2))
123
125
  self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
124
126
 
127
+ def scratch_outout_folder(self):
128
+ if self.enabled:
129
+ output_dir = self.output_full_path()
130
+ list_dir = os.listdir(output_dir)
131
+ n_files = len(list_dir)
132
+ if n_files > 0:
133
+ for filename in list_dir:
134
+ file_path = os.path.join(output_dir, filename)
135
+ if os.path.isfile(file_path):
136
+ os.remove(file_path)
137
+ self.print_message(
138
+ color_str(f"output directory {self.output_path} content erased",
139
+ 'yellow'))
140
+ else:
141
+ self.print_message(
142
+ color_str(f"module disabled, output directory {self.output_path}"
143
+ " not scratched", 'yellow'))
144
+
125
145
  def init(self, job):
126
146
  if self.working_path == '':
127
147
  self.working_path = job.working_path
@@ -130,22 +150,10 @@ class ImageSequenceManager:
130
150
  if not os.path.exists(output_dir):
131
151
  os.makedirs(output_dir)
132
152
  else:
133
- list_dir = os.listdir(output_dir)
134
- if len(list_dir) > 0:
153
+ if len(os.listdir(output_dir)):
135
154
  if self.scratch_output_dir:
136
- if self.enabled:
137
- for filename in list_dir:
138
- file_path = os.path.join(output_dir, filename)
139
- if os.path.isfile(file_path):
140
- os.remove(file_path)
141
- self.print_message(
142
- color_str(f": output directory {self.output_path} content erased",
143
- 'yellow'))
144
- else:
145
- self.print_message(
146
- color_str(f": module disabled, output directory {self.output_path}"
147
- " not scratched", 'yellow'))
148
- else:
155
+ self.scratch_outout_folder()
156
+ elif self.enabled:
149
157
  self.print_message(
150
158
  color_str(
151
159
  f": output directory {self.output_path} not empty, "
@@ -168,6 +176,11 @@ class ImageSequenceManager:
168
176
  self._input_filepaths.append(filepath)
169
177
  job.add_action_path(self.output_path)
170
178
 
179
+ def end_job(self):
180
+ if self.delete_output_at_end:
181
+ self.scratch_outout_folder()
182
+ os.rmdir(self.output_full_path())
183
+
171
184
  def folder_list_str(self):
172
185
  if isinstance(self.input_full_path(), list):
173
186
  file_list = ", ".join(
@@ -206,6 +219,9 @@ class ReferenceFrameTask(SequentialTask, ImageSequenceManager):
206
219
  def end(self):
207
220
  SequentialTask.end(self)
208
221
 
222
+ def end_job(self):
223
+ ImageSequenceManager.end_job(self)
224
+
209
225
  def run_frame(self, _idx, _ref_idx):
210
226
  return None
211
227
 
@@ -323,6 +339,9 @@ class CombinedActions(ReferenceFrameTask):
323
339
  if a.enabled:
324
340
  a.end()
325
341
 
342
+ def end_job(self):
343
+ ReferenceFrameTask.end_job(self)
344
+
326
345
  def sequential_processing(self):
327
346
  for a in self._actions:
328
347
  if a.sequential_processing():
@@ -140,8 +140,12 @@ def save_plot(filename, fig=None):
140
140
  if not os.path.isdir(dir_path):
141
141
  os.makedirs(dir_path)
142
142
  if fig is None:
143
+ logging_level = logging.getLogger().level
144
+ logger = logging.getLogger()
145
+ logger.setLevel(logging.WARNING)
143
146
  fig = plt.gcf()
144
- fig.savefig(filename, dpi=150)
147
+ fig.savefig(filename, dpi=150)
148
+ logger.setLevel(logging_level)
145
149
  if config.JUPYTER_NOTEBOOK:
146
150
  plt.show()
147
151
  plt.close(fig)
@@ -1,23 +1,28 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0718, R0903, E0611, R0902
2
2
  from PySide6.QtCore import Signal
3
3
  from PySide6.QtWidgets import QFrame, QLabel, QCheckBox, QComboBox, QDoubleSpinBox, QSpinBox
4
- from .. gui.config_dialog import ConfigDialog
5
4
  from .. config.settings import Settings
6
5
  from .. config.constants import constants
7
6
  from .. config.gui_constants import gui_constants
7
+ from .. gui.config_dialog import ConfigDialog
8
+ from .. gui.action_config_dialog import AlignFramesConfigBase
8
9
 
9
10
 
10
- class SettingsDialog(ConfigDialog):
11
+ class SettingsDialog(ConfigDialog, AlignFramesConfigBase):
11
12
  update_project_config_requested = Signal()
12
13
  update_retouch_config_requested = Signal()
13
14
 
14
15
  def __init__(self, parent=None, project_settings=True, retouch_settings=True):
16
+ AlignFramesConfigBase.__init__(self)
15
17
  self.project_settings = project_settings
16
18
  self.retouch_settings = retouch_settings
17
19
  self.settings = Settings.instance()
18
20
  self.expert_options = None
19
21
  self.combined_actions_max_threads = None
20
22
  self.align_frames_max_threads = None
23
+ self.detector = None
24
+ self.descriptor = None
25
+ self.matching_method = None
21
26
  self.focus_stack_max_threads = None
22
27
  self.view_strategy = None
23
28
  self.min_mouse_step_brush_fraction = None
@@ -58,6 +63,32 @@ class SettingsDialog(ConfigDialog):
58
63
  self.container_layout.addRow("Max num. of cores, align frames:",
59
64
  self.align_frames_max_threads)
60
65
 
66
+ def change_match_config():
67
+ self.change_match_config(
68
+ self.detector, self.descriptor,
69
+ self. matching_method, self.show_info)
70
+
71
+ self.detector = QComboBox()
72
+ self.detector.addItems(constants.VALID_DETECTORS)
73
+ self.descriptor = QComboBox()
74
+ self.descriptor.addItems(constants.VALID_DESCRIPTORS)
75
+ self.matching_method = QComboBox()
76
+ self.info_label = QLabel()
77
+ self.info_label.setStyleSheet("color: orange; font-style: italic;")
78
+ self.matching_method = QComboBox()
79
+ for k, v in zip(self.MATCHING_METHOD_OPTIONS, constants.VALID_MATCHING_METHODS):
80
+ self.matching_method.addItem(k, v)
81
+ self.detector.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['detector'])
82
+ self.descriptor.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['descriptor'])
83
+ self.matching_method.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['match_method'])
84
+ self.detector.currentIndexChanged.connect(change_match_config)
85
+ self.descriptor.currentIndexChanged.connect(change_match_config)
86
+ self.matching_method.currentIndexChanged.connect(change_match_config)
87
+ self.container_layout.addRow('Detector:', self.detector)
88
+ self.container_layout.addRow('Descriptor:', self.descriptor)
89
+ self.container_layout.addRow(self.info_label)
90
+ self.container_layout.addRow('Match method:', self.matching_method)
91
+
61
92
  self.focus_stack_max_threads = QSpinBox()
62
93
  self.focus_stack_max_threads.setRange(0, 64)
63
94
  self.focus_stack_max_threads.setValue(
@@ -115,7 +146,14 @@ class SettingsDialog(ConfigDialog):
115
146
  })
116
147
  self.settings.set(
117
148
  'align_frames_params', {
118
- 'max_threads': self.align_frames_max_threads.value()
149
+ 'max_threads':
150
+ self.align_frames_max_threads.value(),
151
+ 'detector':
152
+ self.descriptor.currentText(),
153
+ 'descriptor':
154
+ self.descriptor.currentText(),
155
+ 'match_method':
156
+ self.matching_method.itemData(self.matching_method.currentIndex())
119
157
  })
120
158
  self.settings.set(
121
159
  'focus_stack_params', {
@@ -148,6 +186,11 @@ class SettingsDialog(ConfigDialog):
148
186
  self.expert_options.setChecked(constants.DEFAULT_EXPERT_OPTIONS)
149
187
  self.combined_actions_max_threads.setValue(constants.DEFAULT_MAX_FWK_THREADS)
150
188
  self.align_frames_max_threads.setValue(constants.DEFAULT_ALIGN_MAX_THREADS)
189
+ self.detector.setCurrentText(constants.DEFAULT_DETECTOR)
190
+ self.descriptor.setCurrentText(constants.DEFAULT_DESCRIPTOR)
191
+ idx = self.matching_method.findData(constants.DEFAULT_MATCHING_METHOD)
192
+ if idx >= 0:
193
+ self.matching_method.setCurrentIndex(idx)
151
194
  self.focus_stack_max_threads.setValue(constants.DEFAULT_PY_MAX_THREADS)
152
195
  if self.retouch_settings:
153
196
  idx = self.view_strategy.findData(constants.DEFAULT_VIEW_STRATEGY)
@@ -42,7 +42,10 @@ DEFAULT_SETTINGS = {
42
42
  'max_threads': constants.DEFAULT_MAX_FWK_THREADS
43
43
  },
44
44
  'align_frames_params': {
45
- 'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS
45
+ 'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS,
46
+ 'detector': constants.DEFAULT_DETECTOR,
47
+ 'descriptor': constants.DEFAULT_DESCRIPTOR,
48
+ 'match_method': constants.DEFAULT_MATCHING_METHOD
46
49
  },
47
50
  'focus_stack_params': {
48
51
  'max_threads': constants.DEFAULT_PY_MAX_THREADS
@@ -52,4 +52,5 @@ def setup_matplotlib_mode():
52
52
  __IPYTHON__ # noqa
53
53
  except Exception:
54
54
  matplotlib.use('agg')
55
+ matplotlib.pyplot.set_loglevel("warning")
55
56
  matplotlib.rcParams['pdf.fonttype'] = 42
@@ -146,6 +146,9 @@ class TaskBase:
146
146
  def sub_message_r(self, msg='', level=logging.INFO):
147
147
  self.sub_message(msg, level, self.end_r, self.begin_r, False)
148
148
 
149
+ def end_job(self):
150
+ pass
151
+
149
152
 
150
153
  class Job(TaskBase):
151
154
  def __init__(self, name, logger_name=None, log_file='', callbacks=None, **kwargs):
@@ -188,6 +191,8 @@ class Job(TaskBase):
188
191
  self.id, self.name) is False:
189
192
  raise RunStopException(self.name)
190
193
  a.run()
194
+ for a in self.__actions:
195
+ a.end_job()
191
196
 
192
197
 
193
198
  class SequentialTask(TaskBase):
@@ -261,11 +266,11 @@ class SequentialTask(TaskBase):
261
266
  try:
262
267
  result = future.result()
263
268
  if result:
264
- self.print_message(color_str(
269
+ self.print_message_r(color_str(
265
270
  f"completed processing step: {self.idx_tot_str(idx)}",
266
271
  constants.LOG_COLOR_LEVEL_1))
267
272
  else:
268
- self.print_message(color_str(
273
+ self.print_message_r(color_str(
269
274
  f"failed processing step: {self.idx_tot_str(idx)}",
270
275
  constants.LOG_COLOR_WARNING))
271
276
  self.current_action_count += 1
@@ -1,5 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0915, R0912
2
- # pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914, C0302
2
+ # pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914, C0302, R0903
3
3
  import os
4
4
  import traceback
5
5
  from PySide6.QtCore import QTimer
@@ -214,7 +214,7 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
214
214
  expert=True,
215
215
  placeholder='relative to working path')
216
216
  self.add_field_to_layout(
217
- layout, 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
217
+ layout, 'scratch_output_dir', FIELD_BOOL, 'Scratch output folder before run',
218
218
  required=False, default=True)
219
219
 
220
220
  def create_algorithm_tab(self, layout):
@@ -366,6 +366,14 @@ class FocusStackBunchConfigurator(FocusStackBaseConfigurator):
366
366
  self.add_field_to_layout(
367
367
  self.general_tab_layout, 'overlap', FIELD_INT, 'Overlapping frames', required=False,
368
368
  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
+ self.add_field_to_layout(
374
+ self.general_tab_layout, 'delete_output_at_end', FIELD_BOOL,
375
+ 'Delete output at end of job',
376
+ required=False, default=False)
369
377
  self.add_field_to_layout(
370
378
  self.general_tab_layout, 'plot_stack', FIELD_BOOL, 'Plot stack', required=False,
371
379
  default=constants.DEFAULT_PLOT_STACK_BUNCH)
@@ -391,7 +399,7 @@ class MultiLayerConfigurator(DefaultActionConfigurator):
391
399
  expert=True,
392
400
  placeholder='relative to working path')
393
401
  self.add_field(
394
- 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
402
+ 'scratch_output_dir', FIELD_BOOL, 'Scratch output folder before run',
395
403
  required=False, default=True)
396
404
  self.add_field(
397
405
  'reverse_order', FIELD_BOOL, 'Reverse file order', required=False,
@@ -413,8 +421,11 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
413
421
  expert=True,
414
422
  placeholder='relative to working path')
415
423
  self.add_field(
416
- 'scratch_output_dir', FIELD_BOOL, 'Scratch output dir.',
424
+ 'scratch_output_dir', FIELD_BOOL, 'Scratch output folder before run',
417
425
  required=False, default=True)
426
+ self.add_field(
427
+ 'delete_output_at_end', FIELD_BOOL, 'Delete output at end of job',
428
+ required=False, default=False)
418
429
  self.add_field(
419
430
  'plot_path', FIELD_REL_PATH, 'Plots path', required=False,
420
431
  expert=True,
@@ -490,22 +501,22 @@ class SubsampleActionConfigurator(DefaultActionConfigurator):
490
501
  self.subsample_field.currentText() not in constants.FIELD_SUBSAMPLE_OPTIONS[:2])
491
502
 
492
503
 
493
- class AlignFramesConfigurator(SubsampleActionConfigurator):
494
- BORDER_MODE_OPTIONS = ['Constant', 'Replicate', 'Replicate and blur']
495
- TRANSFORM_OPTIONS = ['Rigid', 'Homography']
496
- METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
504
+ class AlignFramesConfigBase:
497
505
  MATCHING_METHOD_OPTIONS = ['K-nearest neighbors', 'Hamming distance']
498
- MODE_OPTIONS = ['Auto', 'Sequential', 'Parallel']
506
+ DETECTOR_DESCRIPTOR_TOOLTIPS = {
507
+ 'detector':
508
+ "SIFT: Requires SIFT descriptor and K-NN matching\n"
509
+ "ORB/AKAZE: Work best with Hamming distance",
510
+ 'descriptor':
511
+ "SIFT: Requires K-NN matching\n"
512
+ "ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors",
513
+ 'match_method':
514
+ "Automatically selected based on detector/descriptor combination"
499
515
 
500
- def __init__(self, expert, current_wd):
501
- super().__init__(expert, current_wd)
502
- self.matching_method_field = None
516
+ }
517
+
518
+ def __init__(self):
503
519
  self.info_label = None
504
- self.detector_field = None
505
- self.descriptor_field = None
506
- self.matching_method_field = None
507
- self.tab_widget = None
508
- self.current_tab_layout = None
509
520
 
510
521
  def show_info(self, message, timeout=3000):
511
522
  self.info_label.setText(message)
@@ -514,32 +525,50 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
514
525
  timer.timeout.connect(lambda: self.info_label.setText(''))
515
526
  timer.start(timeout)
516
527
 
517
- def change_match_config(self):
518
- detector = self.detector_field.currentText()
519
- descriptor = self.descriptor_field.currentText()
528
+ def change_match_config(
529
+ self, detector_field, descriptor_field, matching_method_field, show_info):
530
+ detector = detector_field.currentText()
531
+ descriptor = descriptor_field.currentText()
520
532
  match_method = dict(
521
533
  zip(self.MATCHING_METHOD_OPTIONS,
522
- constants.VALID_MATCHING_METHODS))[self.matching_method_field.currentText()]
534
+ constants.VALID_MATCHING_METHODS))[matching_method_field.currentText()]
523
535
  try:
524
536
  validate_align_config(detector, descriptor, match_method)
525
537
  except Exception as e:
526
- self.show_info(str(e))
538
+ show_info(str(e))
527
539
  if descriptor == constants.DETECTOR_SIFT and \
528
540
  match_method == constants.MATCHING_NORM_HAMMING:
529
- self.matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[0])
541
+ matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[0])
530
542
  if detector == constants.DETECTOR_ORB and descriptor == constants.DESCRIPTOR_AKAZE and \
531
543
  match_method == constants.MATCHING_NORM_HAMMING:
532
- self.matching_method_field.setCurrentText(constants.MATCHING_NORM_HAMMING)
544
+ matching_method_field.setCurrentText(constants.MATCHING_NORM_HAMMING)
533
545
  if detector == constants.DETECTOR_BRISK and descriptor == constants.DESCRIPTOR_AKAZE:
534
- self.descriptor_field.setCurrentText('BRISK')
546
+ descriptor_field.setCurrentText('BRISK')
535
547
  if detector == constants.DETECTOR_SURF and descriptor == constants.DESCRIPTOR_AKAZE:
536
- self.descriptor_field.setCurrentText('SIFT')
548
+ descriptor_field.setCurrentText('SIFT')
537
549
  if detector == constants.DETECTOR_SIFT and descriptor != constants.DESCRIPTOR_SIFT:
538
- self.descriptor_field.setCurrentText('SIFT')
550
+ descriptor_field.setCurrentText('SIFT')
539
551
  if detector in constants.NOKNN_METHODS['detectors'] and \
540
552
  descriptor in constants.NOKNN_METHODS['descriptors']:
541
553
  if match_method == constants.MATCHING_KNN:
542
- self.matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[1])
554
+ matching_method_field.setCurrentText(self.MATCHING_METHOD_OPTIONS[1])
555
+
556
+
557
+ class AlignFramesConfigurator(SubsampleActionConfigurator, AlignFramesConfigBase):
558
+ BORDER_MODE_OPTIONS = ['Constant', 'Replicate', 'Replicate and blur']
559
+ TRANSFORM_OPTIONS = ['Rigid', 'Homography']
560
+ METHOD_OPTIONS = ['Random Sample Consensus (RANSAC)', 'Least Median (LMEDS)']
561
+ MODE_OPTIONS = ['Auto', 'Sequential', 'Parallel']
562
+
563
+ def __init__(self, expert, current_wd):
564
+ SubsampleActionConfigurator.__init__(self, expert, current_wd)
565
+ AlignFramesConfigBase.__init__(self)
566
+ self.matching_method_field = None
567
+ self.detector_field = None
568
+ self.descriptor_field = None
569
+ self.matching_method_field = None
570
+ self.tab_widget = None
571
+ self.current_tab_layout = None
543
572
 
544
573
  def create_form(self, layout, action):
545
574
  super().create_form(layout, action)
@@ -557,23 +586,23 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
557
586
  self.create_miscellanea_tab(misc_layout)
558
587
 
559
588
  def create_feature_tab(self, layout):
589
+
590
+ def change_match_config():
591
+ self.change_match_config(
592
+ self.detector_field, self.descriptor_field,
593
+ self. matching_method_field, self.show_info)
594
+
560
595
  self.add_bold_label_to_layout(layout, "Feature identification:")
561
596
  self.detector_field = self.add_field_to_layout(
562
597
  layout, 'detector', FIELD_COMBO, 'Detector', required=False,
563
- options=constants.VALID_DETECTORS, default=constants.DEFAULT_DETECTOR)
598
+ options=constants.VALID_DETECTORS, default=AppConfig.get('detector'))
564
599
  self.descriptor_field = self.add_field_to_layout(
565
600
  layout, 'descriptor', FIELD_COMBO, 'Descriptor', required=False,
566
- options=constants.VALID_DESCRIPTORS, default=constants.DEFAULT_DESCRIPTOR)
567
- self.detector_field.setToolTip(
568
- "SIFT: Requires SIFT descriptor and K-NN matching\n"
569
- "ORB/AKAZE: Work best with Hamming distance"
570
- )
571
- self.descriptor_field.setToolTip(
572
- "SIFT: Requires K-NN matching\n"
573
- "ORB/AKAZE: Require Hamming distance with ORB/AKAZE detectors"
574
- )
575
- self.detector_field.currentIndexChanged.connect(self.change_match_config)
576
- self.descriptor_field.currentIndexChanged.connect(self.change_match_config)
601
+ options=constants.VALID_DESCRIPTORS, default=AppConfig.get('descriptor'))
602
+ self.detector_field.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['detector'])
603
+ self.descriptor_field.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['descriptor'])
604
+ self.detector_field.currentIndexChanged.connect(change_match_config)
605
+ self.descriptor_field.currentIndexChanged.connect(change_match_config)
577
606
  self.info_label = QLabel()
578
607
  self.info_label.setStyleSheet("color: orange; font-style: italic;")
579
608
  layout.addRow(self.info_label)
@@ -581,11 +610,9 @@ class AlignFramesConfigurator(SubsampleActionConfigurator):
581
610
  self.matching_method_field = self.add_field_to_layout(
582
611
  layout, 'match_method', FIELD_COMBO, 'Match method', required=False,
583
612
  options=self.MATCHING_METHOD_OPTIONS, values=constants.VALID_MATCHING_METHODS,
584
- default=constants.DEFAULT_MATCHING_METHOD)
585
- self.matching_method_field.setToolTip(
586
- "Automatically selected based on detector/descriptor combination"
587
- )
588
- self.matching_method_field.currentIndexChanged.connect(self.change_match_config)
613
+ default=AppConfig.get('match_method'))
614
+ self.matching_method_field.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['match_method'])
615
+ self.matching_method_field.currentIndexChanged.connect(change_match_config)
589
616
  self.add_field_to_layout(
590
617
  layout, 'flann_idx_kdtree', FIELD_INT, 'Flann idx kdtree', required=False,
591
618
  expert=True,
@@ -215,8 +215,7 @@ class RunWindow(QTextEditLogger):
215
215
  raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
216
216
  self.image_views.append(image_view)
217
217
  self.image_layout.addWidget(image_view)
218
- max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
219
- needed_width = max_width + 20
218
+ needed_width = gui_constants.GUI_IMG_WIDTH + 20
220
219
  self.right_area.setFixedWidth(needed_width)
221
220
  self.image_area_widget.setFixedWidth(needed_width)
222
221
  self.right_area.updateGeometry()
@@ -3,9 +3,9 @@
3
3
  import os
4
4
  import subprocess
5
5
  from PySide6.QtCore import Qt
6
- from PySide6.QtGui import QGuiApplication, QAction, QIcon
6
+ from PySide6.QtGui import QGuiApplication, QAction, QPalette
7
7
  from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QMessageBox,
8
- QSplitter, QToolBar, QMenu, QMainWindow)
8
+ QSplitter, QToolBar, QMenu, QMainWindow, QApplication)
9
9
  from .. config.constants import constants
10
10
  from .. config.app_config import AppConfig
11
11
  from .. core.core_utils import running_under_windows, running_under_macos
@@ -82,7 +82,9 @@ class MainWindow(QMainWindow, LogManager):
82
82
  "Run Job": self.run_job,
83
83
  "Run All Jobs": self.run_all_jobs,
84
84
  }
85
- self.menu_manager = MenuManager(self.menuBar(), actions, self.project_editor, self)
85
+ dark_theme = self.is_dark_theme()
86
+ self.menu_manager = MenuManager(
87
+ self.menuBar(), actions, self.project_editor, dark_theme, self)
86
88
  self.script_dir = os.path.dirname(__file__)
87
89
  self._windows = []
88
90
  self._workers = []
@@ -107,7 +109,7 @@ class MainWindow(QMainWindow, LogManager):
107
109
  top_widget = QWidget()
108
110
  top_widget.setLayout(h_layout)
109
111
  h_splitter.addWidget(top_widget)
110
- self.tab_widget = TabWidgetWithPlaceholder()
112
+ self.tab_widget = TabWidgetWithPlaceholder(dark_theme)
111
113
  self.tab_widget.resize(1000, 500)
112
114
  h_splitter.addWidget(self.tab_widget)
113
115
  self.job_list().currentRowChanged.connect(self.project_editor.on_job_selected)
@@ -128,6 +130,7 @@ class MainWindow(QMainWindow, LogManager):
128
130
  layout.addWidget(h_splitter)
129
131
  self.central_widget.setLayout(layout)
130
132
  self.update_title()
133
+ QApplication.instance().paletteChanged.connect(self.on_theme_changed)
131
134
 
132
135
  def handle_modified(modified):
133
136
  self.save_actions_set_enabled(modified)
@@ -367,9 +370,6 @@ class MainWindow(QMainWindow, LogManager):
367
370
  menu.exec(event.globalPos())
368
371
  # pylint: enable=C0103
369
372
 
370
- def get_icon(self, icon):
371
- return QIcon(os.path.join(self.script_dir, f"img/{icon}.png"))
372
-
373
373
  def get_retouch_path(self, job):
374
374
  frames_path = [get_action_output_path(action)[0]
375
375
  for action in job.sub_actions
@@ -581,3 +581,16 @@ class MainWindow(QMainWindow, LogManager):
581
581
  for action in self.findChildren(QAction):
582
582
  if action.property("requires_file"):
583
583
  action.setEnabled(enabled)
584
+
585
+ def is_dark_theme(self):
586
+ palette = QApplication.palette()
587
+ window_color = palette.color(QPalette.Window)
588
+ brightness = (window_color.red() * 0.299 +
589
+ window_color.green() * 0.587 +
590
+ window_color.blue() * 0.114)
591
+ return brightness < 128
592
+
593
+ def on_theme_changed(self):
594
+ dark_theme = self.is_dark_theme()
595
+ self.menu_manager.change_theme(dark_theme)
596
+ self.tab_widget.change_theme(dark_theme)
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, R0904, E0611, R0902, W0201
1
+ # pylint: disable=C0114, C0115, C0116, R0904, E0611, R0902, W0201, R0913, R0917
2
2
  import os
3
3
  from functools import partial
4
4
  from PySide6.QtCore import Signal, QObject
@@ -12,11 +12,12 @@ from .recent_file_manager import RecentFileManager
12
12
  class MenuManager(QObject):
13
13
  open_file_requested = Signal(str)
14
14
 
15
- def __init__(self, menubar, actions, project_editor, parent):
15
+ def __init__(self, menubar, actions, project_editor, dark_theme, parent):
16
16
  super().__init__(parent)
17
17
  self.script_dir = os.path.dirname(__file__)
18
18
  self._recent_file_manager = RecentFileManager("shinestacker-recent-project-files.txt")
19
19
  self.project_editor = project_editor
20
+ self.dark_theme = dark_theme
20
21
  self.parent = parent
21
22
  self.menubar = menubar
22
23
  self.actions = actions
@@ -58,8 +59,9 @@ class MenuManager(QObject):
58
59
  "Run All Jobs": "Run all jobs",
59
60
  }
60
61
 
61
- def get_icon(self, icon):
62
- return QIcon(os.path.join(self.script_dir, f"img/{icon}.png"))
62
+ def get_icon(self, icon_name):
63
+ icon_dir = 'dark' if self.dark_theme else 'light'
64
+ return QIcon(os.path.join(self.script_dir, f"img/{icon_dir}/{icon_name}.png"))
63
65
 
64
66
  def action(self, name, requires_file=False):
65
67
  action = QAction(name, self.parent)
@@ -68,9 +70,11 @@ class MenuManager(QObject):
68
70
  shortcut = self.shortcuts.get(name, '')
69
71
  if shortcut:
70
72
  action.setShortcut(shortcut)
71
- icon = self.icons.get(name, '')
72
- if icon:
73
- action.setIcon(self.get_icon(icon))
73
+ icon_name = self.icons.get(name, '')
74
+ if icon_name:
75
+ action.setIcon(self.get_icon(icon_name))
76
+ action.setProperty('theme_dependent', True)
77
+ action.setProperty('base_icon_name', icon_name)
74
78
  tooltip = self.tooltips.get(name, '')
75
79
  if tooltip:
76
80
  action.setToolTip(tooltip)
@@ -79,6 +83,13 @@ class MenuManager(QObject):
79
83
  action.triggered.connect(action_fun)
80
84
  return action
81
85
 
86
+ def change_theme(self, dark_theme):
87
+ self.dark_theme = dark_theme
88
+ for action in self.parent.findChildren(QAction):
89
+ if action.property("theme_dependent"):
90
+ base_name = action.property("base_icon_name")
91
+ action.setIcon(self.get_icon(base_name))
92
+
82
93
  def update_recent_files(self):
83
94
  self.recent_files_menu.clear()
84
95
  recent_files = self._recent_file_manager.get_files_with_display_names()
@@ -169,13 +169,11 @@ class NewProjectDialog(BaseFormDialog):
169
169
  border-radius: 5px;
170
170
  margin-top: 10px;
171
171
  padding-top: 15px;
172
- background-color: #f8f8f8;
173
172
  }
174
173
  QGroupBox::title {
175
174
  subcontrol-origin: margin;
176
175
  left: 10px;
177
176
  padding: 0 5px 0 5px;
178
- background-color: #f8f8f8;
179
177
  }
180
178
  """
181
179
  for group in [step1_group, step2_group, step3_group, step4_group]:
@@ -9,8 +9,10 @@ class TabWidgetWithPlaceholder(QWidget):
9
9
  currentChanged = Signal(int)
10
10
  tabCloseRequested = Signal(int)
11
11
 
12
- def __init__(self, parent=None):
12
+ def __init__(self, dark_theme, parent=None):
13
13
  super().__init__(parent)
14
+ self.script_dir = os.path.dirname(__file__)
15
+ self.dark_theme = dark_theme
14
16
  self.main_layout = QVBoxLayout(self)
15
17
  self.main_layout.setContentsMargins(0, 0, 0, 0)
16
18
  self.stacked_widget = QStackedWidget()
@@ -19,7 +21,15 @@ class TabWidgetWithPlaceholder(QWidget):
19
21
  self.stacked_widget.addWidget(self.tab_widget)
20
22
  self.placeholder = QLabel()
21
23
  self.placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
22
- icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker_bkg.png"
24
+ self.set_bkg_icon()
25
+ self.stacked_widget.addWidget(self.placeholder)
26
+ self.tab_widget.currentChanged.connect(self._on_current_changed)
27
+ self.tab_widget.tabCloseRequested.connect(self._on_tab_close_requested)
28
+ self.update_placeholder_visibility()
29
+
30
+ def set_bkg_icon(self):
31
+ icon_dir = 'dark' if self.dark_theme else 'light'
32
+ icon_path = os.path.join(self.script_dir, f"img/{icon_dir}/shinestacker_bkg.png")
23
33
  if os.path.exists(icon_path):
24
34
  pixmap = QPixmap(icon_path)
25
35
  pixmap = pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio,
@@ -27,10 +37,10 @@ class TabWidgetWithPlaceholder(QWidget):
27
37
  self.placeholder.setPixmap(pixmap)
28
38
  else:
29
39
  self.placeholder.setText("Run logs will appear here.")
30
- self.stacked_widget.addWidget(self.placeholder)
31
- self.tab_widget.currentChanged.connect(self._on_current_changed)
32
- self.tab_widget.tabCloseRequested.connect(self._on_tab_close_requested)
33
- self.update_placeholder_visibility()
40
+
41
+ def change_theme(self, dark_theme):
42
+ self.dark_theme = dark_theme
43
+ self.set_bkg_icon()
34
44
 
35
45
  def _on_current_changed(self, index):
36
46
  self.currentChanged.emit(index)
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0917, R0902, R0914, E1101
2
+ from abc import abstractmethod
2
3
  import math
3
4
  import cv2
4
5
  from .base_filter import BaseFilter
@@ -24,6 +25,10 @@ class GammaSCurveFilter(BaseFilter):
24
25
  self.lumi_slider = None
25
26
  self.contrast_slider = None
26
27
 
28
+ @abstractmethod
29
+ def apply(self, image, *params):
30
+ pass
31
+
27
32
  def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
28
33
  dlg.setWindowTitle(self.window_title)
29
34
  dlg.setMinimumWidth(600)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.7.0
3
+ Version: 1.8.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -85,11 +85,11 @@ In order to prevent this, follow the instructions below:
85
85
  1. Download the compressed archive ```shinestacker-macos.tar.gz``` in your ```Download``` folder.
86
86
  2. Double-click the archive to uncompress it. You will find a new folder ```shinestacker```.
87
87
  3. Open a terminal (*Applications > Utilities > Terminal*)
88
- 4. Type the folliwng command on the terminal (assuming you have expanded the ```tar.gz``` under ```Downloads```):
88
+ 4. Type the folliwng command on the terminal (assuming you installed the app from the ```dmg``` image under ```Applications```):
89
89
  ```bash
90
- xattr -cr ~/Downloads/shinestacker/shinestacker.app
90
+ xattr -cr /Applications/shinestacker/shinestacker.app
91
91
  ```
92
- 5. Now you can double-click the Sine Stacker icon app in the ```shiestacker``` folder and it should run.
92
+ 5. Now you can double-click the Sine Stacker icon app and it should run.
93
93
 
94
94
  macOS adds a quarantine flag to all files downloaded from the internet. The above command removes that flag while preserving all other application functionality.
95
95
 
@@ -1,5 +1,5 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=ZUl2xcRi75lbPrlvl0NxpUt61vfrXEhe0YCcbPhhIX0,21
2
+ shinestacker/_version.py,sha256=xuQIDWcR8IgFZlyRjAXziglSj4bv_gjPpNiRgJlQSGc,21
3
3
  shinestacker/algorithms/__init__.py,sha256=1FwVJ3w9GGbFFkjYJRUedTvcdE4j0ieSgaH9RC9iCY4,877
4
4
  shinestacker/algorithms/align.py,sha256=mb44u-YxZI1TTSHz81nRpX_2c8awlOhnGrK0LyfTQeQ,33543
5
5
  shinestacker/algorithms/align_auto.py,sha256=pJetw6zZEWQLouzcelkI8gD4cPiOp887ePXzVbm0E6Q,3800
@@ -16,9 +16,9 @@ shinestacker/algorithms/pyramid.py,sha256=Z7tlp8Hh3ploAXJCr0VNe33d8H9GNrlqHXq_La
16
16
  shinestacker/algorithms/pyramid_auto.py,sha256=fl_jXNYLWsBiX0M0UghzCLqai0SGXlmKYHU7Z9SUYSo,6173
17
17
  shinestacker/algorithms/pyramid_tiles.py,sha256=ZBWIeifkDOIVFF4SCyspRZHSj6K_1P3dk4WLmuo53RU,12213
18
18
  shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
19
- shinestacker/algorithms/stack.py,sha256=p9bLCbMjDMm7rX_FRfgXRTEJAj3GhNPOCvwePE9hVmc,5549
20
- shinestacker/algorithms/stack_framework.py,sha256=OMrjD5dKquHQXhM7TfLRExDsqN1n938WWhGAfkPYLZM,13883
21
- shinestacker/algorithms/utils.py,sha256=l6GJpEXpzDr_ml9Not03a1_F7wYvPwn8JkEWDuNwL9o,12116
19
+ shinestacker/algorithms/stack.py,sha256=VvYo6w01sGbMIWS3w_4fVz6k7qOTpuEREXTP4kze6B0,5738
20
+ shinestacker/algorithms/stack_framework.py,sha256=x_IFYFgKn8Gz56BOhU7SwsT41WN0DDhITwtmvLaCVgw,14357
21
+ shinestacker/algorithms/utils.py,sha256=VJFOT-OBtDe5ds64VgwyZKa8AX1N91SSJlsgUVC9vMs,12303
22
22
  shinestacker/algorithms/vignetting.py,sha256=gJOv-FN3GnTgaVn70W_6d-qbw3WmqinDiO9oL053cus,10351
23
23
  shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
24
24
  shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -30,22 +30,22 @@ shinestacker/app/main.py,sha256=c9_rax1eemOfkJqWV7tT7-ZkEBkqz6n4kBST8iXsjD4,1066
30
30
  shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
31
31
  shinestacker/app/project.py,sha256=nwvXllD2FBLQ4ChePQdIGVug46Wh2ubjrJ0sC7klops,2596
32
32
  shinestacker/app/retouch.py,sha256=8XcYMv7-feG6yxNCpvlijZQRPlhmRK0OfZO5MuBju-0,2552
33
- shinestacker/app/settings_dialog.py,sha256=0P3nqqZEiTIFgidW1_e3Q_zE7NbAouNsuj-yNsU41vk,8192
33
+ shinestacker/app/settings_dialog.py,sha256=udeRsSuTtXNkFio8CVeshOK6g_8ZY-lKMkpJL0iA7-c,10500
34
34
  shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
35
35
  shinestacker/config/app_config.py,sha256=rM1Rndk1GDa5c0AhcVNEN9zSAzxPZixzQYfjODbJUwE,771
36
36
  shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
37
37
  shinestacker/config/constants.py,sha256=Z7QjaklrYYsPjTX68Tjyh_wCOuYyQPBR8dnYrZfNwA8,8376
38
38
  shinestacker/config/gui_constants.py,sha256=PNxzwmVEppJ2mV_vwp68NhWzJOEitVy1Pk9SwSmRsho,2882
39
- shinestacker/config/settings.py,sha256=4p4r6wKOCbttzfH9tyHQSTd-iv-GfgCd1LxI3C7WIjU,3861
39
+ shinestacker/config/settings.py,sha256=5_NuuSarJOEPcEH6QyZptvmVp16IzTftp8u0F_FXe8Y,4020
40
40
  shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
41
41
  shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
42
- shinestacker/core/core_utils.py,sha256=0x9iK9_iPQuj3BwF_QdWoxWTM-jyQytO57BvTQLdwmw,1378
42
+ shinestacker/core/core_utils.py,sha256=PYrV5asW-_L_AavPlk6p63UqzuQGrm55hwkRzmaaCd0,1424
43
43
  shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk2c,1618
44
- shinestacker/core/framework.py,sha256=QaTfnzEUHwzlbyFG7KzeyteckTSWHWEEJE4d5Tc8H18,11015
44
+ shinestacker/core/framework.py,sha256=ndSW7xWNDC2OYgaTv_34iVgD_mRXA0dQLSBuuB16REs,11113
45
45
  shinestacker/core/logging.py,sha256=pN4FGcHwI5ouJKwCVoDWQx_Tg3t84mmPh0xhqszDDkw,3111
46
46
  shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
47
  shinestacker/gui/action_config.py,sha256=Xv7SGbhPl1F_dUnU04VBt_E-wIItnN_q6QuhU_d9GfI,25929
48
- shinestacker/gui/action_config_dialog.py,sha256=QN95FiVPYL6uin2sYO5F7tq6G5rBWh9yRkeTVvwKrwU,38341
48
+ shinestacker/gui/action_config_dialog.py,sha256=mhxcV3loZWq1S7XBUneQHyORboDJbJyxVK-WjgLwj24,39503
49
49
  shinestacker/gui/base_form_dialog.py,sha256=KAUQNtmJazttmOIe4E4pFifbtvcByTAhtCmcIYeA4UE,766
50
50
  shinestacker/gui/colors.py,sha256=-HaFprDuzRSKjXoZfX1rdOuvawQAkazqdgLBEiZcFII,1476
51
51
  shinestacker/gui/config_dialog.py,sha256=yt3nvh0HPHQuCn3AFlzlIHUJnnxcz-Rrw3W3jS9ZYiE,3447
@@ -53,10 +53,10 @@ shinestacker/gui/flow_layout.py,sha256=3yBU_z7VtvHKpx1H97CHVd81eq9pe1Dcja2EZBGGK
53
53
  shinestacker/gui/folder_file_selection.py,sha256=IYWfZQFkoD5iO7zJ7BxVVDP9F3Dc0EXLILAhL4q-Cb8,4117
54
54
  shinestacker/gui/gui_images.py,sha256=k39DpdsxcmYoRdHNNZj6OpFAas0GOHS4JSG542wfheg,5728
55
55
  shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
56
- shinestacker/gui/gui_run.py,sha256=zr7x4BVmM0n_ZRsSEaJVVKvHSWHuwhftgkUvgeg90gU,15767
57
- shinestacker/gui/main_window.py,sha256=0G-ZjSVKY_rCK_DmstRn3wxOdvS5i3Ba3FBR2ijxpe0,25220
58
- shinestacker/gui/menu_manager.py,sha256=q4m3cBSxUR68gexpwfIROVRKJ86zp-XPZVrohh1PfQU,11786
59
- shinestacker/gui/new_project.py,sha256=XMv1ttYrkuqaN9629anXtVSn1bxosgyJpxSFPjlVryU,16437
56
+ shinestacker/gui/gui_run.py,sha256=38ke2Zq7KfQBZDNCzfw6RVIUdDTElLKf-tawBarlWyw,15684
57
+ shinestacker/gui/main_window.py,sha256=VYGX-w-A8sy1zsQAJEfLpImax8oB-inx_nZ2XofDEBQ,25777
58
+ shinestacker/gui/menu_manager.py,sha256=mS-pRMymd1yYimbr6Z5YXjMA5AsNuaNcezs8MYWF2DU,12364
59
+ shinestacker/gui/new_project.py,sha256=bbEKz0e5YsKzB920hTRD4s_Q5B39dQTT_Js7ygVWfeo,16351
60
60
  shinestacker/gui/project_controller.py,sha256=MYv8QJNXUdc7r1K5D6LnBbds9YalCKSAo_CaO6b0TO8,16636
61
61
  shinestacker/gui/project_converter.py,sha256=Gmna0HwbvACcXiX74TaQYumif8ZV8sZ2APLTMM-L1mU,7436
62
62
  shinestacker/gui/project_editor.py,sha256=lSgQ42IoaobHs-NQQWT88Qhg5l7nu5ejxAO5VgIupr8,25498
@@ -64,19 +64,24 @@ shinestacker/gui/project_model.py,sha256=9dId8N-np4YHDpz_wO20Mvd06np3YKlej-0TMWa
64
64
  shinestacker/gui/recent_file_manager.py,sha256=010bciuirKLiVCfOAKs0uFlB3iUjHNBlPX_9K2vH5j0,2916
65
65
  shinestacker/gui/select_path_widget.py,sha256=HSwgSr702w5Et4c-6nkRXnIpm1KFqKJetAF5xQNa5zI,1017
66
66
  shinestacker/gui/sys_mon.py,sha256=zU41YYVeqQ1-v6oGIh2_BFzQWq87keN-398Wdm59-Nk,3526
67
- shinestacker/gui/tab_widget.py,sha256=Hu01_neCxTOG9TMGSI-Ha1w4brp1bEXyvxS24Gi_JS8,2786
67
+ shinestacker/gui/tab_widget.py,sha256=rS4OCJMKjjE0yZxVEzohZo7wVlPnTt_BExbh3xWOlio,3122
68
68
  shinestacker/gui/time_progress_bar.py,sha256=7_sllrQgayjRh__mwJ0-4lghXIakuRAx8wWucJ6olYs,3028
69
69
  shinestacker/gui/ico/shinestacker.icns,sha256=lKmyIUBTjpMQ6Cajcov6WA5neAbZS9-JN5ca02nCz5I,204834
70
70
  shinestacker/gui/ico/shinestacker.ico,sha256=8IMRk-toObWUz8iDXA-zHBWQ8Ps3vXN5u5ZEyw7sP3c,109613
71
71
  shinestacker/gui/ico/shinestacker.png,sha256=VybGY-nig_-wMW8g_uImGxRYvcnltWcAVEPSX6AZUHM,22448
72
72
  shinestacker/gui/ico/shinestacker.svg,sha256=r8jx5aiIT9K70MRP0ANWniFE0ctRCqH7LMQ7vcGJ8ss,6571
73
- shinestacker/gui/ico/shinestacker_bkg.png,sha256=C91Ek4bm8hPcit4JUac8FAjRTQsHfiKIKPTZMweHKqo,10462
74
- shinestacker/gui/img/close-round-line-icon.png,sha256=9HZwCjgni1s_JGUPUb_MoOfoe4tRZgM5OWzk92XFZlE,8019
75
- shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzdsHvmDAjlbE18Pgo,4788
76
- shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
77
- shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
73
+ shinestacker/gui/img/dark/close-round-line-icon.png,sha256=kq3xxlLpig4t2yBJ_MaSN8zW11YyX7gHlUx6a1L7RV4,13819
74
+ shinestacker/gui/img/dark/forward-button-icon.png,sha256=o26xL4ep0W9Upe6oYoSR4pBZgkZU_eU2UGSczeWOkyM,8102
75
+ shinestacker/gui/img/dark/play-button-round-icon.png,sha256=jAaTZGMsvjWBKMXBANLMGRkQx_tUjBOiEX6oKTmhU5w,7745
76
+ shinestacker/gui/img/dark/plus-round-line-icon.png,sha256=S52m17SVjCxZJECSioDrTInSVgXgVoWYfHqMvlvy1zE,13369
77
+ shinestacker/gui/img/dark/shinestacker_bkg.png,sha256=0kgFDH8lF_ttyhILRJAOn71hbpU-rO4W2DYH05Ib_XY,9748
78
+ shinestacker/gui/img/light/close-round-line-icon.png,sha256=9HZwCjgni1s_JGUPUb_MoOfoe4tRZgM5OWzk92XFZlE,8019
79
+ shinestacker/gui/img/light/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzdsHvmDAjlbE18Pgo,4788
80
+ shinestacker/gui/img/light/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
81
+ shinestacker/gui/img/light/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
82
+ shinestacker/gui/img/light/shinestacker_bkg.png,sha256=C91Ek4bm8hPcit4JUac8FAjRTQsHfiKIKPTZMweHKqo,10462
78
83
  shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
- shinestacker/retouch/adjustments.py,sha256=tNroGN7zjr4SsMJpB-ciSHUUOWgUWai8CChWgxZpr6Q,3826
84
+ shinestacker/retouch/adjustments.py,sha256=LGbsUnPF5CJaJ5U_5Fmk5LoNGW0STttrFUcBjWN06As,3928
80
85
  shinestacker/retouch/base_filter.py,sha256=o_OkJbdD3jOGY--_sGL1_WqAMQI-QHGw-EEYxGhXOaQ,13976
81
86
  shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
82
87
  shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
@@ -104,9 +109,9 @@ shinestacker/retouch/unsharp_mask_filter.py,sha256=SO-6ZgPPDAO9em_MMefVvvSvt01-2
104
109
  shinestacker/retouch/view_strategy.py,sha256=jZxB_vX3_0notH0ClxKkLzbdtx4is3vQiYoIP-sDv3M,30216
105
110
  shinestacker/retouch/vignetting_filter.py,sha256=M7PZGPdVSq4bqo6wkEznrILMIG3-mTT7iwpgK4Hieyg,3794
106
111
  shinestacker/retouch/white_balance_filter.py,sha256=UaH4yxG3fU4vPutBAkV5oTXIQyUTN09x0uTywAzv3sY,8286
107
- shinestacker-1.7.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
108
- shinestacker-1.7.0.dist-info/METADATA,sha256=mAPgYAr3pS4P6hdAXRB-WBoYKXJZLRvADmX1CE0tDjA,7046
109
- shinestacker-1.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
110
- shinestacker-1.7.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
111
- shinestacker-1.7.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
112
- shinestacker-1.7.0.dist-info/RECORD,,
112
+ shinestacker-1.8.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
113
+ shinestacker-1.8.0.dist-info/METADATA,sha256=CMjh4hu6hIdcM1y3GNgnQQ9mAzWV_7PkpR6hBMpMe9c,7031
114
+ shinestacker-1.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
115
+ shinestacker-1.8.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
116
+ shinestacker-1.8.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
117
+ shinestacker-1.8.0.dist-info/RECORD,,