shinestacker 1.0.4.post2__py3-none-any.whl → 1.2.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 (37) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/__init__.py +4 -1
  3. shinestacker/algorithms/align.py +128 -14
  4. shinestacker/algorithms/balance.py +362 -163
  5. shinestacker/algorithms/base_stack_algo.py +33 -4
  6. shinestacker/algorithms/depth_map.py +9 -12
  7. shinestacker/algorithms/multilayer.py +12 -2
  8. shinestacker/algorithms/noise_detection.py +8 -3
  9. shinestacker/algorithms/pyramid.py +57 -42
  10. shinestacker/algorithms/pyramid_auto.py +141 -0
  11. shinestacker/algorithms/pyramid_tiles.py +264 -0
  12. shinestacker/algorithms/stack.py +14 -11
  13. shinestacker/algorithms/stack_framework.py +17 -11
  14. shinestacker/algorithms/utils.py +180 -1
  15. shinestacker/algorithms/vignetting.py +23 -5
  16. shinestacker/config/constants.py +31 -5
  17. shinestacker/gui/action_config.py +6 -7
  18. shinestacker/gui/action_config_dialog.py +425 -258
  19. shinestacker/gui/base_form_dialog.py +11 -6
  20. shinestacker/gui/flow_layout.py +105 -0
  21. shinestacker/gui/gui_run.py +24 -19
  22. shinestacker/gui/main_window.py +4 -3
  23. shinestacker/gui/menu_manager.py +12 -2
  24. shinestacker/gui/new_project.py +28 -22
  25. shinestacker/gui/project_controller.py +40 -23
  26. shinestacker/gui/project_converter.py +6 -6
  27. shinestacker/gui/project_editor.py +21 -7
  28. shinestacker/gui/time_progress_bar.py +2 -2
  29. shinestacker/retouch/exif_data.py +5 -5
  30. shinestacker/retouch/shortcuts_help.py +4 -4
  31. shinestacker/retouch/vignetting_filter.py +12 -8
  32. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/METADATA +20 -1
  33. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/RECORD +37 -34
  34. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/WHEEL +0 -0
  35. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/entry_points.txt +0 -0
  36. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/licenses/LICENSE +0 -0
  37. {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,10 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0602, R0903
2
+ import os
2
3
  import numpy as np
3
- from .. core.exceptions import InvalidOptionError, ImageLoadError
4
+ from .. core.exceptions import InvalidOptionError, ImageLoadError, RunStopException
4
5
  from .. config.constants import constants
5
6
  from .. core.colors import color_str
6
- from .utils import read_img, get_img_metadata, validate_image
7
+ from .utils import read_img, get_img_metadata, validate_image, get_img_file_shape, extension_tif_jpg
7
8
 
8
9
 
9
10
  class BaseStackAlgo:
@@ -11,6 +12,9 @@ class BaseStackAlgo:
11
12
  self._name = name
12
13
  self._steps_per_frame = steps_per_frame
13
14
  self.process = None
15
+ self.filenames = None
16
+ self.shape = None
17
+ self.do_step_callback = False
14
18
  if float_type == constants.FLOAT_32:
15
19
  self.float_type = np.float32
16
20
  elif float_type == constants.FLOAT_64:
@@ -24,8 +28,23 @@ class BaseStackAlgo:
24
28
  def name(self):
25
29
  return self._name
26
30
 
27
- def steps_per_frame(self):
28
- return self._steps_per_frame
31
+ def set_process(self, process):
32
+ self.process = process
33
+
34
+ def set_do_step_callback(self, enable):
35
+ self.do_step_callback = enable
36
+
37
+ def init(self, filenames):
38
+ self.filenames = filenames
39
+ first_img_file = ''
40
+ for filename in filenames:
41
+ if os.path.isfile(filename) and extension_tif_jpg(filename):
42
+ first_img_file = filename
43
+ break
44
+ self.shape = get_img_file_shape(first_img_file)
45
+
46
+ def total_steps(self, n_frames):
47
+ return self._steps_per_frame * n_frames
29
48
 
30
49
  def print_message(self, msg):
31
50
  self.process.sub_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_3))
@@ -40,3 +59,13 @@ class BaseStackAlgo:
40
59
  else:
