shinestacker 1.0.4__py3-none-any.whl → 1.1.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.

shinestacker/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '1.0.4'
1
+ __version__ = '1.1.0'
@@ -6,7 +6,7 @@ import cv2
6
6
  from .. config.constants import constants
7
7
  from .. core.exceptions import AlignmentError, InvalidOptionError
8
8
  from .. core.colors import color_str
9
- from .utils import img_8bit, img_bw_8bit, save_plot, get_img_metadata, validate_image, img_subsample
9
+ from .utils import img_8bit, img_bw_8bit, save_plot, img_subsample
10
10
  from .stack_framework import SubAction
11
11
 
12
12
  _DEFAULT_FEATURE_CONFIG = {
@@ -161,9 +161,10 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
161
161
  except KeyError as e:
162
162
  raise InvalidOptionError("border_mode", alignment_config['border_mode']) from e
163
163
  min_matches = 4 if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY else 3
164
- validate_image(img_0, *get_img_metadata(img_1))
165
164
  if callbacks and 'message' in callbacks:
166
165
  callbacks['message']()
166
+ h_ref, w_ref = img_1.shape[:2]
167
+ h0, w0 = img_0.shape[:2]
167
168
  subsample = alignment_config['subsample']
168
169
  fast_subsampling = alignment_config['fast_subsampling']
169
170
  min_good_matches = alignment_config['min_good_matches']
@@ -204,15 +205,14 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
204
205
  flags=2), cv2.COLOR_BGR2RGB)
205
206
  plt.figure(figsize=(10, 5))
206
207
  plt.imshow(img_match, 'gray')
207
- plt.savefig(plot_path)
208
+ save_plot(plot_path)
208
209
  if callbacks and 'save_plot' in callbacks:
209
210
  callbacks['save_plot'](plot_path)
210
- h, w = img_0.shape[:2]
211
211
  h_sub, w_sub = img_0_sub.shape[:2]
212
212
  if subsample > 1:
213
213
  if transform == constants.ALIGN_HOMOGRAPHY:
214
214
  low_size = np.float32([[0, 0], [0, h_sub], [w_sub, h_sub], [w_sub, 0]])
215
- high_size = np.float32([[0, 0], [0, h], [w, h], [w, 0]])
215
+ high_size = np.float32([[0, 0], [0, h0], [w0, h0], [w0, 0]])
216
216
  scale_up = cv2.getPerspectiveTransform(low_size, high_size)
217
217
  scale_down = cv2.getPerspectiveTransform(high_size, low_size)
218
218
  m = scale_up @ m @ scale_down
@@ -230,17 +230,17 @@ def align_images(img_1, img_0, feature_config=None, matching_config=None, alignm
230
230
  img_mask = np.ones_like(img_0, dtype=np.uint8)
231
231
  if alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY:
232
232
  img_warp = cv2.warpPerspective(
233
- img_0, m, (w, h),
233
+ img_0, m, (w_ref, h_ref),
234
234
  borderMode=cv2_border_mode, borderValue=alignment_config['border_value'])
235
235
  if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
236
- mask = cv2.warpPerspective(img_mask, m, (w, h),
236
+ mask = cv2.warpPerspective(img_mask, m, (w_ref, h_ref),
237
237
  borderMode=cv2.BORDER_CONSTANT, borderValue=0)
238
238
  elif alignment_config['transform'] == constants.ALIGN_RIGID:
239
239
  img_warp = cv2.warpAffine(
240
- img_0, m, (w, h),
240
+ img_0, m, (w_ref, h_ref),
241
241
  borderMode=cv2_border_mode, borderValue=alignment_config['border_value'])
242
242
  if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
243
- mask = cv2.warpAffine(img_mask, m, (w, h),
243
+ mask = cv2.warpAffine(img_mask, m, (w_ref, h_ref),
244
244
  borderMode=cv2.BORDER_CONSTANT, borderValue=0)
245
245
  if alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
246
246
  if callbacks and 'blur_message' in callbacks:
@@ -293,7 +293,7 @@ class AlignFrames(SubAction):
293
293
  'ecc_message': lambda: self.sub_msg(": ecc refinement"),
294
294
  'blur_message': lambda: self.sub_msg(': blur borders'),
295
295
  'warning': lambda msg: self.sub_msg(
296
- f': {msg}', constants.LOG_COLOR_ALERT),
296
+ f': {msg}', constants.LOG_COLOR_WARNING),
297
297
  'save_plot': lambda plot_path: self.process.callback(
298
298
  'save_plot', self.process.id,
299
299
  f"{self.process.name}: matches\nframe {idx_str}", plot_path)
@@ -315,7 +315,7 @@ class AlignFrames(SubAction):
315
315
  if n_good_matches < self.min_matches:
316
316
  self.process.sub_message(f": image not aligned, too few matches found: "
317
317
  f"{n_good_matches}", level=logging.CRITICAL)
318
- raise AlignmentError(idx, f"too few matches found: "
318
+ raise AlignmentError(idx, f"Image not aligned, too few matches found: "
319
319
  f"{n_good_matches} < {self.min_matches}")
320
320
  return img
321
321
 
@@ -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,17 @@ 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 init(self, filenames):
32
+ self.filenames = filenames
33
+ first_img_file = ''
34
+ for filename in filenames:
35
+ if os.path.isfile(filename) and extension_tif_jpg(filename):
36
+ first_img_file = filename
37
+ break
38
+ self.shape = get_img_file_shape(first_img_file)
39
+
40
+ def total_steps(self, n_frames):
41
+ return self._steps_per_frame * n_frames
29
42
 
30
43
  def print_message(self, msg):
31
44
  self.process.sub_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_3))
@@ -40,3 +53,13 @@ class BaseStackAlgo:
40
53
  else:
41
54
  validate_image(img, *metadata)
42
55
  return img, metadata, updated
56
+
57
+ def check_running(self, cleanup_callback=None):
58
+ if self.process.callback('check_running', self.process.id, self.process.name) is False:
59
+ if cleanup_callback is not None:
60
+ cleanup_callback()
61
+ raise RunStopException(self.name)
62
+
63
+ def after_step(self, step):
64
+ if self.do_step_callback:
65
+ 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,10 +109,8 @@ 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):
@@ -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
 
@@ -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,22 @@ 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
+ for i, img_path in enumerate(self.filenames):
128
+ self.print_message(f": validating file {img_path.split('/')[-1]}")
113
129
 
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
130
+ _img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
131
+ if updated:
132
+ self.dtype = metadata[1]
133
+ self.num_pixel_values = constants.NUM_UINT8 \
134
+ if self.dtype == np.uint8 else constants.NUM_UINT16
135
+ self.max_pixel_value = constants.MAX_UINT8 \
136
+ if self.dtype == np.uint8 else constants.MAX_UINT16
137
+ self.after_step(i + 1)
138
+ self.check_running(cleanup_callback)
124
139
 
