shinestacker 1.2.1__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 +126 -94
- shinestacker/algorithms/align_auto.py +64 -0
- shinestacker/algorithms/align_parallel.py +296 -0
- shinestacker/algorithms/balance.py +3 -1
- shinestacker/algorithms/base_stack_algo.py +11 -2
- shinestacker/algorithms/multilayer.py +8 -8
- shinestacker/algorithms/noise_detection.py +10 -10
- shinestacker/algorithms/pyramid.py +4 -4
- shinestacker/algorithms/pyramid_auto.py +16 -10
- shinestacker/algorithms/pyramid_tiles.py +19 -11
- shinestacker/algorithms/stack.py +21 -17
- shinestacker/algorithms/stack_framework.py +97 -46
- shinestacker/algorithms/vignetting.py +13 -10
- shinestacker/app/main.py +7 -3
- shinestacker/config/constants.py +60 -25
- shinestacker/config/gui_constants.py +1 -1
- shinestacker/core/core_utils.py +4 -0
- shinestacker/core/framework.py +104 -23
- shinestacker/gui/action_config.py +4 -5
- shinestacker/gui/action_config_dialog.py +152 -12
- shinestacker/gui/base_form_dialog.py +2 -2
- shinestacker/gui/folder_file_selection.py +101 -0
- shinestacker/gui/gui_run.py +12 -10
- shinestacker/gui/main_window.py +6 -1
- shinestacker/gui/new_project.py +171 -73
- shinestacker/gui/project_controller.py +10 -6
- shinestacker/gui/project_converter.py +4 -2
- shinestacker/gui/project_editor.py +37 -27
- shinestacker/gui/select_path_widget.py +1 -1
- shinestacker/gui/sys_mon.py +96 -0
- 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.1.dist-info → shinestacker-1.3.0.dist-info}/METADATA +6 -6
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/RECORD +40 -36
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/WHEEL +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -609,6 +609,8 @@ class BalanceFrames(SubAction):
|
|
|
609
609
|
|
|
610
610
|
def run_frame(self, idx, _ref_idx, image):
|
|
611
611
|
if idx != self.process.ref_idx:
|
|
612
|
-
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))
|
|
613
615
|
image = self.correction.apply_correction(idx, image)
|
|
614
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,9 +170,9 @@ class MultiLayer(JobBase, FramePaths):
|
|
|
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
|
|
|
@@ -217,4 +217,4 @@ class MultiLayer(JobBase, FramePaths):
|
|
|
217
217
|
write_multilayer_tiff(input_files, output_file, labels=None, exif_path=self.exif_path,
|
|
218
218
|
callbacks=callbacks)
|
|
219
219
|
app = 'internal_retouch_app' if config.COMBINED_APP else f'{constants.RETOUCH_APP}'
|
|
220
|
-
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, FramePaths):
|
|
|
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):
|
|
@@ -78,13 +78,13 @@ class NoiseDetection(JobBase, FramePaths):
|
|
|
78
78
|
))
|
|
79
79
|
in_paths = self.input_filepaths()
|
|
80
80
|
n_frames = min(len(in_paths), self.max_frames) if self.max_frames > 0 else len(in_paths)
|
|
81
|
-
self.callback(
|
|
81
|
+
self.callback(constants.CALLBACK_STEP_COUNTS, self.id, self.name, n_frames)
|
|
82
82
|
if not config.DISABLE_TQDM:
|
|
83
83
|
self.tbar = make_tqdm_bar(self.name, n_frames)
|
|
84
84
|
|
|
85
85
|
def progress_callback(i):
|
|
86
86
|
self.progress(i)
|
|
87
|
-
if self.callback(
|
|
87
|
+
if self.callback(constants.CALLBACK_CHECK_RUNNING, self.id, self.name) is False:
|
|
88
88
|
raise RunStopException(self.name)
|
|
89
89
|
mean_img = mean_image(
|
|
90
90
|
file_paths=in_paths, max_frames=self.max_frames,
|
|
@@ -137,7 +137,7 @@ class NoiseDetection(JobBase, FramePaths):
|
|
|
137
137
|
plt.ylim(0)
|
|
138
138
|
plot_path = f"{self.working_path}/{self.plot_path}/{self.name}-hot-pixels.pdf"
|
|
139
139
|
save_plot(plot_path)
|
|
140
|
-
self.callback(
|
|
140
|
+
self.callback(constants.CALLBACK_SAVE_PLOT, self.id, f"{self.name}: noise", plot_path)
|
|
141
141
|
plt.close('all')
|
|
142
142
|
|
|
143
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)
|
|
@@ -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)
|
|
@@ -46,14 +46,14 @@ class FocusStackBase(JobBase, FramePaths):
|
|
|
46
46
|
name = f"{self.name}: {self.stack_algo.name()}"
|
|
47
47
|
if idx_str != '':
|
|
48
48
|
name += f"\nbunch: {idx_str}"
|
|
49
|
-
self.callback(
|
|
49
|
+
self.callback(constants.CALLBACK_SAVE_PLOT, self.id, name, out_filename)
|
|
50
50
|
if self.frame_count >= 0:
|
|
51
51
|
self.frame_count += 1
|
|
52
52
|
|
|
53
53
|
def init(self, job, working_path=''):
|
|
54
|
-
|
|
54
|
+
ImageSequenceManager.init(self, job)
|
|
55
55
|
if self.exif_path is None:
|
|
56
|
-
self.exif_path = job.
|
|
56
|
+
self.exif_path = job.action_path(0)
|
|
57
57
|
if self.exif_path != '':
|
|
58
58
|
self.exif_path = working_path + "/" + self.exif_path
|
|
59
59
|
|
|
@@ -64,9 +64,9 @@ def get_bunches(collection, n_frames, n_overlap):
|
|
|
64
64
|
return bunches
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
class FocusStackBunch(
|
|
67
|
+
class FocusStackBunch(SequentialTask, FocusStackBase):
|
|
68
68
|
def __init__(self, name, stack_algo, enabled=True, **kwargs):
|
|
69
|
-
|
|
69
|
+
SequentialTask.__init__(self, name, enabled)
|
|
70
70
|
FocusStackBase.__init__(self, name, stack_algo, enabled, **kwargs)
|
|
71
71
|
self._chunks = None
|
|
72
72
|
self.frame_count = 0
|
|
@@ -78,24 +78,28 @@ class FocusStackBunch(ActionList, FocusStackBase):
|
|
|
78
78
|
raise InvalidOptionError("overlap", self.overlap,
|
|
79
79
|
"overlap must be smaller than batch size")
|
|
80
80
|
|
|
81
|
+
def sequential_processing(self):
|
|
82
|
+
return True
|
|
83
|
+
|
|
81
84
|
def init(self, job, _working_path=''):
|
|
82
85
|
FocusStackBase.init(self, job, self.working_path)
|
|
83
86
|
|
|
84
87
|
def begin(self):
|
|
85
|
-
|
|
88
|
+
SequentialTask.begin(self)
|
|
86
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
|
-
color_str(f"fusing bunch: {
|
|
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}",
|
|
95
98
|
constants.LOG_COLOR_LEVEL_2))
|
|
96
|
-
img_files = self._chunks[
|
|
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):
|
|
@@ -1,30 +1,49 @@
|
|
|
1
|
-
# pylint: disable=C0114, C0115, C0116, W0102, R0902, R0903
|
|
1
|
+
# pylint: disable=C0114, C0115, C0116, W0102, R0902, R0903, E1128
|
|
2
2
|
# pylint: disable=R0917, R0913, R1702, R0912, E1111, E1121, W0613
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
5
|
from .. config.constants import constants
|
|
6
6
|
from .. core.colors import color_str
|
|
7
|
-
from .. core.framework import Job,
|
|
7
|
+
from .. core.framework import Job, SequentialTask
|
|
8
8
|
from .. core.core_utils import check_path_exists
|
|
9
9
|
from .. core.exceptions import RunStopException
|
|
10
10
|
from .utils import read_img, write_img, extension_tif_jpg, get_img_metadata, validate_image
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class StackJob(Job):
|
|
14
|
-
def __init__(self, name, working_path, input_path='', **kwargs):
|
|
14
|
+
def __init__(self, name, working_path, input_path='', input_filepaths=[], **kwargs):
|
|
15
15
|
check_path_exists(working_path)
|
|
16
16
|
self.working_path = working_path
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
self._input_path = input_path
|
|
18
|
+
self._action_paths = [] if input_path == '' else [input_path]
|
|
19
|
+
self._input_filepaths = []
|
|
20
|
+
self._input_full_path = None
|
|
21
|
+
self._input_filepaths = input_filepaths
|
|
21
22
|
Job.__init__(self, name, **kwargs)
|
|
22
23
|
|
|
23
24
|
def init(self, a):
|
|
24
25
|
a.init(self)
|
|
25
26
|
|
|
27
|
+
def input_filepaths(self):
|
|
28
|
+
return self._input_filepaths
|
|
29
|
+
|
|
30
|
+
def num_input_filepaths(self):
|
|
31
|
+
return len(self._input_filepaths)
|
|
32
|
+
|
|
33
|
+
def action_paths(self):
|
|
34
|
+
return self._action_paths
|
|
35
|
+
|
|
36
|
+
def add_action_path(self, path):
|
|
37
|
+
self._action_paths.append(path)
|
|
38
|
+
|
|
39
|
+
def num_action_paths(self):
|
|
40
|
+
return len(self._action_paths)
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
def action_path(self, i):
|
|
43
|
+
return self._action_paths[i]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ImageSequenceManager:
|
|
28
47
|
def __init__(self, name, input_path='', output_path='', working_path='',
|
|
29
48
|
plot_path=constants.DEFAULT_PLOTS_PATH,
|
|
30
49
|
scratch_output_dir=True, resample=1,
|
|
@@ -34,7 +53,7 @@ class FramePaths:
|
|
|
34
53
|
self.plot_path = plot_path
|
|
35
54
|
self.input_path = input_path
|
|
36
55
|
self.output_path = self.name if output_path == '' else output_path
|
|
37
|
-
self.
|
|
56
|
+
self._resample = resample
|
|
38
57
|
self.reverse_order = reverse_order
|
|
39
58
|
self.scratch_output_dir = scratch_output_dir
|
|
40
59
|
self.enabled = None
|
|
@@ -78,8 +97,8 @@ class FramePaths:
|
|
|
78
97
|
filelist.sort()
|
|
79
98
|
if self.reverse_order:
|
|
80
99
|
filelist.reverse()
|
|
81
|
-
if self.
|
|
82
|
-
filelist = filelist[0::self.
|
|
100
|
+
if self._resample > 1:
|
|
101
|
+
filelist = filelist[0::self._resample]
|
|
83
102
|
files += filelist
|
|
84
103
|
if len(files) == 0:
|
|
85
104
|
self.print_message(color_str(f"input folder {d} does not contain any image",
|
|
@@ -138,10 +157,16 @@ class FramePaths:
|
|
|
138
157
|
if not os.path.exists(self.plot_path):
|
|
139
158
|
os.makedirs(self.plot_path)
|
|
140
159
|
if self.input_path in ['', []]:
|
|
141
|
-
if
|
|
160
|
+
if job.num_action_paths() == 0:
|
|
142
161
|
raise RuntimeError(f"Job {job.name} does not have any configured path")
|
|
143
|
-
self.input_path = job.
|
|
144
|
-
|
|
162
|
+
self.input_path = job.action_path(-1)
|
|
163
|
+
if job.num_input_filepaths() > 0:
|
|
164
|
+
self._input_filepaths = []
|
|
165
|
+
for filepath in job.input_filepaths():
|
|
166
|
+
if not os.path.isabs(filepath):
|
|
167
|
+
filepath = os.path.join(self.input_full_path(), filepath)
|
|
168
|
+
self._input_filepaths.append(filepath)
|
|
169
|
+
job.add_action_path(self.output_path)
|
|
145
170
|
|
|
146
171
|
def folder_list_str(self):
|
|
147
172
|
if isinstance(self.input_full_path(), list):
|
|
@@ -152,10 +177,10 @@ class FramePaths:
|
|
|
152
177
|
return "folder: " + self.input_full_path().replace(self.working_path, '').lstrip('/')
|
|
153
178
|
|
|
154
179
|
|
|
155
|
-
class
|
|
180
|
+
class ReferenceFrameTask(SequentialTask, ImageSequenceManager):
|
|
156
181
|
def __init__(self, name, enabled=True, reference_index=0, step_process=False, **kwargs):
|
|
157
|
-
|
|
158
|
-
|
|
182
|
+
ImageSequenceManager.__init__(self, name, **kwargs)
|
|
183
|
+
SequentialTask.__init__(self, name, enabled)
|
|
159
184
|
self.ref_idx = reference_index
|
|
160
185
|
self.step_process = step_process
|
|
161
186
|
self.current_idx = None
|
|
@@ -163,7 +188,7 @@ class FramesRefActions(ActionList, FramePaths):
|
|
|
163
188
|
self.current_idx_step = None
|
|
164
189
|
|
|
165
190
|
def begin(self):
|
|
166
|
-
|
|
191
|
+
SequentialTask.begin(self)
|
|
167
192
|
self.set_filelist()
|
|
168
193
|
n = self.num_input_filepaths()
|
|
169
194
|
self.set_counts(n)
|
|
@@ -179,33 +204,44 @@ class FramesRefActions(ActionList, FramePaths):
|
|
|
179
204
|
raise IndexError(msg)
|
|
180
205
|
|
|
181
206
|
def end(self):
|
|
182
|
-
|
|
207
|
+
SequentialTask.end(self)
|
|
183
208
|
|
|
184
209
|
def run_frame(self, _idx, _ref_idx):
|
|
185
210
|
return None
|
|
186
211
|
|
|
187
|
-
def run_step(self):
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
ll = self.num_input_filepaths()
|
|
193
|
-
self.print_message_r(
|
|
194
|
-
color_str(f"step {self.current_action_count + 1}/{ll}: process file: "
|
|
195
|
-
f"{os.path.basename(self.input_filepath(self.current_idx))}, "
|
|
196
|
-
f"reference: {os.path.basename(self.input_filepath(self.current_ref_idx))}",
|
|
197
|
-
constants.LOG_COLOR_LEVEL_2))
|
|
198
|
-
self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
|
|
199
|
-
success = self.run_frame(self.current_idx, self.current_ref_idx) is not None
|
|
200
|
-
if self.current_idx < ll:
|
|
201
|
-
if self.step_process and success:
|
|
202
|
-
self.current_ref_idx = self.current_idx
|
|
203
|
-
self.current_idx += self.current_idx_step
|
|
204
|
-
if self.current_idx == ll:
|
|
205
|
-
self.current_idx = self.ref_idx - 1
|
|
206
|
-
if self.step_process:
|
|
212
|
+
def run_step(self, action_count=-1):
|
|
213
|
+
num_files = self.num_input_filepaths()
|
|
214
|
+
if self.run_sequential():
|
|
215
|
+
if action_count == 0:
|
|
216
|
+
self.current_idx = self.ref_idx if self.step_process else 0
|
|
207
217
|
self.current_ref_idx = self.ref_idx
|
|
208
|
-
|
|
218
|
+
self.current_idx_step = +1
|
|
219
|
+
idx, ref_idx = self.current_idx, self.current_ref_idx
|
|
220
|
+
self.print_message_r(
|
|
221
|
+
color_str(f"step {action_count + 1}/{num_files}: process file: "
|
|
222
|
+
f"{os.path.basename(self.input_filepath(idx))}, "
|
|
223
|
+
f"reference: "
|
|
224
|
+
f"{os.path.basename(self.input_filepath(self.current_ref_idx))}",
|
|
225
|
+
constants.LOG_COLOR_LEVEL_2))
|
|
226
|
+
else:
|
|
227
|
+
idx, ref_idx = action_count, -1
|
|
228
|
+
self.print_message_r(
|
|
229
|
+
color_str(f"step {idx + 1}/{num_files}: process file: "
|
|
230
|
+
f"{os.path.basename(self.input_filepath(idx))}, "
|
|
231
|
+
"parallel thread", constants.LOG_COLOR_LEVEL_2))
|
|
232
|
+
self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
|
|
233
|
+
img = self.run_frame(idx, ref_idx)
|
|
234
|
+
if self.run_sequential():
|
|
235
|
+
if self.current_idx < num_files:
|
|
236
|
+
if self.step_process and img is not None:
|
|
237
|
+
self.current_ref_idx = self.current_idx
|
|
238
|
+
self.current_idx += self.current_idx_step
|
|
239
|
+
if self.current_idx == num_files:
|
|
240
|
+
self.current_idx = self.ref_idx - 1
|
|
241
|
+
if self.step_process:
|
|
242
|
+
self.current_ref_idx = self.ref_idx
|
|
243
|
+
self.current_idx_step = -1
|
|
244
|
+
return img is not None
|
|
209
245
|
|
|
210
246
|
|
|
211
247
|
class SubAction:
|
|
@@ -218,15 +254,18 @@ class SubAction:
|
|
|
218
254
|
def end(self):
|
|
219
255
|
pass
|
|
220
256
|
|
|
257
|
+
def sequential_processing(self):
|
|
258
|
+
return False
|
|
259
|
+
|
|
221
260
|
|
|
222
|
-
class CombinedActions(
|
|
261
|
+
class CombinedActions(ReferenceFrameTask):
|
|
223
262
|
def __init__(self, name, actions=[], enabled=True, **kwargs):
|
|
224
|
-
|
|
263
|
+
ReferenceFrameTask.__init__(self, name, enabled, **kwargs)
|
|
225
264
|
self._actions = actions
|
|
226
265
|
self._metadata = (None, None)
|
|
227
266
|
|
|
228
267
|
def begin(self):
|
|
229
|
-
|
|
268
|
+
ReferenceFrameTask.begin(self)
|
|
230
269
|
for a in self._actions:
|
|
231
270
|
if a.enabled:
|
|
232
271
|
a.begin(self)
|
|
@@ -241,7 +280,10 @@ class CombinedActions(FramesRefActions):
|
|
|
241
280
|
|
|
242
281
|
def run_frame(self, idx, ref_idx):
|
|
243
282
|
input_path = self.input_filepath(idx)
|
|
244
|
-
self.
|
|
283
|
+
self.print_message(
|
|
284
|
+
color_str(f'read input image '
|
|
285
|
+
f'{idx + 1}/{self.total_action_counts}, '
|
|
286
|
+
f'{os.path.basename(input_path)}', constants.LOG_COLOR_LEVEL_3))
|
|
245
287
|
img = read_img(input_path)
|
|
246
288
|
validate_image(img, *(self._metadata))
|
|
247
289
|
if img is None:
|
|
@@ -254,7 +296,7 @@ class CombinedActions(FramesRefActions):
|
|
|
254
296
|
self.get_logger().warning(color_str(f"{self.base_message}: sub-action disabled",
|
|
255
297
|
constants.LOG_COLOR_ALERT))
|
|
256
298
|
else:
|
|
257
|
-
if self.callback(
|
|
299
|
+
if self.callback(constants.CALLBACK_CHECK_RUNNING, self.id, self.name) is False:
|
|
258
300
|
raise RunStopException(self.name)
|
|
259
301
|
if img is not None:
|
|
260
302
|
img = a.run_frame(idx, ref_idx, img)
|
|
@@ -264,8 +306,11 @@ class CombinedActions(FramesRefActions):
|
|
|
264
306
|
constants.LOG_COLOR_ALERT),
|
|
265
307
|
level=logging.WARNING)
|
|
266
308
|
if img is not None:
|
|
267
|
-
self.sub_message_r(color_str(': write output image', constants.LOG_COLOR_LEVEL_3))
|
|
268
309
|
output_path = os.path.join(self.output_full_path(), os.path.basename(input_path))
|
|
310
|
+
self.print_message(
|
|
311
|
+
color_str(f'write output image '
|
|
312
|
+
f'{idx + 1}/{self.total_action_counts}, '
|
|
313
|
+
f'{os.path.basename(output_path)}', constants.LOG_COLOR_LEVEL_3))
|
|
269
314
|
write_img(output_path, img)
|
|
270
315
|
return img
|
|
271
316
|
self.print_message(color_str(
|
|
@@ -277,3 +322,9 @@ class CombinedActions(FramesRefActions):
|
|
|
277
322
|
for a in self._actions:
|
|
278
323
|
if a.enabled:
|
|
279
324
|
a.end()
|
|
325
|
+
|
|
326
|
+
def sequential_processing(self):
|
|
327
|
+
for a in self._actions:
|
|
328
|
+
if a.sequential_processing():
|
|
329
|
+
return True
|
|
330
|
+
return False
|