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.

Files changed (43) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +148 -115
  3. shinestacker/algorithms/align_auto.py +64 -0
  4. shinestacker/algorithms/align_parallel.py +296 -0
  5. shinestacker/algorithms/balance.py +14 -13
  6. shinestacker/algorithms/base_stack_algo.py +11 -2
  7. shinestacker/algorithms/multilayer.py +14 -15
  8. shinestacker/algorithms/noise_detection.py +13 -14
  9. shinestacker/algorithms/pyramid.py +4 -4
  10. shinestacker/algorithms/pyramid_auto.py +16 -10
  11. shinestacker/algorithms/pyramid_tiles.py +19 -11
  12. shinestacker/algorithms/stack.py +30 -26
  13. shinestacker/algorithms/stack_framework.py +200 -178
  14. shinestacker/algorithms/vignetting.py +16 -13
  15. shinestacker/app/main.py +7 -3
  16. shinestacker/config/constants.py +63 -26
  17. shinestacker/config/gui_constants.py +1 -1
  18. shinestacker/core/core_utils.py +4 -0
  19. shinestacker/core/framework.py +114 -33
  20. shinestacker/gui/action_config.py +57 -5
  21. shinestacker/gui/action_config_dialog.py +156 -17
  22. shinestacker/gui/base_form_dialog.py +2 -2
  23. shinestacker/gui/folder_file_selection.py +101 -0
  24. shinestacker/gui/gui_images.py +10 -10
  25. shinestacker/gui/gui_run.py +13 -11
  26. shinestacker/gui/main_window.py +10 -5
  27. shinestacker/gui/menu_manager.py +4 -0
  28. shinestacker/gui/new_project.py +171 -74
  29. shinestacker/gui/project_controller.py +13 -9
  30. shinestacker/gui/project_converter.py +4 -2
  31. shinestacker/gui/project_editor.py +72 -53
  32. shinestacker/gui/select_path_widget.py +1 -1
  33. shinestacker/gui/sys_mon.py +96 -0
  34. shinestacker/gui/tab_widget.py +3 -3
  35. shinestacker/gui/time_progress_bar.py +4 -3
  36. shinestacker/retouch/exif_data.py +1 -1
  37. shinestacker/retouch/image_editor_ui.py +2 -0
  38. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/METADATA +6 -6
  39. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/RECORD +43 -39
  40. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/WHEEL +0 -0
  41. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/entry_points.txt +0 -0
  42. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/licenses/LICENSE +0 -0
  43. {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, ActionList
7
+ from .. core.framework import Job, SequentialTask
8
8
  from .. core.core_utils import check_path_exists
9
- from .. core.exceptions import ShapeError, BitDepthError, RunStopException
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
- if input_path == '':
18
- self.paths = []
19
- else:
20
- self.paths = [input_path]
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 FramePaths:
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.output_dir = None
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
- def folder_filelist(self):
47
- assert False, "this method should be overwritten"
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
- self.filenames = self.folder_filelist()
54
- file_folder = self.input_full_path.replace(self.working_path, '').lstrip('/')
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
- if self.output_path == '':
64
- self.output_path = self.name
65
- self.output_dir = self.working_path + \
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(self.output_dir)
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(self.output_dir, filename)
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 len(job.paths) == 0:
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.paths[-1]
101
- job.paths.append(self.output_path)
102
-
103
-
104
- class FrameDirectory(FramePaths):
105
- def __init__(self, name, **kwargs):
106
- FramePaths.__init__(self, name, **kwargs)
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
- list(self.input_full_path.replace(self.working_path, '').lstrip('/')))
112
- return "folder" + ('s' if len(self.input_full_path) > 1 else '') + f": {file_list}"
113
- return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
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
- 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)
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._idx = None
197
- self._ref_idx = None
198
- self._idx_step = None
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
- ActionList.begin(self)
191
+ SequentialTask.begin(self)
202
192
  self.set_filelist()
203
- self.set_counts(len(self.filenames))
204
- if self.ref_idx == -1:
205
- self.ref_idx = len(self.filenames) // 2
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
- ActionList.end(self)
207
+ SequentialTask.end(self)
209
208
 
210
209
  def run_frame(self, _idx, _ref_idx):
211
- pass
212
-
213
- def run_step(self):
214
- if self.count == 0:
215
- self._idx = self.ref_idx if self.step_process else 0
216
- self._ref_idx = self.ref_idx
217
- self._idx_step = +1
218
- ll = len(self.filenames)
219
- self.print_message_r(
220
- color_str(f"step {self.count + 1}/{ll}: process file: {self.filenames[self._idx]}, "
221
- f"reference: {self.filenames[self._ref_idx]}", constants.LOG_COLOR_LEVEL_2))
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(self._idx, self._ref_idx)
224
- if self._idx < ll:
225
- if self.step_process:
226
- self._ref_idx = self._idx
227
- self._idx += self._idx_step
228
- if self._idx == ll:
229
- self._idx = self.ref_idx - 1
230
- if self.step_process:
231
- self._ref_idx = self.ref_idx
232
- self._idx_step = -1
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(FramesRefActions):
261
+ class CombinedActions(ReferenceFrameTask):
247
262
  def __init__(self, name, actions=[], enabled=True, **kwargs):
248
- FramesRefActions.__init__(self, name, enabled, **kwargs)
263
+ ReferenceFrameTask.__init__(self, name, enabled, **kwargs)
249
264
  self._actions = actions
250
- self.dtype = None
251
- self.shape = None
265
+ self._metadata = (None, None)
252
266
 
253
267
  def begin(self):
254
- FramesRefActions.begin(self)
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
- filename = self.filenames[idx]
261
- img = read_img((self.output_dir
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: {self.input_full_path}/{filename}")
265
- self.dtype = img.dtype
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
- filename = self.filenames[idx]
271
- self.sub_message_r(color_str(': read input image', constants.LOG_COLOR_LEVEL_3))
272
- img = read_img(f"{self.input_full_path}/{filename}")
273
- if self.dtype is not None and img.dtype != self.dtype:
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)
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: {self.input_full_path}/{filename}")
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
- 'red'))
297
+ constants.LOG_COLOR_ALERT))
286
298
  else:
287
- if self.callback('check_running', self.id, self.name) is False:
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
- write_img(self.output_dir + "/" + filename, img)
299
- else:
300
- self.print_message(color_str(
301
- "no output file resulted from processing input file: "
302
- f"{self.input_full_path}/{filename}",
303
- constants.LOG_COLOR_ALERT), level=logging.WARNING)
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.sub_message_r(color_str(": compute vignetting", "cyan"))
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.sub_message(color_str(": vignetting model parameters: ", "cyan") +
157
- color_str(f"i0={i0_fit / 2:.4f}, "
158
- f"k={k_fit * self.r_max:.4f}, "
159
- f"r0={r0_fit / self.r_max:.4f}",
160
- "light_blue"),
161
- level=logging.DEBUG)
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=(10, 5))
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
- 'save_plot', self.process.id,
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.sub_message_r(color_str(": correct vignetting", "cyan"))
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.counts, None, dtype=float)
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=(10, 5))
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('save_plot', self.process.id,
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
- self.project_window.quit()
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.main_app.quit()
198
+ if not self.quit():
199
+ return True
196
200
  return super().event(event)
197
201
 
198
202