shinestacker 1.6.1__py3-none-any.whl → 1.7.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.

shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.6.1'
1
+ __version__ = '1.7.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)
@@ -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()
@@ -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:
@@ -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)
Binary file
@@ -3,7 +3,6 @@ import os
3
3
  from PySide6.QtCore import Qt, Signal
4
4
  from PySide6.QtGui import QPixmap
5
5
  from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QLabel, QStackedWidget
6
- from .. core.core_utils import get_app_base_path
7
6
 
8
7
 
9
8
  class TabWidgetWithPlaceholder(QWidget):
@@ -20,10 +19,7 @@ class TabWidgetWithPlaceholder(QWidget):
20
19
  self.stacked_widget.addWidget(self.tab_widget)
21
20
  self.placeholder = QLabel()
22
21
  self.placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
23
- rel_path = 'ico/focus_stack_bkg.png'
24
- icon_path = f'{get_app_base_path()}/{rel_path}'
25
- if not os.path.exists(icon_path):
26
- icon_path = f'{get_app_base_path()}/../{rel_path}'
22
+ icon_path = f"{os.path.dirname(__file__)}/ico/shinestacker_bkg.png"
27
23
  if os.path.exists(icon_path):
28
24
  pixmap = QPixmap(icon_path)
29
25
  pixmap = pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio,
@@ -0,0 +1,93 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0913, R0917, R0902, R0914, E1101
2
+ import math
3
+ import cv2
4
+ from .base_filter import BaseFilter
5
+ from .. algorithms.utils import bgr_to_hls, hls_to_bgr
6
+ from .. algorithms.corrections import gamma_correction, contrast_correction
7
+
8
+
9
+ class GammaSCurveFilter(BaseFilter):
10
+ def __init__(
11
+ self, name, parent, image_viewer, layer_collection, undo_manager,
12
+ window_title, gamma_label, scurve_label):
13
+ super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
14
+ preview_at_startup=True)
15
+ self.window_title = window_title
16
+ self.gamma_label = gamma_label
17
+ self.scurve_label = scurve_label
18
+ self.min_gamma = -1
19
+ self.max_gamma = +1
20
+ self.initial_gamma = 0
21
+ self.min_scurve = -1
22
+ self.max_scurve = 1
23
+ self.initial_scurve = 0
24
+ self.lumi_slider = None
25
+ self.contrast_slider = None
26
+
27
+ def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
28
+ dlg.setWindowTitle(self.window_title)
29
+ dlg.setMinimumWidth(600)
30
+ params = {
31
+ self.gamma_label: (self.min_gamma, self.max_gamma, self.initial_gamma, "{:.1%}"),
32
+ self.scurve_label: (self.min_scurve, self.max_scurve, self.initial_scurve, "{:.1%}"),
33
+ }
34
+
35
+ def set_slider(name, slider):
36
+ if name == self.gamma_label:
37
+ self.lumi_slider = slider
38
+ elif name == self.scurve_label:
39
+ self.contrast_slider = slider
40
+
41
+ value_labels = self.create_sliders(params, dlg, layout, set_slider)
42
+
43
+ def update_value(name, slider_value, min_val, max_val, fmt):
44
+ value = self.value_from_slider(slider_value, min_val, max_val)
45
+ value_labels[name].setText(fmt.format(value))
46
+ if self.preview_check.isChecked():
47
+ self.preview_timer.start()
48
+
49
+ self.lumi_slider.valueChanged.connect(
50
+ lambda v: update_value(
51
+ self.gamma_label, v, self.min_gamma,
52
+ self.max_gamma, params[self.gamma_label][3]))
53
+ self.contrast_slider.valueChanged.connect(
54
+ lambda v: update_value(
55
+ self.scurve_label, v, self.min_scurve,
56
+ self.max_scurve, params[self.scurve_label][3]))
57
+ self.set_timer(do_preview, restore_original, dlg)
58
+
59
+ def get_params(self):
60
+ return (
61
+ self.value_from_slider(
62
+ self.lumi_slider.value(), self.min_gamma, self.max_gamma),
63
+ self.value_from_slider(
64
+ self.contrast_slider.value(), self.min_scurve, self.max_scurve)
65
+ )
66
+
67
+
68
+ class LumiContrastFilter(GammaSCurveFilter):
69
+ def __init__(self, name, parent, image_viewer, layer_collection, undo_manager):
70
+ super().__init__(
71
+ name, parent, image_viewer, layer_collection, undo_manager,
72
+ "Luminosity, Contrast", "Luminosity", "Constrat")
73
+
74
+ def apply(self, image, luminosity, contrast):
75
+ img_corr = contrast_correction(image, 0.5 * contrast)
76
+ img_corr = gamma_correction(img_corr, math.exp(0.5 * luminosity))
77
+ return img_corr
78
+
79
+
80
+ class SaturationVibranceFilter(GammaSCurveFilter):
81
+ def __init__(self, name, parent, image_viewer, layer_collection, undo_manager):
82
+ super().__init__(
83
+ name, parent, image_viewer, layer_collection, undo_manager,
84
+ "Saturation, Vibrance", "Saturation", "Vibrance")
85
+
86
+ def apply(self, image, stauration, vibrance):
87
+ img_corr = bgr_to_hls(image)
88
+ h, l, s = cv2.split(img_corr)
89
+ s_corr = contrast_correction(s, - vibrance)
90
+ s_corr = gamma_correction(s_corr, math.exp(0.5 * stauration))
91
+ img_corr = cv2.merge([h, l, s_corr])
92
+ img_corr = hls_to_bgr(img_corr)
93
+ return img_corr
@@ -3,6 +3,7 @@ import traceback
3
3
  from abc import abstractmethod
