Artyom commited on
Commit
e91104d
1 Parent(s): 82567db
IVL/Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+
3
+ FROM python:3.10-slim-buster
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache -r requirements.txt
7
+
8
+ RUN apt-get update
9
+ RUN apt-get install ffmpeg libsm6 libxext6 libopenblas-dev -y
10
+
11
+ COPY . /pipe24
12
+ WORKDIR /pipe24
IVL/Grayness_Index.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import sys
5
+
6
+
7
+ def DerivGauss(im, sigma=0.5):
8
+ gaussian_die_off = 0.000001
9
+ var = sigma ** 2
10
+ # compute filter width
11
+ width = None
12
+ for i in range(1, 51):
13
+ if np.exp(-(i ** 2) / (2 * var)) > gaussian_die_off:
14
+ width = i
15
+ if width is None:
16
+ width = 1
17
+
18
+ # create filter (derivative of Gaussian filter)
19
+ x = np.arange(-width, width + 1)
20
+ y = np.arange(-width, width + 1)
21
+ coordinates = np.meshgrid(x, y)
22
+ x = coordinates[0]
23
+ y = coordinates[1]
24
+ derivate_gaussian_2D = -x * \
25
+ np.exp(-(x * x + y * y) / (2 * var)) / (var * np.pi)
26
+
27
+ # apply filter and return magnitude
28
+ ax = cv2.filter2D(im, -1, derivate_gaussian_2D)
29
+ ay = cv2.filter2D(im, -1, np.transpose(derivate_gaussian_2D))
30
+ magnitude = np.sqrt((ax ** 2) + (ay ** 2))
31
+ return magnitude
32
+
33
+
34
+ def GPconstancy_GI(im, gray_pixels, delta_th=10**(-4)):
35
+
36
+ # mask saturated pixels and mask very dark pixels
37
+ mask = np.logical_or(np.max(im, axis=2) >= 0.95,
38
+ np.sum(im, axis=2) <= 0.0315)
39
+
40
+ # remove noise with mean filter
41
+ # mean_kernel = np.ones((7, 7), np.float32) / 7**2
42
+ # im = cv2.filter2D(im, -1, mean_kernel)
43
+
44
+ # decompose rgb values
45
+ r = im[:, :, 0]
46
+ g = im[:, :, 1]
47
+ b = im[:, :, 2]
48
+
49
+ # mask 0 elements
50
+ mask = np.logical_or.reduce((mask, r == 0, g == 0, b == 0))
51
+
52
+ # replace 0 values with machine epsilon
53
+ eps = np.finfo(np.float32).eps
54
+ r[r == 0] = eps
55
+ g[g == 0] = eps
56
+ b[b == 0] = eps
57
+ norm = r + g + b
58
+
59
+ # mask low contrast pixels
60
+ delta_r = DerivGauss(r)
61
+ delta_g = DerivGauss(g)
62
+ delta_b = DerivGauss(b)
63
+ mask = np.logical_or(mask, np.logical_and.reduce(
64
+ (delta_r <= delta_th, delta_g <= delta_th, delta_b <= delta_th)))
65
+
66
+ # compute colors in log domain, only red and blue
67
+ log_r = np.log(r) - np.log(norm)
68
+ log_b = np.log(b) - np.log(norm)
69
+
70
+ # mask low contrast pixels in the log domain
71
+ delta_log_r = DerivGauss(log_r)
72
+ delta_log_b = DerivGauss(log_b)
73
+ mask = np.logical_or.reduce(
74
+ (mask, delta_log_r == np.inf, delta_log_b == np.inf))
75
+
76
+ # normalize each channel in log domain
77
+ data = np.concatenate(
78
+ (np.reshape(delta_log_r, (-1, 1)), np.reshape(delta_log_b, (-1, 1))), axis=1)
79
+ mink_norm = 2
80
+ norm2_data = np.sum(data ** mink_norm, axis=1) ** (1 / mink_norm)
81
+ map_uniquelight = np.reshape(norm2_data, delta_log_r.shape)
82
+
83
+ # make masked pixels to max value
84
+ map_uniquelight[mask] = np.max(map_uniquelight)
85
+
86
+ # denoise
87
+ # map_uniquelight = cv2.filter2D(map_uniquelight, -1, mean_kernel)
88
+
89
+ # filter using map_uniquelight
90
+ gray_index_unique = map_uniquelight
91
+ sort_unique = np.sort(gray_index_unique.flatten())
92
+ gindex_unique = np.full(gray_index_unique.shape, False, dtype=bool)
93
+ gindex_unique[gray_index_unique <= sort_unique[gray_pixels - 1]] = True
94
+ choosen_pixels = im[gindex_unique]
95
+ mean = np.mean(choosen_pixels, axis=0)
96
+ result = mean / np.apply_along_axis(np.linalg.norm, 0, mean)
97
+ return result
98
+
99
+
100
+ if __name__ == "__main__":
101
+
102
+ # read image and convert to 0 1
103
+ im_path = 'example.png'
104
+ im = cv2.cvtColor(cv2.imread(im_path), cv2.COLOR_BGR2RGB)
105
+ im = np.float32(im) / 255
106
+
107
+ tot_pixels = im.shape[0] * im.shape[1]
108
+ # compute number of gray pixels
109
+ n = 0.1 # 0.01%
110
+ num_gray_pixels = int(np.floor(n * tot_pixels / 100))
111
+
112
+ # compute global illuminant values
113
+ gt = np.array([0.6918, 0.6635, 0.2850])
114
+ lumTriplet = GPconstancy_GI(im, num_gray_pixels, 10**(-4))
115
+
116
+ # show results and angular error w.r.t gt
117
+ print(lumTriplet)
118
+ print(np.arccos(np.dot(lumTriplet, gt)) * 180 / np.pi)
IVL/blocks.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import skimage.color as cl
4
+ from Grayness_Index import GPconstancy_GI
5
+ from scipy.ndimage.filters import gaussian_filter
6
+ from skimage import exposure
7
+ from skimage.restoration import denoise_nl_means, estimate_sigma
8
+
9
+ _RGB_TO_YCBCR = np.array([[0.257, 0.504, 0.098],
10
+ [-0.148, -0.291, 0.439],
11
+ [0.439, -0.368, -0.071]])
12
+
13
+ _YCBCR_OFF = np.array([0.063, 0.502, 0.502])
14
+
15
+
16
+ def _mul(coeffs, image):
17
+
18
+ r = image[:, :, 0]
19
+ g = image[:, :, 1]
20
+ b = image[:, :, 2]
21
+
22
+ r0 = np.repeat(r[:, :, np.newaxis], 3, 2) * coeffs[:, 0]
23
+ r1 = np.repeat(g[:, :, np.newaxis], 3, 2) * coeffs[:, 1]
24
+ r2 = np.repeat(b[:, :, np.newaxis], 3, 2) * coeffs[:, 2]
25
+
26
+ return r0 + r1 + r2
27
+
28
+
29
+ def rgb2ycbcr(rgb):
30
+ """sRGB to YCbCr conversion."""
31
+ clip_rgb = False
32
+ if clip_rgb:
33
+ rgb = np.clip(rgb, 0, 1)
34
+ return _mul(_RGB_TO_YCBCR, rgb) + _YCBCR_OFF
35
+
36
+
37
+ def ycbcr2rgb(rgb):
38
+ """YCbCr to sRGB conversion."""
39
+ clip_rgb = False
40
+ rgb = _mul(np.linalg.inv(_RGB_TO_YCBCR), rgb - _YCBCR_OFF)
41
+ if clip_rgb:
42
+ rgb = np.clip(rgb, 0, 1)
43
+ return rgb
44
+
45
+
46
+ def normalize(raw_image, black_level, white_level):
47
+ if type(black_level) is list and len(black_level) == 1:
48
+ black_level = float(black_level[0])
49
+ if type(white_level) is list and len(white_level) == 1:
50
+ white_level = float(white_level[0])
51
+ black_level_mask = black_level
52
+ if type(black_level) is list and len(black_level) == 4:
53
+ if type(black_level[0]) is Ratio:
54
+ black_level = ratios2floats(black_level)
55
+ if type(black_level[0]) is Fraction:
56
+ black_level = fractions2floats(black_level)
57
+ black_level_mask = np.zeros(raw_image.shape)
58
+ idx2by2 = [[0, 0], [0, 1], [1, 0], [1, 1]]
59
+ step2 = 2
60
+ for i, idx in enumerate(idx2by2):
61
+ black_level_mask[idx[0]::step2, idx[1]::step2] = black_level[i]
62
+ normalized_image = raw_image.astype(np.float32) - black_level_mask
63
+ # if some values were smaller than black level
64
+ normalized_image[normalized_image < 0] = 0
65
+ normalized_image = normalized_image / (white_level - black_level_mask)
66
+ return normalized_image
67
+
68
+
69
+ class LCC():
70
+
71
+ def __init__(self, sigma=None):
72
+ super(LCC, self).__init__()
73
+ if sigma is None:
74
+ sigma = np.sqrt(512 ** 2 + 512 ** 2) * 0.01
75
+ self.sigma = sigma
76
+
77
+ def __call__(self, image):
78
+ ycbcr = cl.rgb2ycbcr(image)
79
+ y = (ycbcr[:, :, 0] - 16) / 219
80
+ cb = ycbcr[:, :, 1]
81
+ cr = ycbcr[:, :, 2]
82
+
83
+ blurred_y = gaussian_filter(y, sigma=self.sigma)
84
+ mask = 1 - blurred_y
85
+
86
+ mean_intensity = np.mean(y)
87
+
88
+ alpha_lower = np.log(mean_intensity) / np.log(0.5)
89
+ alpha_upper = np.log(0.5) / np.log(mean_intensity)
90
+
91
+ condition = mean_intensity < 0.5
92
+ alpha = np.zeros(mask.shape)
93
+ alpha = np.where(condition, alpha_lower, alpha_upper)
94
+
95
+ gamma = alpha ** ((0.5 - mask) / 0.5)
96
+
97
+ new_y = y ** gamma
98
+
99
+ new_y = new_y * 219 + 16
100
+
101
+ new_ycbcr = np.stack([new_y, cb, cr], 2)
102
+
103
+ im_rgb = cl.ycbcr2rgb(new_ycbcr)
104
+ # im_rgb = np.clip(im_rgb, 0, 1)
105
+
106
+ im_out = contrast_saturation_fix(im_rgb, image)
107
+
108
+ return im_out
109
+
110
+
111
+ def contrast_saturation_fix(enhanced_image, input_image, mode="LCC", n_bits=8):
112
+
113
+ im_ycbcr = rgb2ycbcr(enhanced_image)
114
+ or_ycbcr = rgb2ycbcr(input_image)
115
+
116
+ y_new = im_ycbcr[:, :, 0];
117
+ cb_new = im_ycbcr[:, :, 1];
118
+ cr_new = im_ycbcr[:, :, 2];
119
+
120
+ y = or_ycbcr[:, :, 0];
121
+ cb = or_ycbcr[:, :, 1];
122
+ cr = or_ycbcr[:, :, 2];
123
+
124
+ # dark pixels percentage
125
+ mask = np.logical_and(y < (35 / 255), (((cb - 0.5) * 2 +
126
+ (cr - 0.5) * 2) / 2) < (20 / 255))
127
+
128
+ dark_pixels = mask.flatten().sum()
129
+
130
+ if dark_pixels > 0:
131
+
132
+ ipixelCount, _ = np.histogram(y.flatten(), 256, range=(0, 1))
133
+ cdf = np.cumsum(ipixelCount)
134
+ idx = np.argmin(abs(cdf - (dark_pixels * 0.3)))
135
+ b_input30 = idx
136
+
137
+ ipixelCount, _ = np.histogram(y_new.flatten(), 256, range=(0, 1))
138
+ cdf = np.cumsum(ipixelCount)
139
+ idx = np.argmin(abs(cdf - (dark_pixels * 0.3)))
140
+ b_output30 = idx
141
+
142
+ bstr = (b_output30 - b_input30)
143
+ else:
144
+
145
+ bstr = np.floor(np.quantile(y_new.flatten(), 0.002) * 255)
146
+
147
+ if bstr > 50:
148
+ bstr = 50
149
+
150
+ dark_bound = bstr / 255
151
+
152
+ bright_b = np.floor(np.quantile(y_new.flatten(), 1 - 0.002) * 255)
153
+
154
+ if (255 - bright_b) > 50:
155
+ bright_b = 255 - 50
156
+
157
+ bright_bound = bright_b / 255
158
+
159
+ # y_new = (y_new - dark_bound) / (bright_bound - dark_bound)
160
+ y_new = exposure.rescale_intensity(y_new, in_range=(
161
+ y_new.min(), y_new.max()), out_range=(dark_bound, bright_bound))
162
+ y_new = y_new.clip(0, 1)
163
+
164
+ im_ycbcr[:, :, 0] = y_new
165
+ im_new = ycbcr2rgb(im_ycbcr)
166
+
167
+ im_new = im_new.clip(0, 1)
168
+
169
+ # Saturation
170
+
171
+ im_tmp = input_image
172
+
173
+ r = im_tmp[:, :, 0]
174
+ g = im_tmp[:, :, 1]
175
+ b = im_tmp[:, :, 2]
176
+
177
+ r_new = 0.5 * (((y_new / (y + 1e-40)) * (r + y)) + r - y)
178
+ g_new = 0.5 * (((y_new / (y + 1e-40)) * (g + y)) + g - y)
179
+ b_new = 0.5 * (((y_new / (y + 1e-40)) * (b + y)) + b - y)
180
+
181
+ im_new[:, :, 0] = r_new
182
+ im_new[:, :, 1] = g_new
183
+ im_new[:, :, 2] = b_new
184
+
185
+ return im_new
186
+
187
+
188
+ def gamma_correction(img, exp):
189
+
190
+ return img ** exp
191
+
192
+
193
+ def black_stretch(img, perc=0.2):
194
+
195
+ im_hsv = cl.rgb2hsv(img.clip(0, 1))
196
+ v = im_hsv[:, :, 2]
197
+
198
+ dark_bound = np.quantile(v.flatten(), perc, method='closest_observation')
199
+
200
+ v_new = (v - dark_bound) / (1 - dark_bound)
201
+
202
+ im_hsv[:, :, 2] = v_new.clip(0, 1)
203
+
204
+ out = cl.hsv2rgb(im_hsv)
205
+
206
+ return out.clip(0, 1)
207
+
208
+
209
+ def saturation_scale(img, scale=2.):
210
+
211
+ img_hsv = cl.rgb2hsv(img.clip(0, 1))
212
+ s = img_hsv[:, :, 1]
213
+ s *= scale
214
+ img_hsv[:, :, 1] = s
215
+
216
+ return cl.hsv2rgb(np.clip(img_hsv, 0, 1))
217
+
218
+
219
+ def global_mean_contrast(x, beta=0.5):
220
+
221
+ x_mean = np.mean(np.mean(x, 0), 0)
222
+ x_mean = np.expand_dims(np.expand_dims(x_mean, 0), 0)
223
+ x_mean = np.repeat(np.repeat(x_mean, x.shape[1], 1), x.shape[0], 0)
224
+
225
+ # scale all channels
226
+ out = x_mean + beta * (x - x_mean)
227
+
228
+ return out
229
+
230
+
231
+ def sharpening(image, sigma=2.0, scale=1):
232
+
233
+ gaussian = cv2.GaussianBlur(image, (0, 0), sigma)
234
+
235
+ unsharp_image = image + scale * (image - gaussian)
236
+
237
+ return unsharp_image.clip(0, 1)
238
+
239
+
240
+ def illumination_parameters_estimation(current_image, illumination_estimation_option):
241
+ ie_method = illumination_estimation_option.lower()
242
+ if ie_method == "gw":
243
+ ie = np.mean(current_image, axis=(0, 1))
244
+ ie /= ie[1]
245
+ return ie
246
+ elif ie_method == "sog":
247
+ sog_p = 4.
248
+ ie = np.mean(current_image**sog_p, axis=(0, 1))**(1 / sog_p)
249
+ ie /= ie[1]
250
+ return ie
251
+ elif ie_method == "wp":
252
+ ie = np.max(current_image, axis=(0, 1))
253
+ ie /= ie[1]
254
+ return ie
255
+ elif ie_method == "iwp":
256
+ samples_count = 20
257
+ sample_size = 20
258
+ rows, cols = current_image.shape[:2]
259
+ data = np.reshape(current_image, (rows * cols, 3))
260
+ maxima = np.zeros((samples_count, 3))
261
+ for i in range(samples_count):
262
+ maxima[i, :] = np.max(data[np.random.randint(
263
+ low=0, high=rows * cols, size=(sample_size)), :], axis=0)
264
+ ie = np.mean(maxima, axis=0)
265
+ ie /= ie[1]
266
+ return ie
267
+ else:
268
+ raise ValueError(
269
+ 'Bad illumination_estimation_option value! Use the following options: "gw", "wp", "sog", "iwp"')
270
+
271
+
272
+ def wb(demosaic_img, as_shot_neutral):
273
+
274
+ as_shot_neutral = np.asarray(as_shot_neutral)
275
+ # transform vector into matrix
276
+ if as_shot_neutral.shape == (3,):
277
+ as_shot_neutral = np.diag(1. / as_shot_neutral)
278
+
279
+ assert as_shot_neutral.shape == (3, 3)
280
+
281
+ white_balanced_image = np.dot(demosaic_img, as_shot_neutral.T)
282
+ white_balanced_image = np.clip(white_balanced_image, 0.0, 1.0)
283
+
284
+ return white_balanced_image
285
+
286
+
287
+ def white_balance(img, n=0.1, th=1e-4, denoise_first=False):
288
+
289
+ uint = False
290
+ if np.issubdtype(img.dtype, np.uint8):
291
+ uint = True
292
+
293
+ if uint:
294
+ img = img.astype(np.float32) / 255
295
+
296
+ tot_pixels = img.shape[0] * img.shape[1]
297
+ # compute number of gray pixels
298
+ num_gray_pixels = int(np.floor(n * tot_pixels / 100))
299
+
300
+ # denoise if necessary
301
+ if denoise_first:
302
+ sigma_est = 1# np.mean(estimate_sigma(img, channel_axis=-1))
303
+ img_ = cv2.GaussianBlur(img,(0,0),5)
304
+ else:
305
+ img_ = img
306
+
307
+ # compute global illuminant values
308
+ lumTriplet = GPconstancy_GI(img_, num_gray_pixels, th)
309
+
310
+ lumTriplet /= lumTriplet.max()
311
+ out = wb(img, lumTriplet)
312
+
313
+ if uint:
314
+ return (out * 255).astype(np.uint8)
315
+ else:
316
+ return out
317
+
318
+
319
+ def scurve(img, alpha=None, lmbd=1 / 1.8, blacks=False):
320
+
321
+ x = img
322
+
323
+ if alpha is None:
324
+ im_hsv = cl.rgb2hsv(img.clip(0, 1))
325
+ v = im_hsv[:, :, 2]
326
+
327
+ alpha = np.quantile(v.flatten(), 0.02, method='closest_observation')
328
+
329
+ if not blacks:
330
+ out = np.where(x <= alpha,
331
+ x, # alpha - alpha * (1 - x / alpha) ** lmbd,
332
+ alpha + (1 - alpha) *
333
+ ((x - alpha) / (1 - alpha)).clip(min=0) ** lmbd
334
+ )
335
+ else:
336
+ out = np.where(x <= alpha,
337
+ alpha - alpha * (1 - x / alpha) ** lmbd,
338
+ x
339
+ )
340
+
341
+ # out = out.clip(0, 1)
342
+
343
+ return out
344
+
345
+
346
+ def scurve_central(img, lmbd=1 / 1.4, blacks=False):
347
+
348
+ x = img
349
+
350
+ im_hsv = cl.rgb2hsv(img.clip(0, 1))
351
+ v = im_hsv[:, :, 2]
352
+
353
+ alpha1 = np.quantile(v.flatten(), 0.2, method='closest_observation')
354
+ alpha2 = np.quantile(v.flatten(), 0.9, method='closest_observation')
355
+
356
+ out = np.where(x <= alpha1,
357
+ x,
358
+ np.where(x >= alpha2,
359
+ x,
360
+ alpha1 + (alpha2 - alpha1) *
361
+ ((x - alpha1) / (alpha2 - alpha1)).clip(min=0) ** lmbd
362
+ )
363
+ )
364
+
365
+ return out
366
+
367
+
368
+ def imadjust(img, hi=0.9999, pi=0.0001):
369
+ '''
370
+ Python version of matlab imadjust
371
+ '''
372
+
373
+ im_hsv = cl.rgb2hsv(img.clip(0, 1))
374
+ v = im_hsv[:, :, 2]
375
+
376
+ hi = np.quantile(v.flatten(), hi, method='closest_observation')
377
+ li = np.quantile(v.flatten(), pi, method='closest_observation')
378
+
379
+ if hi < 0.7:
380
+ hi = np.quantile(v.flatten(), 0.995, method='closest_observation')
381
+
382
+ if hi == 1:
383
+ v_tmp = v.flatten()
384
+ v_tmp = v_tmp[v_tmp != 1]
385
+ hi = np.quantile(v_tmp, 0.9995, method='closest_observation')
386
+ if li == 0:
387
+ v_tmp = v.flatten()
388
+ v_tmp = v_tmp[v_tmp != 0]
389
+ li = np.quantile(v_tmp, 0.0001, method='closest_observation')
390
+
391
+ x = img
392
+ li = li
393
+ hi = hi
394
+
395
+ lo = 0
396
+ ho = 0.9
397
+ gamma = 1
398
+
399
+ out = ((x - li) / (hi - li)) ** gamma
400
+ out = out * (ho - lo) + lo
401
+
402
+ return out
403
+
404
+
405
+ def denoise_raw(image, l_w=3, ch_w=20):
406
+
407
+ im_yuv = cl.rgb2yuv(image)
408
+
409
+ # Separately process luma and choma
410
+
411
+ patch_kw = dict(patch_size=5,
412
+ patch_distance=6
413
+ )
414
+ sigma_est = np.mean(estimate_sigma(im_yuv[:, :, 0]))
415
+
416
+ den_y = denoise_nl_means(im_yuv[:, :, 0], h=l_w * sigma_est, fast_mode=True,
417
+ **patch_kw)
418
+
419
+ patch_kw = dict(patch_size=5,
420
+ patch_distance=6,
421
+ channel_axis=-1
422
+ )
423
+ sigma_est = np.mean(estimate_sigma(im_yuv[:, :, 1:2], channel_axis=-1))
424
+ den_uv = denoise_nl_means(im_yuv[:, :, 1:3], h=ch_w * sigma_est, fast_mode=True,
425
+ **patch_kw)
426
+
427
+ out = im_yuv
428
+
429
+ out[:, :, 0] = den_y
430
+ out[:, :, 1:3] = den_uv
431
+
432
+ del den_y
433
+ del den_uv
434
+
435
+ out = cl.yuv2rgb(out)
436
+
437
+ return out
438
+
439
+ def denoise_rgb(image, l_w=3, ch_w=None):
440
+
441
+ #patch_kw = dict(patch_size=5,
442
+ # patch_distance=6
443
+ # )
444
+ patch_kw = {}
445
+ sigma_est = np.mean(estimate_sigma(image, channel_axis=2))
446
+ out = denoise_nl_means(image, h=l_w * sigma_est, fast_mode=True,
447
+ **patch_kw, channel_axis=2)
448
+
449
+
450
+ return out
IVL/pipeline24.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ###################################################
2
+ # Night challenge 2024
3
+ ###################################################
4
+
5
+
6
+ import argparse
7
+ import threading
8
+ from pathlib import Path
9
+ import os
10
+ import queue
11
+ from time import time
12
+ from tqdm import tqdm
13
+
14
+ import blocks as B
15
+ import cv2
16
+ import numpy as np
17
+ import skimage.color as cl
18
+ from skimage.transform import resize
19
+ from raw_prc_pipeline import (expected_img_ext, expected_landscape_img_height,
20
+ expected_landscape_img_width)
21
+ from raw_prc_pipeline.pipeline import RawProcessingPipelineDemo
22
+ from tqdm import tqdm
23
+ from utils import fraction_from_json, json_read
24
+
25
+
26
+ def parse_args():
27
+ parser = argparse.ArgumentParser(
28
+ description='Script for processing PNG images with given metadata files.')
29
+ # folder params
30
+ parser.add_argument('-p', '--png_dir', type=Path, required=True,
31
+ help='Path of the directory containing PNG images with metadata files.')
32
+ parser.add_argument('-o', '--out_dir', type=Path, default='./results',
33
+ help='Path to the directory where processed images will be saved. Images will be saved in JPG format.')
34
+ # raw processing params
35
+ parser.add_argument('-ie', '--illumination_estimation', type=str, default='',
36
+ help='Options for illumination estimation algorithms: "gw", "wp", "sog", "iwp".')
37
+ parser.add_argument('-tm', '--tone_mapping', type=str, default='Flash',
38
+ help='Options for tone mapping algorithms: "Base", "Flash", "Storm", "Linear", "Drago", "Mantiuk", "Reinhard".')
39
+ # srgb processing params
40
+ parser.add_argument('-gc', '--gamma_correction', type=float, default=1 / 1.4,
41
+ help='Global gamma correction.')
42
+ parser.add_argument('-dm', '--denoise_mask', type=float, default=0.6,
43
+ help='Value to control denoising effect in bright regions. Should be between 0 and 1')
44
+ args = parser.parse_args()
45
+
46
+ if args.out_dir is None:
47
+ args.out_dir = args.png_dir
48
+
49
+ return args
50
+
51
+
52
+ class PNGProcessing():
53
+ def __init__(self, ie_method, tone_mapping, gamma_correction, denoise_mask):
54
+ self.pipeline_params = {
55
+ 'illumination_estimation': ie_method,
56
+ # 'tone_mapping': tone_mapping,
57
+ 'out_landscape_width': expected_landscape_img_width,
58
+ 'out_landscape_height': expected_landscape_img_height
59
+ }
60
+
61
+ self.pipeline = RawProcessingPipelineDemo(**self.pipeline_params)
62
+ self.gamma_correction = gamma_correction
63
+ self.denoise_mask = denoise_mask
64
+
65
+ def pipeline_exec(self, raw_image, metadata):
66
+
67
+ normalized_image = self.pipeline.normalize(raw_image, metadata)
68
+
69
+ demosaiced_image = self.pipeline.demosaic(normalized_image, metadata)
70
+ # check the original demosaicing to see if results are the same
71
+
72
+ demosaiced_image = resize(demosaiced_image, (768, 1024), preserve_range=True, anti_aliasing=True)
73
+
74
+ wb_image = self.pipeline.white_balance(demosaiced_image, metadata)
75
+
76
+ xyz_image = self.pipeline.xyz_transform(wb_image, metadata)
77
+ srgb_image = self.pipeline.srgb_transform(xyz_image, metadata)
78
+
79
+ denoised_image = B.denoise_raw(
80
+ srgb_image, l_w=1, ch_w=7)
81
+ # srgb_image, l_w=4.5, ch_w=20)
82
+ # srgb_image, l_w=1.659923974475318, ch_w=5.459274910995606)
83
+
84
+ light_enhancer = B.LCC(2)
85
+ # light_enhancer = B.LCC(sigma=6.463076463115174)
86
+ light_image = light_enhancer(denoised_image).clip(0)
87
+
88
+ contrast_image = B.global_mean_contrast(light_image, beta=1.5).clip(0)
89
+ # contrast_image = B.global_mean_contrast(light_image, beta=0.8653634653721171).clip(0)
90
+
91
+ gamma_image = B.scurve(contrast_image, alpha=0, lmbd=(1 / 1.8)).clip(0)
92
+ # gamma_image = B.scurve(contrast_image, alpha=0.7050463096367395, lmbd=0.9740931227248038).clip(0)
93
+
94
+ black_adj_image = B.imadjust(gamma_image, 0.99).clip(0)
95
+ # black_adj_image = B.imadjust(gamma_image, 0.9957007298972433, 0.01697803128505186).clip(0)
96
+
97
+ im_h = cl.rgb2hsv(black_adj_image)[:, :, 2]
98
+ if im_h.mean() < 0.2:
99
+ black_adj_image = B.scurve_central(black_adj_image, lmbd=(1 / 1.8)).clip(0)
100
+ # black_adj_image = B.scurve_central(black_adj_image, lmbd=0.6913136563678325).clip(0)
101
+ elif im_h.mean() < 0.25:
102
+ black_adj_image = B.scurve_central(black_adj_image, lmbd=(1 / 1.4)).clip(0)
103
+ # black_adj_image = B.scurve_central(black_adj_image, lmbd=0.3612134419536918).clip(0)
104
+ elif im_h.mean() > 0.4:
105
+ black_adj_image = B.gamma_correction(black_adj_image, 1.6).clip(0)
106
+ # black_adj_image = B.gamma_correction(black_adj_image, 3.5208650132731356).clip(0)
107
+
108
+ sharp_image = B.sharpening(black_adj_image, sigma=1).clip(0)
109
+ # sharp_image = B.sharpening(black_adj_image, 1.5389081796026578, 0.05456721376794549).clip(0)
110
+
111
+ wb_image = B.white_balance(sharp_image, denoise_first=True).clip(0)
112
+ # wb_image = B.white_balance(sharp_image, 0.740831363817609, 0.004044358054560114).clip(0)
113
+
114
+ uint8_image = self.pipeline.to_uint8(wb_image, metadata)
115
+ # resized_image = self.pipeline.resize(uint8_image, metadata)
116
+ resulted_image = self.pipeline.fix_orientation(uint8_image, metadata)
117
+
118
+
119
+ return resulted_image
120
+
121
+ def __call__(self, png_path: Path, out_path: Path):
122
+
123
+ # parse raw img
124
+ raw_image = cv2.imread(str(png_path), cv2.IMREAD_UNCHANGED)
125
+ # parse metadata
126
+ metadata = json_read(png_path.with_suffix(
127
+ '.json'), object_hook=fraction_from_json)
128
+
129
+ start = time()
130
+ output_image = self.pipeline_exec(raw_image, metadata)
131
+ end = time()
132
+
133
+ # save results
134
+ output_image = cv2.cvtColor(output_image, cv2.COLOR_RGB2BGR)
135
+ cv2.imwrite(str(out_path), output_image, [
136
+ cv2.IMWRITE_JPEG_QUALITY, 100])
137
+ return end - start
138
+
139
+
140
+ def process(png_processor, out_dir, png_paths):
141
+ out_paths = [
142
+ out_dir / png_path.with_suffix(expected_img_ext).name for png_path in png_paths]
143
+ times = []
144
+ pbar = tqdm(total=len(png_paths), ncols=100)
145
+ for png_path, out_path in zip(png_paths, out_paths):
146
+ runtime = png_processor(png_path, out_path)
147
+ times.append(runtime)
148
+ pbar.update()
149
+ return times
150
+
151
+
152
+ def main(png_dir, out_dir, illumination_estimation, tone_mapping, gamma_correction, denoise_mask):
153
+ # out_dir.mkdir(exist_ok=True)
154
+ os.makedirs(out_dir, exist_ok=True)
155
+
156
+ png_paths = list(png_dir.glob('*.png'))
157
+
158
+ png_processor = PNGProcessing(
159
+ illumination_estimation, tone_mapping, gamma_correction, denoise_mask)
160
+
161
+ times = process(png_processor, out_dir, png_paths)
162
+ print(f'Average time: {np.mean(times)} seconds.')
163
+
164
+
165
+ if __name__ == '__main__':
166
+ args = parse_args()
167
+ main(**vars((args)))
IVL/raw_prc_pipeline/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ expected_img_ext = '.jpg'
2
+ expected_landscape_img_height = 866
3
+ expected_landscape_img_width = 1300
IVL/raw_prc_pipeline/__pycache__/__init__.cpython-38.pyc ADDED
Binary file (262 Bytes). View file
 
