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
@@ -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"
@@ -35,19 +37,25 @@ class _Constants:
35
37
  ACTION_VIGNETTING = "Vignetting"
36
38
  ACTION_ALIGNFRAMES = "AlignFrames"
37
39
  ACTION_BALANCEFRAMES = "BalanceFrames"
38
- SUB_ACTION_TYPES = [ACTION_MASKNOISE, ACTION_VIGNETTING, ACTION_ALIGNFRAMES,
39
- ACTION_BALANCEFRAMES]
40
+ SUB_ACTION_TYPES = [ACTION_ALIGNFRAMES, ACTION_BALANCEFRAMES,
41
+ ACTION_VIGNETTING, ACTION_MASKNOISE]
40
42
  STACK_ALGO_PYRAMID = 'Pyramid'
41
43
  STACK_ALGO_DEPTH_MAP = 'Depth map'
42
44
  STACK_ALGO_OPTIONS = [STACK_ALGO_PYRAMID, STACK_ALGO_DEPTH_MAP]
43
45
  STACK_ALGO_DEFAULT = STACK_ALGO_PYRAMID
44
- DEFAULT_PLOTS_PATH = 'plots'
45
46
 
46
- FIELD_SUBSAMPLE_VALUES_1 = [2, 3, 4, 6, 8, 12, 16, 24, 32]
47
- FIELD_SUBSAMPLE_OPTIONS_1 = [f"1/{n} × 1/{n}" for n in FIELD_SUBSAMPLE_VALUES_1]
48
- FIELD_SUBSAMPLE_VALUES = [0, 1] + FIELD_SUBSAMPLE_VALUES_1
49
- FIELD_SUBSAMPLE_OPTIONS = ['Auto', 'Full resolution'] + FIELD_SUBSAMPLE_OPTIONS_1
50
- FIELD_SUBSAMPLE_DEFAULT = FIELD_SUBSAMPLE_VALUES[0]
47
+ ACTION_ICONS = {
48
+ ACTION_JOB: '',
49
+ ACTION_COMBO: '',
50
+ ACTION_NOISEDETECTION: '',
51
+ ACTION_FOCUSSTACK: '',
52
+ ACTION_FOCUSSTACKBUNCH: '',
53
+ ACTION_MULTILAYER: '',
54
+ ACTION_MASKNOISE: '',
55
+ ACTION_VIGNETTING: '',
56
+ ACTION_ALIGNFRAMES: '',
57
+ ACTION_BALANCEFRAMES: ''
58
+ }
51
59
 
52
60
  PATH_SEPARATOR = ';'
53
61
 
@@ -58,10 +66,39 @@ class _Constants:
58
66
  LOG_COLOR_LEVEL_2 = 'magenta'
59
67
  LOG_COLOR_LEVEL_3 = 'cyan'
60
68
 
69
+ STATUS_RUNNING = 1
70
+ STATUS_PAUSED = 2
71
+ STATUS_STOPPED = 3
72
+
73
+ RUN_COMPLETED = 0
74
+ RUN_ONGOING = 1
75
+ RUN_FAILED = 2
76
+ RUN_STOPPED = 3
77
+
78
+ CALLBACK_BEFORE_ACTION = 'before_action'
79
+ CALLBACK_AFTER_ACTION = 'after_action'
80
+ CALLBACK_STEP_COUNTS = 'step_counts'
81
+ CALLBACK_BEGIN_STEPS = 'begin_steps'
82
+ CALLBACK_END_STEPS = 'end_steps'
83
+ CALLBACK_AFTER_STEP = 'after_step'
84
+ CALLBACK_CHECK_RUNNING = 'check_running'
85
+ CALLBACK_SAVE_PLOT = 'save_plot'
86
+ CALLBACK_OPEN_APP = 'open_app'
87
+
61
88
  DEFAULT_FILE_REVERSE_ORDER = False
62
89
  DEFAULT_MULTILAYER_FILE_REVERSE_ORDER = True
63
90
  MULTILAYER_WARNING_MEM_GB = 1
64
91
 