41
60
  validate_image(img, *metadata)
42
61
  return img, metadata, updated
62
+
63
+ def check_running(self, cleanup_callback=None):
64
+ if self.process.callback('check_running', self.process.id, self.process.name) is False:
65
+ if cleanup_callback is not None:
66
+ cleanup_callback()
67
+ raise RunStopException(self.name)
68
+
69
+ def after_step(self, step):
70
+ if self.do_step_callback:
71
+ self.process.callback('after_step', self.process.id, self.process.name, step)
@@ -2,7 +2,7 @@
2
2
  import numpy as np
3
3
  import cv2
4
4
  from .. config.constants import constants
5
- from .. core.exceptions import InvalidOptionError, RunStopException
5
+ from .. core.exceptions import InvalidOptionError
6
6
  from .utils import read_img, img_bw
7
7
  from .base_stack_algo import BaseStackAlgo
8
8
 
@@ -61,19 +61,18 @@ class DepthMapStack(BaseStackAlgo):
61
61
  raise InvalidOptionError("map_type", self.map_type, details=f" valid values are "
62
62
  f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
63
63
 
64
- def focus_stack(self, filenames):
64
+ def focus_stack(self):
65
65
  gray_images = []
66
66
  metadata = None
67
- for i, img_path in enumerate(filenames):
67
+ for i, img_path in enumerate(self.filenames):
68
68
  self.print_message(f": reading file (1/2) {img_path.split('/')[-1]}")
69
69
 
70
70
  img, metadata, _updated = self.read_image_and_update_metadata(img_path, metadata)
71
71
 
72
72
  gray = img_bw(img)
73
73
  gray_images.append(gray)
74
- self.process.callback('after_step', self.process.id, self.process.name, i)
75
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
76
- raise RunStopException(self.name)
74
+ self.after_step(i)
75
+ self.check_running()
77
76
  dtype = metadata[1]
78
77
  gray_images = np.array(gray_images, dtype=self.float_type)
79
78
  if self.energy == constants.DM_ENERGY_SOBEL:
@@ -92,7 +91,7 @@ class DepthMapStack(BaseStackAlgo):
92
91
  energies = self.smooth_energy(energies)
93
92
  weights = self.get_focus_map(energies)
94
93
  blended_pyramid = None
95
- for i, img_path in enumerate(filenames):
94
+ for i, img_path in enumerate(self.filenames):
96
95
  self.print_message(f": reading file (2/2) {img_path.split('/')[-1]}")
97
96
  img = read_img(img_path).astype(self.float_type)
98
97
  weight = weights[i]
@@ -110,14 +109,12 @@ class DepthMapStack(BaseStackAlgo):
110
109
  for j in range(self.levels)]
111
110
  blended_pyramid = current_blend if blended_pyramid is None \
112
111
  else [np.add(bp, cb) for bp, cb in zip(blended_pyramid, current_blend)]
113
- self.process.callback('after_step', self.process.id,
114
- self.process.name, i + len(filenames))
115
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
116
- raise RunStopException(self.name)
112
+ self.after_step(i + len(self.filenames))
113
+ self.check_running()
117
114
  result = blended_pyramid[0]
118
115
  self.print_message(': blend levels')
119
116
  for j in range(1, self.levels):
120
117
  size = (blended_pyramid[j].shape[1], blended_pyramid[j].shape[0])
121
118
  result = cv2.pyrUp(result, dstsize=size) + blended_pyramid[j]
122
- n_values = 255 if dtype == np.uint8 else 65535
119
+ n_values = constants.MAX_UINT8 if dtype == np.uint8 else constants.MAX_UINT16
123
120
  return np.clip(np.absolute(result), 0, n_values).astype(dtype)
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, E1101, R0914, E0606
1
+ # pylint: disable=C0114, C0115, C0116, E1101, R0914, E0606, R0912
2
2
  import os
3
3
  import logging
4
4
  import cv2
