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
|
@@ -17,8 +17,9 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
17
17
|
n_tiled_layers=constants.DEFAULT_PY_N_TILED_LAYERS,
|
|
18
18
|
memory_limit=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
|
|
19
19
|
max_threads=constants.DEFAULT_PY_MAX_THREADS,
|
|
20
|
-
max_tile_size=
|
|
21
|
-
|
|
20
|
+
max_tile_size=constants.DEFAULT_PY_MAX_TILE_SIZE,
|
|
21
|
+
min_tile_size=constants.DEFAULT_PY_MIN_TILE_SIZE,
|
|
22
|
+
min_n_tiled_layers=constants.DEFAULT_PY_MIN_N_TILED_LAYERS,
|
|
22
23
|
mode='auto'):
|
|
23
24
|
super().__init__("auto_pyramid", 2, float_type)
|
|
24
25
|
self.min_size = min_size
|
|
@@ -32,6 +33,7 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
32
33
|
available_cores = os.cpu_count() or 1
|
|
33
34
|
self.num_threads = min(max_threads, available_cores)
|
|
34
35
|
self.max_tile_size = max_tile_size
|
|
36
|
+
self.min_tile_size = min_tile_size
|
|
35
37
|
self.min_n_tiled_layers = min_n_tiled_layers
|
|
36
38
|
self.mode = mode
|
|
37
39
|
self._implementation = None
|
|
@@ -39,10 +41,10 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
39
41
|
self.shape = None
|
|
40
42
|
self.n_levels = None
|
|
41
43
|
self.n_frames = 0
|
|
42
|
-
self.channels = 3
|
|
44
|
+
self.channels = 3 # r, g, b
|
|
43
45
|
dtype = np.float32 if self.float_type == constants.FLOAT_32 else np.float64
|
|
44
46
|
self.bytes_per_pixel = self.channels * np.dtype(dtype).itemsize
|
|
45
|
-
self.overhead =
|
|
47
|
+
self.overhead = constants.PY_MEMORY_OVERHEAD
|
|
46
48
|
|
|
47
49
|
def init(self, filenames):
|
|
48
50
|
first_img_file = None
|
|
@@ -97,15 +99,19 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
97
99
|
return self.overhead * total_memory * self.n_frames
|
|
98
100
|
|
|
99
101
|
def _find_optimal_tile_params(self):
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
h, w = self.shape[:2]
|
|
103
|
+
base_level_memory = h * w * self.bytes_per_pixel
|
|
104
|
+
available_memory = self.memory_limit - base_level_memory
|
|
105
|
+
available_memory /= self.overhead
|
|
106
|
+
tile_size_max = int(np.sqrt(available_memory /
|
|
107
|
+
(self.num_threads * self.n_frames * self.bytes_per_pixel)))
|
|
103
108
|
tile_size = min(self.max_tile_size, tile_size_max, self.shape[0], self.shape[1])
|
|
109
|
+
tile_size = max(self.min_tile_size, tile_size)
|
|
104
110
|
n_tiled_layers = 0
|
|
105
111
|
for layer in range(self.n_levels):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if
|
|
112
|
+
h_layer = max(1, self.shape[0] // (2 ** layer))
|
|
113
|
+
w_layer = max(1, self.shape[1] // (2 ** layer))
|
|
114
|
+
if h_layer > tile_size or w_layer > tile_size:
|
|
109
115
|
n_tiled_layers = layer + 1
|
|
110
116
|
else:
|
|
111
117
|
break
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
# pylint: disable=C0114, C0115, C0116, E1101, R0914, R1702, R1732, R0913
|
|
3
3
|
# pylint: disable=R0917, R0912, R0915, R0902, W0718
|
|
4
4
|
import os
|
|
5
|
+
import gc
|
|
5
6
|
import time
|
|
6
7
|
import shutil
|
|
7
8
|
import tempfile
|
|
8
|
-
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
10
|
import numpy as np
|
|
10
11
|
from .. config.constants import constants
|
|
11
12
|
from .. core.exceptions import RunStopException
|
|
@@ -121,13 +122,13 @@ class PyramidTilesStack(PyramidBase):
|
|
|
121
122
|
for x in range(0, w, self.tile_size):
|
|
122
123
|
tiles.append((y, x))
|
|
123
124
|
self.print_message(f': starting parallel propcessging on {self.num_threads} cores')
|
|
124
|
-
with
|
|
125
|
+
with ThreadPoolExecutor(max_workers=self.num_threads) as executor:
|
|
125
126
|
future_to_tile = {
|
|
126
127
|
executor.submit(
|
|
127
128
|
self._process_tile, level, num_images, all_level_counts, y, x, h, w): (y, x)
|
|
128
129
|
for y, x in tiles
|
|
129
130
|
}
|
|
130
|
-
for future in
|
|
131
|
+
for future in as_completed(future_to_tile):
|
|
131
132
|
y, x = future_to_tile[future]
|
|
132
133
|
try:
|
|
133
134
|
fused_tile = future.result()
|
|
@@ -154,7 +155,9 @@ class PyramidTilesStack(PyramidBase):
|
|
|
154
155
|
if laplacians:
|
|
155
156
|
stacked = np.stack(laplacians, axis=0)
|
|
156
157
|
return self.fuse_laplacian(stacked)
|
|
157
|
-
y_end
|
|
158
|
+
y_end = min(y + self.tile_size, h)
|
|
159
|
+
x_end = min(x + self.tile_size, w)
|
|
160
|
+
gc.collect()
|
|
158
161
|
return np.zeros((y_end - y, x_end - x, 3), dtype=self.float_type)
|
|
159
162
|
|
|
160
163
|
def fuse_pyramids(self, all_level_counts, num_images):
|
|
@@ -202,26 +205,29 @@ class PyramidTilesStack(PyramidBase):
|
|
|
202
205
|
self.focus_stack_validate(self.cleanup_temp_files)
|
|
203
206
|
all_level_counts = [0] * n
|
|
204
207
|
if self.num_threads > 1:
|
|
205
|
-
self.print_message(f': starting parallel
|
|
208
|
+
self.print_message(f': starting parallel processing on {self.num_threads} cores')
|
|
206
209
|
args_list = [(file_path, i, n) for i, file_path in enumerate(self.filenames)]
|
|
207
210
|
executor = None
|
|
208
211
|
try:
|
|
209
|
-
executor =
|
|
212
|
+
executor = ThreadPoolExecutor(max_workers=self.num_threads)
|
|
210
213
|
future_to_index = {
|
|
211
214
|
executor.submit(self._process_single_image_wrapper, args): i
|
|
212
215
|
for i, args in enumerate(args_list)
|
|
213
216
|
}
|
|
214
217
|
completed_count = 0
|
|
215
|
-
for future in
|
|
218
|
+
for future in as_completed(future_to_index):
|
|
216
219
|
i = future_to_index[future]
|
|
217
220
|
try:
|
|
218
221
|
img_index, level_count = future.result()
|
|
219
222
|
all_level_counts[img_index] = level_count
|
|
220
223
|
completed_count += 1
|
|
221
|
-
self.print_message(
|
|
224
|
+
self.print_message(
|
|
225
|
+
": processing completed, image "
|
|
226
|
+
f"{self.idx_tot_str(completed_count - 1)}")
|
|
222
227
|
except Exception as e:
|
|
223
|
-
self.print_message(
|
|
224
|
-
|
|
228
|
+
self.print_message(
|
|
229
|
+
f"Error processing image {self.idx_tot_str(i)}: {str(e)}")
|
|
230
|
+
self.after_step(completed_count + n + 1)
|
|
225
231
|
self.check_running(lambda: None)
|
|
226
232
|
except RunStopException:
|
|
227
233
|
self.print_message(": stopping image processing...")
|
|
@@ -235,7 +241,9 @@ class PyramidTilesStack(PyramidBase):
|
|
|
235
241
|
executor.shutdown(wait=True)
|
|
236
242
|
else:
|
|
237
243
|
for i, file_path in enumerate(self.filenames):
|
|
238
|
-
self.print_message(
|
|
244
|
+
self.print_message(
|
|
245
|
+
f": processing file {os.path.basename(file_path)}, "
|
|
246
|
+
f"{self.idx_tot_str(i)}")
|
|
239
247
|
img = read_img(file_path)
|
|
240
248
|
level_count = self.process_single_image(img, self.n_levels, i)
|
|
241
249
|
all_level_counts[i] = level_count
|
shinestacker/algorithms/stack.py
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import numpy as np
|
|
4
4
|
from .. config.constants import constants
|
|
5
|
-
from .. core.framework import
|
|
5
|
+
from .. core.framework import TaskBase
|
|
6
6
|
from .. core.colors import color_str
|
|
7
7
|
from .. core.exceptions import InvalidOptionError
|
|
8
8
|
from .utils import write_img, extension_tif_jpg
|
|
9
|
-
from .stack_framework import
|
|
9
|
+
from .stack_framework import ImageSequenceManager, SequentialTask
|
|
10
10
|
from .exif import copy_exif_from_file_to_file
|
|
11
11
|
from .denoise import denoise
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class FocusStackBase(
|
|
14
|
+
class FocusStackBase(TaskBase, ImageSequenceManager):
|
|
15
15
|
def __init__(self, name, stack_algo, enabled=True, **kwargs):
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
ImageSequenceManager.__init__(self, name, **kwargs)
|
|
17
|
+
TaskBase.__init__(self, name, enabled)
|
|
18
18
|
self.stack_algo = stack_algo
|
|
19
19
|
self.exif_path = kwargs.pop('exif_path', '')
|
|
20
20
|
self.prefix = kwargs.pop('prefix', constants.DEFAULT_STACK_PREFIX)
|
|
@@ -26,9 +26,10 @@ class FocusStackBase(JobBase, FrameDirectory):
|
|
|
26
26
|
def focus_stack(self, filenames):
|
|
27
27
|
self.sub_message_r(color_str(': reading input files', constants.LOG_COLOR_LEVEL_3))
|
|
28
28
|
stacked_img = self.stack_algo.focus_stack()
|
|
29
|
-
in_filename = filenames[0].split(".")
|
|
30
|
-
out_filename =
|
|
31
|
-
|
|
29
|
+
in_filename = os.path.basename(filenames[0]).split(".")
|
|
30
|
+
out_filename = os.path.join(
|
|
31
|
+
self.output_full_path(),
|
|
32
|
+
f"{self.prefix}{in_filename[0]}." + '.'.join(in_filename[1:]))
|
|
32
33
|
if self.denoise_amount > 0:
|
|
33
34
|
self.sub_message_r(': denoise image')
|
|
34
35
|
stacked_img = denoise(stacked_img, self.denoise_amount, self.denoise_amount)
|
|
@@ -45,14 +46,14 @@ class FocusStackBase(JobBase, FrameDirectory):
|
|
|
45
46
|
name = f"{self.name}: {self.stack_algo.name()}"
|
|
46
47
|
if idx_str != '':
|
|
47
48
|
name += f"\nbunch: {idx_str}"
|
|
48
|
-
self.callback(
|
|
49
|
+
self.callback(constants.CALLBACK_SAVE_PLOT, self.id, name, out_filename)
|
|
49
50
|
if self.frame_count >= 0:
|
|
50
51
|
self.frame_count += 1
|
|
51
52
|
|
|
52
53
|
def init(self, job, working_path=''):
|
|
53
|
-
|
|
54
|
+
ImageSequenceManager.init(self, job)
|
|
54
55
|
if self.exif_path is None:
|
|
55
|
-
self.exif_path = job.
|
|
56
|
+
self.exif_path = job.action_path(0)
|
|
56
57
|
if self.exif_path != '':
|
|
57
58
|
self.exif_path = working_path + "/" + self.exif_path
|
|
58
59
|
|
|
@@ -63,9 +64,9 @@ def get_bunches(collection, n_frames, n_overlap):
|
|
|
63
64
|
return bunches
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
class FocusStackBunch(
|
|
67
|
+
class FocusStackBunch(SequentialTask, FocusStackBase):
|
|
67
68
|
def __init__(self, name, stack_algo, enabled=True, **kwargs):
|
|
68
|
-
|
|
69
|
+
SequentialTask.__init__(self, name, enabled)
|
|
69
70
|
FocusStackBase.__init__(self, name, stack_algo, enabled, **kwargs)
|
|
70
71
|
self._chunks = None
|
|
71
72
|
self.frame_count = 0
|
|
@@ -77,25 +78,28 @@ class FocusStackBunch(ActionList, FocusStackBase):
|
|
|
77
78
|
raise InvalidOptionError("overlap", self.overlap,
|
|
78
79
|
"overlap must be smaller than batch size")
|
|
79
80
|
|
|
81
|
+
def sequential_processing(self):
|
|
82
|
+
return True
|
|
83
|
+
|
|
80
84
|
def init(self, job, _working_path=''):
|
|
81
85
|
FocusStackBase.init(self, job, self.working_path)
|
|
82
86
|
|
|
83
87
|
def begin(self):
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
self._chunks = get_bunches(fnames, self.frames, self.overlap)
|
|
88
|
+
SequentialTask.begin(self)
|
|
89
|
+
self._chunks = get_bunches(self.input_filepaths(), self.frames, self.overlap)
|
|
87
90
|
self.set_counts(len(self._chunks))
|
|
88
91
|
|
|
89
92
|
def end(self):
|
|
90
|
-
|
|
93
|
+
SequentialTask.end(self)
|
|
91
94
|
|
|
92
|
-
def run_step(self):
|
|
93
|
-
self.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
def run_step(self, action_count=-1):
|
|
96
|
+
self.print_message(
|
|
97
|
+
color_str(f"fusing bunch: {action_count + 1}/{self.total_action_counts}",
|
|
98
|
+
constants.LOG_COLOR_LEVEL_2))
|
|
99
|
+
img_files = self._chunks[action_count - 1]
|
|
97
100
|
self.stack_algo.init(img_files)
|
|
98
|
-
self.focus_stack(self._chunks[
|
|
101
|
+
self.focus_stack(self._chunks[action_count - 1])
|
|
102
|
+
return True
|
|
99
103
|
|
|
100
104
|
|
|
101
105
|
class FocusStack(FocusStackBase):
|
|
@@ -106,11 +110,11 @@ class FocusStack(FocusStackBase):
|
|
|
106
110
|
|
|
107
111
|
def run_core(self):
|
|
108
112
|
self.set_filelist()
|
|
109
|
-
img_files = sorted(
|
|
113
|
+
img_files = sorted(self.input_filepaths())
|
|
110
114
|
self.stack_algo.init(img_files)
|
|
111
115
|
self.callback('step_counts', self.id, self.name,
|
|
112
|
-
self.stack_algo.total_steps(
|
|
113
|
-
self.focus_stack(
|
|
116
|
+
self.stack_algo.total_steps(self.num_input_filepaths()))
|
|
117
|
+
self.focus_stack(img_files)
|
|
114
118
|
|
|
115
119
|
def init(self, job, _working_path=''):
|
|
116
120
|
FocusStackBase.init(self, job, self.working_path)
|