4
4
  import numpy as np
5
5
  from PySide6.QtCore import Qt, QThread, QTimer, QObject, Signal
6
+ from PySide6.QtGui import QFontMetrics
6
7
  from PySide6.QtWidgets import (
7
8
  QHBoxLayout, QLabel, QSlider, QDialog, QVBoxLayout, QCheckBox, QDialogButtonBox)
8
9
  from .layer_collection import LayerCollectionHandler
@@ -27,6 +28,7 @@ class BaseFilter(QObject, LayerCollectionHandler):
27
28
  self.preview_check = None
28
29
  self.button_box = None
29
30
  self.preview_timer = None
31
+ self.max_range = 500
30
32
 
31
33
  @abstractmethod
32
34
  def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
@@ -40,6 +42,47 @@ class BaseFilter(QObject, LayerCollectionHandler):
40
42
  def apply(self, image, *params):
41
43
  pass
42
44
 
45
+ def slider_from_value(self, value, min_val, max_val):
46
+ return (value - min_val) / (max_val - min_val) * self.max_range
47
+
48
+ def value_from_slider(self, slider_value, min_val, max_val):
49
+ return min_val + (max_val - min_val) * float(slider_value) / self.max_range
50
+
51
+ def create_sliders(self, params, dlg, layout, set_slider):
52
+ value_labels = {}
53
+ font_metrics = QFontMetrics(dlg.font())
54
+ max_name_width = 0
55
+ max_value_width = 0
56
+ for name, (min_val, max_val, init_val, fmt) in params.items():
57
+ name_width = font_metrics.horizontalAdvance(f"{name}:")
58
+ max_name_width = max(max_name_width, name_width)
59
+ sample_values = [min_val, max_val, init_val]
60
+ for val in sample_values:
61
+ value_width = font_metrics.horizontalAdvance(fmt.format(val))
62
+ max_value_width = max(max_value_width, value_width)
63
+ max_name_width += 10
64
+ max_value_width += 10
65
+ for name, (min_val, max_val, init_val, fmt) in params.items():
66
+ param_layout = QHBoxLayout()
67
+ name_label = QLabel(f"{name}:")
68
+ name_label.setFixedWidth(max_name_width)
69
+ name_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
70
+ param_layout.addWidget(name_label)
71
+ slider = QSlider(Qt.Horizontal)
72
+ slider.setRange(0, self.max_range)
73
+ slider.setValue(self.slider_from_value(init_val, min_val, max_val))
74
+ param_layout.addWidget(slider, 1)
75
+ value_label = QLabel(fmt.format(init_val))
76
+ value_label.setFixedWidth(max_value_width)
77
+ value_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
78
+ param_layout.addWidget(value_label)
79
+ layout.addLayout(param_layout)
80
+ set_slider(name, slider)
81
+ value_labels[name] = value_label
82
+ self.create_base_widgets(
83
+ layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200, dlg)
84
+ return value_labels
85
+
43
86
  def connect_signals(self, update_master_thumbnail, mark_as_modified, filter_gui_set_enabled):
44
87
  self.update_master_thumbnail_requested.connect(update_master_thumbnail)
45
88
  self.mark_as_modified_requested.connect(mark_as_modified)
@@ -191,6 +234,13 @@ class BaseFilter(QObject, LayerCollectionHandler):
191
234
  self.preview_timer.setSingleShot(True)
192
235
  self.preview_timer.setInterval(preview_latency)