@@ -61,6 +61,13 @@ def write_multilayer_tiff_from_images(image_dict, output_file, exif_path='', cal
61
61
  if len(dtypes) > 1:
62
62
  raise RuntimeError("All input files must all have 8 bit or 16 bit depth.")
63
63
  dtype = dtypes[0]
64
+ bytes_per_pixel = 3 * np.dtype(dtype).itemsize
65
+ est_memory = shape[0] * shape[1] * bytes_per_pixel * len(image_dict)
66
+ if est_memory > constants.MULTILAYER_WARNING_MEM_GB * constants.ONE_GIGA:
67
+ if callbacks:
68
+ callback = callbacks.get('memory_warning', None)
69
+ if callback:
70
+ callback(float(est_memory) / constants.ONE_GIGA)
64
71
  max_pixel_value = constants.MAX_UINT16 if dtype == np.uint16 else constants.MAX_UINT8
65
72
  transp = np.full_like(list(image_dict.values())[0][..., 0], max_pixel_value)
66
73
  compression_type = PsdCompressionType.ZIP_PREDICTED
@@ -203,7 +210,10 @@ class MultiLayer(JobBase, FrameMultiDirectory):
203
210
  'exif_msg': lambda path: self.print_message(
204
211
  color_str(f"copying exif data from path: {path}", constants.LOG_COLOR_LEVEL_2)),
205
212
  'write_msg': lambda path: self.print_message(
206
- color_str(f"writing multilayer tiff file: {path}", constants.LOG_COLOR_LEVEL_2))
213
+ color_str(f"writing multilayer tiff file: {path}", constants.LOG_COLOR_LEVEL_2)),
214
+ 'memory_warning': lambda mem: self.print_message(
215
+ color_str(f"warning: estimated file size: {mem:.2f} GBytes",
216
+ constants.LOG_COLOR_WARNING))
207
217
  }
208
218
  write_multilayer_tiff(input_files, output_file, labels=None, exif_path=self.exif_path,
209
219
  callbacks=callbacks)
@@ -1,4 +1,4 @@
1
- # pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915
1
+ # pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915, R0902
2
2
  import os
3
3
  import errno
4
4
  import logging
@@ -11,7 +11,7 @@ from .. core.colors import color_str
11
11
  from .. core.exceptions import ImageLoadError
12
12
  from .. core.framework import JobBase
13
13
  from .. core.core_utils import make_tqdm_bar
14
- from .. core.exceptions import RunStopException
14
+ from .. core.exceptions import RunStopException, ShapeError
15
15
  from .stack_framework import FrameMultiDirectory, SubAction
16
16
  from .utils import read_img, save_plot, get_img_metadata, validate_image
17
17
 
@@ -49,7 +49,7 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
49
49
  def __init__(self, name="noise-map", enabled=True, **kwargs):
50
50
  FrameMultiDirectory.__init__(self, name, **kwargs)
51
51
  JobBase.__init__(self, name, enabled)
52
- self.max_frames = kwargs.get('max_frames', -1)
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)
55
55
  if self.file_name == '':
@@ -154,6 +154,7 @@ class MaskNoise(SubAction):
154
154
  self.method = method
155
155
  self.process = None
156
156
  self.noise_mask_img = None
157
+ self.expected_shape = None
157
158
 
158
159
  def begin(self, process):
159
160
  self.process = process
@@ -163,6 +164,7 @@ class MaskNoise(SubAction):
163
164
  f': reading noisy pixel mask file: {self.noise_mask}',
164
165
  constants.LOG_COLOR_LEVEL_3))
165
166
  self.noise_mask_img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
167
+ self.expected_shape = self.noise_mask_img.shape[:2]
166
168
  if self.noise_mask_img is None:
167
169
  raise ImageLoadError(path, f"failed to load image file {self.noise_mask}.")
168
170
  else:
@@ -170,6 +172,9 @@ class MaskNoise(SubAction):
170
172
 
171
173
  def run_frame(self, _idx, _ref_idx, image):
172
174
  self.process.sub_message_r(color_str(': mask noisy pixels', constants.LOG_COLOR_LEVEL_3))
175
+ shape = image.shape[:2]
176
+ if shape != self.expected_shape:
177
+ raise ShapeError(self.expected_shape, shape)
173
178
  if len(image.shape) == 3:
174
179
  corrected = image.copy()
175
180
  for c in range(3):
@@ -1,25 +1,36 @@
1
- # pylint: disable=C0114, C0115, C0116, E1101
1
+ # pylint: disable=C0114, C0115, C0116, E1101, R0913, R0917, R0902
2
2
  import numpy as np
