shinestacker 1.6.0__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.

Files changed (37) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/corrections.py +26 -0
  3. shinestacker/app/args_parser_opts.py +39 -0
  4. shinestacker/app/gui_utils.py +19 -2
  5. shinestacker/app/main.py +16 -25
  6. shinestacker/app/project.py +12 -21
  7. shinestacker/app/retouch.py +12 -20
  8. shinestacker/core/core_utils.py +2 -12
  9. shinestacker/core/logging.py +4 -3
  10. shinestacker/gui/ico/shinestacker.icns +0 -0
  11. shinestacker/gui/ico/shinestacker_bkg.png +0 -0
  12. shinestacker/gui/tab_widget.py +1 -5
  13. shinestacker/retouch/adjustments.py +93 -0
  14. shinestacker/retouch/base_filter.py +63 -8
  15. shinestacker/retouch/denoise_filter.py +1 -1
  16. shinestacker/retouch/display_manager.py +1 -2
  17. shinestacker/retouch/image_editor_ui.py +39 -39
  18. shinestacker/retouch/image_viewer.py +17 -9
  19. shinestacker/retouch/io_gui_handler.py +96 -44
  20. shinestacker/retouch/io_threads.py +78 -0
  21. shinestacker/retouch/layer_collection.py +12 -0
  22. shinestacker/retouch/overlaid_view.py +13 -5
  23. shinestacker/retouch/paint_area_manager.py +30 -0
  24. shinestacker/retouch/sidebyside_view.py +3 -3
  25. shinestacker/retouch/transformation_manager.py +1 -2
  26. shinestacker/retouch/undo_manager.py +15 -13
  27. shinestacker/retouch/unsharp_mask_filter.py +13 -28
  28. shinestacker/retouch/view_strategy.py +65 -22
  29. shinestacker/retouch/vignetting_filter.py +1 -1
  30. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/METADATA +2 -2
  31. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/RECORD +35 -32
  32. shinestacker/gui/ico/focus_stack_bkg.png +0 -0
  33. shinestacker/retouch/io_manager.py +0 -69
  34. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/WHEEL +0 -0
  35. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/entry_points.txt +0 -0
  36. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/licenses/LICENSE +0 -0
  37. {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.6.0'
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,26 +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.core.logging import setup_logging
17
14
  from shinestacker.gui.main_window import MainWindow
18
15
  from shinestacker.retouch.image_editor_ui import ImageEditorUI
19
- from shinestacker.app.gui_utils import (
20
- disable_macos_special_menu_items, fill_app_menu, set_css_style)
16
+ from shinestacker.app.gui_utils import fill_app_menu
21
17
  from shinestacker.app.help_menu import add_help_action
22
18
  from shinestacker.app.open_frames import open_frames
23
- 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
24
24
 
25
25
 
26
26
  class SelectionDialog(QDialog):
@@ -205,15 +205,12 @@ class Application(QApplication):
205
205
 
206
206
 
207
207
  def main():
208
+ positional_filename, filtered_args = extract_positional_filename()
208
209
  parser = argparse.ArgumentParser(
209
- prog=f'{constants.APP_STRING.lower()}-retouch',
210
+ prog=f'{constants.APP_STRING.lower()}',
210
211
  description='Focus stacking App.',
211
212
  epilog=f'This app is part of the {constants.APP_STRING} package.')
212
- parser.add_argument('-f', '--filename', nargs='?', help='''
213
- if a single file is specified, it can be either a project or an image.
214
- Multiple frames can be specified as a list of files.
215
- Multiple files can be specified separated by ';'.
216
- ''')
213
+ setup_filename_argument(parser, use_const=True)
217
214
  app_group = parser.add_mutually_exclusive_group()
218
215
  app_group.add_argument('-j', '--project', action='store_true', help='''
219
216
  open project window at startup instead of project windows (default).
@@ -223,23 +220,17 @@ open retouch window at startup instead of project windows.
223
220
  ''')
224
221
  add_project_arguments(parser)
225
222
  add_retouch_arguments(parser)
226
- args = vars(parser.parse_args(sys.argv[1:]))
227
- filename = args['filename']
223
+ args = vars(parser.parse_args(filtered_args))
224
+ filename = process_filename_argument(args, positional_filename)
228
225
  path = args['path']
229
226
  if filename and path:
230
227
  print("can't specify both arguments --filename and --path", file=sys.stderr)
231
228
  sys.exit(1)
232
- setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
233
- app = Application(sys.argv)
234
- if config.DONT_USE_NATIVE_MENU:
235
- app.setAttribute(Qt.AA_DontUseNativeMenuBar)
236
- else:
237
- disable_macos_special_menu_items()
238
- icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
239
- app.setWindowIcon(QIcon(icon_path))
229
+
230
+ app = make_app(Application)
240
231
  main_app = MainApp()
241
232
  app.main_app = main_app
242
- set_css_style(app)
233
+
243
234
  main_app.show()
244
235
  main_app.activateWindow()
245
236
  if args['expert']:
@@ -1,23 +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.core.logging import setup_logging
16
13
  from shinestacker.gui.main_window import MainWindow
17
- from shinestacker.app.gui_utils import (
18
- disable_macos_special_menu_items, fill_app_menu, set_css_style)
14
+ from shinestacker.app.gui_utils import fill_app_menu
19
15
  from shinestacker.app.help_menu import add_help_action
20
- 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
21
21
 
22
22
 
23
23
  class ProjectApp(MainWindow):
@@ -48,30 +48,21 @@ class Application(QApplication):
48
48
 
49
49
 
50
50
  def main():
51
+ positional_filename, filtered_args = extract_positional_filename()
51
52
  parser = argparse.ArgumentParser(
52
53
  prog=f'{constants.APP_STRING.lower()}-project',
53
54
  description='Manage and run focus stack jobs.',
54
55
  epilog=f'This app is part of the {constants.APP_STRING} package.')
55
- parser.add_argument('-f', '--filename', nargs='?', help='''
56
- project filename.
57
- ''')
56
+ setup_filename_argument(parser, use_const=True)
58
57
  add_project_arguments(parser)
59
- args = vars(parser.parse_args(sys.argv[1:]))
60
- setup_logging(console_level=logging.DEBUG, file_level=logging.DEBUG, disable_console=True)
61
- app = Application(sys.argv)
62
- if config.DONT_USE_NATIVE_MENU:
63
- app.setAttribute(Qt.AA_DontUseNativeMenuBar)
64
- else:
65
- disable_macos_special_menu_items()
66
- icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
67
- app.setWindowIcon(QIcon(icon_path))
68
- 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)
69
61
  window = ProjectApp()