92
+ DEFAULT_PLOTS_PATH = 'plots'
93
+ DEFAULT_MAX_FWK_THREADS = 8
94
+ DEFAULT_MAX_FWK_CHUNK_SUBMIT = True
95
+
96
+ FIELD_SUBSAMPLE_VALUES_1 = [2, 3, 4, 6, 8, 12, 16, 24, 32]
97
+ FIELD_SUBSAMPLE_OPTIONS_1 = [f"1/{n} × 1/{n}" for n in FIELD_SUBSAMPLE_VALUES_1]
98
+ FIELD_SUBSAMPLE_VALUES = [0, 1] + FIELD_SUBSAMPLE_VALUES_1
99
+ FIELD_SUBSAMPLE_OPTIONS = ['Auto', 'Full resolution'] + FIELD_SUBSAMPLE_OPTIONS_1
100
+ FIELD_SUBSAMPLE_DEFAULT = FIELD_SUBSAMPLE_VALUES[0]
101
+
65
102
  DEFAULT_NOISE_MAP_FILENAME = "noise-map/hot_pixels.png"
66
103
  DEFAULT_NOISE_MAX_FRAMES = 10
67
104
  DEFAULT_MN_KERNEL_SIZE = 3
@@ -102,9 +139,9 @@ class _Constants:
102
139
  NOKNN_METHODS = {'detectors': [DETECTOR_ORB, DETECTOR_SURF, DETECTOR_AKAZE, DETECTOR_BRISK],
103
140
  'descriptors': [DESCRIPTOR_ORB, DESCRIPTOR_AKAZE, DESCRIPTOR_BRISK]}
104
141
 
105
- DEFAULT_DETECTOR = DETECTOR_SIFT
106
- DEFAULT_DESCRIPTOR = DESCRIPTOR_SIFT
107
- DEFAULT_MATCHING_METHOD = MATCHING_KNN
142
+ DEFAULT_DETECTOR = DETECTOR_ORB
143
+ DEFAULT_DESCRIPTOR = DESCRIPTOR_ORB
144
+ DEFAULT_MATCHING_METHOD = MATCHING_NORM_HAMMING
108
145
  DEFAULT_FLANN_IDX_KDTREE = 2
109
146
  DEFAULT_FLANN_TREES = 5
110
147
  DEFAULT_FLANN_CHECKS = 50
@@ -116,13 +153,19 @@ class _Constants:
116
153
  DEFAULT_REFINE_ITERS = 100
117
154
  DEFAULT_ALIGN_CONFIDENCE = 99.9
118
155
  DEFAULT_ALIGN_MAX_ITERS = 2000
119
- DEFAULT_ALIGN_ABORT_ABNORMAL = True
156
+ DEFAULT_ALIGN_ABORT_ABNORMAL = False
120
157
  DEFAULT_BORDER_VALUE = [0] * 4
121
158
  DEFAULT_BORDER_BLUR = 50
122
159
  DEFAULT_ALIGN_SUBSAMPLE = 0
123
160
  DEFAULT_ALIGN_RES_TARGET_MPX = 2
124
161
  DEFAULT_ALIGN_FAST_SUBSAMPLING = False
125
- DEFAULT_ALIGN_MIN_GOOD_MATCHES = 50
162
+ DEFAULT_ALIGN_MIN_GOOD_MATCHES = 20
163
+ ALIGN_VALID_MODES = ['auto', 'sequential', 'parallel']
164
+ DEFAULT_ALIGN_MODE = 'auto'
165
+ DEFAULT_ALIGN_MEMORY_LIMIT_GB = 8
166
+ DEFAULT_ALIGN_MAX_THREADS = min(os.cpu_count() or 4, 8)
167
+ DEFAULT_ALIGN_CHUNK_SUBMIT = True
168
+ DEFAULT_ALIGN_BW_MATCHING = False
126
169
 
127
170
  BALANCE_LINEAR = "LINEAR"
128
171
  BALANCE_GAMMA = "GAMMA"
@@ -160,13 +203,13 @@ class _Constants:
160
203
  DEFAULT_STACK_PREFIX = "stack_"
161
204
  DEFAULT_BUNCH_PREFIX = "bunch_"
162
205
 
163
- DEFAULT_DM_FLOAT = FLOAT_32
164
206
  DM_ENERGY_LAPLACIAN = "laplacian"
165
207
  DM_ENERGY_SOBEL = "sobel"
