shinestacker 1.2.1__py3-none-any.whl → 1.3.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.

Files changed (46) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +152 -112
  3. shinestacker/algorithms/align_auto.py +76 -0
  4. shinestacker/algorithms/align_parallel.py +336 -0
  5. shinestacker/algorithms/balance.py +3 -1
  6. shinestacker/algorithms/base_stack_algo.py +25 -22
  7. shinestacker/algorithms/depth_map.py +9 -14
  8. shinestacker/algorithms/multilayer.py +8 -8
  9. shinestacker/algorithms/noise_detection.py +10 -10
  10. shinestacker/algorithms/pyramid.py +10 -24
  11. shinestacker/algorithms/pyramid_auto.py +21 -24
  12. shinestacker/algorithms/pyramid_tiles.py +31 -25
  13. shinestacker/algorithms/stack.py +21 -17
  14. shinestacker/algorithms/stack_framework.py +98 -47
  15. shinestacker/algorithms/utils.py +16 -0
  16. shinestacker/algorithms/vignetting.py +13 -10
  17. shinestacker/app/gui_utils.py +10 -0
  18. shinestacker/app/main.py +10 -4
  19. shinestacker/app/project.py +3 -1
  20. shinestacker/app/retouch.py +3 -1
  21. shinestacker/config/constants.py +60 -25
  22. shinestacker/config/gui_constants.py +1 -1
  23. shinestacker/core/core_utils.py +4 -0
  24. shinestacker/core/framework.py +104 -23
  25. shinestacker/gui/action_config.py +4 -5
  26. shinestacker/gui/action_config_dialog.py +409 -239
  27. shinestacker/gui/base_form_dialog.py +2 -2
  28. shinestacker/gui/colors.py +1 -0
  29. shinestacker/gui/folder_file_selection.py +106 -0
  30. shinestacker/gui/gui_run.py +12 -10
  31. shinestacker/gui/main_window.py +10 -5
  32. shinestacker/gui/new_project.py +171 -73
  33. shinestacker/gui/project_controller.py +10 -6
  34. shinestacker/gui/project_converter.py +4 -2
  35. shinestacker/gui/project_editor.py +40 -28
  36. shinestacker/gui/select_path_widget.py +1 -1
  37. shinestacker/gui/sys_mon.py +97 -0
  38. shinestacker/gui/time_progress_bar.py +4 -3
  39. shinestacker/retouch/exif_data.py +1 -1
  40. shinestacker/retouch/image_editor_ui.py +2 -0
  41. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/METADATA +6 -6
  42. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/RECORD +46 -42
  43. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/WHEEL +0 -0
  44. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/entry_points.txt +0 -0
  45. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/licenses/LICENSE +0 -0
  46. {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,6 @@
2
2
  import os
3
3
  import numpy as np
4
4
  from .. config.constants import constants
5
- from .utils import extension_tif_jpg
6
5
  from .base_stack_algo import BaseStackAlgo
7
6
  from .pyramid import PyramidStack
8
7
  from .pyramid_tiles import PyramidTilesStack
@@ -17,10 +16,11 @@ class PyramidAutoStack(BaseStackAlgo):
17
16
  n_tiled_layers=constants.DEFAULT_PY_N_TILED_LAYERS,
18
17
  memory_limit=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
19
18
  max_threads=constants.DEFAULT_PY_MAX_THREADS,
20
- max_tile_size=2048,
21
- min_n_tiled_layers=1,
19
+ max_tile_size=constants.DEFAULT_PY_MAX_TILE_SIZE,
20
+ min_tile_size=constants.DEFAULT_PY_MIN_TILE_SIZE,
21
+ min_n_tiled_layers=constants.DEFAULT_PY_MIN_N_TILED_LAYERS,
22
22
  mode='auto'):
23
- super().__init__("auto_pyramid", 2, float_type)
23
+ super().__init__("auto_pyramid", 1, float_type)
24
24
  self.min_size = min_size
25
25
  self.kernel_size = kernel_size
26
26
  self.gen_kernel = gen_kernel
@@ -32,6 +32,7 @@ class PyramidAutoStack(BaseStackAlgo):
32
32
  available_cores = os.cpu_count() or 1
33
33
  self.num_threads = min(max_threads, available_cores)
34
34
  self.max_tile_size = max_tile_size
35
+ self.min_tile_size = min_tile_size
35
36
  self.min_n_tiled_layers = min_n_tiled_layers
36
37
  self.mode = mode
37
38
  self._implementation = None
@@ -39,21 +40,13 @@ class PyramidAutoStack(BaseStackAlgo):
39
40
  self.shape = None
