shinestacker 1.6.1__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shinestacker might be problematic. Click here for more details.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/corrections.py +26 -0
- shinestacker/algorithms/stack.py +9 -0
- shinestacker/algorithms/stack_framework.py +35 -16
- shinestacker/algorithms/utils.py +5 -1
- 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/app/settings_dialog.py +46 -3
- shinestacker/config/settings.py +4 -1
- shinestacker/core/core_utils.py +2 -2
- shinestacker/core/framework.py +7 -2
- shinestacker/core/logging.py +2 -2
- shinestacker/gui/action_config_dialog.py +72 -45
- shinestacker/gui/gui_run.py +1 -2
- shinestacker/gui/ico/shinestacker.icns +0 -0
- shinestacker/gui/img/dark/close-round-line-icon.png +0 -0
- shinestacker/gui/img/dark/forward-button-icon.png +0 -0
- shinestacker/gui/img/dark/play-button-round-icon.png +0 -0
- shinestacker/gui/img/dark/plus-round-line-icon.png +0 -0
- shinestacker/gui/img/dark/shinestacker_bkg.png +0 -0
- shinestacker/gui/img/light/shinestacker_bkg.png +0 -0
- shinestacker/gui/main_window.py +20 -7
- shinestacker/gui/menu_manager.py +18 -7
- shinestacker/gui/new_project.py +0 -2
- shinestacker/gui/tab_widget.py +16 -10
- shinestacker/retouch/adjustments.py +98 -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.8.0.dist-info}/METADATA +4 -4
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/RECORD +44 -37
- shinestacker/gui/ico/focus_stack_bkg.png +0 -0
- /shinestacker/gui/img/{close-round-line-icon.png → light/close-round-line-icon.png} +0 -0
- /shinestacker/gui/img/{forward-button-icon.png → light/forward-button-icon.png} +0 -0
- /shinestacker/gui/img/{play-button-round-icon.png → light/play-button-round-icon.png} +0 -0
- /shinestacker/gui/img/{plus-round-line-icon.png → light/plus-round-line-icon.png} +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.6.1.dist-info → shinestacker-1.8.0.dist-info}/top_level.txt +0 -0
shinestacker/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = '1.
|
|
1
|
+
__version__ = '1.8.0'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E1101
|
|
2
|
+
import numpy as np
|
|
3
|
+
import cv2
|
|
4
|
+
from ..config.constants import constants
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def gamma_correction(img, gamma):
|
|
8
|
+
max_px_val = constants.MAX_UINT8 if img.dtype == np.uint8 else constants.MAX_UINT16
|
|
9
|
+
ar = np.arange(0, max_px_val + 1, dtype=np.float64)
|
|
10
|
+
lut = (((ar / max_px_val) ** (1.0 / gamma)) * max_px_val).astype(img.dtype)
|
|
11
|
+
return cv2.LUT(img, lut) if img.dtype == np.uint8 else np.take(lut, img)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def contrast_correction(img, k):
|
|
15
|
+
max_px_val = constants.MAX_UINT8 if img.dtype == np.uint8 else constants.MAX_UINT16
|
|
16
|
+
ar = np.arange(0, max_px_val + 1, dtype=np.float64)
|
|
17
|
+
x = 2.0 * (ar / max_px_val) - 1.0
|
|
18
|
+
# f(x) = x * exp(k) / (1 + (exp(k) - 1)|x|), -1 < x < +1
|
|
19
|
+
# note that: f(f(x, k), -k) = x
|
|
20
|
+
exp_k = np.exp(k)
|
|
21
|
+
numerator = x * exp_k
|
|
22
|
+
denominator = 1 + (exp_k - 1) * np.abs(x)
|
|
23
|
+
corrected = numerator / denominator
|
|
24
|
+
corrected = (corrected + 1.0) * 0.5 * max_px_val
|
|
25
|
+
lut = np.clip(corrected, 0, max_px_val).astype(img.dtype)
|
|
26
|
+
return cv2.LUT(img, lut) if img.dtype == np.uint8 else np.take(lut, img)
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -65,6 +65,9 @@ class FocusStackBase(TaskBase, ImageSequenceManager):
|
|
|
65
65
|
if self.exif_path != '':
|
|
66
66
|
self.exif_path = os.path.join(working_path, self.exif_path)
|
|
67
67
|
|
|
68
|
+
def end_job(self):
|
|
69
|
+
ImageSequenceManager.end_job(self)
|
|
70
|
+
|
|
68
71
|
|
|
69
72
|
def get_bunches(collection, n_frames, n_overlap):
|
|
70
73
|
bunches = [collection[x:x + n_frames]
|
|
@@ -100,6 +103,9 @@ class FocusStackBunch(SequentialTask, FocusStackBase):
|
|
|
100
103
|
def end(self):
|
|
101
104
|
SequentialTask.end(self)
|
|
102
105
|
|
|
106
|
+
def end_job(self):
|
|
107
|
+
FocusStackBase.end_job(self)
|
|
108
|
+
|
|
103
109
|
def run_step(self, action_count=-1):
|
|
104
110
|
self.print_message(
|
|
105
111
|
color_str(f"fusing bunch: {action_count + 1}/{self.total_action_counts}",
|
|
@@ -126,3 +132,6 @@ class FocusStack(FocusStackBase):
|
|
|
126
132
|
|
|
127
133
|
def init(self, job, _working_path=''):
|
|
128
134
|
FocusStackBase.init(self, job, self.working_path)
|
|
135
|
+
|
|
136
|
+
def end_job(self):
|
|
137
|
+
FocusStackBase.end_job(self)
|
|
@@ -46,7 +46,8 @@ class StackJob(Job):
|
|
|
46
46
|
class ImageSequenceManager:
|
|
47
47
|
def __init__(self, name, input_path='', output_path='', working_path='',
|
|
48
48
|
plot_path=constants.DEFAULT_PLOTS_PATH,
|
|
49
|
-
scratch_output_dir=True,
|
|
49
|
+
scratch_output_dir=True, delete_output_at_end=False,
|
|
50
|
+
resample=1,
|
|
50
51
|
reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **_kwargs):
|
|
51
52
|
self.name = name
|
|
52
53
|
self.working_path = working_path
|
|
@@ -56,6 +57,7 @@ class ImageSequenceManager:
|
|
|
56
57
|
self._resample = resample
|
|
57
58
|
self.reverse_order = reverse_order
|
|
58
59
|
self.scratch_output_dir = scratch_output_dir
|
|
60
|
+
self.delete_output_at_end = delete_output_at_end
|
|
59
61
|
self.enabled = None
|
|
60
62
|
self.base_message = ''
|
|
61
63
|
self._input_full_path = None
|
|
@@ -122,6 +124,24 @@ class ImageSequenceManager:
|
|
|
122
124
|
constants.LOG_COLOR_LEVEL_2))
|
|
123
125
|
self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
|
|
124
126
|
|
|
127
|
+
def scratch_outout_folder(self):
|
|
128
|
+
if self.enabled:
|
|
129
|
+
output_dir = self.output_full_path()
|
|
130
|
+
list_dir = os.listdir(output_dir)
|
|
131
|
+
n_files = len(list_dir)
|
|
132
|
+
if n_files > 0:
|
|
133
|
+
for filename in list_dir:
|
|
134
|
+
file_path = os.path.join(output_dir, filename)
|
|
135
|
+
if os.path.isfile(file_path):
|
|
136
|
+
os.remove(file_path)
|
|
137
|
+
self.print_message(
|
|
138
|
+
color_str(f"output directory {self.output_path} content erased",
|
|
139
|
+
'yellow'))
|
|
140
|
+
else:
|
|
141
|
+
self.print_message(
|
|
142
|
+
color_str(f"module disabled, output directory {self.output_path}"
|
|
143
|
+
" not scratched", 'yellow'))
|
|
144
|
+
|
|
125
145
|
def init(self, job):
|
|
126
146
|
if self.working_path == '':
|
|
127
147
|
self.working_path = job.working_path
|
|
@@ -130,22 +150,10 @@ class ImageSequenceManager:
|
|
|
130
150
|
if not os.path.exists(output_dir):
|
|
131
151
|
os.makedirs(output_dir)
|
|
132
152
|
else:
|
|
133
|
-
|
|
134
|
-
if len(list_dir) > 0:
|
|
153
|
+
if len(os.listdir(output_dir)):
|
|
135
154
|
if self.scratch_output_dir:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
file_path = os.path.join(output_dir, filename)
|
|
139
|
-
if os.path.isfile(file_path):
|
|
140
|
-
os.remove(file_path)
|
|
141
|
-
self.print_message(
|
|
142
|
-
color_str(f": output directory {self.output_path} content erased",
|
|
143
|
-
'yellow'))
|
|
144
|
-
else:
|
|
145
|
-
self.print_message(
|
|
146
|
-
color_str(f": module disabled, output directory {self.output_path}"
|
|
147
|
-
" not scratched", 'yellow'))
|
|
148
|
-
else:
|
|
155
|
+
self.scratch_outout_folder()
|
|
156
|
+
elif self.enabled:
|
|
149
157
|
self.print_message(
|
|
150
158
|
color_str(
|
|
151
159
|
f": output directory {self.output_path} not empty, "
|
|
@@ -168,6 +176,11 @@ class ImageSequenceManager:
|
|
|
168
176
|
self._input_filepaths.append(filepath)
|
|
169
177
|
job.add_action_path(self.output_path)
|
|
170
178
|
|
|
179
|
+
def end_job(self):
|
|
180
|
+
if self.delete_output_at_end:
|
|
181
|
+
self.scratch_outout_folder()
|
|
182
|
+
os.rmdir(self.output_full_path())
|
|
183
|
+
|
|
171
184
|
def folder_list_str(self):
|
|
172
185
|
if isinstance(self.input_full_path(), list):
|
|
173
186
|
file_list = ", ".join(
|
|
@@ -206,6 +219,9 @@ class ReferenceFrameTask(SequentialTask, ImageSequenceManager):
|
|
|
206
219
|
def end(self):
|
|
207
220
|
SequentialTask.end(self)
|
|
208
221
|
|
|
222
|
+
def end_job(self):
|
|
223
|
+
ImageSequenceManager.end_job(self)
|
|
224
|
+
|
|
209
225
|
def run_frame(self, _idx, _ref_idx):
|
|
210
226
|
return None
|
|
211
227
|
|
|
@@ -323,6 +339,9 @@ class CombinedActions(ReferenceFrameTask):
|
|
|
323
339
|
if a.enabled:
|
|
324
340
|
a.end()
|
|
325
341
|
|
|
342
|
+
def end_job(self):
|
|
343
|
+
ReferenceFrameTask.end_job(self)
|
|
344
|
+
|
|
326
345
|
def sequential_processing(self):
|
|
327
346
|
for a in self._actions:
|
|
328
347
|
if a.sequential_processing():
|
shinestacker/algorithms/utils.py
CHANGED
|
@@ -140,8 +140,12 @@ def save_plot(filename, fig=None):
|
|
|
140
140
|
if not os.path.isdir(dir_path):
|
|
141
141
|
os.makedirs(dir_path)
|
|
142
142
|
if fig is None:
|
|
143
|
+
logging_level = logging.getLogger().level
|
|
144
|
+
logger = logging.getLogger()
|
|
145
|
+
logger.setLevel(logging.WARNING)
|
|
143
146
|
fig = plt.gcf()
|
|
144
|
-
|
|
147
|
+
fig.savefig(filename, dpi=150)
|
|
148
|
+
logger.setLevel(logging_level)
|
|
145
149
|
if config.JUPYTER_NOTEBOOK:
|
|
146
150
|
plt.show()
|
|
147
151
|
plt.close(fig)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0116
|
|
2
|
+
import sys
|
|
3
|
+
|
|
2
4
|
|
|
3
5
|
def add_project_arguments(parser):
|
|
4
6
|
parser.add_argument('-x', '--expert', action='store_true', help='''
|
|
@@ -25,3 +27,40 @@ set side-by-side view.
|
|
|
25
27
|
view_group.add_argument('-v3', '--view-top-bottom', action='store_true', help='''
|
|
26
28
|
set top-bottom view.
|
|
27
29
|
''')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_positional_filename():
|
|
33
|
+
positional_filename = None
|
|
34
|
+
filtered_args = []
|
|
35
|
+
for arg in sys.argv[1:]:
|
|
36
|
+
if not arg.startswith('-') and not positional_filename:
|
|
37
|
+
positional_filename = arg
|
|
38
|
+
else:
|
|
39
|
+
filtered_args.append(arg)
|
|
40
|
+
return positional_filename, filtered_args
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def setup_filename_argument(parser, use_const=True):
|
|
44
|
+
if use_const:
|
|
45
|
+
parser.add_argument('-f', '--filename', nargs='?', const=True, help='''
|
|
46
|
+
filename to open. Can be a project file or image file.
|
|
47
|
+
Multiple files can be specified separated by ';'.
|
|
48
|
+
''')
|
|
49
|
+
else:
|
|
50
|
+
parser.add_argument('-f', '--filename', nargs='?', help='''
|
|
51
|
+
filename to open. Can be a project file or image file.
|
|
52
|
+
Multiple files can be specified separated by ';'.
|
|
53
|
+
''')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def process_filename_argument(args, positional_filename):
|
|
57
|
+
filename = args.get('filename')
|
|
58
|
+
if positional_filename and not filename:
|
|
59
|
+
filename = positional_filename
|
|
60
|
+
if filename is True:
|
|
61
|
+
if positional_filename:
|
|
62
|
+
filename = positional_filename
|
|
63
|
+
else:
|
|
64
|
+
print("Error: -f flag used but no filename provided", file=sys.stderr)
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
return filename
|
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()
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0611, W0718, R0903, E0611, R0902
|
|
2
2
|
from PySide6.QtCore import Signal
|
|
3
3
|
from PySide6.QtWidgets import QFrame, QLabel, QCheckBox, QComboBox, QDoubleSpinBox, QSpinBox
|
|
4
|
-
from .. gui.config_dialog import ConfigDialog
|
|
5
4
|
from .. config.settings import Settings
|
|
6
5
|
from .. config.constants import constants
|
|
7
6
|
from .. config.gui_constants import gui_constants
|
|
7
|
+
from .. gui.config_dialog import ConfigDialog
|
|
8
|
+
from .. gui.action_config_dialog import AlignFramesConfigBase
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class SettingsDialog(ConfigDialog):
|
|
11
|
+
class SettingsDialog(ConfigDialog, AlignFramesConfigBase):
|
|
11
12
|
update_project_config_requested = Signal()
|
|
12
13
|
update_retouch_config_requested = Signal()
|
|
13
14
|
|
|
14
15
|
def __init__(self, parent=None, project_settings=True, retouch_settings=True):
|
|
16
|
+
AlignFramesConfigBase.__init__(self)
|
|
15
17
|
self.project_settings = project_settings
|
|
16
18
|
self.retouch_settings = retouch_settings
|
|
17
19
|
self.settings = Settings.instance()
|
|
18
20
|
self.expert_options = None
|
|
19
21
|
self.combined_actions_max_threads = None
|
|
20
22
|
self.align_frames_max_threads = None
|
|
23
|
+
self.detector = None
|
|
24
|
+
self.descriptor = None
|
|
25
|
+
self.matching_method = None
|
|
21
26
|
self.focus_stack_max_threads = None
|
|
22
27
|
self.view_strategy = None
|
|
23
28
|
self.min_mouse_step_brush_fraction = None
|
|
@@ -58,6 +63,32 @@ class SettingsDialog(ConfigDialog):
|
|
|
58
63
|
self.container_layout.addRow("Max num. of cores, align frames:",
|
|
59
64
|
self.align_frames_max_threads)
|
|
60
65
|
|
|
66
|
+
def change_match_config():
|
|
67
|
+
self.change_match_config(
|
|
68
|
+
self.detector, self.descriptor,
|
|
69
|
+
self. matching_method, self.show_info)
|
|
70
|
+
|
|
71
|
+
self.detector = QComboBox()
|
|
72
|
+
self.detector.addItems(constants.VALID_DETECTORS)
|
|
73
|
+
self.descriptor = QComboBox()
|
|
74
|
+
self.descriptor.addItems(constants.VALID_DESCRIPTORS)
|
|
75
|
+
self.matching_method = QComboBox()
|
|
76
|
+
self.info_label = QLabel()
|
|
77
|
+
self.info_label.setStyleSheet("color: orange; font-style: italic;")
|
|
78
|
+
self.matching_method = QComboBox()
|
|
79
|
+
for k, v in zip(self.MATCHING_METHOD_OPTIONS, constants.VALID_MATCHING_METHODS):
|
|
80
|
+
self.matching_method.addItem(k, v)
|
|
81
|
+
self.detector.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['detector'])
|
|
82
|
+
self.descriptor.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['descriptor'])
|
|
83
|
+
self.matching_method.setToolTip(self.DETECTOR_DESCRIPTOR_TOOLTIPS['match_method'])
|
|
84
|
+
self.detector.currentIndexChanged.connect(change_match_config)
|
|
85
|
+
self.descriptor.currentIndexChanged.connect(change_match_config)
|
|
86
|
+
self.matching_method.currentIndexChanged.connect(change_match_config)
|
|
87
|
+
self.container_layout.addRow('Detector:', self.detector)
|
|
88
|
+
self.container_layout.addRow('Descriptor:', self.descriptor)
|
|
89
|
+
self.container_layout.addRow(self.info_label)
|
|
90
|
+
self.container_layout.addRow('Match method:', self.matching_method)
|
|
91
|
+
|
|
61
92
|
self.focus_stack_max_threads = QSpinBox()
|
|
62
93
|
self.focus_stack_max_threads.setRange(0, 64)
|
|
63
94
|
self.focus_stack_max_threads.setValue(
|
|
@@ -115,7 +146,14 @@ class SettingsDialog(ConfigDialog):
|
|
|
115
146
|
})
|
|
116
147
|
self.settings.set(
|
|
117
148
|
'align_frames_params', {
|
|
118
|
-
'max_threads':
|
|
149
|
+
'max_threads':
|
|
150
|
+
self.align_frames_max_threads.value(),
|
|
151
|
+
'detector':
|
|
152
|
+
self.descriptor.currentText(),
|
|
153
|
+
'descriptor':
|
|
154
|
+
self.descriptor.currentText(),
|
|
155
|
+
'match_method':
|
|
156
|
+
self.matching_method.itemData(self.matching_method.currentIndex())
|
|
119
157
|
})
|
|
120
158
|
self.settings.set(
|
|
121
159
|
'focus_stack_params', {
|
|
@@ -148,6 +186,11 @@ class SettingsDialog(ConfigDialog):
|
|
|
148
186
|
self.expert_options.setChecked(constants.DEFAULT_EXPERT_OPTIONS)
|
|
149
187
|
self.combined_actions_max_threads.setValue(constants.DEFAULT_MAX_FWK_THREADS)
|
|
150
188
|
self.align_frames_max_threads.setValue(constants.DEFAULT_ALIGN_MAX_THREADS)
|
|
189
|
+
self.detector.setCurrentText(constants.DEFAULT_DETECTOR)
|
|
190
|
+
self.descriptor.setCurrentText(constants.DEFAULT_DESCRIPTOR)
|
|
191
|
+
idx = self.matching_method.findData(constants.DEFAULT_MATCHING_METHOD)
|
|
192
|
+
if idx >= 0:
|
|
193
|
+
self.matching_method.setCurrentIndex(idx)
|
|
151
194
|
self.focus_stack_max_threads.setValue(constants.DEFAULT_PY_MAX_THREADS)
|
|
152
195
|
if self.retouch_settings:
|
|
153
196
|
idx = self.view_strategy.findData(constants.DEFAULT_VIEW_STRATEGY)
|
shinestacker/config/settings.py
CHANGED
|
@@ -42,7 +42,10 @@ DEFAULT_SETTINGS = {
|
|
|
42
42
|
'max_threads': constants.DEFAULT_MAX_FWK_THREADS
|
|
43
43
|
},
|
|
44
44
|
'align_frames_params': {
|
|
45
|
-
'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS
|
|
45
|
+
'max_threads': constants.DEFAULT_ALIGN_MAX_THREADS,
|
|
46
|
+
'detector': constants.DEFAULT_DETECTOR,
|
|
47
|
+
'descriptor': constants.DEFAULT_DESCRIPTOR,
|
|
48
|
+
'match_method': constants.DEFAULT_MATCHING_METHOD
|
|
46
49
|
},
|
|
47
50
|
'focus_stack_params': {
|
|
48
51
|
'max_threads': constants.DEFAULT_PY_MAX_THREADS
|
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:
|
|
@@ -53,4 +52,5 @@ def setup_matplotlib_mode():
|
|
|
53
52
|
__IPYTHON__ # noqa
|
|
54
53
|
except Exception:
|
|
55
54
|
matplotlib.use('agg')
|
|
55
|
+
matplotlib.pyplot.set_loglevel("warning")
|
|
56
56
|
matplotlib.rcParams['pdf.fonttype'] = 42
|
shinestacker/core/framework.py
CHANGED
|
@@ -146,6 +146,9 @@ class TaskBase:
|
|
|
146
146
|
def sub_message_r(self, msg='', level=logging.INFO):
|
|
147
147
|
self.sub_message(msg, level, self.end_r, self.begin_r, False)
|
|
148
148
|
|
|
149
|
+
def end_job(self):
|
|
150
|
+
pass
|
|
151
|
+
|
|
149
152
|
|
|
150
153
|
class Job(TaskBase):
|
|
151
154
|
def __init__(self, name, logger_name=None, log_file='', callbacks=None, **kwargs):
|
|
@@ -188,6 +191,8 @@ class Job(TaskBase):
|
|
|
188
191
|
self.id, self.name) is False:
|
|
189
192
|
raise RunStopException(self.name)
|
|
190
193
|
a.run()
|
|
194
|
+
for a in self.__actions:
|
|
195
|
+
a.end_job()
|
|
191
196
|
|
|
192
197
|
|
|
193
198
|
class SequentialTask(TaskBase):
|
|
@@ -261,11 +266,11 @@ class SequentialTask(TaskBase):
|
|
|
261
266
|
try:
|
|
262
267
|
result = future.result()
|
|
263
268
|
if result:
|
|
264
|
-
self.
|
|
269
|
+
self.print_message_r(color_str(
|
|
265
270
|
f"completed processing step: {self.idx_tot_str(idx)}",
|
|
266
271
|
constants.LOG_COLOR_LEVEL_1))
|
|
267
272
|
else:
|
|
268
|
-
self.
|
|
273
|
+
self.print_message_r(color_str(
|
|
269
274
|
f"failed processing step: {self.idx_tot_str(idx)}",
|
|
270
275
|
constants.LOG_COLOR_WARNING))
|
|
271
276
|
self.current_action_count += 1
|
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)
|