166
208
  DM_MAP_AVERAGE = "average"
167
209
  DM_MAP_MAX = "max"
168
210
  VALID_DM_MAP = [DM_MAP_AVERAGE, DM_MAP_MAX]
169
211
  VALID_DM_ENERGY = [DM_ENERGY_LAPLACIAN, DM_ENERGY_SOBEL]
212
+ DEFAULT_DM_FLOAT = FLOAT_32
170
213
  DEFAULT_DM_MAP = DM_MAP_AVERAGE
171
214
  DEFAULT_DM_ENERGY = DM_ENERGY_LAPLACIAN
172
215
  DEFAULT_DM_KERNEL_SIZE = 5
@@ -183,22 +226,16 @@ class _Constants:
183
226
  DEFAULT_PY_N_TILED_LAYERS = 2
184
227
  DEFAULT_PY_MEMORY_LIMIT_GB = 8
185
228
  DEFAULT_PY_MAX_THREADS = min(os.cpu_count() or 4, 8)
186
- DEFAULT_PY_MODE = 'auto'
187
229
  PY_VALID_MODES = ['auto', 'memory', 'tiled']
188
- MIN_PY_TILE_SIZE = 256
230
+ DEFAULT_PY_MODE = 'auto'
231
+ DEFAULT_PY_MAX_TILE_SIZE = 4096
232
+ DEFAULT_PY_MIN_TILE_SIZE = 128
233
+ DEFAULT_PY_MIN_N_TILED_LAYERS = 1
234
+ PY_MEMORY_OVERHEAD = 2.5
189
235
 
190
- DEFAULT_PLOT_STACK_BUNCH = False
236
+ DEFAULT_PLOT_STACK_BUNCH = True
191
237
  DEFAULT_PLOT_STACK = True
192
238
 
193
- STATUS_RUNNING = 1
194
- STATUS_PAUSED = 2
195
- STATUS_STOPPED = 3
196
-
197
- RUN_COMPLETED = 0
198
- RUN_ONGOING = 1
199
- RUN_FAILED = 2
200
- RUN_STOPPED = 3
201
-
202
239
  def __setattr__aux(self, name, value):
203
240
  raise AttributeError(f"Can't reassign constant '{name}'")
204
241
 
@@ -16,7 +16,7 @@ class _GuiConstants:
16
16
  NEW_PROJECT_ALIGN_FRAMES = True
17
17
  NEW_PROJECT_BALANCE_FRAMES = True
18
18
  NEW_PROJECT_BUNCH_STACK = False
19
- NEW_PROJECT_BUNCH_FRAMES = {'min': 2, 'max': 20}
19
+ NEW_PROJECT_BUNCH_FRAMES = {'min': 1, 'max': 20}
20
20
  NEW_PROJECT_BUNCH_OVERLAP = {'min': 0, 'max': 10}
21
21
  NEW_PROJECT_FOCUS_STACK_PYRAMID = True
22
22
  NEW_PROJECT_FOCUS_STACK_DEPTH_MAP = False
@@ -50,3 +50,7 @@ def running_under_macos() -> bool:
50
50
 
51
51
  def running_under_linux() -> bool:
52
52
  return platform.system().lower() == 'linux'
53
+
54
+
55
+ def make_chunks(ll, max_size):
56
+ return [ll[i:i + max_size] for i in range(0, len(ll), max_size)]
@@ -1,15 +1,17 @@
1
- # pylint: disable=C0114, C0115, C0116, R0917, R0913, R0902
1
+ # pylint: disable=C0114, C0115, C0116, R0917, R0913, R0902, W0718
2
2
  import time
3
3
  import logging
4
+ import traceback
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
4
6
  from .. config.constants import constants
5
7
  from .. config.config import config
6
8
  from .colors import color_str
7
9
  from .logging import setup_logging
8
- from .core_utils import make_tqdm_bar
10
+ from .core_utils import make_tqdm_bar, make_chunks
9
11
  from .exceptions import RunStopException
10
12
 
11
13
  LINE_UP = "\r\033[A"
12
- TRAILING_SPACES = " " * 30
14
+ TRAILING_SPACES = " " * 50
13
15
 
14
16
 
15
17
  class TqdmCallbacks:
@@ -24,7 +26,7 @@ class TqdmCallbacks:
24
26
 
25
27
  def __init__(self):
26
28
  self.tbar = None
27
- self.counts = -1
29
+ self.total_action_counts = -1
28
30
 
29
31
  @classmethod
30
32
  def instance(cls):
@@ -33,8 +35,8 @@ class TqdmCallbacks:
33
35
  return cls._instance
34
36
 
35
37
  def step_counts(self, name, counts):
36
- self.counts = counts
37
- self.tbar = make_tqdm_bar(name, self.counts)
38
+ self.total_action_counts = counts
39
+ self.tbar = make_tqdm_bar(name, self.total_action_counts)
38
40
 
39
41
  def begin_steps(self, name):
40
42
  pass
@@ -62,7 +64,7 @@ def elapsed_time_str(start):
62
64
  return f"{hh:02d}:{mm:02d}:{ss:05.2f}s"
63
65
 
64
66
 
65
- class JobBase:
67
+ class TaskBase:
66
68
  def __init__(self, name, enabled=True):
67
69
  self.id = -1
68
70
  self.name = name
@@ -77,8 +79,7 @@ class JobBase:
77
79
  self.begin_r, self.end_r = LINE_UP, None
78
80
 
79
81
  def callback(self, key, *args):
80
- has_callbacks = hasattr(self, 'callbacks')
81
- if has_callbacks and self.callbacks is not None:
82
+ if self.callbacks is not None:
82
83
  callback = self.callbacks.get(key, None)
83
84
  if callback:
84
85
  return callback(*args)
@@ -92,9 +93,9 @@ class JobBase:
92
93
  if not self.enabled:
93
94
  self.get_logger().warning(color_str(self.name + ": entire job disabled",
94
95
  constants.LOG_COLOR_ALERT))
95
- self.callback('before_action', self.id, self.name)
96
+ self.callback(constants.CALLBACK_BEFORE_ACTION, self.id, self.name)
96
97
  self.run_core()
97
- self.callback('after_action', self.id, self.name)
98
+ self.callback(constants.CALLBACK_AFTER_ACTION, self.id, self.name)
98
99
  msg_name = color_str(self.name + ":", constants.LOG_COLOR_LEVEL_JOB, "bold")
99
100
  msg_time = color_str(f"elapsed time: {elapsed_time_str(self._t0)}",
100
101
  constants.LOG_COLOR_LEVEL_JOB)
@@ -146,9 +147,9 @@ class JobBase:
146
147
  self.sub_message(msg, level, self.end_r, self.begin_r, False)
147
148
 
148
149
 
149
- class Job(JobBase):
150
+ class Job(TaskBase):
150
151
  def __init__(self, name, logger_name=None, log_file='', callbacks=None, **kwargs):
151
- JobBase.__init__(self, name, **kwargs)
152
+ TaskBase.__init__(self, name, **kwargs)
152
153
  self.action_counter = 0
153
154
  self.__actions = []
154
155
  if logger_name is None:
@@ -163,7 +164,7 @@ class Job(JobBase):
163
164
  def init(self, a):
164
165
  pass
165
166
 
166
- def add_action(self, a: JobBase):
167
+ def add_action(self, a: TaskBase):
167
168
  a.id = self.action_counter
168
169
  self.action_counter += 1
169
170
  a.logger = self.logger
@@ -183,47 +184,127 @@ class Job(JobBase):
183
184
  self.get_logger().warning(color_str(a.name + f": {msg} disabled",
184
185
  constants.LOG_COLOR_ALERT))
185
186
  else:
186
- if self.callback('check_running', self.id, self.name) is False:
187
+ if self.callback(constants.CALLBACK_CHECK_RUNNING,
188
+ self.id, self.name) is False:
187
189
  raise RunStopException(self.name)
188
190
  a.run()
189
191
 
190
192
 
191
- class ActionList(JobBase):
193
+ class SequentialTask(TaskBase):
192
194
  def __init__(self, name, enabled=True, **kwargs):