193
236
 
237
+ def set_timer(self, do_preview, restore_original, dlg):
238
+ self.preview_timer.timeout.connect(do_preview)
239
+ self.connect_preview_toggle(self.preview_check, do_preview, restore_original)
240
+ self.button_box.accepted.connect(dlg.accept)
241
+ self.button_box.rejected.connect(dlg.reject)
242
+ QTimer.singleShot(0, do_preview)
243
+
194
244
  class PreviewWorker(QThread):
195
245
  finished = Signal(np.ndarray, int, tuple)
196
246
 
@@ -222,14 +272,14 @@ class BaseFilter(QObject, LayerCollectionHandler):
222
272
 
223
273
  class OneSliderBaseFilter(BaseFilter):
224
274
  def __init__(self, name, parent, image_viewer, layer_collection, undo_manager,
225
- max_value, initial_value, title,
275
+ min_value, max_value, initial_value, title,
226
276
  allow_partial_preview=True, partial_preview_threshold=0.5,
227
277
  preview_at_startup=True):
228
278
  super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
229
279
  allow_partial_preview,
230
280
  partial_preview_threshold, preview_at_startup)
231
- self.max_range = 500
232
281
  self.max_value = max_value
282
+ self.min_value = min_value
233
283
  self.initial_value = initial_value
234
284
  self.slider = None
235
285
  self.value_label = None
@@ -239,6 +289,13 @@ class OneSliderBaseFilter(BaseFilter):
239
289
  def add_widgets(self, layout, dlg):
240
290
  pass
241
291
 
292
+ def slider_from_value_1(self, value):
293
+ return int((value - self.min_value) / (self.max_value - self.min_value) * self.max_range)
294
+
295
+ def value_from_slider_1(self, slider_value):
296
+ return self.min_value + \
297
+ (self.max_value - self.min_value) * float(slider_value) / self.max_range
298
+
242
299
  def setup_ui(self, dlg, layout, do_preview, restore_original, **kwargs):
243
300
  dlg.setWindowTitle(self.title)
244
301
  dlg.setMinimumWidth(600)
@@ -246,7 +303,7 @@ class OneSliderBaseFilter(BaseFilter):
246
303
  slider_layout.addWidget(QLabel("Amount:"))
247
304
  slider_local = QSlider(Qt.Horizontal)
248
305
  slider_local.setRange(0, self.max_range)
249
- slider_local.setValue(int(self.initial_value / self.max_value * self.max_range))
306
+ slider_local.setValue(self.slider_from_value_1(self.initial_value))
250
307
  slider_layout.addWidget(slider_local)
251
308
  self.value_label = QLabel(self.format.format(self.initial_value))
252
309
  slider_layout.addWidget(self.value_label)
@@ -254,9 +311,7 @@ class OneSliderBaseFilter(BaseFilter):
254
311
  self.add_widgets(layout, dlg)
255
312
  self.create_base_widgets(
256
313
  layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200, dlg)
257
-
258
314
  self.preview_timer.timeout.connect(do_preview)
259
-
260
315
  slider_local.valueChanged.connect(self.config_changed)
261
316
  self.connect_preview_toggle(
262
317
  self.preview_check, self.do_preview_delayed, restore_original)
@@ -269,7 +324,7 @@ class OneSliderBaseFilter(BaseFilter):
269
324
  self.do_preview_delayed()
270
325
 
271
326
  def config_changed(self, val):
272
- float_val = self.max_value * float(val) / self.max_range
327
+ float_val = self.value_from_slider_1(val)
273
328
  self.value_label.setText(self.format.format(float_val))
274
329
  self.param_changed(val)
275
330
 
@@ -277,7 +332,7 @@ class OneSliderBaseFilter(BaseFilter):
277
332
  self.preview_timer.start()
278
333
 
279
334
  def get_params(self):
280
- return (self.max_value * self.slider.value() / self.max_range,)
335
+ return (self.value_from_slider_1(self.slider.value()),)
281
336
 
282
337
  def apply(self, image, *params):
283
338
  assert False
@@ -6,7 +6,7 @@ from .. algorithms.denoise import denoise
6
6
  class DenoiseFilter(OneSliderBaseFilter):
7
7
  def __init__(self, name, parent, image_viewer, layer_collection, undo_manager):
8
8
  super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
9
- 10.0, 2.5, "Denoise",
9
+ 0.0, 10.0, 2.5, "Denoise",
10
10
  allow_partial_preview=True, preview_at_startup=False)
11
11
 
12
12
  def apply(self, image, strength):