3
3
  import cv2
4
4
  from .. config.constants import constants
5
- from .. core.exceptions import RunStopException
6
5
  from .utils import read_img
7
6
  from .base_stack_algo import BaseStackAlgo
8
7
 
9
8
 
10
9
  class PyramidBase(BaseStackAlgo):
11
- def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
10
+ def __init__(self, name, min_size=constants.DEFAULT_PY_MIN_SIZE,
12
11
  kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
13
12
  gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
14
13
  float_type=constants.DEFAULT_PY_FLOAT):
15
- super().__init__("pyramid", 2, float_type)
14
+ super().__init__(name, 2, float_type)
16
15
  self.min_size = min_size
17
16
  self.kernel_size = kernel_size
18
17
  self.pad_amount = (kernel_size - 1) // 2
19
- self.do_step_callback = False
20
18
  kernel = np.array([0.25 - gen_kernel / 2.0, 0.25,
21
19
  gen_kernel, 0.25, 0.25 - gen_kernel / 2.0])
22
20
  self.gen_kernel = np.outer(kernel, kernel)
21
+ self.dtype = None
22
+ self.num_pixel_values = None
23
+ self.max_pixel_value = None
24
+ self.n_levels = 0
25
+ self.n_frames = 0
26
+
27
+ def init(self, filenames):
28
+ super().init(filenames)
29
+ self.n_levels = int(np.log2(min(self.shape) / self.min_size))
30
+
31
+ def total_steps(self, n_frames):
32
+ self.n_frames = n_frames
33
+ return self._steps_per_frame * n_frames + self.n_levels
23
34
 
24
35
  def convolve(self, image):
25
36
  return cv2.filter2D(image, -1, self.gen_kernel, borderType=cv2.BORDER_REFLECT101)
@@ -55,6 +66,7 @@ class PyramidBase(BaseStackAlgo):
55
66
  return fused
56
67
 
57
68
  def collapse(self, pyramid):
69
+ self.print_message(': collapsing pyramid')
58
70
  img = pyramid[-1]
59
71
  for layer in pyramid[-2::-1]:
60
72
  expanded = self.expand_layer(img)
@@ -110,19 +122,23 @@ class PyramidBase(BaseStackAlgo):
110
122
  fused += np.where(best_d[:, :, np.newaxis] == layer, img, 0)
111
123
  return (fused / 2).astype(images.dtype)
112
124
 
125
+ def focus_stack_validate(self, cleanup_callback=None):
126
+ metadata = None
127
+ n = len(self.filenames)
128
+ for i, img_path in enumerate(self.filenames):
129
+ self.print_message(f": validating file {img_path.split('/')[-1]}, {i + 1}/{n}")
113
130
 
114
- class PyramidStack(PyramidBase):
115
- def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
116
- kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
117
- gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
118
- float_type=constants.DEFAULT_PY_FLOAT):
119
- super().__init__(min_size, kernel_size, gen_kernel, float_type)
120
- self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
121
- self.dtype = None
122
- self.num_pixel_values = None
123
- self.max_pixel_value = None
131
+ _img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
132
+ if updated:
133
+ self.dtype = metadata[1]
134
+ self.num_pixel_values = constants.NUM_UINT8 \
135
+ if self.dtype == np.uint8 else constants.NUM_UINT16
136
+ self.max_pixel_value = constants.MAX_UINT8 \
137
+ if self.dtype == np.uint8 else constants.MAX_UINT16
138
+ self.after_step(i + 1)
139
+ self.check_running(cleanup_callback)
124
140
 
125
- def process_single_image(self, img, levels):
141
+ def single_image_laplacian(self, img, levels):
126
142
  pyramid = [img.astype(self.float_type)]
127
143
  for _ in range(levels):
128
144
  next_layer = self.reduce_layer(pyramid[-1])
@@ -136,44 +152,43 @@ class PyramidStack(PyramidBase):
136
152
  h, w = pyr.shape[:2]
137
153
  expanded = expanded[:h, :w]
138
154
  laplacian.append(pyr - expanded)