193
- JobBase.__init__(self, name, enabled, **kwargs)
194
- self.counts = None
195
- self.count = None
195
+ self.max_threads = kwargs.pop('max_threads', constants.DEFAULT_MAX_FWK_THREADS)
196
+ self.chunk_submit = kwargs.pop('chunk_submit', constants.DEFAULT_MAX_FWK_CHUNK_SUBMIT)
197
+ TaskBase.__init__(self, name, enabled, **kwargs)
198
+ self.total_action_counts = None
199
+ self.current_action_count = None
200
+ self.begin_steps = 0
196
201
 
197
202
  def set_counts(self, counts):
198
- self.counts = counts
199
- self.callback('step_counts', self.id, self.name, self.counts)
203
+ self.total_action_counts = counts
204
+ self.callback(constants.CALLBACK_STEP_COUNTS,
205
+ self.id, self.name, self.total_action_counts)
206
+
207
+ def add_begin_steps(self, steps):
208
+ self.begin_steps += steps
200
209
 
201
210
  def begin(self):
202
- self.callback('begin_steps', self.id, self.name)
211
+ self.callback(constants.CALLBACK_BEGIN_STEPS, self.id, self.name)
203
212
 
204
213
  def end(self):
205
- self.callback('end_steps', self.id, self.name)
214
+ self.callback(constants.CALLBACK_END_STEPS, self.id, self.name)
206
215
 
207
216
  def __iter__(self):
208
- self.count = 0
217
+ self.current_action_count = 0
209
218
  return self
210
219
 
211
- def run_step(self):
220
+ def run_step(self, action_count=-1):
212
221
  pass
213
222
 
214
223
  def __next__(self):
215
- if self.count < self.counts:
216
- self.run_step()
217
- x = self.count
218
- self.count += 1
224
+ if self.current_action_count < self.total_action_counts:
225
+ self.run_step(self.current_action_count)
226
+ x = self.current_action_count
227
+ self.current_action_count += 1
219
228
  return x
220
229
  raise StopIteration
221
230
 
231
+ def check_running(self):
232
+ if self.callback(constants.CALLBACK_CHECK_RUNNING,
233
+ self.id, self.name) is False:
234
+ raise RunStopException(self.name)
235
+
236
+ def after_step(self, step=-1):
237
+ if step == -1:
238
+ step = self.current_action_count + self.begin_steps
239
+ self.callback(constants.CALLBACK_AFTER_STEP, self.id, self.name, step)
240
+
241
+ def run_core_serial(self):
242
+ for _ in iter(self):
243
+ self.after_step()
244
+ self.check_running()
245
+
246
+ def idx_tot_str(self, idx):
247
+ return f"{idx + 1}/{self.total_action_counts}"
248
+
249
+ def run_core_parallel_single_chunk(self, idx_chunk):
250
+ with ThreadPoolExecutor(max_workers=self.max_threads) as executor:
251
+ future_to_index = {}
252
+ for idx in idx_chunk:
253
+ self.print_message(color_str(
254
+ f"submit processing step: {self.idx_tot_str(idx)}",
255
+ constants.LOG_COLOR_LEVEL_1))
256
+ future = executor.submit(self.run_step, idx)
257
+ future_to_index[future] = idx
258
+ self.check_running()
259
+ for future in as_completed(future_to_index):
260
+ idx = future_to_index[future]
261
+ try:
262
+ result = future.result()
263
+ if result:
264
+ self.print_message(color_str(
265
+ f"completed processing step: {self.idx_tot_str(idx)}",
266
+ constants.LOG_COLOR_LEVEL_1))
267
+ else:
268
+ self.print_message(color_str(
269
+ f"failed processing step: {self.idx_tot_str(idx)}",
270
+ constants.LOG_COLOR_WARNING))
271
+ self.current_action_count += 1
272
+ self.after_step()
273
+ self.check_running()
274
+ except RunStopException as e:
275
+ raise e
276
+ except Exception as e:
277
+ traceback.print_tb(e.__traceback__)
278
+ self.print_message(color_str(
279
+ f"failed processing step: {idx + 1}: {str(e)}",
280
+ constants.LOG_COLOR_ALERT))
281
+
282
+ def run_core_parallel(self):
283
+ self.current_action_count = 0
284
+ self.run_core_parallel_single_chunk(list(range(self.total_action_counts)))
285
+
286
+ def run_core_parallel_chunks(self):
287
+ self.current_action_count = 0
288
+ action_idx_list = list(range(self.total_action_counts))
289
+ max_chunck_size = self.max_threads
290
+ action_idx_chunks = make_chunks(action_idx_list, max_chunck_size)
291
+ for idx_chunk in action_idx_chunks:
292
+ self.run_core_parallel_single_chunk(idx_chunk)
293
+
222
294
  def run_core(self):