@@ -24,6 +24,7 @@ from .denoise_filter import DenoiseFilter
24
24
  from .unsharp_mask_filter import UnsharpMaskFilter
25
25
  from .white_balance_filter import WhiteBalanceFilter
26
26
  from .vignetting_filter import VignettingFilter
27
+ from .adjustments import LumiContrastFilter, SaturationVibranceFilter
27
28
  from .transformation_manager import TransfromationManager
28
29
 
29
30
 
@@ -296,6 +297,21 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
296
297
  transf_menu.addAction(rotate_180_action)
297
298
  edit_menu.addMenu(transf_menu)
298
299
 
300
+ adjust_menu = QMenu("&Adjust")
301
+ luminosity_action = QAction("Luminosity, Contrast", self)
302
+ luminosity_action.setProperty("requires_file", True)
303
+ luminosity_action.triggered.connect(self.luminosity_filter)
304
+ adjust_menu.addAction(luminosity_action)
305
+ saturation_action = QAction("Saturation, Vibrance", self)
306
+ saturation_action.setProperty("requires_file", True)
307
+ saturation_action.triggered.connect(self.saturation_filter)
308
+ adjust_menu.addAction(saturation_action)
309
+ white_balance_action = QAction("White Balance", self)
310
+ white_balance_action.setProperty("requires_file", True)
311
+ white_balance_action.triggered.connect(self.white_balance)
312
+ adjust_menu.addAction(white_balance_action)
313
+ edit_menu.addMenu(adjust_menu)
314
+
299
315
  edit_menu.addSeparator()
300
316
 
301
317
  copy_current_to_master_action = QAction("Copy Current Layer to Master", self)
@@ -343,6 +359,10 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
343
359
  self.mark_as_modified,
344
360
  self.view_strategy_menu.setEnabled
345
361
  )
362
+ self.filter_manager.register_filter(
363
+ "Luminosity, Contrast", LumiContrastFilter, *filter_handles)
364
+ self.filter_manager.register_filter(
365
+ "Saturation, Vibrance", SaturationVibranceFilter, *filter_handles)
346
366
  self.filter_manager.register_filter(
347
367
  "Denoise", DenoiseFilter, *filter_handles)
348
368
  self.filter_manager.register_filter(
@@ -462,10 +482,6 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
462
482
  unsharp_mask_action.setProperty("requires_file", True)
463
483
  unsharp_mask_action.triggered.connect(self.unsharp_mask)
464
484
  filter_menu.addAction(unsharp_mask_action)
465
- white_balance_action = QAction("White Balance", self)
466
- white_balance_action.setProperty("requires_file", True)
467
- white_balance_action.triggered.connect(self.white_balance)
468
- filter_menu.addAction(white_balance_action)
469
485
  vignetting_action = QAction("Vignetting Correction", self)
470
486
  vignetting_action.setProperty("requires_file", True)
471
487
  vignetting_action.triggered.connect(self.vignetting_correction)
@@ -660,6 +676,12 @@ class ImageEditorUI(QMainWindow, LayerCollectionHandler):
660
676
  self.redo_action.setText("Redo")
661
677
  self.redo_action.setEnabled(False)
662
678
 
679
+ def luminosity_filter(self):
680
+ self.filter_manager.apply("Luminosity, Contrast")
681
+
682
+ def saturation_filter(self):
683
+ self.filter_manager.apply("Saturation, Vibrance")
684
+
663
685
  def denoise_filter(self):
664
686
  self.filter_manager.apply("Denoise")
665
687
 
@@ -1,6 +1,4 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, W0221, R0902, R0914, R0913, R0917
2
- from PySide6.QtWidgets import QHBoxLayout, QLabel, QSlider, QDialogButtonBox
3
- from PySide6.QtCore import Qt, QTimer
4
2
  from .. algorithms.sharpen import unsharp_mask
5
3
  from .base_filter import BaseFilter
6
4
 
@@ -9,9 +7,11 @@ class UnsharpMaskFilter(BaseFilter):
9
7
  def __init__(self, name, parent, image_viewer, layer_collection, undo_manager):
10
8
  super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
11
9
  preview_at_startup=True)
12
- self.max_range = 500.0
10
+ self.min_radius = 0.0
13
11
  self.max_radius = 4.0
12
+ self.min_amount = 0.0
14
13
  self.max_amount = 3.0
14
+ self.min_threshold = 0.0
15
15
  self.max_threshold = 64.0
16
16
  self.initial_radius = 1.0
17
17
  self.initial_amount = 0.5
@@ -24,31 +24,20 @@ class UnsharpMaskFilter(BaseFilter):
24
24
  dlg.setWindowTitle("Unsharp Mask")
