shinestacker 0.3.3__py3-none-any.whl → 0.3.5__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 (71) hide show
  1. shinestacker/__init__.py +2 -1
  2. shinestacker/_version.py +1 -1
  3. shinestacker/algorithms/__init__.py +3 -2
  4. shinestacker/algorithms/align.py +102 -64
  5. shinestacker/algorithms/balance.py +89 -42
  6. shinestacker/algorithms/base_stack_algo.py +41 -0
  7. shinestacker/algorithms/core_utils.py +6 -6
  8. shinestacker/algorithms/denoise.py +4 -1
  9. shinestacker/algorithms/depth_map.py +28 -39
  10. shinestacker/algorithms/exif.py +43 -38
  11. shinestacker/algorithms/multilayer.py +48 -28
  12. shinestacker/algorithms/noise_detection.py +34 -26
  13. shinestacker/algorithms/pyramid.py +42 -42
  14. shinestacker/algorithms/sharpen.py +1 -0
  15. shinestacker/algorithms/stack.py +42 -42
  16. shinestacker/algorithms/stack_framework.py +118 -66
  17. shinestacker/algorithms/utils.py +12 -11
  18. shinestacker/algorithms/vignetting.py +52 -25
  19. shinestacker/algorithms/white_balance.py +1 -0
  20. shinestacker/app/about_dialog.py +6 -2
  21. shinestacker/app/app_config.py +1 -0
  22. shinestacker/app/gui_utils.py +20 -0
  23. shinestacker/app/help_menu.py +2 -1
  24. shinestacker/app/main.py +9 -18
  25. shinestacker/app/open_frames.py +5 -4
  26. shinestacker/app/project.py +5 -16
  27. shinestacker/app/retouch.py +5 -17
  28. shinestacker/core/colors.py +4 -4
  29. shinestacker/core/core_utils.py +1 -1
  30. shinestacker/core/exceptions.py +2 -1
  31. shinestacker/core/framework.py +46 -33
  32. shinestacker/core/logging.py +9 -10
  33. shinestacker/gui/action_config.py +253 -197
  34. shinestacker/gui/actions_window.py +36 -35
  35. shinestacker/gui/colors.py +1 -0
  36. shinestacker/gui/gui_images.py +7 -3
  37. shinestacker/gui/gui_logging.py +3 -2
  38. shinestacker/gui/gui_run.py +53 -38
  39. shinestacker/gui/main_window.py +69 -25
  40. shinestacker/gui/new_project.py +35 -2
  41. shinestacker/gui/project_converter.py +21 -20
  42. shinestacker/gui/project_editor.py +51 -52
  43. shinestacker/gui/project_model.py +15 -23
  44. shinestacker/retouch/{filter_base.py → base_filter.py} +7 -4
  45. shinestacker/retouch/brush.py +1 -0
  46. shinestacker/retouch/brush_gradient.py +17 -3
  47. shinestacker/retouch/brush_preview.py +14 -10
  48. shinestacker/retouch/brush_tool.py +28 -19
  49. shinestacker/retouch/denoise_filter.py +3 -2
  50. shinestacker/retouch/display_manager.py +11 -5
  51. shinestacker/retouch/exif_data.py +1 -0
  52. shinestacker/retouch/file_loader.py +13 -9
  53. shinestacker/retouch/filter_manager.py +1 -0
  54. shinestacker/retouch/image_editor.py +14 -48
  55. shinestacker/retouch/image_editor_ui.py +10 -5
  56. shinestacker/retouch/image_filters.py +4 -2
  57. shinestacker/retouch/image_viewer.py +33 -31
  58. shinestacker/retouch/io_gui_handler.py +25 -13
  59. shinestacker/retouch/io_manager.py +3 -2
  60. shinestacker/retouch/layer_collection.py +79 -23
  61. shinestacker/retouch/shortcuts_help.py +1 -0
  62. shinestacker/retouch/undo_manager.py +7 -0
  63. shinestacker/retouch/unsharp_mask_filter.py +3 -2
  64. shinestacker/retouch/white_balance_filter.py +11 -6
  65. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/METADATA +18 -6
  66. shinestacker-0.3.5.dist-info/RECORD +86 -0
  67. shinestacker-0.3.3.dist-info/RECORD +0 -85
  68. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/WHEEL +0 -0
  69. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/entry_points.txt +0 -0
  70. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/licenses/LICENSE +0 -0
  71. {shinestacker-0.3.3.dist-info → shinestacker-0.3.5.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,10 @@
1
+ # pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915
2
+ import os
3
+ import errno
4
+ import logging
1
5
  import cv2
2
6
  import numpy as np
3
7
  import matplotlib.pyplot as plt
4
- import logging
5
- import os
6
- import errno
7
8
  from .. config.config import config
8
9
  from .. config.constants import constants
9
10
  from .. core.colors import color_str
@@ -21,7 +22,7 @@ def mean_image(file_paths, max_frames=-1, message_callback=None, progress_callba
21
22
  mean_img = None
22
23
  counter = 0
23
24
  for i, path in enumerate(file_paths):
24
- if max_frames >= 1 and i > max_frames:
25
+ if 1 <= max_frames < i:
25
26
  break
26
27
  if message_callback:
27
28
  message_callback(path)
@@ -31,7 +32,7 @@ def mean_image(file_paths, max_frames=-1, message_callback=None, progress_callba
31
32
  img = read_img(path)
32
33
  except Exception:
33
34
  logger = logging.getLogger(__name__)
34
- logger.error("Can't open file: " + path)
35
+ logger.error(msg=f"Can't open file: {path}")
35
36
  if mean_img is None:
36
37
  metadata = get_img_metadata(img)
37
38
  mean_img = img.astype(np.float64)
@@ -44,7 +45,7 @@ def mean_image(file_paths, max_frames=-1, message_callback=None, progress_callba
44
45
  return None if mean_img is None else (mean_img / counter).astype(np.uint8)
45
46
 
46
47
 
47
- class NoiseDetection(FrameMultiDirectory, JobBase):
48
+ class NoiseDetection(JobBase, FrameMultiDirectory):
48
49
  def __init__(self, name="noise-map", enabled=True, **kwargs):
49
50
  FrameMultiDirectory.__init__(self, name, **kwargs)
50
51
  JobBase.__init__(self, name, enabled)
@@ -53,9 +54,12 @@ class NoiseDetection(FrameMultiDirectory, JobBase):
53
54
  self.file_name = kwargs.get('file_name', constants.DEFAULT_NOISE_MAP_FILENAME)
54
55
  if self.file_name == '':
55
56
  self.file_name = constants.DEFAULT_NOISE_MAP_FILENAME
56
- self.channel_thresholds = kwargs.get('channel_thresholds', constants.DEFAULT_CHANNEL_THRESHOLDS)
57
+ self.channel_thresholds = kwargs.get(
58
+ 'channel_thresholds', constants.DEFAULT_CHANNEL_THRESHOLDS
59
+ )
57
60
  self.plot_range = kwargs.get('plot_range', constants.DEFAULT_NOISE_PLOT_RANGE)
58
61
  self.plot_histograms = kwargs.get('plot_histograms', False)
62
+ self.tbar = None
59
63
 
60
64
  def hot_map(self, ch, th):
61
65
  return cv2.threshold(ch, th, 255, cv2.THRESH_BINARY)[1]
@@ -63,18 +67,20 @@ class NoiseDetection(FrameMultiDirectory, JobBase):
63
67
  def progress(self, i):
64
68
  self.callback('after_step', self.id, self.name, i)
65
69
  if not config.DISABLE_TQDM:
66
- self.bar.update(1)
70
+ self.tbar.update(1)
67
71
  if self.callback('check_running', self.id, self.name) is False:
68
72
  raise RunStopException(self.name)
69
73
 
70
74
  def run_core(self):
71
- self.print_message(color_str("map noisy pixels from frames in " + self.folder_list_str(), "blue"))
75
+ self.print_message(color_str(
76
+ f"map noisy pixels from frames in {self.folder_list_str()}", "blue"
77
+ ))
72
78
  files = self.folder_filelist()
73
79
  in_paths = [self.working_path + "/" + f for f in files]
74
80
  n_frames = min(len(in_paths), self.max_frames) if self.max_frames > 0 else len(in_paths)
75
81
  self.callback('step_counts', self.id, self.name, n_frames)
76
82
  if not config.DISABLE_TQDM:
77
- self.bar = make_tqdm_bar(self.name, n_frames)
83
+ self.tbar = make_tqdm_bar(self.name, n_frames)
78
84
 
79
85
  def progress_callback(i):
80
86
  self.progress(i)
@@ -82,10 +88,12 @@ class NoiseDetection(FrameMultiDirectory, JobBase):
82
88
  raise RunStopException(self.name)
83
89
  mean_img = mean_image(
84
90
  file_paths=in_paths, max_frames=self.max_frames,
85
- message_callback=lambda path: self.print_message_r(color_str(f"reading frame: {path.split('/')[-1]}", "blue")),
91
+ message_callback=lambda path: self.print_message_r(
92
+ color_str(f"reading frame: {path.split('/')[-1]}", "blue")
93
+ ),
86
94
  progress_callback=progress_callback)
87
95
  if not config.DISABLE_TQDM:
88
- self.bar.close()
96
+ self.tbar.close()
89
97
  if mean_img is None:
90
98
  raise RuntimeError("Mean image is None")
91
99
  blurred = cv2.GaussianBlur(mean_img, (self.blur_size, self.blur_size), 0)
@@ -95,15 +103,14 @@ class NoiseDetection(FrameMultiDirectory, JobBase):
95
103
  hot_rgb = cv2.bitwise_or(hot_px[0], cv2.bitwise_or(hot_px[1], hot_px[2]))
96
104
  msg = []
97
105
  for ch, hot in zip(['rgb', *constants.RGB_LABELS], [hot_rgb] + hot_px):
98
- msg.append("{}: {}".format(ch, np.count_nonzero(hot > 0)))
106
+ msg.append(f"{ch}: {np.count_nonzero(hot > 0)}")
99
107
  self.print_message("hot pixels: " + ", ".join(msg))
100
108
  path = "/".join(self.file_name.split("/")[:-1])
101
- if not os.path.exists(self.working_path + '/' + path):
102
- self.print_message("create directory: " + path)
103
- os.mkdir(self.working_path + '/' + path)
104
-
105
- self.print_message("writing hot pixels map file: " + self.file_name)
106
- cv2.imwrite(self.working_path + '/' + self.file_name, hot)
109
+ if not os.path.exists(f"{self.working_path}/{path}"):
110
+ self.print_message(f"create directory: {path}")
111
+ os.mkdir(f"{self.working_path}/{path}")
112
+ self.print_message(f"writing hot pixels map file: {self.file_name}")
113
+ cv2.imwrite(f"{self.working_path}/{self.file_name}", hot_rgb)
107
114
  plot_range = self.plot_range
108
115
  min_th, max_th = min(self.channel_thresholds), max(self.channel_thresholds)
109
116
  if min_th < plot_range[0]:
@@ -114,7 +121,8 @@ class NoiseDetection(FrameMultiDirectory, JobBase):
114
121
  if self.plot_histograms:
115
122
  plt.figure(figsize=(10, 5))
116
123
  x = np.array(list(th_range))
117
- ys = [[np.count_nonzero(self.hot_map(ch, th) > 0) for th in th_range] for ch in channels]
124
+ ys = [[np.count_nonzero(self.hot_map(ch, th) > 0)
125
+ for th in th_range] for ch in channels]
118
126
  for i, ch, y in zip(range(3), constants.RGB_LABELS, ys):
119
127
  plt.plot(x, y, c=ch, label=ch)
120
128
  plt.plot([self.channel_thresholds[i], self.channel_thresholds[i]],
@@ -124,7 +132,7 @@ class NoiseDetection(FrameMultiDirectory, JobBase):
124
132
  plt.legend()
125
133
  plt.xlim(x[0], x[-1])
126
134
  plt.ylim(0)
127
- plot_path = self.working_path + "/" + self.plot_path + "/" + self.name + "-hot-pixels.pdf"
135
+ plot_path = f"{self.working_path}/{self.plot_path}/{self.name}-hot-pixels.pdf"
128
136
  save_plot(plot_path)
129
137
  self.callback('save_plot', self.id, f"{self.name}: noise", plot_path)
130
138
  plt.close('all')
@@ -132,13 +140,16 @@ class NoiseDetection(FrameMultiDirectory, JobBase):
132
140
 
133
141
  class MaskNoise(SubAction):
134
142
  def __init__(self, noise_mask=constants.DEFAULT_NOISE_MAP_FILENAME,
135
- kernel_size=constants.DEFAULT_MN_KERNEL_SIZE, method=constants.INTERPOLATE_MEAN, **kwargs):
143
+ kernel_size=constants.DEFAULT_MN_KERNEL_SIZE,
144
+ method=constants.INTERPOLATE_MEAN, **kwargs):
136
145
  super().__init__(**kwargs)
137
146
  self.noise_mask = noise_mask if noise_mask != '' else constants.DEFAULT_NOISE_MAP_FILENAME
138
147
  self.kernel_size = kernel_size
139
148
  self.ks2 = self.kernel_size // 2
140
149
  self.ks2_1 = self.ks2 + 1
141
150
  self.method = method
151
+ self.process = None
152
+ self.noise_mask_img = None
142
153
 
143
154
  def begin(self, process):
144
155
  self.process = process
@@ -151,10 +162,7 @@ class MaskNoise(SubAction):
151
162
  else:
152
163
  raise ImageLoadError(path, "file not found.")
153
164
 
154
- def end(self):
155
- pass
156
-
157
- def run_frame(self, idx, ref_idx, image):
165
+ def run_frame(self, _idx, _ref_idx, image):
158
166
  self.process.sub_message_r(': mask noisy pixels')
159
167
  if len(image.shape) == 3:
160
168
  corrected = image.copy()
@@ -1,32 +1,25 @@
1
+ # pylint: disable=C0114, C0115, C0116, E1101
1
2
  import numpy as np
2
3
  import cv2
3
4
  from .. config.constants import constants
4
- from .. core.colors import color_str
5
- from .. core.exceptions import RunStopException, ImageLoadError, InvalidOptionError
6
- from .utils import read_img, get_img_metadata, validate_image
5
+ from .. core.exceptions import RunStopException
6
+ from .utils import read_img
7
+ from .base_stack_algo import BaseStackAlgo
7
8
 
8
9
 
9
- class PyramidBase:
10
- def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE, kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
11
- gen_kernel=constants.DEFAULT_PY_GEN_KERNEL, float_type=constants.DEFAULT_PY_FLOAT):
10
+ class PyramidBase(BaseStackAlgo):
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
+ super().__init__("pyramid", 1, float_type)
12
16
  self.min_size = min_size
13
17
  self.kernel_size = kernel_size
14
18
  self.pad_amount = (kernel_size - 1) // 2
15
19
  self.do_step_callback = False
16
- kernel = np.array([0.25 - gen_kernel / 2.0, 0.25, gen_kernel, 0.25, 0.25 - gen_kernel / 2.0])
20
+ kernel = np.array([0.25 - gen_kernel / 2.0, 0.25,
21
+ gen_kernel, 0.25, 0.25 - gen_kernel / 2.0])
17
22
  self.gen_kernel = np.outer(kernel, kernel)
18
- if float_type == constants.FLOAT_32:
19
- self.float_type = np.float32
20
- elif float_type == constants.FLOAT_64:
21
- self.float_type = np.float64
22
- else:
23
- raise InvalidOptionError("float_type", float_type, details=" valid values are FLOAT_32 and FLOAT_64")
24
-
25
- def print_message(self, msg):
26
- self.process.sub_message_r(color_str(msg, "light_blue"))
27
-
28
- def steps_per_frame(self):
29
- return 1
30
23
 
31
24
  def convolve(self, image):
32
25
  return cv2.filter2D(image, -1, self.gen_kernel, borderType=cv2.BORDER_REFLECT101)
@@ -34,7 +27,8 @@ class PyramidBase:
34
27
  def reduce_layer(self, layer):
35
28
  if len(layer.shape) == 2:
36
29
  return self.convolve(layer)[::2, ::2]
37
- reduced_channels = [self.reduce_layer(layer[:, :, channel]) for channel in range(layer.shape[2])]
30
+ reduced_channels = [self.reduce_layer(layer[:, :, channel])
31
+ for channel in range(layer.shape[2])]
38
32
  return np.stack(reduced_channels, axis=-1)
39
33
 
40
34
  def expand_layer(self, layer):
@@ -83,7 +77,9 @@ class PyramidBase:
83
77
  return self.float_type(-1. * (levels * np.log(probabilities[levels])).sum())
84
78
 
85
79
  def get_pad(self, padded_image, row, column):
86
- return padded_image[row + self.pad_amount + self.offset[:, np.newaxis], column + self.pad_amount + self.offset]
80
+ return padded_image[row + self.pad_amount +
81
+ self.offset[:, np.newaxis], column +
82
+ self.pad_amount + self.offset]
87
83
 
88
84
  def area_deviation(self, area):
89
85
  return np.square(area - np.average(area).astype(self.float_type)).sum() / area.size
@@ -92,15 +88,17 @@ class PyramidBase:
92
88
  padded_image = cv2.copyMakeBorder(image, self.pad_amount, self.pad_amount, self.pad_amount,
93
89
  self.pad_amount, cv2.BORDER_REFLECT101)
94
90
  return np.fromfunction(
95
- np.vectorize(lambda row, column: self.area_deviation(self.get_pad(padded_image, row, column))),
91
+ np.vectorize(lambda row, column:
92
+ self.area_deviation(self.get_pad(padded_image, row, column))),
96
93
  image.shape[:2], dtype=int)
97
94
 
98
95
  def get_fused_base(self, images):
99
- layers, height, width, _ = images.shape
96
+ layers = images.shape[0]
100
97
  entropies = np.zeros(images.shape[:3], dtype=self.float_type)
101
98
  deviations = np.copy(entropies)
102
- gray_images = np.array([cv2.cvtColor(images[layer].astype(np.float32),
103
- cv2.COLOR_BGR2GRAY).astype(self.dtype) for layer in range(layers)])
99
+ gray_images = np.array([cv2.cvtColor(
100
+ images[layer].astype(np.float32),
101
+ cv2.COLOR_BGR2GRAY).astype(self.dtype) for layer in range(layers)])
104
102
  entropies = np.array([self.entropy(img) for img in gray_images])
105
103
  deviations = np.array([self.deviation(img) for img in gray_images])
106
104
  best_e = np.argmax(entropies, axis=0)
@@ -114,13 +112,15 @@ class PyramidBase:
114
112
 
115
113
 
116
114
  class PyramidStack(PyramidBase):
117
- def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE, kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
118
- gen_kernel=constants.DEFAULT_PY_GEN_KERNEL, float_type=constants.DEFAULT_PY_FLOAT):
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
119
  super().__init__(min_size, kernel_size, gen_kernel, float_type)
120
120
  self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
121
-
122
- def name(self):
123
- return "pyramid"
121
+ self.dtype = None
122
+ self.num_pixel_values = None
123
+ self.max_pixel_value = None
124
124
 
125
125
  def process_single_image(self, img, levels):
126
126
  pyramid = [img.astype(self.float_type)]
@@ -141,7 +141,7 @@ class PyramidStack(PyramidBase):
141
141
  def fuse_pyramids(self, all_laplacians):
142
142
  fused = [self.get_fused_base(np.stack([p[-1] for p in all_laplacians], axis=0))]
143
143
  for layer in range(len(all_laplacians[0]) - 2, -1, -1):
144
- self.print_message(': fusing pyramids, layer: {}'.format(layer + 1))
144
+ self.print_message(f': fusing pyramids, layer: {layer + 1}')
145
145
  laplacians = np.stack([p[layer] for p in all_laplacians], axis=0)
146
146
  fused.append(self.fuse_laplacian(laplacians))
147
147
  self.print_message(': pyramids fusion completed')
@@ -152,24 +152,24 @@ class PyramidStack(PyramidBase):
152
152
  all_laplacians = []
153
153
  levels = None
154
154
  for i, img_path in enumerate(filenames):
155
- self.print_message(': validating file {}'.format(img_path.split('/')[-1]))
156
- img = read_img(img_path)
157
- if img is None:
158
- raise ImageLoadError(img_path)
159
- if metadata is None:
160
- metadata = get_img_metadata(img)
155
+ self.print_message(f": validating file {img_path.split('/')[-1]}")
156
+
157
+ img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
158
+
159
+ if updated:
161
160
  self.dtype = metadata[1]
162
- self.num_pixel_values = constants.NUM_UINT8 if self.dtype == np.uint8 else constants.NUM_UINT16
163
- self.max_pixel_value = constants.MAX_UINT8 if self.dtype == np.uint8 else constants.MAX_UINT16
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
164
165
  levels = int(np.log2(min(img.shape[:2]) / self.min_size))
165
- else:
166
- validate_image(img, *metadata)
166
+
167
167
  if self.do_step_callback:
168
168
  self.process.callback('after_step', self.process.id, self.process.name, i)
169
169
  if self.process.callback('check_running', self.process.id, self.process.name) is False:
170
170
  raise RunStopException(self.name)
171
171
  for img_path in filenames:
172
- self.print_message(': processing file {}'.format(img_path.split('/')[-1]))
172
+ self.print_message(f": processing file {img_path.split('/')[-1]}")
173
173
  img = read_img(img_path)
174
174
  all_laplacians.append(self.process_single_image(img, levels))
175
175
  stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
@@ -1,3 +1,4 @@
1
+ # pylint: disable=C0114, C0116, E1101
1
2
  import cv2
2
3
  import numpy as np
3
4
 
@@ -1,7 +1,7 @@
1
- import numpy as np
1
+ # pylint: disable=C0114, C0115, C0116, R0913, R0917
2
2
  import os
3
+ import numpy as np
3
4
  from .. config.constants import constants
4
- from .. core.colors import color_str
5
5
  from .. core.framework import JobBase
6
6
  from .. core.exceptions import InvalidOptionError
7
7
  from .utils import write_img
@@ -10,15 +10,15 @@ from .exif import copy_exif_from_file_to_file
10
10
  from .denoise import denoise
11
11
 
12
12
 
13
- class FocusStackBase:
14
- def __init__(self, stack_algo, exif_path='',
15
- prefix=constants.DEFAULT_STACK_PREFIX,
16
- denoise=0, plot_stack=False):
13
+ class FocusStackBase(JobBase, FrameDirectory):
14
+ def __init__(self, name, stack_algo, enabled=True, **kwargs):
15
+ FrameDirectory.__init__(self, name, **kwargs)
16
+ JobBase.__init__(self, name, enabled)
17
17
  self.stack_algo = stack_algo
18
- self.exif_path = exif_path
19
- self.prefix = prefix if prefix != '' else constants.DEFAULT_STACK_PREFIX
20
- self.denoise = denoise
21
- self.plot_stack = plot_stack
18
+ self.exif_path = kwargs.pop('exif_path', '')
19
+ self.prefix = kwargs.pop('prefix', constants.DEFAULT_STACK_PREFIX)
20
+ self.denoise_amount = kwargs.pop('denoise_amount', 0)
21
+ self.plot_stack = kwargs.pop('plot_stack', constants.DEFAULT_PLOT_STACK)
22
22
  self.stack_algo.process = self
23
23
  self.frame_count = -1
24
24
 
@@ -27,20 +27,22 @@ class FocusStackBase:
27
27
  img_files = sorted([os.path.join(self.input_full_path, name) for name in filenames])
28
28
  stacked_img = self.stack_algo.focus_stack(img_files)
29
29
  in_filename = filenames[0].split(".")
30
- out_filename = f"{self.output_dir}/{self.prefix}{in_filename[0]}." + '.'.join(in_filename[1:])
31
- if self.denoise > 0:
30
+ out_filename = f"{self.output_dir}/{self.prefix}{in_filename[0]}." + \
31
+ '.'.join(in_filename[1:])
32
+ if self.denoise_amount > 0:
32
33
  self.sub_message_r(': denoise image')
33
- stacked_img = denoise(stacked_img, self.denoise, self.denoise)
34
+ stacked_img = denoise(stacked_img, self.denoise_amount, self.denoise_amount)
34
35
  write_img(out_filename, stacked_img)
35
36
  if self.exif_path != '' and stacked_img.dtype == np.uint8:
36
37
  self.sub_message_r(': copy exif data')
37
- dirpath, _, fnames = next(os.walk(self.exif_path))
38
- fnames = [name for name in fnames if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
38
+ _dirpath, _, fnames = next(os.walk(self.exif_path))
39
+ fnames = [name for name in fnames
40
+ if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
39
41
  exif_filename = f"{self.exif_path}/{fnames[0]}"
40
42
  copy_exif_from_file_to_file(exif_filename, out_filename)
41
43
  self.sub_message_r(' ' * 60)
42
44
  if self.plot_stack:
43
- idx_str = "{:04d}".format(self.frame_count) if self.frame_count >= 0 else ''
45
+ idx_str = f"{self.frame_count:04d}" if self.frame_count >= 0 else ''
44
46
  name = f"{self.name}: {self.stack_algo.name()}"
45
47
  if idx_str != '':
46
48
  name += f"\nbunch: {idx_str}"
@@ -48,65 +50,63 @@ class FocusStackBase:
48
50
  if self.frame_count >= 0:
49
51
  self.frame_count += 1
50
52
 
51
- def init(self, job, working_path):
53
+ def init(self, job, working_path=''):
52
54
  if self.exif_path is None:
53
55
  self.exif_path = job.paths[0]
54
56
  if self.exif_path != '':
55
57
  self.exif_path = working_path + "/" + self.exif_path
56
58
 
57
59
 
58
- class FocusStackBunch(FocusStackBase, FrameDirectory, ActionList):
60
+ def get_bunches(collection, n_frames, n_overlap):
61
+ bunches = [collection[x:x + n_frames]
62
+ for x in range(0, len(collection) - n_overlap, n_frames - n_overlap)]
63
+ return bunches
64
+
65
+
66
+ class FocusStackBunch(ActionList, FocusStackBase):
59
67
  def __init__(self, name, stack_algo, enabled=True, **kwargs):
60
- FocusStackBase.__init__(self, stack_algo,
61
- exif_path=kwargs.pop('exif_path', ''),
62
- prefix=kwargs.pop('prefix', constants.DEFAULT_STACK_PREFIX),
63
- denoise=kwargs.pop('denoise', 0),
64
- plot_stack=kwargs.pop('plot_stack', constants.DEFAULT_PLOT_STACK_BUNCH))
65
- FrameDirectory.__init__(self, name, **kwargs)
66
68
  ActionList.__init__(self, name, enabled)
69
+ FocusStackBase.__init__(self, name, stack_algo, enabled, **kwargs)
70
+ self._chunks = None
67
71
  self.frame_count = 0
68
72
  self.frames = kwargs.get('frames', constants.DEFAULT_FRAMES)
69
73
  self.overlap = kwargs.get('overlap', constants.DEFAULT_OVERLAP)
70
- self.denoise = kwargs.get('denoise', 0)
74
+ self.denoise_amount = kwargs.get('denoise_amount', 0)
71
75
  self.stack_algo.do_step_callback = False
72
76
  if self.overlap >= self.frames:
73
- raise InvalidOptionError("overlap", self.overlap, "overlap must be smaller than batch size")
77
+ raise InvalidOptionError("overlap", self.overlap,
78
+ "overlap must be smaller than batch size")
74
79
 
75
- def init(self, job):
80
+ def init(self, job, _working_path=''):
76
81
  FrameDirectory.init(self, job)
77
82
  FocusStackBase.init(self, job, self.working_path)
78
83
 
79
84
  def begin(self):
80
85
  ActionList.begin(self)
81
- fnames = self.folder_filelist(self.input_full_path)
82
- self.__chunks = [fnames[x:x + self.frames] for x in range(0, len(fnames) - self.overlap, self.frames - self.overlap)]
83
- self.set_counts(len(self.__chunks))
86
+ fnames = self.folder_filelist()
87
+ self._chunks = get_bunches(fnames, self.frames, self.overlap)
88
+ self.set_counts(len(self._chunks))
84
89
 
85
90
  def end(self):
86
91
  ActionList.end(self)
87
92
 
88
93
  def run_step(self):
89
- self.print_message_r(color_str("fusing bunch: {}".format(self.count), "blue"))
90
- self.focus_stack(self.__chunks[self.count - 1])
94
+ self.print_message_r(f"fusing bunch: {self.count}")
95
+ self.focus_stack(self._chunks[self.count - 1])
91
96
  self.callback('after_step', self.id, self.name, self.count)
92
97
 
93
98
 
94
- class FocusStack(FocusStackBase, FrameDirectory, JobBase):
99
+ class FocusStack(FocusStackBase):
95
100
  def __init__(self, name, stack_algo, enabled=True, **kwargs):
96
- FocusStackBase.__init__(self, stack_algo,
97
- exif_path=kwargs.pop('exif_path', ''),
98
- prefix=kwargs.pop('prefix', constants.DEFAULT_STACK_PREFIX),
99
- denoise=kwargs.pop('denoise', 0),
100
- plot_stack=kwargs.pop('plot_stack', constants.DEFAULT_PLOT_STACK))
101
- FrameDirectory.__init__(self, name, **kwargs)
102
- JobBase.__init__(self, name, enabled)
101
+ super().__init__(name, stack_algo, enabled, **kwargs)
103
102
  self.stack_algo.do_step_callback = True
104
103
 
105
104
  def run_core(self):
106
105
  self.set_filelist()
107
- self.callback('step_counts', self.id, self.name, self.stack_algo.steps_per_frame() * len(self.filenames))
106
+ self.callback('step_counts', self.id, self.name,
107
+ self.stack_algo.steps_per_frame() * len(self.filenames))
108
108
  self.focus_stack(self.filenames)
109
109
 
110
- def init(self, job):
110
+ def init(self, job, _working_path=''):
111
111
  FrameDirectory.init(self, job)
112
112
  FocusStackBase.init(self, job, self.working_path)