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.
- shinestacker/__init__.py +3 -0
- shinestacker/_version.py +1 -0
- shinestacker/algorithms/__init__.py +14 -0
- shinestacker/algorithms/align.py +307 -0
- shinestacker/algorithms/balance.py +367 -0
- shinestacker/algorithms/core_utils.py +22 -0
- shinestacker/algorithms/depth_map.py +164 -0
- shinestacker/algorithms/exif.py +238 -0
- shinestacker/algorithms/multilayer.py +187 -0
- shinestacker/algorithms/noise_detection.py +182 -0
- shinestacker/algorithms/pyramid.py +176 -0
- shinestacker/algorithms/stack.py +112 -0
- shinestacker/algorithms/stack_framework.py +248 -0
- shinestacker/algorithms/utils.py +71 -0
- shinestacker/algorithms/vignetting.py +137 -0
- shinestacker/app/__init__.py +0 -0
- shinestacker/app/about_dialog.py +24 -0
- shinestacker/app/app_config.py +39 -0
- shinestacker/app/gui_utils.py +35 -0
- shinestacker/app/help_menu.py +16 -0
- shinestacker/app/main.py +176 -0
- shinestacker/app/open_frames.py +39 -0
- shinestacker/app/project.py +91 -0
- shinestacker/app/retouch.py +82 -0
- shinestacker/config/__init__.py +4 -0
- shinestacker/config/config.py +53 -0
- shinestacker/config/constants.py +174 -0
- shinestacker/config/gui_constants.py +85 -0
- shinestacker/core/__init__.py +5 -0
- shinestacker/core/colors.py +60 -0
- shinestacker/core/core_utils.py +52 -0
- shinestacker/core/exceptions.py +50 -0
- shinestacker/core/framework.py +210 -0
- shinestacker/core/logging.py +89 -0
- shinestacker/gui/__init__.py +0 -0
- shinestacker/gui/action_config.py +879 -0
- shinestacker/gui/actions_window.py +283 -0
- shinestacker/gui/colors.py +57 -0
- shinestacker/gui/gui_images.py +152 -0
- shinestacker/gui/gui_logging.py +213 -0
- shinestacker/gui/gui_run.py +393 -0
- shinestacker/gui/img/close-round-line-icon.png +0 -0
- shinestacker/gui/img/forward-button-icon.png +0 -0
- shinestacker/gui/img/play-button-round-icon.png +0 -0
- shinestacker/gui/img/plus-round-line-icon.png +0 -0
- shinestacker/gui/main_window.py +599 -0
- shinestacker/gui/new_project.py +170 -0
- shinestacker/gui/project_converter.py +148 -0
- shinestacker/gui/project_editor.py +539 -0
- shinestacker/gui/project_model.py +138 -0
- shinestacker/retouch/__init__.py +0 -0
- shinestacker/retouch/brush.py +9 -0
- shinestacker/retouch/brush_controller.py +57 -0
- shinestacker/retouch/brush_preview.py +126 -0
- shinestacker/retouch/exif_data.py +65 -0
- shinestacker/retouch/file_loader.py +104 -0
- shinestacker/retouch/image_editor.py +651 -0
- shinestacker/retouch/image_editor_ui.py +380 -0
- shinestacker/retouch/image_viewer.py +356 -0
- shinestacker/retouch/shortcuts_help.py +98 -0
- shinestacker/retouch/undo_manager.py +38 -0
- shinestacker-0.2.0.post1.dev1.dist-info/METADATA +55 -0
- shinestacker-0.2.0.post1.dev1.dist-info/RECORD +67 -0
- shinestacker-0.2.0.post1.dev1.dist-info/WHEEL +5 -0
- shinestacker-0.2.0.post1.dev1.dist-info/entry_points.txt +4 -0
- shinestacker-0.2.0.post1.dev1.dist-info/licenses/LICENSE +1 -0
- 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)
|