shinestacker 1.0.4.post2__py3-none-any.whl → 1.2.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/__init__.py +4 -1
- shinestacker/algorithms/align.py +128 -14
- shinestacker/algorithms/balance.py +362 -163
- shinestacker/algorithms/base_stack_algo.py +33 -4
- shinestacker/algorithms/depth_map.py +9 -12
- shinestacker/algorithms/multilayer.py +12 -2
- shinestacker/algorithms/noise_detection.py +8 -3
- shinestacker/algorithms/pyramid.py +57 -42
- shinestacker/algorithms/pyramid_auto.py +141 -0
- shinestacker/algorithms/pyramid_tiles.py +264 -0
- shinestacker/algorithms/stack.py +14 -11
- shinestacker/algorithms/stack_framework.py +17 -11
- shinestacker/algorithms/utils.py +180 -1
- shinestacker/algorithms/vignetting.py +23 -5
- shinestacker/config/constants.py +31 -5
- shinestacker/gui/action_config.py +6 -7
- shinestacker/gui/action_config_dialog.py +425 -258
- shinestacker/gui/base_form_dialog.py +11 -6
- shinestacker/gui/flow_layout.py +105 -0
- shinestacker/gui/gui_run.py +24 -19
- shinestacker/gui/main_window.py +4 -3
- shinestacker/gui/menu_manager.py +12 -2
- shinestacker/gui/new_project.py +28 -22
- shinestacker/gui/project_controller.py +40 -23
- shinestacker/gui/project_converter.py +6 -6
- shinestacker/gui/project_editor.py +21 -7
- shinestacker/gui/time_progress_bar.py +2 -2
- shinestacker/retouch/exif_data.py +5 -5
- shinestacker/retouch/shortcuts_help.py +4 -4
- shinestacker/retouch/vignetting_filter.py +12 -8
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/METADATA +20 -1
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/RECORD +37 -34
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.0.4.post2.dist-info → shinestacker-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# pylint: disable=C0114, C0115, C0116, E0602, R0903
|
|
2
|
+
import os
|
|
2
3
|
import numpy as np
|
|
3
|
-
from .. core.exceptions import InvalidOptionError, ImageLoadError
|
|
4
|
+
from .. core.exceptions import InvalidOptionError, ImageLoadError, RunStopException
|
|
4
5
|
from .. config.constants import constants
|
|
5
6
|
from .. core.colors import color_str
|
|
6
|
-
from .utils import read_img, get_img_metadata, validate_image
|
|
7
|
+
from .utils import read_img, get_img_metadata, validate_image, get_img_file_shape, extension_tif_jpg
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class BaseStackAlgo:
|
|
@@ -11,6 +12,9 @@ class BaseStackAlgo:
|
|
|
11
12
|
self._name = name
|
|
12
13
|
self._steps_per_frame = steps_per_frame
|
|
13
14
|
self.process = None
|
|
15
|
+
self.filenames = None
|
|
16
|
+
self.shape = None
|
|
17
|
+
self.do_step_callback = False
|
|
14
18
|
if float_type == constants.FLOAT_32:
|
|
15
19
|
self.float_type = np.float32
|
|
16
20
|
elif float_type == constants.FLOAT_64:
|
|
@@ -24,8 +28,23 @@ class BaseStackAlgo:
|
|
|
24
28
|
def name(self):
|
|
25
29
|
return self._name
|
|
26
30
|
|
|
27
|
-
def
|
|
28
|
-
|
|
31
|
+
def set_process(self, process):
|
|
32
|
+
self.process = process
|
|
33
|
+
|
|
34
|
+
def set_do_step_callback(self, enable):
|
|
35
|
+
self.do_step_callback = enable
|
|
36
|
+
|
|
37
|
+
def init(self, filenames):
|
|
38
|
+
self.filenames = filenames
|
|
39
|
+
first_img_file = ''
|
|
40
|
+
for filename in filenames:
|
|
41
|
+
if os.path.isfile(filename) and extension_tif_jpg(filename):
|
|
42
|
+
first_img_file = filename
|
|
43
|
+
break
|
|
44
|
+
self.shape = get_img_file_shape(first_img_file)
|
|
45
|
+
|
|
46
|
+
def total_steps(self, n_frames):
|
|
47
|
+
return self._steps_per_frame * n_frames
|
|
29
48
|
|
|
30
49
|
def print_message(self, msg):
|
|
31
50
|
self.process.sub_message_r(color_str(msg, constants.LOG_COLOR_LEVEL_3))
|
|
@@ -40,3 +59,13 @@ class BaseStackAlgo:
|
|
|
40
59
|
else:
|
|
41
60
|
validate_image(img, *metadata)
|
|
42
61
|
return img, metadata, updated
|
|
62
|
+
|
|
63
|
+
def check_running(self, cleanup_callback=None):
|
|
64
|
+
if self.process.callback('check_running', self.process.id, self.process.name) is False:
|
|
65
|
+
if cleanup_callback is not None:
|
|
66
|
+
cleanup_callback()
|
|
67
|
+
raise RunStopException(self.name)
|
|
68
|
+
|
|
69
|
+
def after_step(self, step):
|
|
70
|
+
if self.do_step_callback:
|
|
71
|
+
self.process.callback('after_step', self.process.id, self.process.name, step)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
import cv2
|
|
4
4
|
from .. config.constants import constants
|
|
5
|
-
from .. core.exceptions import InvalidOptionError
|
|
5
|
+
from .. core.exceptions import InvalidOptionError
|
|
6
6
|
from .utils import read_img, img_bw
|
|
7
7
|
from .base_stack_algo import BaseStackAlgo
|
|
8
8
|
|
|
@@ -61,19 +61,18 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
61
61
|
raise InvalidOptionError("map_type", self.map_type, details=f" valid values are "
|
|
62
62
|
f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
|
|
63
63
|
|
|
64
|
-
def focus_stack(self
|
|
64
|
+
def focus_stack(self):
|
|
65
65
|
gray_images = []
|
|
66
66
|
metadata = None
|
|
67
|
-
for i, img_path in enumerate(filenames):
|
|
67
|
+
for i, img_path in enumerate(self.filenames):
|
|
68
68
|
self.print_message(f": reading file (1/2) {img_path.split('/')[-1]}")
|
|
69
69
|
|
|
70
70
|
img, metadata, _updated = self.read_image_and_update_metadata(img_path, metadata)
|
|
71
71
|
|
|
72
72
|
gray = img_bw(img)
|
|
73
73
|
gray_images.append(gray)
|
|
74
|
-
self.
|
|
75
|
-
|
|
76
|
-
raise RunStopException(self.name)
|
|
74
|
+
self.after_step(i)
|
|
75
|
+
self.check_running()
|
|
77
76
|
dtype = metadata[1]
|
|
78
77
|
gray_images = np.array(gray_images, dtype=self.float_type)
|
|
79
78
|
if self.energy == constants.DM_ENERGY_SOBEL:
|
|
@@ -92,7 +91,7 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
92
91
|
energies = self.smooth_energy(energies)
|
|
93
92
|
weights = self.get_focus_map(energies)
|
|
94
93
|
blended_pyramid = None
|
|
95
|
-
for i, img_path in enumerate(filenames):
|
|
94
|
+
for i, img_path in enumerate(self.filenames):
|
|
96
95
|
self.print_message(f": reading file (2/2) {img_path.split('/')[-1]}")
|
|
97
96
|
img = read_img(img_path).astype(self.float_type)
|
|
98
97
|
weight = weights[i]
|
|
@@ -110,14 +109,12 @@ class DepthMapStack(BaseStackAlgo):
|
|
|
110
109
|
for j in range(self.levels)]
|
|
111
110
|
blended_pyramid = current_blend if blended_pyramid is None \
|
|
112
111
|
else [np.add(bp, cb) for bp, cb in zip(blended_pyramid, current_blend)]
|
|
113
|
-
self.
|
|
114
|
-
|
|
115
|
-
if self.process.callback('check_running', self.process.id, self.process.name) is False:
|
|
116
|
-
raise RunStopException(self.name)
|
|
112
|
+
self.after_step(i + len(self.filenames))
|
|
113
|
+
self.check_running()
|
|
117
114
|
result = blended_pyramid[0]
|
|
118
115
|
self.print_message(': blend levels')
|
|
119
116
|
for j in range(1, self.levels):
|
|
120
117
|
size = (blended_pyramid[j].shape[1], blended_pyramid[j].shape[0])
|
|
121
118
|
result = cv2.pyrUp(result, dstsize=size) + blended_pyramid[j]
|
|
122
|
-
n_values =
|
|
119
|
+
n_values = constants.MAX_UINT8 if dtype == np.uint8 else constants.MAX_UINT16
|
|
123
120
|
return np.clip(np.absolute(result), 0, n_values).astype(dtype)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E1101, R0914, E0606
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E1101, R0914, E0606, R0912
|
|
2
2
|
import os
|
|
3
3
|
import logging
|
|
4
4
|
import cv2
|
|
@@ -61,6 +61,13 @@ def write_multilayer_tiff_from_images(image_dict, output_file, exif_path='', cal
|
|
|
61
61
|
if len(dtypes) > 1:
|
|
62
62
|
raise RuntimeError("All input files must all have 8 bit or 16 bit depth.")
|
|
63
63
|
dtype = dtypes[0]
|
|
64
|
+
bytes_per_pixel = 3 * np.dtype(dtype).itemsize
|
|
65
|
+
est_memory = shape[0] * shape[1] * bytes_per_pixel * len(image_dict)
|
|
66
|
+
if est_memory > constants.MULTILAYER_WARNING_MEM_GB * constants.ONE_GIGA:
|
|
67
|
+
if callbacks:
|
|
68
|
+
callback = callbacks.get('memory_warning', None)
|
|
69
|
+
if callback:
|
|
70
|
+
callback(float(est_memory) / constants.ONE_GIGA)
|
|
64
71
|
max_pixel_value = constants.MAX_UINT16 if dtype == np.uint16 else constants.MAX_UINT8
|
|
65
72
|
transp = np.full_like(list(image_dict.values())[0][..., 0], max_pixel_value)
|
|
66
73
|
compression_type = PsdCompressionType.ZIP_PREDICTED
|
|
@@ -203,7 +210,10 @@ class MultiLayer(JobBase, FrameMultiDirectory):
|
|
|
203
210
|
'exif_msg': lambda path: self.print_message(
|
|
204
211
|
color_str(f"copying exif data from path: {path}", constants.LOG_COLOR_LEVEL_2)),
|
|
205
212
|
'write_msg': lambda path: self.print_message(
|
|
206
|
-
color_str(f"writing multilayer tiff file: {path}", constants.LOG_COLOR_LEVEL_2))
|
|
213
|
+
color_str(f"writing multilayer tiff file: {path}", constants.LOG_COLOR_LEVEL_2)),
|
|
214
|
+
'memory_warning': lambda mem: self.print_message(
|
|
215
|
+
color_str(f"warning: estimated file size: {mem:.2f} GBytes",
|
|
216
|
+
constants.LOG_COLOR_WARNING))
|
|
207
217
|
}
|
|
208
218
|
write_multilayer_tiff(input_files, output_file, labels=None, exif_path=self.exif_path,
|
|
209
219
|
callbacks=callbacks)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E1101, W0718, R0914, R0915, R0902
|
|
2
2
|
import os
|
|
3
3
|
import errno
|
|
4
4
|
import logging
|
|
@@ -11,7 +11,7 @@ from .. core.colors import color_str
|
|
|
11
11
|
from .. core.exceptions import ImageLoadError
|
|
12
12
|
from .. core.framework import JobBase
|
|
13
13
|
from .. core.core_utils import make_tqdm_bar
|
|
14
|
-
from .. core.exceptions import RunStopException
|
|
14
|
+
from .. core.exceptions import RunStopException, ShapeError
|
|
15
15
|
from .stack_framework import FrameMultiDirectory, SubAction
|
|
16
16
|
from .utils import read_img, save_plot, get_img_metadata, validate_image
|
|
17
17
|
|
|
@@ -49,7 +49,7 @@ class NoiseDetection(JobBase, FrameMultiDirectory):
|
|
|
49
49
|
def __init__(self, name="noise-map", enabled=True, **kwargs):
|
|
50
50
|
FrameMultiDirectory.__init__(self, name, **kwargs)
|
|
51
51
|
JobBase.__init__(self, name, enabled)
|
|
52
|
-
self.max_frames = kwargs.get('max_frames',
|
|
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)
|
|
55
55
|
if self.file_name == '':
|
|
@@ -154,6 +154,7 @@ class MaskNoise(SubAction):
|
|
|
154
154
|
self.method = method
|
|
155
155
|
self.process = None
|
|
156
156
|
self.noise_mask_img = None
|
|
157
|
+
self.expected_shape = None
|
|
157
158
|
|
|
158
159
|
def begin(self, process):
|
|
159
160
|
self.process = process
|
|
@@ -163,6 +164,7 @@ class MaskNoise(SubAction):
|
|
|
163
164
|
f': reading noisy pixel mask file: {self.noise_mask}',
|
|
164
165
|
constants.LOG_COLOR_LEVEL_3))
|
|
165
166
|
self.noise_mask_img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
|
|
167
|
+
self.expected_shape = self.noise_mask_img.shape[:2]
|
|
166
168
|
if self.noise_mask_img is None:
|
|
167
169
|
raise ImageLoadError(path, f"failed to load image file {self.noise_mask}.")
|
|
168
170
|
else:
|
|
@@ -170,6 +172,9 @@ class MaskNoise(SubAction):
|
|
|
170
172
|
|
|
171
173
|
def run_frame(self, _idx, _ref_idx, image):
|
|
172
174
|
self.process.sub_message_r(color_str(': mask noisy pixels', constants.LOG_COLOR_LEVEL_3))
|
|
175
|
+
shape = image.shape[:2]
|
|
176
|
+
if shape != self.expected_shape:
|
|
177
|
+
raise ShapeError(self.expected_shape, shape)
|
|
173
178
|
if len(image.shape) == 3:
|
|
174
179
|
corrected = image.copy()
|
|
175
180
|
for c in range(3):
|
|
@@ -1,25 +1,36 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, E1101
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E1101, R0913, R0917, R0902
|
|
2
2
|
import numpy as np
|
|
3
3
|
import cv2
|
|
4
4
|
from .. config.constants import constants
|
|
5
|
-
from .. core.exceptions import RunStopException
|
|
6
5
|
from .utils import read_img
|
|
7
6
|
from .base_stack_algo import BaseStackAlgo
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class PyramidBase(BaseStackAlgo):
|
|
11
|
-
def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
|
|
10
|
+
def __init__(self, name, min_size=constants.DEFAULT_PY_MIN_SIZE,
|
|
12
11
|
kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
|
|
13
12
|
gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
|
|
14
13
|
float_type=constants.DEFAULT_PY_FLOAT):
|
|
15
|
-
super().__init__(
|
|
14
|
+
super().__init__(name, 2, float_type)
|
|
16
15
|
self.min_size = min_size
|
|
17
16
|
self.kernel_size = kernel_size
|
|
18
17
|
self.pad_amount = (kernel_size - 1) // 2
|
|
19
|
-
self.do_step_callback = False
|
|
20
18
|
kernel = np.array([0.25 - gen_kernel / 2.0, 0.25,
|
|
21
19
|
gen_kernel, 0.25, 0.25 - gen_kernel / 2.0])
|
|
22
20
|
self.gen_kernel = np.outer(kernel, kernel)
|
|
21
|
+
self.dtype = None
|
|
22
|
+
self.num_pixel_values = None
|
|
23
|
+
self.max_pixel_value = None
|
|
24
|
+
self.n_levels = 0
|
|
25
|
+
self.n_frames = 0
|
|
26
|
+
|
|
27
|
+
def init(self, filenames):
|
|
28
|
+
super().init(filenames)
|
|
29
|
+
self.n_levels = int(np.log2(min(self.shape) / self.min_size))
|
|
30
|
+
|
|
31
|
+
def total_steps(self, n_frames):
|
|
32
|
+
self.n_frames = n_frames
|
|
33
|
+
return self._steps_per_frame * n_frames + self.n_levels
|
|
23
34
|
|
|
24
35
|
def convolve(self, image):
|
|
25
36
|
return cv2.filter2D(image, -1, self.gen_kernel, borderType=cv2.BORDER_REFLECT101)
|
|
@@ -55,6 +66,7 @@ class PyramidBase(BaseStackAlgo):
|
|
|
55
66
|
return fused
|
|
56
67
|
|
|
57
68
|
def collapse(self, pyramid):
|
|
69
|
+
self.print_message(': collapsing pyramid')
|
|
58
70
|
img = pyramid[-1]
|
|
59
71
|
for layer in pyramid[-2::-1]:
|
|
60
72
|
expanded = self.expand_layer(img)
|
|
@@ -110,19 +122,23 @@ class PyramidBase(BaseStackAlgo):
|
|
|
110
122
|
fused += np.where(best_d[:, :, np.newaxis] == layer, img, 0)
|
|
111
123
|
return (fused / 2).astype(images.dtype)
|
|
112
124
|
|
|
125
|
+
def focus_stack_validate(self, cleanup_callback=None):
|
|
126
|
+
metadata = None
|
|
127
|
+
n = len(self.filenames)
|
|
128
|
+
for i, img_path in enumerate(self.filenames):
|
|
129
|
+
self.print_message(f": validating file {img_path.split('/')[-1]}, {i + 1}/{n}")
|
|
113
130
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
self.max_pixel_value = None
|
|
131
|
+
_img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
|
|
132
|
+
if updated:
|
|
133
|
+
self.dtype = metadata[1]
|
|
134
|
+
self.num_pixel_values = constants.NUM_UINT8 \
|
|
135
|
+
if self.dtype == np.uint8 else constants.NUM_UINT16
|
|
136
|
+
self.max_pixel_value = constants.MAX_UINT8 \
|
|
137
|
+
if self.dtype == np.uint8 else constants.MAX_UINT16
|
|
138
|
+
self.after_step(i + 1)
|
|
139
|
+
self.check_running(cleanup_callback)
|
|
124
140
|
|
|
125
|
-
def
|
|
141
|
+
def single_image_laplacian(self, img, levels):
|
|
126
142
|
pyramid = [img.astype(self.float_type)]
|
|
127
143
|
for _ in range(levels):
|
|
128
144
|
next_layer = self.reduce_layer(pyramid[-1])
|
|
@@ -136,44 +152,43 @@ class PyramidStack(PyramidBase):
|
|
|
136
152
|
h, w = pyr.shape[:2]
|
|
137
153
|
expanded = expanded[:h, :w]
|
|
138
154
|
laplacian.append(pyr - expanded)
|
|
155
|
+
return laplacian
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class PyramidStack(PyramidBase):
|
|
159
|
+
def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
|
|
160
|
+
kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
|
|
161
|
+
gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
|
|
162
|
+
float_type=constants.DEFAULT_PY_FLOAT):
|
|
163
|
+
super().__init__("pyramid", min_size, kernel_size, gen_kernel, float_type)
|
|
164
|
+
self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
|
|
165
|
+
|
|
166
|
+
def process_single_image(self, img, levels):
|
|
167
|
+
laplacian = self.single_image_laplacian(img, levels)
|
|
139
168
|
return laplacian[::-1]
|
|
140
169
|
|
|
141
170
|
def fuse_pyramids(self, all_laplacians):
|
|
142
171
|
fused = [self.get_fused_base(np.stack([p[-1] for p in all_laplacians], axis=0))]
|
|
172
|
+
count = 0
|
|
143
173
|
for layer in range(len(all_laplacians[0]) - 2, -1, -1):
|
|
144
174
|
self.print_message(f': fusing pyramids, layer: {layer + 1}')
|
|
145
175
|
laplacians = np.stack([p[layer] for p in all_laplacians], axis=0)
|
|
146
176
|
fused.append(self.fuse_laplacian(laplacians))
|
|
177
|
+
count += 1
|
|
178
|
+
self.after_step(self._steps_per_frame * self.n_frames + count)
|
|
179
|
+
self.check_running()
|
|
147
180
|
self.print_message(': pyramids fusion completed')
|
|
148
181
|
return fused[::-1]
|
|
149
182
|
|
|
150
|
-
def focus_stack(self
|
|
151
|
-
|
|
183
|
+
def focus_stack(self):
|
|
184
|
+
n = len(self.filenames)
|
|
185
|
+
self.focus_stack_validate()
|
|
152
186
|
all_laplacians = []
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
for i, img_path in enumerate(filenames):
|
|
156
|
-
self.print_message(f": validating file {img_path.split('/')[-1]}")
|
|
157
|
-
|
|
158
|
-
img, metadata, updated = self.read_image_and_update_metadata(img_path, metadata)
|
|
159
|
-
if updated:
|
|
160
|
-
self.dtype = metadata[1]
|
|
161
|
-
self.num_pixel_values = constants.NUM_UINT8 \
|
|
162
|
-
if self.dtype == np.uint8 else constants.NUM_UINT16
|
|
163
|
-
self.max_pixel_value = constants.MAX_UINT8 \
|
|
164
|
-
if self.dtype == np.uint8 else constants.MAX_UINT16
|
|
165
|
-
levels = int(np.log2(min(img.shape[:2]) / self.min_size))
|
|
166
|
-
if self.do_step_callback:
|
|
167
|
-
self.process.callback('after_step', self.process.id, self.process.name, i)
|
|
168
|
-
if self.process.callback('check_running', self.process.id, self.process.name) is False:
|
|
169
|
-
raise RunStopException(self.name)
|
|
170
|
-
for i, img_path in enumerate(filenames):
|
|
171
|
-
self.print_message(f": processing file {img_path.split('/')[-1]}")
|
|
187
|
+
for i, img_path in enumerate(self.filenames):
|
|
188
|
+
self.print_message(f": processing file {img_path.split('/')[-1]} ({i + 1}/{n})")
|
|
172
189
|
img = read_img(img_path)
|
|
173
|
-
all_laplacians.append(self.process_single_image(img,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if self.process.callback('check_running', self.process.id, self.process.name) is False:
|
|
177
|
-
raise RunStopException(self.name)
|
|
190
|
+
all_laplacians.append(self.process_single_image(img, self.n_levels))
|
|
191
|
+
self.after_step(i + n + 1)
|
|
192
|
+
self.check_running()
|
|
178
193
|
stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
|
|
179
194
|
return stacked_image.astype(self.dtype)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, E1101, R0913, R0902, R0914, R0917
|
|
2
|
+
import os
|
|
3
|
+
import numpy as np
|
|
4
|
+
from .. config.constants import constants
|
|
5
|
+
from .utils import extension_tif_jpg
|
|
6
|
+
from .base_stack_algo import BaseStackAlgo
|
|
7
|
+
from .pyramid import PyramidStack
|
|
8
|
+
from .pyramid_tiles import PyramidTilesStack
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PyramidAutoStack(BaseStackAlgo):
|
|
12
|
+
def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE,
|
|
13
|
+
kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
|
|
14
|
+
gen_kernel=constants.DEFAULT_PY_GEN_KERNEL,
|
|
15
|
+
float_type=constants.DEFAULT_PY_FLOAT,
|
|
16
|
+
tile_size=constants.DEFAULT_PY_TILE_SIZE,
|
|
17
|
+
n_tiled_layers=constants.DEFAULT_PY_N_TILED_LAYERS,
|
|
18
|
+
memory_limit=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
|
|
19
|
+
max_threads=constants.DEFAULT_PY_MAX_THREADS,
|
|
20
|
+
max_tile_size=2048,
|
|
21
|
+
min_n_tiled_layers=1,
|
|
22
|
+
mode='auto'):
|
|
23
|
+
super().__init__("auto_pyramid", 2, float_type)
|
|
24
|
+
self.min_size = min_size
|
|
25
|
+
self.kernel_size = kernel_size
|
|
26
|
+
self.gen_kernel = gen_kernel
|
|
27
|
+
self.float_type = float_type
|
|
28
|
+
self.tile_size = tile_size
|
|
29
|
+
self.n_tiled_layers = n_tiled_layers
|
|
30
|
+
self.memory_limit = memory_limit * constants.ONE_GIGA
|
|
31
|
+
self.max_threads = max_threads
|
|
32
|
+
available_cores = os.cpu_count() or 1
|
|
33
|
+
self.num_threads = min(max_threads, available_cores)
|
|
34
|
+
self.max_tile_size = max_tile_size
|
|
35
|
+
self.min_n_tiled_layers = min_n_tiled_layers
|
|
36
|
+
self.mode = mode
|
|
37
|
+
self._implementation = None
|
|
38
|
+
self.dtype = None
|
|
39
|
+
self.shape = None
|
|
40
|
+
self.n_levels = None
|
|
41
|
+
self.n_frames = 0
|
|
42
|
+
self.channels = 3
|
|
43
|
+
dtype = np.float32 if self.float_type == constants.FLOAT_32 else np.float64
|
|
44
|
+
self.bytes_per_pixel = self.channels * np.dtype(dtype).itemsize
|
|
45
|
+
self.overhead = 1.5
|
|
46
|
+
|
|
47
|
+
def init(self, filenames):
|
|
48
|
+
first_img_file = None
|
|
49
|
+
for filename in filenames:
|
|
50
|
+
if os.path.isfile(filename) and extension_tif_jpg(filename):
|
|
51
|
+
first_img_file = filename
|
|
52
|
+
break
|
|
53
|
+
if first_img_file is None:
|
|
54
|
+
raise ValueError("No valid image files found")
|
|
55
|
+
_img, metadata, _ = self.read_image_and_update_metadata(first_img_file, None)
|
|
56
|
+
self.shape, self.dtype = metadata
|
|
57
|
+
self.n_levels = int(np.log2(min(self.shape) / self.min_size))
|
|
58
|
+
self.n_frames = len(filenames)
|
|
59
|
+
memory_required_memory = self._estimate_memory_memory()
|
|
60
|
+
if self.mode == 'memory' or (self.mode == 'auto' and
|
|
61
|
+
memory_required_memory <= self.memory_limit):
|
|
62
|
+
self._implementation = PyramidStack(
|
|
63
|
+
min_size=self.min_size,
|
|
64
|
+
kernel_size=self.kernel_size,
|
|
65
|
+
gen_kernel=self.gen_kernel,
|
|
66
|
+
float_type=self.float_type
|
|
67
|
+
)
|
|
68
|
+
self.print_message(": using memory-based pyramid stacking")
|
|
69
|
+
else:
|
|
70
|
+
optimal_params = self._find_optimal_tile_params()
|
|
71
|
+
self._implementation = PyramidTilesStack(
|
|
72
|
+
min_size=self.min_size,
|
|
73
|
+
kernel_size=self.kernel_size,
|
|
74
|
+
gen_kernel=self.gen_kernel,
|
|
75
|
+
float_type=self.float_type,
|
|
76
|
+
tile_size=optimal_params['tile_size'],
|
|
77
|
+
n_tiled_layers=optimal_params['n_tiled_layers'],
|
|
78
|
+
max_threads=self.num_threads
|
|
79
|
+
)
|
|
80
|
+
self.print_message(f": using tile-based pyramid stacking "
|
|
81
|
+
f"(tile_size: {optimal_params['tile_size']}, "
|
|
82
|
+
f"n_tiled_layers: {optimal_params['n_tiled_layers']}), "
|
|
83
|
+
f"{self.num_threads} cores.")
|
|
84
|
+
self._implementation.init(filenames)
|
|
85
|
+
self._implementation.set_do_step_callback(self.do_step_callback)
|
|
86
|
+
if self.process is not None:
|
|
87
|
+
self._implementation.set_process(self.process)
|
|
88
|
+
else:
|
|
89
|
+
raise RuntimeError("self.process must be initialized.")
|
|
90
|
+
|
|
91
|
+
def _estimate_memory_memory(self):
|
|
92
|
+
h, w = self.shape[:2]
|
|
93
|
+
total_memory = 0
|
|
94
|
+
for _ in range(self.n_levels):
|
|
95
|
+
total_memory += h * w * self.bytes_per_pixel
|
|
96
|
+
h, w = max(1, h // 2), max(1, w // 2)
|
|
97
|
+
return self.overhead * total_memory * self.n_frames
|
|
98
|
+
|
|
99
|
+
def _find_optimal_tile_params(self):
|
|
100
|
+
tile_size_max = int(np.sqrt(self.memory_limit /
|
|
101
|
+
(self.num_threads * self.n_frames *
|
|
102
|
+
self.bytes_per_pixel * self.overhead)))
|
|
103
|
+
tile_size = min(self.max_tile_size, tile_size_max, self.shape[0], self.shape[1])
|
|
104
|
+
n_tiled_layers = 0
|
|
105
|
+
for layer in range(self.n_levels):
|
|
106
|
+
h = max(1, self.shape[0] // (2 ** layer))
|
|
107
|
+
w = max(1, self.shape[1] // (2 ** layer))
|
|
108
|
+
if h > tile_size or w > tile_size:
|
|
109
|
+
n_tiled_layers = layer + 1
|
|
110
|
+
else:
|
|
111
|
+
break
|
|
112
|
+
n_tiled_layers = max(n_tiled_layers, self.min_n_tiled_layers)
|
|
113
|
+
n_tiled_layers = min(n_tiled_layers, self.n_levels)
|
|
114
|
+
return {'tile_size': tile_size, 'n_tiled_layers': n_tiled_layers}
|
|
115
|
+
|
|
116
|
+
def set_process(self, process):
|
|
117
|
+
super().set_process(process)
|
|
118
|
+
if self._implementation is not None:
|
|
119
|
+
self._implementation.set_process(process)
|
|
120
|
+
|
|
121
|
+
def total_steps(self, n_frames):
|
|
122
|
+
if self._implementation is None:
|
|
123
|
+
return super().total_steps(n_frames)
|
|
124
|
+
return self._implementation.total_steps(n_frames)
|
|
125
|
+
|
|
126
|
+
def focus_stack(self):
|
|
127
|
+
if self._implementation is None:
|
|
128
|
+
raise RuntimeError("PyramidAutoStack not initialized")
|
|
129
|
+
return self._implementation.focus_stack()
|
|
130
|
+
|
|
131
|
+
def after_step(self, step):
|
|
132
|
+
if self._implementation is not None:
|
|
133
|
+
self._implementation.after_step(step)
|
|
134
|
+
else:
|
|
135
|
+
super().after_step(step)
|
|
136
|
+
|
|
137
|
+
def check_running(self, cleanup_callback=None):
|
|
138
|
+
if self._implementation is not None:
|
|
139
|
+
self._implementation.check_running(cleanup_callback)
|
|
140
|
+
else:
|
|
141
|
+
super().check_running(cleanup_callback)
|