223
295
  self.print_message(color_str('begin run', constants.LOG_COLOR_LEVEL_2), end='\n')
224
296
  self.begin()
225
- for _ in iter(self):
226
- self.callback('after_step', self.id, self.name, self.count)
227
- if self.callback('check_running', self.id, self.name) is False:
228
- raise RunStopException(self.name)
297
+ if self.run_sequential():
298
+ self.run_core_serial()
299
+ else:
300
+ if self.chunk_submit:
301
+ self.run_core_parallel_chunks()
302
+ else:
303
+ self.run_core_parallel()
229
304
  self.end()
305
+
306
+ def sequential_processing(self):
307
+ return False
308
+
309
+ def run_sequential(self):
310
+ return self.sequential_processing() or self.max_threads == 1
@@ -1,8 +1,7 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0913, R0917, R0915, R0912
2
- # pylint: disable=E0606, W0718, R1702, W0102, W0221
2
+ # pylint: disable=E0606, W0718, R1702, W0102, W0221, R0914, E1121
3
3
  import traceback
4
4
  from abc import ABC, abstractmethod
5
- from typing import Dict, Any
6
5
  import os.path
7
6
  from PySide6.QtWidgets import (QPushButton, QHBoxLayout, QFileDialog, QLabel, QComboBox,
8
7
  QMessageBox, QSizePolicy, QLineEdit, QSpinBox,
@@ -16,12 +15,16 @@ FIELD_ABS_PATH = 'abs_path'
16
15
  FIELD_REL_PATH = 'rel_path'
17
16
  FIELD_FLOAT = 'float'
18
17
  FIELD_INT = 'int'
18
+ FIELD_REF_IDX = 'ref_idx'
19
19
  FIELD_INT_TUPLE = 'int_tuple'
20
20
  FIELD_BOOL = 'bool'
21
21
  FIELD_COMBO = 'combo'
22
22
  FIELD_TYPES = [FIELD_TEXT, FIELD_ABS_PATH, FIELD_REL_PATH, FIELD_FLOAT,
23
23
  FIELD_INT, FIELD_INT_TUPLE, FIELD_BOOL, FIELD_COMBO]
24
24
 
25
+ FIELD_REF_IDX_OPTIONS = ['Median frame', 'First frame', 'Last frame', 'Specify index']
26
+ FIELD_REF_IDX_MAX = 1000
27
+
25
28
 
26
29
  class ActionConfigurator(ABC):
27
30
  def __init__(self, expert, current_wd):
@@ -56,6 +59,8 @@ class FieldBuilder:
56
59
  widget = self.create_float_field(tag, **kwargs)
57
60
  elif field_type == FIELD_INT:
58
61
  widget = self.create_int_field(tag, **kwargs)
62
+ elif field_type == FIELD_REF_IDX:
63
+ widget = self.create_ref_idx_field(tag, **kwargs)
59
64
  elif field_type == FIELD_INT_TUPLE:
60
65
  widget = self.create_int_tuple_field(tag, **kwargs)
61
66
  elif field_type == FIELD_BOOL:
@@ -75,6 +80,8 @@ class FieldBuilder:
75
80
  default_value = kwargs.get('default', 0.0)
76
81
  elif field_type == FIELD_INT:
77
82
  default_value = kwargs.get('default', 0)
83
+ elif field_type == FIELD_REF_IDX:
84
+ default_value = kwargs.get('default', 0)
78
85
  elif field_type == FIELD_INT_TUPLE:
79
86
  default_value = kwargs.get('default', [0] * kwargs.get('size', 1))
80
87
  elif field_type == FIELD_BOOL:
@@ -112,12 +119,15 @@ class FieldBuilder:
112
119
  widget.setChecked(default)
113
120
  elif field['type'] == FIELD_INT:
114
121
  widget.setValue(default)
122
+ elif field['type'] == FIELD_REF_IDX:
123
+ widget.layout().itemAt(2).widget().setValue(default)
124
+ widget.layout().itemAt(0).widget().setCurrentText(FIELD_REF_IDX_OPTIONS[0])
115
125
  elif field['type'] == FIELD_INT_TUPLE:
116
126
  for i in range(field['size']):
117
127
  spinbox = widget.layout().itemAt(1 + i * 2).widget()
118
128
  spinbox.setValue(default[i])
119
129
  elif field['type'] == FIELD_COMBO:
120
- widget.setCurrentText(default)
130
+ widget.setCurrentText(str(default))
121
131
 
122
132
  def get_path_widget(self, widget):
123
133
  return widget.layout().itemAt(0).widget()
@@ -134,7 +144,7 @@ class FieldBuilder:
134
144
  parent = parent.parent
135
145
  return ''
136
146
 
137
- def update_params(self, params: Dict[str, Any]) -> bool:
147
+ def update_params(self, params):
138
148
  for tag, field in self.fields.items():
139
149
  if field['type'] == FIELD_TEXT:
140
150
  params[tag] = field['widget'].text()
@@ -146,6 +156,17 @@ class FieldBuilder:
146
156
  params[tag] = field['widget'].isChecked()
147
157
  elif field['type'] == FIELD_INT:
148
158
  params[tag] = field['widget'].value()
159
+ elif field['type'] == FIELD_REF_IDX:
160
+ wl = field['widget'].layout()
161
+ txt = wl.itemAt(0).widget().currentText()
162
+ if txt == FIELD_REF_IDX_OPTIONS[0]:
163
+ params[tag] = 0
164
+ elif txt == FIELD_REF_IDX_OPTIONS[1]:
165
+ params[tag] = 1
166
+ elif txt == FIELD_REF_IDX_OPTIONS[2]:
167
+ params[tag] = -1
168
+ else:
169
+ params[tag] = wl.itemAt(2).widget().value()
149
170
  elif field['type'] == FIELD_INT_TUPLE:
150
171
  params[tag] = [field['widget'].layout().itemAt(1 + i * 2).widget().value()
151
172
  for i in range(field['size'])]
@@ -202,7 +223,7 @@ class FieldBuilder:
202
223
  self.action.params.get(tag, ''),
203
224
  kwargs.get('placeholder', ''),
204
225
  tag.replace('_', ' ')
205
- )[1]
226
+ )
206
227
 