25
25
  dlg.setMinimumWidth(600)
26
26
  params = {
27
- "Radius": (self.max_radius, self.initial_radius, "{:.2f}"),
28
- "Amount": (self.max_amount, self.initial_amount, "{:.1%}"),
29
- "Threshold": (self.max_threshold, self.initial_threshold, "{:.2f}")
27
+ "Radius": (self.min_radius, self.max_radius, self.initial_radius, "{:.2f}"),
28
+ "Amount": (self.min_amount, self.max_amount, self.initial_amount, "{:.1%}"),
29
+ "Threshold": (self.min_threshold, self.max_threshold, self.initial_threshold, "{:.2f}")
30
30
  }
31
- value_labels = {}
32
- for name, (max_val, init_val, fmt) in params.items():
33
- param_layout = QHBoxLayout()
34
- name_label = QLabel(f"{name}:")
35
- param_layout.addWidget(name_label)
36
- slider = QSlider(Qt.Horizontal)
37
- slider.setRange(0, self.max_range)
38
- slider.setValue(int(init_val / max_val * self.max_range))
39
- param_layout.addWidget(slider)
40
- value_label = QLabel(fmt.format(init_val))
41
- param_layout.addWidget(value_label)
42
- layout.addLayout(param_layout)
31
+
32
+ def set_slider(name, slider):
43
33
  if name == "Radius":
44
34
  self.radius_slider = slider
45
35
  elif name == "Amount":
46
36
  self.amount_slider = slider
47
37
  elif name == "Threshold":
48
38
  self.threshold_slider = slider
49
- value_labels[name] = value_label
50
- self.create_base_widgets(
51
- layout, QDialogButtonBox.Ok | QDialogButtonBox.Cancel, 200, dlg)
39
+
40
+ value_labels = self.create_sliders(params, dlg, layout, set_slider)
52
41
 
53
42
  def update_value(name, value, max_val, fmt):
54
43
  float_value = max_val * value / self.max_range
@@ -57,16 +46,12 @@ class UnsharpMaskFilter(BaseFilter):
57
46
  self.preview_timer.start()
58
47
 
59
48
  self.radius_slider.valueChanged.connect(
60
- lambda v: update_value("Radius", v, self.max_radius, params["Radius"][2]))
49
+ lambda v: update_value("Radius", v, self.max_radius, params["Radius"][3]))
61
50
  self.amount_slider.valueChanged.connect(
62
- lambda v: update_value("Amount", v, self.max_amount, params["Amount"][2]))
51
+ lambda v: update_value("Amount", v, self.max_amount, params["Amount"][3]))
63
52
  self.threshold_slider.valueChanged.connect(
64
53
  lambda v: update_value("Threshold", v, self.max_threshold, params["Threshold"][2]))
65
- self.preview_timer.timeout.connect(do_preview)
66
- self.connect_preview_toggle(self.preview_check, do_preview, restore_original)
67
- self.button_box.accepted.connect(dlg.accept)
68
- self.button_box.rejected.connect(dlg.reject)
69
- QTimer.singleShot(0, do_preview)
54
+ self.set_timer(do_preview, restore_original, dlg)
70
55
 
71
56
  def get_params(self):
