shinestacker 0.2.0.post1.dev1__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.

Files changed (67) hide show
  1. shinestacker/__init__.py +3 -0
  2. shinestacker/_version.py +1 -0
  3. shinestacker/algorithms/__init__.py +14 -0
  4. shinestacker/algorithms/align.py +307 -0
  5. shinestacker/algorithms/balance.py +367 -0
  6. shinestacker/algorithms/core_utils.py +22 -0
  7. shinestacker/algorithms/depth_map.py +164 -0
  8. shinestacker/algorithms/exif.py +238 -0
  9. shinestacker/algorithms/multilayer.py +187 -0
  10. shinestacker/algorithms/noise_detection.py +182 -0
  11. shinestacker/algorithms/pyramid.py +176 -0
  12. shinestacker/algorithms/stack.py +112 -0
  13. shinestacker/algorithms/stack_framework.py +248 -0
  14. shinestacker/algorithms/utils.py +71 -0
  15. shinestacker/algorithms/vignetting.py +137 -0
  16. shinestacker/app/__init__.py +0 -0
  17. shinestacker/app/about_dialog.py +24 -0
  18. shinestacker/app/app_config.py +39 -0
  19. shinestacker/app/gui_utils.py +35 -0
  20. shinestacker/app/help_menu.py +16 -0
  21. shinestacker/app/main.py +176 -0
  22. shinestacker/app/open_frames.py +39 -0
  23. shinestacker/app/project.py +91 -0
  24. shinestacker/app/retouch.py +82 -0
  25. shinestacker/config/__init__.py +4 -0
  26. shinestacker/config/config.py +53 -0
  27. shinestacker/config/constants.py +174 -0
  28. shinestacker/config/gui_constants.py +85 -0
  29. shinestacker/core/__init__.py +5 -0
  30. shinestacker/core/colors.py +60 -0
  31. shinestacker/core/core_utils.py +52 -0
  32. shinestacker/core/exceptions.py +50 -0
  33. shinestacker/core/framework.py +210 -0
  34. shinestacker/core/logging.py +89 -0
  35. shinestacker/gui/__init__.py +0 -0
  36. shinestacker/gui/action_config.py +879 -0
  37. shinestacker/gui/actions_window.py +283 -0
  38. shinestacker/gui/colors.py +57 -0
  39. shinestacker/gui/gui_images.py +152 -0
  40. shinestacker/gui/gui_logging.py +213 -0
  41. shinestacker/gui/gui_run.py +393 -0
  42. shinestacker/gui/img/close-round-line-icon.png +0 -0
  43. shinestacker/gui/img/forward-button-icon.png +0 -0
  44. shinestacker/gui/img/play-button-round-icon.png +0 -0
  45. shinestacker/gui/img/plus-round-line-icon.png +0 -0
  46. shinestacker/gui/main_window.py +599 -0
  47. shinestacker/gui/new_project.py +170 -0
  48. shinestacker/gui/project_converter.py +148 -0
  49. shinestacker/gui/project_editor.py +539 -0
  50. shinestacker/gui/project_model.py +138 -0
  51. shinestacker/retouch/__init__.py +0 -0
  52. shinestacker/retouch/brush.py +9 -0
  53. shinestacker/retouch/brush_controller.py +57 -0
  54. shinestacker/retouch/brush_preview.py +126 -0
  55. shinestacker/retouch/exif_data.py +65 -0
  56. shinestacker/retouch/file_loader.py +104 -0
  57. shinestacker/retouch/image_editor.py +651 -0
  58. shinestacker/retouch/image_editor_ui.py +380 -0
  59. shinestacker/retouch/image_viewer.py +356 -0
  60. shinestacker/retouch/shortcuts_help.py +98 -0
  61. shinestacker/retouch/undo_manager.py +38 -0
  62. shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
  63. shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
  64. shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
  65. shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
  66. shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
  67. shinestacker-0.2.0.post1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,176 @@