70
62
  if args['expert']:
71
63
  window.set_expert_options()
72
64
  app.window = window
73
65
  window.show()
74
- filename = args['filename']
75
66
  if filename:
76
67
  QTimer.singleShot(100, lambda: window.project_controller.open_project(filename))
77
68
  elif args['new-project']:
@@ -1,19 +1,20 @@
1
1
  # pylint: disable=C0114, C0115, C0116, C0413, E0611, R0903, E1121, W0201
2
- import os
3
2
  import sys
4
3
  import argparse
5
4
  from PySide6.QtWidgets import QApplication, QMenu
6
- from PySide6.QtGui import QIcon
7
- from PySide6.QtCore import Qt, QEvent
5
+ from PySide6.QtCore import QEvent
8
6
  from shinestacker.config.config import config
9
7
  config.init(DISABLE_TQDM=True, DONT_USE_NATIVE_MENU=True)
10
8
  from shinestacker.config.constants import constants
11
9
  from shinestacker.retouch.image_editor_ui import ImageEditorUI
12
- from shinestacker.app.gui_utils import (
13
- disable_macos_special_menu_items, fill_app_menu, set_css_style)
10
+ from shinestacker.app.gui_utils import fill_app_menu
14
11
  from shinestacker.app.help_menu import add_help_action
15
12
  from shinestacker.app.open_frames import open_frames
16
- 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
17
18
 
18
19
 
19
20
  class RetouchApp(ImageEditorUI):
@@ -39,29 +40,20 @@ class Application(QApplication):
39
40
 
40
41
 
41
42
  def main():
43
+ positional_filename, filtered_args = extract_positional_filename()
42
44
  parser = argparse.ArgumentParser(
43
45
  prog=f'{constants.APP_STRING.lower()}-retouch',
44
46
  description='Final retouch focus stack image from individual frames.',
45
47
  epilog=f'This app is part of the {constants.APP_STRING} package.')