155
+ return laplacian
156
+
157
+
158
+ class PyramidStack(PyramidBase):
159
+ def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
160
+ kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
161
+ gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
162
+ float_type=constants.DEFAULT_PY_FLOAT):
163
+ super().__init__("pyramid", min_size, kernel_size, gen_kernel, float_type)
164
+ self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
165
+
166
+ def process_single_image(self, img, levels):
167
+ laplacian = self.single_image_laplacian(img, levels)
139
168
  return laplacian[::-1]
140
169
 
141
170
  def fuse_pyramids(self, all_laplacians):
142
171
  fused = [self.get_fused_base(np.stack([p[-1] for p in all_laplacians], axis=0))]
172
+ count = 0
143
173
  for layer in range(len(all_laplacians[0]) - 2, -1, -1):
144
174
  self.print_message(f': fusing pyramids, layer: {layer + 1}')
145
175
  laplacians = np.stack([p[layer] for p in all_laplacians], axis=0)
146
176
  fused.append(self.fuse_laplacian(laplacians))
177
+ count += 1
178
+ self.after_step(self._steps_per_frame * self.n_frames + count)
179
+ self.check_running()
147
180
  self.print_message(': pyramids fusion completed')
148
181
  return fused[::-1]
149
182
 
150
- def focus_stack(self, filenames):
151
- metadata = None
183
+ def focus_stack(self):
184
+ n = len(self.filenames)
185
+ self.focus_stack_validate()
152
186
  all_laplacians = []
153
- levels = None
154
- n = len(filenames)
155
- for i, img_path in enumerate(filenames):
156
- self.print_message(f": validating file {img_path.split('/')[-1]}")
157
-
158
- img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
159
- if updated:
160
- self.dtype = metadata[1]
161
- self.num_pixel_values = constants.NUM_UINT8 \
162
- if self.dtype == np.uint8 else constants.NUM_UINT16
163
- self.max_pixel_value = constants.MAX_UINT8 \
164
- if self.dtype == np.uint8 else constants.MAX_UINT16
165
- levels = int(np.log2(min(img.shape[:2]) / self.min_size))
166
- if self.do_step_callback:
167
- self.process.callback('after_step', self.process.id, self.process.name, i)
168
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
169
- raise RunStopException(self.name)
170
- for i, img_path in enumerate(filenames):
171
- self.print_message(f": processing file {img_path.split('/')[-1]}")
187
+ for i, img_path in enumerate(self.filenames):
188
+ self.print_message(f": processing file {img_path.split('/')[-1]} ({i + 1}/{n})")
172
189
  img = read_img(img_path)
173
- all_laplacians.append(self.process_single_image(img, levels))
174
- if self.do_step_callback:
175
- self.process.callback('after_step', self.process.id, self.process.name, i + n)
176
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
177
- raise RunStopException(self.name)
190
+ all_laplacians.append(self.process_single_image(img, self.n_levels))
191
+ self.after_step(i + n + 1)
192
+ self.check_running()
178
193
  stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
179
194
  return stacked_image.astype(self.dtype)
