shinestacker 0.3.3__py3-none-any.whl → 0.3.4__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/__init__.py +2 -1
- shinestacker/_version.py +1 -1
- shinestacker/algorithms/__init__.py +3 -2
- shinestacker/algorithms/align.py +102 -64
- shinestacker/algorithms/balance.py +89 -42
- shinestacker/algorithms/base_stack_algo.py +42 -0
- shinestacker/algorithms/core_utils.py +6 -6
- shinestacker/algorithms/denoise.py +4 -1
- shinestacker/algorithms/depth_map.py +28 -39
- shinestacker/algorithms/exif.py +43 -38
- shinestacker/algorithms/multilayer.py +48 -28
- shinestacker/algorithms/noise_detection.py +34 -23
- shinestacker/algorithms/pyramid.py +42 -42
- shinestacker/algorithms/sharpen.py +1 -0
- shinestacker/algorithms/stack.py +42 -41
- shinestacker/algorithms/stack_framework.py +111 -65
- shinestacker/algorithms/utils.py +12 -11
- shinestacker/algorithms/vignetting.py +48 -22
- shinestacker/algorithms/white_balance.py +1 -0
- shinestacker/app/about_dialog.py +6 -2
- shinestacker/app/app_config.py +1 -0
- shinestacker/app/gui_utils.py +20 -0
- shinestacker/app/help_menu.py +1 -0
- shinestacker/app/main.py +9 -18
- shinestacker/app/open_frames.py +5 -4
- shinestacker/app/project.py +5 -16
- shinestacker/app/retouch.py +5 -17
- shinestacker/core/colors.py +4 -4
- shinestacker/core/core_utils.py +1 -1
- shinestacker/core/exceptions.py +2 -1
- shinestacker/core/framework.py +46 -33
- shinestacker/core/logging.py +9 -10
- shinestacker/gui/action_config.py +253 -197
- shinestacker/gui/actions_window.py +32 -28
- shinestacker/gui/colors.py +1 -0
- shinestacker/gui/gui_images.py +7 -3
- shinestacker/gui/gui_logging.py +3 -2
- shinestacker/gui/gui_run.py +53 -38
- shinestacker/gui/main_window.py +69 -25
- shinestacker/gui/new_project.py +35 -2
- shinestacker/gui/project_converter.py +21 -20
- shinestacker/gui/project_editor.py +45 -52
- shinestacker/gui/project_model.py +15 -23
- shinestacker/retouch/{filter_base.py → base_filter.py} +7 -4
- shinestacker/retouch/brush.py +1 -0
- shinestacker/retouch/brush_gradient.py +17 -3
- shinestacker/retouch/brush_preview.py +14 -10
- shinestacker/retouch/brush_tool.py +28 -19
- shinestacker/retouch/denoise_filter.py +3 -2
- shinestacker/retouch/display_manager.py +11 -5
- shinestacker/retouch/exif_data.py +1 -0
- shinestacker/retouch/file_loader.py +13 -9
- shinestacker/retouch/filter_manager.py +1 -0
- shinestacker/retouch/image_editor.py +14 -48
- shinestacker/retouch/image_editor_ui.py +10 -5
- shinestacker/retouch/image_filters.py +4 -2
- shinestacker/retouch/image_viewer.py +33 -31
- shinestacker/retouch/io_gui_handler.py +25 -13
- shinestacker/retouch/io_manager.py +3 -2
- shinestacker/retouch/layer_collection.py +79 -23
- shinestacker/retouch/shortcuts_help.py +1 -0
- shinestacker/retouch/undo_manager.py +7 -0
- shinestacker/retouch/unsharp_mask_filter.py +3 -2
- shinestacker/retouch/white_balance_filter.py +11 -6
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.4.dist-info}/METADATA +10 -4
- shinestacker-0.3.4.dist-info/RECORD +86 -0
- shinestacker-0.3.3.dist-info/RECORD +0 -85
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.4.dist-info}/WHEEL +0 -0
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.4.dist-info}/entry_points.txt +0 -0
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-0.3.3.dist-info → shinestacker-0.3.4.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
|
|
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: "
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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.
|
|
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("{}: {
|
|
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
|
|
102
|
-
self.print_message("create directory: "
|
|
103
|
-
os.mkdir(self.working_path
|
|
104
|
-
|
|
105
|
-
|
|
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)
|
|
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
|
|
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,
|
|
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
|
|
@@ -154,7 +165,7 @@ class MaskNoise(SubAction):
|
|
|
154
165
|
def end(self):
|
|
155
166
|
pass
|
|
156
167
|
|
|
157
|
-
def run_frame(self,
|
|
168
|
+
def run_frame(self, _idx, _ref_idx, image):
|
|
158
169
|
self.process.sub_message_r(': mask noisy pixels')
|
|
159
170
|
if len(image.shape) == 3:
|
|
160
171
|
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.
|
|
5
|
-
from
|
|
6
|
-
from .
|
|
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,
|
|
11
|
-
|
|
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,
|
|
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])
|
|
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 +
|
|
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:
|
|
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
|
|
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(
|
|
103
|
-
|
|
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,
|
|
118
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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: {
|
|
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(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if
|
|
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
|
|
163
|
-
|
|
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
|
-
|
|
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(
|
|
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))
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
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
5
|
from .. core.colors import color_str
|
|
5
6
|
from .. core.framework import JobBase
|
|
@@ -10,15 +11,15 @@ from .exif import copy_exif_from_file_to_file
|
|
|
10
11
|
from .denoise import denoise
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
class FocusStackBase:
|
|
14
|
-
def __init__(self, stack_algo,
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
class FocusStackBase(JobBase, FrameDirectory):
|
|
15
|
+
def __init__(self, name, stack_algo, enabled=True, **kwargs):
|
|
16
|
+
FrameDirectory.__init__(self, name, **kwargs)
|
|
17
|
+
JobBase.__init__(self, name, enabled)
|
|
17
18
|
self.stack_algo = stack_algo
|
|
18
|
-
self.exif_path = exif_path
|
|
19
|
-
self.prefix = prefix
|
|
20
|
-
self.
|
|
21
|
-
self.plot_stack = plot_stack
|
|
19
|
+
self.exif_path = kwargs.pop('exif_path', '')
|
|
20
|
+
self.prefix = kwargs.pop('prefix', constants.DEFAULT_STACK_PREFIX)
|
|
21
|
+
self.denoise_amount = kwargs.pop('denoise_amount', 0)
|
|
22
|
+
self.plot_stack = kwargs.pop('plot_stack', constants.DEFAULT_PLOT_STACK)
|
|
22
23
|
self.stack_algo.process = self
|
|
23
24
|
self.frame_count = -1
|
|
24
25
|
|
|
@@ -27,20 +28,22 @@ class FocusStackBase:
|
|
|
27
28
|
img_files = sorted([os.path.join(self.input_full_path, name) for name in filenames])
|
|
28
29
|
stacked_img = self.stack_algo.focus_stack(img_files)
|
|
29
30
|
in_filename = filenames[0].split(".")
|
|
30
|
-
out_filename = f"{self.output_dir}/{self.prefix}{in_filename[0]}." +
|
|
31
|
-
|
|
31
|
+
out_filename = f"{self.output_dir}/{self.prefix}{in_filename[0]}." + \
|
|
32
|
+
'.'.join(in_filename[1:])
|
|
33
|
+
if self.denoise_amount > 0:
|
|
32
34
|
self.sub_message_r(': denoise image')
|
|
33
|
-
stacked_img = denoise(stacked_img, self.
|
|
35
|
+
stacked_img = denoise(stacked_img, self.denoise_amount, self.denoise_amount)
|
|
34
36
|
write_img(out_filename, stacked_img)
|
|
35
37
|
if self.exif_path != '' and stacked_img.dtype == np.uint8:
|
|
36
38
|
self.sub_message_r(': copy exif data')
|
|
37
|
-
|
|
38
|
-
fnames = [name for name in fnames
|
|
39
|
+
_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
42
|
exif_filename = f"{self.exif_path}/{fnames[0]}"
|
|
40
43
|
copy_exif_from_file_to_file(exif_filename, out_filename)
|
|
41
44
|
self.sub_message_r(' ' * 60)
|
|
42
45
|
if self.plot_stack:
|
|
43
|
-
idx_str = "{:04d}"
|
|
46
|
+
idx_str = f"{self.frame_count:04d}" if self.frame_count >= 0 else ''
|
|
44
47
|
name = f"{self.name}: {self.stack_algo.name()}"
|
|
45
48
|
if idx_str != '':
|
|
46
49
|
name += f"\nbunch: {idx_str}"
|
|
@@ -48,65 +51,63 @@ class FocusStackBase:
|
|
|
48
51
|
if self.frame_count >= 0:
|
|
49
52
|
self.frame_count += 1
|
|
50
53
|
|
|
51
|
-
def init(self, job, working_path):
|
|
54
|
+
def init(self, job, working_path=''):
|
|
52
55
|
if self.exif_path is None:
|
|
53
56
|
self.exif_path = job.paths[0]
|
|
54
57
|
if self.exif_path != '':
|
|
55
58
|
self.exif_path = working_path + "/" + self.exif_path
|
|
56
59
|
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
def get_bunches(collection, n_frames, n_overlap):
|
|
62
|
+
bunches = [collection[x:x + n_frames]
|
|
63
|
+
for x in range(0, len(collection) - n_overlap, n_frames - n_overlap)]
|
|
64
|
+
return bunches
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class FocusStackBunch(ActionList, FocusStackBase):
|
|
59
68
|
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
69
|
ActionList.__init__(self, name, enabled)
|
|
70
|
+
FocusStackBase.__init__(self, name, stack_algo, enabled, **kwargs)
|
|
71
|
+
self._chunks = None
|
|
67
72
|
self.frame_count = 0
|
|
68
73
|
self.frames = kwargs.get('frames', constants.DEFAULT_FRAMES)
|
|
69
74
|
self.overlap = kwargs.get('overlap', constants.DEFAULT_OVERLAP)
|
|
70
|
-
self.
|
|
75
|
+
self.denoise_amount = kwargs.get('denoise_amount', 0)
|
|
71
76
|
self.stack_algo.do_step_callback = False
|
|
72
77
|
if self.overlap >= self.frames:
|
|
73
|
-
raise InvalidOptionError("overlap", self.overlap,
|
|
78
|
+
raise InvalidOptionError("overlap", self.overlap,
|
|
79
|
+
"overlap must be smaller than batch size")
|
|
74
80
|
|
|
75
|
-
def init(self, job):
|
|
81
|
+
def init(self, job, _working_path=''):
|
|
76
82
|
FrameDirectory.init(self, job)
|
|
77
83
|
FocusStackBase.init(self, job, self.working_path)
|
|
78
84
|
|
|
79
85
|
def begin(self):
|
|
80
86
|
ActionList.begin(self)
|
|
81
|
-
fnames = self.folder_filelist(
|
|
82
|
-
self.
|
|
83
|
-
self.set_counts(len(self.
|
|
87
|
+
fnames = self.folder_filelist()
|
|
88
|
+
self._chunks = get_bunches(fnames, self.frames, self.overlap)
|
|
89
|
+
self.set_counts(len(self._chunks))
|
|
84
90
|
|
|
85
91
|
def end(self):
|
|
86
92
|
ActionList.end(self)
|
|
87
93
|
|
|
88
94
|
def run_step(self):
|
|
89
|
-
self.print_message_r(color_str("fusing bunch: {
|
|
90
|
-
self.focus_stack(self.
|
|
95
|
+
self.print_message_r(color_str(f"fusing bunch: {self.count}", "blue"))
|
|
96
|
+
self.focus_stack(self._chunks[self.count - 1])
|
|
91
97
|
self.callback('after_step', self.id, self.name, self.count)
|
|
92
98
|
|
|
93
99
|
|
|
94
|
-
class FocusStack(FocusStackBase
|
|
100
|
+
class FocusStack(FocusStackBase):
|
|
95
101
|
def __init__(self, name, stack_algo, enabled=True, **kwargs):
|
|
96
|
-
|
|
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)
|
|
102
|
+
super().__init__(name, stack_algo, enabled, **kwargs)
|
|
103
103
|
self.stack_algo.do_step_callback = True
|
|
104
104
|
|
|
105
105
|
def run_core(self):
|
|
106
106
|
self.set_filelist()
|
|
107
|
-
self.callback('step_counts', self.id, self.name,
|
|
107
|
+
self.callback('step_counts', self.id, self.name,
|
|
108
|
+
self.stack_algo.steps_per_frame() * len(self.filenames))
|
|
108
109
|
self.focus_stack(self.filenames)
|
|
109
110
|
|
|
110
|
-
def init(self, job):
|
|
111
|
+
def init(self, job, _working_path=''):
|
|
111
112
|
FrameDirectory.init(self, job)
|
|
112
113
|
FocusStackBase.init(self, job, self.working_path)
|