IVL/raw_prc_pipeline/__pycache__/__init__.cpython-39.pyc ADDED
Binary file (295 Bytes). View file
 
IVL/raw_prc_pipeline/__pycache__/exif_data_formats.cpython-38.pyc ADDED
Binary file (1.03 kB). View file
 
IVL/raw_prc_pipeline/__pycache__/exif_utils.cpython-38.pyc ADDED
Binary file (5.37 kB). View file
 
IVL/raw_prc_pipeline/__pycache__/fs.cpython-38.pyc ADDED
Binary file (1.62 kB). View file
 
IVL/raw_prc_pipeline/__pycache__/pipeline.cpython-38.pyc ADDED
Binary file (9.32 kB). View file
 
IVL/raw_prc_pipeline/__pycache__/pipeline_utils.cpython-38.pyc ADDED
Binary file (12.5 kB). View file
 
IVL/raw_prc_pipeline/__pycache__/pipeline_utils.cpython-39.pyc ADDED
Binary file (12.2 kB). View file
 
IVL/raw_prc_pipeline/exif_data_formats.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class ExifFormat:
2
+ def __init__(self, id, name, size, short_name):
3
+ self.id = id
4
+ self.name = name
5
+ self.size = size
6
+ self.short_name = short_name # used with struct.unpack()
7
+
8
+
9
+ exif_formats = {
10
+ 1: ExifFormat(1, 'unsigned byte', 1, 'B'),
11
+ 2: ExifFormat(2, 'ascii string', 1, 's'),
12
+ 3: ExifFormat(3, 'unsigned short', 2, 'H'),
13
+ 4: ExifFormat(4, 'unsigned long', 4, 'L'),
14
+ 5: ExifFormat(5, 'unsigned rational', 8, ''),
15
+ 6: ExifFormat(6, 'signed byte', 1, 'b'),
16
+ 7: ExifFormat(7, 'undefined', 1, 'B'), # consider `undefined` as `unsigned byte`
17
+ 8: ExifFormat(8, 'signed short', 2, 'h'),
18
+ 9: ExifFormat(9, 'signed long', 4, 'l'),
19
+ 10: ExifFormat(10, 'signed rational', 8, ''),
20
+ 11: ExifFormat(11, 'single float', 4, 'f'),
21
+ 12: ExifFormat(12, 'double float', 8, 'd'),
22
+ }
IVL/raw_prc_pipeline/exif_utils.py ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Manual parsing of image file directories (IFDs).
3
+ """
4
+
5
+
6
+ import struct
7
+ from fractions import Fraction
8
+ from raw_prc_pipeline.exif_data_formats import exif_formats
9
+
10
+ class Ifd:
11
+ def __init__(self):
12
+ self.offset = -1
13
+ self.tags = {} # <key, tag> dict; tag number will be key.
14
+
15
+
16
+ class Tag:
17
+ def __init__(self):
18
+ self.offset = -1
19
+ self.tag_num = -1
20
+ self.data_format = -1
21
+ self.num_values = -1
22
+ self.values = []
23
+
24
+
25
+ def parse_exif(image_path, verbose=True):
26
+ """
27
+ Parse EXIF tags from a binary file and return IFDs.
28
+ Returned IFDs include EXIF SubIFDs, if any.
29
+ """
30
+
31
+ def print_(str_):
32
+ if verbose:
33
+ print(str_)
34
+
35
+ ifds = {} # dict of <offset, Ifd> pairs; using offset to IFD as key.
36
+
37
+ with open(image_path, 'rb') as fid:
38
+ fid.seek(0)
39
+ b0 = fid.read(1)
40
+ _ = fid.read(1)
41
+ # byte storage direction (endian):
42
+ # +1: b'M' (big-endian/Motorola)
43
+ # -1: b'I' (little-endian/Intel)
44
+ endian = 1 if b0 == b'M' else -1
45
+ print_("Endian = {}".format(b0))
46
+ endian_sign = "<" if endian == -1 else ">" # used in struct.unpack
47
+ print_("Endian sign = {}".format(endian_sign))
48
+ _ = fid.read(2) # 0x002A
49
+ b4_7 = fid.read(4) # offset to first IFD
50
+ offset_ = struct.unpack(endian_sign + "I", b4_7)[0]
51
+ i = 0
52
+ ifd_offsets = [offset_]
53
+ while len(ifd_offsets) > 0:
54
+ offset_ = ifd_offsets.pop(0)
55
+ # check if IFD at this offset was already parsed before
56
+ if offset_ in ifds:
57
+ continue
58
+ print_("=========== Parsing IFD # {} ===========".format(i))
59
+ ifd_ = parse_exif_ifd(fid, offset_, endian_sign, verbose)
60
+ ifds.update({ifd_.offset: ifd_})
61
+ print_("=========== Finished parsing IFD # {} ===========".format(i))
62
+ i += 1
63
+ # check SubIFDs; zero or more offsets at tag 0x014a
64
+ sub_idfs_tag_num = int('0x014a', 16)
65
+ if sub_idfs_tag_num in ifd_.tags:
66
+ ifd_offsets.extend(ifd_.tags[sub_idfs_tag_num].values)
67
+ # check Exif SUbIDF; usually one offset at tag 0x8769
68
+ exif_sub_idf_tag_num = int('0x8769', 16)
69
+ if exif_sub_idf_tag_num in ifd_.tags:
70
+ ifd_offsets.extend(ifd_.tags[exif_sub_idf_tag_num].values)
71
+ return ifds
72
+
73
+
74
+ def parse_exif_ifd(binary_file, offset_, endian_sign, verbose=True):
75
+ """
76
+ Parse an EXIF IFD.
77
+ """
78
+
79
+ def print_(str_):
80
+ if verbose:
81
+ print(str_)
82
+
83
+ ifd = Ifd()
84
+ ifd.offset = offset_
85
+ print_("IFD offset = {}".format(ifd.offset))
86
+ binary_file.seek(offset_)
87
+ num_entries = struct.unpack(endian_sign + "H", binary_file.read(2))[0] # format H = unsigned short
88
+ print_("Number of entries = {}".format(num_entries))
89
+ for t in range(num_entries):
90
+ print_("---------- Tag {} / {} ----------".format(t + 1, num_entries))
91
+ if t == 22:
92
+ ttt = 1
93
+ tag_ = parse_exif_tag(binary_file, endian_sign, verbose)
94
+ ifd.tags.update({tag_.tag_num: tag_}) # supposedly, EXIF tag numbers won't repeat in the same IFD
95
+ # TODO: check for subsequent IFDs by parsing the next 4 bytes immediately after the IFD
96
+ return ifd
97
+
98
+
99
+ def parse_exif_tag(binary_file, endian_sign, verbose=True):
100
+ """
101
+ Parse EXIF tag from a binary file starting from the current file pointer and returns the tag values.
102
+ """
103
+
104
+ def print_(str_):
105
+ if verbose:
106
+ print(str_)
107
+
108
+ tag = Tag()
109
+
110
+ # tag offset
111
+ tag.offset = binary_file.tell()
112
+ print_("Tag offset = {}".format(tag.offset))
113
+
114
+ # tag number
115
+ bytes_ = binary_file.read(2)
116
+ tag.tag_num = struct.unpack(endian_sign + "H", bytes_)[0] # H: unsigned 2-byte short
117
+ print_("Tag number = {} = 0x{:04x}".format(tag.tag_num, tag.tag_num))
118
+
119
+ # data format (some value between [1, 12])
120
+ tag.data_format = struct.unpack(endian_sign + "H", binary_file.read(2))[0] # H: unsigned 2-byte short
121
+ exif_format = exif_formats[tag.data_format]
122
+ print_("Data format = {} = {}".format(tag.data_format, exif_format.name))
123
+
124
+ # number of components/values
125
+ tag.num_values = struct.unpack(endian_sign + "I", binary_file.read(4))[0] # I: unsigned 4-byte integer
126
+ print_("Number of values = {}".format(tag.num_values))
127
+
128
+ # total number of data bytes
129
+ total_bytes = tag.num_values * exif_format.size
130
+ print_("Total bytes = {}".format(total_bytes))
131
+
132
+ # seek to data offset (if needed)
133
+ data_is_offset = False
134
+ current_offset = binary_file.tell()
135
+ if total_bytes > 4:
136
+ print_("Total bytes > 4; The next 4 bytes are an offset.")
137
+ data_is_offset = True
138
+ data_offset = struct.unpack(endian_sign + "I", binary_file.read(4))[0]
139
+ current_offset = binary_file.tell()
140
+ print_("Current offset = {}".format(current_offset))
141
+ print_("Seeking to data offset = {}".format(data_offset))
142
+ binary_file.seek(data_offset)
143
+
144
+ # read values
145
+ # TODO: need to distinguish between numeric and text values?
146
+ if tag.num_values == 1 and total_bytes < 4:
147
+ # special case: data is a single value that is less than 4 bytes inside 4 bytes, take care of endian
148
+ val_bytes = binary_file.read(4)
149
+ # if endian_sign == ">":
150
+ # val_bytes = val_bytes[4 - total_bytes:]
151
+ # else:
152
+ # val_bytes = val_bytes[:total_bytes][::-1]
153
+ val_bytes = val_bytes[:total_bytes]
154
+ tag.values.append(struct.unpack(endian_sign + exif_format.short_name, val_bytes)[0])
155
+ else:
156
+ # read data values one by one
157
+ for k in range(tag.num_values):
158
+ val_bytes = binary_file.read(exif_format.size)
159
+ if exif_format.name == 'unsigned rational':
160
+ tag.values.append(eight_bytes_to_fraction(val_bytes, endian_sign, signed=False))
161
+ elif exif_format.name == 'signed rational':
162
+ tag.values.append(eight_bytes_to_fraction(val_bytes, endian_sign, signed=True))
163
+ else:
164
+ tag.values.append(struct.unpack(endian_sign + exif_format.short_name, val_bytes)[0])
165
+ if total_bytes < 4:
166
+ # special case: multiple values less than 4 bytes in total, inside the 4 bytes; skip the extra bytes
167
+ binary_file.seek(4 - total_bytes, 1)
168
+
169
+ if verbose:
170
+ if len(tag.values) > 100:
171
+ print_("Got more than 100 values; printing first 100 only:")
172
+ print_("Tag values = {}".format(tag.values[:100]))
173
+ else:
174
+ print_("Tag values = {}".format(tag.values))
175
+ if tag.data_format == 2:
176
+ print_("Tag values (string) = {}".format(b''.join(tag.values).decode()))
177
+
178
+ if data_is_offset:
179
+ # seek back to current position to read the next tag
180
+ print_("Seeking back to current offset = {}".format(current_offset))
181
+ binary_file.seek(current_offset)
182
+
183
+ return tag
184
+
185
+
186
+ def get_tag_values_from_ifds(tag_num, ifds):
187
+ """
188
+ Return values of a tag, if found in ifds. Return None otherwise.
189
+ Assuming any tag exists only once in all ifds.
190
+ """
191
+ for key, ifd in ifds.items():
192
+ if tag_num in ifd.tags:
193
+ return ifd.tags[tag_num].values
194
+ return None
195
+
196
+
197
+ def eight_bytes_to_fraction(eight_bytes, endian_sign, signed):
198
+ """
199
+ Convert 8-byte array into a Fraction. Take care of endian and sign.
200
+ """
201
+ if signed:
202
+ num = struct.unpack(endian_sign + "l", eight_bytes[:4])[0]
203
+ den = struct.unpack(endian_sign + "l", eight_bytes[4:])[0]
204
+ else:
205
+ num = struct.unpack(endian_sign + "L", eight_bytes[:4])[0]
206
+ den = struct.unpack(endian_sign + "L", eight_bytes[4:])[0]
207
+ den = den if den != 0 else 1
208
+ return Fraction(num, den)
IVL/raw_prc_pipeline/fs.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+
4
+
5
+ def perform_flash(source, a=5, target=-1, perform_gamma_correction=True):
6
+ rows, cols, _ = source.shape
7
+
8
+ v = np.max(source, axis=2)
9
+ vd = np.copy(v)
10
+ vd[vd == 0] = 1e-9
11
+ result = source / (a * np.exp(np.mean(np.log(vd))) + np.tile(np.expand_dims(vd, axis=2), (1, 1, 3)))
12
+
13
+ if perform_gamma_correction:
14
+ result **= 1.0 / 2.2
15
+
16
+ if target >= 0:
17
+ result *= target / np.mean((0.299 * result[:, :, 2] + 0.587 * result[:, :, 1] + 0.114 * result[:, :, 0]))
18
+ else:
19
+ result *= 255.0 / np.max(result)
20
+
21
+ return result
22
+
23
+
24
+ def perform_storm(source, a=5, target=-1, kernels=(1, 4, 16, 64, 256), perform_gamma_correction=True):
25
+ rows, cols, _ = source.shape
26
+
27
+ v = np.max(source, axis=2)
28
+ vd = np.copy(v)
29
+ vd[vd == 0] = 1e-9
30
+ lv = np.log(vd)
31
+ result = sum([source / np.tile(
32
+ np.expand_dims(a * np.exp(cv2.boxFilter(lv, -1, (int(min(rows // kernel, cols // kernel)),) * 2)) + vd, axis=2),
33
+ (1, 1, 3)) for kernel in kernels])
34
+
35
+ if perform_gamma_correction:
36
+ result **= 1.0 / 2.2
37
+
38
+ if target >= 0:
39
+ result *= target / np.mean((0.299 * result[:, :, 2] + 0.587 * result[:, :, 1] + 0.114 * result[:, :, 0]))
40
+ else:
41
+ result *= 255.0 / np.max(result)
42
+
43
+ return result
IVL/raw_prc_pipeline/pipeline.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Demo raw processing pipeline and pipeline executor.
3
+ """
4
+
5
+ import numpy as np
6
+ from raw_prc_pipeline.pipeline_utils import *
7
+
8
+
9
+ class RawProcessingPipelineDemo:
10
+ """
11
+ Demonstration pipeline of raw image processing.
12
+
13
+ This pipeline is a baseline pipeline to process raw image.
14
+ The public methods of this class are successive steps of raw image processing pipeline.
15
+ The declaration order of the public methods must correspond to the order in which these methods (steps) are supposed to be called when processing raw image.
16
+
17
+ It is assumed that each public method has 2 parameters:
18
+ raw_img : ndarray
19
+ Array with images data.
20
+ img_meta : Dict
21
+ Some metadata of image.
22
+
23
+ Also each such public method must return an image (ndarray) as the result of processing.
24
+ """
25
+ def __init__(self, illumination_estimation='', denoise_flg=True, tone_mapping='Flash', out_landscape_width=None, out_landscape_height=None):
26
+ """
27
+ RawProcessingPipelineDemo __init__ method.
28
+
29
+ Parameters
30
+ ----------
31
+ illumination_estimation : str, optional
32
+ Options for illumination estimation algorithms: '', 'gw', 'wp', 'sog', 'iwp', by default ''.
33
+ denoise_flg : bool, optional
34
+ Denoising flag, by default True.
35
+ If True, resulted images will be denoised with some predefined parameters.
36
+ tone_mapping : str, optional
37
+ Options for tone mapping methods, defined in function `apply_tone_map` from `pipeline_utils` module.
38
+ By default 'Flash'.
39
+ out_landscape_width : int, optional
40
+ The width of output image (when orientation is landscape). If None, the image resize will not be performed.
41
+ By default None.
42
+ out_landscape_height : int, optional
43
+ The height of output image (when orientation is landscape). If None, the image resize will not be performed.
44
+ By default None.
45
+ """
46
+ self.params = locals()
47
+ del self.params['self']
48
+
49
+ # Linearization not handled.
50
+ def linearize_raw(self, raw_img, img_meta):
51
+ return raw_img
52
+
53
+ def normalize(self, linearized_raw, img_meta):
54
+ return normalize(linearized_raw, img_meta['black_level'], img_meta['white_level'])
55
+
56
+ def demosaic(self, normalized, img_meta):
57
+ return simple_demosaic(normalized, img_meta['cfa_pattern'])
58
+
59
+ def denoise(self, demosaic, img_meta):
60
+ if not self.params['denoise_flg']:
61
+ return demosaic
62
+ return denoise_image(demosaic)
63
+
64
+ def white_balance(self, demosaic, img_meta):
65
+ if self.params['illumination_estimation'] == '':
66
+ wb_params = img_meta['as_shot_neutral']
67
+ else:
68
+ wb_params = illumination_parameters_estimation(
69
+ demosaic, self.params['illumination_estimation'])
70
+
71
+ white_balanced = white_balance(demosaic, wb_params)
72
+ return white_balanced
73
+
74
+ def xyz_transform(self, white_balanced, img_meta):
75
+ # in case of absence of color matrix we use mean color matrix
76
+ if "color_matrix_1" not in img_meta.keys():
77
+ ccm_default = [1.06835938, -0.29882812, -0.14257812,
78
+ -0.43164062, 1.35546875, 0.05078125,
79
+ -0.1015625, 0.24414062, 0.5859375]
80
+ img_meta["color_matrix_1"] = ccm_default
81
+ img_meta["color_matrix_2"] = ccm_default
82
+ return apply_color_space_transform(white_balanced, img_meta['color_matrix_1'], img_meta['color_matrix_2'])
83
+
84
+ def srgb_transform(self, xyz, img_meta):
85
+ return transform_xyz_to_srgb(xyz)
86
+
87
+ def tone_mapping(self, srgb, img_meta):
88
+ if self.params['tone_mapping'] is None:
89
+ return apply_tone_map(srgb, 'Base')
90
+ return apply_tone_map(srgb, self.params['tone_mapping'])
91
+
92
+ def gamma_correct(self, srgb, img_meta):
93
+ return apply_gamma(srgb)
94
+
95
+ def autocontrast(self, srgb, img_meta):
96
+ # return autocontrast(srgb)
97
+ return autocontrast_using_pil(srgb)
98
+
99
+ def to_uint8(self, srgb, img_meta):
100
+ return (srgb*255).astype(np.uint8)
101
+
102
+ def resize(self, img, img_meta):
103
+ if self.params['out_landscape_width'] is None or self.params['out_landscape_height'] is None:
104
+ return img
105
+ return resize_using_pil(img, self.params['out_landscape_width'], self.params['out_landscape_height'])
106
+
107
+ def fix_orientation(self, img, img_meta):
108
+ return fix_orientation(img, img_meta['orientation'])
109
+
110
+
111
+ class PipelineExecutor:
112
+ """
113
+ Pipeline executor class.
114
+
115
+ This class can be used to successively execute the steps of some image pipeline class (for example `RawProcessingPipelineDemo`).
116
+ The declaration order of the public methods of pipeline class must correspond to the order in which these methods (steps) are supposed to be called when processing image.
117
+
118
+ It is assumed that each public method of the pipeline class has 2 parameters:
119
+ raw_img : ndarray
120
+ Array with images data.
121
+ img_meta : Dict
122
+ Some meta data of image.
123
+
124
+ Also each such public method must return an image (ndarray) as the result of processing.
125
+ """
126
+ def __init__(self, img, img_meta, pipeline_obj, first_stage=None, last_stage=None):
127
+ """
128
+ PipelineExecutor __init__ method.
129
+
130
+ Parameters
131
+ ----------
132
+ img : ndarray
133
+ Image that should be processed by pipeline.
134
+ img_meta : Dict
135
+ Some image metadata.
136
+ pipeline_obj : pipeline object
137
+ Some pipeline object such as RawProcessingPipelineDemo.
138
+ first_stage : str, optional
139
+ The name of first public method of pipeline object that should be called by PipelineExecutor.
140
+ If None, the first public method from defined in pipeline object will be considered as `first_stage` method.
141
+ By default None.
142
+ last_stage : str, optional
143
+ The name of last public method of pipeline object that should be called by PipelineExecutor.
144
+ If None, the last public method from defined in pipeline object will be considered as `last_stage` method.
145
+ By default None.
146
+ """
147
+ self.pipeline_obj = pipeline_obj
148
+ self.stages_dict = self._init_stages()
149
+ self.stages_names, self.stages = list(
150
+ self.stages_dict.keys()), list(self.stages_dict.values())
151
+
152
+ if first_stage is None:
153
+ self.next_stage_indx = 0
154
+ else:
155
+ assert first_stage in self.stages_names, f"Invalid first_stage={first_stage}. Try use the following stages: {self.stages_names}"
156
+ self.next_stage_indx = self.stages_names.index(first_stage)
157
+
158
+ if last_stage is None:
159
+ self.last_stage_indx = len(self.stages_names) - 1
160
+ else:
161
+ assert last_stage in self.stages_names, f"Invalid last_stage={last_stage}. Try use the following stages: {self.stages_names}"
162
+ self.last_stage_indx = self.stages_names.index(last_stage)
163
+ if self.next_stage_indx > self.last_stage_indx:
164
+ print(f'Warning: the specified first_stage={first_stage} follows the specified last_stage={last_stage}, so using __call__ no image processing will be done.')
165
+
166
+ self.current_image = img
167
+ self.img_meta = img_meta
168
+
169
+ def _init_stages(self):
170
+ stages = {func: getattr(self.pipeline_obj, func) for func in self.pipeline_obj.__class__.__dict__ if callable(
171
+ getattr(self.pipeline_obj, func)) and not func.startswith("_")}
172
+ return stages
173
+
174
+ @property
175
+ def next_stage(self):
176
+ if self.next_stage_indx < len(self.stages):
177
+ return self.stages_names[self.next_stage_indx]
178
+ else:
179
+ return None
180
+
181
+ @property
182
+ def last_stage(self):
183
+ return self.stages_names[self.last_stage_indx]
184
+
185
+ def __iter__(self):
186
+ return self
187
+
188
+ def __next__(self):
189
+ if self.next_stage_indx < len(self.stages):
190
+ stage_func = self.stages[self.next_stage_indx]
191
+ self.current_image = stage_func(self.current_image, self.img_meta)
192
+ self.next_stage_indx += 1
193
+ return self.current_image
194
+ else:
195
+ raise StopIteration
196
+
197
+ def __call__(self):
198
+ """
199
+ PipelineExecutor __call__ method.
200
+
201
+ This method will sequentially execute the methods defined in the pipeline object from the `first_stage` to the `last_stage` inclusive.
202
+
203
+ Returns
204
+ -------
205
+ ndarray
206
+ Resulted processed raw image.
207
+ """
208
+ for current_image in self:
209
+ if self.next_stage_indx > self.last_stage_indx:
210
+ return current_image
211
+ return self.current_image
IVL/raw_prc_pipeline/pipeline_utils.py ADDED
@@ -0,0 +1,493 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Camera pipeline utilities.
3
+ """
4
+
5
+ import os
6
+ from fractions import Fraction
7
+
8
+ import cv2
9
+ import numpy as np
10
+ import exifread
11
+ # from exifread import Ratio
12
+ from exifread.utils import Ratio
13
+ import rawpy
14
+ from scipy.io import loadmat
15
+ from raw_prc_pipeline.exif_utils import parse_exif, get_tag_values_from_ifds
16
+ from raw_prc_pipeline.fs import perform_storm, perform_flash
17
+ from PIL import Image, ImageOps
18
+ from skimage.restoration import denoise_bilateral
19
+ from skimage.transform import resize as skimage_resize
20
+
21
+
22
+ def get_visible_raw_image(image_path):
23
+ raw_image = rawpy.imread(image_path).raw_image_visible.copy()
24
+ # raw_image = rawpy.imread(image_path).raw_image.copy()
25
+ return raw_image
26
+
27
+
28
+ def get_image_tags(image_path):
29
+ with open(image_path, 'rb') as f:
30
+ tags = exifread.process_file(f)
31
+ return tags
32
+
33
+
34
+ def get_image_ifds(image_path):
35
+ ifds = parse_exif(image_path, verbose=False)
36
+ return ifds
37
+
38
+
39
+ def get_metadata(image_path):
40
+ metadata = {}
41
+ tags = get_image_tags(image_path)
42
+ ifds = get_image_ifds(image_path)
43
+ metadata['linearization_table'] = get_linearization_table(tags, ifds)
44
+ metadata['black_level'] = get_black_level(tags, ifds)
45
+ metadata['white_level'] = get_white_level(tags, ifds)
46
+ metadata['cfa_pattern'] = get_cfa_pattern(tags, ifds)
47
+ metadata['as_shot_neutral'] = get_as_shot_neutral(tags, ifds)
48
+ color_matrix_1, color_matrix_2 = get_color_matrices(tags, ifds)
49
+ metadata['color_matrix_1'] = color_matrix_1
50
+ metadata['color_matrix_2'] = color_matrix_2
51
+ metadata['orientation'] = get_orientation(tags, ifds)
52
+ # isn't used
53
+ metadata['noise_profile'] = get_noise_profile(tags, ifds)
54
+ # ...
55
+ # fall back to default values, if necessary
56
+ if metadata['black_level'] is None:
57
+ metadata['black_level'] = 0
58
+ print("Black level is None; using 0.")
59
+ if metadata['white_level'] is None:
60
+ metadata['white_level'] = 2 ** 16
61
+ print("White level is None; using 2 ** 16.")
62
+ if metadata['cfa_pattern'] is None:
63
+ metadata['cfa_pattern'] = [0, 1, 1, 2]
64
+ print("CFAPattern is None; using [0, 1, 1, 2] (RGGB)")
65
+ if metadata['as_shot_neutral'] is None:
66
+ metadata['as_shot_neutral'] = [1, 1, 1]
67
+ print("AsShotNeutral is None; using [1, 1, 1]")
68
+ if metadata['color_matrix_1'] is None:
69
+ metadata['color_matrix_1'] = [1] * 9
70
+ print("ColorMatrix1 is None; using [1, 1, 1, 1, 1, 1, 1, 1, 1]")
71
+ if metadata['color_matrix_2'] is None:
72
+ metadata['color_matrix_2'] = [1] * 9
73
+ print("ColorMatrix2 is None; using [1, 1, 1, 1, 1, 1, 1, 1, 1]")
74
+ if metadata['orientation'] is None:
75
+ metadata['orientation'] = 0
76
+ print("Orientation is None; using 0.")
77
+ # ...
78
+ return metadata
79
+
80
+
81
+ def get_linearization_table(tags, ifds):
82
+ possible_keys = ['Image Tag 0xC618', 'Image Tag 50712',
83
+ 'LinearizationTable', 'Image LinearizationTable']
84
+ return get_values(tags, possible_keys)
85
+
86
+
87
+ def get_black_level(tags, ifds):
88
+ possible_keys = ['Image Tag 0xC61A', 'Image Tag 50714',
89
+ 'BlackLevel', 'Image BlackLevel']
90
+ vals = get_values(tags, possible_keys)
91
+ if vals is None:
92
+ # print("Black level not found in exifread tags. Searching IFDs.")
93
+ vals = get_tag_values_from_ifds(50714, ifds)
94
+ return vals
95
+
96
+
97
+ def get_white_level(tags, ifds):
98
+ possible_keys = ['Image Tag 0xC61D', 'Image Tag 50717',
99
+ 'WhiteLevel', 'Image WhiteLevel']
100
+ vals = get_values(tags, possible_keys)
101
+ if vals is None:
102
+ # print("White level not found in exifread tags. Searching IFDs.")
103
+ vals = get_tag_values_from_ifds(50717, ifds)
104
+ return vals
105
+
106
+
107
+ def get_cfa_pattern(tags, ifds):
108
+ possible_keys = ['CFAPattern', 'Image CFAPattern']
109
+ vals = get_values(tags, possible_keys)
110
+ if vals is None:
111
+ # print("CFAPattern not found in exifread tags. Searching IFDs.")
112
+ vals = get_tag_values_from_ifds(33422, ifds)
113
+ return vals
114
+
115
+
116
+ def get_as_shot_neutral(tags, ifds):
117
+ possible_keys = ['Image Tag 0xC628', 'Image Tag 50728',
118
+ 'AsShotNeutral', 'Image AsShotNeutral']
119
+ return get_values(tags, possible_keys)
120
+
121
+
122
+ def get_color_matrices(tags, ifds):
123
+ possible_keys_1 = ['Image Tag 0xC621', 'Image Tag 50721',
124
+ 'ColorMatrix1', 'Image ColorMatrix1']
125
+ color_matrix_1 = get_values(tags, possible_keys_1)
126
+ possible_keys_2 = ['Image Tag 0xC622', 'Image Tag 50722',
127
+ 'ColorMatrix2', 'Image ColorMatrix2']
128
+ color_matrix_2 = get_values(tags, possible_keys_2)
129
+ #print(f'Color matrix 1:{color_matrix_1}')
130
+ #print(f'Color matrix 2:{color_matrix_2}')
131
+ #print(np.sum(np.abs(np.array(color_matrix_1) - np.array(color_matrix_2))))
132
+ return color_matrix_1, color_matrix_2
133
+
134
+
135
+ def get_orientation(tags, ifds):
136
+ possible_tags = ['Orientation', 'Image Orientation']
137
+ return get_values(tags, possible_tags)
138
+
139
+
140
+ def get_noise_profile(tags, ifds):
141
+ possible_keys = ['Image Tag 0xC761', 'Image Tag 51041',
142
+ 'NoiseProfile', 'Image NoiseProfile']
143
+ vals = get_values(tags, possible_keys)
144
+ if vals is None:
145
+ # print("Noise profile not found in exifread tags. Searching IFDs.")
146
+ vals = get_tag_values_from_ifds(51041, ifds)
147
+ return vals
148
+
149
+
150
+ def get_values(tags, possible_keys):
151
+ values = None
152
+ for key in possible_keys:
153
+ if key in tags.keys():
154
+ values = tags[key].values
155
+ return values
156
+
157
+
158
+ def normalize(raw_image, black_level, white_level):
159
+ if type(black_level) is list and len(black_level) == 1:
160
+ black_level = float(black_level[0])
161
+ if type(white_level) is list and len(white_level) == 1:
162
+ white_level = float(white_level[0])
163
+ black_level_mask = black_level
164
+ if type(black_level) is list and len(black_level) == 4:
165
+ if type(black_level[0]) is Ratio:
166
+ black_level = ratios2floats(black_level)
167
+ if type(black_level[0]) is Fraction:
168
+ black_level = fractions2floats(black_level)
169
+ black_level_mask = np.zeros(raw_image.shape)
170
+ idx2by2 = [[0, 0], [0, 1], [1, 0], [1, 1]]
171
+ step2 = 2
172
+ for i, idx in enumerate(idx2by2):
173
+ black_level_mask[idx[0]::step2, idx[1]::step2] = black_level[i]
174
+ normalized_image = raw_image.astype(np.float32) - black_level_mask
175
+ # if some values were smaller than black level
176
+ normalized_image[normalized_image < 0] = 0
177
+ normalized_image = normalized_image / (white_level - black_level_mask)
178
+ return normalized_image
179
+
180
+
181
+ def ratios2floats(ratios):
182
+ floats = []
183
+ for ratio in ratios:
184
+ floats.append(float(ratio.num) / ratio.den)
185
+ return floats
186
+
187
+
188
+ def fractions2floats(fractions):
189
+ floats = []
190
+ for fraction in fractions:
191
+ floats.append(float(fraction.numerator) / fraction.denominator)
192
+ return floats
193
+
194
+
195
+ def illumination_parameters_estimation(current_image, illumination_estimation_option):
196
+ ie_method = illumination_estimation_option.lower()
197
+ if ie_method == "gw":
198
+ ie = np.mean(current_image, axis=(0, 1))
199
+ ie /= ie[1]
200
+ return ie
201
+ elif ie_method == "sog":
202
+ sog_p = 4.
203
+ ie = np.mean(current_image**sog_p, axis=(0, 1))**(1 / sog_p)
204
+ ie /= ie[1]
205
+ return ie
206
+ elif ie_method == "wp":
207
+ ie = np.max(current_image, axis=(0, 1))
208
+ ie /= ie[1]
209
+ return ie
210
+ elif ie_method == "iwp":
211
+ samples_count = 20
212
+ sample_size = 20
213
+ rows, cols = current_image.shape[:2]
214
+ data = np.reshape(current_image, (rows * cols, 3))
215
+ maxima = np.zeros((samples_count, 3))
216
+ for i in range(samples_count):
217
+ maxima[i, :] = np.max(data[np.random.randint(
218
+ low=0, high=rows * cols, size=(sample_size)), :], axis=0)
219
+ ie = np.mean(maxima, axis=0)
220
+ ie /= ie[1]
221
+ return ie
222
+ else:
223
+ raise ValueError(
224
+ 'Bad illumination_estimation_option value! Use the following options: "gw", "wp", "sog", "iwp"')
225
+
226
+
227
+ def white_balance(demosaic_img, as_shot_neutral):
228
+ if type(as_shot_neutral[0]) is Ratio:
229
+ as_shot_neutral = ratios2floats(as_shot_neutral)
230
+
231
+ as_shot_neutral = np.asarray(as_shot_neutral)
232
+ # transform vector into matrix
233
+ if as_shot_neutral.shape == (3,):
234
+ as_shot_neutral = np.diag(1. / as_shot_neutral)
235
+
236
+ assert as_shot_neutral.shape == (3, 3)
237
+
238
+ white_balanced_image = np.dot(demosaic_img, as_shot_neutral.T)
239
+ white_balanced_image = np.clip(white_balanced_image, 0.0, 1.0)
240
+
241
+ return white_balanced_image
242
+
243
+
244
+ def simple_demosaic(img, cfa_pattern):
245
+ raw_colors = np.asarray(cfa_pattern).reshape((2, 2))
246
+ demosaiced_image = np.zeros((img.shape[0] // 2, img.shape[1] // 2, 3))
247
+ for i in range(2):
248
+ for j in range(2):
249
+ ch = raw_colors[i, j]
250
+ if ch == 1:
251
+ demosaiced_image[:, :, ch] += img[i::2, j::2] / 2
252
+ else:
253
+ demosaiced_image[:, :, ch] = img[i::2, j::2]
254
+ return demosaiced_image
255
+
256
+
257
+ def denoise_image(demosaiced_image):
258
+ current_image = denoise_bilateral(
259
+ demosaiced_image, sigma_color=None, sigma_spatial=0.01, channel_axis=2, mode='reflect')
260
+ return current_image
261
+
262
+
263
+ def apply_color_space_transform(demosaiced_image, color_matrix_1, color_matrix_2):
264
+ if isinstance(color_matrix_1[0], Fraction):
265
+ color_matrix_1 = fractions2floats(color_matrix_1)
266
+ if isinstance(color_matrix_2[0], Fraction):
267
+ color_matrix_2 = fractions2floats(color_matrix_2)
268
+
269
+ xyz2cam1 = np.reshape(np.asarray(color_matrix_1), (3, 3))
270
+ xyz2cam2 = np.reshape(np.asarray(color_matrix_2), (3, 3))
271
+
272
+ # normalize rows (needed?)
273
+
274
+ xyz2cam1 = xyz2cam1 / np.sum(xyz2cam1, axis=1, keepdims=True)
275
+ xyz2cam2 = xyz2cam2 / np.sum(xyz2cam1, axis=1, keepdims=True)
276
+
277
+ # inverse
278
+ cam2xyz1 = np.linalg.inv(xyz2cam1)
279
+ cam2xyz2 = np.linalg.inv(xyz2cam2)
280
+
281
+ # cam2xyz1 = cam2xyz1 * 0.9 + cam2xyz2 * 0.1
282
+ # for now, use one matrix # TODO: interpolate btween both
283
+
284
+ # simplified matrix multiplication
285
+ # xyz_image = cam2xyz1[np.newaxis, np.newaxis, :, :] * \
286
+ # demosaiced_image[:, :, np.newaxis, :]
287
+ # xyz_image = np.sum(xyz_image, axis=-1)
288
+
289
+ xyz_image = np.einsum('kc,ijc', cam2xyz1, demosaiced_image)
290
+
291
+ xyz_image = np.clip(xyz_image, 0.0, 1.0)
292
+
293
+ return xyz_image
294
+
295
+
296
+ def srgb2xyz(xyz_image):
297
+ srgb2xyz = np.array([[0.4124564, 0.3575761, 0.1804375],
298
+ [0.2126729, 0.7151522, 0.0721750],
299
+ [0.0193339, 0.1191920, 0.9503041]])
300
+
301
+ out = srgb2xyz[np.newaxis, np.newaxis,
302
+ :, :] * xyz_image[:, :, np.newaxis, :]
303
+ out = np.sum(out, axis=-1)
304
+ out = np.clip(out, 0.0, 1.0)
305
+ return out
306
+
307
+
308
+ def transform_xyz_to_srgb(xyz_image):
309
+ # srgb2xyz = np.array([[0.4124564, 0.3575761, 0.1804375],
310
+ # [0.2126729, 0.7151522, 0.0721750],
311
+ # [0.0193339, 0.1191920, 0.9503041]])
312
+
313
+ # xyz2srgb = np.linalg.inv(srgb2xyz)
314
+
315
+ xyz2srgb = np.array([[2.0413690, -0.5649464, -0.3446944],
316
+ [-0.9692660, 1.8760108, 0.0415560],
317
+ [0.0134474, -0.1183897, 1.0154096]])
318
+ # xyz2srgb = np.array([[3.2404542, -1.5371385, -0.4985314],
319
+ # [-0.9692660, 1.8760108, 0.0415560],
320
+ # [0.0556434, -0.2040259, 1.0572252]])
321
+
322
+ # normalize rows (needed?)
323
+ xyz2srgb = xyz2srgb / np.sum(xyz2srgb, axis=-1, keepdims=True)
324
+
325
+ srgb_image = xyz2srgb[np.newaxis, np.newaxis,
326
+ :, :] * xyz_image[:, :, np.newaxis, :]
327
+ srgb_image = np.sum(srgb_image, axis=-1)
328
+ srgb_image = np.clip(srgb_image, 0.0, 1.0)
329
+ return srgb_image
330
+
331
+
332
+ def reverse_orientation(image, orientation):
333
+ # 1 = Horizontal(normal)
334
+ # 2 = Mirror horizontal
335
+ # 3 = Rotate 180
336
+ # 4 = Mirror vertical
337
+ # 5 = Mirror horizontal and rotate 270 CW
338
+ # 6 = Rotate 90 CW
339
+ # 7 = Mirror horizontal and rotate 90 CW
340
+ # 8 = Rotate 270 CW
341
+ rev_orientations = np.array([1, 2, 3, 4, 5, 8, 7, 6])
342
+ return fix_orientation(image, rev_orientations[orientation - 1])
343
+
344
+
345
+ def apply_gamma(x):
346
+ # return x ** (1.0 / 2.2)
347
+ x = x.copy()
348
+ idx = x <= 0.0031308
349
+ x[idx] *= 12.92
350
+ x[idx == False] = (x[idx == False] ** (1.0 / 2.4)) * 1.055 - 0.055
351
+ return x
352
+
353
+
354
+ def apply_tone_map(x, tone_mapping='Base'):
355
+ if tone_mapping == 'Flash':
356
+ return perform_flash(x, perform_gamma_correction=0) / 255.
357
+ elif tone_mapping == 'Storm':
358
+ return perform_storm(x, perform_gamma_correction=0) / 255.
359
+ elif tone_mapping == 'Drago':
360
+ tonemap = cv2.createTonemapDrago()
361
+ return tonemap.process(x.astype(np.float32))
362
+ elif tone_mapping == 'Mantiuk':
363
+ tonemap = cv2.createTonemapMantiuk()
364
+ return tonemap.process(x.astype(np.float32))
365
+ elif tone_mapping == 'Reinhard':
366
+ tonemap = cv2.createTonemapReinhard()
367
+ return tonemap.process(x.astype(np.float32))
368
+ elif tone_mapping == 'Linear':
369
+ return np.clip(x / np.sort(x.flatten())[-50000], 0, 1)
370
+ elif tone_mapping == 'Base':
371
+ # return 3 * x ** 2 - 2 * x ** 3
372
+ # tone_curve = loadmat('tone_curve.mat')
373
+ tone_curve = loadmat(os.path.join(os.path.dirname(
374
+ os.path.realpath(__file__)), 'tone_curve.mat'))
375
+ tone_curve = tone_curve['tc']
376
+ x = np.round(x * (len(tone_curve) - 1)).astype(int)
377
+ tone_mapped_image = np.squeeze(tone_curve[x])
378
+ return tone_mapped_image
379
+ else:
380
+ raise ValueError(
381
+ 'Bad tone_mapping option value! Use the following options: "Base", "Flash", "Storm", "Linear", "Drago", "Mantiuk", "Reinhard"')
382
+
383
+
384
+ def autocontrast(output_image, cutoff_prcnt=2, preserve_tone=False):
385
+ if preserve_tone:
386
+ min_val, max_val = np.percentile(
387
+ output_image, [cutoff_prcnt, 100 - cutoff_prcnt])
388
+ output_image = (output_image - min_val) / (max_val - min_val)
389
+ else:
390
+ channels = [None] * 3
391
+ for ch in range(3):
392
+ min_val, max_val = np.percentile(
393
+ output_image[..., ch], [cutoff_prcnt, 100 - cutoff_prcnt])
394
+ channels[ch] = (output_image[..., ch] - min_val) / \
395
+ (max_val - min_val)
396
+ output_image = np.dstack(channels)
397
+ output_image = np.clip(output_image, 0, 1)
398
+ return output_image
399
+
400
+
401
+ def autocontrast_using_pil(img, cutoff=2):
402
+ img_uint8 = np.clip(255 * img, 0, 255).astype(np.uint8)
403
+ img_pil = Image.fromarray(img_uint8)
404
+ img_pil = ImageOps.autocontrast(img_pil, cutoff=cutoff)
405
+ output_image = np.array(img_pil).astype(np.float32) / 255
406
+ return output_image
407
+
408
+
409
+ def raw_rgb_to_cct(rawRgb, xyz2cam1, xyz2cam2):
410
+ """Convert raw-RGB triplet to corresponding correlated color temperature (CCT)"""
411
+ pass
412
+ # pxyz = [.5, 1, .5]
413
+ # loss = 1e10
414
+ # k = 1
415
+ # while loss > 1e-4:
416
+ # cct = XyzToCct(pxyz)
417
+ # xyz = RawRgbToXyz(rawRgb, cct, xyz2cam1, xyz2cam2)
418
+ # loss = norm(xyz - pxyz)
419
+ # pxyz = xyz
420
+ # fprintf('k = %d, loss = %f\n', [k, loss])
421
+ # k = k + 1
422
+ # end
423
+ # temp = cct
424
+
425
+
426
+ def resize_using_skimage(img, width=1296, height=864):
427
+ out_shape = (height, width) + img.shape[2:]
428
+ if img.shape == out_shape:
429
+ return img
430
+ out_img = skimage_resize(
431
+ img, out_shape, preserve_range=True, anti_aliasing=True)
432
+ out_img = out_img.astype(np.uint8)
433
+ return out_img
434
+
435
+
436
+ def resize_using_pil(img, width=1296, height=864):
437
+ img_pil = Image.fromarray(img)
438
+ out_size = (width, height)
439
+ if img_pil.size == out_size:
440
+ return img
441
+ out_img = img_pil.resize(out_size, Image.ANTIALIAS)
442
+ out_img = np.array(out_img)
443
+ return out_img
444
+
445
+
446
+ def fix_orientation(image, orientation):
447
+ # 1 = Horizontal(normal)
448
+ # 2 = Mirror horizontal
449
+ # 3 = Rotate 180
450
+ # 4 = Mirror vertical
451
+ # 5 = Mirror horizontal and rotate 270 CW
452
+ # 6 = Rotate 90 CW
453
+ # 7 = Mirror horizontal and rotate 90 CW
454
+ # 8 = Rotate 270 CW
455
+
456
+ map_ = {'Horizontal (normal)': 1,
457
+ 'Mirror horizontal': 2,
458
+ 'Rotate 180': 3,
459
+ 'Mirror vertical': 4,
460
+ 'Mirror horizontal and rotate 270 CW': 5,
461
+ 'Rotate 90 CW': 6,
462
+ 'Mirror horizontal and rotate 90 CW': 7,
463
+ 'Rotate 270 CW': 8
464
+
465
+ }
466
+
467
+ if type(orientation) is list:
468
+ orientation = orientation[0]
469
+
470
+ orientation = map_[orientation]
471
+
472
+ if orientation == 1:
473
+ pass
474
+ elif orientation == 2:
475
+ image = cv2.flip(image, 0)
476
+ elif orientation == 3:
477
+ image = cv2.rotate(image, cv2.ROTATE_180)
478
+ elif orientation == 4:
479
+ image = cv2.flip(image, 1)
480
+ elif orientation == 5:
481
+ image = cv2.flip(image, 0)
482
+ image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
483
+ elif orientation == 6:
484
+ image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
485
+ elif orientation == 7:
486
+ image = cv2.flip(image, 0)
487
+ image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
488
+ elif orientation == 8:
489
+ image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
490
+ else:
491
+ raise NotImplementedError('Orientation not defined')
492
+
493
+ return image
IVL/raw_prc_pipeline/tone_curve.mat ADDED
Binary file (6.57 kB). View file
 
IVL/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ ExifRead==2.3.2
2
+ matplotlib==3.5.1
3
+ numpy==1.24.2
4
+ opencv_python==4.5.5.62
5
+ Pillow==10.2.0
6
+ rawpy==0.17.0
7
+ scipy==1.9.1
8
+ scikit-image==0.20.0
9
+ tqdm==4.62.3
IVL/run.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ python pipeline24.py -p /data/ -o /data/
IVL/utils/__init__.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fractions import Fraction
2
+ from pathlib import Path
3
+ from json import JSONEncoder
4
+ from .utils import *
5
+
6
+
7
+ def rmtree(path: Path):
8
+ if path.is_file():
9
+ path.unlink()
10
+ else:
11
+ for ch in path.iterdir():
12
+ rmtree(ch)
13
+ path.rmdir()
14
+
15
+
16
+ def safe_save(fpath, data, save_fun, rewrite=False, error_msg='File {fpath} exists! To rewite it use `--rewrite` flag', **kwargs):
17
+ if not fpath.is_file() or rewrite:
18
+ save_fun(str(fpath), data, **kwargs)
19
+ else:
20
+ raise FileExistsError(error_msg.format(fpath=fpath))
21
+
22
+
23
+ class FractionJSONEncoder(JSONEncoder):
24
+ def default(self, o):
25
+ if isinstance(o, Fraction):
26
+ return {'Fraction': [o.numerator, o.denominator]}
27
+ else:
28
+ return o.__dict__
29
+
30
+
31
+ def fraction_from_json(json_object):
32
+ if 'Fraction' in json_object:
33
+ return Fraction(*json_object['Fraction'])
34
+ return json_object
35
+
36
+
IVL/utils/__pycache__/__init__.cpython-38.pyc ADDED
Binary file (1.35 kB). View file
 
IVL/utils/__pycache__/utils.cpython-38.pyc ADDED
Binary file (2.31 kB). View file
 
IVL/utils/utils.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+ import json
3
+ import os
4
+
5
+ def json_read(fname, **kwargs):
6
+ with open(fname) as j:
7
+ data = json.load(j, **kwargs)
8
+ return data
9
+
10
+
11
+ def json_save(fname, data, indent_len=4, **kwargs):
12
+ with open(fname, "w") as f:
13
+ s = json.dumps(data, sort_keys=True, ensure_ascii=False,
14
+ indent=" " * indent_len, **kwargs)
15
+ f.write(s)
16
+
17
+
18
+ def process_wb_from_txt(txt_path):
19
+ with open(txt_path, 'r') as fh:
20
+ txt = [line.rstrip().split() for line in fh]
21
+
22
+ txt = [[float(k) for k in row] for row in txt]
23
+
24
+ assert len(txt) in [1, 3]
25
+
26
+ if len(txt) == 1:
27
+ # wb vector
28
+ txt = txt[0]
29
+
30
+ return txt
31
+
32
+
33
+ def process_ids_from_txt(txt_path):
34
+ with open(txt_path, 'r') as fh:
35
+ temp = fh.read().splitlines()
36
+ return temp
37
+
38
+
39
+ def save_txt(p, s):
40
+ with open(p, 'w') as text_file:
41
+ text_file.write(s)
42
+
43
+
44
+ def downscale_jpg(img_path, new_shape, quality_perc=100):
45
+ img = Image.open(img_path)
46
+ if (img.size[0], img.size[1]) != new_shape:
47
+ new_img = img.resize(new_shape, Image.ANTIALIAS)
48
+ new_img.save(img_path[:-len('.jpg')] + '.jpg',
49
+ 'JPEG', quality=quality_perc)
50
+
51
+
52
+ def rename_img(img_path):
53
+ if img_path.lower().endswith('jpeg'):
54
+ os.rename(img_path, img_path[:-len('jpeg')] + 'jpg')
55
+ else:
56
+ os.rename(img_path, img_path[:-len('JPG')] + 'jpg')