46
- parser.add_argument('-f', '--filename', nargs='?', help='''
47
- import frames from files.
48
- Multiple files can be specified separated by ';'.
49
- ''')
48
+ setup_filename_argument(parser, use_const=True)
50
49
  add_retouch_arguments(parser)
51
- args = vars(parser.parse_args(sys.argv[1:]))
52
- filename = args['filename']
50
+ args = vars(parser.parse_args(filtered_args))
51
+ filename = process_filename_argument(args, positional_filename)
53
52
  path = args['path']
54
53
  if filename and path:
55
54
  print("can't specify both arguments --filename and --path", file=sys.stderr)
56
55
  sys.exit(1)
57
- app = Application(sys.argv)
58
- if config.DONT_USE_NATIVE_MENU:
59
- app.setAttribute(Qt.AA_DontUseNativeMenuBar)
60
- else:
61
- disable_macos_special_menu_items()
62
- icon_path = f"{os.path.dirname(__file__)}/../gui/ico/shinestacker.png"
63
- app.setWindowIcon(QIcon(icon_path))
64
- set_css_style(app)
56
+ app = make_app(Application)
65
57
  editor = RetouchApp()
66
58
  app.editor = editor
67
59
  editor.show()
@@ -26,19 +26,9 @@ def make_tqdm_bar(name, size, ncols=80):
26
26
 
27
27
 
28
28
  def get_app_base_path():
29
- sep = '\\' if (platform.system() == 'Windows') else '/'
30
29
  if getattr(sys, 'frozen', False):
31
- path = os.path.dirname(os.path.realpath(sys.executable))
32
- dirs = path.split(sep)
33
- last = -1
34
- for i in range(len(dirs) - 1, -1, -1):
35
- if dirs[i] == 'shinestacker':
36
- last = i
37
- break
38
- path = sep.join(dirs if last == 1 else dirs[:last + 1])
39
- elif __file__:
40
- path = sep.join(os.path.dirname(os.path.abspath(__file__)).split(sep)[:-3])
41
- return path
30
+ return os.path.dirname(os.path.abspath(sys.executable))
31
+ return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
42
32
 
43
33
 
44
34
  def running_under_windows() -> bool:
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=C0114, C0115, C0116
2
+ import os
2
3
  import logging
3
4
  import sys
4
5
  from pathlib import Path
@@ -63,9 +64,9 @@ def setup_logging(console_level=logging.INFO, file_level=logging.DEBUG, log_file
63
64
  if log_file is not None:
64
65
  if log_file == '':
65
66
  today = datetime.date.today().strftime("%Y-%m-%d")
66
- log_file = f"logs/{constants.APP_STRING.lower()}-{today}.log"
67
- if log_file[0] != '/':
68
- log_file = f'{get_app_base_path()}/{log_file}'
67
+ log_file = os.path.join('logs', f"{constants.APP_STRING.lower()}-{today}.log")
68
+ if not os.path.isabs(log_file):
69
+ log_file = os.path.join(get_app_base_path(), log_file)
69
70
  Path(log_file).parent.mkdir(parents=True, exist_ok=True)
70
71
  file_handler = logging.FileHandler(log_file)
71
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)
@@ -157,7 +200,7 @@ class BaseFilter(QObject, LayerCollectionHandler):
157
200
  except Exception:
158
201
  h, w = self.master_layer_copy().shape[:2]
159
202
  try:
160
- self.undo_manager.extend_undo_area(0, 0, w, h)
203
+ self.undo_manager.set_paint_area(0, 0, w, h)
161
204
  self.undo_manager.save_undo_state(
162
205
  self.master_layer_copy(),
163
206
  self.name
@@ -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):
@@ -215,7 +215,7 @@ class DisplayManager(QObject, LayerCollectionHandler):
215
215
  def refresh_master_view(self):
216
216
  if self.has_no_master_layer():
217
217
  return
218
- self.image_viewer.update_master_display()
218
+ self.image_viewer.update_master_display_area()
219
219
  self.update_master_thumbnail()
220
220
 
221
221
  def refresh_current_view(self):
@@ -229,7 +229,6 @@ class DisplayManager(QObject, LayerCollectionHandler):
229
229
  self.status_message_requested.emit("Temporary view: Individual layer.")
230
230
  else:
231
231
  self._master_refresh_and_thumb()
232
- self.image_viewer.strategy.brush_preview.hide()
233
232
  self.status_message_requested.emit("Temporary view: Master.")
234
233
 
235
234
  def end_temp_view(self):