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
|