Artyom
commited on
Commit
•
1b5ee0e
1
Parent(s):
00c3521
ozuvgl
Browse files- OzUVGL/Dockerfile +19 -0
- OzUVGL/README.md +25 -0
- OzUVGL/ips/__init__.py +27 -0
- OzUVGL/ips/ops.py +418 -0
- OzUVGL/ips/wb.py +40 -0
- OzUVGL/main.py +93 -0
- OzUVGL/requirements.txt +10 -0
- OzUVGL/run.sh +3 -0
- OzUVGL/utils/color.py +306 -0
- OzUVGL/utils/io.py +57 -0
- OzUVGL/utils/misc.py +224 -0
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
|