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.
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/align.py +148 -115
- shinestacker/algorithms/align_auto.py +64 -0
- shinestacker/algorithms/align_parallel.py +296 -0
- shinestacker/algorithms/balance.py +14 -13
- shinestacker/algorithms/base_stack_algo.py +11 -2
- shinestacker/algorithms/multilayer.py +14 -15
- shinestacker/algorithms/noise_detection.py +13 -14
- shinestacker/algorithms/pyramid.py +4 -4
- shinestacker/algorithms/pyramid_auto.py +16 -10
- shinestacker/algorithms/pyramid_tiles.py +19 -11
- shinestacker/algorithms/stack.py +30 -26
- shinestacker/algorithms/stack_framework.py +200 -178
- shinestacker/algorithms/vignetting.py +16 -13
- shinestacker/app/main.py +7 -3
- shinestacker/config/constants.py +63 -26
- shinestacker/config/gui_constants.py +1 -1
- shinestacker/core/core_utils.py +4 -0
- shinestacker/core/framework.py +114 -33
- shinestacker/gui/action_config.py +57 -5
- shinestacker/gui/action_config_dialog.py +156 -17
- shinestacker/gui/base_form_dialog.py +2 -2
- shinestacker/gui/folder_file_selection.py +101 -0
- shinestacker/gui/gui_images.py +10 -10
- shinestacker/gui/gui_run.py +13 -11
- shinestacker/gui/main_window.py +10 -5
- shinestacker/gui/menu_manager.py +4 -0
- shinestacker/gui/new_project.py +171 -74
- shinestacker/gui/project_controller.py +13 -9
- shinestacker/gui/project_converter.py +4 -2
- shinestacker/gui/project_editor.py +72 -53
- shinestacker/gui/select_path_widget.py +1 -1
- shinestacker/gui/sys_mon.py +96 -0
- shinestacker/gui/tab_widget.py +3 -3
- shinestacker/gui/time_progress_bar.py +4 -3
- shinestacker/retouch/exif_data.py +1 -1
- shinestacker/retouch/image_editor_ui.py +2 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/METADATA +6 -6
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/RECORD +43 -39
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.2.0.dist-info → shinestacker-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
593
|
+
img = read_img(self.process.input_filepath(process.ref_idx))
|
|
595
594
|
self.shape = img.shape
|
|
596
|
-
self.correction.begin(img, self.process.
|
|
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=
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
|
15
|
+
from .. core.framework import TaskBase
|
|
16
16
|
from .utils import EXTENSIONS_TIF, EXTENSIONS_JPG, EXTENSIONS_PNG
|
|
17
|
-
from .stack_framework import
|
|
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(
|
|
162
|
+
class MultiLayer(TaskBase, ImageSequenceManager):
|
|
163
163
|
def __init__(self, name, enabled=True, **kwargs):
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
173
|
+
ImageSequenceManager.init(self, job)
|
|
174
174
|
if self.exif_path == '':
|
|
175
|
-
self.exif_path = job.
|
|
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
|
-
|
|
192
|
-
if len(
|
|
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([
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
48
|
+
class NoiseDetection(TaskBase, ImageSequenceManager):
|
|
49
49
|
def __init__(self, name="noise-map", enabled=True, **kwargs):
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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=
|
|
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(
|
|
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(
|
|
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(
|
|
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)
|