125
- def process_single_image(self, img, levels):
140
+ def single_image_laplacian(self, img, levels):
126
141
  pyramid = [img.astype(self.float_type)]
127
142
  for _ in range(levels):
128
143
  next_layer = self.reduce_layer(pyramid[-1])
@@ -136,44 +151,43 @@ class PyramidStack(PyramidBase):
136
151
  h, w = pyr.shape[:2]
137
152
  expanded = expanded[:h, :w]
138
153
  laplacian.append(pyr - expanded)
154
+ return laplacian
155
+
156
+
157
+ class PyramidStack(PyramidBase):
158
+ def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
159
+ kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
160
+ gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
161
+ float_type=constants.DEFAULT_PY_FLOAT):
162
+ super().__init__("pyramid", min_size, kernel_size, gen_kernel, float_type)
163
+ self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
164
+
165
+ def process_single_image(self, img, levels):
166
+ laplacian = self.single_image_laplacian(img, levels)
139
167
  return laplacian[::-1]
140
168
 
141
169
  def fuse_pyramids(self, all_laplacians):
142
170
  fused = [self.get_fused_base(np.stack([p[-1] for p in all_laplacians], axis=0))]
171
+ count = 0
143
172
  for layer in range(len(all_laplacians[0]) - 2, -1, -1):
144
173
  self.print_message(f': fusing pyramids, layer: {layer + 1}')
145
174
  laplacians = np.stack([p[layer] for p in all_laplacians], axis=0)
146
175
  fused.append(self.fuse_laplacian(laplacians))
176
+ count += 1
177
+ self.after_step(self._steps_per_frame * self.n_frames + count)
178
+ self.check_running()
147
179
  self.print_message(': pyramids fusion completed')
148
180
  return fused[::-1]
149
181
 
150
- def focus_stack(self, filenames):
151
- metadata = None
182
+ def focus_stack(self):
183
+ n = len(self.filenames)
184
+ self.focus_stack_validate()
152
185
  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):
186
+ for i, img_path in enumerate(self.filenames):
171
187
  self.print_message(f": processing file {img_path.split('/')[-1]}")
172
188
  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)
189
+ all_laplacians.append(self.process_single_image(img, self.n_levels))
190
+ self.after_step(i + n + 1)
191
+ self.check_running()
178
192
  stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
179
193
  return stacked_image.astype(self.dtype)
@@ -0,0 +1,109 @@
1
+ # pylint: disable=C0114, C0115, C0116, E1101, R0914, R1702, R1732, R0913, R0917, R0912, R0915
2
+ import os
3
+ import tempfile
4
+ import numpy as np
5
+ from .. config.constants import constants
6
+ from .utils import read_img
7
+ from .pyramid import PyramidBase
8
+
9
+
10
+ class PyramidTilesStack(PyramidBase):
11
+ def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
12
+ kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
13
+ gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
14
+ float_type=constants.DEFAULT_PY_FLOAT,
15
+ tile_size=constants.DEFAULT_PY_TILE_SIZE):
16
+ super().__init__("fast_pyramid", min_size, kernel_size, gen_kernel, float_type)
17
+ self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
18
+ self.dtype = None
19
+ self.num_pixel_values = None
20
+ self.max_pixel_value = None
21
+ self.tile_size = tile_size
22
+ self.temp_dir = tempfile.TemporaryDirectory()
23
+ self.n_tiles = 0
24
+
25
+ def init(self, filenames):
26
+ super().init(filenames)
27
+ self.n_tiles = (self.shape[0] // self.tile_size + 1) * (self.shape[1] // self.tile_size + 1)
28
+
29
+ def total_steps(self, n_frames):
30
+ n_steps = super().total_steps(n_frames)
31
+ return n_steps + self.n_tiles
32
+
33
+ def process_single_image(self, img, levels, img_index):
34
+ laplacian = self.single_image_laplacian(img, levels)
35
+ for i, level_data in enumerate(laplacian[::-1]):
36
+ np.save(os.path.join(self.temp_dir.name, f'img_{img_index}_level_{i}.npy'), level_data)
37
+ return len(laplacian)
38
+
39
+ def load_level(self, img_index, level):
40
+ return np.load(os.path.join(self.temp_dir.name, f'img_{img_index}_level_{level}.npy'))
41
+
42
+ def cleanup_temp_files(self):
43
+ self.temp_dir.cleanup()
44
+
45
+ def fuse_pyramids(self, all_level_counts, num_images):
46
+ max_levels = max(all_level_counts)
47
+ fused = []
48
+ count = self._steps_per_frame * self.n_frames
49
+ for level in range(max_levels - 1, -1, -1):
50
+ self.print_message(f': fusing pyramids, layer: {level + 1}')
51
+ if level == 0:
52
+ sample_level = self.load_level(0, 0)
53
+ h, w = sample_level.shape[:2]
54
+ del sample_level
55
+ fused_level = np.zeros((h, w, 3), dtype=self.float_type)
56
+ for y in range(0, h, self.tile_size):
57
+ for x in range(0, w, self.tile_size):
58
+ y_end = min(y + self.tile_size, h)
59
+ x_end = min(x + self.tile_size, w)
60
+ self.print_message(f': fusing tile [{x}, {x_end - 1}]×[{y}, {y_end - 1}]')
61
+ laplacians = []
62
+ for img_index in range(num_images):
63
+ if level < all_level_counts[img_index]:
64
+ full_laplacian = self.load_level(img_index, level)
65
+ tile = full_laplacian[y:y_end, x:x_end]
66
+ laplacians.append(tile)
67
+ del full_laplacian
68
+ stacked = np.stack(laplacians, axis=0)
69
+ fused_tile = self.fuse_laplacian(stacked)
70
+ fused_level[y:y_end, x:x_end] = fused_tile
71
+ del laplacians, stacked, fused_tile
72
+ self.after_step(count)
73
+ self.check_running(self.cleanup_temp_files)
74
+ count += 1
75
+ else:
76
+ laplacians = []
77
+ for img_index in range(num_images):
78
+ if level < all_level_counts[img_index]:
79
+ laplacian = self.load_level(img_index, level)
80
+ laplacians.append(laplacian)
81
+ if level == max_levels - 1:
82
+ stacked = np.stack(laplacians, axis=0)
83
+ fused_level = self.get_fused_base(stacked)
84
+ else:
85
+ stacked = np.stack(laplacians, axis=0)
86
+ fused_level = self.fuse_laplacian(stacked)
87
+ self.check_running(self.cleanup_temp_files)
88
+ fused.append(fused_level)
89
+ count += 1
90
+ self.after_step(count)
91
+ self.check_running(self.cleanup_temp_files)
92
+ self.print_message(': pyramids fusion completed')
93
+ return fused[::-1]
94
+
95
+ def focus_stack(self):
96
+ n = len(self.filenames)
97
+ self.focus_stack_validate(self.cleanup_temp_files)
98
+ all_level_counts = []
99
+ for i, img_path in enumerate(self.filenames):
100
+ self.print_message(f": processing file {img_path.split('/')[-1]}")
101
+ img = read_img(img_path)
102
+ level_count = self.process_single_image(img, self.n_levels, i)
103
+ all_level_counts.append(level_count)
104
+ self.after_step(i + n + 1)
105
+ self.check_running(self.cleanup_temp_files)
106
+ fused_pyramid = self.fuse_pyramids(all_level_counts, n)
107
+ stacked_image = self.collapse(fused_pyramid)
108
+ self.cleanup_temp_files()
109
+ return stacked_image.astype(self.dtype)
@@ -5,7 +5,7 @@ from .. config.constants import constants
5
5
  from .. core.framework import JobBase
6
6
  from .. core.colors import color_str
7
7
  from .. core.exceptions import InvalidOptionError
8
- from .utils import write_img
8
+ from .utils import write_img, extension_tif_jpg
9
9
  from .stack_framework import FrameDirectory, ActionList
10
10
  from .exif import copy_exif_from_file_to_file
11
11
  from .denoise import denoise
@@ -25,8 +25,7 @@ class FocusStackBase(JobBase, FrameDirectory):
25
25
 
26
26
  def focus_stack(self, filenames):
27
27
  self.sub_message_r(color_str(': reading input files', constants.LOG_COLOR_LEVEL_3))
28
- img_files = sorted([os.path.join(self.input_full_path, name) for name in filenames])
29
- stacked_img = self.stack_algo.focus_stack(img_files)
28
+ stacked_img = self.stack_algo.focus_stack()
30
29
  in_filename = filenames[0].split(".")
31
30
  out_filename = f"{self.output_dir}/{self.prefix}{in_filename[0]}." + \
32
31
  '.'.join(in_filename[1:])
@@ -37,8 +36,7 @@ class FocusStackBase(JobBase, FrameDirectory):
37
36
  if self.exif_path != '' and stacked_img.dtype == np.uint8:
38
37
  self.sub_message_r(': copy exif data')
39
38
  _dirpath, _, fnames = next(os.walk(self.exif_path))
40
- fnames = [name for name in fnames
41
- if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
39
+ fnames = [name for name in fnames if extension_tif_jpg(name)]
42
40
  exif_filename = f"{self.exif_path}/{fnames[0]}"
43
41
  copy_exif_from_file_to_file(exif_filename, out_filename)
44
42
  self.sub_message_r(' ' * 60)
@@ -52,6 +50,7 @@ class FocusStackBase(JobBase, FrameDirectory):
52
50
  self.frame_count += 1
53
51
 
54
52
  def init(self, job, working_path=''):
53
+ FrameDirectory.init(self, job)
55
54
  if self.exif_path is None:
56
55
  self.exif_path = job.paths[0]
57
56
  if self.exif_path != '':
@@ -79,7 +78,6 @@ class FocusStackBunch(ActionList, FocusStackBase):
79
78
  "overlap must be smaller than batch size")
80
79
 
81
80
  def init(self, job, _working_path=''):
82
- FrameDirectory.init(self, job)
83
81
  FocusStackBase.init(self, job, self.working_path)
84
82
 
85
83
  def begin(self):
@@ -94,6 +92,9 @@ class FocusStackBunch(ActionList, FocusStackBase):
94
92
  def run_step(self):
95
93
  self.print_message_r(color_str(f"fusing bunch: {self.count + 1}/{self.counts}",
96
94
  constants.LOG_COLOR_LEVEL_2))
95
+ img_files = [os.path.join(self.input_full_path, name)
96
+ for name in self._chunks[self.count - 1]]
97
+ self.stack_algo.init(img_files)
97
98
  self.focus_stack(self._chunks[self.count - 1])
98
99
 
99
100
 
@@ -101,13 +102,15 @@ class FocusStack(FocusStackBase):
101
102
  def __init__(self, name, stack_algo, enabled=True, **kwargs):
102
103
  super().__init__(name, stack_algo, enabled, **kwargs)
103
104
  self.stack_algo.do_step_callback = True
105
+ self.shape = None
104
106
 
105
107
  def run_core(self):
106
108
  self.set_filelist()
109
+ img_files = sorted([os.path.join(self.input_full_path, name) for name in self.filenames])
110
+ self.stack_algo.init(img_files)
107
111
  self.callback('step_counts', self.id, self.name,
108
- self.stack_algo.steps_per_frame() * len(self.filenames))
112
+ self.stack_algo.total_steps(len(self.filenames)))
109
113
  self.focus_stack(self.filenames)