40
41
  self.n_levels = None
41
42
  self.n_frames = 0
42
- self.channels = 3
43
+ self.channels = 3 # r, g, b
43
44
  dtype = np.float32 if self.float_type == constants.FLOAT_32 else np.float64
44
45
  self.bytes_per_pixel = self.channels * np.dtype(dtype).itemsize
45
- self.overhead = 1.5
46
+ self.overhead = constants.PY_MEMORY_OVERHEAD
46
47
 
47
48
  def init(self, filenames):
48
- first_img_file = None
49
- for filename in filenames:
50
- if os.path.isfile(filename) and extension_tif_jpg(filename):
51
- first_img_file = filename
52
- break
53
- if first_img_file is None:
54
- raise ValueError("No valid image files found")
55
- _img, metadata, _ = self.read_image_and_update_metadata(first_img_file, None)
56
- self.shape, self.dtype = metadata
49
+ super().init(filenames)
57
50
  self.n_levels = int(np.log2(min(self.shape) / self.min_size))
58
51
  self.n_frames = len(filenames)
59
52
  memory_required_memory = self._estimate_memory_memory()
@@ -77,9 +70,9 @@ class PyramidAutoStack(BaseStackAlgo):
77
70
  n_tiled_layers=optimal_params['n_tiled_layers'],
78
71
  max_threads=self.num_threads
79
72
  )
80
- self.print_message(f": using tile-based pyramid stacking "
81
- f"(tile_size: {optimal_params['tile_size']}, "
82
- f"n_tiled_layers: {optimal_params['n_tiled_layers']}), "
73
+ self.print_message(f": using tile-based pyramid stacking, "
74
+ f"tile size: {optimal_params['tile_size']}, "
75
+ f"n. tiled layers: {optimal_params['n_tiled_layers']}, "
83
76
  f"{self.num_threads} cores.")
84
77
  self._implementation.init(filenames)
85
78
  self._implementation.set_do_step_callback(self.do_step_callback)
@@ -97,15 +90,19 @@ class PyramidAutoStack(BaseStackAlgo):
97
90
  return self.overhead * total_memory * self.n_frames
98
91
 
99
92
  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)))
93
+ h, w = self.shape[:2]
94
+ base_level_memory = h * w * self.bytes_per_pixel
95
+ available_memory = self.memory_limit - base_level_memory
96
+ available_memory /= self.overhead
97
+ tile_size_max = int(np.sqrt(available_memory /
98
+ (self.num_threads * self.n_frames * self.bytes_per_pixel)))
103
99
  tile_size = min(self.max_tile_size, tile_size_max, self.shape[0], self.shape[1])
100
+ tile_size = max(self.min_tile_size, tile_size)
104
101
  n_tiled_layers = 0
105
102
  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:
