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.
- 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 -25
- shinestacker/app/project.py +12 -21
- shinestacker/app/retouch.py +12 -20
- shinestacker/core/core_utils.py +2 -12
- shinestacker/core/logging.py +4 -3
- 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 +63 -8
- shinestacker/retouch/denoise_filter.py +1 -1
- shinestacker/retouch/display_manager.py +1 -2
- shinestacker/retouch/image_editor_ui.py +39 -39
- shinestacker/retouch/image_viewer.py +17 -9
- shinestacker/retouch/io_gui_handler.py +96 -44
- shinestacker/retouch/io_threads.py +78 -0
- shinestacker/retouch/layer_collection.py +12 -0
- shinestacker/retouch/overlaid_view.py +13 -5
- shinestacker/retouch/paint_area_manager.py +30 -0
- shinestacker/retouch/sidebyside_view.py +3 -3
- shinestacker/retouch/transformation_manager.py +1 -2
- shinestacker/retouch/undo_manager.py +15 -13
- shinestacker/retouch/unsharp_mask_filter.py +13 -28
- shinestacker/retouch/view_strategy.py +65 -22
- shinestacker/retouch/vignetting_filter.py +1 -1
- {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/METADATA +2 -2
- {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/RECORD +35 -32
- shinestacker/gui/ico/focus_stack_bkg.png +0 -0
- shinestacker/retouch/io_manager.py +0 -69
- {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.6.0.dist-info → shinestacker-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {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.
|
|
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,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,
|
|
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.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
|
|
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()}
|
|
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
|
|
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(
|
|
227
|
-
filename = args
|
|
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
|
-
|
|
233
|
-
app = Application
|
|
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
|
-
|
|
233
|
+
|
|
243
234
|
main_app.show()
|
|
244
235
|
main_app.activateWindow()
|
|
245
236
|
if args['expert']:
|
shinestacker/app/project.py
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
56
|
-
project filename.
|
|
57
|
-
''')
|
|
56
|
+
setup_filename_argument(parser, use_const=True)
|
|
58
57
|
add_project_arguments(parser)
|
|
59
|
-
args = vars(parser.parse_args(
|
|
60
|
-
|
|
61
|
-
app = Application
|
|
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']:
|
shinestacker/app/retouch.py
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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(
|
|
52
|
-
filename = args
|
|
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
|
|
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()
|
shinestacker/core/core_utils.py
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
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:
|
shinestacker/core/logging.py
CHANGED
|
@@ -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"
|
|
67
|
-
if log_file
|
|
68
|
-
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
|
|
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)
|
|
@@ -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.
|
|
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(
|
|
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):
|
|
@@ -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.
|
|
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):
|