207
228
  def create_rel_path_field(self, tag, **kwargs):
208
229
  value = self.action.params.get(tag, kwargs.get('default', ''))
@@ -327,6 +348,37 @@ class FieldBuilder:
327
348
  spin.setValue(self.action.params.get(tag, default))
328
349
  return spin
329
350
 
351
+ def create_ref_idx_field(self, tag, default=0):
352
+ layout = QHBoxLayout()
353
+ combo = QComboBox()
354
+ combo.addItems(FIELD_REF_IDX_OPTIONS)
355
+ label = QLabel("index [1, ..., N]: ")
356
+ spin = QSpinBox()
357
+ spin.setRange(1, FIELD_REF_IDX_MAX)
358
+ value = self.action.params.get(tag, default)
359
+ if value == 0:
360
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[0])
361
+ spin.setValue(1)
362
+ elif value == 1:
363
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[1])
364
+ spin.setValue(1)
365
+ elif value == -1:
366
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[2])
367
+ spin.setValue(1)
368
+ else:
369
+ combo.setCurrentText(FIELD_REF_IDX_OPTIONS[3])
370
+ spin.setValue(value)
371
+
372
+ def set_enabled():
373
+ spin.setEnabled(combo.currentText() == FIELD_REF_IDX_OPTIONS[-1])
374
+
375
+ combo.currentTextChanged.connect(set_enabled)
376
+ set_enabled()
377
+ layout.addWidget(combo)
378
+ layout.addWidget(label)
379
+ layout.addWidget(spin)
380
+ return create_layout_widget_no_margins(layout)
381
+
330
382
  def create_int_tuple_field(self, tag, size=1,
331
383
  default=[0] * 100, min_val=[0] * 100, max_val=[100] * 100,
332
384
  **kwargs):