103
+ h_layer = max(1, self.shape[0] // (2 ** layer))
104
+ w_layer = max(1, self.shape[1] // (2 ** layer))
105
+ if h_layer > tile_size or w_layer > tile_size:
109
106
  n_tiled_layers = layer + 1
110
107
  else:
111
108
  break
@@ -2,14 +2,15 @@
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
12
- from .utils import read_img
13
+ from .utils import read_img, read_and_validate_img
13
14
  from .pyramid import PyramidBase
14
15
 
15
16
 
@@ -46,11 +47,11 @@ class PyramidTilesStack(PyramidBase):
46
47
  return n_steps + self.n_tiles
47
48
 
48
49
  def _process_single_image_wrapper(self, args):
49
- img_path, img_index, _n = args
50
- # self.print_message(f": processing file {img_path.split('/')[-1]}, {img_index + 1}/{n}")
51
- img = read_img(img_path)
52
- level_count = self.process_single_image(img, self.n_levels, img_index)
53
- return img_index, level_count
50
+ img_path, idx, _n = args
51
+ img = read_and_validate_img(img_path, self.shape, self.dtype)
52
+ self.check_running(self.cleanup_temp_files)
53
+ level_count = self.process_single_image(img, self.n_levels, idx)
54
+ return idx, level_count
54
55
 
55
56
  def process_single_image(self, img, levels, img_index):
56
57
  laplacian = self.single_image_laplacian(img, levels)
@@ -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,13 +155,16 @@ 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
- def fuse_pyramids(self, all_level_counts, num_images):
163
+ def fuse_pyramids(self, all_level_counts):
164
+ num_images = self.num_images()
161
165
  max_levels = max(all_level_counts)
162
166
  fused = []
163
- count = self._steps_per_frame * self.n_frames
167
+ count = super().total_steps(num_images)
164
168
  for level in range(max_levels - 1, -1, -1):
165
169
  self.print_message(f': fusing pyramids, layer: {level + 1}')
166
170
  if level < self.n_tiled_layers:
@@ -198,30 +202,31 @@ class PyramidTilesStack(PyramidBase):
198
202
  return fused[::-1]
199
203
 
200
204
  def focus_stack(self):
201
- n = len(self.filenames)
202
- self.focus_stack_validate(self.cleanup_temp_files)
203
- all_level_counts = [0] * n
205
+ all_level_counts = [0] * self.num_images()
204
206
  if self.num_threads > 1:
205
- self.print_message(f': starting parallel image processing on {self.num_threads} cores')
206
- args_list = [(file_path, i, n) for i, file_path in enumerate(self.filenames)]
207
+ self.print_message(f': starting parallel processing on {self.num_threads} cores')
208
+ args_list = [(file_path, i, self.num_images())
209
+ 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
+ f": processing completed, {self.image_str(completed_count - 1)}")
222
226
  except Exception as e:
223
- self.print_message(f"Error processing image {i + 1}: {str(e)}")
224
- self.after_step(i + n + 1)
227
+ self.print_message(
228
+ f"Error processing {self.image_str(i)}: {str(e)}")
229
+ self.after_step(completed_count)
225
230
  self.check_running(lambda: None)
226
231
  except RunStopException:
227
232
  self.print_message(": stopping image processing...")
@@ -235,15 +240,16 @@ class PyramidTilesStack(PyramidBase):
235
240
  executor.shutdown(wait=True)
236
241
  else:
237
242
  for i, file_path in enumerate(self.filenames):
238
- self.print_message(f": processing file {file_path.split('/')[-1]}, {i + 1}/{n}")
243
+ self.print_message(
244
+ f": processing {self.image_str(i)}")
239
245
  img = read_img(file_path)
240
246
  level_count = self.process_single_image(img, self.n_levels, i)
241
247
  all_level_counts[i] = level_count
242
- self.after_step(i + n + 1)
248
+ self.after_step(i + 1)
243
249
  self.check_running(lambda: None)
244
250
  try:
245
251
  self.check_running(lambda: None)
246
- fused_pyramid = self.fuse_pyramids(all_level_counts, n)
252
+ fused_pyramid = self.fuse_pyramids(all_level_counts)
247
253
  stacked_image = self.collapse(fused_pyramid)
248
254
  return stacked_image.astype(self.dtype)
249
255
  except RunStopException:
@@ -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",
@@ -98,7 +117,7 @@ class FramePaths:
98
117
  assert False, "this method should be overwritten"
99
118
 
100
119
  def set_filelist(self):
101
- file_folder = self.input_full_path().replace(self.working_path, '').lstrip('/')
120
+ file_folder = os.path.relpath(self.input_full_path(), self.working_path)
102
121
  self.print_message(color_str(f"{self.num_input_filepaths()} files in folder: {file_folder}",
103
122
  constants.LOG_COLOR_LEVEL_2))
104
123
  self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
@@ -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
@@ -87,6 +87,17 @@ def img_bw(img):
87
87
  return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
88
88
 
89
89
 
90
+ def get_first_image_file(filenames):
91
+ first_img_file = None
92
+ for filename in filenames:
93
+ if os.path.isfile(filename) and extension_tif_jpg(filename):
94
+ first_img_file = filename
95
+ break
96
+ if first_img_file is None:
97
+ raise ValueError("No valid image files found")
98
+ return first_img_file
99
+
100
+
90
101
  def get_img_file_shape(file_path):
91
102
  img = read_img(file_path)
92
103
  return img.shape[:2]
@@ -106,6 +117,11 @@ def validate_image(img, expected_shape=None, expected_dtype=None):
106
117
  raise ShapeError(expected_shape, shape)
107
118
  if expected_dtype and dtype != expected_dtype:
108
119
  raise BitDepthError(expected_dtype, dtype)
120
+ return img
121
+
122
+
123
+ def read_and_validate_img(filename, expected_shape=None, expected_dtype=None):
124
+ return validate_image(read_img(filename), expected_shape, expected_dtype)
109
125
 
110
126
 
111
127
  def save_plot(filename):
@@ -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,12 +154,13 @@ 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
165
  plt.figure(figsize=constants.PLT_FIG_SIZE)
164
166
  plt.plot(radii, intensities, label="image mean intensity")
@@ -175,12 +177,13 @@ 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)
@@ -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)