shinestacker 1.2.1__py3-none-any.whl → 1.3.1__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 +152 -112
- shinestacker/algorithms/align_auto.py +76 -0
- shinestacker/algorithms/align_parallel.py +336 -0
- shinestacker/algorithms/balance.py +3 -1
- shinestacker/algorithms/base_stack_algo.py +25 -22
- shinestacker/algorithms/depth_map.py +9 -14
- shinestacker/algorithms/multilayer.py +8 -8
- shinestacker/algorithms/noise_detection.py +10 -10
- shinestacker/algorithms/pyramid.py +10 -24
- shinestacker/algorithms/pyramid_auto.py +21 -24
- shinestacker/algorithms/pyramid_tiles.py +31 -25
- shinestacker/algorithms/stack.py +21 -17
- shinestacker/algorithms/stack_framework.py +98 -47
- shinestacker/algorithms/utils.py +16 -0
- shinestacker/algorithms/vignetting.py +13 -10
- shinestacker/app/gui_utils.py +10 -0
- shinestacker/app/main.py +10 -4
- shinestacker/app/project.py +3 -1
- shinestacker/app/retouch.py +3 -1
- 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 +409 -239
- shinestacker/gui/base_form_dialog.py +2 -2
- shinestacker/gui/colors.py +1 -0
- shinestacker/gui/folder_file_selection.py +106 -0
- shinestacker/gui/gui_run.py +12 -10
- shinestacker/gui/main_window.py +10 -5
- 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 +40 -28
- shinestacker/gui/select_path_widget.py +1 -1
- shinestacker/gui/sys_mon.py +97 -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.1.dist-info}/METADATA +6 -6
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/RECORD +46 -42
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/WHEEL +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/entry_points.txt +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {shinestacker-1.2.1.dist-info → shinestacker-1.3.1.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import numpy as np
|
|
4
4
|
from .. config.constants import constants
|
|
5
|
-
from .utils import extension_tif_jpg
|
|
6
5
|
from .base_stack_algo import BaseStackAlgo
|
|
7
6
|
from .pyramid import PyramidStack
|
|
8
7
|
from .pyramid_tiles import PyramidTilesStack
|
|
@@ -17,10 +16,11 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
17
16
|
n_tiled_layers=constants.DEFAULT_PY_N_TILED_LAYERS,
|
|
18
17
|
memory_limit=constants.DEFAULT_PY_MEMORY_LIMIT_GB,
|
|
19
18
|
max_threads=constants.DEFAULT_PY_MAX_THREADS,
|
|
20
|
-
max_tile_size=
|
|
21
|
-
|
|
19
|
+
max_tile_size=constants.DEFAULT_PY_MAX_TILE_SIZE,
|
|
20
|
+
min_tile_size=constants.DEFAULT_PY_MIN_TILE_SIZE,
|
|
21
|
+
min_n_tiled_layers=constants.DEFAULT_PY_MIN_N_TILED_LAYERS,
|
|
22
22
|
mode='auto'):
|
|
23
|
-
super().__init__("auto_pyramid",
|
|
23
|
+
super().__init__("auto_pyramid", 1, float_type)
|
|
24
24
|
self.min_size = min_size
|
|
25
25
|
self.kernel_size = kernel_size
|
|
26
26
|
self.gen_kernel = gen_kernel
|
|
@@ -32,6 +32,7 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
32
32
|
available_cores = os.cpu_count() or 1
|
|
33
33
|
self.num_threads = min(max_threads, available_cores)
|
|
34
34
|
self.max_tile_size = max_tile_size
|
|
35
|
+
self.min_tile_size = min_tile_size
|
|
35
36
|
self.min_n_tiled_layers = min_n_tiled_layers
|
|
36
37
|
self.mode = mode
|
|
37
38
|
self._implementation = None
|
|
@@ -39,21 +40,13 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
39
40
|
self.shape = None
|
|
40
41
|
self.n_levels = None
|
|
41
42
|
self.n_frames = 0
|
|
42
|
-
self.channels = 3
|
|
43
|
+
self.channels = 3 # r, g, b
|
|
43
44
|
dtype = np.float32 if self.float_type == constants.FLOAT_32 else np.float64
|
|
44
45
|
self.bytes_per_pixel = self.channels * np.dtype(dtype).itemsize
|
|
45
|
-
self.overhead =
|
|
46
|
+
self.overhead = constants.PY_MEMORY_OVERHEAD
|
|
46
47
|
|
|
47
48
|
def init(self, filenames):
|
|
48
|
-
|
|
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
|
|
49
|
+
super().init(filenames)
|
|
57
50
|
self.n_levels = int(np.log2(min(self.shape) / self.min_size))
|
|
58
51
|
self.n_frames = len(filenames)
|
|
59
52
|
memory_required_memory = self._estimate_memory_memory()
|
|
@@ -77,9 +70,9 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
77
70
|
n_tiled_layers=optimal_params['n_tiled_layers'],
|
|
78
71
|
max_threads=self.num_threads
|
|
79
72
|
)
|
|
80
|
-
self.print_message(f": using tile-based pyramid stacking "
|
|
81
|
-
f"
|
|
82
|
-
f"
|
|
73
|
+
self.print_message(f": using tile-based pyramid stacking, "
|
|
74
|
+
f"tile size: {optimal_params['tile_size']}, "
|
|
75
|
+
f"n. tiled layers: {optimal_params['n_tiled_layers']}, "
|
|
83
76
|
f"{self.num_threads} cores.")
|
|
84
77
|
self._implementation.init(filenames)
|
|
85
78
|
self._implementation.set_do_step_callback(self.do_step_callback)
|
|
@@ -97,15 +90,19 @@ class PyramidAutoStack(BaseStackAlgo):
|
|
|
97
90
|
return self.overhead * total_memory * self.n_frames
|
|
98
91
|
|
|
99
92
|
def _find_optimal_tile_params(self):
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
93
|
+
h, w = self.shape[:2]
|
|
94
|
+
base_level_memory = h * w * self.bytes_per_pixel
|
|
95
|
+
available_memory = self.memory_limit - base_level_memory
|
|
96
|
+
available_memory /= self.overhead
|
|
97
|
+
tile_size_max = int(np.sqrt(available_memory /
|
|
98
|
+
(self.num_threads * self.n_frames * self.bytes_per_pixel)))
|
|
103
99
|
tile_size = min(self.max_tile_size, tile_size_max, self.shape[0], self.shape[1])
|
|
100
|
+
tile_size = max(self.min_tile_size, tile_size)
|
|
104
101
|
n_tiled_layers = 0
|
|
105
102
|
for layer in range(self.n_levels):
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if
|
|
103
|
+
h_layer = max(1, self.shape[0] // (2 ** layer))
|
|
104
|
+
w_layer = max(1, self.shape[1] // (2 ** layer))
|
|
105
|
+
if h_layer > tile_size or w_layer > tile_size:
|
|
109
106
|
n_tiled_layers = layer + 1
|
|
110
107
|
else:
|
|
111
108
|
break
|
|
@@ -2,14 +2,15 @@
|
|
|
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
|
|
12
|
-
from .utils import read_img
|
|
13
|
+
from .utils import read_img, read_and_validate_img
|
|
13
14
|
from .pyramid import PyramidBase
|
|
14
15
|
|
|
15
16
|
|
|
@@ -46,11 +47,11 @@ class PyramidTilesStack(PyramidBase):
|
|
|
46
47
|
return n_steps + self.n_tiles
|
|
47
48
|
|
|
48
49
|
def _process_single_image_wrapper(self, args):
|
|
49
|
-
img_path,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
level_count = self.process_single_image(img, self.n_levels,
|
|
53
|
-
return
|
|
50
|
+
img_path, idx, _n = args
|
|
51
|
+
img = read_and_validate_img(img_path, self.shape, self.dtype)
|
|
52
|
+
self.check_running(self.cleanup_temp_files)
|
|
53
|
+
level_count = self.process_single_image(img, self.n_levels, idx)
|
|
54
|
+
return idx, level_count
|
|
54
55
|
|
|
55
56
|
def process_single_image(self, img, levels, img_index):
|
|
56
57
|
laplacian = self.single_image_laplacian(img, levels)
|
|
@@ -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,13 +155,16 @@ 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
|
-
def fuse_pyramids(self, all_level_counts
|
|
163
|
+
def fuse_pyramids(self, all_level_counts):
|
|
164
|
+
num_images = self.num_images()
|
|
161
165
|
max_levels = max(all_level_counts)
|
|
162
166
|
fused = []
|
|
163
|
-
count =
|
|
167
|
+
count = super().total_steps(num_images)
|
|
164
168
|
for level in range(max_levels - 1, -1, -1):
|
|
165
169
|
self.print_message(f': fusing pyramids, layer: {level + 1}')
|
|
166
170
|
if level < self.n_tiled_layers:
|
|
@@ -198,30 +202,31 @@ class PyramidTilesStack(PyramidBase):
|
|
|
198
202
|
return fused[::-1]
|
|
199
203
|
|
|
200
204
|
def focus_stack(self):
|
|
201
|
-
|
|
202
|
-
self.focus_stack_validate(self.cleanup_temp_files)
|
|
203
|
-
all_level_counts = [0] * n
|
|
205
|
+
all_level_counts = [0] * self.num_images()
|
|
204
206
|
if self.num_threads > 1:
|
|
205
|
-
self.print_message(f': starting parallel
|
|
206
|
-
args_list = [(file_path, i,
|
|
207
|
+
self.print_message(f': starting parallel processing on {self.num_threads} cores')
|
|
208
|
+
args_list = [(file_path, i, self.num_images())
|
|
209
|
+
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
|
+
f": processing completed, {self.image_str(completed_count - 1)}")
|
|
222
226
|
except Exception as e:
|
|
223
|
-
self.print_message(
|
|
224
|
-
|
|
227
|
+
self.print_message(
|
|
228
|
+
f"Error processing {self.image_str(i)}: {str(e)}")
|
|
229
|
+
self.after_step(completed_count)
|
|
225
230
|
self.check_running(lambda: None)
|
|
226
231
|
except RunStopException:
|
|
227
232
|
self.print_message(": stopping image processing...")
|
|
@@ -235,15 +240,16 @@ class PyramidTilesStack(PyramidBase):
|
|
|
235
240
|
executor.shutdown(wait=True)
|
|
236
241
|
else:
|
|
237
242
|
for i, file_path in enumerate(self.filenames):
|
|
238
|
-
self.print_message(
|
|
243
|
+
self.print_message(
|
|
244
|
+
f": processing {self.image_str(i)}")
|
|
239
245
|
img = read_img(file_path)
|
|
240
246
|
level_count = self.process_single_image(img, self.n_levels, i)
|
|
241
247
|
all_level_counts[i] = level_count
|
|
242
|
-
self.after_step(i +
|
|
248
|
+
self.after_step(i + 1)
|
|
243
249
|
self.check_running(lambda: None)
|
|
244
250
|
try:
|
|
245
251
|
self.check_running(lambda: None)
|
|
246
|
-
fused_pyramid = self.fuse_pyramids(all_level_counts
|
|
252
|
+
fused_pyramid = self.fuse_pyramids(all_level_counts)
|
|
247
253
|
stacked_image = self.collapse(fused_pyramid)
|
|
248
254
|
return stacked_image.astype(self.dtype)
|
|
249
255
|
except RunStopException:
|
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",
|
|
@@ -98,7 +117,7 @@ class FramePaths:
|
|
|
98
117
|
assert False, "this method should be overwritten"
|
|
99
118
|
|
|
100
119
|
def set_filelist(self):
|
|
101
|
-
file_folder = self.input_full_path()
|
|
120
|
+
file_folder = os.path.relpath(self.input_full_path(), self.working_path)
|
|
102
121
|
self.print_message(color_str(f"{self.num_input_filepaths()} files in folder: {file_folder}",
|
|
103
122
|
constants.LOG_COLOR_LEVEL_2))
|
|
104
123
|
self.base_message = color_str(self.name, constants.LOG_COLOR_LEVEL_1, "bold")
|
|
@@ -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
|
shinestacker/algorithms/utils.py
CHANGED
|
@@ -87,6 +87,17 @@ def img_bw(img):
|
|
|
87
87
|
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
def get_first_image_file(filenames):
|
|
91
|
+
first_img_file = None
|
|
92
|
+
for filename in filenames:
|
|
93
|
+
if os.path.isfile(filename) and extension_tif_jpg(filename):
|
|
94
|
+
first_img_file = filename
|
|
95
|
+
break
|
|
96
|
+
if first_img_file is None:
|
|
97
|
+
raise ValueError("No valid image files found")
|
|
98
|
+
return first_img_file
|
|
99
|
+
|
|
100
|
+
|
|
90
101
|
def get_img_file_shape(file_path):
|
|
91
102
|
img = read_img(file_path)
|
|
92
103
|
return img.shape[:2]
|
|
@@ -106,6 +117,11 @@ def validate_image(img, expected_shape=None, expected_dtype=None):
|
|
|
106
117
|
raise ShapeError(expected_shape, shape)
|
|
107
118
|
if expected_dtype and dtype != expected_dtype:
|
|
108
119
|
raise BitDepthError(expected_dtype, dtype)
|
|
120
|
+
return img
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def read_and_validate_img(filename, expected_shape=None, expected_dtype=None):
|
|
124
|
+
return validate_image(read_img(filename), expected_shape, expected_dtype)
|
|
109
125
|
|
|
110
126
|
|
|
111
127
|
def save_plot(filename):
|
|
@@ -134,7 +134,8 @@ class Vignetting(SubAction):
|
|
|
134
134
|
self.corrections = None
|
|
135
135
|
|
|
136
136
|
def run_frame(self, idx, _ref_idx, img_0):
|
|
137
|
-
self.process.
|
|
137
|
+
self.process.print_message(
|
|
138
|
+
color_str(f"{self.process.idx_tot_str(idx)}: compute vignetting", "cyan"))
|
|
138
139
|
h, w = img_0.shape[:2]
|
|
139
140
|
self.w_2, self.h_2 = w / 2, h / 2
|
|
140
141
|
self.r_max = np.sqrt((w / 2)**2 + (h / 2)**2)
|
|
@@ -153,12 +154,13 @@ class Vignetting(SubAction):
|
|
|
153
154
|
return img_0
|
|
154
155
|
self.v0 = sigmoid_model(0, *params)
|
|
155
156
|
i0_fit, k_fit, r0_fit = params
|
|
156
|
-
self.process.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
157
|
+
self.process.print_message(
|
|
158
|
+
color_str(f"{self.process.idx_tot_str(idx)}: vignetting model parameters: ", "cyan") +
|
|
159
|
+
color_str(f"i0={i0_fit / 2:.4f}, "
|
|
160
|
+
f"k={k_fit * self.r_max:.4f}, "
|
|
161
|
+
f"r0={r0_fit / self.r_max:.4f}",
|
|
162
|
+
"light_blue"),
|
|
163
|
+
level=logging.DEBUG)
|
|
162
164
|
if self.plot_correction:
|
|
163
165
|
plt.figure(figsize=constants.PLT_FIG_SIZE)
|
|
164
166
|
plt.plot(radii, intensities, label="image mean intensity")
|
|
@@ -175,12 +177,13 @@ class Vignetting(SubAction):
|
|
|
175
177
|
save_plot(plot_path)
|
|
176
178
|
plt.close('all')
|
|
177
179
|
self.process.callback(
|
|
178
|
-
|
|
180
|
+
constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
179
181
|
f"{self.process.name}: intensity\nframe {idx_str}", plot_path)
|
|
180
182
|
for i, p in enumerate(self.percentiles):
|
|
181
183
|
self.corrections[i][idx] = fsolve(lambda x: sigmoid_model(x, *params) /
|
|
182
184
|
self.v0 - p, r0_fit)[0]
|
|
183
|
-
self.process.
|
|
185
|
+
self.process.print_message(
|
|
186
|
+
color_str(f"{self.process.idx_tot_str(idx)}: correct vignetting", "cyan"))
|
|
184
187
|
return correct_vignetting(
|
|
185
188
|
img_0, self.max_correction, self.black_threshold, None, params, self.v0,
|
|
186
189
|
subsample, self.fast_subsampling)
|
|
@@ -224,5 +227,5 @@ class Vignetting(SubAction):
|
|
|
224
227
|
f"{self.process.name}-r0.pdf"
|
|
225
228
|
save_plot(plot_path)
|
|
226
229
|
plt.close('all')
|
|
227
|
-
self.process.callback(
|
|
230
|
+
self.process.callback(constants.CALLBACK_SAVE_PLOT, self.process.id,
|
|
228
231
|
f"{self.process.name}: vignetting", plot_path)
|