@@ -0,0 +1,141 @@
1
+ # pylint: disable=C0114, C0115, C0116, E1101, R0913, R0902, R0914, R0917
2
+ import os
3
+ import numpy as np
4
+ from .. config.constants import constants
5
+ from .utils import extension_tif_jpg
6
+ from .base_stack_algo import BaseStackAlgo
7
+ from .pyramid import PyramidStack
8
+ from .pyramid_tiles import PyramidTilesStack
9
+
10
+
11
+ class PyramidAutoStack(BaseStackAlgo):
12
+ def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
13
+ kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
14
+ gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
15
+ float_type=constants.DEFAULT_PY_FLOAT,
16
+ tile_size=constants.DEFAULT_PY_TILE_SIZE,
17
+ n_tiled_layers=constants.DEFAULT_PY_N_TILED_LAYERS,
18
+ memory_limit=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
19
+ max_threads=constants.DEFAULT_PY_MAX_THREADS,
20
+ max_tile_size=2048,
21
+ min_n_tiled_layers=1,
22
+ mode='auto'):
23
+ super().__init__("auto_pyramid", 2, float_type)
24
+ self.min_size = min_size
25
+ self.kernel_size = kernel_size
26
+ self.gen_kernel = gen_kernel
27
+ self.float_type = float_type
28
+ self.tile_size = tile_size
29
+ self.n_tiled_layers = n_tiled_layers
30
+ self.memory_limit = memory_limit * constants.ONE_GIGA
31
+ self.max_threads = max_threads
32
+ available_cores = os.cpu_count() or 1
33
+ self.num_threads = min(max_threads, available_cores)
34
+ self.max_tile_size = max_tile_size
35
+ self.min_n_tiled_layers = min_n_tiled_layers
36
+ self.mode = mode
37
+ self._implementation = None
38
+ self.dtype = None
39
+ self.shape = None
40
+ self.n_levels = None
41
+ self.n_frames = 0
42
+ self.channels = 3
43
+ dtype = np.float32 if self.float_type == constants.FLOAT_32 else np.float64
44
+ self.bytes_per_pixel = self.channels * np.dtype(dtype).itemsize
45
+ self.overhead = 1.5
46
+
47
+ 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
57
+ self.n_levels = int(np.log2(min(self.shape) / self.min_size))
58
+ self.n_frames = len(filenames)
59
+ memory_required_memory = self._estimate_memory_memory()
60
+ if self.mode == 'memory' or (self.mode == 'auto' and
61
+ memory_required_memory <= self.memory_limit):
62
+ self._implementation = PyramidStack(
63
+ min_size=self.min_size,
64
+ kernel_size=self.kernel_size,
65
+ gen_kernel=self.gen_kernel,
66
+ float_type=self.float_type
67
+ )
68
+ self.print_message(": using memory-based pyramid stacking")
69
+ else:
70
+ optimal_params = self._find_optimal_tile_params()
71
+ self._implementation = PyramidTilesStack(
72
+ min_size=self.min_size,
73
+ kernel_size=self.kernel_size,
74
+ gen_kernel=self.gen_kernel,
75
+ float_type=self.float_type,
76
+ tile_size=optimal_params['tile_size'],
77
+ n_tiled_layers=optimal_params['n_tiled_layers'],
78
+ max_threads=self.num_threads
79
+ )
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']}), "
83
+ f"{self.num_threads} cores.")
84
+ self._implementation.init(filenames)
85
+ self._implementation.set_do_step_callback(self.do_step_callback)
86
+ if self.process is not None:
87
+ self._implementation.set_process(self.process)
88
+ else:
89
+ raise RuntimeError("self.process must be initialized.")
90
+
91
+ def _estimate_memory_memory(self):
92
+ h, w = self.shape[:2]
93
+ total_memory = 0
94
+ for _ in range(self.n_levels):
95
+ total_memory += h * w * self.bytes_per_pixel
96
+ h, w = max(1, h // 2), max(1, w // 2)
97
+ return self.overhead * total_memory * self.n_frames
98
+
99
+ 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)))
103
+ tile_size = min(self.max_tile_size, tile_size_max, self.shape[0], self.shape[1])
104
+ n_tiled_layers = 0
105
+ 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:
109
+ n_tiled_layers = layer + 1
110
+ else:
111
+ break
112
+ n_tiled_layers = max(n_tiled_layers, self.min_n_tiled_layers)
113
+ n_tiled_layers = min(n_tiled_layers, self.n_levels)
114
+ return {'tile_size': tile_size, 'n_tiled_layers': n_tiled_layers}
115
+
116
+ def set_process(self, process):
117
+ super().set_process(process)
118
+ if self._implementation is not None:
119
+ self._implementation.set_process(process)
120
+
121
+ def total_steps(self, n_frames):
122
+ if self._implementation is None:
123
+ return super().total_steps(n_frames)
124
+ return self._implementation.total_steps(n_frames)
125
+
126
+ def focus_stack(self):
127
+ if self._implementation is None:
128
+ raise RuntimeError("PyramidAutoStack not initialized")
129
+ return self._implementation.focus_stack()
130
+
131
+ def after_step(self, step):
132
+ if self._implementation is not None:
133
+ self._implementation.after_step(step)
134
+ else:
135
+ super().after_step(step)
136
+
137
+ def check_running(self, cleanup_callback=None):
138
+ if self._implementation is not None:
139
+ self._implementation.check_running(cleanup_callback)
140
+ else:
141
+ super().check_running(cleanup_callback)