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,367 @@
1
+ import numpy as np
2
+ import cv2
3
+ import matplotlib.pyplot as plt
4
+ from scipy.optimize import bisect
5
+ from scipy.interpolate import interp1d
6
+ from .. config.constants import constants
7
+ from .. core.exceptions import InvalidOptionError
8
+ from .utils import read_img, save_plot
9
+ from .stack_framework import SubAction
10
+
11
+
12
+ class CorrectionMapBase:
13
+ def __init__(self, dtype, ref_hist, intensity_interval=None):
14
+ intensity_interval = {**constants.DEFAULT_INTENSITY_INTERVAL, **(intensity_interval or {})}
15
+ self.dtype = dtype
16
+ self.num_pixel_values = constants.NUM_UINT8 if dtype == np.uint8 else constants.NUM_UINT16
17
+ self.max_pixel_value = self.num_pixel_values - 1
18
+ self.id_lut = np.array(list(range(self.num_pixel_values)))
19
+ i_min, i_max = intensity_interval['min'], intensity_interval['max']
20
+ self.i_min = i_min
21
+ self.i_end = i_max + 1 if i_max >= 0 else self.num_pixel_values
22
+ self.channels = len(ref_hist)
23
+ self.reference = None
24
+
25
+ def lut(self, correction, reference):
26
+ assert False, 'abstract method'
27
+
28
+ def apply_lut(self, correction, reference, img):
29
+ lut = self.lut(correction, reference)
30
+ return cv2.LUT(img, lut) if self.dtype == np.uint8 else np.take(lut, img)
31
+
32
+ def adjust(self, image, correction):
33
+ if self.channels == 1:
34
+ return self.apply_lut(correction[0], self.reference[0], image)
35
+ else:
36
+ chans = cv2.split(image)
37
+ if self.channels == 2:
38
+ ch_out = [chans[0]] + [self.apply_lut(correction[c - 1], self.reference[c - 1], chans[c]) for c in range(1, 3)]
39
+ elif self.channels == 3:
40
+ ch_out = [self.apply_lut(correction[c], self.reference[c], chans[c]) for c in range(3)]
41
+ return cv2.merge(ch_out)
42
+
43
+ def correction_size(self, correction):
44
+ return correction
45
+
46
+
47
+ class MatchHist(CorrectionMapBase):
48
+ def __init__(self, dtype, ref_hist, intensity_interval=None):
49
+ CorrectionMapBase.__init__(self, dtype, ref_hist, intensity_interval)
50
+ self.reference = self.cumsum(ref_hist)
51
+ self.reference_mean = [r.mean() for r in self.reference]
52
+ self.values = [*range(self.num_pixel_values)]
53
+
54
+ def cumsum(self, hist):
55
+ return [np.cumsum(h) / h.sum() * self.max_pixel_value for h in hist]
56
+
57
+ def lut(self, correction, reference):
58
+ interp = interp1d(reference, self.values)
59
+ lut = np.array([interp(v) for v in np.clip(correction, reference.min(), reference.max())])
60
+ l0, l1 = lut[0], lut[-1]
61
+ ll = lut[(lut != l0) & (lut != l1)]
62
+ if ll.size > 0:
63
+ l_min, l_max = ll.min(), ll.max()
64
+ i0, i1 = self.id_lut[lut == l0], self.id_lut[lut == l1]
65
+ i0_max = i0.max()
66
+ lut[lut == l0] = (i0 / i0_max * l_min) if i0_max > 0 else 0
67
+ lut[lut == l1] = i1 + (i1 - self.max_pixel_value) * (self.max_pixel_value - l_max) / float(i1.size) if i1.size > 0 else self.max_pixel_value
68
+ return lut.astype(self.dtype)
69
+
70
+ def correction(self, hist):
71
+ return self.cumsum(hist)
72
+
73
+ def correction_size(self, correction):
74
+ return [c.mean() / m for c, m in zip(correction, self.reference_mean)]
75
+
76
+
77
+ class CorrectionMap(CorrectionMapBase):
78
+ def __init__(self, dtype, ref_hist, intensity_interval=None):
79
+ CorrectionMapBase.__init__(self, dtype, ref_hist, intensity_interval)
80
+ self.reference = [self.mid_val(self.id_lut, h) for h in ref_hist]
81
+
82
+ def mid_val(self, lut, h):
83
+ return np.average(lut[self.i_min:self.i_end], weights=h.flatten()[self.i_min:self.i_end])
84
+
85
+
86
+ class GammaMap(CorrectionMap):
87
+ def __init__(self, dtype, ref_hist, intensity_interval=None):
88
+ CorrectionMap.__init__(self, dtype, ref_hist, intensity_interval)
89
+
90
+ def correction(self, hist):
91
+ return [bisect(lambda x: self.mid_val(self.lut(x), h) - r, 0.1, 5) for h, r in zip(hist, self.reference)]
92
+
93
+ def lut(self, correction, reference=None):
94
+ gamma_inv = 1.0 / correction
95
+ return (((np.arange(0, self.num_pixel_values) / self.max_pixel_value) ** gamma_inv) * self.max_pixel_value).astype(self.dtype)
96
+
97
+
98
+ class LinearMap(CorrectionMap):
99
+ def __init__(self, dtype, ref_hist, intensity_interval=None):
100
+ CorrectionMap.__init__(self, dtype, ref_hist, intensity_interval)
101
+
102
+ def lut(self, correction, reference=None):
103
+ return np.clip(np.arange(0, self.num_pixel_values) * correction, 0, self.max_pixel_value).astype(self.dtype)
104
+
105
+ def correction(self, hist):
106
+ return [r / self.mid_val(self.id_lut, h) for h, r in zip(hist, self.reference)]
107
+
108
+
109
+ class Correction:
110
+ def __init__(self, channels, mask_size=0, intensity_interval=None, subsample=-1, corr_map=constants.DEFAULT_CORR_MAP,
111
+ plot_histograms=False, plot_summary=False):
112
+ self.mask_size = mask_size
113
+ self.intensity_interval = intensity_interval
114
+ self.plot_histograms = plot_histograms
115
+ self.plot_summary = plot_summary
116
+ self.subsample = constants.DEFAULT_BALANCE_SUBSAMPLE if subsample == -1 else subsample
117
+ self.corr_map = corr_map
118
+ self.channels = channels
119
+
120
+ def begin(self, ref_image, size, ref_idx):
121
+ self.dtype = ref_image.dtype
122
+ self.num_pixel_values = constants.NUM_UINT8 if ref_image.dtype == np.uint8 else constants.NUM_UINT16
123
+ self.max_pixel_value = self.num_pixel_values - 1
124
+ hist = self.get_hist(self.preprocess(ref_image), ref_idx)
125
+ if self.corr_map == constants.BALANCE_LINEAR:
126
+ self.corr_map = LinearMap(self.dtype, hist, self.intensity_interval)
127
+ elif self.corr_map == constants.BALANCE_GAMMA:
128
+ self.corr_map = GammaMap(self.dtype, hist, self.intensity_interval)
129
+ elif self.corr_map == constants.BALANCE_MATCH_HIST:
130
+ self.corr_map = MatchHist(self.dtype, hist, self.intensity_interval)
131
+ else:
132
+ raise InvalidOptionError("corr_map", self.corr_map)
133
+ self.corrections = np.ones((size, self.channels))
134
+
135
+ def calc_hist_1ch(self, image):
136
+ if self.mask_size == 0:
137
+ image_sel = image
138
+ else:
139
+ height, width = image.shape[:2]
140
+ xv, yv = np.meshgrid(np.linspace(0, width - 1, width), np.linspace(0, height - 1, height))
141
+ mask_radius = (min(width, height) * self.mask_size / 2)
142
+ image_sel = image[(xv - width / 2) ** 2 + (yv - height / 2) ** 2 <= mask_radius ** 2]
143
+ hist, bins = np.histogram((image_sel if self.subsample == 1
144
+ else image_sel[::self.subsample, ::self.subsample]),
145
+ bins=np.linspace(-0.5, self.num_pixel_values - 0.5,
146
+ self.num_pixel_values + 1))
147
+ return hist
148
+
149
+ def balance(self, image, idx):
150
+ correction = self.corr_map.correction(self.get_hist(image, idx))
151
+ return correction, self.corr_map.adjust(image, correction)
152
+
153
+ def get_hist(self, image, idx):
154
+ assert False, 'abstract method'
155
+
156
+ def end(self):
157
+ assert False, 'abstract method'
158
+
159
+ def apply_correction(self, idx, image):
160
+ image = self.preprocess(image)
161
+ correction, image = self.balance(image, idx)
162
+ image = self.postprocess(image)
163
+ self.corrections[idx] = self.corr_map.correction_size(correction)
164
+ return image
165
+
166
+ def preprocess(self, image):
167
+ return image
168
+
169
+ def postprocess(self, image):
170
+ return image
171
+
172
+ def histo_plot(self, ax, hist, x_label, color, alpha=1):
173
+ ax.set_ylabel("# of pixels")
174
+ ax.set_xlabel(x_label)
175
+ ax.set_xlim([0, self.num_pixel_values])
176
+ ax.set_yscale('log')
177
+ ax.plot(hist, color=color, alpha=alpha)
178
+
179
+ def save_plot(self, idx):
180
+ idx_str = "{:04d}".format(idx)
181
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/{self.process.name}-hist-{idx_str}.pdf"
182
+ save_plot(plot_path)
183
+ plt.close('all')
184
+ self.process.callback('save_plot', self.process.id, f"{self.process.name}: balance\nframe {idx_str}", plot_path)
185
+
186
+ def save_summary_plot(self, name='balance'):
187
+ plot_path = f"{self.process.working_path}/{self.process.plot_path}/{self.process.name}-{name}.pdf"
188
+ save_plot(plot_path)
189
+ plt.close('all')
190
+ self.process.callback('save_plot', self.process.id, f"{self.process.name}: {name}", plot_path)
191
+
192
+
193
+ class LumiCorrection(Correction):
194
+ def __init__(self, **kwargs):
195
+ Correction.__init__(self, 1, **kwargs)
196
+
197
+ def get_hist(self, image, idx):
198
+ hist = self.calc_hist_1ch(cv2.cvtColor(image, cv2.COLOR_BGR2GRAY))
199
+ chans = cv2.split(image)
200
+ colors = ("r", "g", "b")
201
+ if self.plot_histograms:
202
+ fig, axs = plt.subplots(1, 2, figsize=(10, 5), sharey=True)
203
+ self.histo_plot(axs[0], hist, "pixel luminosity", 'black')
204
+ for (chan, color) in zip(chans, colors):
205
+ hist_col = self.calc_hist_1ch(chan)
206
+ self.histo_plot(axs[1], hist_col, "r,g,b luminosity", color, alpha=0.5)
207
+ plt.xlim(0, self.max_pixel_value)
208
+ self.save_plot(idx)
209
+ return [hist]
210
+
211
+ def end(self, ref_idx):
212
+ if self.plot_summary:
213
+ plt.figure(figsize=(10, 5))
214
+ x = np.arange(1, len(self.corrections) + 1, dtype=int)
215
+ y = self.corrections
216
+ plt.plot([ref_idx + 1, ref_idx + 1], [0, 1], color='cornflowerblue', linestyle='--', label='reference frame')
217
+ plt.plot([x[0], x[-1]], [1, 1], color='lightgray', linestyle='--', label='no correction')
218
+ plt.plot(x, y, color='navy', label='luminosity correction')
219
+ plt.xlabel('frame')
220
+ plt.ylabel('correction')
221
+ plt.legend()
222
+ plt.xlim(x[0], x[-1])
223
+ plt.ylim(0)
224
+ self.save_summary_plot()
225
+
226
+
227
+ class RGBCorrection(Correction):
228
+ def __init__(self, **kwargs):
229
+ Correction.__init__(self, 3, **kwargs)
230
+
231
+ def get_hist(self, image, idx):
232
+ hist = [self.calc_hist_1ch(chan) for chan in cv2.split(image)]
233
+ colors = ("r", "g", "b")
234
+ if self.plot_histograms:
235
+ fig, axs = plt.subplots(1, 3, figsize=(10, 5), sharey=True)
236
+ for c in [2, 1, 0]:
237
+ self.histo_plot(axs[c], hist[c], colors[c] + " luminosity", colors[c])
238
+ plt.xlim(0, self.max_pixel_value)
239
+ self.save_plot(idx)
240
+ return hist
241
+
242
+ def end(self, ref_idx):
243
+ if self.plot_summary:
244
+ plt.figure(figsize=(10, 5))
245
+ x = np.arange(1, len(self.corrections) + 1, dtype=int)
246
+ y = self.corrections
247
+ plt.plot([ref_idx + 1, ref_idx + 1], [0, 1], color='cornflowerblue', linestyle='--', label='reference frame')
248
+ plt.plot([x[0], x[-1]], [1, 1], color='lightgray', linestyle='--', label='no correction')
249
+ plt.plot(x, y[:, 0], color='r', label='R correction')
250
+ plt.plot(x, y[:, 1], color='g', label='G correction')
251
+ plt.plot(x, y[:, 2], color='b', label='B correction')
252
+ plt.xlabel('frame')
253
+ plt.ylabel('correction')
254
+ plt.legend()
255
+ plt.xlim(x[0], x[-1])
256
+ plt.ylim(0)
257
+ self.save_summary_plot()
258
+
259
+
260
+ class Ch2Correction(Correction):
261
+ def __init__(self, **kwargs):
262
+ Correction.__init__(self, 2, **kwargs)
263
+
264
+ def preprocess(self, image):
265
+ assert False, 'abstract method'
266
+
267
+ def get_labels(self):
268
+ assert False, 'abstract method'
269
+
270
+ def get_hist(self, image, idx):
271
+ hist = [self.calc_hist_1ch(chan) for chan in cv2.split(image)]
272
+ if self.plot_histograms:
273
+ fig, axs = plt.subplots(1, 3, figsize=(10, 5), sharey=True)
274
+ for c in range(3):
275
+ self.histo_plot(axs[c], hist[c], self.labels[c], self.colors[c])
276
+ plt.xlim(0, self.max_pixel_value)
277
+ self.save_plot(idx)
278
+ return hist[1:]
279
+
280
+ def end(self, ref_idx):
281
+ if self.plot_summary:
282
+ plt.figure(figsize=(10, 5))
283
+ x = np.arange(1, len(self.corrections) + 1, dtype=int)
284
+ y = self.corrections
285
+ plt.plot([ref_idx + 1, ref_idx + 1], [0, 1], color='cornflowerblue', linestyle='--', label='reference frame')
286
+ plt.plot([x[0], x[-1]], [1, 1], color='lightgray', linestyle='--', label='no correction')
287
+ plt.plot(x, y[:, 0], color=self.colors[1], label=self.labels[1] + ' correction')
288
+ plt.plot(x, y[:, 1], color=self.colors[2], label=self.labels[2] + ' correction')
289
+ plt.xlabel('frame')
290
+ plt.ylabel('correction')
291
+ plt.legend()
292
+ plt.xlim(x[0], x[-1])
293
+ plt.ylim(0)
294
+ self.save_summary_plot()
295
+
296
+
297
+ class SVCorrection(Ch2Correction):
298
+ def __init__(self, **kwargs):
299
+ Ch2Correction.__init__(self, **kwargs)
300
+ self.labels = ("H", "S", "V")
301
+ self.colors = ("hotpink", "orange", "navy")
302
+
303
+ def preprocess(self, image):
304
+ return cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
305
+
306
+ def postprocess(self, image):
307
+ return cv2.cvtColor(image, cv2.COLOR_HSV2BGR)
308
+
309
+
310
+ class LSCorrection(Ch2Correction):
311
+ def __init__(self, **kwargs):
312
+ Ch2Correction.__init__(self, **kwargs)
313
+ self.labels = ("H", "L", "S")
314
+ self.colors = ("hotpink", "navy", "orange")
315
+
316
+ def preprocess(self, image):
317
+ return cv2.cvtColor(image, cv2.COLOR_BGR2HLS)
318
+
319
+ def postprocess(self, image):
320
+ return cv2.cvtColor(image, cv2.COLOR_HLS2BGR)
321
+
322
+
323
+ class BalanceFrames(SubAction):
324
+ def __init__(self, enabled=True, **kwargs):
325
+ super().__init__(enabled=enabled)
326
+ corr_map = kwargs.get('corr_map', constants.DEFAULT_CORR_MAP)
327
+ subsample = kwargs.get('subsample', constants.DEFAULT_BALANCE_SUBSAMPLE)
328
+ channel = kwargs.pop('channel', constants.DEFAULT_CHANNEL)
329
+ kwargs['subsample'] = (1 if corr_map == constants.BALANCE_MATCH_HIST else constants.DEFAULT_BALANCE_SUBSAMPLE) if subsample == -1 else subsample
330
+ self.mask_size = kwargs.get('mask_size', 0)
331
+ self.plot_summary = kwargs.get('plot_summary', False)
332
+ if channel == constants.BALANCE_LUMI:
333
+ self.correction = LumiCorrection(**kwargs)
334
+ elif channel == constants.BALANCE_RGB:
335
+ self.correction = RGBCorrection(**kwargs)
336
+ elif channel == constants.BALANCE_HSV:
337
+ self.correction = SVCorrection(**kwargs)
338
+ elif channel == constants.BALANCE_HLS:
339
+ self.correction = LSCorrection(**kwargs)
340
+ else:
341
+ raise InvalidOptionError("channel", channel)
342
+
343
+ def begin(self, process):
344
+ self.process = process
345
+ self.correction.process = process
346
+ img = read_img(self.process.input_full_path + "/" + self.process.filenames[process.ref_idx])
347
+ self.shape = img.shape
348
+ self.correction.begin(img, self.process.counts, process.ref_idx)
349
+
350
+ def end(self):
351
+ self.process.print_message(' ' * 60)
352
+ self.correction.end(self.process.ref_idx)
353
+ if self.plot_summary and self.mask_size > 0:
354
+ shape = self.shape[:2]
355
+ img = np.zeros(shape)
356
+ mask_radius = int(min(*shape) * self.mask_size / 2)
357
+ cv2.circle(img, (shape[1] // 2, shape[0] // 2), mask_radius, 255, -1)
358
+ plt.figure(figsize=(10, 5))
359
+ plt.title('Mask')
360
+ plt.imshow(img, 'gray')
361
+ self.correction.save_summary_plot("mask")
362
+
363
+ def run_frame(self, idx, ref_idx, image):
364
+ if idx != self.process.ref_idx:
365
+ self.process.sub_message_r(': balance image')
366
+ image = self.correction.apply_correction(idx, image)
367
+ return image
@@ -0,0 +1,22 @@
1
+ import os
2
+ from config.config import config
3
+
4
+ if not config.DISABLE_TQDM:
5
+ from tqdm import tqdm
6
+ from tqdm.notebook import tqdm_notebook
7
+
8
+
9
+ def check_path_exists(path):
10
+ if not os.path.exists(path):
11
+ raise Exception('Path does not exist: ' + path)
12
+
13
+
14
+ def make_tqdm_bar(name, size, ncols=80):
15
+ if not config.DISABLE_TQDM:
16
+ if config.JUPYTER_NOTEBOOK:
17
+ bar = tqdm_notebook(desc=name, total=size)
18
+ else:
19
+ bar = tqdm(desc=name, total=size, ncols=ncols)
20
+ return bar
21
+ else:
22
+ return None
@@ -0,0 +1,164 @@
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 ImageLoadError, InvalidOptionError, RunStopException
6
+ from .utils import read_img, get_img_metadata, validate_image, img_bw
7
+
8
+
9
+ class DepthMapStack:
10
+ def __init__(self, map_type=constants.DEFAULT_DM_MAP, energy=constants.DEFAULT_DM_ENERGY,
11
+ kernel_size=constants.DEFAULT_DM_KERNEL_SIZE, blur_size=constants.DEFAULT_DM_BLUR_SIZE,
12
+ smooth_size=constants.DEFAULT_DM_SMOOTH_SIZE, temperature=constants.DEFAULT_DM_TEMPERATURE,
13
+ levels=constants.DEFAULT_DM_LEVELS, float_type=constants.DEFAULT_DM_FLOAT):
14
+ self.map_type = map_type
15
+ self.energy = energy
16
+ self.kernel_size = kernel_size
17
+ self.blur_size = blur_size
18
+ self.smooth_size = smooth_size
19
+ self.temperature = temperature
20
+ self.levels = levels
21
+ if float_type == constants.FLOAT_32:
22
+ self.float_type = np.float32
23
+ elif float_type == constants.FLOAT_64:
24
+ self.float_type = np.float64
25
+ else:
26
+ raise InvalidOptionError("float_type", float_type, details=" valid values are FLOAT_32 and FLOAT_64")
27
+
28
+ def name(self):
29
+ return "depth map"
30
+
31
+ def steps_per_frame(self):
32
+ return 2
33
+
34
+ def print_message(self, msg):
35
+ self.process.sub_message_r(color_str(msg, "light_blue"))
36
+
37
+ def get_sobel_map(self, gray_images):
38
+ energies = np.zeros(gray_images.shape, dtype=self.float_type)
39
+ for i in range(gray_images.shape[0]):
40
+ img = gray_images[i]
41
+ energies[i] = np.abs(cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)) + \
42
+ np.abs(cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3))
43
+ return energies
44
+
45
+ def get_laplacian_map(self, gray_images):
46
+ laplacian = np.zeros(gray_images.shape, dtype=self.float_type)
47
+ for i in range(gray_images.shape[0]):
48
+ blurred = cv2.GaussianBlur(gray_images[i], (self.blur_size, self.blur_size), 0)
49
+ laplacian[i] = np.abs(cv2.Laplacian(blurred, cv2.CV_64F, ksize=self.kernel_size))
50
+ return laplacian
51
+
52
+ def smooth_energy(self, energy_map):
53
+ if self.smooth_size <= 0:
54
+ return energy_map
55
+ smoothed = np.zeros(energy_map.shape, dtype=np.float32)
56
+ for i in range(energy_map.shape[0]):
57
+ energy_32f = energy_map[i].astype(np.float32)
58
+ smoothed_32f = cv2.bilateralFilter(energy_32f, self.smooth_size, 25, 25)
59
+ smoothed[i] = smoothed_32f.astype(energy_map.dtype)
60
+ return smoothed
61
+
62
+ def get_focus_map(self, energies):
63
+ if self.map_type == constants.DM_MAP_AVERAGE:
64
+ sum_energies = np.sum(energies, axis=0)
65
+ return np.divide(energies, sum_energies, where=sum_energies != 0)
66
+ elif self.map_type == constants.DM_MAP_MAX:
67
+ max_energy = np.max(energies, axis=0)
68
+ relative = np.exp((energies - max_energy) / self.temperature)
69
+ return relative / np.sum(relative, axis=0)
70
+ else:
71
+ raise InvalidOptionError("map_type", self.map_type, details=f" valid values are "
72
+ f"{constants.DM_MAP_AVERAGE} and {constants.DM_MAP_MAX}.")
73
+
74
+ def pyramid_blend(self, images, weights):
75
+ blended = None
76
+ for i in range(images.shape[0]):
77
+ img = images[i].astype(self.float_type)
78
+ weight = weights[i]
79
+ gp_img = [img]
80
+ gp_weight = [weight]
81
+ for _ in range(self.levels - 1):
82
+ gp_img.append(cv2.pyrDown(gp_img[-1]))
83
+ gp_weight.append(cv2.pyrDown(gp_weight[-1]))
84
+ lp_img = [gp_img[-1]]
85
+ for j in range(self.levels - 1, 0, -1):
86
+ size = (gp_img[j - 1].shape[1], gp_img[j - 1].shape[0])
87
+ expanded = cv2.pyrUp(gp_img[j], dstsize=size)
88
+ lp_img.append(gp_img[j - 1] - expanded)
89
+ current_blend = []
90
+ for j in range(self.levels):
91
+ w = gp_weight[self.levels - 1 - j][..., np.newaxis]
92
+ current_blend.append(lp_img[j] * w)
93
+ if blended is None:
94
+ blended = current_blend
95
+ else:
96
+ for j in range(self.levels):
97
+ blended[j] += current_blend[j]
98
+ result = blended[0]
99
+ for j in range(1, self.levels):
100
+ size = (blended[j].shape[1], blended[j].shape[0])
101
+ result = cv2.pyrUp(result, dstsize=size) + blended[j]
102
+ return result
103
+
104
+ def focus_stack(self, filenames):
105
+ gray_images = []
106
+ metadata = None
107
+ for i, img_path in enumerate(filenames):
108
+ self.print_message(': reading file (1/2) {}'.format(img_path.split('/')[-1]))
109
+ img = read_img(img_path)
110
+ if img is None:
111
+ raise ImageLoadError(img_path)
112
+ if metadata is None:
113
+ metadata = get_img_metadata(img)
114
+ else:
115
+ validate_image(img, *metadata)
116
+ gray = img_bw(img)
117
+ gray_images.append(gray)
118
+ self.process.callback('after_step', self.process.id, self.process.name, i)
119
+ if self.process.callback('check_running', self.process.id, self.process.name) is False:
120
+ raise RunStopException(self.name)
121
+ dtype = metadata[1]
122
+ gray_images = np.array(gray_images, dtype=self.float_type)
123
+ if self.energy == constants.DM_ENERGY_SOBEL:
124
+ energies = self.get_sobel_map(gray_images)
125
+ elif self.energy == constants.DM_ENERGY_LAPLACIAN:
126
+ energies = self.get_laplacian_map(gray_images)
127
+ else:
128
+ raise InvalidOptionError('energy', self.energy, details=f" valid values are "
129
+ f"{constants.DM_ENERGY_SOBEL} and {constants.DM_ENERGY_LAPLACIAN}.")
130
+ max_energy = np.max(energies)
131
+ if max_energy > 0:
132
+ energies = energies / max_energy
133
+ if self.smooth_size > 0:
134
+ energies = self.smooth_energy(energies)
135
+ weights = self.get_focus_map(energies)
136
+ blended_pyramid = None
137
+ for i, img_path in enumerate(filenames):
138
+ self.print_message(': reading file (2/2) {}'.format(img_path.split('/')[-1]))
139
+ img = read_img(img_path).astype(self.float_type)
140
+ weight = weights[i]
141
+ gp_img = [img]
142
+ gp_weight = [weight]
143
+ for _ in range(self.levels - 1):
144
+ gp_img.append(cv2.pyrDown(gp_img[-1]))
145
+ gp_weight.append(cv2.pyrDown(gp_weight[-1]))
146
+ lp_img = [gp_img[-1]]
147
+ for j in range(self.levels - 1, 0, -1):
148
+ size = (gp_img[j - 1].shape[1], gp_img[j - 1].shape[0])
149
+ expanded = cv2.pyrUp(gp_img[j], dstsize=size)
150
+ lp_img.append(gp_img[j - 1] - expanded)
151
+ current_blend = [lp_img[j] * gp_weight[self.levels - 1 - j][..., np.newaxis]
152
+ for j in range(self.levels)]
153
+ blended_pyramid = current_blend if blended_pyramid is None \
154
+ else [np.add(bp, cb) for bp, cb in zip(blended_pyramid, current_blend)]
155
+ self.process.callback('after_step', self.process.id, self.process.name, i + len(filenames))
156
+ if self.process.callback('check_running', self.process.id, self.process.name) is False:
157
+ raise RunStopException(self.name)
158
+ result = blended_pyramid[0]
159
+ self.print_message(': blend levels')
160
+ for j in range(1, self.levels):
161
+ size = (blended_pyramid[j].shape[1], blended_pyramid[j].shape[0])
162
+ result = cv2.pyrUp(result, dstsize=size) + blended_pyramid[j]
163
+ n_values = 255 if dtype == np.uint8 else 65535
164
+ return np.clip(np.absolute(result), 0, n_values).astype(dtype)