Artyom commited on
Commit
1b5ee0e
1 Parent(s): 00c3521
OzUVGL/Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ RUN apt-get update && apt-get install -y \
4
+ build-essential \
5
+ ffmpeg \
6
+ libsm6 \
7
+ libxext6 \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ RUN pip install numpy scipy
11
+
12
+ COPY requirements.txt /npr-vgl-ozu/
13
+ WORKDIR /npr-vgl-ozu
14
+ RUN python -m pip install --no-cache-dir -r requirements.txt
15
+
16
+ COPY . /npr-vgl-ozu
17
+
18
+ RUN chmod +x run.sh
19
+ CMD ["./run.sh"]
OzUVGL/README.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VGL OZU - Night Photography Rendering Challenge @ NTIRE 2024, CVPR Workshops
2
+
3
+ Please put the test data into folder `data/` before building the Docker image.
4
+
5
+ **IMPORTANT:** Illuminant estimation algorithm contains random subsampling steps, to reproduce the 3rd validation outputs exactly, please do not forget to include "*_wb.json" files in submitted outputs folder to the corresponding data folder.
6
+
7
+ To build the Docker image:
8
+
9
+ ```
10
+ docker build -t npr-vgl-ozu .
11
+ ```
12
+
13
+ You may run the process as follows:
14
+
15
+ ```
16
+ docker run -v $(pwd)/results:/npr-vgl-ozu/results npr-vgl-ozu
17
+ ```
18
+
19
+ Results will be placed at `./results`
20
+
21
+ To cite the challenge report:
22
+
23
+ ```
24
+ TBD
25
+ ```
OzUVGL/ips/__init__.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ips.ops import *
2
+
3
+
4
+ def process(raw_image, metadata):
5
+ out = normalize(raw_image, metadata["black_level"], metadata["white_level"])
6
+ out = demosaic(out, metadata["cfa_pattern"])
7
+ out = raw_color_denoise(out, metadata["noise_profile"][1])
8
+ out = white_balance(out, metadata)
9
+ color_matrix = [ # average color transformation matrix of Huawei Mate 40 Pro
10
+ 1.06835938, -0.29882812, -0.14257812,
11
+ -0.43164062, 1.35546875, 0.05078125,
12
+ -0.1015625, 0.24414062, 0.5859375
13
+ ]
14
+ out = xyz_transform(out, color_matrix)
15
+ out = xyz_to_srgb(out)
16
+ out = luminance_denoise(out, metadata["tv_weight"])
17
+ out = perform_tone_mapping(out, metadata)
18
+ out = global_mean_contrast(out, metadata["global_mc_beta"])
19
+ out = s_curve_correction(out, metadata["scc_alpha"], metadata["scc_lambda"])
20
+ out = histogram_stretching(out)
21
+ out = memory_color_enhancement(out)
22
+ out = unsharp_masking(out)
23
+ out = to_uint8(out)
24
+ out = resize(out, metadata["exp_width"], metadata["exp_height"]) # None means direct return the image, change the params to (w, h) if downsampling required.
25
+ out = fix_orientation(out, metadata["orientation"])
26
+
27
+ return out
OzUVGL/ips/ops.py ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+
4
+ from fractions import Fraction
5
+ from exifread.utils import Ratio
6
+ from PIL import Image
7
+ from skimage.color import rgb2hsv, hsv2rgb
8
+ from skimage.exposure import rescale_intensity
9
+ from skimage.filters import gaussian as sk_gaussian
10
+ from skimage.restoration import denoise_tv_bregman
11
+ from scipy import signal
12
+
13
+ from colour_demosaicing import demosaicing_CFA_Bayer_Menon2007
14
+
15
+ from utils.misc import *
16
+ from utils.color import *
17
+ from ips.wb import illumination_parameters_estimation
18
+
19
+
20
+ def normalize(raw_image, black_level, white_level):
21
+ if isinstance(black_level, list) and len(black_level) == 1:
22
+ black_level = float(black_level[0])
23
+ if isinstance(white_level, list) and len(white_level) == 1:
24
+ white_level = float(white_level[0])
25
+ black_level_mask = black_level
26
+ if type(black_level) is list and len(black_level) == 4:
27
+ if type(black_level[0]) is Ratio:
28
+ black_level = ratios2floats(black_level)
29
+ if type(black_level[0]) is Fraction:
30
+ black_level = fractions2floats(black_level)
31
+ black_level_mask = np.zeros(raw_image.shape)
32
+ idx2by2 = [[0, 0], [0, 1], [1, 0], [1, 1]]
33
+ step2 = 2
34
+ for i, idx in enumerate(idx2by2):
35
+ black_level_mask[idx[0]::step2, idx[1]::step2] = black_level[i]
36
+ normalized_image = raw_image.astype(np.float32) - black_level_mask
37
+ # if some values were smaller than black level
38
+ normalized_image[normalized_image < 0] = 0
39
+ normalized_image = normalized_image / (white_level - black_level_mask)
40
+ return normalized_image
41
+
42
+
43
+ def demosaic(norm_image, cfa_pattern):
44
+ return demosaicing_CFA_Bayer_Menon2007(norm_image, decode_cfa_pattern(cfa_pattern))
45
+
46
+
47
+ def denoise(demosaiced_image, y_noise_profile, cc_noise_profile):
48
+ ycc_demosaiced = rgb2ycc(demosaiced_image[:, :, ::-1])
49
+ y_demosaiced = ycc_demosaiced[:, :, 0]
50
+ cc_demosaiced = ycc_demosaiced[:, :, 1:]
51
+ current_image_y = y_demosaiced
52
+ current_image_cc = gaussian(cc_demosaiced, sigma=cc_noise_profile)
53
+ current_image_ycc = np.concatenate([
54
+ np.expand_dims(current_image_y, -1),
55
+ current_image_cc
56
+ ], axis=-1)
57
+ return ycc2rgb(current_image_ycc)[:, :, ::-1]
58
+
59
+
60
+ def raw_color_denoise(demosaiced_image, cc_noise_profile):
61
+ ycc_demosaiced = rgb2ycc(demosaiced_image[:, :, ::-1])
62
+ cc_demosaiced = ycc_demosaiced[:, :, 1:]
63
+ cc_demosaiced_denoised = sk_gaussian(cc_demosaiced, sigma=cc_noise_profile)
64
+ ycc_demosaiced[:, :, 1:] = cc_demosaiced_denoised
65
+ return ycc2rgb(ycc_demosaiced)[:, :, ::-1]
66
+
67
+
68
+ def luminance_denoise(tone_mapped_image, weight=20.0):
69
+ ycc_tone_mapped = rgb2ycc(tone_mapped_image[:, :, ::-1])
70
+ y_tone_mapped = ycc_tone_mapped[:, :, 0]
71
+ y_tone_mapped_denoised = denoise_tv_bregman(y_tone_mapped, weight=weight)
72
+ ycc_tone_mapped[:, :, 0] = np.clip(y_tone_mapped_denoised, 1e-4, 0.999)
73
+ return ycc2rgb(ycc_tone_mapped)[:, :, ::-1]
74
+
75
+
76
+ def white_balance(denoised_image, metadata, max_repeat_limit=10000):
77
+ if metadata["wb_estimation"] is not None:
78
+ as_shot_neutral = np.array(metadata["wb_estimation"])
79
+ white_balanced_image = np.dot(denoised_image, as_shot_neutral.T)
80
+ return np.clip(white_balanced_image, 0.0, 1.0)
81
+ illumuniation_estimation_algorithm = metadata["wb_method"]
82
+ as_shot_neutral = illumination_parameters_estimation(denoised_image, illumuniation_estimation_algorithm)
83
+
84
+ if isinstance(as_shot_neutral[0], Ratio):
85
+ as_shot_neutral = ratios2floats(as_shot_neutral)
86
+
87
+ as_shot_neutral = np.asarray(as_shot_neutral)
88
+ # transform vector into matrix
89
+ if as_shot_neutral.shape == (3,):
90
+ as_shot_neutral = np.diag(1./as_shot_neutral)
91
+
92
+ assert as_shot_neutral.shape == (3, 3)
93
+ repeat_count = 0
94
+ while (as_shot_neutral[0, 0] < 2.3 and as_shot_neutral[2, 2] < 2.3) or (as_shot_neutral[0, 0] < 2.02 or as_shot_neutral[2, 2] < 1.92):
95
+ if repeat_count < max_repeat_limit:
96
+ as_shot_neutral = illumination_parameters_estimation(denoised_image, illumuniation_estimation_algorithm)
97
+ if isinstance(as_shot_neutral[0], Ratio):
98
+ as_shot_neutral = ratios2floats(as_shot_neutral)
99
+
100
+ as_shot_neutral = np.asarray(as_shot_neutral)
101
+ # transform vector into matrix
102
+ if as_shot_neutral.shape == (3,):
103
+ as_shot_neutral = np.diag(1./as_shot_neutral)
104
+
105
+ assert as_shot_neutral.shape == (3, 3)
106
+ else:
107
+ print(f"WARNING! Invalid range for illumination matrix and repeated to estimate by '{illumuniation_estimation_algorithm}' so many times. Using 'gw' for illumination estimation now...")
108
+ as_shot_neutral = illumination_parameters_estimation(denoised_image, "gw")
109
+ if isinstance(as_shot_neutral[0], Ratio):
110
+ as_shot_neutral = ratios2floats(as_shot_neutral)
111
+
112
+ as_shot_neutral = np.asarray(as_shot_neutral)
113
+ # transform vector into matrix
114
+ if as_shot_neutral.shape == (3,):
115
+ as_shot_neutral = np.diag(1./as_shot_neutral)
116
+
117
+ assert as_shot_neutral.shape == (3, 3)
118
+ break
119
+ repeat_count += 1
120
+
121
+ white_balanced_image = np.dot(denoised_image, as_shot_neutral.T)
122
+ metadata["wb_estimation"] = as_shot_neutral.tolist()
123
+
124
+ # print(as_shot_neutral)
125
+ return np.clip(white_balanced_image, 0.0, 1.0)
126
+
127
+
128
+ def xyz_transform(wb_image, color_matrix):
129
+ if isinstance(color_matrix[0], Fraction):
130
+ color_matrix = fractions2floats(color_matrix)
131
+ xyz2cam = np.reshape(np.asarray(color_matrix), (3, 3))
132
+ # normalize rows (needed?)
133
+ xyz2cam = xyz2cam / np.sum(xyz2cam, axis=1, keepdims=True)
134
+ # inverse
135
+ cam2xyz = np.linalg.inv(xyz2cam)
136
+ # for now, use one matrix # TODO: interpolate btween both
137
+ # simplified matrix multiplication
138
+ xyz_image = cam2xyz[np.newaxis, np.newaxis, :, :] * wb_image[:, :, np.newaxis, :]
139
+ xyz_image = np.sum(xyz_image, axis=-1)
140
+ xyz_image = np.clip(xyz_image, 0.0, 1.0)
141
+ return xyz_image
142
+
143
+
144
+ def xyz_to_srgb(xyz_image):
145
+ # srgb2xyz = np.array([[0.4124564, 0.3575761, 0.1804375],
146
+ # [0.2126729, 0.7151522, 0.0721750],
147
+ # [0.0193339, 0.1191920, 0.9503041]])
148
+
149
+ # xyz2srgb = np.linalg.inv(srgb2xyz)
150
+
151
+ xyz2srgb = np.array([[3.2404542, -1.5371385, -0.4985314],
152
+ [-0.9692660, 1.8760108, 0.0415560],
153
+ [0.0556434, -0.2040259, 1.0572252]])
154
+
155
+ # normalize rows (needed?)
156
+ xyz2srgb = xyz2srgb / np.sum(xyz2srgb, axis=-1, keepdims=True)
157
+
158
+ srgb_image = xyz2srgb[np.newaxis, np.newaxis,
159
+ :, :] * xyz_image[:, :, np.newaxis, :]
160
+ srgb_image = np.sum(srgb_image, axis=-1)
161
+ srgb_image = np.clip(srgb_image, 0.0, 1.0)
162
+ return srgb_image
163
+
164
+
165
+ def apply_tmo_flash(Y, a):
166
+ Y[Y == 0] = 1e-9
167
+ return Y / (Y + a * np.exp(np.mean(np.log(Y))))
168
+
169
+
170
+ def apply_tmo_storm(Y, a, kernels):
171
+ rows, cols = Y.shape
172
+ Y[Y == 0] = 1e-9
173
+ return sum([
174
+ Y / (Y + a * np.exp(cv2.boxFilter(np.log(Y), -1, (int(min(rows // kernel, cols // kernel)),) * 2)))
175
+ for kernel in kernels
176
+ ]) / len(kernels)
177
+
178
+
179
+ def apply_tmo_nite(Y, CC, kernels):
180
+ rows, cols = Y.shape
181
+ Y[Y == 0] = 1e-9
182
+ y_mu, y_std = max(Y.mean(), 0.001), Y.std()
183
+ cc_std = CC.std()
184
+ # tmo_offset = np.exp(y_mu * (cc_std / y_std) * 100)
185
+ tmo_offset = 10. / np.sqrt(np.exp(np.log(y_mu) * (np.log(cc_std) / np.log(y_std))) * 100)
186
+ # print(f"Y mean: {y_mu:.3f}, Y std: {y_std:.3f}, CC std: {cc_std:.3f}, Offset: {tmo_offset:.3f}")
187
+
188
+ # tmo_scale = 8.5 + min(6.5, round(tmo_offset))
189
+ tmo_scale = min(28., max(5., tmo_offset))
190
+ # print(f"TMO scale: {tmo_scale}")
191
+ return sum([
192
+ Y / np.clip((Y + tmo_scale * np.exp(cv2.boxFilter(np.log(Y), -1, (int(min(rows // kernel, cols // kernel)),) * 2))), 0., 1.)
193
+ for kernel in kernels
194
+ ]) / len(kernels)
195
+
196
+
197
+ def perform_tone_mapping(source, metadata):
198
+ ycc_source = rgb2ycc(source[:, :, ::-1])
199
+ y_source = ycc_source[:, :, 0]
200
+ cc_source = ycc_source[:, :, 1:]
201
+
202
+ if metadata["tmo_type"].lower() == "flash":
203
+ y_hat_source = apply_tmo_flash(y_source, metadata["tmo_scale"])
204
+ elif metadata["tmo_type"].lower() == "storm":
205
+ y_hat_source = apply_tmo_storm(y_source, metadata["tmo_scale"], metadata["tmo_kernels"])
206
+ else: # nite
207
+ y_hat_source = apply_tmo_nite(y_source, cc_source, metadata["tmo_kernels"])
208
+
209
+ ycc_nite = np.concatenate([
210
+ np.expand_dims(y_hat_source, -1),
211
+ cc_source
212
+ ], axis=-1)
213
+ result = ycc2rgb(ycc_nite)[:, :, ::-1]
214
+ if metadata["tmo_do_leap"]:
215
+ target_mean_grayscale = 0.282 # 72 / 255
216
+ result = np.clip(result, a_min=0., a_max=1.)
217
+ grayscale = cv2.cvtColor(result * 255., cv2.COLOR_BGR2GRAY) / 255.
218
+ result *= target_mean_grayscale / np.mean(grayscale)
219
+ result = np.clip(result, a_min=0., a_max=1.)
220
+ return result
221
+
222
+
223
+ def global_mean_contrast(input_im, beta=1.0):
224
+ mu_ = input_im.mean(axis=(0, 1), keepdims=True)
225
+ output_im = mu_ + beta * (input_im - mu_)
226
+ output_im = np.where(0 > output_im, input_im, output_im)
227
+ output_im = np.where(1 < output_im, input_im, output_im)
228
+ return output_im
229
+
230
+
231
+ def s_curve_correction(input_im, alpha=0.5, lambd=0.5):
232
+ ycc_ = rgb2ycc(input_im[:, :, ::-1])
233
+ Y = ycc_[:, :, 0]
234
+ Y_hat = alpha + np.where(
235
+ Y >= alpha,
236
+ (1 - alpha) * np.power(((Y - alpha) / (1 - alpha)), lambd),
237
+ -alpha * np.power((1 - (Y / alpha)), lambd)
238
+ )
239
+ ycc_[:, :, 0] = Y_hat
240
+ bgr_ = np.clip(ycc2rgb(ycc_)[:, :, ::-1], a_min=0., a_max=1.)
241
+ return bgr_
242
+
243
+
244
+ def histogram_stretching(input_im):
245
+ hsv = rgb2hsv(input_im[:, :, ::-1])
246
+ V = hsv[:, :, 0]
247
+ p0_01, p99 = np.percentile(V, (0.01, 99.99))
248
+ if 0.7 > p99:
249
+ _, p99 = np.percentile(V, (0.01, 99.5))
250
+
251
+ V_hat = rescale_intensity(V, in_range=(p0_01, p99))
252
+ hsv[:, :, 0] = V_hat
253
+ bgr_ = np.clip(hsv2rgb(hsv), a_min=0., a_max=1.)[:, :, ::-1]
254
+ return bgr_
255
+
256
+
257
+ def conditional_contrast_correction(input_im, threshold=0.5):
258
+ ycc_ = rgb2ycc(input_im[:, :, ::-1])
259
+ Y = ycc_[:, :, 0]
260
+ y_avg = Y.mean()
261
+ if y_avg > threshold:
262
+ Y_hat = Y.copy()
263
+ idx = Y_hat <= 0.0031308
264
+ Y_hat[idx] *= 12.92
265
+ Y_hat[idx == False] = (Y_hat[idx == False] ** (1.0 / 2.4)) * 1.055 - 0.055
266
+ else:
267
+ alpha = 0.5
268
+ lambd = 1.2
269
+ Y_hat = alpha + np.where(
270
+ Y >= alpha,
271
+ (1 - alpha) * np.power(((Y - alpha) / (1 - alpha)), lambd),
272
+ -alpha * np.power((1 - (Y / alpha)), lambd)
273
+ )
274
+ ycc_[:, :, 0] = Y_hat
275
+ bgr_ = np.clip(ycc2rgb(ycc_)[:, :, ::-1], a_min=0., a_max=1.)
276
+ return bgr_
277
+
278
+
279
+ def memory_color_enhancement(data, color_space="srgb", illuminant="D65", clip_range=[0, 1], cie_version="1964"):
280
+ target_hue = [30., -125., 100.]
281
+ hue_preference = [20., -118., 130.]
282
+ hue_sigma = [20., 10., 5.]
283
+ is_both_side = [True, False, False]
284
+ multiplier = [0.6, 0.6, 0.6]
285
+ chroma_preference = [25., 14., 30.]
286
+ chroma_sigma = [10., 10., 5.]
287
+
288
+ # RGB to xyz
289
+ data = rgb2xyz(data, color_space, clip_range)
290
+ # xyz to lab
291
+ data = xyz2lab(data, cie_version, illuminant)
292
+ # lab to lch
293
+ data = lab2lch(data)
294
+
295
+ # hue squeezing
296
+ # we are traversing through different color preferences
297
+ height, width, _ = data.shape
298
+ hue_correction = np.zeros((height, width), dtype=np.float32)
299
+ for i in range(0, np.size(target_hue)):
300
+
301
+ delta_hue = data[:, :, 2] - hue_preference[i]
302
+
303
+ if is_both_side[i]:
304
+ weight_temp = np.exp(-np.power(data[:, :, 2] - target_hue[i], 2) / (2 * hue_sigma[i] ** 2)) + \
305
+ np.exp(-np.power(data[:, :, 2] + target_hue[i], 2) / (2 * hue_sigma[i] ** 2))
306
+ else:
307
+ weight_temp = np.exp(-np.power(data[:, :, 2] - target_hue[i], 2) / (2 * hue_sigma[i] ** 2))
308
+
309
+ weight_hue = multiplier[i] * weight_temp / np.max(weight_temp)
310
+
311
+ weight_chroma = np.exp(-np.power(data[:, :, 1] - chroma_preference[i], 2) / (2 * chroma_sigma[i] ** 2))
312
+
313
+ hue_correction = hue_correction + np.multiply(np.multiply(delta_hue, weight_hue), weight_chroma)
314
+
315
+ # correct the hue
316
+ data[:, :, 2] = data[:, :, 2] - hue_correction
317
+
318
+ # lch to lab
319
+ data = lch2lab(data)
320
+ # lab to xyz
321
+ data = lab2xyz(data, cie_version, illuminant)
322
+ # xyz to rgb
323
+ data = xyz2rgb(data, color_space, clip_range)
324
+
325
+ data = outOfGamutClipping(data, range=clip_range[1])
326
+ return data
327
+
328
+
329
+ def unsharp_masking(data, gaussian_kernel_size=[5, 5], gaussian_sigma=2.0, slope=1.5, tau_threshold=0.05, gamma_speed=4., clip_range=[0, 1]):
330
+ # create gaussian kernel
331
+ gaussian_kernel = gaussian(gaussian_kernel_size, gaussian_sigma)
332
+
333
+ # convolve the image with the gaussian kernel
334
+ # first input is the image
335
+ # second input is the kernel
336
+ # output shape will be the same as the first input
337
+ # boundary will be padded by using symmetrical method while convolving
338
+ if np.ndim(data) > 2:
339
+ image_blur = np.empty(np.shape(data), dtype=np.float32)
340
+ for i in range(0, np.shape(data)[2]):
341
+ image_blur[:, :, i] = signal.convolve2d(data[:, :, i], gaussian_kernel, mode="same", boundary="symm")
342
+ else:
343
+ image_blur = signal.convolve2d(data, gaussian_kernel, mode="same", boundary="symm")
344
+
345
+ # the high frequency component image
346
+ image_high_pass = data - image_blur
347
+
348
+ # soft coring (see in utility)
349
+ # basically pass the high pass image via a slightly nonlinear function
350
+ tau_threshold = tau_threshold * clip_range[1]
351
+
352
+ # add the soft cored high pass image to the original and clip
353
+ # within range and return
354
+ def soft_coring(img_hp, slope, tau_threshold, gamma_speed):
355
+ return slope * np.float32(img_hp) * (1. - np.exp(-((np.abs(img_hp / tau_threshold))**gamma_speed)))
356
+ return np.clip(data + soft_coring(image_high_pass, slope, tau_threshold, gamma_speed), clip_range[0], clip_range[1])
357
+
358
+
359
+ def to_uint8(srgb):
360
+ return (srgb * 255).astype(np.uint8)
361
+
362
+
363
+ def resize(img, width=None, height=None):
364
+ if width is None or height is None:
365
+ return img
366
+ img_pil = Image.fromarray(img)
367
+ out_size = (width, height)
368
+ if img_pil.size == out_size:
369
+ return img
370
+ out_img = img_pil.resize(out_size, Image.Resampling.LANCZOS)
371
+ out_img = np.array(out_img)
372
+ return out_img
373
+
374
+
375
+ def fix_orientation(image, orientation):
376
+ # 1 = Horizontal (normal)
377
+ # 2 = Mirror horizontal
378
+ # 3 = Rotate 180
379
+ # 4 = Mirror vertical
380
+ # 5 = Mirror horizontal and rotate 270 CW
381
+ # 6 = Rotate 90 CW
382
+ # 7 = Mirror horizontal and rotate 90 CW
383
+ # 8 = Rotate 270 CW
384
+
385
+ orientation_dict = {
386
+ "Horizontal (normal)": 1,
387
+ "Mirror horizontal": 2,
388
+ "Rotate 180": 3,
389
+ "Mirror vertical": 4,
390
+ "Mirror horizontal and rotate 270 CW": 5,
391
+ "Rotate 90 CW": 6,
392
+ "Mirror horizontal and rotate 90 CW": 7,
393
+ "Rotate 270 CW": 8
394
+ }
395
+
396
+ if type(orientation) is list:
397
+ orientation = orientation[0]
398
+ orientation = orientation_dict[orientation]
399
+ if orientation == 1:
400
+ pass
401
+ elif orientation == 2:
402
+ image = cv2.flip(image, 0)
403
+ elif orientation == 3:
404
+ image = cv2.rotate(image, cv2.ROTATE_180)
405
+ elif orientation == 4:
406
+ image = cv2.flip(image, 1)
407
+ elif orientation == 5:
408
+ image = cv2.flip(image, 0)
409
+ image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
410
+ elif orientation == 6:
411
+ image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
412
+ elif orientation == 7:
413
+ image = cv2.flip(image, 0)
414
+ image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
415
+ elif orientation == 8:
416
+ image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
417
+
418
+ return image
OzUVGL/ips/wb.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from utils.color import rgb2ycc
3
+
4
+ def illumination_parameters_estimation(current_image, illumination_estimation_option):
5
+ ie_method = illumination_estimation_option.lower()
6
+
7
+ if ie_method == "gw":
8
+ ie = np.mean(current_image, axis=(0, 1))
9
+ ie /= ie[1]
10
+ return ie
11
+ elif ie_method == "sog":
12
+ sog_p = 4.
13
+ ie = np.mean(current_image**sog_p, axis=(0, 1))**(1/sog_p)
14
+ ie /= ie[1]
15
+ return ie
16
+ elif ie_method == "wp":
17
+ ie = np.max(current_image, axis=(0, 1))
18
+ ie /= ie[1]
19
+ return ie
20
+ elif ie_method == "iwp":
21
+ samples_count = 10
22
+ sample_size = 10
23
+ rows, cols = current_image.shape[:2]
24
+ data = np.reshape(current_image, (rows*cols, 3))
25
+ maxima = np.zeros((samples_count, 3))
26
+ for i in range(samples_count):
27
+ maxima[i, :] = np.max(data[np.random.randint(low=0, high=rows*cols, size=(sample_size)), :], axis=0)
28
+ ie = np.mean(maxima, axis=0)
29
+ ie /= ie[1]
30
+ return ie
31
+ else:
32
+ raise ValueError(
33
+ 'Bad illumination_estimation_option value! Use the following options: "gw", "wp", "sog", "iwp"')
34
+
35
+
36
+ def ratios2floats(ratios):
37
+ floats = []
38
+ for ratio in ratios:
39
+ floats.append(float(ratio.num) / ratio.den)
40
+ return floats
OzUVGL/main.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import random
4
+ import glog as log
5
+ import numpy as np
6
+ from typing import List
7
+
8
+ from utils.io import read_image, write_processed_as_jpg, write_illuminant_estimation
9
+ import ips
10
+
11
+ expected_landscape_img_height = 768 # 6144
12
+ expected_landscape_img_width = 1024 # 8192
13
+
14
+
15
+ # Flash TMO works better with a=20 and Leap_35 for night images.
16
+ # Storm TMO tends to have higher a value than the default one. Leap is must.
17
+ # Not stable for different illuminant settings, so the scale parameter should be adaptive to something.
18
+ # Luma and Color statistics could be the best option we have to make it adaptive.
19
+ # Higher number of kernels higher details visible in local areas, however too large numbers produces flares or makes it unrealistic.
20
+ def single_run(
21
+ base_dir: str,
22
+ img_names: List,
23
+ out_dir: str,
24
+ wb_method: str = "iwp",
25
+ tmo_type: str = "nite",
26
+ tv_weight: int = 20
27
+ ):
28
+ log.info(
29
+ "Parameters:\n"
30
+ f"WB Method: {wb_method}\n"
31
+ f"TMO Type: {tmo_type}\n"
32
+ f"Luma TV weight : {tv_weight}\n"
33
+ )
34
+ os.makedirs("./" + out_dir, exist_ok=True)
35
+ # random.shuffle(img_names)
36
+ infer_times = list()
37
+
38
+ for i, img_name in enumerate(img_names):
39
+ p = round(100 * (i+1) / len(img_names), 2)
40
+ log.info(f"({p:.2f}%) Processing {i+1} of {len(img_names)} images, image name: {img_name}")
41
+ path = os.path.join(base_dir, img_name)
42
+ assert os.path.exists(path)
43
+
44
+ raw_image, metadata = read_image(path)
45
+ save_ill_est = metadata["wb_estimation"] is None
46
+ metadata["exp_height"] = expected_landscape_img_height
47
+ metadata["exp_width"] = expected_landscape_img_width
48
+ metadata["wb_method"] = wb_method
49
+ metadata["tv_weight"] = tv_weight
50
+ metadata["tmo_type"] = tmo_type
51
+ if tmo_type.lower() in ["flash", "storm"]:
52
+ metadata["tmo_scale"] = 10 # 20 can be also used, 10 better for some images, but 20 for some others depending on the variety of the illuminant source.
53
+ if tmo_type.lower() in ["storm", "nite"]:
54
+ metadata["tmo_kernels"] = (1, 2, 4, 8, 16, 32) # more than 16, produce flares in dark regions in the case of occlusion.
55
+ metadata["tmo_do_leap"] = True # Leap is must for Flash, Storm and Nite.
56
+ metadata["global_mc_beta"] = 1.2
57
+ metadata["scc_alpha"] = 0.5
58
+ metadata["scc_lambda"] = 0.9
59
+
60
+ out_path = os.path.join(out_dir, img_name.replace("png", "jpg"))
61
+ if os.path.exists(out_path):
62
+ continue
63
+ start_time = time.time()
64
+ out = ips.process(raw_image=raw_image, metadata=metadata)
65
+ end_time = time.time()
66
+ infer_times.append(end_time - start_time)
67
+
68
+ if save_ill_est:
69
+ ill_est_path = os.path.join(out_dir, img_name.replace(".png", "_wb.json"))
70
+ write_illuminant_estimation(metadata["wb_estimation"], ill_est_path)
71
+ write_processed_as_jpg(out, out_path)
72
+ print(f"Average inference time: {np.mean(infer_times)} seconds")
73
+
74
+
75
+ if __name__ == "__main__":
76
+ import argparse
77
+ parser = argparse.ArgumentParser(description='Night Photography Rendering Challenge - Team VGL OzU')
78
+ parser.add_argument('-d', '--data_dir', type=str, default="data/", help="data directory")
79
+ parser.add_argument('-o', '--output_dir', type=str, default="results/", help="output directory")
80
+ parser.add_argument('-s', '--submission_name', type=str, default="vgl-ozu", help='submission name')
81
+ args = parser.parse_args()
82
+
83
+ data_dir = args.data_dir
84
+ if not os.path.exists(data_dir) or len(os.listdir(data_dir)) == 0:
85
+ log.info(f"Data does not exist, please put the data from given link into '{data_dir}'...")
86
+ os.makedirs(data_dir, exist_ok=True)
87
+ log.info("After this, please re-run.")
88
+ else:
89
+ base_dir = args.data_dir
90
+ out_dir = args.output_dir
91
+ img_names = os.listdir(base_dir)
92
+ img_names = [img_name for img_name in img_names if ".png" in img_name]
93
+ single_run(base_dir, img_names, out_dir)
OzUVGL/requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ colour_demosaicing==0.2.5
2
+ ExifRead==3.0.0
3
+ glog==0.3.1
4
+ numpy==1.24.3
5
+ opencv_contrib_python==4.7.0.72
6
+ Pillow==10.2.0
7
+ requests==2.31.0
8
+ scikit_image==0.19.3
9
+ scipy==1.12.0
10
+ scikit-image
OzUVGL/run.sh ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ python3 main.py
OzUVGL/utils/color.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+
4
+ def rgb2gray(data):
5
+ return 0.299 * data[:, :, 0] + \
6
+ 0.587 * data[:, :, 1] + \
7
+ 0.114 * data[:, :, 2]
8
+
9
+
10
+ def rgb2ycc(data, rule="bt601"):
11
+ # map to select kr and kb
12
+ kr_kb_dict = {"bt601": [0.299, 0.114],
13
+ "bt709": [0.2126, 0.0722],
14
+ "bt2020": [0.2627, 0.0593]}
15
+
16
+ kr = kr_kb_dict[rule][0]
17
+ kb = kr_kb_dict[rule][1]
18
+ kg = 1 - (kr + kb)
19
+
20
+ output = np.empty(np.shape(data), dtype=np.float32)
21
+ output[:, :, 0] = kr * data[:, :, 0] + \
22
+ kg * data[:, :, 1] + \
23
+ kb * data[:, :, 2]
24
+ output[:, :, 1] = 0.5 * ((data[:, :, 2] - output[:, :, 0]) / (1 - kb))
25
+ output[:, :, 2] = 0.5 * ((data[:, :, 0] - output[:, :, 0]) / (1 - kr))
26
+
27
+ return output
28
+
29
+
30
+ def ycc2rgb(data, rule="bt601"):
31
+ # map to select kr and kb
32
+ kr_kb_dict = {"bt601": [0.299, 0.114],
33
+ "bt709": [0.2126, 0.0722],
34
+ "bt2020": [0.2627, 0.0593]}
35
+
36
+ kr = kr_kb_dict[rule][0]
37
+ kb = kr_kb_dict[rule][1]
38
+ kg = 1 - (kr + kb)
39
+
40
+ output = np.empty(np.shape(data), dtype=np.float32)
41
+ output[:, :, 0] = 2. * data[:, :, 2] * (1 - kr) + data[:, :, 0]
42
+ output[:, :, 2] = 2. * data[:, :, 1] * (1 - kb) + data[:, :, 0]
43
+ output[:, :, 1] = (data[:, :, 0] - kr * output[:, :, 0] - kb * output[:, :, 2]) / kg
44
+
45
+ return output
46
+
47
+
48
+ def degamma_srgb(data, clip_range=[0, 65535]):
49
+ # bring data in range 0 to 1
50
+ data = np.clip(data, clip_range[0], clip_range[1])
51
+ data = np.divide(data, clip_range[1])
52
+
53
+ data = np.asarray(data)
54
+ mask = data > 0.04045
55
+
56
+ # basically, if data[x, y, c] > 0.04045, data[x, y, c] = ( (data[x, y, c] + 0.055) / 1.055 ) ^ 2.4
57
+ # else, data[x, y, c] = data[x, y, c] / 12.92
58
+ data[mask] += 0.055
59
+ data[mask] /= 1.055
60
+ data[mask] **= 2.4
61
+
62
+ data[np.invert(mask)] /= 12.92
63
+
64
+ # rescale
65
+ return np.clip(data * clip_range[1], clip_range[0], clip_range[1])
66
+
67
+
68
+ def degamma_adobe_rgb_1998(data, clip_range=[0, 65535]):
69
+ # bring data in range 0 to 1
70
+ data = np.clip(data, clip_range[0], clip_range[1])
71
+ data = np.divide(data, clip_range[1])
72
+
73
+ data = np.power(data, 2.2) # originally raised to 2.19921875
74
+
75
+ # rescale
76
+ return np.clip(data * clip_range[1], clip_range[0], clip_range[1])
77
+
78
+
79
+ def rgb2xyz(data, color_space="srgb", clip_range=[0, 255]):
80
+ # input rgb in range clip_range
81
+ # output xyz is in range 0 to 1
82
+ if color_space == "srgb":
83
+ # degamma / linearization
84
+ data = degamma_srgb(data, clip_range)
85
+ data = np.float32(data)
86
+ data = np.divide(data, clip_range[1])
87
+
88
+ # matrix multiplication`
89
+ output = np.empty(np.shape(data), dtype=np.float32)
90
+ output[:, :, 0] = data[:, :, 0] * 0.4124 + data[:, :, 1] * 0.3576 + data[:, :, 2] * 0.1805
91
+ output[:, :, 1] = data[:, :, 0] * 0.2126 + data[:, :, 1] * 0.7152 + data[:, :, 2] * 0.0722
92
+ output[:, :, 2] = data[:, :, 0] * 0.0193 + data[:, :, 1] * 0.1192 + data[:, :, 2] * 0.9505
93
+ elif color_space == "adobe-rgb-1998":
94
+ # degamma / linearization
95
+ data = degamma_adobe_rgb_1998(data, clip_range)
96
+ data = np.float32(data)
97
+ data = np.divide(data, clip_range[1])
98
+
99
+ # matrix multiplication
100
+ output = np.empty(np.shape(data), dtype=np.float32)
101
+ output[:, :, 0] = data[:, :, 0] * 0.5767309 + data[:, :, 1] * 0.1855540 + data[:, :, 2] * 0.1881852
102
+ output[:, :, 1] = data[:, :, 0] * 0.2973769 + data[:, :, 1] * 0.6273491 + data[:, :, 2] * 0.0752741
103
+ output[:, :, 2] = data[:, :, 0] * 0.0270343 + data[:, :, 1] * 0.0706872 + data[:, :, 2] * 0.9911085
104
+ elif color_space == "linear":
105
+ # matrix multiplication`
106
+ output = np.empty(np.shape(data), dtype=np.float32)
107
+ data = np.float32(data)
108
+ data = np.divide(data, clip_range[1])
109
+ output[:, :, 0] = data[:, :, 0] * 0.4124 + data[:, :, 1] * 0.3576 + data[:, :, 2] * 0.1805
110
+ output[:, :, 1] = data[:, :, 0] * 0.2126 + data[:, :, 1] * 0.7152 + data[:, :, 2] * 0.0722
111
+ output[:, :, 2] = data[:, :, 0] * 0.0193 + data[:, :, 1] * 0.1192 + data[:, :, 2] * 0.9505
112
+ else:
113
+ print("Warning! color_space must be srgb or adobe-rgb-1998.")
114
+ return
115
+
116
+ return output
117
+
118
+
119
+ def gamma_srgb(data, clip_range=[0, 65535]):
120
+ # bring data in range 0 to 1
121
+ data = np.clip(data, clip_range[0], clip_range[1])
122
+ data = np.divide(data, clip_range[1])
123
+
124
+ data = np.asarray(data)
125
+ mask = data > 0.0031308
126
+
127
+ # basically, if data[x, y, c] > 0.0031308, data[x, y, c] = 1.055 * ( var_R(i, j) ^ ( 1 / 2.4 ) ) - 0.055
128
+ # else, data[x, y, c] = data[x, y, c] * 12.92
129
+ data[mask] **= 0.4167
130
+ data[mask] *= 1.055
131
+ data[mask] -= 0.055
132
+
133
+ data[np.invert(mask)] *= 12.92
134
+
135
+ # rescale
136
+ return np.clip(data * clip_range[1], clip_range[0], clip_range[1])
137
+
138
+
139
+ def gamma_adobe_rgb_1998(data, clip_range=[0, 65535]):
140
+ # bring data in range 0 to 1
141
+ data = np.clip(data, clip_range[0], clip_range[1])
142
+ data = np.divide(data, clip_range[1])
143
+
144
+ data = np.power(data, 0.4545)
145
+
146
+ # rescale
147
+ return np.clip(data * clip_range[1], clip_range[0], clip_range[1])
148
+
149
+
150
+ def xyz2rgb(data, color_space="srgb", clip_range=[0, 255]):
151
+ # input xyz is in range 0 to 1
152
+ # output rgb in clip_range
153
+
154
+ # allocate space for output
155
+ output = np.empty(np.shape(data), dtype=np.float32)
156
+
157
+ if color_space == "srgb":
158
+ # matrix multiplication
159
+ output[:, :, 0] = data[:, :, 0] * 3.2406 + data[:, :, 1] * -1.5372 + data[:, :, 2] * -0.4986
160
+ output[:, :, 1] = data[:, :, 0] * -0.9689 + data[:, :, 1] * 1.8758 + data[:, :, 2] * 0.0415
161
+ output[:, :, 2] = data[:, :, 0] * 0.0557 + data[:, :, 1] * -0.2040 + data[:, :, 2] * 1.0570
162
+
163
+ # gamma to retain nonlinearity
164
+ output = gamma_srgb(output * clip_range[1], clip_range)
165
+ elif color_space == "adobe-rgb-1998":
166
+ # matrix multiplication
167
+ output[:, :, 0] = data[:, :, 0] * 2.0413690 + data[:, :, 1] * -0.5649464 + data[:, :, 2] * -0.3446944
168
+ output[:, :, 1] = data[:, :, 0] * -0.9692660 + data[:, :, 1] * 1.8760108 + data[:, :, 2] * 0.0415560
169
+ output[:, :, 2] = data[:, :, 0] * 0.0134474 + data[:, :, 1] * -0.1183897 + data[:, :, 2] * 1.0154096
170
+
171
+ # gamma to retain nonlinearity
172
+ output = gamma_adobe_rgb_1998(output * clip_range[1], clip_range)
173
+ elif color_space == "linear":
174
+
175
+ # matrix multiplication
176
+ output[:, :, 0] = data[:, :, 0] * 3.2406 + data[:, :, 1] * -1.5372 + data[:, :, 2] * -0.4986
177
+ output[:, :, 1] = data[:, :, 0] * -0.9689 + data[:, :, 1] * 1.8758 + data[:, :, 2] * 0.0415
178
+ output[:, :, 2] = data[:, :, 0] * 0.0557 + data[:, :, 1] * -0.2040 + data[:, :, 2] * 1.0570
179
+
180
+ # gamma to retain nonlinearity
181
+ output = output * clip_range[1]
182
+ else:
183
+ print("Warning! color_space must be srgb or adobe-rgb-1998.")
184
+ return
185
+
186
+ return output
187
+
188
+
189
+ def get_xyz_reference(cie_version="1931", illuminant="d65"):
190
+ if cie_version == "1931":
191
+ xyz_reference_dictionary = {"A": [109.850, 100.0, 35.585],
192
+ "B": [99.0927, 100.0, 85.313],
193
+ "C": [98.074, 100.0, 118.232],
194
+ "d50": [96.422, 100.0, 82.521],
195
+ "d55": [95.682, 100.0, 92.149],
196
+ "d65": [95.047, 100.0, 108.883],
197
+ "d75": [94.972, 100.0, 122.638],
198
+ "E": [100.0, 100.0, 100.0],
199
+ "F1": [92.834, 100.0, 103.665],
200
+ "F2": [99.187, 100.0, 67.395],
201
+ "F3": [103.754, 100.0, 49.861],
202
+ "F4": [109.147, 100.0, 38.813],
203
+ "F5": [90.872, 100.0, 98.723],
204
+ "F6": [97.309, 100.0, 60.191],
205
+ "F7": [95.044, 100.0, 108.755],
206
+ "F8": [96.413, 100.0, 82.333],
207
+ "F9": [100.365, 100.0, 67.868],
208
+ "F10": [96.174, 100.0, 81.712],
209
+ "F11": [100.966, 100.0, 64.370],
210
+ "F12": [108.046, 100.0, 39.228]}
211
+ elif cie_version == "1964":
212
+ xyz_reference_dictionary = {"A": [111.144, 100.0, 35.200],
213
+ "B": [99.178, 100.0, 84.3493],
214
+ "C": [97.285, 100.0, 116.145],
215
+ "D50": [96.720, 100.0, 81.427],
216
+ "D55": [95.799, 100.0, 90.926],
217
+ "D65": [94.811, 100.0, 107.304],
218
+ "D75": [94.416, 100.0, 120.641],
219
+ "E": [100.0, 100.0, 100.0],
220
+ "F1": [94.791, 100.0, 103.191],
221
+ "F2": [103.280, 100.0, 69.026],
222
+ "F3": [108.968, 100.0, 51.965],
223
+ "F4": [114.961, 100.0, 40.963],
224
+ "F5": [93.369, 100.0, 98.636],
225
+ "F6": [102.148, 100.0, 62.074],
226
+ "F7": [95.792, 100.0, 107.687],
227
+ "F8": [97.115, 100.0, 81.135],
228
+ "F9": [102.116, 100.0, 67.826],
229
+ "F10": [99.001, 100.0, 83.134],
230
+ "F11": [103.866, 100.0, 65.627],
231
+ "F12": [111.428, 100.0, 40.353]}
232
+ else:
233
+ print("Warning! cie_version must be 1931 or 1964.")
234
+ return
235
+ return np.divide(xyz_reference_dictionary[illuminant], 100.0)
236
+
237
+
238
+ def xyz2lab(data, cie_version="1931", illuminant="d65"):
239
+ xyz_reference = get_xyz_reference(cie_version, illuminant)
240
+
241
+ data = data
242
+ data[:, :, 0] = data[:, :, 0] / xyz_reference[0]
243
+ data[:, :, 1] = data[:, :, 1] / xyz_reference[1]
244
+ data[:, :, 2] = data[:, :, 2] / xyz_reference[2]
245
+
246
+ data = np.asarray(data)
247
+
248
+ # if data[x, y, c] > 0.008856, data[x, y, c] = data[x, y, c] ^ (1/3)
249
+ # else, data[x, y, c] = 7.787 * data[x, y, c] + 16/116
250
+ mask = data > 0.008856
251
+ data[mask] **= 1. / 3.
252
+ data[np.invert(mask)] *= 7.787
253
+ data[np.invert(mask)] += 16. / 116.
254
+
255
+ data = np.float32(data)
256
+ output = np.empty(np.shape(data), dtype=np.float32)
257
+ output[:, :, 0] = 116. * data[:, :, 1] - 16.
258
+ output[:, :, 1] = 500. * (data[:, :, 0] - data[:, :, 1])
259
+ output[:, :, 2] = 200. * (data[:, :, 1] - data[:, :, 2])
260
+
261
+ return output
262
+
263
+
264
+ def lab2xyz(data, cie_version="1931", illuminant="d65"):
265
+ output = np.empty(np.shape(data), dtype=np.float32)
266
+
267
+ output[:, :, 1] = (data[:, :, 0] + 16.) / 116.
268
+ output[:, :, 0] = (data[:, :, 1] / 500.) + output[:, :, 1]
269
+ output[:, :, 2] = output[:, :, 1] - (data[:, :, 2] / 200.)
270
+
271
+ # if output[x, y, c] > 0.008856, output[x, y, c] ^ 3
272
+ # else, output[x, y, c] = ( output[x, y, c] - 16/116 ) / 7.787
273
+ output = np.asarray(output)
274
+ mask = output > 0.008856
275
+ output[mask] **= 3.
276
+ output[np.invert(mask)] -= 16 / 116
277
+ output[np.invert(mask)] /= 7.787
278
+
279
+ xyz_reference = get_xyz_reference(cie_version, illuminant)
280
+
281
+ output = np.float32(output)
282
+ output[:, :, 0] = output[:, :, 0] * xyz_reference[0]
283
+ output[:, :, 1] = output[:, :, 1] * xyz_reference[1]
284
+ output[:, :, 2] = output[:, :, 2] * xyz_reference[2]
285
+
286
+ return output
287
+
288
+
289
+ def lab2lch(data):
290
+ output = np.empty(np.shape(data), dtype=np.float32)
291
+
292
+ output[:, :, 0] = data[:, :, 0] # L transfers directly
293
+ output[:, :, 1] = np.power(np.power(data[:, :, 1], 2) + np.power(data[:, :, 2], 2), 0.5)
294
+ output[:, :, 2] = np.arctan2(data[:, :, 2], data[:, :, 1]) * 180 / np.pi
295
+
296
+ return output
297
+
298
+
299
+ def lch2lab(data):
300
+ output = np.empty(np.shape(data), dtype=np.float32)
301
+
302
+ output[:, :, 0] = data[:, :, 0] # L transfers directly
303
+ output[:, :, 1] = np.multiply(np.cos(data[:, :, 2] * np.pi / 180), data[:, :, 1])
304
+ output[:, :, 2] = np.multiply(np.sin(data[:, :, 2] * np.pi / 180), data[:, :, 1])
305
+
306
+ return output
OzUVGL/utils/io.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import json
3
+ from pathlib import Path
4
+ from fractions import Fraction
5
+
6
+
7
+ def fraction_from_json(json_object):
8
+ if 'Fraction' in json_object:
9
+ return Fraction(*json_object['Fraction'])
10
+ return json_object
11
+
12
+
13
+ def json_read(fname, **kwargs):
14
+ with open(fname) as j:
15
+ data = json.load(j, **kwargs)
16
+ return data
17
+
18
+
19
+ def read_image(path):
20
+ png_path = Path(path)
21
+ raw_image = cv2.imread(str(png_path), cv2.IMREAD_UNCHANGED)
22
+ metadata = json_read(png_path.with_suffix('.json'), object_hook=fraction_from_json)
23
+
24
+ ill_path = Path(str(png_path).replace(".png", "_wb.json"))
25
+ if ill_path.with_suffix('.json').exists():
26
+ metadata["wb_estimation"] = json_read(ill_path, object_hook=fraction_from_json)
27
+ else:
28
+ print("WARNING! Illuminant estimations are not included in data folder and results may differ due to the randomness in the algorithm.")
29
+ print("For reproducibility, please include the corresponding files to that folder.")
30
+ metadata["wb_estimation"] = None
31
+ return raw_image, metadata
32
+
33
+
34
+ def write_processed_as_jpg(out, dst_path, quality=100):
35
+ cv2.imwrite(dst_path, out, [cv2.IMWRITE_JPEG_QUALITY, quality])
36
+
37
+
38
+ def write_illuminant_estimation(as_shot_neutral, dst_path):
39
+ with open(dst_path, 'w') as f:
40
+ json.dump(as_shot_neutral, f)
41
+
42
+
43
+ def download_weights(url, fname):
44
+ import requests
45
+ r = requests.get(url, stream=True)
46
+ with open(fname, 'wb') as f:
47
+ total_length = int(r.headers.get('content-length'))
48
+ for chunk in r.iter_content(chunk_size=1024):
49
+ if chunk:
50
+ f.write(chunk)
51
+ f.flush()
52
+
53
+
54
+ def unzip(path_to_zip_file, directory_to_extract_to):
55
+ import zipfile
56
+ with zipfile.ZipFile(path_to_zip_file, 'r') as zip_ref:
57
+ zip_ref.extractall(directory_to_extract_to)
OzUVGL/utils/misc.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from math import ceil
3
+
4
+
5
+ def decode_cfa_pattern(cfa_pattern):
6
+ cfa_dict = {0: 'B', 1: 'G', 2: 'R'}
7
+ return "".join([cfa_dict[x] for x in cfa_pattern])
8
+
9
+
10
+ def outOfGamutClipping(I, range=1.):
11
+ """ Clips out-of-gamut pixels. """
12
+ if range == 1.:
13
+ I[I > 1] = 1 # any pixel is higher than 1, clip it to 1
14
+ I[I < 0] = 0 # any pixel is below 0, clip it to 0
15
+ else:
16
+ I[I > 255] = 255 # any pixel is higher than 255, clip it to 255
17
+ I[I < 0] = 0 # any pixel is below 0, clip it to 0
18
+ return I
19
+
20
+
21
+ def ratios2floats(ratios):
22
+ floats = []
23
+ for ratio in ratios:
24
+ floats.append(float(ratio.num) / ratio.den)
25
+ return floats
26
+
27
+
28
+ def fractions2floats(fractions):
29
+ floats = []
30
+ for fraction in fractions:
31
+ floats.append(float(fraction.numerator) / fraction.denominator)
32
+ return floats
33
+
34
+
35
+ def gaussian(kernel_size, sigma):
36
+ # calculate which number to where the grid should be
37
+ # remember that, kernel_size[0] is the width of the kernel
38
+ # and kernel_size[1] is the height of the kernel
39
+ temp = np.floor(np.float32(kernel_size) / 2.)
40
+
41
+ # create the grid
42
+ # example: if kernel_size = [5, 3], then:
43
+ # x: array([[-2., -1., 0., 1., 2.],
44
+ # [-2., -1., 0., 1., 2.],
45
+ # [-2., -1., 0., 1., 2.]])
46
+ # y: array([[-1., -1., -1., -1., -1.],
47
+ # [ 0., 0., 0., 0., 0.],
48
+ # [ 1., 1., 1., 1., 1.]])
49
+ x, y = np.meshgrid(np.linspace(-temp[0], temp[0], kernel_size[0]), np.linspace(-temp[1], temp[1], kernel_size[1]))
50
+
51
+ # Gaussian equation
52
+ temp = np.exp(-(x ** 2 + y ** 2) / (2. * sigma ** 2))
53
+
54
+ # make kernel sum equal to 1
55
+ return temp / np.sum(temp)
56
+
57
+
58
+ def aspect_ratio_imresize(im, max_output=256):
59
+ h, w, c = im.shape
60
+ if max(h, w) > max_output:
61
+ ratio = max_output / max(h, w)
62
+ im = imresize.imresize(im, scalar_scale=ratio)
63
+ h, w, c = im.shape
64
+
65
+ if w % (2 ** 4) == 0:
66
+ new_size_w = w
67
+ else:
68
+ new_size_w = w + (2 ** 4) - w % (2 ** 4)
69
+
70
+ if h % (2 ** 4) == 0:
71
+ new_size_h = h
72
+ else:
73
+ new_size_h = h + (2 ** 4) - h % (2 ** 4)
74
+
75
+ new_size = (new_size_h, new_size_w)
76
+ if not ((h, w) == new_size):
77
+ im = imresize.imresize(im, output_shape=new_size)
78
+
79
+ return im
80
+
81
+
82
+ def cubic(x):
83
+ x = np.array(x).astype(np.float64)
84
+ absx = np.absolute(x)
85
+ absx2 = np.multiply(absx, absx)
86
+ absx3 = np.multiply(absx2, absx)
87
+ f = np.multiply(1.5*absx3 - 2.5*absx2 + 1, absx <= 1) + np.multiply(-0.5*absx3 + 2.5*absx2 - 4*absx + 2, (1 < absx) & (absx <= 2))
88
+ return f
89
+
90
+
91
+ def triangle(x):
92
+ x = np.array(x).astype(np.float64)
93
+ lessthanzero = np.logical_and((x>=-1),x<0)
94
+ greaterthanzero = np.logical_and((x<=1),x>=0)
95
+ f = np.multiply((x+1),lessthanzero) + np.multiply((1-x),greaterthanzero)
96
+ return f
97
+
98
+
99
+ def deriveSizeFromScale(img_shape, scale):
100
+ output_shape = []
101
+ for k in range(2):
102
+ output_shape.append(int(ceil(scale[k] * img_shape[k])))
103
+ return output_shape
104
+
105
+
106
+ def deriveScaleFromSize(img_shape_in, img_shape_out):
107
+ scale = []
108
+ for k in range(2):
109
+ scale.append(1.0 * img_shape_out[k] / img_shape_in[k])
110
+ return scale
111
+
112
+
113
+ def contributions(in_length, out_length, scale, kernel, k_width):
114
+ if scale < 1:
115
+ h = lambda x: scale * kernel(scale * x)
116
+ kernel_width = 1.0 * k_width / scale
117
+ else:
118
+ h = kernel
119
+ kernel_width = k_width
120
+ x = np.arange(1, out_length+1).astype(np.float64)
121
+ u = x / scale + 0.5 * (1 - 1 / scale)
122
+ left = np.floor(u - kernel_width / 2)
123
+ P = int(ceil(kernel_width)) + 2
124
+ ind = np.expand_dims(left, axis=1) + np.arange(P) - 1 # -1 because indexing from 0
125
+ indices = ind.astype(np.int32)
126
+ weights = h(np.expand_dims(u, axis=1) - indices - 1) # -1 because indexing from 0
127
+ weights = np.divide(weights, np.expand_dims(np.sum(weights, axis=1), axis=1))
128
+ aux = np.concatenate((np.arange(in_length), np.arange(in_length - 1, -1, step=-1))).astype(np.int32)
129
+ indices = aux[np.mod(indices, aux.size)]
130
+ ind2store = np.nonzero(np.any(weights, axis=0))
131
+ weights = weights[:, ind2store]
132
+ indices = indices[:, ind2store]
133
+ return weights, indices
134
+
135
+
136
+ def imresizemex(inimg, weights, indices, dim):
137
+ in_shape = inimg.shape
138
+ w_shape = weights.shape
139
+ out_shape = list(in_shape)
140
+ out_shape[dim] = w_shape[0]
141
+ outimg = np.zeros(out_shape)
142
+ if dim == 0:
143
+ for i_img in range(in_shape[1]):
144
+ for i_w in range(w_shape[0]):
145
+ w = weights[i_w, :]
146
+ ind = indices[i_w, :]
147
+ im_slice = inimg[ind, i_img].astype(np.float64)
148
+ outimg[i_w, i_img] = np.sum(np.multiply(np.squeeze(im_slice, axis=0), w.T), axis=0)
149
+ elif dim == 1:
150
+ for i_img in range(in_shape[0]):
151
+ for i_w in range(w_shape[0]):
152
+ w = weights[i_w, :]
153
+ ind = indices[i_w, :]
154
+ im_slice = inimg[i_img, ind].astype(np.float64)
155
+ outimg[i_img, i_w] = np.sum(np.multiply(np.squeeze(im_slice, axis=0), w.T), axis=0)
156
+ if inimg.dtype == np.uint8:
157
+ outimg = np.clip(outimg, 0, 255)
158
+ return np.around(outimg).astype(np.uint8)
159
+ else:
160
+ return outimg
161
+
162
+
163
+ def imresizevec(inimg, weights, indices, dim):
164
+ wshape = weights.shape
165
+ if dim == 0:
166
+ weights = weights.reshape((wshape[0], wshape[2], 1, 1))
167
+ outimg = np.sum(weights*((inimg[indices].squeeze(axis=1)).astype(np.float64)), axis=1)
168
+ elif dim == 1:
169
+ weights = weights.reshape((1, wshape[0], wshape[2], 1))
170
+ outimg = np.sum(weights*((inimg[:, indices].squeeze(axis=2)).astype(np.float64)), axis=2)
171
+ if inimg.dtype == np.uint8:
172
+ outimg = np.clip(outimg, 0, 255)
173
+ return np.around(outimg).astype(np.uint8)
174
+ else:
175
+ return outimg
176
+
177
+
178
+ def resizeAlongDim(A, dim, weights, indices, mode="vec"):
179
+ if mode == "org":
180
+ out = imresizemex(A, weights, indices, dim)
181
+ else:
182
+ out = imresizevec(A, weights, indices, dim)
183
+ return out
184
+
185
+
186
+ def imresize(I, scalar_scale=None, method='bicubic', output_shape=None, mode="vec"):
187
+ if method == 'bicubic':
188
+ kernel = cubic
189
+ elif method == 'bilinear':
190
+ kernel = triangle
191
+ else:
192
+ print ('Error: Unidentified method supplied')
193
+
194
+ kernel_width = 4.0
195
+ # Fill scale and output_size
196
+ if scalar_scale is not None:
197
+ scalar_scale = float(scalar_scale)
198
+ scale = [scalar_scale, scalar_scale]
199
+ output_size = deriveSizeFromScale(I.shape, scale)
200
+ elif output_shape is not None:
201
+ scale = deriveScaleFromSize(I.shape, output_shape)
202
+ output_size = list(output_shape)
203
+ else:
204
+ print ('Error: scalar_scale OR output_shape should be defined!')
205
+ return
206
+ scale_np = np.array(scale)
207
+ order = np.argsort(scale_np)
208
+ weights = []
209
+ indices = []
210
+ for k in range(2):
211
+ w, ind = contributions(I.shape[k], output_size[k], scale[k], kernel, kernel_width)
212
+ weights.append(w)
213
+ indices.append(ind)
214
+ B = np.copy(I)
215
+ flag2D = False
216
+ if B.ndim == 2:
217
+ B = np.expand_dims(B, axis=2)
218
+ flag2D = True
219
+ for k in range(2):
220
+ dim = order[k]
221
+ B = resizeAlongDim(B, dim, weights[dim], indices[dim], mode)
222
+ if flag2D:
223
+ B = np.squeeze(B, axis=2)
224
+ return B