shinestacker 1.6.1__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 (45) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/corrections.py +26 -0
  3. shinestacker/algorithms/stack.py +9 -0
  4. shinestacker/algorithms/stack_framework.py +35 -16
  5. shinestacker/algorithms/utils.py +5 -1
  6. shinestacker/app/args_parser_opts.py +39 -0
  7. shinestacker/app/gui_utils.py +19 -2
  8. shinestacker/app/main.py +16 -27
  9. shinestacker/app/project.py +12 -23
  10. shinestacker/app/retouch.py +12 -25
  11. shinestacker/app/settings_dialog.py +46 -3
  12. shinestacker/config/settings.py +4 -1
  13. shinestacker/core/core_utils.py +2 -2
  14. shinestacker/core/framework.py +7 -2
  15. shinestacker/core/logging.py +2 -2
  16. shinestacker/gui/action_config_dialog.py +72 -45
  17. shinestacker/gui/gui_run.py +1 -2
  18. shinestacker/gui/ico/shinestacker.icns +0 -0
  19. shinestacker/gui/img/dark/close-round-line-icon.png +0 -0
  20. shinestacker/gui/img/dark/forward-button-icon.png +0 -0
  21. shinestacker/gui/img/dark/play-button-round-icon.png +0 -0
  22. shinestacker/gui/img/dark/plus-round-line-icon.png +0 -0
  23. shinestacker/gui/img/dark/shinestacker_bkg.png +0 -0
  24. shinestacker/gui/img/light/shinestacker_bkg.png +0 -0
  25. shinestacker/gui/main_window.py +20 -7
  26. shinestacker/gui/menu_manager.py +18 -7
  27. shinestacker/gui/new_project.py +0 -2
  28. shinestacker/gui/tab_widget.py +16 -10
  29. shinestacker/retouch/adjustments.py +98 -0
  30. shinestacker/retouch/base_filter.py +62 -7
  31. shinestacker/retouch/denoise_filter.py +1 -1
  32. shinestacker/retouch/image_editor_ui.py +26 -4
  33. shinestacker/retouch/unsharp_mask_filter.py +13 -28
  34. shinestacker/retouch/vignetting_filter.py +1 -1
  35. {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/METADATA +4 -4
  36. {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/RECORD +44 -37
  37. shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  38. /shinestacker/gui/img/{close-round-line-icon.png → light/close-round-line-icon.png} +0 -0
  39. /shinestacker/gui/img/{forward-button-icon.png → light/forward-button-icon.png} +0 -0
  40. /shinestacker/gui/img/{play-button-round-icon.png → light/play-button-round-icon.png} +0 -0
  41. /shinestacker/gui/img/{plus-round-line-icon.png → light/plus-round-line-icon.png} +0 -0
  42. {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/WHEEL +0 -0
  43. {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/entry_points.txt +0 -0
  44. {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/licenses/LICENSE +0 -0
  45. {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.6.1'
1
+ __version__ = '1.8.0'
@@ -0,0 +1,26 @@
1
+ # pylint: disable=C0114, C0115, C0116, E1101
2
+ import numpy as np
3
+ import cv2
4
+ from ..config.constants import constants
5
+
6
+
7
+ def gamma_correction(img, gamma):
8
+ max_px_val = constants.MAX_UINT8 if img.dtype == np.uint8 else constants.MAX_UINT16
9
+ ar = np.arange(0, max_px_val + 1, dtype=np.float64)
10
+ lut = (((ar / max_px_val) ** (1.0 / gamma)) * max_px_val).astype(img.dtype)
11
+ return cv2.LUT(img, lut) if img.dtype == np.uint8 else np.take(lut, img)
12
+
13
+
14
+ def contrast_correction(img, k):
15
+ max_px_val = constants.MAX_UINT8 if img.dtype == np.uint8 else constants.MAX_UINT16
16
+ ar = np.arange(0, max_px_val + 1, dtype=np.float64)
17
+ x = 2.0 * (ar / max_px_val) - 1.0
18
+ # f(x) = x * exp(k) / (1 + (exp(k) - 1)|x|), -1 < x < +1
19
+ # note that: f(f(x, k), -k) = x
20
+ exp_k = np.exp(k)
21
+ numerator = x * exp_k
22
+ denominator = 1 + (exp_k - 1) * np.abs(x)
23
+ corrected = numerator / denominator
24
+ corrected = (corrected + 1.0) * 0.5 * max_px_val
25
+ lut = np.clip(corrected, 0, max_px_val).astype(img.dtype)
26
+ return cv2.LUT(img, lut) if img.dtype == np.uint8 else np.take(lut, img)
@@ -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,4 +1,6 @@
1
1
  # pylint: disable=C0114, C0116
2
+ import sys
3
+
2
4
 
3
5
  def add_project_arguments(parser):
4
6
  parser.add_argument('-x', '--expert', action='store_true', help='''
@@ -25,3 +27,40 @@ set side-by-side view.
25
27
  view_group.add_argument('-v3', '--view-top-bottom', action='store_true', help='''
26
28
  set top-bottom view.
27
29
  ''')
30
+
31
+
32
+ def extract_positional_filename():
33
+ positional_filename = None
34
+ filtered_args = []
35
+ for arg in sys.argv[1:]:
36
+ if not arg.startswith('-') and not positional_filename:
37
+ positional_filename = arg
38
+ else:
39
+ filtered_args.append(arg)
40
+ return positional_filename, filtered_args
41
+
42
+
43
+ def setup_filename_argument(parser, use_const=True):
44
+ if use_const:
45
+ parser.add_argument('-f', '--filename', nargs='?', const=True, help='''
46
+ filename to open. Can be a project file or image file.
47
+ Multiple files can be specified separated by ';'.
48
+ ''')
49
+ else:
50
+ parser.add_argument('-f', '--filename', nargs='?', help='''
51
+ filename to open. Can be a project file or image file.
52
+ Multiple files can be specified separated by ';'.
53
+ ''')
54
+
55
+
56
+ def process_filename_argument(args, positional_filename):
57
+ filename = args.get('filename')
58
+ if positional_filename and not filename:
59
+ filename = positional_filename
60
+ if filename is True:
61
+ if positional_filename:
62
+ filename = positional_filename
63
+ else:
64
+ print("Error: -f flag used but no filename provided", file=sys.stderr)
65
+ sys.exit(1)
66
+ return filename
@@ -1,12 +1,15 @@
1
1
  # pylint: disable=C0114, C0116, E0611, R0913, R0917
2
2
  import os
3
3
  import sys
4
- from PySide6.QtCore import QCoreApplication, QProcess
5
- from PySide6.QtGui import QAction
4
+ import logging
5
+ from PySide6.QtCore import Qt, QCoreApplication, QProcess
6
+ from PySide6.QtGui import QAction, QIcon
6
7
  from shinestacker.config.constants import constants
7
8
  from shinestacker.config.config import config
9
+ from shinestacker.config.settings import StdPathFile
8
10
  from shinestacker.app.about_dialog import show_about_dialog
9
11
  from shinestacker.app.settings_dialog import show_settings_dialog
12
+ from shinestacker.core.logging import setup_logging
10
13
 
11
14
 
12
15
  def disable_macos_special_menu_items():
@@ -71,3 +74,17 @@ def set_css_style(app):
71
74
  }
72
75
  """
73
76
  app.setStyleSheet(css_style)
77
+
78
+
79
+ def make_app(application_class):
80
+ setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
81
+ log_file=StdPathFile('shinestacker.log').get_file_path())
82
+ app = application_class(sys.argv)
83
+ if config.DONT_USE_NATIVE_MENU:
84
+ app.setAttribute(Qt.AA_DontUseNativeMenuBar)
85
+ else:
86
+ disable_macos_special_menu_items()
87
+ icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
88
+ app.setWindowIcon(QIcon(icon_path))
89
+ set_css_style(app)
90
+ return app
shinestacker/app/main.py CHANGED
@@ -1,27 +1,26 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201, R0915, R0912
2
2
  import sys
3
- import os
4
- import logging
5
3
  import argparse
6
4
  import matplotlib
7
5
  import matplotlib.backends.backend_pdf
8
6
  matplotlib.use('agg')
9
7
  from PySide6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QStackedWidget,
10
8
  QMenu, QMessageBox, QDialog, QLabel, QListWidget, QPushButton)
11
- from PySide6.QtGui import QAction, QIcon, QGuiApplication
12
- from PySide6.QtCore import Qt, QEvent, QTimer, Signal
9
+ from PySide6.QtGui import QAction, QGuiApplication
10
+ from PySide6.QtCore import QEvent, QTimer, Signal
13
11
  from shinestacker.config.config import config
14
12
  config.init(DISABLE_TQDM=True, COMBINED_APP=True, DONT_USE_NATIVE_MENU=True)
15
13
  from shinestacker.config.constants import constants
16
- from shinestacker.config.settings import StdPathFile
17
- from shinestacker.core.logging import setup_logging
18
14
  from shinestacker.gui.main_window import MainWindow
19
15
  from shinestacker.retouch.image_editor_ui import ImageEditorUI
20
- from shinestacker.app.gui_utils import (
21
- disable_macos_special_menu_items, fill_app_menu, set_css_style)
16
+ from shinestacker.app.gui_utils import fill_app_menu
22
17
  from shinestacker.app.help_menu import add_help_action
23
18
  from shinestacker.app.open_frames import open_frames
24
- from shinestacker.app.args_parser_opts import add_project_arguments, add_retouch_arguments
19
+ from shinestacker.app.args_parser_opts import (
20
+ add_project_arguments, add_retouch_arguments, extract_positional_filename,
21
+ setup_filename_argument, process_filename_argument
22
+ )
23
+ from shinestacker.app.gui_utils import make_app
25
24
 
26
25
 
27
26
  class SelectionDialog(QDialog):
@@ -206,15 +205,12 @@ class Application(QApplication):
206
205
 
207
206
 
208
207
  def main():
208
+ positional_filename, filtered_args = extract_positional_filename()
209
209
  parser = argparse.ArgumentParser(
210
- prog=f'{constants.APP_STRING.lower()}-retouch',
210
+ prog=f'{constants.APP_STRING.lower()}',
211
211
  description='Focus stacking App.',
212
212
  epilog=f'This app is part of the {constants.APP_STRING} package.')
213
- parser.add_argument('-f', '--filename', nargs='?', help='''
214
- if a single file is specified, it can be either a project or an image.
215
- Multiple frames can be specified as a list of files.
216
- Multiple files can be specified separated by ';'.
217
- ''')
213
+ setup_filename_argument(parser, use_const=True)
218
214
  app_group = parser.add_mutually_exclusive_group()
219
215
  app_group.add_argument('-j', '--project', action='store_true', help='''
220
216
  open project window at startup instead of project windows (default).
@@ -224,24 +220,17 @@ open retouch window at startup instead of project windows.
224
220
  ''')
225
221
  add_project_arguments(parser)
226
222
  add_retouch_arguments(parser)
227
- args = vars(parser.parse_args(sys.argv[1:]))
228
- filename = args['filename']
223
+ args = vars(parser.parse_args(filtered_args))
224
+ filename = process_filename_argument(args, positional_filename)
229
225
  path = args['path']
230
226
  if filename and path:
231
227
  print("can't specify both arguments --filename and --path", file=sys.stderr)
232
228
  sys.exit(1)
233
- setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
234
- log_file=StdPathFile('shinestacker.log').get_file_path())
235
- app = Application(sys.argv)
236
- if config.DONT_USE_NATIVE_MENU:
237
- app.setAttribute(Qt.AA_DontUseNativeMenuBar)
238
- else:
239
- disable_macos_special_menu_items()
240
- icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
241
- app.setWindowIcon(QIcon(icon_path))
229
+
230
+ app = make_app(Application)
242
231
  main_app = MainApp()
243
232
  app.main_app = main_app
244
- set_css_style(app)
233
+
245
234
  main_app.show()
246
235
  main_app.activateWindow()
247
236
  if args['expert']:
@@ -1,24 +1,23 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201
2
2
  import os
3
3
  import sys
4
- import logging
5
4
  import argparse
6
5
  import matplotlib
7
6
  import matplotlib.backends.backend_pdf
8
7
  matplotlib.use('agg')
9
8
  from PySide6.QtWidgets import QApplication, QMenu
10
- from PySide6.QtGui import QIcon
11
- from PySide6.QtCore import Qt, QTimer, QEvent
9
+ from PySide6.QtCore import QTimer, QEvent
12
10
  from shinestacker.config.config import config
13
11
  config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
14
12
  from shinestacker.config.constants import constants
15
- from shinestacker.config.settings import StdPathFile
16
- from shinestacker.core.logging import setup_logging
17
13
  from shinestacker.gui.main_window import MainWindow
18
- from shinestacker.app.gui_utils import (
19
- disable_macos_special_menu_items, fill_app_menu, set_css_style)
14
+ from shinestacker.app.gui_utils import fill_app_menu
20
15
  from shinestacker.app.help_menu import add_help_action
21
- from shinestacker.app.args_parser_opts import add_project_arguments
16
+ from shinestacker.app.args_parser_opts import (
17
+ add_project_arguments, extract_positional_filename,
18
+ setup_filename_argument, process_filename_argument
19
+ )
20
+ from shinestacker.app.gui_utils import make_app
22
21
 
23
22
 
24
23
  class ProjectApp(MainWindow):
@@ -49,31 +48,21 @@ class Application(QApplication):
49
48
 
50
49
 
51
50
  def main():
51
+ positional_filename, filtered_args = extract_positional_filename()
52
52
  parser = argparse.ArgumentParser(
53
53
  prog=f'{constants.APP_STRING.lower()}-project',
54
54
  description='Manage and run focus stack jobs.',
55
55
  epilog=f'This app is part of the {constants.APP_STRING} package.')
56
- parser.add_argument('-f', '--filename', nargs='?', help='''
57
- project filename.
58
- ''')
56
+ setup_filename_argument(parser, use_const=True)
59
57
  add_project_arguments(parser)
60
- args = vars(parser.parse_args(sys.argv[1:]))
61
- setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
62
- log_file=StdPathFile('shinestacker.log').get_file_path())
63
- app = Application(sys.argv)
64
- if config.DONT_USE_NATIVE_MENU:
65
- app.setAttribute(Qt.AA_DontUseNativeMenuBar)
66
- else:
67
- disable_macos_special_menu_items()
68
- icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
69
- app.setWindowIcon(QIcon(icon_path))
70
- set_css_style(app)
58
+ args = vars(parser.parse_args(filtered_args))
59
+ filename = process_filename_argument(args, positional_filename)
60
+ app = make_app(Application)
71
61
  window = ProjectApp()
72
62
  if args['expert']:
73
63
  window.set_expert_options()
74
64
  app.window = window
75
65
  window.show()
76
- filename = args['filename']
77
66
  if filename:
78
67
  QTimer.singleShot(100, lambda: window.project_controller.open_project(filename))
79
68
  elif args['new-project']:
@@ -1,22 +1,20 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201
2
- import os
3
2
  import sys
4
- import logging
5
3
  import argparse
6
4
  from PySide6.QtWidgets import QApplication, QMenu
7
- from PySide6.QtGui import QIcon
8
- from PySide6.QtCore import Qt, QEvent
5
+ from PySide6.QtCore import QEvent
9
6
  from shinestacker.config.config import config
10
7
  config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
11
8
  from shinestacker.config.constants import constants
12
- from shinestacker.config.settings import StdPathFile
13
- from shinestacker.core.logging import setup_logging
14
9
  from shinestacker.retouch.image_editor_ui import ImageEditorUI
15
- from shinestacker.app.gui_utils import (
16
- disable_macos_special_menu_items, fill_app_menu, set_css_style)
10
+ from shinestacker.app.gui_utils import fill_app_menu
17
11
  from shinestacker.app.help_menu import add_help_action
18
12
  from shinestacker.app.open_frames import open_frames
19
- from shinestacker.app.args_parser_opts import add_retouch_arguments
13
+ from shinestacker.app.args_parser_opts import (
14
+ add_retouch_arguments, extract_positional_filename,
15
+ setup_filename_argument, process_filename_argument
16
+ )
17
+ from shinestacker.app.gui_utils import make_app
20
18
 
21
19
 
22
20
  class RetouchApp(ImageEditorUI):
@@ -42,31 +40,20 @@ class Application(QApplication):
42
40
 
43
41
 
44
42
  def main():
43
+ positional_filename, filtered_args = extract_positional_filename()
45
44
  parser = argparse.ArgumentParser(
46
45
  prog=f'{constants.APP_STRING.lower()}-retouch',
47
46
  description='Final retouch focus stack image from individual frames.',
48
47
  epilog=f'This app is part of the {constants.APP_STRING} package.')
49
- parser.add_argument('-f', '--filename', nargs='?', help='''
50
- import frames from files.
51
- Multiple files can be specified separated by ';'.
52
- ''')
48
+ setup_filename_argument(parser, use_const=True)
53
49
  add_retouch_arguments(parser)
54
- args = vars(parser.parse_args(sys.argv[1:]))
55
- filename = args['filename']
50
+ args = vars(parser.parse_args(filtered_args))
51
+ filename = process_filename_argument(args, positional_filename)
56
52
  path = args['path']
57
53
  if filename and path:
58
54
  print("can't specify both arguments --filename and --path", file=sys.stderr)
59
55
  sys.exit(1)
60
- setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True,
61
- log_file=StdPathFile('shinestacker.log').get_file_path())
62
- app = Application(sys.argv)
63
- if config.DONT_USE_NATIVE_MENU:
64
- app.setAttribute(Qt.AA_DontUseNativeMenuBar)
65
- else:
66
- disable_macos_special_menu_items()
67
- icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
68
- app.setWindowIcon(QIcon(icon_path))
69
- set_css_style(app)
56
+ app = make_app(Application)
70
57
  editor = RetouchApp()
71
58
  app.editor = editor
72
59
  editor.show()
@@ -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
@@ -28,8 +28,7 @@ def make_tqdm_bar(name, size, ncols=80):
28
28
  def get_app_base_path():
29
29
  if getattr(sys, 'frozen', False):
30
30
  return os.path.dirname(os.path.abspath(sys.executable))
31
- else:
32
- return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
31
+ return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
33
32
 
34
33
 
35
34
  def running_under_windows() -> bool:
@@ -53,4 +52,5 @@ def setup_matplotlib_mode():
53
52
  __IPYTHON__ # noqa
54
53
  except Exception:
55
54
  matplotlib.use('agg')
55
+ matplotlib.pyplot.set_loglevel("warning")
56
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
@@ -64,9 +64,9 @@ def setup_logging(console_level=logging.INFO, file_level=logging.DEBUG, log_file
64
64
  if log_file is not None:
65
65
  if log_file == '':
66
66
  today = datetime.date.today().strftime("%Y-%m-%d")
67
- log_file = f"logs/{constants.APP_STRING.lower()}-{today}.log"
67
+ log_file = os.path.join('logs', f"{constants.APP_STRING.lower()}-{today}.log")
68
68
  if not os.path.isabs(log_file):
69
- log_file = os.path.join(get_app_base_path(), {log_file})
69
+ log_file = os.path.join(get_app_base_path(), log_file)
70
70
  Path(log_file).parent.mkdir(parents=True, exist_ok=True)
71
71
  file_handler = logging.FileHandler(log_file)
72
72
  file_handler.setLevel(file_level)