110
114
 
111
115
  def init(self, job, _working_path=''):
112
- FrameDirectory.init(self, job)
113
116
  FocusStackBase.init(self, job, self.working_path)
@@ -7,7 +7,7 @@ from .. core.colors import color_str
7
7
  from .. core.framework import Job, ActionList
8
8
  from .. core.core_utils import check_path_exists
9
9
  from .. core.exceptions import ShapeError, BitDepthError, RunStopException
10
- from .utils import read_img, write_img
10
+ from .utils import read_img, write_img, extension_tif_jpg
11
11
 
12
12
 
13
13
  class StackJob(Job):
@@ -50,8 +50,8 @@ class FramePaths:
50
50
 
51
51
  def set_filelist(self):
52
52
  self.filenames = self.folder_filelist()
53
- file_list = self.input_full_path.replace(self.working_path, '').lstrip('/')
54
- self.print_message(color_str(f": {len(self.filenames)} files in folder: {file_list}",
53
+ file_folder = self.input_full_path.replace(self.working_path, '').lstrip('/')
54
+ self.print_message(color_str(f": {len(self.filenames)} files in folder: {file_folder}",
55
55
  constants.LOG_COLOR_LEVEL_2))
56
56
 
57
57
  def init(self, job):
@@ -113,8 +113,7 @@ class FrameDirectory(FramePaths):
113
113
  def folder_filelist(self):
114
114
  src_contents = os.walk(self.input_full_path)
115
115
  _dirpath, _, filenames = next(src_contents)
116
- filelist = [name for name in filenames
117
- if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
116
+ filelist = [name for name in filenames if extension_tif_jpg(name)]
118
117
  filelist.sort()
119
118
  if self.reverse_order:
120
119
  filelist.reverse()
@@ -159,9 +158,7 @@ class FrameMultiDirectory(FramePaths):
159
158
  for d, p in zip(dirs, paths):
160
159
  filelist = []
161
160
  for _dirpath, _, filenames in os.walk(d):
162
- filelist = [p + "/" + name
163
- for name in filenames
164
- if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
161
+ filelist = [f"{p}/{name}" for name in filenames if extension_tif_jpg(name)]
165
162
  if self.reverse_order:
166
163
  filelist.reverse()
167
164
  if self.resample > 1:
@@ -87,6 +87,11 @@ def img_bw(img):
87
87
  return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
88
88
 
89
89
 
90
+ def get_img_file_shape(file_path):
91
+ img = read_img(file_path)
92
+ return img.shape[:2]
93
+
94
+
90
95
  def get_img_metadata(img):
91
96
  if img is None:
92
97
  return None, None
@@ -34,14 +34,16 @@ class _Constants:
34
34
  SUB_ACTION_TYPES = [ACTION_MASKNOISE, ACTION_VIGNETTING, ACTION_ALIGNFRAMES,
35
35
  ACTION_BALANCEFRAMES]
36
36
  STACK_ALGO_PYRAMID = 'Pyramid'
37
+ STACK_ALGO_PYRAMID_TILES = 'Pyramid Tiles'
37
38
  STACK_ALGO_DEPTH_MAP = 'Depth map'
38
- STACK_ALGO_OPTIONS = [STACK_ALGO_PYRAMID, STACK_ALGO_DEPTH_MAP]
39
+ STACK_ALGO_OPTIONS = [STACK_ALGO_PYRAMID, STACK_ALGO_PYRAMID_TILES, STACK_ALGO_DEPTH_MAP]
39
40
  STACK_ALGO_DEFAULT = STACK_ALGO_PYRAMID
40
41
  DEFAULT_PLOTS_PATH = 'plots'
41
42
 
42
43
  PATH_SEPARATOR = ';'
43
44
 
44
45
  LOG_COLOR_ALERT = 'red'
46
+ LOG_COLOR_WARNING = 'yellow'
45
47
  LOG_COLOR_LEVEL_JOB = 'green'
46
48
  LOG_COLOR_LEVEL_1 = 'blue'
47
49
  LOG_COLOR_LEVEL_2 = 'magenta'
@@ -107,7 +109,7 @@ class _Constants:
107
109
  DEFAULT_BORDER_BLUR = 50
108
110
  DEFAULT_ALIGN_SUBSAMPLE = 2
109
111
  DEFAULT_ALIGN_FAST_SUBSAMPLING = False
110
- DEFAULT_ALIGN_MIN_GOOD_MATCHES = 100
112
+ DEFAULT_ALIGN_MIN_GOOD_MATCHES = 50
111
113
 
112
114
  BALANCE_LINEAR = "LINEAR"
113
115
  BALANCE_GAMMA = "GAMMA"
@@ -160,6 +162,7 @@ class _Constants:
160
162
  DEFAULT_PY_MIN_SIZE = 32
161
163
  DEFAULT_PY_KERNEL_SIZE = 5
162
164
  DEFAULT_PY_GEN_KERNEL = 0.4
165
+ DEFAULT_PY_TILE_SIZE = 512
163
166
 
164
167
  DEFAULT_PLOT_STACK_BUNCH = False
165
168
  DEFAULT_PLOT_STACK = True
@@ -156,8 +156,8 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
156
156
  combo = self.builder.add_field('stacker', FIELD_COMBO, 'Stacking algorithm', required=True,
157
157
  options=constants.STACK_ALGO_OPTIONS,
158
158
  default=constants.STACK_ALGO_DEFAULT)
159
- q_pyramid, q_depthmap = QWidget(), QWidget()
160
- for q in [q_pyramid, q_depthmap]:
159
+ q_pyramid, q_pyramid_tiles, q_depthmap = QWidget(), QWidget(), QWidget()
160
+ for q in [q_pyramid, q_pyramid_tiles, q_depthmap]:
161
161
  layout = QFormLayout()
162
162
  layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
163
163
  layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
@@ -166,13 +166,16 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
166
166
  q.setLayout(layout)
167
167
  stacked = QStackedWidget()
168
168
  stacked.addWidget(q_pyramid)
169
+ stacked.addWidget(q_pyramid_tiles)
169
170
  stacked.addWidget(q_depthmap)
170
171
 
171
172
  def change():
172
173
  text = combo.currentText()
173
- if text == 'Pyramid':
174
+ if text == constants.STACK_ALGO_PYRAMID:
174
175
  stacked.setCurrentWidget(q_pyramid)
175
- elif text == 'Depth map':
176
+ if text == constants.STACK_ALGO_PYRAMID_TILES:
177
+ stacked.setCurrentWidget(q_pyramid_tiles)
178
+ elif text == constants.STACK_ALGO_DEPTH_MAP:
176
179
  stacked.setCurrentWidget(q_depthmap)
177
180
  change()
178
181
  if self.expert:
@@ -191,6 +194,25 @@ class FocusStackBaseConfigurator(DefaultActionConfigurator):
191
194
  options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
192
195
  default=dict(zip(constants.VALID_FLOATS,
193
196
  self.FLOAT_OPTIONS))[constants.DEFAULT_PY_FLOAT])
197
+ self.builder.add_field('tiles_pyramid_min_size', FIELD_INT, 'Minimum size (px)',
198
+ required=False, add_to_layout=q_pyramid_tiles.layout(),
199
+ default=constants.DEFAULT_PY_MIN_SIZE, min_val=2, max_val=256)
200
+ self.builder.add_field('tiles_pyramid_kernel_size', FIELD_INT, 'Kernel size (px)',
201
+ required=False, add_to_layout=q_pyramid_tiles.layout(),
202
+ default=constants.DEFAULT_PY_KERNEL_SIZE, min_val=3, max_val=21)
203
+ self.builder.add_field('tiles_pyramid_gen_kernel', FIELD_FLOAT, 'Gen. kernel',
204
+ required=False, add_to_layout=q_pyramid_tiles.layout(),
205
+ default=constants.DEFAULT_PY_GEN_KERNEL,
206
+ min_val=0.0, max_val=2.0)
207
+ self.builder.add_field('tiles_pyramid_float_type', FIELD_COMBO, 'Precision',
208
+ required=False, add_to_layout=q_pyramid_tiles.layout(),
209
+ options=self.FLOAT_OPTIONS, values=constants.VALID_FLOATS,
210
+ default=dict(zip(constants.VALID_FLOATS,
211
+ self.FLOAT_OPTIONS))[constants.DEFAULT_PY_FLOAT])
212
+ self.builder.add_field('tiles_pyramid_tile_size', FIELD_INT, 'Tile size (px)',
213
+ required=False, add_to_layout=q_pyramid_tiles.layout(),
214
+ default=constants.DEFAULT_PY_TILE_SIZE,
215
+ min_val=128, max_val=2048)
194
216
  self.builder.add_field('depthmap_energy', FIELD_COMBO, 'Energy', required=False,
195
217
  add_to_layout=q_depthmap.layout(),
196
218
  options=self.ENERGY_OPTIONS, values=constants.VALID_DM_ENERGY,
@@ -0,0 +1,105 @@
1
+ # pylint: disable=C0114, C0115, C0116, E0611, C0103, R0914
2
+ from PySide6.QtWidgets import QLayout
3
+ from PySide6.QtCore import Qt, QRect, QSize, QPoint
4
+
5
+
6
+ class FlowLayout(QLayout):
7
+ def __init__(self, parent=None, margin=0, spacing=-1, justify=True):
8
+ super().__init__(parent)
9
+ self._item_list = []
10
+ self._justify = justify
11
+ self.setContentsMargins(margin, margin, margin, margin)
12
+ self.setSpacing(spacing)
13
+
14
+ def addItem(self, item):
15
+ self._item_list.append(item)
16
+
17
+ def count(self):
18
+ return len(self._item_list)
19
+
20
+ def itemAt(self, index):
21
+ if 0 <= index < len(self._item_list):
22
+ return self._item_list[index]
23
+ return None
24
+
25
+ def takeAt(self, index):
26
+ if 0 <= index < len(self._item_list):
27
+ return self._item_list.pop(index)
28
+ return None
29
+
30
+ def expandingDirections(self):
31
+ return Qt.Orientations(0)
32
+
33
+ def hasHeightForWidth(self):
34
+ return True
35
+
36
+ def heightForWidth(self, width):
37
+ return self._do_layout(QRect(0, 0, width, 0), True)
38
+
39
+ def setGeometry(self, rect):
40
+ super().setGeometry(rect)
41
+ self._do_layout(rect, False)
42
+
43
+ def sizeHint(self):
44
+ return self.minimumSize()
45
+
46
+ def minimumSize(self):
47
+ size = QSize()
48
+ for item in self._item_list:
49
+ size = size.expandedTo(item.minimumSize())
50
+ margins = self.contentsMargins()
51
+ size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom())
52
+ return size
53
+
54
+ def setJustify(self, justify):
55
+ self._justify = justify
56
+ self.invalidate()
57
+
58
+ def justify(self):
59
+ return self._justify
60
+
61
+ def _do_layout(self, rect, test_only):
62
+ x = rect.x()
63
+ y = rect.y()
64
+ line_height = 0
65
+ spacing = self.spacing()
66
+ lines = []
67
+ current_line = []
68
+ current_line_width = 0
69
+ for item in self._item_list:
70
+ space_x = spacing
71
+ next_x = x + item.sizeHint().width() + space_x
72
+ if next_x - space_x > rect.right() and line_height > 0:
73
+ lines.append((current_line, current_line_width, line_height))
74
+ x = rect.x()
75
+ y = y + line_height + spacing
76
+ next_x = x + item.sizeHint().width() + space_x
77
+ current_line = []
78
+ current_line_width = 0
79
+ line_height = 0
80
+ current_line.append(item)
81
+ current_line_width += item.sizeHint().width()
82
+ x = next_x
83
+ line_height = max(line_height, item.sizeHint().height())
84
+ if current_line:
85
+ lines.append((current_line, current_line_width, line_height))
86
+ y_offset = rect.y()
87
+ for line, line_width, line_height in lines:
88
+ if not test_only:
89
+ available_width = rect.width() - (len(line) - 1) * spacing
90
+ if self._justify and len(line) > 1:
91
+ stretch_factor = available_width / line_width if line_width > 0 else 1
92
+ x_offset = rect.x()
93
+ for item in line:
94
+ item_width = int(item.sizeHint().width() * stretch_factor)
95
+ item.setGeometry(QRect(QPoint(x_offset, y_offset),
96
+ QSize(item_width, line_height)))
97
+ x_offset += item_width + spacing
98
+ else:
99
+ x_offset = rect.x()
100
+ for item in line:
101
+ item.setGeometry(QRect(QPoint(x_offset, y_offset),
102
+ item.sizeHint()))
103
+ x_offset += item.sizeHint().width() + spacing
104
+ y_offset += line_height + spacing
105
+ return y_offset - spacing - rect.y()
@@ -1,5 +1,6 @@
1
1
  # pylint: disable=C0114, C0115, C0116, E0611, R0903, R0915, R0914, R0917, R0913, R0902
2
2
  import os
3
+ import traceback
3
4
  from PySide6.QtWidgets import (QWidget, QPushButton, QVBoxLayout, QHBoxLayout,
4
5
  QMessageBox, QScrollArea, QSizePolicy, QFrame, QLabel, QComboBox)
5
6
  from PySide6.QtGui import QColor
@@ -16,6 +17,7 @@ from .colors import (
16
17
  ACTION_RUNNING_COLOR, ACTION_COMPLETED_COLOR,
17
18
  ACTION_STOPPED_COLOR, ACTION_FAILED_COLOR)
18
19
  from .time_progress_bar import TimerProgressBar
20
+ from .flow_layout import FlowLayout
19
21
 
20
22
 
21
23
  class ColorButton(QPushButton):
@@ -55,12 +57,12 @@ class RunWindow(QTextEditLogger):
55
57
  for label_row in labels:
56
58
  self.color_widgets.append([])
57
59
  row = QWidget(self)
58
- h_layout = QHBoxLayout(row)
60
+ h_layout = FlowLayout(row) # QHBoxLayout(row)
59
61
  h_layout.setContentsMargins(0, 0, 0, 0)
60
62
  h_layout.setSpacing(2)
61
63
  for label, enabled in label_row:
62
64
  widget = ColorButton(label, enabled)
63
- h_layout.addWidget(widget, stretch=1)
65
+ h_layout.addWidget(widget) # addWidget(widget, stretch=1)
64
66
  self.color_widgets[-1].append(widget)
65
67
  layout.addWidget(row)
66
68
  self.progress_bar = TimerProgressBar()
@@ -202,23 +204,26 @@ class RunWindow(QTextEditLogger):
202
204
  label = QLabel(name, self)
203
205
  label.setStyleSheet("QLabel {margin-top: 5px; font-weight: bold;}")
204
206
  self.image_layout.addWidget(label)
205
- if extension_pdf(path):
206
- image_view = GuiPdfView(path, self)
207
- elif extension_tif_jpg(path):
208
- image_view = GuiImageView(path, self)
209
- else:
210
- raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
211
- self.image_views.append(image_view)
212
- self.image_layout.addWidget(image_view)
213
- max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
214
- needed_width = max_width + 20
215
- self.right_area.setFixedWidth(needed_width)
216
- self.image_area_widget.setFixedWidth(needed_width)
217
- self.right_area.updateGeometry()
218
- self.image_area_widget.updateGeometry()
219
- QTimer.singleShot(
220
- 0, lambda: self.right_area.verticalScrollBar().setValue(
221
- self.right_area.verticalScrollBar().maximum()))
207
+ try:
208
+ if extension_pdf(path):
209
+ image_view = GuiPdfView(path, self)
210
+ elif extension_tif_jpg(path):
211
+ image_view = GuiImageView(path, self)
212
+ else:
213
+ raise RuntimeError(f"Can't visualize file type {os.path.splitext(path)[1]}.")
214
+ self.image_views.append(image_view)
215
+ self.image_layout.addWidget(image_view)
216
+ max_width = max(pv.size().width() for pv in self.image_views) if self.image_views else 0
217
+ needed_width = max_width + 20
218
+ self.right_area.setFixedWidth(needed_width)
219
+ self.image_area_widget.setFixedWidth(needed_width)
220
+ self.right_area.updateGeometry()
221
+ self.image_area_widget.updateGeometry()
222
+ QTimer.singleShot(
223
+ 0, lambda: self.right_area.verticalScrollBar().setValue(
224
+ self.right_area.verticalScrollBar().maximum()))
225
+ except RuntimeError as e:
226
+ traceback.print_tb(e.__traceback__)
222
227
 
223
228
  @Slot(int, str, str, str)
224
229
  def handle_open_app(self, _run_id, name, app, path):
@@ -406,8 +406,8 @@ class MainWindow(QMainWindow, LogManager):
406
406
  if self.job_list_count() == 0:
407
407
  self.menu_manager.add_action_entry_action.setEnabled(False)
408
408
  self.menu_manager.action_selector.setEnabled(False)
409
- self.run_job_action.setEnabled(False)
410
- self.run_all_jobs_action.setEnabled(False)
409
+ self.menu_manager.run_job_action.setEnabled(False)
410
+ self.menu_manager.run_all_jobs_action.setEnabled(False)
411
411
  else:
412
412
  self.menu_manager.add_action_entry_action.setEnabled(True)
413
413
  self.menu_manager.action_selector.setEnabled(True)
@@ -110,6 +110,7 @@ class NewProjectDialog(BaseFormDialog):
110
110
  self.layout.addRow("Focus stack (depth map):", self.focus_stack_depth_map)
111
111
  else:
112
112
  self.layout.addRow("Focus stack:", self.focus_stack_pyramid)
113
+ if self.expert():
113
114
  self.layout.addRow("Save multi layer TIFF:", self.multi_layer)
114
115
  self.add_label("")
115
116
  self.add_bold_label("3️⃣ Push 🆗 for further options, then press ▶️ to run.")
@@ -290,6 +290,7 @@ class ProjectController(QObject):
290
290
  with open(file_path, 'w', encoding="utf-8") as f:
291
291
  f.write(json_obj)
292
292
  self.mark_as_modified(False)
293
+ self.update_title_requested.emit()
293
294
  except Exception as e:
294
295
  QMessageBox.critical(self.parent, "Error", f"Cannot save file:\n{str(e)}")
295
296
 
@@ -10,6 +10,7 @@ from .. algorithms.align import AlignFrames
10
10
  from .. algorithms.balance import BalanceFrames
11
11
  from .. algorithms.stack import FocusStack, FocusStackBunch
12
12
  from .. algorithms.pyramid import PyramidStack
13
+ from .. algorithms.pyramid_tiles import PyramidTilesStack
13
14
  from .. algorithms.depth_map import DepthMapStack
14
15
  from .. algorithms.multilayer import MultiLayer
15
16
  from .project_model import Project, ActionConfig
@@ -104,21 +105,26 @@ class ProjectConverter:
104
105
  constants.ACTION_FOCUSSTACKBUNCH):
105
106
  stacker = action_config.params.get('stacker', constants.STACK_ALGO_DEFAULT)
106
107
  if stacker == constants.STACK_ALGO_PYRAMID:
107
- algo_dict, module_dict = self.filter_dict_keys(action_config.params, 'pyramid_')
108
+ algo_dict, module_dict = self.filter_dict_keys(
109
+ action_config.params, 'pyramid_')
108
110
  stack_algo = PyramidStack(**algo_dict)
111
+ elif stacker == constants.STACK_ALGO_PYRAMID_TILES:
112
+ algo_dict, module_dict = self.filter_dict_keys(
113
+ action_config.params, 'tiles_pyramid_')
114
+ stack_algo = PyramidTilesStack(**algo_dict)
109
115
  elif stacker == constants.STACK_ALGO_DEPTH_MAP:
110
- algo_dict, module_dict = self.filter_dict_keys(action_config.params, 'depthmap_')
116
+ algo_dict, module_dict = self.filter_dict_keys(
117
+ action_config.params, 'depthmap_')
111
118
  stack_algo = DepthMapStack(**algo_dict)
112
119
  else:
113
120
  raise InvalidOptionError('stacker', stacker, f"valid options are: "
114
121
  f"{constants.STACK_ALGO_PYRAMID}, "
122
+ f"{constants.STACK_ALGO_PYRAMID_TILES}, "
115
123
  f"{constants.STACK_ALGO_DEPTH_MAP}")
116
124
  if action_config.type_name == constants.ACTION_FOCUSSTACK:
117
125
  return FocusStack(**module_dict, stack_algo=stack_algo)
118
126
  if action_config.type_name == constants.ACTION_FOCUSSTACKBUNCH:
119
127
  return FocusStackBunch(**module_dict, stack_algo=stack_algo)
120
- raise InvalidOptionError(
121
- "stracker", stacker, details="valid values are: Pyramid, Depth map.")
122
128
  if action_config.type_name == constants.ACTION_MULTILAYER:
123
129
  input_path = list(filter(lambda p: p != '',
124
130
  action_config.params.get('input_path', '').split(";")))
@@ -39,8 +39,8 @@ class TimerProgressBar(QProgressBar):
39
39
  """)
40
40
 
41
41
  def time_str(self, secs):
42
- ss = int(secs)
43
- x = secs - ss
42
+ x = secs % 1
43
+ ss = int(secs // 1)
44
44
  s = ss % 60
45
45
  mm = ss // 60
46
46
  m = mm % 60
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shinestacker
3
- Version: 1.0.4
3
+ Version: 1.1.0
4
4
  Summary: ShineStacker
5
5
  Author-email: Luca Lista <luka.lista@gmail.com>
6
6
  License-Expression: LGPL-3.0
@@ -69,6 +69,25 @@ The GUI has two main working areas:
69
69
 
70
70
  <img src='https://raw.githubusercontent.com/lucalista/shinestacker/main/img/gui-retouch.png' width="600" referrerpolicy="no-referrer">
71
71
 
72
+ # Note for macOS users
73
+
74
+ **The following note is only relevant if you download the application as compressed archive from the [release page](https://github.com/lucalista/shinestacker/releases).**
75
+
76
+ The macOS system security protection prevent to run applications downloaded from the web that come from developers that don't hold an Apple Developer Certificate.
77
+
78
+ In order to prevent this, follow the instructions below:
79
+
80
+ 1. Download the compressed archive ```shinestacker-macos.tar.gz``` in your ```Download``` folder.
81
+ 2. Double-click the archive to uncompress it. You will find a new folder ```shinestacker```.
82
+ 3. Open a terminal (*Applications > Utilities > Terminal*)
83
+ 4. Type the folliwng command on the terminal:
84
+ ```bash
85
+ xattr -cr ~/Downloads/shinestacker/shinestacker.app
86
+ ```
87
+ 5. Now you can double-click the Sine Stacker icon app in the ```shiestacker``` folder and it should run.
88
+
89
+ macOS adds a quarantine flag to all files downloaded from the internet. The above command removes that flag while preserving all other application functionality.
90
+
72
91
  # Resources
73
92
 
74
93
  🌍 [Website on WordPress](https://shinestacker.wordpress.com) • 📖 [Main documentation](https://shinestacker.readthedocs.io) • 📝 [Changelog](https://github.com/lucalista/shinestacker/blob/main/CHANGELOG.md)
@@ -1,19 +1,20 @@
1
1
  shinestacker/__init__.py,sha256=uq2fjAw2z_6TpH3mOcWFZ98GoEPRsNhTAK8N0MMm_e8,448
2
- shinestacker/_version.py,sha256=Oi2b5pm3sFbESQW0xgj8kqwDPX_Hxmx4gNILYpLzYqI,21
2
+ shinestacker/_version.py,sha256=XIz3qAg9G9YysQi3Ryp0CN3rtc_JiecHZ9L2vEzcM6s,21
3
3
  shinestacker/algorithms/__init__.py,sha256=c4kRrdTLlVI70Q16XkI1RSmz5MD7npDqIpO_02jTG6g,747
4
- shinestacker/algorithms/align.py,sha256=XT4DJoD5ZvpkC1-J3W3GWmWRsXJg3qJ-3zr9erT8oW0,17514
4
+ shinestacker/algorithms/align.py,sha256=EhsV50QtpLSJG0uDMfOJw89u8CGFJvBC2sYuJg5cv6g,17516
5
5
  shinestacker/algorithms/balance.py,sha256=iSjO-pl0vQv58iEQ077EUcDTAExMKDBdtXmJXbMhazk,16721
6
- shinestacker/algorithms/base_stack_algo.py,sha256=AFV2QkcFNaTcnISpsWHuAVy2De9hhaPcBNjE1O0h50I,1430
6
+ shinestacker/algorithms/base_stack_algo.py,sha256=O7pDXqLM8MBdLR634Vk3UNV6cEV2q0U7CNcnpC_AOig,2363
7
7
  shinestacker/algorithms/denoise.py,sha256=GL3Z4_6MHxSa7Wo4ZzQECZS87tHBFqO0sIVF_jPuYQU,426
8
- shinestacker/algorithms/depth_map.py,sha256=FOR5M0brO5-9NnXDY7TWpc3OtKKSuzrOSoBMe0cP6Ho,6076
8
+ shinestacker/algorithms/depth_map.py,sha256=KVThrnynPKuijlh-DrenSkdkZ0Qm6TaNMYKhRByhcN4,5682
9
9
  shinestacker/algorithms/exif.py,sha256=SM4ZDDe8hCJ3xY6053FNndOiwzEStzdp0WrXurlcHVc,9429
10
10
  shinestacker/algorithms/multilayer.py,sha256=-pQXDlooSMGKPhMgF-_naXdkGdolclYvSD-RrjwLiyI,9328
11
- shinestacker/algorithms/noise_detection.py,sha256=CDnN8pglxufY5Y-dT3mVooD4zPySdSq9CMgtDGMXBnA,8970
12
- shinestacker/algorithms/pyramid.py,sha256=_Pk19lRQ21b3W3aHQ6DgAe9VVOfbsi2a9jrynF0qFVw,8610
11
+ shinestacker/algorithms/noise_detection.py,sha256=CJb57mE7ePJBgrwnsEkeK8xVIl2Hrzti11ZEI6JQczs,9218
12
+ shinestacker/algorithms/pyramid.py,sha256=cxwA6gf02009dFv5-m79NpJkD58-Wbu3im4bfA5HVUc,8822
13
+ shinestacker/algorithms/pyramid_tiles.py,sha256=967L42MfwSOewisqpAzuXivZgVoKZbIjDIQVWP1_rHk,5094
13
14
  shinestacker/algorithms/sharpen.py,sha256=h7PMJBYxucg194Usp_6pvItPUMFYbT-ebAc_-7XBFUw,949
14
- shinestacker/algorithms/stack.py,sha256=FCU89Of-s6C_DuMleG06c8V6fnIm9MFInvkkKtTsGBo,4906
15
- shinestacker/algorithms/stack_framework.py,sha256=peAlUUl7y8OcquhjQoXpiwsEhZw6zgZnzwt1IDpf4aU,12466
16
- shinestacker/algorithms/utils.py,sha256=0AeMVaFuhpUiIpUUFqrqAJ_-ohGVdX7-EdMyLoVflbg,3279
15
+ shinestacker/algorithms/stack.py,sha256=T1y-qoYUNzcIpnhKcou_4ifiKtGC2ZA1bOZXlfnKB6A,5045
16
+ shinestacker/algorithms/stack_framework.py,sha256=fHdU8uYZquRut6NW_1vHbTCjPD99gQfOhVDdaaLZH34,12334
17
+ shinestacker/algorithms/utils.py,sha256=GSKPUxU98Q8F0k4TgY9ydEgBul1gf2I0ypdmyDm--Mg,3371
17
18
  shinestacker/algorithms/vignetting.py,sha256=yW-1TF4tesLWfKQOS0XxRkOEN82U-YDmMaj09C9cH4M,9552
18
19
  shinestacker/algorithms/white_balance.py,sha256=PMKsBtxOSn5aRr_Gkx1StHS4eN6kBN2EhNnhg4UG24g,501
19
20
  shinestacker/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -26,7 +27,7 @@ shinestacker/app/project.py,sha256=W0u715LZne_PNJvg9msSy27ybIjgDXiEAQdJ7_6BjYI,2
26
27
  shinestacker/app/retouch.py,sha256=ZQ-nRKnHo6xurcP34RNqaAWkmuGBjJ5jE05hTQ_ycis,2482
27
28
  shinestacker/config/__init__.py,sha256=aXxi-LmAvXd0daIFrVnTHE5OCaYeK1uf1BKMr7oaXQs,197
28
29
  shinestacker/config/config.py,sha256=eBko2D3ADhLTIm9X6hB_a_WsIjwgfE-qmBVkhP1XSvc,1636
29
- shinestacker/config/constants.py,sha256=l7RcRxXlIThkBXcy2GVRBWFZQRXNgqYShNEVfDpgjEU,6096
30
+ shinestacker/config/constants.py,sha256=X9e0fXr7ZHN9DCEMiObpanNJfZ5cMgWJdm3Xybmh-Wk,6232
30
31
  shinestacker/config/gui_constants.py,sha256=5DR-ET1oeMMD7lIsjvAwSuln89A7I9wy9VuAeRo2G64,2575
31
32
  shinestacker/core/__init__.py,sha256=IUEIx6SQ3DygDEHN3_E6uKpHjHtUa4a_U_1dLd_8yEU,484
32
33
  shinestacker/core/colors.py,sha256=kr_tJA1iRsdck2JaYDb2lS-codZ4Ty9gdu3kHfiWvuM,1340
@@ -36,22 +37,23 @@ shinestacker/core/framework.py,sha256=zCnJuQrHNpwEgJW23_BgS7iQrLolRWTAMB1oRp_a7K
36
37
  shinestacker/core/logging.py,sha256=9SuSSy9Usbh7zqmLYMqkmy-VBkOJW000lwqAR0XQs30,3067
37
38
  shinestacker/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
39
  shinestacker/gui/action_config.py,sha256=yXNDv0MyONbHk4iUrkvMkLKKaDvpJyzA5Yr0Eikgo0c,16986
39
- shinestacker/gui/action_config_dialog.py,sha256=6Xjbtj7oHGXBNiogcnPoFHqcuTOnZFlMzWCZXv8eBAI,32623
40
+ shinestacker/gui/action_config_dialog.py,sha256=Xb8DryEQt9R3VvM2DVoT_SdlJMU_qzJs2Z7NdLeqsgY,34520
40
41
  shinestacker/gui/base_form_dialog.py,sha256=yYqMee1mzw9VBx8siBS0jDk1qqsTIKJUgdjh92aprQk,687
41
42
  shinestacker/gui/colors.py,sha256=m0pQQ-uvtIN1xmb_-N06BvC7pZYZZnq59ZSEJwutHuk,1432
43
+ shinestacker/gui/flow_layout.py,sha256=3yBU_z7VtvHKpx1H97CHVd81eq9pe1Dcja2EZBGGKcI,3791
42
44
  shinestacker/gui/gui_images.py,sha256=e0KAXSPruZoRHrajfdlmOKBYoRJJQBDan1jgs7YFltY,5678
43
45
  shinestacker/gui/gui_logging.py,sha256=kiZcrC2AFYCWgPZo0O5SKw-E5cFrezwf4anS3HjPuNw,8168
44
- shinestacker/gui/gui_run.py,sha256=Lf_hXWPk1bgAYumxqjPHSK8UfiAIUR7A047ECGKD_uU,15183
45
- shinestacker/gui/main_window.py,sha256=l5iMk5aIi5nXPccnibB1tswc0agatrYgXULVkXo7OV0,24254
46
+ shinestacker/gui/gui_run.py,sha256=ahbl6xMFR78QrcBbEDMuaQpkxw6DBFtSX8DCMIyr_7I,15439
47
+ shinestacker/gui/main_window.py,sha256=KVr3ApbQSjJgmhHnrcqTjGQNTj1LoTN2PD6bWLBjsh8,24280
46
48
  shinestacker/gui/menu_manager.py,sha256=_L6LOikB3impEYqilqwXc0WJuunishjz57ozZlrBn7Q,9616
47
- shinestacker/gui/new_project.py,sha256=DbndYiaxwjW0OJGJ3N9JwpwLrXVsdtoSSlFRf6T8yEA,10781
48
- shinestacker/gui/project_controller.py,sha256=QJSQlwEJeXJyJnkp42D9NSqWBb8q2kLf_GvLfe3pe_c,15076
49
- shinestacker/gui/project_converter.py,sha256=_AFfU2HYKPX78l6iX6bXJrlKpdjSl63pmKzrc6kQpn8,7348
49
+ shinestacker/gui/new_project.py,sha256=c0y2BjnAVaf5Z88UDmuOGR5rdju0Q72ltqiE7T3QivY,10807
50
+ shinestacker/gui/project_controller.py,sha256=zVMH8kcNJ75dXPjaTa0IQiavqcWxG1URlVVYWnnu1C0,15123
51
+ shinestacker/gui/project_converter.py,sha256=8ko3D4D7x4hhwENxwpTeElnLtEex3lpR51nZbq30Uco,7655
50
52
  shinestacker/gui/project_editor.py,sha256=uouzmUkrqouQlq-dqPOgSO16r1WOnGNV2v8jTcZlRXU,23749
51
53
  shinestacker/gui/project_model.py,sha256=eRUmH3QmRzDtPtZoxgT6amKzN8_5XzwjHgEJeL-_JOE,4263
52
54
  shinestacker/gui/select_path_widget.py,sha256=OfQImOmkzbvl5BBshmb7ePWrSGDJQ8VvyaAOypHAGd4,1023
53
55
  shinestacker/gui/tab_widget.py,sha256=6iUifK-wu0EzjVFccKHirhA2fENglVi6xREKiD96aaY,2950
54
- shinestacker/gui/time_progress_bar.py,sha256=Ty7pNTfbKU44Y_0YQNYtgEcxpOD-Bbi4lC8g-u9bno0,3012
56
+ shinestacker/gui/time_progress_bar.py,sha256=4_5DT_EzFdVJi5bgd9TEpoTJXeU3M08CF91cZLi75Wc,3016
55
57
  shinestacker/gui/ico/focus_stack_bkg.png,sha256=Q86TgqvKEi_IzKI8m6aZB2a3T40UkDtexf2PdeBM9XE,163151
56
58
  shinestacker/gui/ico/shinestacker.icns,sha256=3IshIOv0uFexYsAEPkE9xiyuw8mB5X5gffekOUhFlt0,45278
57
59
  shinestacker/gui/ico/shinestacker.ico,sha256=8IMRk-toObWUz8iDXA-zHBWQ8Ps3vXN5u5ZEyw7sP3c,109613
@@ -83,9 +85,9 @@ shinestacker/retouch/undo_manager.py,sha256=_ekbcOLcPbQLY7t-o8wf-b1uA6OPY9rRyLM-
83
85
  shinestacker/retouch/unsharp_mask_filter.py,sha256=uFnth8fpZFGhdIgJCnS8x5v6lBQgJ3hX0CBke9pFXeM,3510
84
86
  shinestacker/retouch/vignetting_filter.py,sha256=3WuoF38lQOIaU1MWmqviItuQn8NnbMN0nwV7pM9IJqU,3453
85
87
  shinestacker/retouch/white_balance_filter.py,sha256=glMBYlmrF-i_OrB3sGUpjZE6X4FQdyLC4GBy2bWtaFc,6056
86
- shinestacker-1.0.4.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
87
- shinestacker-1.0.4.dist-info/METADATA,sha256=tKyo0ogsBjWAHk4lAshkSqx62swN-s01hXEO2mJkHOg,5903
88
- shinestacker-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
89
- shinestacker-1.0.4.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
90
- shinestacker-1.0.4.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
91
- shinestacker-1.0.4.dist-info/RECORD,,
88
+ shinestacker-1.1.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
89
+ shinestacker-1.1.0.dist-info/METADATA,sha256=cg9TAx9qmIME39z02b5lLxjbtXhf_sT0aGNsfHh296E,6951
90
+ shinestacker-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
91
+ shinestacker-1.1.0.dist-info/entry_points.txt,sha256=SY6g1LqtMmp23q1DGwLUDT_dhLX9iss8DvWkiWLyo_4,166
92
+ shinestacker-1.1.0.dist-info/top_level.txt,sha256=MhijwnBVX5psfsyX8JZjqp3SYiWPsKe69f3Gnyze4Fw,13
93
+ shinestacker-1.1.0.dist-info/RECORD,,