shinestacker 1.2.1__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 (40) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +126 -94
  3. shinestacker/algorithms/align_auto.py +64 -0
  4. shinestacker/algorithms/align_parallel.py +296 -0
  5. shinestacker/algorithms/balance.py +3 -1
  6. shinestacker/algorithms/base_stack_algo.py +11 -2
  7. shinestacker/algorithms/multilayer.py +8 -8
  8. shinestacker/algorithms/noise_detection.py +10 -10
  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 +21 -17
  13. shinestacker/algorithms/stack_framework.py +97 -46
  14. shinestacker/algorithms/vignetting.py +13 -10
  15. shinestacker/app/main.py +7 -3
  16. shinestacker/config/constants.py +60 -25
  17. shinestacker/config/gui_constants.py +1 -1
  18. shinestacker/core/core_utils.py +4 -0
  19. shinestacker/core/framework.py +104 -23
  20. shinestacker/gui/action_config.py +4 -5
  21. shinestacker/gui/action_config_dialog.py +152 -12
  22. shinestacker/gui/base_form_dialog.py +2 -2
  23. shinestacker/gui/folder_file_selection.py +101 -0
  24. shinestacker/gui/gui_run.py +12 -10
  25. shinestacker/gui/main_window.py +6 -1
  26. shinestacker/gui/new_project.py +171 -73
  27. shinestacker/gui/project_controller.py +10 -6
  28. shinestacker/gui/project_converter.py +4 -2
  29. shinestacker/gui/project_editor.py +37 -27
  30. shinestacker/gui/select_path_widget.py +1 -1
  31. shinestacker/gui/sys_mon.py +96 -0
  32. shinestacker/gui/time_progress_bar.py +4 -3
  33. shinestacker/retouch/exif_data.py +1 -1
  34. shinestacker/retouch/image_editor_ui.py +2 -0
  35. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/METADATA +6 -6
  36. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/RECORD +40 -36
  37. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/WHEEL +0 -0
  38. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/entry_points.txt +0 -0
  39. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/licenses/LICENSE +0 -0
  40. {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/top_level.txt +0 -0
@@ -609,6 +609,8 @@ class BalanceFrames(SubAction):
609
609
 
610
610
  def run_frame(self, idx, _ref_idx, image):
611
611
  if idx != self.process.ref_idx:
612
- self.process.sub_message_r(color_str(': balance image', constants.LOG_COLOR_LEVEL_3))
612
+ self.process.print_message(
613
+ color_str(f'{self.process.idx_tot_str(idx)}: balance image',
614
+ constants.LOG_COLOR_LEVEL_3))
613
615
  image = self.correction.apply_correction(idx, image)
614
616
  return image
@@ -34,6 +34,13 @@ class BaseStackAlgo:
34
34
  def set_do_step_callback(self, enable):
35
35
  self.do_step_callback = enable
36
36
 
37
+ def idx_tot_str(self, idx):
38
+ return f"{idx + 1}/{len(self.filenames)}"
39
+
40
+ def image_str(self, idx):
41
+ return f"image: {self.idx_tot_str(idx)}, " \
42
+ f"{os.path.basename(self.filenames[idx])}"
43
+
37
44
  def init(self, filenames):
38
45
  self.filenames = filenames
39
46
  first_img_file = ''
@@ -61,11 +68,13 @@ class BaseStackAlgo:
61
68
  return img, metadata, updated
62
69
 
63
70
  def check_running(self, cleanup_callback=None):
64
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
71
+ if self.process.callback(constants.CALLBACK_CHECK_RUNNING,
72
+ self.process.id, self.process.name) is False:
65
73
  if cleanup_callback is not None:
66
74
  cleanup_callback()
67
75
  raise RunStopException(self.name)
68
76
 
69
77
  def after_step(self, step):
70
78
  if self.do_step_callback:
71
- self.process.callback('after_step', self.process.id, self.process.name, step)
79
+ self.process.callback(constants.CALLBACK_AFTER_STEP,
80
+ self.process.id, self.process.name, step)
@@ -12,9 +12,9 @@ from psdtags import (PsdBlendMode, PsdChannel, PsdChannelId, PsdClippingType, Ps
12
12
  from .. config.constants import constants
13
13
  from .. config.config import config
14
14
  from .. core.colors import color_str
15
- from .. core.framework import JobBase
15
+ from .. core.framework import TaskBase
16
16
  from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG
17
- from .stack_framework import FramePaths
17
+ from .stack_framework import ImageSequenceManager
18
18
  from .exif import exif_extra_tags_for_tif, get_exif
19
19
 
20
20
 
@@ -159,10 +159,10 @@ def write_multilayer_tiff_from_images(image_dict, output_file, exif_path='', cal
159
159
  compression=compression, metadata=None, **tiff_tags)
160
160
 
161
161
 
162
- class MultiLayer(JobBase, FramePaths):
162
+ class MultiLayer(TaskBase, ImageSequenceManager):
163
163
  def __init__(self, name, enabled=True, **kwargs):
164
- FramePaths.__init__(self, name, **kwargs)
165
- JobBase.__init__(self, name, enabled)
164
+ ImageSequenceManager.__init__(self, name, **kwargs)
165
+ TaskBase.__init__(self, name, enabled)
166
166
  self.exif_path = kwargs.get('exif_path', '')
167
167
  self.reverse_order = kwargs.get(
168
168
  'reverse_order',
@@ -170,9 +170,9 @@ class MultiLayer(JobBase, FramePaths):
170
170
  )
171
171
 
172
172
  def init(self, job):
173
- FramePaths.init(self, job)
173
+ ImageSequenceManager.init(self, job)
174
174
  if self.exif_path == '':
175
- self.exif_path = job.paths[0]
175
+ self.exif_path = job.action_path(0)
176
176
  if self.exif_path != '':
177
177
  self.exif_path = self.working_path + "/" + self.exif_path
178
178
 
@@ -217,4 +217,4 @@ class MultiLayer(JobBase, FramePaths):
217
217
  write_multilayer_tiff(input_files, output_file, labels=None, exif_path=self.exif_path,
218
218
  callbacks=callbacks)
219
219
  app = 'internal_retouch_app' if config.COMBINED_APP else f'{constants.RETOUCH_APP}'
220
- self.callback('open_app', self.id, self.name, app, output_file)
220
+ self.callback(constants.CALLBACK_OPEN_APP, self.id, self.name, app, output_file)
@@ -9,10 +9,10 @@ from .. config.config import config
9
9
  from .. config.constants import constants
10
10
  from .. core.colors import color_str
11
11
  from .. core.exceptions import ImageLoadError
12
- from .. core.framework import JobBase
12
+ from .. core.framework import TaskBase
13
13
  from .. core.core_utils import make_tqdm_bar
14
14
  from .. core.exceptions import RunStopException, ShapeError
15
- from .stack_framework import FramePaths, SubAction
15
+ from .stack_framework import ImageSequenceManager, SubAction
16
16
  from .utils import read_img, save_plot, get_img_metadata, validate_image
17
17
 
18
18
  MAX_NOISY_PIXELS = 1000
@@ -45,10 +45,10 @@ def mean_image(file_paths, max_frames=-1, message_callback=None, progress_callba
45
45
  return None if mean_img is None else (mean_img / counter).astype(np.uint8)
46
46
 
47
47
 
48
- class NoiseDetection(JobBase, FramePaths):
48
+ class NoiseDetection(TaskBase, ImageSequenceManager):
49
49
  def __init__(self, name="noise-map", enabled=True, **kwargs):
50
- FramePaths.__init__(self, name, **kwargs)
51
- JobBase.__init__(self, name, enabled)
50
+ ImageSequenceManager.__init__(self, name, **kwargs)
51
+ TaskBase.__init__(self, name, enabled)
52
52
  self.max_frames = kwargs.get('max_frames', constants.DEFAULT_NOISE_MAX_FRAMES)
53
53
  self.blur_size = kwargs.get('blur_size', constants.DEFAULT_BLUR_SIZE)
54
54
  self.file_name = kwargs.get('file_name', constants.DEFAULT_NOISE_MAP_FILENAME)
@@ -65,10 +65,10 @@ class NoiseDetection(JobBase, FramePaths):
65
65
  return cv2.threshold(ch, th, 255, cv2.THRESH_BINARY)[1]
66
66
 
67
67
  def progress(self, i):
68
- self.callback('after_step', self.id, self.name, i)
68
+ self.callback(constants.CALLBACK_AFTER_STEP, self.id, self.name, i)
69
69
  if not config.DISABLE_TQDM:
70
70
  self.tbar.update(1)
71
- if self.callback('check_running', self.id, self.name) is False:
71
+ if self.callback(constants.CALLBACK_CHECK_RUNNING, self.id, self.name) is False:
72
72
  raise RunStopException(self.name)
73
73
 
74
74
  def run_core(self):
@@ -78,13 +78,13 @@ class NoiseDetection(JobBase, FramePaths):
78
78
  ))
79
79
  in_paths = self.input_filepaths()
80
80
  n_frames = min(len(in_paths), self.max_frames) if self.max_frames > 0 else len(in_paths)
81
- self.callback('step_counts', self.id, self.name, n_frames)
81
+ self.callback(constants.CALLBACK_STEP_COUNTS, self.id, self.name, n_frames)
82
82
  if not config.DISABLE_TQDM:
83
83
  self.tbar = make_tqdm_bar(self.name, n_frames)
84
84
 
85
85
  def progress_callback(i):
86
86
  self.progress(i)
87
- if self.callback('check_running', self.id, self.name) is False:
87
+ if self.callback(constants.CALLBACK_CHECK_RUNNING, self.id, self.name) is False:
88
88
  raise RunStopException(self.name)
89
89
  mean_img = mean_image(
90
90
  file_paths=in_paths, max_frames=self.max_frames,
@@ -137,7 +137,7 @@ class NoiseDetection(JobBase, FramePaths):
137
137
  plt.ylim(0)
138
138
  plot_path = f"{self.working_path}/{self.plot_path}/{self.name}-hot-pixels.pdf"
139
139
  save_plot(plot_path)
140
- self.callback('save_plot', self.id, f"{self.name}: noise", plot_path)
140
+ self.callback(constants.CALLBACK_SAVE_PLOT, self.id, f"{self.name}: noise", plot_path)
141
141
  plt.close('all')
142
142
 
143
143
 
@@ -124,10 +124,9 @@ class PyramidBase(BaseStackAlgo):
124
124
 
125
125
  def focus_stack_validate(self, cleanup_callback=None):
126
126
  metadata = None
127
- n = len(self.filenames)
128
127
  for i, img_path in enumerate(self.filenames):
129
- self.print_message(f": validating file {img_path.split('/')[-1]}, {i + 1}/{n}")
130
-
128
+ self.print_message(
129
+ f": validating file {self.image_str(i)}")
131
130
  _img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
132
131
  if updated:
133
132
  self.dtype = metadata[1]
@@ -185,7 +184,8 @@ class PyramidStack(PyramidBase):
185
184
  self.focus_stack_validate()
186
185
  all_laplacians = []
187
186
  for i, img_path in enumerate(self.filenames):
188
- self.print_message(f": processing file {img_path.split('/')[-1]} ({i + 1}/{n})")
187
+ self.print_message(
188
+ f": processing {self.image_str(i)}")
189
189
  img = read_img(img_path)
190
190
  all_laplacians.append(self.process_single_image(img, self.n_levels))
191
191
  self.after_step(i + n + 1)
@@ -17,8 +17,9 @@ class PyramidAutoStack(BaseStackAlgo):
17
17
  n_tiled_layers=constants.DEFAULT_PY_N_TILED_LAYERS,
18
18
  memory_limit=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
19
19
  max_threads=constants.DEFAULT_PY_MAX_THREADS,
20
- max_tile_size=2048,
21
- min_n_tiled_layers=1,
20
+ max_tile_size=constants.DEFAULT_PY_MAX_TILE_SIZE,
21
+ min_tile_size=constants.DEFAULT_PY_MIN_TILE_SIZE,
22
+ min_n_tiled_layers=constants.DEFAULT_PY_MIN_N_TILED_LAYERS,
22
23
  mode='auto'):
23
24
  super().__init__("auto_pyramid", 2, float_type)
24
25
  self.min_size = min_size
@@ -32,6 +33,7 @@ class PyramidAutoStack(BaseStackAlgo):
32
33
  available_cores = os.cpu_count() or 1
33
34
  self.num_threads = min(max_threads, available_cores)
34
35
  self.max_tile_size = max_tile_size
36
+ self.min_tile_size = min_tile_size
35
37
  self.min_n_tiled_layers = min_n_tiled_layers
36
38
  self.mode = mode
37
39
  self._implementation = None
@@ -39,10 +41,10 @@ class PyramidAutoStack(BaseStackAlgo):
39
41
  self.shape = None
40
42
  self.n_levels = None
41
43
  self.n_frames = 0
42
- self.channels = 3
44
+ self.channels = 3 # r, g, b
43
45
  dtype = np.float32 if self.float_type == constants.FLOAT_32 else np.float64
44
46
  self.bytes_per_pixel = self.channels * np.dtype(dtype).itemsize
45
- self.overhead = 1.5
47
+ self.overhead = constants.PY_MEMORY_OVERHEAD
46
48
 
47
49
  def init(self, filenames):
48
50
  first_img_file = None
@@ -97,15 +99,19 @@ class PyramidAutoStack(BaseStackAlgo):
97
99
  return self.overhead * total_memory * self.n_frames
98
100
 
99
101
  def _find_optimal_tile_params(self):
100
- tile_size_max = int(np.sqrt(self.memory_limit /
101
- (self.num_threads * self.n_frames *
102
- self.bytes_per_pixel * self.overhead)))
102
+ h, w = self.shape[:2]
103
+ base_level_memory = h * w * self.bytes_per_pixel
104
+ available_memory = self.memory_limit - base_level_memory
105
+ available_memory /= self.overhead
106
+ tile_size_max = int(np.sqrt(available_memory /
107
+ (self.num_threads * self.n_frames * self.bytes_per_pixel)))
103
108
  tile_size = min(self.max_tile_size, tile_size_max, self.shape[0], self.shape[1])
109
+ tile_size = max(self.min_tile_size, tile_size)
104
110
  n_tiled_layers = 0
105
111
  for layer in range(self.n_levels):
106
- h = max(1, self.shape[0] // (2 ** layer))
107
- w = max(1, self.shape[1] // (2 ** layer))
108
- if h > tile_size or w > tile_size:
112
+ h_layer = max(1, self.shape[0] // (2 ** layer))
113
+ w_layer = max(1, self.shape[1] // (2 ** layer))
114
+ if h_layer > tile_size or w_layer > tile_size:
109
115
  n_tiled_layers = layer + 1
110
116
  else:
111
117
  break
@@ -2,10 +2,11 @@
2
2
  # pylint: disable=C0114, C0115, C0116, E1101, R0914, R1702, R1732, R0913
3
3
  # pylint: disable=R0917, R0912, R0915, R0902, W0718
4
4
  import os
5
+ import gc
5
6
  import time
6
7
  import shutil
7
8
  import tempfile
8
- import concurrent.futures
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
10
  import numpy as np
10
11
  from .. config.constants import constants
11
12
  from .. core.exceptions import RunStopException
@@ -121,13 +122,13 @@ class PyramidTilesStack(PyramidBase):
121
122
  for x in range(0, w, self.tile_size):
122
123
  tiles.append((y, x))
123
124
  self.print_message(f': starting parallel propcessging on {self.num_threads} cores')
124
- with concurrent.futures.ThreadPoolExecutor(max_workers=self.num_threads) as executor:
125
+ with ThreadPoolExecutor(max_workers=self.num_threads) as executor:
125
126
  future_to_tile = {
126
127
  executor.submit(
127
128
  self._process_tile, level, num_images, all_level_counts, y, x, h, w): (y, x)
128
129
  for y, x in tiles
129
130
  }
130
- for future in concurrent.futures.as_completed(future_to_tile):
131
+ for future in as_completed(future_to_tile):
131
132
  y, x = future_to_tile[future]
132
133
  try:
133
134
  fused_tile = future.result()
@@ -154,7 +155,9 @@ class PyramidTilesStack(PyramidBase):
154
155
  if laplacians:
155
156
  stacked = np.stack(laplacians, axis=0)
156
157
  return self.fuse_laplacian(stacked)
157
- y_end, x_end = min(y + self.tile_size, h), min(x + self.tile_size, w)
158
+ y_end = min(y + self.tile_size, h)
159
+ x_end = min(x + self.tile_size, w)
160
+ gc.collect()
158
161
  return np.zeros((y_end - y, x_end - x, 3), dtype=self.float_type)
159
162
 
160
163
  def fuse_pyramids(self, all_level_counts, num_images):
@@ -202,26 +205,29 @@ class PyramidTilesStack(PyramidBase):
202
205
  self.focus_stack_validate(self.cleanup_temp_files)
203
206
  all_level_counts = [0] * n
204
207
  if self.num_threads > 1:
205
- self.print_message(f': starting parallel image processing on {self.num_threads} cores')
208
+ self.print_message(f': starting parallel processing on {self.num_threads} cores')
206
209
  args_list = [(file_path, i, n) for i, file_path in enumerate(self.filenames)]
207
210
  executor = None
208
211
  try:
209
- executor = concurrent.futures.ThreadPoolExecutor(max_workers=self.num_threads)
212
+ executor = ThreadPoolExecutor(max_workers=self.num_threads)
210
213
  future_to_index = {
211
214
  executor.submit(self._process_single_image_wrapper, args): i
212
215
  for i, args in enumerate(args_list)
213
216
  }
214
217
  completed_count = 0
215
- for future in concurrent.futures.as_completed(future_to_index):
218
+ for future in as_completed(future_to_index):
216
219
  i = future_to_index[future]
217
220
  try:
218
221
  img_index, level_count = future.result()
219
222
  all_level_counts[img_index] = level_count
220
223
  completed_count += 1
221
- self.print_message(f': completed processing image {completed_count}/{n}')
224
+ self.print_message(
225
+ ": processing completed, image "
226
+ f"{self.idx_tot_str(completed_count - 1)}")
222
227
  except Exception as e:
223
- self.print_message(f"Error processing image {i + 1}: {str(e)}")
224
- self.after_step(i + n + 1)
228
+ self.print_message(
229
+ f"Error processing image {self.idx_tot_str(i)}: {str(e)}")
230
+ self.after_step(completed_count + n + 1)
225
231
  self.check_running(lambda: None)
226
232
  except RunStopException:
227
233
  self.print_message(": stopping image processing...")
@@ -235,7 +241,9 @@ class PyramidTilesStack(PyramidBase):
235
241
  executor.shutdown(wait=True)
236
242
  else:
237
243
  for i, file_path in enumerate(self.filenames):
238
- self.print_message(f": processing file {file_path.split('/')[-1]}, {i + 1}/{n}")
244
+ self.print_message(
245
+ f": processing file {os.path.basename(file_path)}, "
246
+ f"{self.idx_tot_str(i)}")
239
247
  img = read_img(file_path)
240
248
  level_count = self.process_single_image(img, self.n_levels, i)
241
249
  all_level_counts[i] = level_count
@@ -2,19 +2,19 @@
2
2
  import os
3
3
  import numpy as np
4
4
  from .. config.constants import constants
5
- from .. core.framework import JobBase
5
+ from .. core.framework import TaskBase
6
6
  from .. core.colors import color_str
7
7
  from .. core.exceptions import InvalidOptionError
8
8
  from .utils import write_img, extension_tif_jpg
9
- from .stack_framework import FramePaths, ActionList
9
+ from .stack_framework import ImageSequenceManager, SequentialTask
10
10
  from .exif import copy_exif_from_file_to_file
11
11
  from .denoise import denoise
12
12
 
13
13
 
14
- class FocusStackBase(JobBase, FramePaths):
14
+ class FocusStackBase(TaskBase, ImageSequenceManager):
15
15
  def __init__(self, name, stack_algo, enabled=True, **kwargs):
16
- FramePaths.__init__(self, name, **kwargs)
17
- JobBase.__init__(self, name, enabled)
16
+ ImageSequenceManager.__init__(self, name, **kwargs)
17
+ TaskBase.__init__(self, name, enabled)
18
18
  self.stack_algo = stack_algo
19
19
  self.exif_path = kwargs.pop('exif_path', '')
20
20
  self.prefix = kwargs.pop('prefix', constants.DEFAULT_STACK_PREFIX)
@@ -46,14 +46,14 @@ class FocusStackBase(JobBase, FramePaths):
46
46
  name = f"{self.name}: {self.stack_algo.name()}"
47
47
  if idx_str != '':
48
48
  name += f"\nbunch: {idx_str}"
49
- self.callback('save_plot', self.id, name, out_filename)
49
+ self.callback(constants.CALLBACK_SAVE_PLOT, self.id, name, out_filename)
50
50
  if self.frame_count >= 0:
51
51
  self.frame_count += 1
52
52
 
53
53
  def init(self, job, working_path=''):
54
- FramePaths.init(self, job)
54
+ ImageSequenceManager.init(self, job)
55
55
  if self.exif_path is None:
56
- self.exif_path = job.paths[0]
56
+ self.exif_path = job.action_path(0)
57
57
  if self.exif_path != '':
58
58
  self.exif_path = working_path + "/" + self.exif_path
59
59
 
@@ -64,9 +64,9 @@ def get_bunches(collection, n_frames, n_overlap):
64
64
  return bunches
65
65
 
66
66
 
67
- class FocusStackBunch(ActionList, FocusStackBase):
67
+ class FocusStackBunch(SequentialTask, FocusStackBase):
68
68
  def __init__(self, name, stack_algo, enabled=True, **kwargs):
69
- ActionList.__init__(self, name, enabled)
69
+ SequentialTask.__init__(self, name, enabled)
70
70
  FocusStackBase.__init__(self, name, stack_algo, enabled, **kwargs)
71
71
  self._chunks = None
72
72
  self.frame_count = 0
@@ -78,24 +78,28 @@ class FocusStackBunch(ActionList, FocusStackBase):
78
78
  raise InvalidOptionError("overlap", self.overlap,
79
79
  "overlap must be smaller than batch size")
80
80
 
81
+ def sequential_processing(self):
82
+ return True
83
+
81
84
  def init(self, job, _working_path=''):
82
85
  FocusStackBase.init(self, job, self.working_path)
83
86
 
84
87
  def begin(self):
85
- ActionList.begin(self)
88
+ SequentialTask.begin(self)
86
89
  self._chunks = get_bunches(self.input_filepaths(), self.frames, self.overlap)
87
90
  self.set_counts(len(self._chunks))
88
91
 
89
92
  def end(self):
90
- ActionList.end(self)
93
+ SequentialTask.end(self)
91
94
 
92
- def run_step(self):
93
- self.print_message_r(
94
- color_str(f"fusing bunch: {self.current_action_count + 1}/{self.total_action_counts}",
95
+ def run_step(self, action_count=-1):
96
+ self.print_message(
97
+ color_str(f"fusing bunch: {action_count + 1}/{self.total_action_counts}",
95
98
  constants.LOG_COLOR_LEVEL_2))
96
- img_files = self._chunks[self.current_action_count - 1]
99
+ img_files = self._chunks[action_count - 1]
97
100
  self.stack_algo.init(img_files)
98
- self.focus_stack(self._chunks[self.current_action_count - 1])
101
+ self.focus_stack(self._chunks[action_count - 1])
102
+ return True
99
103
 
100
104
 
101
105
  class FocusStack(FocusStackBase):
@@ -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
9
  from .. core.exceptions import RunStopException
10
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)
26
41
 
27
- class FramePaths:
42
+ def action_path(self, i):
43
+ return self._action_paths[i]
44
+
45
+
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,
@@ -34,7 +53,7 @@ class FramePaths:
34
53
  self.plot_path = plot_path
35
54
  self.input_path = input_path
36
55
  self.output_path = self.name if output_path == '' else output_path
37
- self.resample = resample
56
+ self._resample = resample
38
57
  self.reverse_order = reverse_order
39
58
  self.scratch_output_dir = scratch_output_dir
40
59
  self.enabled = None
@@ -78,8 +97,8 @@ class FramePaths:
78
97
  filelist.sort()
79
98
  if self.reverse_order:
80
99
  filelist.reverse()
81
- if self.resample > 1:
82
- filelist = filelist[0::self.resample]
100
+ if self._resample > 1:
101
+ filelist = filelist[0::self._resample]
83
102
  files += filelist
84
103
  if len(files) == 0:
85
104
  self.print_message(color_str(f"input folder {d} does not contain any image",
@@ -138,10 +157,16 @@ class FramePaths:
138
157
  if not os.path.exists(self.plot_path):
139
158
  os.makedirs(self.plot_path)
140
159
  if self.input_path in ['', []]:
141
- if len(job.paths) == 0:
160
+ if job.num_action_paths() == 0:
142
161
  raise RuntimeError(f"Job {job.name} does not have any configured path")
143
- self.input_path = job.paths[-1]
144
- job.paths.append(self.output_path)
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)
145
170
 
146
171
  def folder_list_str(self):
147
172
  if isinstance(self.input_full_path(), list):
@@ -152,10 +177,10 @@ class FramePaths:
152
177
  return "folder: " + self.input_full_path().replace(self.working_path, '').lstrip('/')
153
178
 
154
179
 
155
- class FramesRefActions(ActionList, FramePaths):
180
+ class ReferenceFrameTask(SequentialTask, ImageSequenceManager):
156
181
  def __init__(self, name, enabled=True, reference_index=0, step_process=False, **kwargs):
157
- FramePaths.__init__(self, name, **kwargs)
158
- ActionList.__init__(self, name, enabled)
182
+ ImageSequenceManager.__init__(self, name, **kwargs)
183
+ SequentialTask.__init__(self, name, enabled)
159
184
  self.ref_idx = reference_index
160
185
  self.step_process = step_process
161
186
  self.current_idx = None
@@ -163,7 +188,7 @@ class FramesRefActions(ActionList, FramePaths):
163
188
  self.current_idx_step = None
164
189
 
165
190
  def begin(self):
166
- ActionList.begin(self)
191
+ SequentialTask.begin(self)
167
192
  self.set_filelist()
168
193
  n = self.num_input_filepaths()
169
194
  self.set_counts(n)
@@ -179,33 +204,44 @@ class FramesRefActions(ActionList, FramePaths):
179
204
  raise IndexError(msg)
180
205
 
181
206
  def end(self):
182
- ActionList.end(self)
207
+ SequentialTask.end(self)
183
208
 
184
209
  def run_frame(self, _idx, _ref_idx):
185
210
  return None
186
211
 
187
- def run_step(self):
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()
193
- self.print_message_r(
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))
198
- self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
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
206
- if self.step_process:
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
207
217
  self.current_ref_idx = self.ref_idx
208
- self.current_idx_step = -1
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))
232
+ self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
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
209
245
 
210
246
 
211
247
  class SubAction:
@@ -218,15 +254,18 @@ class SubAction:
218
254
  def end(self):
219
255
  pass
220
256
 
257
+ def sequential_processing(self):
258
+ return False
259
+
221
260
 
222
- class CombinedActions(FramesRefActions):
261
+ class CombinedActions(ReferenceFrameTask):
223
262
  def __init__(self, name, actions=[], enabled=True, **kwargs):
224
- FramesRefActions.__init__(self, name, enabled, **kwargs)
263
+ ReferenceFrameTask.__init__(self, name, enabled, **kwargs)
225
264
  self._actions = actions
226
265
  self._metadata = (None, None)
227
266
 
228
267
  def begin(self):
229
- FramesRefActions.begin(self)
268
+ ReferenceFrameTask.begin(self)
230
269
  for a in self._actions:
231
270
  if a.enabled:
232
271
  a.begin(self)
@@ -241,7 +280,10 @@ class CombinedActions(FramesRefActions):
241
280
 
242
281
  def run_frame(self, idx, ref_idx):
243
282
  input_path = self.input_filepath(idx)
244
- self.sub_message_r(color_str(': read input image', constants.LOG_COLOR_LEVEL_3))
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))
245
287
  img = read_img(input_path)
246
288
  validate_image(img, *(self._metadata))
247
289
  if img is None:
@@ -254,7 +296,7 @@ class CombinedActions(FramesRefActions):
254
296
  self.get_logger().warning(color_str(f"{self.base_message}: sub-action disabled",
255
297
  constants.LOG_COLOR_ALERT))
256
298
  else:
257
- 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:
258
300
  raise RunStopException(self.name)
259
301
  if img is not None:
260
302
  img = a.run_frame(idx, ref_idx, img)
@@ -264,8 +306,11 @@ class CombinedActions(FramesRefActions):
264
306
  constants.LOG_COLOR_ALERT),
265
307
  level=logging.WARNING)
266
308
  if img is not None:
267
- self.sub_message_r(color_str(': write output image', constants.LOG_COLOR_LEVEL_3))
268
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))
269
314
  write_img(output_path, img)
270
315
  return img
271
316
  self.print_message(color_str(
@@ -277,3 +322,9 @@ class CombinedActions(FramesRefActions):
277
322
  for a in self._actions:
278
323
  if a.enabled:
279
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