72
57
  return (
@@ -9,7 +9,7 @@ from .base_filter import OneSliderBaseFilter
9
9
  class VignettingFilter(OneSliderBaseFilter):
10
10
  def __init__(self, name, parent, image_viewer, layer_collection, undo_manager):
11
11
  super().__init__(name, parent, image_viewer, layer_collection, undo_manager,
12
- 1.0, 0.90, "Vignetting correction",
12
+ 0.0, 1.0, 0.90, "Vignetting correction",
13
13
  allow_partial_preview=False, preview_at_startup=False)
14
14
  self.subsample_box = None
15
15
  self.fast_subsampling_check = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.6.1
3
+ Version: 1.7.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -85,7 +85,7 @@ 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:
88
+ 4. Type the folliwng command on the terminal (assuming you have expanded the ```tar.gz``` under ```Downloads```):
89
89
  ```bash
90
90
  xattr -cr ~/Downloads/shinestacker/shinestacker.app
91
91
  ```
@@ -1,11 +1,12 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=DH87PU9LcZeI_uvttoH-lyhGyb55UShgeFtsG2qIZgE,21
2
+ shinestacker/_version.py,sha256=ZUl2xcRi75lbPrlvl0NxpUt61vfrXEhe0YCcbPhhIX0,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
6
6
  shinestacker/algorithms/align_parallel.py,sha256=5ru4HDcUObLNDP8bPUgsgwpLyflpcyVA16pPqHpYZfs,17671
7
7
  shinestacker/algorithms/balance.py,sha256=aoqnc1u5A2C3R7fKaOoKnzudRiOT8GRIu4LEP-uzyZQ,24053
8
8
  shinestacker/algorithms/base_stack_algo.py,sha256=RzxvLDHqxqhnAl83u2onjvfsRea1qGK_blbh2Vo0kxA,2670
9
+ shinestacker/algorithms/corrections.py,sha256=DrfLM33D20l4svuuBtoOiH-KGUH_BL1mAV7mHCA_nGA,1094
9
10
  shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
10
11
  shinestacker/algorithms/depth_map.py,sha256=nRBrZQWbdUqFOtYMEQx9UNdnybrBTeAOr1eV91FlN8U,5611
11
12
  shinestacker/algorithms/exif.py,sha256=SM4ZDDe8hCJ3xY6053FNndOiwzEStzdp0WrXurlcHVc,9429
@@ -22,13 +23,13 @@ shinestacker/algorithms/vignetting.py,sha256=gJOv-FN3GnTgaVn70W_6d-qbw3WmqinDiO9
22
23
  shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
23
24
  shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
25
  shinestacker/app/about_dialog.py,sha256=pkH7nnxUP8yc0D3vRGd1jRb5cwi1nDVbQRk_OC9yLk8,4144
25
- shinestacker/app/args_parser_opts.py,sha256=c6IUXOI0SJIFckPWPXYnwmBmdNnOcrtvU5S8hUDU_AQ,979
26
- shinestacker/app/gui_utils.py,sha256=EGZejp0XZXRLVa_Wd_2VYAwK3oe9hMSoZzNgT7NUNRw,2986
26
+ shinestacker/app/args_parser_opts.py,sha256=G3jQjxBYk87ycmyf8Idk40c5H90O1l0owz0asTodm88,2183
27
+ shinestacker/app/gui_utils.py,sha256=rNDtC6vQ1hAJ5F3Vd-VKglCE06mhleu5eiw-oitgnxU,3656
27
28
  shinestacker/app/help_menu.py,sha256=g8lKG_xZmXtNQaC3SIRzyROKVWva_PLEgZsQWh6zUcQ,499
28
- shinestacker/app/main.py,sha256=OQwPJvhrsAThv9kmiqtX7CX-smfwXdFXagQmGsQdmuU,11258
29
+ shinestacker/app/main.py,sha256=c9_rax1eemOfkJqWV7tT7-ZkEBkqz6n4kBST8iXsjD4,10662
29
30
  shinestacker/app/open_frames.py,sha256=bsu32iJSYJQLe_tQQbvAU5DuMDVX6MRuNdE7B5lojZc,1488
30
- shinestacker/app/project.py,sha256=jn4LlMTMxrJeAyQQrR5nVS4Di1rOrjiOJ8LOg0CZbwE,3045
31
- shinestacker/app/retouch.py,sha256=fyk74fgpeoBqY1mRJiXWf1hRzSeG7xXFx4V-Sphr3Kg,3069
31
+ shinestacker/app/project.py,sha256=nwvXllD2FBLQ4ChePQdIGVug46Wh2ubjrJ0sC7klops,2596
32
+ shinestacker/app/retouch.py,sha256=8XcYMv7-feG6yxNCpvlijZQRPlhmRK0OfZO5MuBju-0,2552
32
33
  shinestacker/app/settings_dialog.py,sha256=0P3nqqZEiTIFgidW1_e3Q_zE7NbAouNsuj-yNsU41vk,8192
33
34
  shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
34
35
  shinestacker/config/app_config.py,sha256=rM1Rndk1GDa5c0AhcVNEN9zSAzxPZixzQYfjODbJUwE,771
@@ -38,10 +39,10 @@ shinestacker/config/gui_constants.py,sha256=PNxzwmVEppJ2mV_vwp68NhWzJOEitVy1Pk9S
38
39
  shinestacker/config/settings.py,sha256=4p4r6wKOCbttzfH9tyHQSTd-iv-GfgCd1LxI3C7WIjU,3861
39
40
  shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
40
41
  shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
41
- shinestacker/core/core_utils.py,sha256=wmpu9_idDRe866JFEylMvGPJRT8lSVhikK9PljcbPSI,1392
42
+ shinestacker/core/core_utils.py,sha256=0x9iK9_iPQuj3BwF_QdWoxWTM-jyQytO57BvTQLdwmw,1378
42
43
  shinestacker/core/exceptions.py,sha256=2-noG-ORAGdvDhL8jBQFs0xxZS4fI6UIkMqrWekgk2c,1618
43
44
  shinestacker/core/framework.py,sha256=QaTfnzEUHwzlbyFG7KzeyteckTSWHWEEJE4d5Tc8H18,11015
44
- shinestacker/core/logging.py,sha256=T-ZTGYjN2n2_5Vu2mBJ2id9qj1c9NA79OD2H2w-W0nM,3096
45
+ shinestacker/core/logging.py,sha256=pN4FGcHwI5ouJKwCVoDWQx_Tg3t84mmPh0xhqszDDkw,3111
45
46
  shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
47
  shinestacker/gui/action_config.py,sha256=Xv7SGbhPl1F_dUnU04VBt_E-wIItnN_q6QuhU_d9GfI,25929
47
48
  shinestacker/gui/action_config_dialog.py,sha256=QN95FiVPYL6uin2sYO5F7tq6G5rBWh9yRkeTVvwKrwU,38341
@@ -63,30 +64,31 @@ shinestacker/gui/project_model.py,sha256=9dId8N-np4YHDpz_wO20Mvd06np3YKlej-0TMWa
63
64
  shinestacker/gui/recent_file_manager.py,sha256=010bciuirKLiVCfOAKs0uFlB3iUjHNBlPX_9K2vH5j0,2916
64
65
  shinestacker/gui/select_path_widget.py,sha256=HSwgSr702w5Et4c-6nkRXnIpm1KFqKJetAF5xQNa5zI,1017
65
66
  shinestacker/gui/sys_mon.py,sha256=zU41YYVeqQ1-v6oGIh2_BFzQWq87keN-398Wdm59-Nk,3526
66
- shinestacker/gui/tab_widget.py,sha256=VgRmuktWXCgbXbV7c1Tho0--W5_EmmzXPfzRZgwhGfg,2965
67
+ shinestacker/gui/tab_widget.py,sha256=Hu01_neCxTOG9TMGSI-Ha1w4brp1bEXyvxS24Gi_JS8,2786
67
68
  shinestacker/gui/time_progress_bar.py,sha256=7_sllrQgayjRh__mwJ0-4lghXIakuRAx8wWucJ6olYs,3028
68
- shinestacker/gui/ico/focus_stack_bkg.png,sha256=Q86TgqvKEi_IzKI8m6aZB2a3T40UkDtexf2PdeBM9XE,163151
69
- shinestacker/gui/ico/shinestacker.icns,sha256=3IshIOv0uFexYsAEPkE9xiyuw8mB5X5gffekOUhFlt0,45278
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
73
74
  shinestacker/gui/img/close-round-line-icon.png,sha256=9HZwCjgni1s_JGUPUb_MoOfoe4tRZgM5OWzk92XFZlE,8019
74
75
  shinestacker/gui/img/forward-button-icon.png,sha256=lNw86T4TOEd_uokHYF8myGSGUXzdsHvmDAjlbE18Pgo,4788
75
76
  shinestacker/gui/img/play-button-round-icon.png,sha256=9j6Ks9mOGa-2cXyRFpimepAAvSaHzqJKBfxShRb4_dE,4595
76
77
  shinestacker/gui/img/plus-round-line-icon.png,sha256=LS068Hlu-CeBvJuB3dwwdJg1lZq6D5MUIv53lu1yKJA,7534
77
78
  shinestacker/retouch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
- shinestacker/retouch/base_filter.py,sha256=ObrAcwZv9YJbIhWMHcryHEEj41oAk9hfHmE4phEd1gE,11263
79
+ shinestacker/retouch/adjustments.py,sha256=tNroGN7zjr4SsMJpB-ciSHUUOWgUWai8CChWgxZpr6Q,3826
80
+ shinestacker/retouch/base_filter.py,sha256=o_OkJbdD3jOGY--_sGL1_WqAMQI-QHGw-EEYxGhXOaQ,13976
79
81
  shinestacker/retouch/brush.py,sha256=dzD2FzSpBIPdJRmTZobcrQ1FrVd3tF__ZPnUplNE72s,357
80
82
  shinestacker/retouch/brush_gradient.py,sha256=F5SFhyzl8YTMqjJU3jK8BrIlLCYLUvITd5wz3cQE4xk,1453
81
83
  shinestacker/retouch/brush_preview.py,sha256=cOFVMCbEsgR_alzmr_-LLghtGU_unrE-hAjLHcvrZAY,5484
82
84
  shinestacker/retouch/brush_tool.py,sha256=8uVncTA375uC3Nhp2YM0eZjpOR-nN47i2eGjN8tJzOU,8714
83
- shinestacker/retouch/denoise_filter.py,sha256=UpNKbFs7uArdglEej8AUHan7oCVYV5E7HNzkovj7XMQ,571
85
+ shinestacker/retouch/denoise_filter.py,sha256=QVXFU54MDcylNWtiIcdQSZ3eClW_xNWZhCMIeoEQ8zk,576
84
86
  shinestacker/retouch/display_manager.py,sha256=fTZTGbvmX5DXagexuvbNgOF5GiH2Vv-stLUQQwoglp8,10181
85
87
  shinestacker/retouch/exif_data.py,sha256=LF-fRXW-reMq-xJ_QRE5j8DC2LVGKIlC6MR3QbC1cdg,1896
86
88
  shinestacker/retouch/file_loader.py,sha256=z02-A8_uDZxayI1NFTxT2GVUvEBWStchX9hlN1o5-0U,4784
87
89
  shinestacker/retouch/filter_manager.py,sha256=tOGIWj5HjViL1-iXHkd91X-sZ1c1G531pDmLO0x6zx0,866
88
90
  shinestacker/retouch/icon_container.py,sha256=6gw1HO1bC2FrdB4dc_iH81DQuLjzuvRGksZ2hKLT9yA,585
89
- shinestacker/retouch/image_editor_ui.py,sha256=r2S4Fgyi3xQi2XeqlVYTUxZ9YlPZe2rMaIpvIak8Aog,33181
91
+ shinestacker/retouch/image_editor_ui.py,sha256=w6tyeYm1Arjyr-MxbLNKYvURV0qEZqigK0iUoqGy92o,34244
90
92
  shinestacker/retouch/image_view_status.py,sha256=2rWi2ugdyjMhWCtRJkwOnb7-tCtVfnGfCY_54qpZhwM,1970
91
93
  shinestacker/retouch/image_viewer.py,sha256=xf1vYZRPb9ClCQbqrqAFhPubdqIIpku7DgcY8O5bvYU,4694
92
94
  shinestacker/retouch/io_gui_handler.py,sha256=tyHMmR6_uE_IEI-CIcJibVhupxarKYHzX2fuhnesHaI,14616
@@ -98,13 +100,13 @@ shinestacker/retouch/shortcuts_help.py,sha256=BFWTT5QvodqMhqa_9LI25hZqjICfckgyWG
98
100
  shinestacker/retouch/sidebyside_view.py,sha256=4sNa_IUMbNH18iECO7eDO9S_ls3v2ENwP1AWrBFhofI,18907
99
101
  shinestacker/retouch/transformation_manager.py,sha256=QFYCL-l9V6qlhw3y7tcs0saWWClNPsh7F9pTBkfPbRU,1711
100
102
  shinestacker/retouch/undo_manager.py,sha256=J4hEAnv9bKLQ0N1wllWswjJBhgRgasCnBoMT5LEw-dM,4453
101
- shinestacker/retouch/unsharp_mask_filter.py,sha256=Iapc8UmSVpj3V0LcJq_38P5qerRqTevMynbbk5Rk6iE,3634
103
+ shinestacker/retouch/unsharp_mask_filter.py,sha256=SO-6ZgPPDAO9em_MMefVvvSvt01-2gm1HF6OBsShmL4,2795
102
104
  shinestacker/retouch/view_strategy.py,sha256=jZxB_vX3_0notH0ClxKkLzbdtx4is3vQiYoIP-sDv3M,30216
103
- shinestacker/retouch/vignetting_filter.py,sha256=JhFr6OVIripQzSJrZEG4lxq7wBsmpofLqJQ-aP2bKw8,3789
105
+ shinestacker/retouch/vignetting_filter.py,sha256=M7PZGPdVSq4bqo6wkEznrILMIG3-mTT7iwpgK4Hieyg,3794
104
106
  shinestacker/retouch/white_balance_filter.py,sha256=UaH4yxG3fU4vPutBAkV5oTXIQyUTN09x0uTywAzv3sY,8286
105
- shinestacker-1.6.1.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
106
- shinestacker-1.6.1.dist-info/METADATA,sha256=9sevPHbXS-yaj-wYc-42q28V34PNOdnPlntqNzlsgOo,6978
107
- shinestacker-1.6.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
- shinestacker-1.6.1.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
109
- shinestacker-1.6.1.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
110
- shinestacker-1.6.1.dist-info/RECORD,,
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,,
Binary file