shinestacker 1.2.0__py3-none-any.whl → 1.2.1__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 +34 -33
- shinestacker/algorithms/balance.py +11 -12
- shinestacker/algorithms/multilayer.py +10 -11
- shinestacker/algorithms/noise_detection.py +6 -7
- shinestacker/algorithms/stack.py +17 -17
- shinestacker/algorithms/stack_framework.py +125 -154
- shinestacker/algorithms/vignetting.py +3 -3
- shinestacker/config/constants.py +3 -1
- shinestacker/core/framework.py +12 -12
- shinestacker/gui/action_config.py +53 -0
- shinestacker/gui/action_config_dialog.py +4 -5
- shinestacker/gui/gui_images.py +10 -10
- shinestacker/gui/gui_run.py +1 -1
- shinestacker/gui/main_window.py +4 -4
- shinestacker/gui/menu_manager.py +4 -0
- shinestacker/gui/new_project.py +0 -1
- shinestacker/gui/project_controller.py +4 -4
- shinestacker/gui/project_editor.py +35 -26
- shinestacker/gui/tab_widget.py +3 -3
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/METADATA +1 -1
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/RECORD +26 -26
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -6,8 +6,8 @@ from .. config.constants import constants
|
|
|
6
6
|
from .. core.colors import color_str
|
|
7
7
|
from .. core.framework import Job, ActionList
|
|
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):
|
|
@@ -33,26 +33,73 @@ class FramePaths:
|
|
|
33
33
|
self.working_path = working_path
|
|
34
34
|
self.plot_path = plot_path
|
|
35
35
|
self.input_path = input_path
|
|
36
|
-
self.output_path = output_path
|
|
37
|
-
self.output_dir = None
|
|
36
|
+
self.output_path = self.name if output_path == '' else output_path
|
|
38
37
|
self.resample = resample
|
|
39
38
|
self.reverse_order = reverse_order
|
|
40
39
|
self.scratch_output_dir = scratch_output_dir
|
|
41
|
-
self.input_full_path = None
|
|
42
40
|
self.enabled = None
|
|
43
|
-
self.filenames = None
|
|
44
41
|
self.base_message = ''
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
self._input_full_path = None
|
|
43
|
+
self._output_full_path = None
|
|
44
|
+
self._input_filepaths = None
|
|
45
|
+
|
|
46
|
+
def output_full_path(self):
|
|
47
|
+
if self._output_full_path is None:
|
|
48
|
+
self._output_full_path = os.path.join(self.working_path, self.output_path)
|
|
49
|
+
return self._output_full_path
|
|
50
|
+
|
|
51
|
+
def input_full_path(self):
|
|
52
|
+
if self._input_full_path is None:
|
|
53
|
+
if isinstance(self.input_path, str):
|
|
54
|
+
self._input_full_path = os.path.join(self.working_path, self.input_path)
|
|
55
|
+
check_path_exists(self._input_full_path)
|
|
56
|
+
elif hasattr(self.input_path, "__len__"):
|
|
57
|
+
self._input_full_path = [os.path.join(self.working_path, path)
|
|
58
|
+
for path in self.input_path]
|
|
59
|
+
for path in self._input_full_path:
|
|
60
|
+
check_path_exists(path)
|
|
61
|
+
return self._input_full_path
|
|
62
|
+
|
|
63
|
+
def input_filepaths(self):
|
|
64
|
+
if self._input_filepaths is None:
|
|
65
|
+
if isinstance(self.input_full_path(), str):
|
|
66
|
+
dirs = [self.input_full_path()]
|
|
67
|
+
elif hasattr(self.input_full_path(), "__len__"):
|
|
68
|
+
dirs = self.input_full_path()
|
|
69
|
+
else:
|
|
70
|
+
raise RuntimeError("input_full_path option must contain "
|
|
71
|
+
"a path or an array of paths")
|
|
72
|
+
files = []
|
|
73
|
+
for d in dirs:
|
|
74
|
+
filelist = []
|
|
75
|
+
for _dirpath, _, filenames in os.walk(d):
|
|
76
|
+
filelist = [os.path.join(_dirpath, name)
|
|
77
|
+
for name in filenames if extension_tif_jpg(name)]
|
|
78
|
+
filelist.sort()
|
|
79
|
+
if self.reverse_order:
|
|
80
|
+
filelist.reverse()
|
|
81
|
+
if self.resample > 1:
|
|
82
|
+
filelist = filelist[0::self.resample]
|
|
83
|
+
files += filelist
|
|
84
|
+
if len(files) == 0:
|
|
85
|
+
self.print_message(color_str(f"input folder {d} does not contain any image",
|
|
86
|
+
constants.LOG_COLOR_WARNING),
|
|
87
|
+
level=logging.WARNING)
|
|
88
|
+
self._input_filepaths = files
|
|
89
|
+
return self._input_filepaths
|
|
90
|
+
|
|
91
|
+
def input_filepath(self, index):
|
|
92
|
+
return self.input_filepaths()[index]
|
|
93
|
+
|
|
94
|
+
def num_input_filepaths(self):
|
|
95
|
+
return len(self.input_filepaths())
|
|
48
96
|
|
|
49
97
|
def print_message(self, msg='', level=logging.INFO, end=None, begin='', tqdm=False):
|
|
50
98
|
assert False, "this method should be overwritten"
|
|
51
99
|
|
|
52
100
|
def set_filelist(self):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
self.print_message(color_str(f"{len(self.filenames)} files in folder: {file_folder}",
|
|
101
|
+
file_folder = self.input_full_path().replace(self.working_path, '').lstrip('/')
|
|
102
|
+
self.print_message(color_str(f"{self.num_input_filepaths()} files in folder: {file_folder}",
|
|
56
103
|
constants.LOG_COLOR_LEVEL_2))
|
|
57
104
|
self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
|
|
58
105
|
|
|
@@ -60,20 +107,16 @@ class FramePaths:
|
|
|
60
107
|
if self.working_path == '':
|
|
61
108
|
self.working_path = job.working_path
|
|
62
109
|
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)
|
|
110
|
+
output_dir = self.output_full_path()
|
|
111
|
+
if not os.path.exists(output_dir):
|
|
112
|
+
os.makedirs(output_dir)
|
|
70
113
|
else:
|
|
71
|
-
list_dir = os.listdir(
|
|
114
|
+
list_dir = os.listdir(output_dir)
|
|
72
115
|
if len(list_dir) > 0:
|
|
73
116
|
if self.scratch_output_dir:
|
|
74
117
|
if self.enabled:
|
|
75
118
|
for filename in list_dir:
|
|
76
|
-
file_path = os.path.join(
|
|
119
|
+
file_path = os.path.join(output_dir, filename)
|
|
77
120
|
if os.path.isfile(file_path):
|
|
78
121
|
os.remove(file_path)
|
|
79
122
|
self.print_message(
|
|
@@ -100,136 +143,69 @@ class FramePaths:
|
|
|
100
143
|
self.input_path = job.paths[-1]
|
|
101
144
|
job.paths.append(self.output_path)
|
|
102
145
|
|
|
103
|
-
|
|
104
|
-
class FrameDirectory(FramePaths):
|
|
105
|
-
def __init__(self, name, **kwargs):
|
|
106
|
-
FramePaths.__init__(self, name, **kwargs)
|
|
107
|
-
|
|
108
146
|
def folder_list_str(self):
|
|
109
|
-
if isinstance(self.input_full_path, list):
|
|
147
|
+
if isinstance(self.input_full_path(), list):
|
|
110
148
|
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
|
|
149
|
+
[path.replace(self.working_path, '').lstrip('/')
|
|
150
|
+
for path in self.input_full_path()])
|
|
151
|
+
return "folder" + ('s' if len(self.input_full_path()) > 1 else '') + f": {file_list}"
|
|
152
|
+
return "folder: " + self.input_full_path().replace(self.working_path, '').lstrip('/')
|
|
125
153
|
|
|
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
|
|
142
154
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
174
|
-
def init(self, job):
|
|
175
|
-
FramePaths.init(self, job)
|
|
176
|
-
if isinstance(self.input_path, str):
|
|
177
|
-
self.input_full_path = self.working_path + \
|
|
178
|
-
('' if self.working_path[-1] == '/' else '/') + \
|
|
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)
|
|
155
|
+
class FramesRefActions(ActionList, FramePaths):
|
|
156
|
+
def __init__(self, name, enabled=True, reference_index=0, step_process=False, **kwargs):
|
|
157
|
+
FramePaths.__init__(self, name, **kwargs)
|
|
193
158
|
ActionList.__init__(self, name, enabled)
|
|
194
|
-
self.ref_idx =
|
|
159
|
+
self.ref_idx = reference_index
|
|
195
160
|
self.step_process = step_process
|
|
196
|
-
self.
|
|
197
|
-
self.
|
|
198
|
-
self.
|
|
161
|
+
self.current_idx = None
|
|
162
|
+
self.current_ref_idx = None
|
|
163
|
+
self.current_idx_step = None
|
|
199
164
|
|
|
200
165
|
def begin(self):
|
|
201
166
|
ActionList.begin(self)
|
|
202
167
|
self.set_filelist()
|
|
203
|
-
self.
|
|
204
|
-
|
|
205
|
-
|
|
168
|
+
n = self.num_input_filepaths()
|
|
169
|
+
self.set_counts(n)
|
|
170
|
+
if self.ref_idx == 0:
|
|
171
|
+
self.ref_idx = n // 2
|
|
172
|
+
elif self.ref_idx == -1:
|
|
173
|
+
self.ref_idx = n - 1
|
|
174
|
+
else:
|
|
175
|
+
self.ref_idx -= 1
|
|
176
|
+
if not 0 <= self.ref_idx < n:
|
|
177
|
+
msg = f"reference index {self.ref_idx} out of range [1, {n}]"
|
|
178
|
+
self.print_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_2))
|
|
179
|
+
raise IndexError(msg)
|
|
206
180
|
|
|
207
181
|
def end(self):
|
|
208
182
|
ActionList.end(self)
|
|
209
183
|
|
|
210
184
|
def run_frame(self, _idx, _ref_idx):
|
|
211
|
-
|
|
185
|
+
return None
|
|
212
186
|
|
|
213
187
|
def run_step(self):
|
|
214
|
-
if self.
|
|
215
|
-
self.
|
|
216
|
-
self.
|
|
217
|
-
self.
|
|
218
|
-
ll =
|
|
188
|
+
if self.current_action_count == 0:
|
|
189
|
+
self.current_idx = self.ref_idx if self.step_process else 0
|
|
190
|
+
self.current_ref_idx = self.ref_idx
|
|
191
|
+
self.current_idx_step = +1
|
|
192
|
+
ll = self.num_input_filepaths()
|
|
219
193
|
self.print_message_r(
|
|
220
|
-
color_str(f"step {self.
|
|
221
|
-
f"
|
|
194
|
+
color_str(f"step {self.current_action_count + 1}/{ll}: process file: "
|
|
195
|
+
f"{os.path.basename(self.input_filepath(self.current_idx))}, "
|
|
196
|
+
f"reference: {os.path.basename(self.input_filepath(self.current_ref_idx))}",
|
|
197
|
+
constants.LOG_COLOR_LEVEL_2))
|
|
222
198
|
self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
|
|
223
|
-
self.run_frame(self.
|
|
224
|
-
if self.
|
|
225
|
-
if self.step_process:
|
|
226
|
-
self.
|
|
227
|
-
self.
|
|
228
|
-
if self.
|
|
229
|
-
self.
|
|
199
|
+
success = self.run_frame(self.current_idx, self.current_ref_idx) is not None
|
|
200
|
+
if self.current_idx < ll:
|
|
201
|
+
if self.step_process and success:
|
|
202
|
+
self.current_ref_idx = self.current_idx
|
|
203
|
+
self.current_idx += self.current_idx_step
|
|
204
|
+
if self.current_idx == ll:
|
|
205
|
+
self.current_idx = self.ref_idx - 1
|
|
230
206
|
if self.step_process:
|
|
231
|
-
self.
|
|
232
|
-
self.
|
|
207
|
+
self.current_ref_idx = self.ref_idx
|
|
208
|
+
self.current_idx_step = -1
|
|
233
209
|
|
|
234
210
|
|
|
235
211
|
class SubAction:
|
|
@@ -247,8 +223,7 @@ class CombinedActions(FramesRefActions):
|
|
|
247
223
|
def __init__(self, name, actions=[], enabled=True, **kwargs):
|
|
248
224
|
FramesRefActions.__init__(self, name, enabled, **kwargs)
|
|
249
225
|
self._actions = actions
|
|
250
|
-
self.
|
|
251
|
-
self.shape = None
|
|
226
|
+
self._metadata = (None, None)
|
|
252
227
|
|
|
253
228
|
def begin(self):
|
|
254
229
|
FramesRefActions.begin(self)
|
|
@@ -257,32 +232,27 @@ class CombinedActions(FramesRefActions):
|
|
|
257
232
|
a.begin(self)
|
|
258
233
|
|
|
259
234
|
def img_ref(self, idx):
|
|
260
|
-
|
|
261
|
-
img = read_img(
|
|
262
|
-
if self.step_process else self.input_full_path) + f"/{filename}")
|
|
235
|
+
input_path = self.input_filepath(idx)
|
|
236
|
+
img = read_img(input_path)
|
|
263
237
|
if img is None:
|
|
264
|
-
raise RuntimeError(f"Invalid file: {
|
|
265
|
-
self.
|
|
266
|
-
self.shape = img.shape
|
|
238
|
+
raise RuntimeError(f"Invalid file: {os.path.basename(input_path)}")
|
|
239
|
+
self._metadata = get_img_metadata(img)
|
|
267
240
|
return img
|
|
268
241
|
|
|
269
242
|
def run_frame(self, idx, ref_idx):
|
|
270
|
-
|
|
243
|
+
input_path = self.input_filepath(idx)
|
|
271
244
|
self.sub_message_r(color_str(': read input image', constants.LOG_COLOR_LEVEL_3))
|
|
272
|
-
img = read_img(
|
|
273
|
-
|
|
274
|
-
raise BitDepthError(self.dtype, img.dtype, )
|
|
275
|
-
if self.shape is not None and img.shape != self.shape:
|
|
276
|
-
raise ShapeError(self.shape, img.shape)
|
|
245
|
+
img = read_img(input_path)
|
|
246
|
+
validate_image(img, *(self._metadata))
|
|
277
247
|
if img is None:
|
|
278
|
-
raise RuntimeError(f"Invalid file:
|
|
248
|
+
raise RuntimeError(f"Invalid file: {os.path.basename(input_path)}")
|
|
279
249
|
if len(self._actions) == 0:
|
|
280
250
|
self.sub_message(color_str(": no actions specified", constants.LOG_COLOR_ALERT),
|
|
281
251
|
level=logging.WARNING)
|
|
282
252
|
for a in self._actions:
|
|
283
253
|
if not a.enabled:
|
|
284
254
|
self.get_logger().warning(color_str(f"{self.base_message}: sub-action disabled",
|
|
285
|
-
|
|
255
|
+
constants.LOG_COLOR_ALERT))
|
|
286
256
|
else:
|
|
287
257
|
if self.callback('check_running', self.id, self.name) is False:
|
|
288
258
|
raise RunStopException(self.name)
|
|
@@ -293,14 +263,15 @@ class CombinedActions(FramesRefActions):
|
|
|
293
263
|
color_str(": null input received, action skipped",
|
|
294
264
|
constants.LOG_COLOR_ALERT),
|
|
295
265
|
level=logging.WARNING)
|
|
296
|
-
self.sub_message_r(color_str(': write output image', constants.LOG_COLOR_LEVEL_3))
|
|
297
266
|
if img is not None:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
267
|
+
self.sub_message_r(color_str(': write output image', constants.LOG_COLOR_LEVEL_3))
|
|
268
|
+
output_path = os.path.join(self.output_full_path(), os.path.basename(input_path))
|
|
269
|
+
write_img(output_path, img)
|
|
270
|
+
return img
|
|
271
|
+
self.print_message(color_str(
|
|
272
|
+
f"no output file resulted from processing input file: {os.path.basename(input_path)}",
|
|
273
|
+
constants.LOG_COLOR_ALERT), level=logging.WARNING)
|
|
274
|
+
return None
|
|
304
275
|
|
|
305
276
|
def end(self):
|
|
306
277
|
for a in self._actions:
|
|
@@ -160,7 +160,7 @@ class Vignetting(SubAction):
|
|
|
160
160
|
"light_blue"),
|
|
161
161
|
level=logging.DEBUG)
|
|
162
162
|
if self.plot_correction:
|
|
163
|
-
plt.figure(figsize=
|
|
163
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
164
164
|
plt.plot(radii, intensities, label="image mean intensity")
|
|
165
165
|
plt.plot(radii, sigmoid_model(radii * subsample, *params), label="sigmoid fit")
|
|
166
166
|
plt.xlabel('radius (pixels)')
|
|
@@ -187,12 +187,12 @@ class Vignetting(SubAction):
|
|
|
187
187
|
|
|
188
188
|
def begin(self, process):
|
|
189
189
|
self.process = process
|
|
190
|
-
self.corrections = [np.full(self.process.
|
|
190
|
+
self.corrections = [np.full(self.process.total_action_counts, None, dtype=float)
|
|
191
191
|
for p in self.percentiles]
|
|
192
192
|
|
|
193
193
|
def end(self):
|
|
194
194
|
if self.plot_summary:
|
|
195
|
-
plt.figure(figsize=
|
|
195
|
+
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
196
196
|
xs = np.arange(1, len(self.corrections[0]) + 1, dtype=int)
|
|
197
197
|
for i, p in enumerate(self.percentiles):
|
|
198
198
|
linestyle = 'solid'
|
shinestacker/config/constants.py
CHANGED
|
@@ -22,6 +22,8 @@ class _Constants:
|
|
|
22
22
|
|
|
23
23
|
ANSI_ESCAPE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
24
24
|
|
|
25
|
+
PLT_FIG_SIZE = (10, 5)
|
|
26
|
+
|
|
25
27
|
ACTION_JOB = "Job"
|
|
26
28
|
ACTION_COMBO = "CombinedActions"
|
|
27
29
|
ACTION_NOISEDETECTION = "NoiseDetection"
|
|
@@ -116,7 +118,7 @@ class _Constants:
|
|
|
116
118
|
DEFAULT_REFINE_ITERS = 100
|
|
117
119
|
DEFAULT_ALIGN_CONFIDENCE = 99.9
|
|
118
120
|
DEFAULT_ALIGN_MAX_ITERS = 2000
|
|
119
|
-
DEFAULT_ALIGN_ABORT_ABNORMAL =
|
|
121
|
+
DEFAULT_ALIGN_ABORT_ABNORMAL = False
|
|
120
122
|
DEFAULT_BORDER_VALUE = [0] * 4
|
|
121
123
|
DEFAULT_BORDER_BLUR = 50
|
|
122
124
|
DEFAULT_ALIGN_SUBSAMPLE = 0
|
shinestacker/core/framework.py
CHANGED
|
@@ -24,7 +24,7 @@ class TqdmCallbacks:
|
|
|
24
24
|
|
|
25
25
|
def __init__(self):
|
|
26
26
|
self.tbar = None
|
|
27
|
-
self.
|
|
27
|
+
self.total_action_counts = -1
|
|
28
28
|
|
|
29
29
|
@classmethod
|
|
30
30
|
def instance(cls):
|
|
@@ -33,8 +33,8 @@ class TqdmCallbacks:
|
|
|
33
33
|
return cls._instance
|
|
34
34
|
|
|
35
35
|
def step_counts(self, name, counts):
|
|
36
|
-
self.
|
|
37
|
-
self.tbar = make_tqdm_bar(name, self.
|
|
36
|
+
self.total_action_counts = counts
|
|
37
|
+
self.tbar = make_tqdm_bar(name, self.total_action_counts)
|
|
38
38
|
|
|
39
39
|
def begin_steps(self, name):
|
|
40
40
|
pass
|
|
@@ -191,12 +191,12 @@ class Job(JobBase):
|
|
|
191
191
|
class ActionList(JobBase):
|
|
192
192
|
def __init__(self, name, enabled=True, **kwargs):
|
|
193
193
|
JobBase.__init__(self, name, enabled, **kwargs)
|
|
194
|
-
self.
|
|
195
|
-
self.
|
|
194
|
+
self.total_action_counts = None
|
|
195
|
+
self.current_action_count = None
|
|
196
196
|
|
|
197
197
|
def set_counts(self, counts):
|
|
198
|
-
self.
|
|
199
|
-
self.callback('step_counts', self.id, self.name, self.
|
|
198
|
+
self.total_action_counts = counts
|
|
199
|
+
self.callback('step_counts', self.id, self.name, self.total_action_counts)
|
|
200
200
|
|
|
201
201
|
def begin(self):
|
|
202
202
|
self.callback('begin_steps', self.id, self.name)
|
|
@@ -205,17 +205,17 @@ class ActionList(JobBase):
|
|
|
205
205
|
self.callback('end_steps', self.id, self.name)
|
|
206
206
|
|
|
207
207
|
def __iter__(self):
|
|
208
|
-
self.
|
|
208
|
+
self.current_action_count = 0
|
|
209
209
|
return self
|
|
210
210
|
|
|
211
211
|
def run_step(self):
|
|
212
212
|
pass
|
|
213
213
|
|
|
214
214
|
def __next__(self):
|
|
215
|
-
if self.
|
|
215
|
+
if self.current_action_count < self.total_action_counts:
|
|
216
216
|
self.run_step()
|
|
217
|
-
x = self.
|
|
218
|
-
self.
|
|
217
|
+
x = self.current_action_count
|
|
218
|
+
self.current_action_count += 1
|
|
219
219
|
return x
|
|
220
220
|
raise StopIteration
|
|
221
221
|
|
|
@@ -223,7 +223,7 @@ class ActionList(JobBase):
|
|
|
223
223
|
self.print_message(color_str('begin run', constants.LOG_COLOR_LEVEL_2), end='\n')
|
|
224
224
|
self.begin()
|
|
225
225
|
for _ in iter(self):
|
|
226
|
-
self.callback('after_step', self.id, self.name, self.
|
|
226
|
+
self.callback('after_step', self.id, self.name, self.current_action_count)
|
|
227
227
|
if self.callback('check_running', self.id, self.name) is False:
|
|
228
228
|
raise RunStopException(self.name)
|
|
229
229
|
self.end()
|
|
@@ -16,12 +16,16 @@ FIELD_ABS_PATH = 'abs_path'
|
|
|
16
16
|
FIELD_REL_PATH = 'rel_path'
|
|
17
17
|
FIELD_FLOAT = 'float'
|
|
18
18
|
FIELD_INT = 'int'
|
|
19
|
+
FIELD_REF_IDX = 'ref_idx'
|
|
19
20
|
FIELD_INT_TUPLE = 'int_tuple'
|
|
20
21
|
FIELD_BOOL = 'bool'
|
|
21
22
|
FIELD_COMBO = 'combo'
|
|
22
23
|
FIELD_TYPES = [FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
|
|
23
24
|
FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO]
|
|
24
25
|
|
|
26
|
+
FIELD_REF_IDX_OPTIONS = ['Median frame', 'First frame', 'Last frame', 'Specify index']
|
|
27
|
+
FIELD_REF_IDX_MAX = 1000
|
|
28
|
+
|
|
25
29
|
|
|
26
30
|
class ActionConfigurator(ABC):
|
|
27
31
|
def __init__(self, expert, current_wd):
|
|
@@ -56,6 +60,8 @@ class FieldBuilder:
|
|
|
56
60
|
widget = self.create_float_field(tag, **kwargs)
|
|
57
61
|
elif field_type == FIELD_INT:
|
|
58
62
|
widget = self.create_int_field(tag, **kwargs)
|
|
63
|
+
elif field_type == FIELD_REF_IDX:
|
|
64
|
+
widget = self.create_ref_idx_field(tag, **kwargs)
|
|
59
65
|
elif field_type == FIELD_INT_TUPLE:
|
|
60
66
|
widget = self.create_int_tuple_field(tag, **kwargs)
|
|
61
67
|
elif field_type == FIELD_BOOL:
|
|
@@ -75,6 +81,8 @@ class FieldBuilder:
|
|
|
75
81
|
default_value = kwargs.get('default', 0.0)
|
|
76
82
|
elif field_type == FIELD_INT:
|
|
77
83
|
default_value = kwargs.get('default', 0)
|
|
84
|
+
elif field_type == FIELD_REF_IDX:
|
|
85
|
+
default_value = kwargs.get('default', 0)
|
|
78
86
|
elif field_type == FIELD_INT_TUPLE:
|
|
79
87
|
default_value = kwargs.get('default', [0] * kwargs.get('size', 1))
|
|
80
88
|
elif field_type == FIELD_BOOL:
|
|
@@ -112,6 +120,9 @@ class FieldBuilder:
|
|
|
112
120
|
widget.setChecked(default)
|
|
113
121
|
elif field['type'] == FIELD_INT:
|
|
114
122
|
widget.setValue(default)
|
|
123
|
+
elif field['type'] == FIELD_REF_IDX:
|
|
124
|
+
widget.layout().itemAt(2).widget().setValue(default)
|
|
125
|
+
widget.layout().itemAt(0).widget().setCurrentText(FIELD_REF_IDX_OPTIONS[0])
|
|
115
126
|
elif field['type'] == FIELD_INT_TUPLE:
|
|
116
127
|
for i in range(field['size']):
|
|
117
128
|
spinbox = widget.layout().itemAt(1 + i * 2).widget()
|
|
@@ -146,6 +157,17 @@ class FieldBuilder:
|
|
|
146
157
|
params[tag] = field['widget'].isChecked()
|
|
147
158
|
elif field['type'] == FIELD_INT:
|
|
148
159
|
params[tag] = field['widget'].value()
|
|
160
|
+
elif field['type'] == FIELD_REF_IDX:
|
|
161
|
+
wl = field['widget'].layout()
|
|
162
|
+
txt = wl.itemAt(0).widget().currentText()
|
|
163
|
+
if txt == FIELD_REF_IDX_OPTIONS[0]:
|
|
164
|
+
params[tag] = 0
|
|
165
|
+
elif txt == FIELD_REF_IDX_OPTIONS[1]:
|
|
166
|
+
params[tag] = 1
|
|
167
|
+
elif txt == FIELD_REF_IDX_OPTIONS[2]:
|
|
168
|
+
params[tag] = -1
|
|
169
|
+
else:
|
|
170
|
+
params[tag] = wl.itemAt(2).widget().value()
|
|
149
171
|
elif field['type'] == FIELD_INT_TUPLE:
|
|
150
172
|
params[tag] = [field['widget'].layout().itemAt(1 + i * 2).widget().value()
|
|
151
173
|
for i in range(field['size'])]
|
|
@@ -327,6 +349,37 @@ class FieldBuilder:
|
|
|
327
349
|
spin.setValue(self.action.params.get(tag, default))
|
|
328
350
|
return spin
|
|
329
351
|
|
|
352
|
+
def create_ref_idx_field(self, tag, default=0):
|
|
353
|
+
layout = QHBoxLayout()
|
|
354
|
+
combo = QComboBox()
|
|
355
|
+
combo.addItems(FIELD_REF_IDX_OPTIONS)
|
|
356
|
+
label = QLabel("index [1, ..., N]: ")
|
|
357
|
+
spin = QSpinBox()
|
|
358
|
+
spin.setRange(1, FIELD_REF_IDX_MAX)
|
|
359
|
+
value = self.action.params.get(tag, default)
|
|
360
|
+
if value == 0:
|
|
361
|
+
combo.setCurrentText(FIELD_REF_IDX_OPTIONS[0])
|
|
362
|
+
spin.setValue(1)
|
|
363
|
+
elif value == 1:
|
|
364
|
+
combo.setCurrentText(FIELD_REF_IDX_OPTIONS[1])
|
|
365
|
+
spin.setValue(1)
|
|
366
|
+
elif value == -1:
|
|
367
|
+
combo.setCurrentText(FIELD_REF_IDX_OPTIONS[2])
|
|
368
|
+
spin.setValue(1)
|
|
369
|
+
else:
|
|
370
|
+
combo.setCurrentText(FIELD_REF_IDX_OPTIONS[3])
|
|
371
|
+
spin.setValue(value)
|
|
372
|
+
|
|
373
|
+
def set_enabled():
|
|
374
|
+
spin.setEnabled(combo.currentText() == FIELD_REF_IDX_OPTIONS[-1])
|
|
375
|
+
|
|
376
|
+
combo.currentTextChanged.connect(set_enabled)
|
|
377
|
+
set_enabled()
|
|
378
|
+
layout.addWidget(combo)
|
|
379
|
+
layout.addWidget(label)
|
|
380
|
+
layout.addWidget(spin)
|
|
381
|
+
return create_layout_widget_no_margins(layout)
|
|
382
|
+
|
|
330
383
|
def create_int_tuple_field(self, tag, size=1,
|
|
331
384
|
default=[0] * 100, min_val=[0] * 100, max_val=[100] * 100,
|
|
332
385
|
**kwargs):
|
|
@@ -12,7 +12,7 @@ from .base_form_dialog import create_form_layout
|
|
|
12
12
|
from . action_config import (
|
|
13
13
|
FieldBuilder, ActionConfigurator,
|
|
14
14
|
FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
|
|
15
|
-
FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO
|
|
15
|
+
FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO, FIELD_REF_IDX
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
|
|
@@ -101,9 +101,8 @@ class ActionConfigDialog(QDialog):
|
|
|
101
101
|
action_type, DefaultActionConfigurator)(self.expert(), self.current_wd)
|
|
102
102
|
|
|
103
103
|
def accept(self):
|
|
104
|
-
self.parent().project_editor.add_undo(self.parent().project().clone())
|
|
105
104
|
if self.configurator.update_params(self.action.params):
|
|
106
|
-
self.parent().mark_as_modified()
|
|
105
|
+
self.parent().mark_as_modified(True, "Modify Configuration")
|
|
107
106
|
super().accept()
|
|
108
107
|
else:
|
|
109
108
|
self.parent().project_editor.pop_undo()
|
|
@@ -431,8 +430,8 @@ class CombinedActionsConfigurator(DefaultActionConfigurator):
|
|
|
431
430
|
'resample', FIELD_INT, 'Resample frame stack', required=False,
|
|
432
431
|
default=1, min_val=1, max_val=100)
|
|
433
432
|
self.add_field(
|
|
434
|
-
'
|
|
435
|
-
default
|
|
433
|
+
'reference_index', FIELD_REF_IDX, 'Reference frame', required=False,
|
|
434
|
+
default=0)
|
|
436
435
|
self.add_field(
|
|
437
436
|
'step_process', FIELD_BOOL, 'Step process', required=False,
|
|
438
437
|
default=True)
|
shinestacker/gui/gui_images.py
CHANGED
|
@@ -67,13 +67,13 @@ class GuiImageView(QWidget):
|
|
|
67
67
|
super().__init__(parent)
|
|
68
68
|
self.file_path = file_path
|
|
69
69
|
self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
|
|
70
|
-
self.
|
|
71
|
-
self.
|
|
72
|
-
self.
|
|
70
|
+
self.main_layout = QVBoxLayout()
|
|
71
|
+
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
72
|
+
self.main_layout.setSpacing(0)
|
|
73
73
|
self.image_label = QLabel()
|
|
74
74
|
self.image_label.setAlignment(Qt.AlignCenter)
|
|
75
|
-
self.
|
|
76
|
-
self.setLayout(self.
|
|
75
|
+
self.main_layout.addWidget(self.image_label)
|
|
76
|
+
self.setLayout(self.main_layout)
|
|
77
77
|
pixmap = QPixmap(file_path)
|
|
78
78
|
if pixmap:
|
|
79
79
|
scaled_pixmap = pixmap.scaledToWidth(
|
|
@@ -105,13 +105,13 @@ class GuiOpenApp(QWidget):
|
|
|
105
105
|
self.file_path = file_path
|
|
106
106
|
self.app = app
|
|
107
107
|
self.setFixedWidth(gui_constants.GUI_IMG_WIDTH)
|
|
108
|
-
self.
|
|
109
|
-
self.
|
|
110
|
-
self.
|
|
108
|
+
self.main_layout = QVBoxLayout()
|
|
109
|
+
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
|
110
|
+
self.main_layout.setSpacing(0)
|
|
111
111
|
self.image_label = QLabel()
|
|
112
112
|
self.image_label.setAlignment(Qt.AlignCenter)
|
|
113
|
-
self.
|
|
114
|
-
self.setLayout(self.
|
|
113
|
+
self.main_layout.addWidget(self.image_label)
|
|
114
|
+
self.setLayout(self.main_layout)
|
|
115
115
|
pixmap = QPixmap(file_path)
|
|
116
116
|
if pixmap:
|
|
117
117
|
scaled_pixmap = pixmap.scaledToWidth(
|
shinestacker/gui/gui_run.py
CHANGED
|
@@ -328,7 +328,7 @@ class RunWorker(LogWorker):
|
|
|
328
328
|
self.status_signal.emit(f"{self.tag} running...", constants.RUN_ONGOING, "", 0)
|
|
329
329
|
self.html_signal.emit(f'''
|
|
330
330
|
<div style="margin: 2px 0; font-family: {constants.LOG_FONTS_STR};">
|
|
331
|
-
<span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-
|
|
331
|
+
<span style="color: #{ColorPalette.DARK_BLUE.hex()}; font-style: italic; font-weight: bold;">{self.tag} begins</span>
|
|
332
332
|
</div>
|
|
333
333
|
''') # noqa
|
|
334
334
|
status, error_message = self.do_run()
|