1
+ import numpy as np
2
+ import cv2
3
+ from .. config.constants import constants
4
+ from .. core.colors import color_str
5
+ from .. core.exceptions import RunStopException, ImageLoadError, InvalidOptionError
6
+ from .utils import read_img, get_img_metadata, validate_image
7
+
8
+
9
+ class PyramidBase:
10
+ def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE, kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
11
+ gen_kernel=constants.DEFAULT_PY_GEN_KERNEL, float_type=constants.DEFAULT_PY_FLOAT):
12
+ self.min_size = min_size
13
+ self.kernel_size = kernel_size
14
+ self.pad_amount = (kernel_size - 1) // 2
15
+ self.do_step_callback = False
16
+ kernel = np.array([0.25 - gen_kernel / 2.0, 0.25, gen_kernel, 0.25, 0.25 - gen_kernel / 2.0])
17
+ self.gen_kernel = np.outer(kernel, kernel)
18
+ if float_type == constants.FLOAT_32:
19
+ self.float_type = np.float32
20
+ elif float_type == constants.FLOAT_64:
21
+ self.float_type = np.float64
22
+ else:
23
+ raise InvalidOptionError("float_type", float_type, details=" valid values are FLOAT_32 and FLOAT_64")
24
+
25
+ def print_message(self, msg):
26
+ self.process.sub_message_r(color_str(msg, "light_blue"))
27
+
28
+ def steps_per_frame(self):
29
+ return 1
30
+
31
+ def convolve(self, image):
32
+ return cv2.filter2D(image, -1, self.gen_kernel, borderType=cv2.BORDER_REFLECT101)
33
+
34
+ def reduce_layer(self, layer):
35
+ if len(layer.shape) == 2:
36
+ return self.convolve(layer)[::2, ::2]
37
+ reduced_channels = [self.reduce_layer(layer[:, :, channel]) for channel in range(layer.shape[2])]
38
+ return np.stack(reduced_channels, axis=-1)
39
+
40
+ def expand_layer(self, layer):
41
+ if len(layer.shape) == 2:
42
+ expand = np.empty((2 * layer.shape[0], 2 * layer.shape[1]), dtype=layer.dtype)
43
+ expand[::2, ::2] = layer
44
+ expand[1::2, :] = 0
45
+ expand[:, 1::2] = 0
46
+ return 4. * self.convolve(expand)
47
+ ch_layer = self.expand_layer(layer[:, :, 0])
48
+ next_layer = np.zeros(list(ch_layer.shape) + [layer.shape[2]], dtype=layer.dtype)
49
+ next_layer[:, :, 0] = ch_layer
50
+ for channel in range(1, layer.shape[2]):
51
+ next_layer[:, :, channel] = self.expand_layer(layer[:, :, channel])
52
+ return next_layer
53
+
54
+ def fuse_laplacian(self, laplacians):
55
+ gray_laps = [cv2.cvtColor(lap.astype(np.float32), cv2.COLOR_BGR2GRAY) for lap in laplacians]
56
+ energies = [self.convolve(np.square(gray_lap)) for gray_lap in gray_laps]
57
+ best = np.argmax(energies, axis=0)
58
+ fused = np.zeros_like(laplacians[0])
59
+ for i, lap in enumerate(laplacians):
60
+ fused += np.where(best[:, :, np.newaxis] == i, lap, 0)
61
+ return fused
62
+
63
+ def collapse(self, pyramid):
64
+ img = pyramid[-1]
65
+ for layer in pyramid[-2::-1]:
66
+ expanded = self.expand_layer(img)
67
+ if expanded.shape != layer.shape:
68
+ expanded = expanded[:layer.shape[0], :layer.shape[1]]
69
+ img = expanded + layer
70
+ return np.clip(np.abs(img), 0, self.max_pixel_value)
71
+
72
+ def entropy(self, image):
73
+ levels, counts = np.unique(image.astype(self.dtype), return_counts=True)
74
+ probabilities = np.zeros((self.num_pixel_values), dtype=self.float_type)
75
+ probabilities[levels] = counts.astype(self.float_type) / counts.sum()
76
+ padded_image = cv2.copyMakeBorder(image, self.pad_amount, self.pad_amount, self.pad_amount,
77
+ self.pad_amount, cv2.BORDER_REFLECT101)
78
+ return np.fromfunction(np.vectorize(lambda row, column: self.area_entropy(
79
+ self.get_pad(padded_image, row, column), probabilities)), image.shape[:2], dtype=int)
80
+
81
+ def area_entropy(self, area, probabilities):
82
+ levels = area.flatten()
83
+ return self.float_type(-1. * (levels * np.log(probabilities[levels])).sum())
84
+
85
+ def get_pad(self, padded_image, row, column):
86
+ return padded_image[row + self.pad_amount + self.offset[:, np.newaxis], column + self.pad_amount + self.offset]
87
+
88
+ def area_deviation(self, area):
89
+ return np.square(area - np.average(area).astype(self.float_type)).sum() / area.size
90
+
91
+ def deviation(self, image):
92
+ padded_image = cv2.copyMakeBorder(image, self.pad_amount, self.pad_amount, self.pad_amount,
93
+ self.pad_amount, cv2.BORDER_REFLECT101)
94
+ return np.fromfunction(
95
+ np.vectorize(lambda row, column: self.area_deviation(self.get_pad(padded_image, row, column))),
96
+ image.shape[:2], dtype=int)
97
+
98
+ def get_fused_base(self, images):
99
+ layers, height, width, _ = images.shape
100
+ entropies = np.zeros(images.shape[:3], dtype=self.float_type)
101
+ deviations = np.copy(entropies)
102
+ gray_images = np.array([cv2.cvtColor(images[layer].astype(np.float32),
103
+ cv2.COLOR_BGR2GRAY).astype(self.dtype) for layer in range(layers)])
104
+ entropies = np.array([self.entropy(img) for img in gray_images])
105
+ deviations = np.array([self.deviation(img) for img in gray_images])
106
+ best_e = np.argmax(entropies, axis=0)
107
+ best_d = np.argmax(deviations, axis=0)
108
+ fused = np.zeros(images.shape[1:], dtype=self.float_type)
109
+ for layer in range(layers):
110
+ img = images[layer]
111
+ fused += np.where(best_e[:, :, np.newaxis] == layer, img, 0)
112
+ fused += np.where(best_d[:, :, np.newaxis] == layer, img, 0)
113
+ return (fused / 2).astype(images.dtype)
114
+
115
+
116
+ class PyramidStack(PyramidBase):
117
+ def __init__(self, min_size=constants.DEFAULT_PY_MIN_SIZE, kernel_size=constants.DEFAULT_PY_KERNEL_SIZE,
118
+ gen_kernel=constants.DEFAULT_PY_GEN_KERNEL, float_type=constants.DEFAULT_PY_FLOAT):
119
+ super().__init__(min_size, kernel_size, gen_kernel, float_type)
120
+ self.offset = np.arange(-self.pad_amount, self.pad_amount + 1)
121
+
122
+ def name(self):
123
+ return "pyramid"
124
+
125
+ def process_single_image(self, img, levels):
126
+ pyramid = [img.astype(self.float_type)]
127
+ for _ in range(levels):
128
+ next_layer = self.reduce_layer(pyramid[-1])
129
+ if min(next_layer.shape[:2]) < 4:
130
+ break
131
+ pyramid.append(next_layer)
132
+ laplacian = [pyramid[-1]]
133
+ for level in range(len(pyramid) - 1, 0, -1):
134
+ expanded = self.expand_layer(pyramid[level])
135
+ pyr = pyramid[level - 1]
136
+ h, w = pyr.shape[:2]
137
+ expanded = expanded[:h, :w]
138
+ laplacian.append(pyr - expanded)
139
+ return laplacian[::-1]
140
+
141
+ def fuse_pyramids(self, all_laplacians):
142
+ fused = [self.get_fused_base(np.stack([p[-1] for p in all_laplacians], axis=0))]
143
+ for layer in range(len(all_laplacians[0]) - 2, -1, -1):
144
+ self.print_message(': fusing pyramids, layer: {}'.format(layer + 1))
145
+ laplacians = np.stack([p[layer] for p in all_laplacians], axis=0)
146
+ fused.append(self.fuse_laplacian(laplacians))
147
+ self.print_message(': pyramids fusion completed')
148
+ return fused[::-1]
149
+
150
+ def focus_stack(self, filenames):
151
+ metadata = None
152
+ all_laplacians = []
153
+ levels = None
154
+ for i, img_path in enumerate(filenames):
155
+ self.print_message(': validating file {}'.format(img_path.split('/')[-1]))
156
+ img = read_img(img_path)
157
+ if img is None:
158
+ raise ImageLoadError(img_path)
159
+ if metadata is None:
160
+ metadata = get_img_metadata(img)
161
+ self.dtype = metadata[1]
162
+ self.num_pixel_values = constants.NUM_UINT8 if self.dtype == np.uint8 else constants.NUM_UINT16
163
+ self.max_pixel_value = constants.MAX_UINT8 if self.dtype == np.uint8 else constants.MAX_UINT16
164
+ levels = int(np.log2(min(img.shape[:2]) / self.min_size))
165
+ else:
166
+ validate_image(img, *metadata)
167
+ if self.do_step_callback:
168
+ self.process.callback('after_step', self.process.id, self.process.name, i)
169
+ if self.process.callback('check_running', self.process.id, self.process.name) is False:
170
+ raise RunStopException(self.name)
171
+ for img_path in filenames:
172
+ self.print_message(': processing file {}'.format(img_path.split('/')[-1]))
173
+ img = read_img(img_path)
174
+ all_laplacians.append(self.process_single_image(img, levels))
175
+ stacked_image = self.collapse(self.fuse_pyramids(all_laplacians))
176
+ return stacked_image.astype(self.dtype)
@@ -0,0 +1,112 @@
1
+ import numpy as np
2
+ import cv2
3
+ import os
4
+ from .. config.constants import constants
5
+ from .. core.colors import color_str
6
+ from .. core.framework import JobBase
7
+ from .. core.exceptions import InvalidOptionError
8
+ from .utils import write_img
9
+ from .stack_framework import FrameDirectory, ActionList
10
+ from .exif import copy_exif_from_file_to_file
11
+
12
+
13
+ class FocusStackBase:
14
+ def __init__(self, stack_algo, exif_path='',
15
+ prefix=constants.DEFAULT_STACK_PREFIX,
16
+ denoise=0, plot_stack=False):
17
+ self.stack_algo = stack_algo
18
+ self.exif_path = exif_path
19
+ self.prefix = prefix if prefix != '' else constants.DEFAULT_STACK_PREFIX
20
+ self.denoise = denoise
21
+ self.plot_stack = plot_stack
22
+ self.stack_algo.process = self
23
+ self.frame_count = -1
24
+
25
+ def focus_stack(self, filenames):
26
+ self.sub_message_r(': reading input files')
27
+ img_files = sorted([os.path.join(self.input_full_path, name) for name in filenames])
28
+ stacked_img = self.stack_algo.focus_stack(img_files)
29
+ in_filename = filenames[0].split(".")
30
+ out_filename = f"{self.output_dir}/{self.prefix}{in_filename[0]}." + '.'.join(in_filename[1:])
31
+ if self.denoise > 0:
32
+ self.sub_message_r(': denoise image')
33
+ stacked_img = cv2.fastNlMeansDenoisingColored(stacked_img, None, self.denoise, self.denoise, 7, 21)
34
+ write_img(out_filename, stacked_img)
35
+ if self.exif_path != '' and stacked_img.dtype == np.uint8:
36
+ self.sub_message_r(': copy exif data')
37
+ dirpath, _, fnames = next(os.walk(self.exif_path))
38
+ fnames = [name for name in fnames if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
39
+ exif_filename = f"{self.exif_path}/{fnames[0]}"
40
+ copy_exif_from_file_to_file(exif_filename, out_filename)
41
+ self.sub_message_r(' ' * 60)
42
+ if self.plot_stack:
43
+ idx_str = "{:04d}".format(self.frame_count) if self.frame_count >= 0 else ''
44
+ name = f"{self.name}: {self.stack_algo.name()}"
45
+ if idx_str != '':
46
+ name += f"\nbunch: {idx_str}"
47
+ self.callback('save_plot', self.id, name, out_filename)
48
+ if self.frame_count >= 0:
49
+ self.frame_count += 1
50
+
51
+ def init(self, job, working_path):
52
+ if self.exif_path is None:
53
+ self.exif_path = job.paths[0]
54
+ if self.exif_path != '':
55
+ self.exif_path = working_path + "/" + self.exif_path
56
+
57
+
58
+ class FocusStackBunch(FocusStackBase, FrameDirectory, ActionList):
59
+ def __init__(self, name, stack_algo, enabled=True, **kwargs):
60
+ FocusStackBase.__init__(self, stack_algo,
61
+ exif_path=kwargs.pop('exif_path', ''),
62
+ prefix=kwargs.pop('prefix', constants.DEFAULT_STACK_PREFIX),
63
+ denoise=kwargs.pop('denoise', 0),
64
+ plot_stack=kwargs.pop('plot_stack', constants.DEFAULT_PLOT_STACK_BUNCH))
65
+ FrameDirectory.__init__(self, name, **kwargs)
66
+ ActionList.__init__(self, name, enabled)
67
+ self.frame_count = 0
68
+ self.frames = kwargs.get('frames', constants.DEFAULT_FRAMES)
69
+ self.overlap = kwargs.get('overlap', constants.DEFAULT_OVERLAP)
70
+ self.denoise = kwargs.get('denoise', 0)
71
+ self.stack_algo.do_step_callback = False
72
+ if self.overlap >= self.frames:
73
+ raise InvalidOptionError("overlap", self.overlap, "overlap must be smaller than batch size")
74
+
75
+ def init(self, job):
76
+ FrameDirectory.init(self, job)
77
+ FocusStackBase.init(self, job, self.working_path)
78
+
79
+ def begin(self):
80
+ ActionList.begin(self)
81
+ fnames = self.folder_filelist(self.input_full_path)
82
+ self.__chunks = [fnames[x:x + self.frames] for x in range(0, len(fnames) - self.overlap, self.frames - self.overlap)]
83
+ self.set_counts(len(self.__chunks))
84
+
85
+ def end(self):
86
+ ActionList.end(self)
87
+
88
+ def run_step(self):
89
+ self.print_message_r(color_str("fusing bunch: {}".format(self.count), "blue"))
90
+ self.focus_stack(self.__chunks[self.count - 1])
91
+ self.callback('after_step', self.id, self.name, self.count)
92
+
93
+
94
+ class FocusStack(FocusStackBase, FrameDirectory, JobBase):
95
+ def __init__(self, name, stack_algo, enabled=True, **kwargs):
96
+ FocusStackBase.__init__(self, stack_algo,
97
+ exif_path=kwargs.pop('exif_path', ''),
98
+ prefix=kwargs.pop('prefix', constants.DEFAULT_STACK_PREFIX),
99
+ denoise=kwargs.pop('denoise', 0),
100
+ plot_stack=kwargs.pop('plot_stack', constants.DEFAULT_PLOT_STACK))
101
+ FrameDirectory.__init__(self, name, **kwargs)
102
+ JobBase.__init__(self, name, enabled)
103
+ self.stack_algo.do_step_callback = True
104
+
105
+ def run_core(self):
106
+ self.set_filelist()
107
+ self.callback('step_counts', self.id, self.name, self.stack_algo.steps_per_frame() * len(self.filenames))
108
+ self.focus_stack(self.filenames)
109
+
110
+ def init(self, job):
111
+ FrameDirectory.init(self, job)
112
+ FocusStackBase.init(self, job, self.working_path)
@@ -0,0 +1,248 @@
1
+ import logging
2
+ import os
3
+ from .. config.constants import constants
4
+ from .. core.colors import color_str
5
+ from .. core.framework import Job, ActionList
6
+ from .. core.core_utils import check_path_exists
7
+ from .. core.exceptions import ShapeError, BitDepthError, RunStopException
8
+ from .utils import read_img, write_img
9
+
10
+
11
+ class StackJob(Job):
12
+ def __init__(self, name, working_path, input_path='', **kwargs):
13
+ check_path_exists(working_path)
14
+ self.working_path = working_path
15
+ if input_path == '':
16
+ self.paths = []
17
+ else:
18
+ self.paths = [input_path]
19
+ Job.__init__(self, name, **kwargs)
20
+
21
+ def init(self, a):
22
+ a.init(self)
23
+
24
+
25
+ class FramePaths:
26
+ def __init__(self, name, input_path='', output_path='', working_path='', plot_path=constants.DEFAULT_PLOTS_PATH,
27
+ scratch_output_dir=True, resample=1, reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **kwargs):
28
+ self.name = name
29
+ self.working_path = working_path
30
+ self.plot_path = plot_path
31
+ self.input_path = input_path
32
+ self.output_path = output_path
33
+ self.resample = resample
34
+ self.reverse_order = reverse_order
35
+ self.scratch_output_dir = scratch_output_dir
36
+
37
+ def set_filelist(self):
38
+ self.filenames = self.folder_filelist(self.input_full_path)
39
+ file_list = self.input_full_path.replace(self.working_path, '').lstrip('/')
40
+ self.print_message(color_str(": {} files ".format(len(self.filenames)) + "in folder: " + file_list, 'blue'))
41
+ self.print_message(color_str("focus stacking", 'blue'))
42
+
43
+ def init(self, job):
44
+ if self.working_path == '':
45
+ self.working_path = job.working_path
46
+ check_path_exists(self.working_path)
47
+ if self.output_path == '':
48
+ self.output_path = self.name
49
+ self.output_dir = self.working_path + ('' if self.working_path[-1] == '/' else '/') + self.output_path
50
+ if not os.path.exists(self.output_dir):
51
+ os.makedirs(self.output_dir)
52
+ else:
53
+ list_dir = os.listdir(self.output_dir)
54
+ if len(list_dir) > 0:
55
+ if self.scratch_output_dir:
56
+ if self.enabled:
57
+ for filename in list_dir:
58
+ file_path = os.path.join(self.output_dir, filename)
59
+ if os.path.isfile(file_path):
60
+ os.remove(file_path)
61
+ self.print_message(color_str(f": output directory {self.output_path} content erased", 'yellow'))
62
+ else:
63
+ self.print_message(color_str(f": module disabled, output directory {self.output_path} not scratched", 'yellow'))
64
+ else:
65
+ self.print_message(color_str(f": output directory {self.output_path} not empty, "
66
+ "files may be overwritten or merged with existing ones.", 'yellow'), level=logging.WARNING) # noqa
67
+ if self.plot_path == '':
68
+ self.plot_path = self.working_path + ('' if self.working_path[-1] == '/' else '/') + self.plot_path
69
+ if not os.path.exists(self.plot_path):
70
+ os.makedirs(self.plot_path)
71
+ if self.input_path == '':
72
+ if len(job.paths) == 0:
73
+ raise Exception(f"Job {job.name} does not have any configured path")
74
+ self.input_path = job.paths[-1]
75
+ job.paths.append(self.output_path)
76
+
77
+
78
+ class FrameDirectory(FramePaths):
79
+ def __init__(self, name, **kwargs):
80
+ FramePaths.__init__(self, name, **kwargs)
81
+
82
+ def folder_list_str(self):
83
+ if isinstance(self.input_full_path, list):
84
+ file_list = ", ".join([i for i in self.input_full_path.replace(self.working_path, '').lstrip('/')])
85
+ return "folder{}: ".format('s' if len(self.input_full_path) > 1 else '') + file_list
86
+ else:
87
+ return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
88
+
89
+ def folder_filelist(self, path):
90
+ src_contents = os.walk(self.input_full_path)
91
+ dirpath, _, filenames = next(src_contents)
92
+ filelist = [name for name in filenames if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
93
+ filelist.sort()
94
+ if self.reverse_order:
95
+ filelist.reverse()
96
+ if self.resample > 1:
97
+ filelist = filelist[0::self.resample]
98
+ return filelist
99
+
100
+ def init(self, job):
101
+ FramePaths.init(self, job)
102
+ self.input_full_path = self.working_path + ('' if self.working_path[-1] == '/' else '/') + self.input_path
103
+ check_path_exists(self.input_full_path)
104
+ job.paths.append(self.output_path)
105
+
106
+
107
+ class FrameMultiDirectory:
108
+ def __init__(self, name, input_path='', output_path='', working_path='', plot_path=constants.DEFAULT_PLOTS_PATH,
109
+ scratch_output_dir=True, resample=1, reverse_order=constants.DEFAULT_FILE_REVERSE_ORDER, **kwargs):
110
+ FramePaths.__init__(self, name, input_path, output_path, working_path, plot_path, scratch_output_dir, resample, reverse_order, **kwargs)
111
+
112
+ def folder_list_str(self):
113
+ if isinstance(self.input_full_path, list):
114
+ file_list = ", ".join([d.replace(self.working_path, '').lstrip('/') for d in self.input_full_path])
115
+ return "folder{}: ".format('s' if len(self.input_full_path) > 1 else '') + file_list
116
+ else:
117
+ return "folder: " + self.input_full_path.replace(self.working_path, '').lstrip('/')
118
+
119
+ def folder_filelist(self):
120
+ if isinstance(self.input_full_path, str):
121
+ dirs = [self.input_full_path]
122
+ paths = [self.input_path]
123
+ elif hasattr(self.input_full_path, "__len__"):
124
+ dirs = self.input_full_path
125
+ paths = self.input_path
126
+ else:
127
+ raise Exception("input_full_path option must contain a path or an array of paths")
128
+ files = []
129
+ for d, p in zip(dirs, paths):
130
+ filelist = []
131
+ for dirpath, _, filenames in os.walk(d):
132
+ filelist = [p + "/" + name for name in filenames if os.path.splitext(name)[-1][1:].lower() in constants.EXTENSIONS]
133
+ if self.reverse_order:
134
+ filelist.reverse()
135
+ if self.resample > 1:
136
+ filelist = filelist[0::self.resample]
137
+ files += filelist
138
+ if len(files) == 0:
139
+ self.print_message(color_str(f"input folder {p} does not contain any image", "red"), level=logging.WARNING)
140
+ return files
141
+
142
+ def init(self, job):
143
+ FramePaths.init(self, job)
144
+ if isinstance(self.input_path, str):
145
+ self.input_full_path = self.working_path + ('' if self.working_path[-1] == '/' else '/') + self.input_path
146
+ check_path_exists(self.input_full_path)
147
+ elif hasattr(self.input_path, "__len__"):
148
+ self.input_full_path = []
149
+ for path in self.input_path:
150
+ self.input_full_path.append(self.working_path + ('' if self.working_path[-1] == '/' else '/') + path)
151
+ job.paths.append(self.output_path)
152
+
153
+
154
+ class FramesRefActions(FrameDirectory, ActionList):
155
+ def __init__(self, name, enabled=True, ref_idx=-1, step_process=False, **kwargs):
156
+ FrameDirectory.__init__(self, name, **kwargs)
157
+ ActionList.__init__(self, name, enabled)
158
+ self.ref_idx = ref_idx
159
+ self.step_process = step_process
160
+
161
+ def begin(self):
162
+ ActionList.begin(self)
163
+ self.set_filelist()
164
+ self.set_counts(len(self.filenames))
165
+ if self.ref_idx == -1:
166
+ self.ref_idx = len(self.filenames) // 2
167
+
168
+ def end(self):
169
+ ActionList.end(self)
170
+
171
+ def run_frame(self, idx, ref_idx):
172
+ assert False, 'abstract method'
173
+
174
+ def run_step(self):
175
+ if self.count == 1:
176
+ self.__idx = self.ref_idx if self.step_process else 0
177
+ self.__ref_idx = self.ref_idx
178
+ self.__idx_step = +1
179
+ ll = len(self.filenames)
180
+ self.print_message_r(
181
+ color_str("step {}/{}: process file: {}, reference: {}".format(self.count, ll, self.filenames[self.__idx],
182
+ self.filenames[self.__ref_idx]), "blue"))
183
+ self.run_frame(self.__idx, self.__ref_idx)
184
+ if self.__idx < ll:
185
+ if self.step_process:
186
+ self.__ref_idx = self.__idx
187
+ self.__idx += self.__idx_step
188
+ if self.__idx == ll:
189
+ self.__idx = self.ref_idx - 1
190
+ if self.step_process:
191
+ self.__ref_idx = self.ref_idx
192
+ self.__idx_step = -1
193
+
194
+
195
+ class SubAction:
196
+ def __init__(self, enabled=True):
197
+ self.enabled = enabled
198
+
199
+
200
+ class CombinedActions(FramesRefActions):
201
+ def __init__(self, name, actions=[], enabled=True, **kwargs):
202
+ FramesRefActions.__init__(self, name, enabled, **kwargs)
203
+ self.__actions = actions
204
+
205
+ def begin(self):
206
+ FramesRefActions.begin(self)
207
+ for a in self.__actions:
208
+ if a.enabled:
209
+ a.begin(self)
210
+
211
+ def img_ref(self, idx):
212
+ filename = self.filenames[idx]
213
+ img = read_img((self.output_dir if self.step_process else self.input_full_path) + "/" + filename)
214
+ self.dtype = img.dtype
215
+ self.shape = img.shape
216
+ if img is None:
217
+ raise Exception("Invalid file: " + self.input_full_path + "/" + filename)
218
+ return img
219
+
220
+ def run_frame(self, idx, ref_idx):
221
+ filename = self.filenames[idx]
222
+ self.sub_message_r(': read input image')
223
+ img = read_img(self.input_full_path + "/" + filename)
224
+ if hasattr(self, 'dtype') and img.dtype != self.dtype:
225
+ raise BitDepthError(img.dtype, self.dtype)
226
+ if hasattr(self, 'shape') and img.shape != self.shape:
227
+ raise ShapeError(img.shape, self.shape)
228
+ if img is None:
229
+ raise Exception("Invalid file: " + self.input_full_path + "/" + filename)
230
+ if len(self.__actions) == 0:
231
+ self.sub_message(color_str(": no actions specified.", "red"), level=logging.WARNING)
232
+ for a in self.__actions:
233
+ if not a.enabled:
234
+ self.get_logger().warning(color_str(self.base_message + ": sub-action disabled", 'red'))
235
+ else:
236
+ if self.callback('check_running', self.id, self.name) is False:
237
+ raise RunStopException(self.name)
238
+ img = a.run_frame(idx, ref_idx, img)
239
+ self.sub_message_r(': write output image')
240
+ if img is not None:
241
+ write_img(self.output_dir + "/" + filename, img)
242
+ else:
243
+ self.print_message("No output file resulted from processing input file: " + self.input_full_path + "/" + filename, level=logging.WARNING)
244
+
245
+ def end(self):
246
+ for a in self.__actions:
247
+ if a.enabled:
248
+ a.end()
@@ -0,0 +1,71 @@
1
+ import cv2
2
+ import os
3
+ import numpy as np
4
+ import logging
5
+ import matplotlib.pyplot as plt
6
+ from .. config.config import config
7
+ from .. core.exceptions import ShapeError, BitDepthError
8
+
9
+
10
+ def read_img(file_path):
11
+ if not os.path.isfile(file_path):
12
+ raise Exception("File does not exist: " + file_path)
13
+ ext = file_path.split(".")[-1]
14
+ if ext == 'jpeg' or ext == 'jpg':
15
+ img = cv2.imread(file_path)
16
+ elif ext == 'tiff' or ext == 'tif' or ext == 'png':
17
+ img = cv2.imread(file_path, cv2.IMREAD_UNCHANGED)
18
+ return img
19
+
20
+
21
+ def write_img(file_path, img):
22
+ ext = file_path.split(".")[-1]
23
+ if ext == 'jpeg' or ext == 'jpg':
24
+ cv2.imwrite(file_path, img, [int(cv2.IMWRITE_JPEG_QUALITY), 100])
25
+ elif ext == 'tiff' or ext == 'tif':
26
+ cv2.imwrite(file_path, img, [int(cv2.IMWRITE_TIFF_COMPRESSION), 1])
27
+ elif ext == 'png':
28
+ cv2.imwrite(file_path, img)
29
+
30
+
31
+ def img_8bit(img):
32
+ return (img >> 8).astype('uint8') if img.dtype == np.uint16 else img
33
+
34
+
35
+ def img_bw_8bit(img):
36
+ img = img_8bit(img)
37
+ if len(img.shape) == 3:
38
+ return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
39
+ elif len(img.shape) == 2:
40
+ return img
41
+ else:
42
+ raise ValueError(f"Unsupported image format: {img.shape}")
43
+
44
+
45
+ def img_bw(img):
46
+ return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
47
+
48
+
49
+ def get_img_metadata(img):
50
+ return img.shape[:2], img.dtype
51
+
52
+
53
+ def validate_image(img, expected_shape=None, expected_dtype=None):
54
+ shape, dtype = get_img_metadata(img)
55
+ if expected_shape and shape[:2] != expected_shape[:2]:
56
+ raise ShapeError(expected_shape, shape)
57
+ if expected_dtype and dtype != expected_dtype:
58
+ raise BitDepthError(expected_dtype, dtype)
59
+
60
+
61
+ def save_plot(filename):
62
+ logging.getLogger(__name__).debug("save plot file: " + filename)
63
+ dir_path = os.path.dirname(filename)
64
+ if not dir_path:
65
+ dir_path = '.'
66
+ if not os.path.isdir(dir_path):
67
+ os.makedirs(dir_path)
68
+ plt.savefig(filename, dpi=150)
69
+ if config.JUPYTER_NOTEBOOK:
70
+ plt.show()
71
+ plt.close('all')