shinestacker 1.2.0__py3-none-any.whl → 1.3.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/align.py +148 -115
- shinestacker/algorithms/align_auto.py +64 -0
- shinestacker/algorithms/align_parallel.py +296 -0
- shinestacker/algorithms/balance.py +14 -13
- shinestacker/algorithms/base_stack_algo.py +11 -2
- shinestacker/algorithms/multilayer.py +14 -15
- shinestacker/algorithms/noise_detection.py +13 -14
- shinestacker/algorithms/pyramid.py +4 -4
- shinestacker/algorithms/pyramid_auto.py +16 -10
- shinestacker/algorithms/pyramid_tiles.py +19 -11
- shinestacker/algorithms/stack.py +30 -26
- shinestacker/algorithms/stack_framework.py +200 -178
- shinestacker/algorithms/vignetting.py +16 -13
- shinestacker/app/main.py +7 -3
- shinestacker/config/constants.py +63 -26
- shinestacker/config/gui_constants.py +1 -1
- shinestacker/core/core_utils.py +4 -0
- shinestacker/core/framework.py +114 -33
- shinestacker/gui/action_config.py +57 -5
- shinestacker/gui/action_config_dialog.py +156 -17
- shinestacker/gui/base_form_dialog.py +2 -2
- shinestacker/gui/folder_file_selection.py +101 -0
- shinestacker/gui/gui_images.py +10 -10
- shinestacker/gui/gui_run.py +13 -11
- shinestacker/gui/main_window.py +10 -5
- shinestacker/gui/menu_manager.py +4 -0
- shinestacker/gui/new_project.py +171 -74
- shinestacker/gui/project_controller.py +13 -9
- shinestacker/gui/project_converter.py +4 -2
- shinestacker/gui/project_editor.py +72 -53
- shinestacker/gui/select_path_widget.py +1 -1
- shinestacker/gui/sys_mon.py +96 -0
- shinestacker/gui/tab_widget.py +3 -3
- shinestacker/gui/time_progress_bar.py +4 -3
- shinestacker/retouch/exif_data.py +1 -1
- shinestacker/retouch/image_editor_ui.py +2 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/METADATA +6 -6
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/RECORD +43 -39
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,30 +1,49 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, W0102, R0902, R0903
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, W0102, R0902, R0903, E1128
|
|
2
2
|
# pylint: disable=R0917, R0913, R1702, R0912, E1111, E1121, W0613
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
from .. config.constants import constants
|
|
6
6
|
from .. core.colors import color_str
|
|
7
|
-
from .. core.framework import Job,
|
|
7
|
+
from .. core.framework import Job, SequentialTask
|
|
8
8
|
from .. core.core_utils import check_path_exists
|
|
9
|
-
from .. core.exceptions import
|
|
10
|
-
from .utils import read_img, write_img, extension_tif_jpg
|
|
9
|
+
from .. core.exceptions import RunStopException
|
|
10
|
+
from .utils import read_img, write_img, extension_tif_jpg, get_img_metadata, validate_image
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class StackJob(Job):
|
|
14
|
-
def __init__(self, name, working_path, input_path='', **kwargs):
|
|
14
|
+
def __init__(self, name, working_path, input_path='', input_filepaths=[], **kwargs):
|
|
15
15
|
check_path_exists(working_path)
|
|
16
16
|
self.working_path = working_path
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
self._input_path = input_path
|
|
18
|
+
self._action_paths = [] if input_path == '' else [input_path]
|
|
19
|
+
self._input_filepaths = []
|
|
20
|
+
self._input_full_path = None
|
|
21
|
+
self._input_filepaths = input_filepaths
|
|
21
22
|
Job.__init__(self, name, **kwargs)
|
|
22
23
|
|
|
23
24
|
def init(self, a):
|
|
24
25
|
a.init(self)
|
|
25
26
|
|
|
27
|
+
def input_filepaths(self):
|
|
28
|
+
return self._input_filepaths
|
|
29
|
+
|
|
30
|
+
def num_input_filepaths(self):
|
|
31
|
+
return len(self._input_filepaths)
|
|
32
|
+
|
|
33
|
+
def action_paths(self):
|
|
34
|
+
return self._action_paths
|
|
35
|
+
|
|
36
|
+
def add_action_path(self, path):
|
|
37
|
+
self._action_paths.append(path)
|
|
38
|
+
|
|
39
|
+
def num_action_paths(self):
|
|
40
|
+
return len(self._action_paths)
|
|
41
|
+
|
|
42
|
+
def action_path(self, i):
|
|
43
|
+
return self._action_paths[i]
|
|
44
|
+
|
|
26
45
|
|
|
27
|
-
class
|
|
46
|
+
class ImageSequenceManager:
|
|
28
47
|
def __init__(self, name, input_path='', output_path='', working_path='',
|
|
29
48
|
plot_path=constants.DEFAULT_PLOTS_PATH,
|
|
30
49
|
scratch_output_dir=True, resample=1,
|
|
@@ -33,26 +52,73 @@ class FramePaths:
|
|
|
33
52
|
self.working_path = working_path
|
|
34
53
|
self.plot_path = plot_path
|
|
35
54
|
self.input_path = input_path
|
|
36
|
-
self.output_path = output_path
|
|
37
|
-
self.
|
|
38
|
-
self.resample = resample
|
|
55
|
+
self.output_path = self.name if output_path == '' else output_path
|
|
56
|
+
self._resample = resample
|
|
39
57
|
self.reverse_order = reverse_order
|
|
40
58
|
self.scratch_output_dir = scratch_output_dir
|
|
41
|
-
self.input_full_path = None
|
|
42
59
|
self.enabled = None
|
|
43
|
-
self.filenames = None
|
|
44
60
|
self.base_message = ''
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
self._input_full_path = None
|
|
62
|
+
self._output_full_path = None
|
|
63
|
+
self._input_filepaths = None
|
|
64
|
+
|
|
65
|
+
def output_full_path(self):
|
|
66
|
+
if self._output_full_path is None:
|
|
67
|
+
self._output_full_path = os.path.join(self.working_path, self.output_path)
|
|
68
|
+
return self._output_full_path
|
|
69
|
+
|
|
70
|
+
def input_full_path(self):
|
|
71
|
+
if self._input_full_path is None:
|
|
72
|
+
if isinstance(self.input_path, str):
|
|
73
|
+
self._input_full_path = os.path.join(self.working_path, self.input_path)
|
|
74
|
+
check_path_exists(self._input_full_path)
|
|
75
|
+
elif hasattr(self.input_path, "__len__"):
|
|
76
|
+
self._input_full_path = [os.path.join(self.working_path, path)
|
|
77
|
+
for path in self.input_path]
|
|
78
|
+
for path in self._input_full_path:
|
|
79
|
+
check_path_exists(path)
|
|
80
|
+
return self._input_full_path
|
|
81
|
+
|
|
82
|
+
def input_filepaths(self):
|
|
83
|
+
if self._input_filepaths is None:
|
|
84
|
+
if isinstance(self.input_full_path(), str):
|
|
85
|
+
dirs = [self.input_full_path()]
|
|
86
|
+
elif hasattr(self.input_full_path(), "__len__"):
|
|
87
|
+
dirs = self.input_full_path()
|
|
88
|
+
else:
|
|
89
|
+
raise RuntimeError("input_full_path option must contain "
|
|
90
|
+
"a path or an array of paths")
|
|
91
|
+
files = []
|
|
92
|
+
for d in dirs:
|
|
93
|
+
filelist = []
|
|
94
|
+
for _dirpath, _, filenames in os.walk(d):
|
|
95
|
+
filelist = [os.path.join(_dirpath, name)
|
|
96
|
+
for name in filenames if extension_tif_jpg(name)]
|
|
97
|
+
filelist.sort()
|
|
98
|
+
if self.reverse_order:
|
|
99
|
+
filelist.reverse()
|
|
100
|
+
if self._resample > 1:
|
|
101
|
+
filelist = filelist[0::self._resample]
|
|
102
|
+
files += filelist
|
|
103
|
+
if len(files) == 0:
|
|
104
|
+
self.print_message(color_str(f"input folder {d} does not contain any image",
|
|
105
|
+
constants.LOG_COLOR_WARNING),
|
|
106
|
+
level=logging.WARNING)
|
|
107
|
+
self._input_filepaths = files
|
|
108
|
+
return self._input_filepaths
|
|
109
|
+
|
|
110
|
+
def input_filepath(self, index):
|
|
111
|
+
return self.input_filepaths()[index]
|
|
112
|
+
|
|
113
|
+
def num_input_filepaths(self):
|
|
114
|
+
return len(self.input_filepaths())
|
|
48
115
|
|
|
49
116
|
def print_message(self, msg='', level=logging.INFO, end=None, begin='', tqdm=False):
|
|
50
117
|
assert False, "this method should be overwritten"
|
|
51
118
|
|
|
52
119
|
def set_filelist(self):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
self.print_message(color_str(f"{len(self.filenames)} files in folder: {file_folder}",
|
|
120
|
+
file_folder = self.input_full_path().replace(self.working_path, '').lstrip('/')
|
|
121
|
+
self.print_message(color_str(f"{self.num_input_filepaths()} files in folder: {file_folder}",
|
|
56
122
|
constants.LOG_COLOR_LEVEL_2))
|
|
57
123
|
self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
|
|
58
124
|
|
|
@@ -60,20 +126,16 @@ class FramePaths:
|
|
|
60
126
|
if self.working_path == '':
|
|
61
127
|
self.working_path = job.working_path
|
|
62
128
|
check_path_exists(self.working_path)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
('' if self.working_path[-1] == '/' else '/') + \
|
|
67
|
-
self.output_path
|
|
68
|
-
if not os.path.exists(self.output_dir):
|
|
69
|
-
os.makedirs(self.output_dir)
|
|
129
|
+
output_dir = self.output_full_path()
|
|
130
|
+
if not os.path.exists(output_dir):
|
|
131
|
+
os.makedirs(output_dir)
|
|
70
132
|
else:
|
|
71
|
-
list_dir = os.listdir(
|
|
133
|
+
list_dir = os.listdir(output_dir)
|
|
72
134
|
if len(list_dir) > 0:
|
|
73
135
|
if self.scratch_output_dir:
|
|
74
136
|
if self.enabled:
|
|
75
137
|
for filename in list_dir:
|
|
76
|
-
file_path = os.path.join(
|
|
138
|
+
file_path = os.path.join(output_dir, filename)
|
|
77
139
|
if os.path.isfile(file_path):
|
|
78
140
|
os.remove(file_path)
|
|
79
141
|
self.print_message(
|
|
@@ -95,141 +157,91 @@ class FramePaths:
|
|
|
95
157
|
if not os.path.exists(self.plot_path):
|
|
96
158
|
os.makedirs(self.plot_path)
|
|
97
159
|
if self.input_path in ['', []]:
|
|
98
|
-
if
|
|
160
|
+
if job.num_action_paths() == 0:
|
|
99
161
|
raise RuntimeError(f"Job {job.name} does not have any configured path")
|
|
100
|
-
self.input_path = job.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
162
|
+
self.input_path = job.action_path(-1)
|
|
163
|
+
if job.num_input_filepaths() > 0:
|
|
164
|
+
self._input_filepaths = []
|
|
165
|
+
for filepath in job.input_filepaths():
|
|
166
|
+
if not os.path.isabs(filepath):
|
|
167
|
+
filepath = os.path.join(self.input_full_path(), filepath)
|
|
168
|
+
self._input_filepaths.append(filepath)
|
|
169
|
+
job.add_action_path(self.output_path)
|
|
107
170
|
|
|
108
171
|
def folder_list_str(self):
|
|
109
|
-
if isinstance(self.input_full_path, list):
|
|
172
|
+
if isinstance(self.input_full_path(), list):
|
|
110
173
|
file_list = ", ".join(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def folder_filelist(self):
|
|
116
|
-
src_contents = os.walk(self.input_full_path)
|
|
117
|
-
_dirpath, _, filenames = next(src_contents)
|
|
118
|
-
filelist = [name for name in filenames if extension_tif_jpg(name)]
|
|
119
|
-
filelist.sort()
|
|
120
|
-
if self.reverse_order:
|
|
121
|
-
filelist.reverse()
|
|
122
|
-
if self.resample > 1:
|
|
123
|
-
filelist = filelist[0::self.resample]
|
|
124
|
-
return filelist
|
|
125
|
-
|
|
126
|
-
def init(self, job, _working_path=''):
|
|
127
|
-
FramePaths.init(self, job)
|
|
128
|
-
self.input_full_path = self.working_path + \
|
|
129
|
-
('' if self.working_path[-1] == '/' else '/') + self.input_path
|
|
130
|
-
check_path_exists(self.input_full_path)
|
|
131
|
-
job.paths.append(self.output_path)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class FrameMultiDirectory(FramePaths):
|
|
135
|
-
def __init__(self, name, input_path='', output_path='', working_path='',
|
|
136
|
-
plot_path=constants.DEFAULT_PLOTS_PATH,
|
|
137
|
-
scratch_output_dir=True, resample=1,
|
|
138
|
-
reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **_kwargs):
|
|
139
|
-
FramePaths.__init__(self, name, input_path, output_path, working_path, plot_path,
|
|
140
|
-
scratch_output_dir, resample, reverse_order)
|
|
141
|
-
self.input_full_path = None
|
|
174
|
+
[path.replace(self.working_path, '').lstrip('/')
|
|
175
|
+
for path in self.input_full_path()])
|
|
176
|
+
return "folder" + ('s' if len(self.input_full_path()) > 1 else '') + f": {file_list}"
|
|
177
|
+
return "folder: " + self.input_full_path().replace(self.working_path, '').lstrip('/')
|
|
142
178
|
|
|
143
|
-
def folder_list_str(self):
|
|
144
|
-
if isinstance(self.input_full_path, list):
|
|
145
|
-
file_list = ", ".join([d.replace(self.working_path, '').lstrip('/')
|
|
146
|
-
for d in self.input_full_path])
|
|
147
|
-
return "folder" + ('s' if len(self.input_full_path) > 1 else '') + f": {file_list}"
|
|
148
|
-
return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
|
|
149
|
-
|
|
150
|
-
def folder_filelist(self):
|
|
151
|
-
if isinstance(self.input_full_path, str):
|
|
152
|
-
dirs = [self.input_full_path]
|
|
153
|
-
paths = [self.input_path]
|
|
154
|
-
elif hasattr(self.input_full_path, "__len__"):
|
|
155
|
-
dirs = self.input_full_path
|
|
156
|
-
paths = self.input_path
|
|
157
|
-
else:
|
|
158
|
-
raise RuntimeError("input_full_path option must contain a path or an array of paths")
|
|
159
|
-
files = []
|
|
160
|
-
for d, p in zip(dirs, paths):
|
|
161
|
-
filelist = []
|
|
162
|
-
for _dirpath, _, filenames in os.walk(d):
|
|
163
|
-
filelist = [f"{p}/{name}" for name in filenames if extension_tif_jpg(name)]
|
|
164
|
-
if self.reverse_order:
|
|
165
|
-
filelist.reverse()
|
|
166
|
-
if self.resample > 1:
|
|
167
|
-
filelist = filelist[0::self.resample]
|
|
168
|
-
files += filelist
|
|
169
|
-
if len(files) == 0:
|
|
170
|
-
self.print_message(color_str(f"input folder {p} does not contain any image", "red"),
|
|
171
|
-
level=logging.WARNING)
|
|
172
|
-
return files
|
|
173
179
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
self.input_path
|
|
180
|
-
check_path_exists(self.input_full_path)
|
|
181
|
-
elif hasattr(self.input_path, "__len__"):
|
|
182
|
-
self.input_full_path = []
|
|
183
|
-
for path in self.input_path:
|
|
184
|
-
self.input_full_path.append(self.working_path +
|
|
185
|
-
('' if self.working_path[-1] == '/' else '/') +
|
|
186
|
-
path)
|
|
187
|
-
job.paths.append(self.output_path)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
class FramesRefActions(ActionList, FrameDirectory):
|
|
191
|
-
def __init__(self, name, enabled=True, ref_idx=-1, step_process=False, **kwargs):
|
|
192
|
-
FrameDirectory.__init__(self, name, **kwargs)
|
|
193
|
-
ActionList.__init__(self, name, enabled)
|
|
194
|
-
self.ref_idx = ref_idx
|
|
180
|
+
class ReferenceFrameTask(SequentialTask, ImageSequenceManager):
|
|
181
|
+
def __init__(self, name, enabled=True, reference_index=0, step_process=False, **kwargs):
|
|
182
|
+
ImageSequenceManager.__init__(self, name, **kwargs)
|
|
183
|
+
SequentialTask.__init__(self, name, enabled)
|
|
184
|
+
self.ref_idx = reference_index
|
|
195
185
|
self.step_process = step_process
|
|
196
|
-
self.
|
|
197
|
-
self.
|
|
198
|
-
self.
|
|
186
|
+
self.current_idx = None
|
|
187
|
+
self.current_ref_idx = None
|
|
188
|
+
self.current_idx_step = None
|
|
199
189
|
|
|
200
190
|
def begin(self):
|
|
201
|
-
|
|
191
|
+
SequentialTask.begin(self)
|
|
202
192
|
self.set_filelist()
|
|
203
|
-
self.
|
|
204
|
-
|
|
205
|
-
|
|
193
|
+
n = self.num_input_filepaths()
|
|
194
|
+
self.set_counts(n)
|
|
195
|
+
if self.ref_idx == 0:
|
|
196
|
+
self.ref_idx = n // 2
|
|
197
|
+
elif self.ref_idx == -1:
|
|
198
|
+
self.ref_idx = n - 1
|
|
199
|
+
else:
|
|
200
|
+
self.ref_idx -= 1
|
|
201
|
+
if not 0 <= self.ref_idx < n:
|
|
202
|
+
msg = f"reference index {self.ref_idx} out of range [1, {n}]"
|
|
203
|
+
self.print_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_2))
|
|
204
|
+
raise IndexError(msg)
|
|
206
205
|
|
|
207
206
|
def end(self):
|
|
208
|
-
|
|
207
|
+
SequentialTask.end(self)
|
|
209
208
|
|
|
210
209
|
def run_frame(self, _idx, _ref_idx):
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
def run_step(self):
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
def run_step(self, action_count=-1):
|
|
213
|
+
num_files = self.num_input_filepaths()
|
|
214
|
+
if self.run_sequential():
|
|
215
|
+
if action_count == 0:
|
|
216
|
+
self.current_idx = self.ref_idx if self.step_process else 0
|
|
217
|
+
self.current_ref_idx = self.ref_idx
|
|
218
|
+
self.current_idx_step = +1
|
|
219
|
+
idx, ref_idx = self.current_idx, self.current_ref_idx
|
|
220
|
+
self.print_message_r(
|
|
221
|
+
color_str(f"step {action_count + 1}/{num_files}: process file: "
|
|
222
|
+
f"{os.path.basename(self.input_filepath(idx))}, "
|
|
223
|
+
f"reference: "
|
|
224
|
+
f"{os.path.basename(self.input_filepath(self.current_ref_idx))}",
|
|
225
|
+
constants.LOG_COLOR_LEVEL_2))
|
|
226
|
+
else:
|
|
227
|
+
idx, ref_idx = action_count, -1
|
|
228
|
+
self.print_message_r(
|
|
229
|
+
color_str(f"step {idx + 1}/{num_files}: process file: "
|
|
230
|
+
f"{os.path.basename(self.input_filepath(idx))}, "
|
|
231
|
+
"parallel thread", constants.LOG_COLOR_LEVEL_2))
|
|
222
232
|
self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
|
|
223
|
-
self.run_frame(
|
|
224
|
-
if self.
|
|
225
|
-
if self.
|
|
226
|
-
self.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
img = self.run_frame(idx, ref_idx)
|
|
234
|
+
if self.run_sequential():
|
|
235
|
+
if self.current_idx < num_files:
|
|
236
|
+
if self.step_process and img is not None:
|
|
237
|
+
self.current_ref_idx = self.current_idx
|
|
238
|
+
self.current_idx += self.current_idx_step
|
|
239
|
+
if self.current_idx == num_files:
|
|
240
|
+
self.current_idx = self.ref_idx - 1
|
|
241
|
+
if self.step_process:
|
|
242
|
+
self.current_ref_idx = self.ref_idx
|
|
243
|
+
self.current_idx_step = -1
|
|
244
|
+
return img is not None
|
|
233
245
|
|
|
234
246
|
|
|
235
247
|
class SubAction:
|
|
@@ -242,49 +254,49 @@ class SubAction:
|
|
|
242
254
|
def end(self):
|
|
243
255
|
pass
|
|
244
256
|
|
|
257
|
+
def sequential_processing(self):
|
|
258
|
+
return False
|
|
259
|
+
|
|
245
260
|
|
|
246
|
-
class CombinedActions(
|
|
261
|
+
class CombinedActions(ReferenceFrameTask):
|
|
247
262
|
def __init__(self, name, actions=[], enabled=True, **kwargs):
|
|
248
|
-
|
|
263
|
+
ReferenceFrameTask.__init__(self, name, enabled, **kwargs)
|
|
249
264
|
self._actions = actions
|
|
250
|
-
self.
|
|
251
|
-
self.shape = None
|
|
265
|
+
self._metadata = (None, None)
|
|
252
266
|
|
|
253
267
|
def begin(self):
|
|
254
|
-
|
|
268
|
+
ReferenceFrameTask.begin(self)
|
|
255
269
|
for a in self._actions:
|
|
256
270
|
if a.enabled:
|
|
257
271
|
a.begin(self)
|
|
258
272
|
|
|
259
273
|
def img_ref(self, idx):
|
|
260
|
-
|
|
261
|
-
img = read_img(
|
|
262
|
-
if self.step_process else self.input_full_path) + f"/{filename}")
|
|
274
|
+
input_path = self.input_filepath(idx)
|
|
275
|
+
img = read_img(input_path)
|
|
263
276
|
if img is None:
|
|
264
|
-
raise RuntimeError(f"Invalid file: {
|
|
265
|
-
self.
|
|
266
|
-
self.shape = img.shape
|
|
277
|
+
raise RuntimeError(f"Invalid file: {os.path.basename(input_path)}")
|
|
278
|
+
self._metadata = get_img_metadata(img)
|
|
267
279
|
return img
|
|
268
280
|
|
|
269
281
|
def run_frame(self, idx, ref_idx):
|
|
270
|
-
|
|
271
|
-
self.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
282
|
+
input_path = self.input_filepath(idx)
|
|
283
|
+
self.print_message(
|
|
284
|
+
color_str(f'read input image '
|
|
285
|
+
f'{idx + 1}/{self.total_action_counts}, '
|
|
286
|
+
f'{os.path.basename(input_path)}', constants.LOG_COLOR_LEVEL_3))
|
|
287
|
+
img = read_img(input_path)
|
|
288
|
+
validate_image(img, *(self._metadata))
|
|
277
289
|
if img is None:
|
|
278
|
-
raise RuntimeError(f"Invalid file:
|
|
290
|
+
raise RuntimeError(f"Invalid file: {os.path.basename(input_path)}")
|
|
279
291
|
if len(self._actions) == 0:
|
|
280
292
|
self.sub_message(color_str(": no actions specified", constants.LOG_COLOR_ALERT),
|
|
281
293
|
level=logging.WARNING)
|
|
282
294
|
for a in self._actions:
|
|
283
295
|
if not a.enabled:
|
|
284
296
|
self.get_logger().warning(color_str(f"{self.base_message}: sub-action disabled",
|
|
285
|
-
|
|
297
|
+
constants.LOG_COLOR_ALERT))
|
|
286
298
|
else:
|
|
287
|
-
if self.callback(
|
|
299
|
+
if self.callback(constants.CALLBACK_CHECK_RUNNING, self.id, self.name) is False:
|
|
288
300
|
raise RunStopException(self.name)
|
|
289
301
|
if img is not None:
|
|
290
302
|
img = a.run_frame(idx, ref_idx, img)
|
|
@@ -293,16 +305,26 @@ class CombinedActions(FramesRefActions):
|
|
|
293
305
|
color_str(": null input received, action skipped",
|
|
294
306
|
constants.LOG_COLOR_ALERT),
|
|
295
307
|
level=logging.WARNING)
|
|
296
|
-
self.sub_message_r(color_str(': write output image', constants.LOG_COLOR_LEVEL_3))
|
|
297
308
|
if img is not None:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
309
|
+
output_path = os.path.join(self.output_full_path(), os.path.basename(input_path))
|
|
310
|
+
self.print_message(
|
|
311
|
+
color_str(f'write output image '
|
|
312
|
+
f'{idx + 1}/{self.total_action_counts}, '
|
|
313
|
+
f'{os.path.basename(output_path)}', constants.LOG_COLOR_LEVEL_3))
|
|
314
|
+
write_img(output_path, img)
|
|
315
|
+
return img
|
|
316
|
+
self.print_message(color_str(
|
|
317
|
+
f"no output file resulted from processing input file: {os.path.basename(input_path)}",
|
|
318
|
+
constants.LOG_COLOR_ALERT), level=logging.WARNING)
|
|
319
|
+
return None
|
|
304
320
|
|
|
305
321
|
def end(self):
|
|
306
322
|
for a in self._actions:
|
|
307
323
|
if a.enabled:
|
|
308
324
|
a.end()
|
|
325
|
+
|
|
326
|
+
def sequential_processing(self):
|
|
327
|
+
for a in self._actions:
|
|
328
|
+
if a.sequential_processing():
|
|
329
|
+
return True
|
|
330
|
+
return False
|
|
@@ -134,7 +134,8 @@ class Vignetting(SubAction):
|
|
|
134
134
|
self.corrections = None
|
|
135
135
|
|
|
136
136
|
def run_frame(self, idx, _ref_idx, img_0):
|
|
137
|
-
self.process.
|
|
137
|
+
self.process.print_message(
|
|
138
|
+
color_str(f"{self.process.idx_tot_str(idx)}: compute vignetting", "cyan"))
|
|
138
139
|
h, w = img_0.shape[:2]
|
|
139
140
|
self.w_2, self.h_2 = w / 2, h / 2
|
|
140
141
|
self.r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
|
|
@@ -153,14 +154,15 @@ class Vignetting(SubAction):
|
|
|
153
154
|
return img_0
|
|
154
155
|
self.v0 = sigmoid_model(0, *params)
|
|
155
156
|
i0_fit, k_fit, r0_fit = params
|
|
156
|
-
self.process.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
157
|
+
self.process.print_message(
|
|
158
|
+
color_str(f"{self.process.idx_tot_str(idx)}: vignetting model parameters: ", "cyan") +
|
|
159
|
+
color_str(f"i0={i0_fit / 2:.4f}, "
|
|
160
|
+
f"k={k_fit * self.r_max:.4f}, "
|
|
161
|
+
f"r0={r0_fit / self.r_max:.4f}",
|
|
162
|
+
"light_blue"),
|
|
163
|
+
level=logging.DEBUG)
|
|
162
164
|
if self.plot_correction:
|
|
163
|
-
plt.figure(figsize=
|
|
165
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
164
166
|
plt.plot(radii, intensities, label="image mean intensity")
|
|
165
167
|
plt.plot(radii, sigmoid_model(radii * subsample, *params), label="sigmoid fit")
|
|
166
168
|
plt.xlabel('radius (pixels)')
|
|
@@ -175,24 +177,25 @@ class Vignetting(SubAction):
|
|
|
175
177
|
save_plot(plot_path)
|
|
176
178
|
plt.close('all')
|
|
177
179
|
self.process.callback(
|
|
178
|
-
|
|
180
|
+
constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
179
181
|
f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
|
|
180
182
|
for i, p in enumerate(self.percentiles):
|
|
181
183
|
self.corrections[i][idx] = fsolve(lambda x: sigmoid_model(x, *params) /
|
|
182
184
|
self.v0 - p, r0_fit)[0]
|
|
183
|
-
self.process.
|
|
185
|
+
self.process.print_message(
|
|
186
|
+
color_str(f"{self.process.idx_tot_str(idx)}: correct vignetting", "cyan"))
|
|
184
187
|
return correct_vignetting(
|
|
185
188
|
img_0, self.max_correction, self.black_threshold, None, params, self.v0,
|
|
186
189
|
subsample, self.fast_subsampling)
|
|
187
190
|
|
|
188
191
|
def begin(self, process):
|
|
189
192
|
self.process = process
|
|
190
|
-
self.corrections = [np.full(self.process.
|
|
193
|
+
self.corrections = [np.full(self.process.total_action_counts, None, dtype=float)
|
|
191
194
|
for p in self.percentiles]
|
|
192
195
|
|
|
193
196
|
def end(self):
|
|
194
197
|
if self.plot_summary:
|
|
195
|
-
plt.figure(figsize=
|
|
198
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
196
199
|
xs = np.arange(1, len(self.corrections[0]) + 1, dtype=int)
|
|
197
200
|
for i, p in enumerate(self.percentiles):
|
|
198
201
|
linestyle = 'solid'
|
|
@@ -224,5 +227,5 @@ class Vignetting(SubAction):
|
|
|
224
227
|
f"{self.process.name}-r0.pdf"
|
|
225
228
|
save_plot(plot_path)
|
|
226
229
|
plt.close('all')
|
|
227
|
-
self.process.callback(
|
|
230
|
+
self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
228
231
|
f"{self.process.name}: vignetting", plot_path)
|
shinestacker/app/main.py
CHANGED
|
@@ -142,9 +142,12 @@ class MainApp(QMainWindow):
|
|
|
142
142
|
return app_menu
|
|
143
143
|
|
|
144
144
|
def quit(self):
|
|
145
|
-
self.retouch_window.quit()
|
|
146
|
-
|
|
145
|
+
if not self.retouch_window.quit():
|
|
146
|
+
return False
|
|
147
|
+
if not self.project_window.quit():
|
|
148
|
+
return False
|
|
147
149
|
self.close()
|
|
150
|
+
return True
|
|
148
151
|
|
|
149
152
|
def switch_app(self, index):
|
|
150
153
|
self.stacked_widget.setCurrentIndex(index)
|
|
@@ -192,7 +195,8 @@ class MainApp(QMainWindow):
|
|
|
192
195
|
class Application(QApplication):
|
|
193
196
|
def event(self, event):
|
|
194
197
|
if event.type() == QEvent.Quit and event.spontaneous():
|
|
195
|
-
self.
|
|
198
|
+
if not self.quit():
|
|
199
|
+
return True
|
|
196
200
|
return super().event(event)
|
|
197
201
|
|
|
198
202
|
|