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 +1 -1
- shinestacker/algorithms/corrections.py +26 -0
- shinestacker/app/args_parser_opts.py +39 -0
- shinestacker/app/gui_utils.py +19 -2
- shinestacker/app/main.py +16 -27
- shinestacker/app/project.py +12 -23
- shinestacker/app/retouch.py +12 -25
- shinestacker/core/core_utils.py +1 -2
- shinestacker/core/logging.py +2 -2
- shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker/gui/ico/shinestacker_bkg.png +0 -0
- shinestacker/gui/tab_widget.py +1 -5
- shinestacker/retouch/adjustments.py +93 -0
- shinestacker/retouch/base_filter.py +62 -7
- shinestacker/retouch/denoise_filter.py +1 -1
- shinestacker/retouch/image_editor_ui.py +26 -4
- shinestacker/retouch/unsharp_mask_filter.py +13 -28
- shinestacker/retouch/vignetting_filter.py +1 -1
- {shinestacker-1.6.1.dist-info → shinestacker-1.7.0.dist-info}/METADATA +2 -2
- {shinestacker-1.6.1.dist-info → shinestacker-1.7.0.dist-info}/RECORD +24 -22
- shinestacker/gui/ico/focus_stack_bkg.png +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.7.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.7.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.7.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '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
|
shinestacker/app/gui_utils.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0116, E0611, R0913, R0917
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
|
-
|
|
5
|
-
from PySide6.
|
|
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,
|
|
12
|
-
from PySide6.QtCore import
|
|
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
|
|
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()}
|
|
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
|
|
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(
|
|
228
|
-
filename = args
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
233
|
+
|
|
245
234
|
main_app.show()
|
|
246
235
|
main_app.activateWindow()
|
|
247
236
|
if args['expert']:
|
shinestacker/app/project.py
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
57
|
-
project filename.
|
|
58
|
-
''')
|
|
56
|
+
setup_filename_argument(parser, use_const=True)
|
|
59
57
|
add_project_arguments(parser)
|
|
60
|
-
args = vars(parser.parse_args(
|
|
61
|
-
|
|
62
|
-
|
|
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']:
|
shinestacker/app/retouch.py
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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(
|
|
55
|
-
filename = args
|
|
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
|
-
|
|
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()
|
shinestacker/core/core_utils.py
CHANGED
|
@@ -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
|
-
|
|
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:
|
shinestacker/core/logging.py
CHANGED
|
@@ -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"
|
|
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(),
|
|
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
|
|
Binary file
|
shinestacker/gui/tab_widget.py
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
50
|
-
self.
|
|
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"][
|
|
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"][
|
|
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.
|
|
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.
|
|
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=
|
|
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=
|
|
26
|
-
shinestacker/app/gui_utils.py,sha256=
|
|
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=
|
|
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=
|
|
31
|
-
shinestacker/app/retouch.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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/
|
|
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/
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
105
|
+
shinestacker/retouch/vignetting_filter.py,sha256=M7PZGPdVSq4bqo6wkEznrILMIG3-mTT7iwpgK4Hieyg,3794
|
|
104
106
|
shinestacker/retouch/white_balance_filter.py,sha256=UaH4yxG3fU4vPutBAkV5oTXIQyUTN09x0uTywAzv3sY,8286
|
|
105
|
-
shinestacker-1.
|
|
106
|
-
shinestacker-1.
|
|
107
|
-
shinestacker-1.
|
|
108
|
-
shinestacker-1.
|
|
109
|
-
shinestacker-1.
|
|
110
|
-
shinestacker-1.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|