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