shinestacker 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of shinestacker might be problematic. Click here for more details.

Files changed (43) hide show
  1. shinestacker/_version.py +1 -1
  2. shinestacker/algorithms/align.py +148 -115
  3. shinestacker/algorithms/align_auto.py +64 -0
  4. shinestacker/algorithms/align_parallel.py +296 -0
  5. shinestacker/algorithms/balance.py +14 -13
  6. shinestacker/algorithms/base_stack_algo.py +11 -2
  7. shinestacker/algorithms/multilayer.py +14 -15
  8. shinestacker/algorithms/noise_detection.py +13 -14
  9. shinestacker/algorithms/pyramid.py +4 -4
  10. shinestacker/algorithms/pyramid_auto.py +16 -10
  11. shinestacker/algorithms/pyramid_tiles.py +19 -11
  12. shinestacker/algorithms/stack.py +30 -26
  13. shinestacker/algorithms/stack_framework.py +200 -178
  14. shinestacker/algorithms/vignetting.py +16 -13
  15. shinestacker/app/main.py +7 -3
  16. shinestacker/config/constants.py +63 -26
  17. shinestacker/config/gui_constants.py +1 -1
  18. shinestacker/core/core_utils.py +4 -0
  19. shinestacker/core/framework.py +114 -33
  20. shinestacker/gui/action_config.py +57 -5
  21. shinestacker/gui/action_config_dialog.py +156 -17
  22. shinestacker/gui/base_form_dialog.py +2 -2
  23. shinestacker/gui/folder_file_selection.py +101 -0
  24. shinestacker/gui/gui_images.py +10 -10
  25. shinestacker/gui/gui_run.py +13 -11
  26. shinestacker/gui/main_window.py +10 -5
  27. shinestacker/gui/menu_manager.py +4 -0
  28. shinestacker/gui/new_project.py +171 -74
  29. shinestacker/gui/project_controller.py +13 -9
  30. shinestacker/gui/project_converter.py +4 -2
  31. shinestacker/gui/project_editor.py +72 -53
  32. shinestacker/gui/select_path_widget.py +1 -1
  33. shinestacker/gui/sys_mon.py +96 -0
  34. shinestacker/gui/tab_widget.py +3 -3
  35. shinestacker/gui/time_progress_bar.py +4 -3
  36. shinestacker/retouch/exif_data.py +1 -1
  37. shinestacker/retouch/image_editor_ui.py +2 -0
  38. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/METADATA +6 -6
  39. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/RECORD +43 -39
  40. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/WHEEL +0 -0
  41. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/entry_points.txt +0 -0
  42. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/licenses/LICENSE +0 -0
  43. {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,296 @@
1
+ # pylint: disable=C0114, C0115, C0116, W0718, R0912, R0915, E1101, R0914, R0911, E0606, R0801, R0902
2
+ import gc
3
+ import copy
4
+ import math
5
+ import traceback
6
+ import threading
7
+ import logging
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ import numpy as np
10
+ import cv2
11
+ from ..config.constants import constants
12
+ from .. core.exceptions import InvalidOptionError, RunStopException
13
+ from .. core.colors import color_str
14
+ from .. core.core_utils import make_chunks
15
+ from .utils import read_img, img_subsample, img_bw
16
+ from .align import (AlignFramesBase, detect_and_compute_matches, find_transform,
17
+ check_transform, _cv2_border_mode_map, rescale_trasnsform)
18
+
19
+
20
+ def compose_transforms(t1, t2, transform_type):
21
+ t1 = t1.astype(np.float64)
22
+ t2 = t2.astype(np.float64)
23
+ if transform_type == constants.ALIGN_RIGID:
24
+ t1_homo = np.vstack([t1, [0, 0, 1]])
25
+ t2_homo = np.vstack([t2, [0, 0, 1]])
26
+ result_homo = t2_homo @ t1_homo
27
+ return result_homo[:2, :]
28
+ return t2 @ t1
29
+
30
+
31
+ class AlignFramesParallel(AlignFramesBase):
32
+ def __init__(self, enabled=True, feature_config=None, matching_config=None,
33
+ alignment_config=None, **kwargs):
34
+ super().__init__(enabled=True, feature_config=None, matching_config=None,
35
+ alignment_config=None, **kwargs)
36
+ self.max_threads = kwargs.get('max_threads', constants.DEFAULT_ALIGN_MAX_THREADS)
37
+ self.chunk_submit = kwargs.get('chunk_submit', constants.DEFAULT_ALIGN_CHUNK_SUBMIT)
38
+ self.bw_matching = kwargs.get('bw_matching', constants.DEFAULT_ALIGN_BW_MATCHING)
39
+ self._img_cache = None
40
+ self._img_locks = None
41
+ self._cache_locks = None
42
+ self._target_indices = None
43
+ self._transforms = None
44
+ self._cumulative_transforms = None
45
+ self.step_counter = 0
46
+
47
+ def cache_img(self, idx):
48
+ with self._cache_locks[idx]:
49
+ self._img_locks[idx] += 1
50
+ if self._img_cache[idx] is None:
51
+ img = read_img(self.process.input_filepath(idx))
52
+ if self.bw_matching:
53
+ img = img_bw(img)
54
+ self._img_cache[idx] = img
55
+ return self._img_cache[idx]
56
+
57
+ def submit_threads(self, idxs, imgs):
58
+ with ThreadPoolExecutor(max_workers=len(imgs)) as executor:
59
+ future_to_index = {}
60
+ for idx in idxs:
61
+ self.print_message(
62
+ f"submit alignment matches, {self.image_str(idx)}")
63
+ future = executor.submit(self.extract_features, idx)
64
+ future_to_index[future] = idx
65
+ for future in as_completed(future_to_index):
66
+ idx = future_to_index[future]
67
+ try:
68
+ info_messages, warning_messages = future.result()
69
+ message = f"{self.image_str(idx)}: " \
70
+ f"matches found: {self._n_good_matches[idx]}"
71
+ if len(info_messages) > 0:
72
+ message += ", " + ", ".join(info_messages)
73
+ color = constants.LOG_COLOR_LEVEL_3
74
+ level = logging.INFO
75
+ if len(warning_messages) > 0:
76
+ message += ", " + color_str(", ".join(warning_messages), 'yellow')
77
+ color = constants.LOG_COLOR_WARNING
78
+ level = logging.WARNING
79
+ self.print_message(message, color=color, level=level)
80
+ self.step_counter += 1
81
+ self.process.after_step(self.step_counter)
82
+ self.process.check_running()
83
+ except RunStopException as e:
84
+ raise e
85
+ except Exception as e:
86
+ traceback.print_tb(e.__traceback__)
87
+ self.print_message(
88
+ f"failed processing {self.image_str(idx)}: {str(e)}")
89
+ cached_images = 0
90
+ for i in range(self.process.num_input_filepaths()):
91
+ if self._img_locks[i] >= 2:
92
+ self._img_cache[i] = None
93
+ self._img_locks[i] = 0
94
+ elif self._img_cache[i] is not None:
95
+ cached_images += 1
96
+ # self.print_message(f"cached images: {cached_images}")
97
+ gc.collect()
98
+
99
+ def begin(self, process):
100
+ super().begin(process)
101
+ n_frames = self.process.num_input_filepaths()
102
+ self.process.callback(constants.CALLBACK_STEP_COUNTS,
103
+ self.process.id, self.process.name, 2 * n_frames)
104
+ self.print_message(f"preprocess {n_frames} images in parallel, cores: {self.max_threads}")
105
+ input_filepaths = self.process.input_filepaths()
106
+ self._img_cache = [None] * n_frames
107
+ self._img_locks = [0] * n_frames
108
+ self._cache_locks = [threading.Lock() for _ in range(n_frames)]
109
+ self._target_indices = [None] * n_frames
110
+ self._n_good_matches = [0] * n_frames
111
+ self._transforms = [None] * n_frames
112
+ self._cumulative_transforms = [None] * n_frames
113
+ max_chunck_size = self.max_threads
114
+ ref_idx = self.process.ref_idx
115
+ self.print_message(f"reference: {self.image_str(ref_idx)}")
116
+ sub_indices = list(range(n_frames))
117
+ sub_indices.remove(ref_idx)
118
+ sub_img_filepaths = copy.deepcopy(input_filepaths)
119
+ sub_img_filepaths.remove(input_filepaths[ref_idx])
120
+ self.step_counter = 0
121
+ if self.chunk_submit:
122
+ img_chunks = make_chunks(sub_img_filepaths, max_chunck_size)
123
+ idx_chunks = make_chunks(sub_indices, max_chunck_size)
124
+ for idxs, imgs in zip(idx_chunks, img_chunks):
125
+ self.submit_threads(idxs, imgs)
126
+ else:
127
+ self.submit_threads(sub_indices, sub_img_filepaths)
128
+ for i in range(n_frames):
129
+ if self._img_cache[i] is not None:
130
+ self._img_cache[i] = None
131
+ gc.collect()
132
+ self.print_message("combining transformations")
133
+ transform_type = self.alignment_config['transform']
134
+ if transform_type == constants.ALIGN_RIGID:
135
+ identity = np.array([[1.0, 0.0, 0.0],
136
+ [0.0, 1.0, 0.0]], dtype=np.float64)
137
+ else:
138
+ identity = np.eye(3, dtype=np.float64)
139
+ self._cumulative_transforms[ref_idx] = identity
140
+ frames_to_process = []
141
+ for i in range(n_frames):
142
+ if i != ref_idx:
143
+ frames_to_process.append((i, abs(i - ref_idx)))
144
+ frames_to_process.sort(key=lambda x: x[1])
145
+ for i, _ in frames_to_process:
146
+ target_idx = self._target_indices[i]
147
+ if target_idx is not None and self._cumulative_transforms[target_idx] is not None:
148
+ self._cumulative_transforms[i] = compose_transforms(
149
+ self._transforms[i], self._cumulative_transforms[target_idx], transform_type)
150
+ else:
151
+ self._cumulative_transforms[i] = None
152
+ self.print_message(
153
+ f"warning: no cumulative transform for {self.image_str(i)}",
154
+ color=constants.LOG_COLOR_WARNING, level=logging.WARNING)
155
+ missing_transforms = 0
156
+ for i in range(n_frames):
157
+ if self._cumulative_transforms[i] is not None:
158
+ self._cumulative_transforms[i] = self._cumulative_transforms[i].astype(np.float32)
159
+ else:
160
+ missing_transforms += 1
161
+ msg = "feature extaction completed"
162
+ if missing_transforms > 0:
163
+ msg += ", " + color_str(f"images not matched: {missing_transforms}",
164
+ constants.LOG_COLOR_WARNING)
165
+ self.print_message(msg)
166
+ self.process.add_begin_steps(n_frames)
167
+
168
+ def extract_features(self, idx, delta=1):
169
+ ref_idx = self.process.ref_idx
170
+ pass_ref_err_msg = "cannot find path to reference frame"
171
+ if idx < ref_idx:
172
+ target_idx = idx + delta
173
+ if target_idx > ref_idx:
174
+ self._target_indices[idx] = None
175
+ self._transforms[idx] = None
176
+ return [], [pass_ref_err_msg]
177
+ elif idx > ref_idx:
178
+ target_idx = idx - delta
179
+ if target_idx < ref_idx:
180
+ self._target_indices[idx] = None
181
+ self._transforms[idx] = None
182
+ return [], [pass_ref_err_msg]
183
+ else:
184
+ self._target_indices[idx] = None
185
+ self._transforms[idx] = None
186
+ return [], []
187
+ info_messages = []
188
+ warning_messages = []
189
+ img_0 = self.cache_img(idx)
190
+ img_ref = self.cache_img(target_idx)
191
+ h0, w0 = img_0.shape[:2]
192
+ subsample = self.alignment_config['subsample']
193
+ if subsample == 0:
194
+ img_res = (float(h0) / constants.ONE_KILO) * (float(w0) / constants.ONE_KILO)
195
+ target_res = constants.DEFAULT_ALIGN_RES_TARGET_MPX
196
+ subsample = int(1 + math.floor(img_res / target_res))
197
+ fast_subsampling = self.alignment_config['fast_subsampling']
198
+ min_good_matches = self.alignment_config['min_good_matches']
199
+ while True:
200
+ if subsample > 1:
201
+ img_0_sub = img_subsample(img_0, subsample, fast_subsampling)
202
+ img_ref_sub = img_subsample(img_ref, subsample, fast_subsampling)
203
+ else:
204
+ img_0_sub, img_ref_sub = img_0, img_ref
205
+ kp_0, kp_ref, good_matches = detect_and_compute_matches(
206
+ img_ref_sub, img_0_sub, self.feature_config, self.matching_config)
207
+ n_good_matches = len(good_matches)
208
+ if n_good_matches > min_good_matches or subsample == 1:
209
+ break
210
+ subsample = 1
211
+ warning_messages.append("too few matches, no subsampling applied")
212
+ self._n_good_matches[idx] = n_good_matches
213
+ m = None
214
+ min_matches = 4 if self.alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY else 3
215
+ if n_good_matches < min_matches:
216
+ self.print_message(
217
+ f"warning: only {n_good_matches} found for "
218
+ f"{self.image_str(idx)}, trying next frame",
219
+ color=constants.LOG_COLOR_WARNING, level=logging.WARNING)
220
+ self._target_indices[idx] = None
221
+ self._transforms[idx] = None
222
+ return self.extract_features(idx, delta + 1)
223
+ transform = self.alignment_config['transform']
224
+ src_pts = np.float32([kp_0[m.queryIdx].pt for m in good_matches]).reshape(-1, 1, 2)
225
+ dst_pts = np.float32([kp_ref[m.trainIdx].pt for m in good_matches]).reshape(-1, 1, 2)
226
+ m, _msk = find_transform(src_pts, dst_pts, transform, self.alignment_config['align_method'],
227
+ *(self.alignment_config[k]
228
+ for k in ['rans_threshold', 'max_iters',
229
+ 'align_confidence', 'refine_iters']))
230
+ h_sub, w_sub = img_0_sub.shape[:2]
231
+ if subsample > 1:
232
+ m = rescale_trasnsform(m, w0, h0, w_sub, h_sub, subsample, transform)
233
+ if m is None:
234
+ warning_messages.append(f"invalid option {transform}")
235
+ self._target_indices[idx] = None
236
+ self._transforms[idx] = None
237
+ return info_messages, warning_messages
238
+ transform_type = self.alignment_config['transform']
239
+ thresholds = self.get_transform_thresholds()
240
+ is_valid, reason = check_transform(m, img_0, transform_type, *thresholds)
241
+ if not is_valid:
242
+ self.print_message(
243
+ f"warning: invalid transformation for {self.image_str(idx)}: {reason}",
244
+ level=logging.WARNING)
245
+ if self.alignment_config['abort_abnormal']:
246
+ raise RuntimeError("invalid transformation: {reason}")
247
+ warning_messages.append(f"invalid transformation found: {reason}")
248
+ self._target_indices[idx] = None
249
+ self._transforms[idx] = None
250
+ return info_messages, warning_messages
251
+ self._transforms[idx] = m
252
+ self._target_indices[idx] = target_idx
253
+ return info_messages, warning_messages
254
+
255
+ def align_images(self, idx, img_ref, img_0):
256
+ m = self._cumulative_transforms[idx]
257
+ if m is None:
258
+ self.print_message(
259
+ f"no transformation for {self.image_str(idx)}, skipping alignment",
260
+ color=constants.LOG_COLOR_WARNING, level=logging.WARNING)
261
+ return img_0
262
+ transform_type = self.alignment_config['transform']
263
+ if transform_type == constants.ALIGN_RIGID and m.shape != (2, 3):
264
+ self.print_message(f"invalid matrix shape for rigid transform: {m.shape}")
265
+ return img_0
266
+ if transform_type == constants.ALIGN_HOMOGRAPHY and m.shape != (3, 3):
267
+ self.print_message(f"invalid matrix shape for homography: {m.shape}")
268
+ return img_0
269
+ self.print_message(f'{self.image_str(idx)}: apply image alignment')
270
+ try:
271
+ cv2_border_mode = _cv2_border_mode_map[self.alignment_config['border_mode']]
272
+ except KeyError as e:
273
+ raise InvalidOptionError("border_mode", self.alignment_config['border_mode']) from e
274
+ img_mask = np.ones_like(img_0, dtype=np.uint8)
275
+ h_ref, w_ref = img_ref.shape[:2]
276
+ if self.alignment_config['transform'] == constants.ALIGN_HOMOGRAPHY:
277
+ img_warp = cv2.warpPerspective(
278
+ img_0, m, (w_ref, h_ref),
279
+ borderMode=cv2_border_mode, borderValue=self.alignment_config['border_value'])
280
+ if self.alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
281
+ mask = cv2.warpPerspective(img_mask, m, (w_ref, h_ref),
282
+ borderMode=cv2.BORDER_CONSTANT, borderValue=0)
283
+ elif self.alignment_config['transform'] == constants.ALIGN_RIGID:
284
+ img_warp = cv2.warpAffine(
285
+ img_0, m, (w_ref, h_ref),
286
+ borderMode=cv2_border_mode, borderValue=self.alignment_config['border_value'])
287
+ if self.alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
288
+ mask = cv2.warpAffine(img_mask, m, (w_ref, h_ref),
289
+ borderMode=cv2.BORDER_CONSTANT, borderValue=0)
290
+ if self.alignment_config['border_mode'] == constants.BORDER_REPLICATE_BLUR:
291
+ self.print_message(f'{self.image_str(idx)}: blur borders')
292
+ mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
293
+ blurred_warp = cv2.GaussianBlur(
294
+ img_warp, (21, 21), sigmaX=self.alignment_config['border_blur'])
295
+ img_warp[mask == 0] = blurred_warp[mask == 0]
296
+ return img_warp
@@ -24,7 +24,6 @@ class BaseHistogrammer:
24
24
  self.plot_summary = plot_summary
25
25
  self.process = process
26
26
  self.corrections = None
27
- self.figsize = (10, 5)
28
27
 
29
28
  def begin(self, size):
30
29
  self.corrections = np.ones((size, self.channels))
@@ -70,7 +69,7 @@ class LumiHistogrammer(BaseHistogrammer):
70
69
  self.colors = ("r", "g", "b")
71
70
 
72
71
  def generate_frame_plot(self, idx, hist, chans, calc_hist_func):
73
- _fig, axs = plt.subplots(1, 2, figsize=self.figsize, sharey=True)
72
+ _fig, axs = plt.subplots(1, 2, figsize=constants.PLT_FIG_SIZE, sharey=True)
74
73
  self.histo_plot(axs[0], hist, "pixel luminosity", 'black')
75
74
  for (chan, color) in zip(chans, self.colors):
76
75
  hist_col = calc_hist_func(chan)
@@ -79,7 +78,7 @@ class LumiHistogrammer(BaseHistogrammer):
79
78
  self.save_plot(idx)
80
79
 
81
80
  def generate_summary_plot(self, ref_idx):
82
- plt.figure(figsize=self.figsize)
81
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
83
82
  x = np.arange(0, len(self.corrections), dtype=int)
84
83
  y = self.corrections
85
84
  plt.plot([ref_idx, ref_idx], [0, np.max(y)], color='cornflowerblue',
@@ -101,14 +100,14 @@ class RGBHistogrammer(BaseHistogrammer):
101
100
  self.colors = ("r", "g", "b")
102
101
 
103
102
  def generate_frame_plot(self, idx, hists):
104
- _fig, axs = plt.subplots(1, 3, figsize=self.figsize, sharey=True)
103
+ _fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
105
104
  for c in [2, 1, 0]:
106
105
  self.histo_plot(axs[c], hists[c], self.colors[c] + " luminosity", self.colors[c])
107
106
  plt.xlim(0, self.max_pixel_value)
108
107
  self.save_plot(idx)
109
108
 
110
109
  def generate_summary_plot(self, ref_idx):
111
- plt.figure(figsize=self.figsize)
110
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
112
111
  x = np.arange(0, len(self.corrections), dtype=int)
113
112
  y = self.corrections
114
113
  max_val = np.max(y) if np.any(y) else 1.0
@@ -134,14 +133,14 @@ class Ch1Histogrammer(BaseHistogrammer):
134
133
  self.colors = colors
135
134
 
136
135
  def generate_frame_plot(self, idx, hists):
137
- _fig, axs = plt.subplots(1, 3, figsize=self.figsize, sharey=True)
136
+ _fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
138
137
  for c in range(3):
139
138
  self.histo_plot(axs[c], hists[c], self.labels[c], self.colors[c])
140
139
  plt.xlim(0, self.max_pixel_value)
141
140
  self.save_plot(idx)
142
141
 
143
142
  def generate_summary_plot(self, ref_idx):
144
- plt.figure(figsize=self.figsize)
143
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
145
144
  x = np.arange(0, len(self.corrections), dtype=int)
146
145
  y = self.corrections
147
146
  max_val = np.max(y) if np.any(y) else 1.0
@@ -165,14 +164,14 @@ class Ch2Histogrammer(BaseHistogrammer):
165
164
  self.colors = colors
166
165
 
167
166
  def generate_frame_plot(self, idx, hists):
168
- _fig, axs = plt.subplots(1, 3, figsize=self.figsize, sharey=True)
167
+ _fig, axs = plt.subplots(1, 3, figsize=constants.PLT_FIG_SIZE, sharey=True)
169
168
  for c in range(3):
170
169
  self.histo_plot(axs[c], hists[c], self.labels[c], self.colors[c])
171
170
  plt.xlim(0, self.max_pixel_value)
172
171
  self.save_plot(idx)
173
172
 
174
173
  def generate_summary_plot(self, ref_idx):
175
- plt.figure(figsize=self.figsize)
174
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
176
175
  x = np.arange(0, len(self.corrections), dtype=int)
177
176
  y = self.corrections
178
177
  max_val = np.max(y) if np.any(y) else 1.0
@@ -591,9 +590,9 @@ class BalanceFrames(SubAction):
591
590
  def begin(self, process):
592
591
  self.process = process
593
592
  self.correction.process = process
594
- img = read_img(self.process.input_full_path + "/" + self.process.filenames[process.ref_idx])
593
+ img = read_img(self.process.input_filepath(process.ref_idx))
595
594
  self.shape = img.shape
596
- self.correction.begin(img, self.process.counts, process.ref_idx)
595
+ self.correction.begin(img, self.process.total_action_counts, process.ref_idx)
597
596
 
598
597
  def end(self):
599
598
  self.process.print_message(' ' * 60)
@@ -603,13 +602,15 @@ class BalanceFrames(SubAction):
603
602
  img = np.zeros(shape)
604
603
  mask_radius = int(min(*shape) * self.mask_size / 2)
605
604
  cv2.circle(img, (shape[1] // 2, shape[0] // 2), mask_radius, 255, -1)
606
- plt.figure(figsize=(10, 5))
605
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
607
606
  plt.title('Mask')
608
607
  plt.imshow(img, 'gray')
609
608
  self.correction.histogrammer.save_summary_plot("mask")
610
609
 
611
610
  def run_frame(self, idx, _ref_idx, image):
612
611
  if idx != self.process.ref_idx:
613
- self.process.sub_message_r(color_str(': balance image', constants.LOG_COLOR_LEVEL_3))
612
+ self.process.print_message(
613
+ color_str(f'{self.process.idx_tot_str(idx)}: balance image',
614
+ constants.LOG_COLOR_LEVEL_3))
614
615
  image = self.correction.apply_correction(idx, image)
615
616
  return image
@@ -34,6 +34,13 @@ class BaseStackAlgo:
34
34
  def set_do_step_callback(self, enable):
35
35
  self.do_step_callback = enable
36
36
 
37
+ def idx_tot_str(self, idx):
38
+ return f"{idx + 1}/{len(self.filenames)}"
39
+
40
+ def image_str(self, idx):
41
+ return f"image: {self.idx_tot_str(idx)}, " \
42
+ f"{os.path.basename(self.filenames[idx])}"
43
+
37
44
  def init(self, filenames):
38
45
  self.filenames = filenames
39
46
  first_img_file = ''
@@ -61,11 +68,13 @@ class BaseStackAlgo:
61
68
  return img, metadata, updated
62
69
 
63
70
  def check_running(self, cleanup_callback=None):
64
- if self.process.callback('check_running', self.process.id, self.process.name) is False:
71
+ if self.process.callback(constants.CALLBACK_CHECK_RUNNING,
72
+ self.process.id, self.process.name) is False:
65
73
  if cleanup_callback is not None:
66
74
  cleanup_callback()
67
75
  raise RunStopException(self.name)
68
76
 
69
77
  def after_step(self, step):
70
78
  if self.do_step_callback:
71
- self.process.callback('after_step', self.process.id, self.process.name, step)
79
+ self.process.callback(constants.CALLBACK_AFTER_STEP,
80
+ self.process.id, self.process.name, step)
@@ -12,9 +12,9 @@ from psdtags import (PsdBlendMode, PsdChannel, PsdChannelId, PsdClippingType, Ps
12
12
  from .. config.constants import constants
13
13
  from .. config.config import config
14
14
  from .. core.colors import color_str
15
- from .. core.framework import JobBase
15
+ from .. core.framework import TaskBase
16
16
  from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG
17
- from .stack_framework import FrameMultiDirectory
17
+ from .stack_framework import ImageSequenceManager
18
18
  from .exif import exif_extra_tags_for_tif, get_exif
19
19
 
20
20
 
@@ -159,10 +159,10 @@ def write_multilayer_tiff_from_images(image_dict, output_file, exif_path='', cal
159
159
  compression=compression, metadata=None, **tiff_tags)
160
160
 
161
161
 
162
- class MultiLayer(JobBase, FrameMultiDirectory):
162
+ class MultiLayer(TaskBase, ImageSequenceManager):
163
163
  def __init__(self, name, enabled=True, **kwargs):
164
- FrameMultiDirectory.__init__(self, name, **kwargs)
165
- JobBase.__init__(self, name, enabled)
164
+ ImageSequenceManager.__init__(self, name, **kwargs)
165
+ TaskBase.__init__(self, name, enabled)
166
166
  self.exif_path = kwargs.get('exif_path', '')
167
167
  self.reverse_order = kwargs.get(
168
168
  'reverse_order',
@@ -170,16 +170,16 @@ class MultiLayer(JobBase, FrameMultiDirectory):
170
170
  )
171
171
 
172
172
  def init(self, job):
173
- FrameMultiDirectory.init(self, job)
173
+ ImageSequenceManager.init(self, job)
174
174
  if self.exif_path == '':
175
- self.exif_path = job.paths[0]
175
+ self.exif_path = job.action_path(0)
176
176
  if self.exif_path != '':
177
177
  self.exif_path = self.working_path + "/" + self.exif_path
178
178
 
179
179
  def run_core(self):
180
- if isinstance(self.input_full_path, str):
180
+ if isinstance(self.input_full_path(), str):
181
181
  paths = [self.input_path]
182
- elif hasattr(self.input_full_path, "__len__"):
182
+ elif hasattr(self.input_full_path(), "__len__"):
183
183
  paths = self.input_path
184
184
  else:
185
185
  raise RuntimeError("input_path option must contain a path or an array of paths")
@@ -188,8 +188,8 @@ class MultiLayer(JobBase, FrameMultiDirectory):
188
188
  constants.LOG_COLOR_ALERT),
189
189
  level=logging.WARNING)
190
190
  return
191
- files = self.folder_filelist()
192
- if len(files) == 0:
191
+ input_files = self.input_filepaths()
192
+ if len(input_files) == 0:
193
193
  self.print_message(
194
194
  color_str(f"no input in {len(paths)} specified path" +
195
195
  ('s' if len(paths) > 1 else '') + ": "
@@ -199,12 +199,11 @@ class MultiLayer(JobBase, FrameMultiDirectory):
199
199
  return
200
200
  self.print_message(color_str("merging frames in " + self.folder_list_str(),
201
201
  constants.LOG_COLOR_LEVEL_2))
202
- input_files = [f"{self.working_path}/{f}" for f in files]
203
202
  self.print_message(
204
- color_str("frames: " + ", ".join([i.split("/")[-1] for i in files]),
203
+ color_str("frames: " + ", ".join([os.path.basename(i) for i in input_files]),
205
204
  constants.LOG_COLOR_LEVEL_2))
206
205
  self.print_message(color_str("reading files", constants.LOG_COLOR_LEVEL_2))
207
- filename = ".".join(files[0].split("/")[-1].split(".")[:-1])
206
+ filename = ".".join(os.path.basename(input_files[0]).split(".")[:-1])
208
207
  output_file = f"{self.working_path}/{self.output_path}/{filename}.tif"
209
208
  callbacks = {
210
209
  'exif_msg': lambda path: self.print_message(
@@ -218,4 +217,4 @@ class MultiLayer(JobBase, FrameMultiDirectory):
218
217
  write_multilayer_tiff(input_files, output_file, labels=None, exif_path=self.exif_path,
219
218
  callbacks=callbacks)
220
219
  app = 'internal_retouch_app' if config.COMBINED_APP else f'{constants.RETOUCH_APP}'
221
- self.callback('open_app', self.id, self.name, app, output_file)
220
+ self.callback(constants.CALLBACK_OPEN_APP, self.id, self.name, app, output_file)
@@ -9,10 +9,10 @@ from .. config.config import config
9
9
  from .. config.constants import constants
10
10
  from .. core.colors import color_str
11
11
  from .. core.exceptions import ImageLoadError
12
- from .. core.framework import JobBase
12
+ from .. core.framework import TaskBase
13
13
  from .. core.core_utils import make_tqdm_bar
14
14
  from .. core.exceptions import RunStopException, ShapeError
15
- from .stack_framework import FrameMultiDirectory, SubAction
15
+ from .stack_framework import ImageSequenceManager, SubAction
16
16
  from .utils import read_img, save_plot, get_img_metadata, validate_image
17
17
 
18
18
  MAX_NOISY_PIXELS = 1000
@@ -45,10 +45,10 @@ def mean_image(file_paths, max_frames=-1, message_callback=None, progress_callba
45
45
  return None if mean_img is None else (mean_img / counter).astype(np.uint8)
46
46
 
47
47
 
48
- class NoiseDetection(JobBase, FrameMultiDirectory):
48
+ class NoiseDetection(TaskBase, ImageSequenceManager):
49
49
  def __init__(self, name="noise-map", enabled=True, **kwargs):
50
- FrameMultiDirectory.__init__(self, name, **kwargs)
51
- JobBase.__init__(self, name, enabled)
50
+ ImageSequenceManager.__init__(self, name, **kwargs)
51
+ TaskBase.__init__(self, name, enabled)
52
52
  self.max_frames = kwargs.get('max_frames', constants.DEFAULT_NOISE_MAX_FRAMES)
53
53
  self.blur_size = kwargs.get('blur_size', constants.DEFAULT_BLUR_SIZE)
54
54
  self.file_name = kwargs.get('file_name', constants.DEFAULT_NOISE_MAP_FILENAME)
@@ -65,10 +65,10 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
65
65
  return cv2.threshold(ch, th, 255, cv2.THRESH_BINARY)[1]
66
66
 
67
67
  def progress(self, i):
68
- self.callback('after_step', self.id, self.name, i)
68
+ self.callback(constants.CALLBACK_AFTER_STEP, self.id, self.name, i)
69
69
  if not config.DISABLE_TQDM:
70
70
  self.tbar.update(1)
71
- if self.callback('check_running', self.id, self.name) is False:
71
+ if self.callback(constants.CALLBACK_CHECK_RUNNING, self.id, self.name) is False:
72
72
  raise RunStopException(self.name)
73
73
 
74
74
  def run_core(self):
@@ -76,21 +76,20 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
76
76
  f"map noisy pixels from frames in {self.folder_list_str()}",
77
77
  constants.LOG_COLOR_LEVEL_2
78
78
  ))
79
- files = self.folder_filelist()
80
- in_paths = [self.working_path + "/" + f for f in files]
79
+ in_paths = self.input_filepaths()
81
80
  n_frames = min(len(in_paths), self.max_frames) if self.max_frames > 0 else len(in_paths)
82
- self.callback('step_counts', self.id, self.name, n_frames)
81
+ self.callback(constants.CALLBACK_STEP_COUNTS, self.id, self.name, n_frames)
83
82
  if not config.DISABLE_TQDM:
84
83
  self.tbar = make_tqdm_bar(self.name, n_frames)
85
84
 
86
85
  def progress_callback(i):
87
86
  self.progress(i)
88
- if self.callback('check_running', self.id, self.name) is False:
87
+ if self.callback(constants.CALLBACK_CHECK_RUNNING, self.id, self.name) is False:
89
88
  raise RunStopException(self.name)
90
89
  mean_img = mean_image(
91
90
  file_paths=in_paths, max_frames=self.max_frames,
92
91
  message_callback=lambda path: self.print_message_r(
93
- color_str(f"reading frame: {path.split('/')[-1]}", constants.LOG_COLOR_LEVEL_2)
92
+ color_str(f"reading frame: {os.path.basename(path)}", constants.LOG_COLOR_LEVEL_2)
94
93
  ),
95
94
  progress_callback=progress_callback)
96
95
  if not config.DISABLE_TQDM:
@@ -123,7 +122,7 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
123
122
  plot_range[1] = max_th + 1
124
123
  th_range = np.arange(self.plot_range[0], self.plot_range[1] + 1)
125
124
  if self.plot_histograms:
126
- plt.figure(figsize=(10, 5))
125
+ plt.figure(figsize=constants.PLT_FIG_SIZE)
127
126
  x = np.array(list(th_range))
128
127
  ys = [[np.count_nonzero(self.hot_map(ch, th) > 0)
129
128
  for th in th_range] for ch in channels]
@@ -138,7 +137,7 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
138
137
  plt.ylim(0)
139
138
  plot_path = f"{self.working_path}/{self.plot_path}/{self.name}-hot-pixels.pdf"
140
139
  save_plot(plot_path)
141
- self.callback('save_plot', self.id, f"{self.name}: noise", plot_path)
140
+ self.callback(constants.CALLBACK_SAVE_PLOT, self.id, f"{self.name}: noise", plot_path)
142
141
  plt.close('all')
143
142
 
144
143
 
@@ -124,10 +124,9 @@ class PyramidBase(BaseStackAlgo):
124
124
 
125
125
  def focus_stack_validate(self, cleanup_callback=None):
126
126
  metadata = None
127
- n = len(self.filenames)
128
127
  for i, img_path in enumerate(self.filenames):
129
- self.print_message(f": validating file {img_path.split('/')[-1]}, {i + 1}/{n}")
130
-
128
+ self.print_message(
129
+ f": validating file {self.image_str(i)}")
131
130
  _img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
132
131
  if updated:
133
132
  self.dtype = metadata[1]
@@ -185,7 +184,8 @@ class PyramidStack(PyramidBase):
185
184
  self.focus_stack_validate()
186
185
  all_laplacians = []
187
186
  for i, img_path in enumerate(self.filenames):
188
- self.print_message(f": processing file {img_path.split('/')[-1]} ({i + 1}/{n})")
187
+ self.print_message(
188
+ f": processing {self.image_str(i)}")
189
189
  img = read_img(img_path)
190
190
  all_laplacians.append(self.process_single_image(img, self.n_levels))
191
191
  self.after_step(i + n + 1)