Artyom
commited on
Commit
•
00c3521
1
Parent(s):
f8d6c27
polyucolor
Browse files- .gitattributes +1 -0
- PolyuColor/.gitignore +6 -0
- PolyuColor/Dockerfile +40 -0
- PolyuColor/LICENSE +21 -0
- PolyuColor/README.md +78 -0
- PolyuColor/raw_prc_pipeline/__init__.py +3 -0
- PolyuColor/raw_prc_pipeline/contrast_enhancement.py +159 -0
- PolyuColor/raw_prc_pipeline/exif_data_formats.py +22 -0
- PolyuColor/raw_prc_pipeline/exif_utils.py +208 -0
- PolyuColor/raw_prc_pipeline/fs.py +43 -0
- PolyuColor/raw_prc_pipeline/grey_pixels.py +102 -0
- PolyuColor/raw_prc_pipeline/lsc.py +21 -0
- PolyuColor/raw_prc_pipeline/model.py +106 -0
- PolyuColor/raw_prc_pipeline/pipeline.py +331 -0
- PolyuColor/raw_prc_pipeline/pipeline_utils.py +575 -0
- PolyuColor/raw_prc_pipeline/sharpening.py +14 -0
- PolyuColor/raw_prc_pipeline/tone_curve.mat +0 -0
- PolyuColor/raw_prc_pipeline/tone_mapping.py +150 -0
- PolyuColor/requirements.txt +8 -0
- PolyuColor/resources/average_shading.png +3 -0
- PolyuColor/resources/sid_fp32_best.ckpt +3 -0
- PolyuColor/run.py +142 -0
- PolyuColor/run.sh +2 -0
- PolyuColor/utils/__init__.py +36 -0
- PolyuColor/utils/image_utils.py +69 -0
- PolyuColor/utils/utils.py +56 -0
.gitattributes
CHANGED
@@ -38,3 +38,4 @@ SCBC/Input/IMG_20240215_213619.png filter=lfs diff=lfs merge=lfs -text
|
|
38 |
SCBC/Input/IMG_20240215_214449.png filter=lfs diff=lfs merge=lfs -text
|
39 |
SCBC/Output/IMG_20240215_213330.png filter=lfs diff=lfs merge=lfs -text
|
40 |
SCBC/Output/IMG_20240215_214449.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
38 |
SCBC/Input/IMG_20240215_214449.png filter=lfs diff=lfs merge=lfs -text
|
39 |
SCBC/Output/IMG_20240215_213330.png filter=lfs diff=lfs merge=lfs -text
|
40 |
SCBC/Output/IMG_20240215_214449.png filter=lfs diff=lfs merge=lfs -text
|
41 |
+
PolyuColor/resources/average_shading.png filter=lfs diff=lfs merge=lfs -text
|
PolyuColor/.gitignore
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
*.png
|
2 |
+
*.jpg
|
3 |
+
*.json
|
4 |
+
__pycache__
|
5 |
+
*.cube
|
6 |
+
*.ckpt
|
PolyuColor/Dockerfile
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
FROM nvidia/cuda:11.3.1-cudnn8-runtime-ubuntu20.04
|
2 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
3 |
+
|
4 |
+
RUN apt-get update && apt-get install -y \
|
5 |
+
libpng-dev libjpeg-dev \
|
6 |
+
libopencv-dev ffmpeg \
|
7 |
+
libgl1-mesa-glx && \
|
8 |
+
apt clean && \
|
9 |
+
rm -rf /var/lib/apt/lists/*
|
10 |
+
|
11 |
+
RUN apt update && \
|
12 |
+
apt install -y \
|
13 |
+
wget build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev \
|
14 |
+
libreadline-dev libffi-dev libsqlite3-dev libbz2-dev liblzma-dev && \
|
15 |
+
apt clean && \
|
16 |
+
rm -rf /var/lib/apt/lists/*
|
17 |
+
|
18 |
+
WORKDIR /temp
|
19 |
+
|
20 |
+
RUN wget https://www.python.org/ftp/python/3.9.10/Python-3.9.10.tgz && \
|
21 |
+
tar -xvf Python-3.9.10.tgz
|
22 |
+
|
23 |
+
RUN cd Python-3.9.10 && \
|
24 |
+
./configure --enable-optimizations && \
|
25 |
+
make && \
|
26 |
+
make install
|
27 |
+
|
28 |
+
WORKDIR /workspace
|
29 |
+
|
30 |
+
RUN rm -r /temp && \
|
31 |
+
ln -s /usr/local/bin/python3 /usr/local/bin/python && \
|
32 |
+
ln -s /usr/local/bin/pip3 /usr/local/bin/pip
|
33 |
+
|
34 |
+
COPY requirements.txt .
|
35 |
+
RUN python -m pip install --no-cache -r requirements.txt
|
36 |
+
RUN pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 --no-cache
|
37 |
+
|
38 |
+
WORKDIR ..
|
39 |
+
COPY . .
|
40 |
+
CMD ["./run.sh"]
|
PolyuColor/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2023 Color Reproduction and Synthesis
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
PolyuColor/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Team: **play1play**
|
2 |
+
|
3 |
+
for 'Night Photography Rendering Challenge'
|
4 |
+
|
5 |
+
This repo contains the source code of [Night Photography Rendering Challenge 2024](https://nightimaging.org/).
|
6 |
+
|
7 |
+
## How to run
|
8 |
+
### Run without Docker
|
9 |
+
- Install python >= 3.9
|
10 |
+
- Install the required packages: `pip install -r requirements.txt`
|
11 |
+
- Put the test images in the `data` folder or specify the input image path with `-p` option.
|
12 |
+
- Run the script:
|
13 |
+
```
|
14 |
+
python run.py -p <input_image_path> -o <output_image_path>
|
15 |
+
```
|
16 |
+
- The output images will be saved in the `output` folder or specifiy the output path with `-o` option.
|
17 |
+
|
18 |
+
### Run with Docker
|
19 |
+
- Build the docker image from beginning (optional):
|
20 |
+
```
|
21 |
+
docker build -t play1play .
|
22 |
+
```
|
23 |
+
- Run the docker container with gpu on linux:
|
24 |
+
```
|
25 |
+
docker run -it --rm --gpus=all -v $(pwd)/data:/data play1play ./run.sh
|
26 |
+
```
|
27 |
+
`Do not forget to to --gpus flag, our model requires GPU to run.`
|
28 |
+
|
29 |
+
## Update
|
30 |
+
*2024.3.21:*
|
31 |
+
|
32 |
+
Final version v3.0 is released for 3rd validation!
|
33 |
+
|
34 |
+
Key features:
|
35 |
+
- Utilize the patch-based and calibration-based white-balance algorithm to improve the image quality.
|
36 |
+
- Modify the resizing strategy to improve the image quality.
|
37 |
+
|
38 |
+
*2024.3.16:*
|
39 |
+
|
40 |
+
Release v2.0 for 3rd validation!
|
41 |
+
|
42 |
+
Key features:
|
43 |
+
- Increase the overall saturation and brightness of the image.
|
44 |
+
- Add more contrast to the image.
|
45 |
+
- Add more dynamic range to the image.
|
46 |
+
|
47 |
+
Algorithm changes:
|
48 |
+
- Add luma shading correction (LSC) module
|
49 |
+
- Add auto-contrast module (dynamic gamma)
|
50 |
+
- LSC, LTM, auto-contrast module can dynamiclly adjust the parameters based on the camera gain from the metadata.
|
51 |
+
- Add another white-balance process at the end of the pipeline to further improve the image quality.
|
52 |
+
|
53 |
+
Key algorithm parameter explanation:
|
54 |
+
- k_th: defines the threshold for the noise level, higher means tolerant to noise, lower means more sensitive to noise. For this sensor, default is 2.5e-3. `Note that this paramerts are shared by all the modules, and hence defined as the member varibale in RawProcessingPipelineDemo class.`
|
55 |
+
- s of local_tone_mapping: defines how to apply the gain map to different image channels. Higher means more saturated, lower means less saturated. Default is 0.7. `Currently, s is automatically adjusted based on the camera gain from the metadata.`
|
56 |
+
|
57 |
+
*2024.3.7:*
|
58 |
+
|
59 |
+
release v1.0 for 3rd validation!
|
60 |
+
|
61 |
+
----
|
62 |
+
|
63 |
+
## Version-1
|
64 |
+
|
65 |
+
- TMO-ratio50
|
66 |
+
|
67 |
+
-----
|
68 |
+
|
69 |
+
## Version-2
|
70 |
+
|
71 |
+
- TMO: ratio50
|
72 |
+
|
73 |
+
- Gamma: 1.5
|
74 |
+
|
75 |
+
- Contrast:[low=2,high=0.2]
|
76 |
+
|
77 |
+
- Post-AWB: GI
|
78 |
+
|
PolyuColor/raw_prc_pipeline/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
expected_img_ext = '.jpg'
|
2 |
+
expected_landscape_img_height = 768
|
3 |
+
expected_landscape_img_width = 1024
|
PolyuColor/raw_prc_pipeline/contrast_enhancement.py
ADDED
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from utils import *
|
3 |
+
|
4 |
+
|
5 |
+
def _global_mean_contrast(img: np.ndarray,
|
6 |
+
beta: float = 1.2,
|
7 |
+
copy=True,
|
8 |
+
channel_wise=False,
|
9 |
+
protect_ratio=0.95) -> np.ndarray:
|
10 |
+
"""
|
11 |
+
Global mean contrast enhancement.
|
12 |
+
|
13 |
+
:param img: input image
|
14 |
+
:param beta: contrast enhancement factor
|
15 |
+
:return: enhanced image
|
16 |
+
"""
|
17 |
+
if copy:
|
18 |
+
img = img.copy()
|
19 |
+
if channel_wise:
|
20 |
+
remain_ratio = (1 - protect_ratio) / 2
|
21 |
+
maxi_value = 1 - remain_ratio
|
22 |
+
mini_value = remain_ratio
|
23 |
+
for i in range(img.shape[-1]):
|
24 |
+
channel = img[:, :, i]
|
25 |
+
mean = np.mean(channel)
|
26 |
+
if protect_ratio > 0:
|
27 |
+
beta = min(beta, (1 - mean) / (maxi_value - mean))
|
28 |
+
beta = min(beta, mean / (mean - mini_value))
|
29 |
+
beta = max(1, beta)
|
30 |
+
gap = channel - mean
|
31 |
+
channel = gap * beta + mean
|
32 |
+
img[:, :, i] = channel
|
33 |
+
else:
|
34 |
+
y = compute_y(img)
|
35 |
+
mean = np.mean(y)
|
36 |
+
beta = min(beta, (1 - mean) / (1 - protect_ratio - mean))
|
37 |
+
new_y = (y - mean) * beta + mean
|
38 |
+
y[y == 0] = 1
|
39 |
+
gain_map = new_y / y
|
40 |
+
img = img * gain_map[:, :, np.newaxis]
|
41 |
+
|
42 |
+
img = np.clip(img, 0, 1)
|
43 |
+
return img
|
44 |
+
|
45 |
+
|
46 |
+
def _s_curve_correction(img: np.ndarray,
|
47 |
+
alpha: float = 0.15,
|
48 |
+
gamma: float = 1 / 1.3,
|
49 |
+
copy=True,
|
50 |
+
channel_wise=False) -> np.ndarray:
|
51 |
+
"""
|
52 |
+
S-curve correction.
|
53 |
+
|
54 |
+
:param img: input image
|
55 |
+
:param alpha: contrast enhancement factor
|
56 |
+
:param gamma: gamma correction factor
|
57 |
+
:return: enhanced image
|
58 |
+
"""
|
59 |
+
if copy:
|
60 |
+
img = img.copy()
|
61 |
+
if channel_wise:
|
62 |
+
for i in range(img.shape[-1]):
|
63 |
+
channel = img[:, :, i]
|
64 |
+
mask = channel > alpha
|
65 |
+
channel[mask] = alpha + (1 - alpha) * (((channel[mask] - alpha) /
|
66 |
+
(1 - alpha))**gamma)
|
67 |
+
channel[~mask] = alpha - alpha * (
|
68 |
+
(1 - channel[~mask] / alpha)**gamma)
|
69 |
+
img[:, :, i] = channel
|
70 |
+
else:
|
71 |
+
y = compute_y(img)
|
72 |
+
new_y = y.copy()
|
73 |
+
mask = new_y > alpha
|
74 |
+
new_y[mask] = alpha + (1 - alpha) * ((new_y[mask] - alpha) /
|
75 |
+
(1 - alpha))**gamma
|
76 |
+
new_y[~mask] = alpha - alpha * ((1 - new_y[~mask] / alpha)**gamma)
|
77 |
+
y[y == 0] = 1
|
78 |
+
gain_map = new_y / y
|
79 |
+
img = img * gain_map[:, :, np.newaxis]
|
80 |
+
img = np.clip(img, 0, 1)
|
81 |
+
return img
|
82 |
+
|
83 |
+
|
84 |
+
def _hist_stretching(img: np.ndarray,
|
85 |
+
copy=True,
|
86 |
+
channel_wise=False) -> np.ndarray:
|
87 |
+
"""
|
88 |
+
Histogram stretching.
|
89 |
+
|
90 |
+
:param img: input image
|
91 |
+
:return: enhanced image
|
92 |
+
"""
|
93 |
+
if copy:
|
94 |
+
img = img.copy()
|
95 |
+
if channel_wise:
|
96 |
+
for i in range(img.shape[-1]):
|
97 |
+
channel = img[:, :, i]
|
98 |
+
channel = (channel - channel.min()) / (channel.max() -
|
99 |
+
channel.min())
|
100 |
+
img[:, :, i] = channel
|
101 |
+
else:
|
102 |
+
y = compute_y(img)
|
103 |
+
y_new = (y - y.min()) / (y.max() - y.min())
|
104 |
+
y[y == 0] = 1
|
105 |
+
gain_map = y_new / y
|
106 |
+
img = img * gain_map[:, :, np.newaxis]
|
107 |
+
img = np.clip(img, 0, 1)
|
108 |
+
|
109 |
+
return img
|
110 |
+
|
111 |
+
|
112 |
+
def _conditioanl_contrast_correction(img: np.ndarray, k: float,
|
113 |
+
k_th: float) -> np.ndarray:
|
114 |
+
y = compute_y(img)
|
115 |
+
"""
|
116 |
+
Conditional contrast correction based on the cameara gain value k
|
117 |
+
|
118 |
+
Parameters:
|
119 |
+
k: camera gain value
|
120 |
+
k_th: basic threshold of camera gain value
|
121 |
+
|
122 |
+
Returns:
|
123 |
+
enhanced image
|
124 |
+
"""
|
125 |
+
mean_y = y.mean()
|
126 |
+
first_k = k_th / 2
|
127 |
+
second_k = k_th
|
128 |
+
third_k = k_th * 2
|
129 |
+
forth_k = k_th * 3
|
130 |
+
if k <= first_k:
|
131 |
+
target_illum = 0.35
|
132 |
+
gamma = np.log(target_illum) / np.log(mean_y)
|
133 |
+
gamma = np.clip(gamma, 1 / 1.6, None)
|
134 |
+
elif first_k < k_th <= second_k:
|
135 |
+
target_illum = 0.32
|
136 |
+
gamma = np.log(target_illum) / np.log(mean_y)
|
137 |
+
gamma = np.clip(gamma, 1 / 1.5, None)
|
138 |
+
elif second_k < k_th <= third_k:
|
139 |
+
target_illum = 0.27
|
140 |
+
gamma = np.log(target_illum) / np.log(mean_y)
|
141 |
+
gamma = np.clip(gamma, 1 / 1.4, None)
|
142 |
+
elif third_k < k_th <= forth_k:
|
143 |
+
target_illum = 0.23
|
144 |
+
gamma = np.log(target_illum) / np.log(mean_y)
|
145 |
+
gamma = np.clip(gamma, 1 / 1.2, None)
|
146 |
+
elif k > forth_k:
|
147 |
+
target_illum = 0.15
|
148 |
+
gamma = np.log(target_illum) / np.log(mean_y)
|
149 |
+
gamma = np.clip(gamma, 1 / 1.1, None)
|
150 |
+
else:
|
151 |
+
gamma = 1.
|
152 |
+
img = img**gamma
|
153 |
+
img = np.clip(img, 0, 1)
|
154 |
+
return img
|
155 |
+
|
156 |
+
|
157 |
+
def contrast_enhancement(img: np.ndarray, k: float, k_th: float) -> np.ndarray:
|
158 |
+
img = _conditioanl_contrast_correction(img, k, k_th)
|
159 |
+
return img
|
PolyuColor/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 |
+
}
|
PolyuColor/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)
|
PolyuColor/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
|
PolyuColor/raw_prc_pipeline/grey_pixels.py
ADDED
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
unofficial implementation of paper
|
3 |
+
"Efficient Illuminant Estimation for Color Constancy Using Grey Pixels"
|
4 |
+
"""
|
5 |
+
|
6 |
+
import cv2
|
7 |
+
import numpy as np
|
8 |
+
|
9 |
+
_params = {
|
10 |
+
'patch_size': 3,
|
11 |
+
'blur_kernel': 7,
|
12 |
+
"top_n": 0.1,
|
13 |
+
'eps': 1e-6,
|
14 |
+
'threshold': 1e-1
|
15 |
+
}
|
16 |
+
|
17 |
+
|
18 |
+
def _compute_local_std(log_img: np.ndarray) -> np.ndarray:
|
19 |
+
mean = cv2.blur(log_img, (_params['patch_size'], _params['patch_size']),
|
20 |
+
borderType=cv2.BORDER_REPLICATE)
|
21 |
+
sq_mean = cv2.blur(log_img**2,
|
22 |
+
(_params['patch_size'], _params['patch_size']),
|
23 |
+
borderType=cv2.BORDER_REPLICATE)
|
24 |
+
tmp = sq_mean - mean**2
|
25 |
+
tmp[tmp < 0] = 0
|
26 |
+
std_dev = np.sqrt(tmp)
|
27 |
+
return std_dev
|
28 |
+
|
29 |
+
|
30 |
+
def _compute_local_derive_gaussian(img: np.ndarray,
|
31 |
+
kernel_half_size=2,
|
32 |
+
sigma: float = .5):
|
33 |
+
x, y = np.meshgrid(np.arange(-kernel_half_size, kernel_half_size + 1),
|
34 |
+
np.arange(-kernel_half_size, kernel_half_size + 1))
|
35 |
+
ssq = sigma**2
|
36 |
+
kernel = -x * np.exp(-(x**2 + y**2) / (2 * ssq)) / (np.pi * ssq)
|
37 |
+
|
38 |
+
ans = cv2.filter2D(img, -1, kernel, borderType=cv2.BORDER_REPLICATE)
|
39 |
+
ans = np.abs(ans)
|
40 |
+
return ans
|
41 |
+
|
42 |
+
|
43 |
+
def _compute_pixel_std(img: np.ndarray) -> np.ndarray:
|
44 |
+
mean = np.mean(img, axis=-1)
|
45 |
+
sq_mean = np.mean(img**2, axis=-1)
|
46 |
+
tmp = sq_mean - mean**2
|
47 |
+
tmp[tmp < 0] = 0
|
48 |
+
std_dev = np.sqrt(tmp)
|
49 |
+
return std_dev
|
50 |
+
|
51 |
+
|
52 |
+
def _compute_grey_index_map(img: np.ndarray, method='std') -> np.ndarray:
|
53 |
+
mask = np.any(img < 2e-2, axis=-1) | np.any(img > 1 - _params['threshold'],
|
54 |
+
axis=-1)
|
55 |
+
img = img * 65535 + 1
|
56 |
+
log_img = np.log(img) + _params['eps']
|
57 |
+
if method == 'std':
|
58 |
+
iim_map = _compute_local_std(log_img)
|
59 |
+
else:
|
60 |
+
iim_map = _compute_local_derive_gaussian(log_img)
|
61 |
+
mask |= np.all(iim_map < _params['eps'], axis=-1)
|
62 |
+
|
63 |
+
Ds = _compute_pixel_std(iim_map)
|
64 |
+
Ds /= (iim_map.mean(axis=-1) + _params['eps'])
|
65 |
+
|
66 |
+
l_value = img.mean(axis=-1)
|
67 |
+
|
68 |
+
Ps = Ds / l_value
|
69 |
+
|
70 |
+
Ps /= (Ps.max() + _params['eps'])
|
71 |
+
|
72 |
+
Ps[mask] = Ps.max()
|
73 |
+
|
74 |
+
grey_index_map = cv2.blur(Ps,
|
75 |
+
(_params['blur_kernel'], _params['blur_kernel']))
|
76 |
+
|
77 |
+
return grey_index_map, mask
|
78 |
+
|
79 |
+
|
80 |
+
def grey_pixels(img: np.ndarray) -> np.ndarray:
|
81 |
+
h, w, c = img.shape
|
82 |
+
img = img.reshape(-1, c)
|
83 |
+
pixel_num = int(h * w * _params['top_n'] / 100)
|
84 |
+
|
85 |
+
grey_index_map, mask = _compute_grey_index_map(img, method='std')
|
86 |
+
valid_num = np.sum(~mask)
|
87 |
+
if valid_num < pixel_num:
|
88 |
+
return np.array([1., 1., 1.])
|
89 |
+
grey_index_map = np.ravel(grey_index_map)
|
90 |
+
indexes = np.argsort(grey_index_map)[:pixel_num]
|
91 |
+
|
92 |
+
candidates: np.ndarray = img[indexes]
|
93 |
+
r_avg, g_avg, b_avg = candidates.mean(axis=0)
|
94 |
+
|
95 |
+
r_avg /= (g_avg + _params['eps'])
|
96 |
+
b_avg /= (g_avg + _params['eps'])
|
97 |
+
if r_avg < 0.2 or r_avg > 5 or b_avg < 0.2 or b_avg > 5:
|
98 |
+
r_avg = 1.
|
99 |
+
b_avg = 1.
|
100 |
+
|
101 |
+
res = np.array([r_avg, 1., b_avg])
|
102 |
+
return res
|
PolyuColor/raw_prc_pipeline/lsc.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
|
3 |
+
from utils import *
|
4 |
+
|
5 |
+
|
6 |
+
def simple_lsc(raw: np.ndarray, shading: np.ndarray) -> np.ndarray:
|
7 |
+
"""
|
8 |
+
Simple LSC algorithm.
|
9 |
+
|
10 |
+
:param raw: raw image
|
11 |
+
:param shading: shading image
|
12 |
+
:param dark_th: threshold for detecting dark image
|
13 |
+
:return: LSC-corrected image
|
14 |
+
"""
|
15 |
+
rggb_calibrated = pack_raw(shading)
|
16 |
+
rggb_raw = pack_raw(raw)
|
17 |
+
rggb_raw /= rggb_calibrated
|
18 |
+
ret = depack_raw(rggb_raw)
|
19 |
+
ret = np.clip(ret, 0, 1)
|
20 |
+
|
21 |
+
return ret
|
PolyuColor/raw_prc_pipeline/model.py
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import torch
|
2 |
+
import torch.nn as nn
|
3 |
+
|
4 |
+
|
5 |
+
class UNetSeeInDark(nn.Module):
|
6 |
+
def __init__(self, in_channels=4, out_channels=4):
|
7 |
+
super(UNetSeeInDark, self).__init__()
|
8 |
+
# device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
|
9 |
+
self.conv1_1 = nn.Conv2d(in_channels,
|
10 |
+
32,
|
11 |
+
kernel_size=3,
|
12 |
+
stride=1,
|
13 |
+
padding=1)
|
14 |
+
self.conv1_2 = nn.Conv2d(32, 32, kernel_size=3, stride=1, padding=1)
|
15 |
+
self.pool1 = nn.MaxPool2d(kernel_size=2)
|
16 |
+
|
17 |
+
self.conv2_1 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
|
18 |
+
self.conv2_2 = nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1)
|
19 |
+
self.pool2 = nn.MaxPool2d(kernel_size=2)
|
20 |
+
|
21 |
+
self.conv3_1 = nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1)
|
22 |
+
self.conv3_2 = nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1)
|
23 |
+
self.pool3 = nn.MaxPool2d(kernel_size=2)
|
24 |
+
|
25 |
+
self.conv4_1 = nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1)
|
26 |
+
self.conv4_2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
|
27 |
+
self.pool4 = nn.MaxPool2d(kernel_size=2)
|
28 |
+
|
29 |
+
self.conv5_1 = nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1)
|
30 |
+
self.conv5_2 = nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1)
|
31 |
+
|
32 |
+
self.upv6 = nn.ConvTranspose2d(512, 256, 2, stride=2)
|
33 |
+
self.conv6_1 = nn.Conv2d(512, 256, kernel_size=3, stride=1, padding=1)
|
34 |
+
self.conv6_2 = nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1)
|
35 |
+
|
36 |
+
self.upv7 = nn.ConvTranspose2d(256, 128, 2, stride=2)
|
37 |
+
self.conv7_1 = nn.Conv2d(256, 128, kernel_size=3, stride=1, padding=1)
|
38 |
+
self.conv7_2 = nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1)
|
39 |
+
|
40 |
+
self.upv8 = nn.ConvTranspose2d(128, 64, 2, stride=2)
|
41 |
+
self.conv8_1 = nn.Conv2d(128, 64, kernel_size=3, stride=1, padding=1)
|
42 |
+
self.conv8_2 = nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1)
|
43 |
+
|
44 |
+
self.upv9 = nn.ConvTranspose2d(64, 32, 2, stride=2)
|
45 |
+
self.conv9_1 = nn.Conv2d(64, 32, kernel_size=3, stride=1, padding=1)
|
46 |
+
self.conv9_2 = nn.Conv2d(32, 32, kernel_size=3, stride=1, padding=1)
|
47 |
+
|
48 |
+
self.conv10_1 = nn.Conv2d(32, out_channels, kernel_size=1, stride=1)
|
49 |
+
|
50 |
+
def forward(self, x):
|
51 |
+
conv1 = self.lrelu(self.conv1_1(x))
|
52 |
+
conv1 = self.lrelu(self.conv1_2(conv1))
|
53 |
+
pool1 = self.pool1(conv1)
|
54 |
+
|
55 |
+
conv2 = self.lrelu(self.conv2_1(pool1))
|
56 |
+
conv2 = self.lrelu(self.conv2_2(conv2))
|
57 |
+
pool2 = self.pool1(conv2)
|
58 |
+
|
59 |
+
conv3 = self.lrelu(self.conv3_1(pool2))
|
60 |
+
conv3 = self.lrelu(self.conv3_2(conv3))
|
61 |
+
pool3 = self.pool1(conv3)
|
62 |
+
|
63 |
+
conv4 = self.lrelu(self.conv4_1(pool3))
|
64 |
+
conv4 = self.lrelu(self.conv4_2(conv4))
|
65 |
+
pool4 = self.pool1(conv4)
|
66 |
+
|
67 |
+
conv5 = self.lrelu(self.conv5_1(pool4))
|
68 |
+
conv5 = self.lrelu(self.conv5_2(conv5))
|
69 |
+
|
70 |
+
up6 = self.upv6(conv5)
|
71 |
+
up6 = torch.cat([up6, conv4], 1)
|
72 |
+
conv6 = self.lrelu(self.conv6_1(up6))
|
73 |
+
conv6 = self.lrelu(self.conv6_2(conv6))
|
74 |
+
|
75 |
+
up7 = self.upv7(conv6)
|
76 |
+
up7 = torch.cat([up7, conv3], 1)
|
77 |
+
conv7 = self.lrelu(self.conv7_1(up7))
|
78 |
+
conv7 = self.lrelu(self.conv7_2(conv7))
|
79 |
+
|
80 |
+
up8 = self.upv8(conv7)
|
81 |
+
up8 = torch.cat([up8, conv2], 1)
|
82 |
+
conv8 = self.lrelu(self.conv8_1(up8))
|
83 |
+
conv8 = self.lrelu(self.conv8_2(conv8))
|
84 |
+
|
85 |
+
up9 = self.upv9(conv8)
|
86 |
+
up9 = torch.cat([up9, conv1], 1)
|
87 |
+
conv9 = self.lrelu(self.conv9_1(up9))
|
88 |
+
conv9 = self.lrelu(self.conv9_2(conv9))
|
89 |
+
|
90 |
+
conv10 = self.conv10_1(conv9)
|
91 |
+
# out = nn.functional.pixel_shuffle(conv10, 2)
|
92 |
+
out = conv10
|
93 |
+
return out
|
94 |
+
|
95 |
+
def _initialize_weights(self):
|
96 |
+
for m in self.modules():
|
97 |
+
if isinstance(m, nn.Conv2d):
|
98 |
+
m.weight.data.normal_(0.0, 0.02)
|
99 |
+
if m.bias is not None:
|
100 |
+
m.bias.data.normal_(0.0, 0.02)
|
101 |
+
if isinstance(m, nn.ConvTranspose2d):
|
102 |
+
m.weight.data.normal_(0.0, 0.02)
|
103 |
+
|
104 |
+
def lrelu(self, x):
|
105 |
+
outt = torch.max(0.2 * x, x)
|
106 |
+
return outt
|
PolyuColor/raw_prc_pipeline/pipeline.py
ADDED
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Demo raw processing pipeline and pipeline executor.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import time
|
6 |
+
|
7 |
+
import cv2
|
8 |
+
import numpy as np
|
9 |
+
import torch
|
10 |
+
import torch.nn as nn
|
11 |
+
|
12 |
+
from raw_prc_pipeline.pipeline_utils import *
|
13 |
+
from utils import *
|
14 |
+
|
15 |
+
from .contrast_enhancement import contrast_enhancement
|
16 |
+
from .grey_pixels import *
|
17 |
+
from .lsc import *
|
18 |
+
from .model import *
|
19 |
+
from .sharpening import *
|
20 |
+
from .tone_mapping import *
|
21 |
+
|
22 |
+
|
23 |
+
class ModelEncaupsulation(nn.Module):
|
24 |
+
def __init__(self, model):
|
25 |
+
super(ModelEncaupsulation, self).__init__()
|
26 |
+
self.model = model()
|
27 |
+
|
28 |
+
def forward(self, x):
|
29 |
+
return self.model(x)
|
30 |
+
|
31 |
+
|
32 |
+
model = ModelEncaupsulation(UNetSeeInDark)
|
33 |
+
parameters = torch.load('resources/sid_fp32_best.ckpt')
|
34 |
+
model.float()
|
35 |
+
model.load_state_dict(parameters['state_dict'])
|
36 |
+
model.eval()
|
37 |
+
model = model.cuda()
|
38 |
+
shading_grid = cv2.imread('resources/average_shading.png',
|
39 |
+
cv2.IMREAD_UNCHANGED).astype(np.float32) / 65535.0
|
40 |
+
|
41 |
+
|
42 |
+
class RawProcessingPipelineDemo:
|
43 |
+
"""
|
44 |
+
Demonstration pipeline of raw image processing.
|
45 |
+
|
46 |
+
This pipeline is a baseline pipeline to process raw image.
|
47 |
+
The public methods of this class are successive steps of raw image processing pipeline.
|
48 |
+
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.
|
49 |
+
|
50 |
+
It is assumed that each public method has 2 parameters:
|
51 |
+
raw_img : ndarray
|
52 |
+
Array with images data.
|
53 |
+
img_meta : Dict
|
54 |
+
Some metadata of image.
|
55 |
+
|
56 |
+
Also each such public method must return an image (ndarray) as the result of processing.
|
57 |
+
"""
|
58 |
+
def __init__(self,
|
59 |
+
illumination_estimation='',
|
60 |
+
denoise_flg=True,
|
61 |
+
tone_mapping='Flash',
|
62 |
+
out_landscape_width=None,
|
63 |
+
out_landscape_height=None,
|
64 |
+
color_matrix=[
|
65 |
+
1.06835938, -0.29882812, -0.14257812, -0.43164062,
|
66 |
+
1.35546875, 0.05078125, -0.1015625, 0.24414062, 0.5859375
|
67 |
+
]):
|
68 |
+
"""
|
69 |
+
RawProcessingPipelineDemo __init__ method.
|
70 |
+
|
71 |
+
Parameters
|
72 |
+
----------
|
73 |
+
illumination_estimation : str, optional
|
74 |
+
Options for illumination estimation algorithms: '', 'gw', 'wp', 'sog', 'iwp', by default ''.
|
75 |
+
denoise_flg : bool, optional
|
76 |
+
Denoising flag, by default True.
|
77 |
+
If True, resulted images will be denoised with some predefined parameters.
|
78 |
+
tone_mapping : str, optional
|
79 |
+
Options for tone mapping methods, defined in function `apply_tone_map` from `pipeline_utils` module.
|
80 |
+
By default 'Flash'.
|
81 |
+
out_landscape_width : int, optional
|
82 |
+
The width of output image (when orientation is landscape). If None, the image resize will not be performed.
|
83 |
+
By default None.
|
84 |
+
out_landscape_height : int, optional
|
85 |
+
The height of output image (when orientation is landscape). If None, the image resize will not be performed.
|
86 |
+
By default None.
|
87 |
+
color_matrix : list, optional
|
88 |
+
Avg color tranformation matrix. If None, average color transformation matrix of Huawei Mate 40 Pro is used.
|
89 |
+
"""
|
90 |
+
|
91 |
+
self.params = locals()
|
92 |
+
del self.params['self']
|
93 |
+
self.current_step = 0
|
94 |
+
self.shading_grid = None
|
95 |
+
self.lut3d = None
|
96 |
+
self.k_th = 2.5e-3
|
97 |
+
self.k_max = 0.02750327847
|
98 |
+
self.k_min = 2.32350645e-06
|
99 |
+
|
100 |
+
# Linearization not handled.
|
101 |
+
def linearize_raw(self, raw_img, img_meta):
|
102 |
+
self.current_step += 1
|
103 |
+
|
104 |
+
return raw_img
|
105 |
+
|
106 |
+
def normalize(self, linearized_raw, img_meta):
|
107 |
+
self.start_time = time.perf_counter()
|
108 |
+
ret = normalize(linearized_raw, img_meta['black_level'],
|
109 |
+
img_meta['white_level'])
|
110 |
+
return ret
|
111 |
+
|
112 |
+
def luma_shading_correction(self, normalized, img_meta):
|
113 |
+
if img_meta['noise_profile'][0] >= self.k_th:
|
114 |
+
lsc_image = normalized
|
115 |
+
else:
|
116 |
+
lsc_image = simple_lsc(normalized, shading_grid)
|
117 |
+
return lsc_image
|
118 |
+
|
119 |
+
def pack_raw_image(self, normalized, img_meta):
|
120 |
+
ret = pack_raw(torch.from_numpy(normalized)).numpy()
|
121 |
+
return ret
|
122 |
+
|
123 |
+
def denoise(self, packed_raw, img_meta):
|
124 |
+
packed_raw = torch.from_numpy(packed_raw)
|
125 |
+
packed_raw = packed_raw.permute(2, 0, 1).unsqueeze(0).cuda()
|
126 |
+
packed_raw = resize_rggb(packed_raw,
|
127 |
+
target_height=1536,
|
128 |
+
target_width=2048)
|
129 |
+
with torch.no_grad():
|
130 |
+
denoised_packed_raw = model(packed_raw.float()).float()
|
131 |
+
denoised_packed_raw = denoised_packed_raw.squeeze(0).permute(
|
132 |
+
1, 2, 0).cpu()
|
133 |
+
denoised_raw = depack_raw(denoised_packed_raw)
|
134 |
+
denoised_raw = np.clip(denoised_raw.numpy(), 0, 1)
|
135 |
+
return denoised_raw
|
136 |
+
|
137 |
+
def demosaic(self, denoised, img_meta):
|
138 |
+
ret = simple_demosaic(denoised, img_meta['cfa_pattern'])
|
139 |
+
ret = np.clip(ret, 0, 1)
|
140 |
+
return ret
|
141 |
+
|
142 |
+
def auto_white_balance(self, demosaic, img_meta):
|
143 |
+
wb_params = patch_based_white_balance(demosaic)
|
144 |
+
white_balanced = white_balance(demosaic, wb_params)
|
145 |
+
|
146 |
+
return white_balanced
|
147 |
+
|
148 |
+
def xyz_transform(self, white_balanced, img_meta):
|
149 |
+
# in case of absence of color matrix we use mean color matrix
|
150 |
+
if "color_matrix_1" not in img_meta.keys():
|
151 |
+
img_meta["color_matrix_1"] = self.params["color_matrix"]
|
152 |
+
img_meta["color_matrix_2"] = self.params["color_matrix"]
|
153 |
+
ret = apply_color_space_transform(white_balanced,
|
154 |
+
img_meta['color_matrix_1'],
|
155 |
+
img_meta['color_matrix_2'])
|
156 |
+
return ret
|
157 |
+
|
158 |
+
def srgb_transform(self, xyz, img_meta):
|
159 |
+
ret = transform_xyz_to_srgb(xyz)
|
160 |
+
return ret
|
161 |
+
|
162 |
+
def ltm(self, srgb, img_meta):
|
163 |
+
k = img_meta['noise_profile'][0]
|
164 |
+
scale_ratio = k * 20000
|
165 |
+
scale_ratio = np.clip(scale_ratio, 5, 500)
|
166 |
+
first_k = self.k_th / 2
|
167 |
+
second_k = self.k_th
|
168 |
+
third_k = self.k_th * 2
|
169 |
+
forth_k = self.k_th * 3
|
170 |
+
if k < first_k:
|
171 |
+
s = 0.8
|
172 |
+
elif k < second_k:
|
173 |
+
s = 0.75
|
174 |
+
elif k < third_k:
|
175 |
+
s = 0.7
|
176 |
+
elif k < forth_k:
|
177 |
+
s = 0.6
|
178 |
+
else:
|
179 |
+
s = 0.5
|
180 |
+
ret = local_tone_mapping(srgb, scale_ratio=scale_ratio, mode=1, s=s)
|
181 |
+
|
182 |
+
return ret
|
183 |
+
|
184 |
+
def autocontrast(self, srgb, img_meta):
|
185 |
+
k = img_meta['noise_profile'][0]
|
186 |
+
ret = contrast_enhancement(srgb, k, self.k_th)
|
187 |
+
|
188 |
+
return ret
|
189 |
+
|
190 |
+
def resize(self, srgb, img_meta):
|
191 |
+
h, w, c = srgb.shape
|
192 |
+
target_height = 768
|
193 |
+
target_width = 1024
|
194 |
+
|
195 |
+
if self.params['out_landscape_width'] is not None and self.params[
|
196 |
+
'out_landscape_height'] is not None:
|
197 |
+
target_height = self.params['out_landscape_height']
|
198 |
+
target_width = self.params['out_landscape_width']
|
199 |
+
if h != self.params['out_landscape_height'] or w != self.params[
|
200 |
+
'out_landscape_width']:
|
201 |
+
srgb = cv2.resize(srgb, (target_width, target_height),
|
202 |
+
interpolation=cv2.INTER_CUBIC)
|
203 |
+
return srgb
|
204 |
+
|
205 |
+
def to_uint8(self, srgb, img_meta):
|
206 |
+
self.current_step = 0
|
207 |
+
srgb = np.clip(srgb, 0, 1)
|
208 |
+
return (srgb * 255).astype(np.uint8)
|
209 |
+
|
210 |
+
def fix_orientation(self, img, img_meta):
|
211 |
+
ret = fix_orientation(img, img_meta['orientation'])
|
212 |
+
return ret
|
213 |
+
|
214 |
+
|
215 |
+
class PipelineExecutor:
|
216 |
+
"""
|
217 |
+
Pipeline executor class.
|
218 |
+
|
219 |
+
This class can be used to successively execute the steps of some image pipeline class (for example `RawProcessingPipelineDemo`).
|
220 |
+
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.
|
221 |
+
|
222 |
+
It is assumed that each public method of the pipeline class has 2 parameters:
|
223 |
+
raw_img : ndarray
|
224 |
+
Array with images data.
|
225 |
+
img_meta : Dict
|
226 |
+
Some meta data of image.
|
227 |
+
|
228 |
+
Also each such public method must return an image (ndarray) as the result of processing.
|
229 |
+
"""
|
230 |
+
def __init__(self,
|
231 |
+
img,
|
232 |
+
img_meta,
|
233 |
+
img_name,
|
234 |
+
pipeline_obj,
|
235 |
+
first_stage=None,
|
236 |
+
last_stage=None,
|
237 |
+
save_dir="debug_output"):
|
238 |
+
"""
|
239 |
+
PipelineExecutor __init__ method.
|
240 |
+
|
241 |
+
Parameters
|
242 |
+
----------
|
243 |
+
img : ndarray
|
244 |
+
Image that should be processed by pipeline.
|
245 |
+
img_meta : Dict
|
246 |
+
Some image metadata.
|
247 |
+
pipeline_obj : pipeline object
|
248 |
+
Some pipeline object such as RawProcessingPipelineDemo.
|
249 |
+
first_stage : str, optional
|
250 |
+
The name of first public method of pipeline object that should be called by PipelineExecutor.
|
251 |
+
If None, the first public method from defined in pipeline object will be considered as `first_stage` method.
|
252 |
+
By default None.
|
253 |
+
last_stage : str, optional
|
254 |
+
The name of last public method of pipeline object that should be called by PipelineExecutor.
|
255 |
+
If None, the last public method from defined in pipeline object will be considered as `last_stage` method.
|
256 |
+
By default None.
|
257 |
+
"""
|
258 |
+
self.pipeline_obj = pipeline_obj
|
259 |
+
self.img_name = img_name
|
260 |
+
self.save_dir = os.path.join(save_dir, img_name)
|
261 |
+
self.pipeline_obj.save_dir = self.save_dir
|
262 |
+
self.stages_dict = self._init_stages()
|
263 |
+
self.stages_names, self.stages = list(self.stages_dict.keys()), list(
|
264 |
+
self.stages_dict.values())
|
265 |
+
|
266 |
+
if first_stage is None:
|
267 |
+
self.next_stage_indx = 0
|
268 |
+
else:
|
269 |
+
assert first_stage in self.stages_names, f"Invalid first_stage={first_stage}. Try use the following stages: {self.stages_names}"
|
270 |
+
self.next_stage_indx = self.stages_names.index(first_stage)
|
271 |
+
|
272 |
+
if last_stage is None:
|
273 |
+
self.last_stage_indx = len(self.stages_names) - 1
|
274 |
+
else:
|
275 |
+
assert last_stage in self.stages_names, f"Invalid last_stage={last_stage}. Try use the following stages: {self.stages_names}"
|
276 |
+
self.last_stage_indx = self.stages_names.index(last_stage)
|
277 |
+
if self.next_stage_indx > self.last_stage_indx:
|
278 |
+
print(
|
279 |
+
f'Warning: the specified first_stage={first_stage} follows the specified last_stage={last_stage}, so using __call__ no image processing will be done.'
|
280 |
+
)
|
281 |
+
|
282 |
+
self.current_image = img
|
283 |
+
self.img_meta = img_meta
|
284 |
+
|
285 |
+
def _init_stages(self):
|
286 |
+
stages = {
|
287 |
+
func: getattr(self.pipeline_obj, func)
|
288 |
+
for func in self.pipeline_obj.__class__.__dict__
|
289 |
+
if callable(getattr(self.pipeline_obj, func))
|
290 |
+
and not func.startswith("_")
|
291 |
+
}
|
292 |
+
return stages
|
293 |
+
|
294 |
+
@property
|
295 |
+
def next_stage(self):
|
296 |
+
if self.next_stage_indx < len(self.stages):
|
297 |
+
return self.stages_names[self.next_stage_indx]
|
298 |
+
else:
|
299 |
+
return None
|
300 |
+
|
301 |
+
@property
|
302 |
+
def last_stage(self):
|
303 |
+
return self.stages_names[self.last_stage_indx]
|
304 |
+
|
305 |
+
def __iter__(self):
|
306 |
+
return self
|
307 |
+
|
308 |
+
def __next__(self):
|
309 |
+
if self.next_stage_indx < len(self.stages):
|
310 |
+
stage_func = self.stages[self.next_stage_indx]
|
311 |
+
self.current_image = stage_func(self.current_image, self.img_meta)
|
312 |
+
self.next_stage_indx += 1
|
313 |
+
return self.current_image
|
314 |
+
else:
|
315 |
+
raise StopIteration
|
316 |
+
|
317 |
+
def __call__(self):
|
318 |
+
"""
|
319 |
+
PipelineExecutor __call__ method.
|
320 |
+
|
321 |
+
This method will sequentially execute the methods defined in the pipeline object from the `first_stage` to the `last_stage` inclusive.
|
322 |
+
|
323 |
+
Returns
|
324 |
+
-------
|
325 |
+
ndarray
|
326 |
+
Resulted processed raw image.
|
327 |
+
"""
|
328 |
+
for current_image in self:
|
329 |
+
if self.next_stage_indx > self.last_stage_indx:
|
330 |
+
return current_image
|
331 |
+
return self.current_image
|
PolyuColor/raw_prc_pipeline/pipeline_utils.py
ADDED
@@ -0,0 +1,575 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Camera pipeline utilities.
|
3 |
+
"""
|
4 |
+
|
5 |
+
import os
|
6 |
+
from fractions import Fraction
|
7 |
+
|
8 |
+
import cv2
|
9 |
+
import exifread
|
10 |
+
import numpy as np
|
11 |
+
import rawpy
|
12 |
+
import torch
|
13 |
+
import torch.nn.functional as F
|
14 |
+
import torchvision.transforms.functional as TF
|
15 |
+
# from exifread import Ratio
|
16 |
+
from exifread.utils import Ratio
|
17 |
+
from PIL import Image, ImageOps
|
18 |
+
from scipy.io import loadmat
|
19 |
+
from skimage.restoration import denoise_bilateral
|
20 |
+
from skimage.transform import resize as skimage_resize
|
21 |
+
|
22 |
+
from raw_prc_pipeline.exif_utils import get_tag_values_from_ifds, parse_exif
|
23 |
+
from raw_prc_pipeline.fs import perform_flash, perform_storm
|
24 |
+
|
25 |
+
EPS = 1e-9
|
26 |
+
|
27 |
+
|
28 |
+
def get_visible_raw_image(image_path):
|
29 |
+
raw_image = rawpy.imread(image_path).raw_image_visible.copy()
|
30 |
+
# raw_image = rawpy.imread(image_path).raw_image.copy()
|
31 |
+
return raw_image
|
32 |
+
|
33 |
+
|
34 |
+
def get_image_tags(image_path):
|
35 |
+
with open(image_path, 'rb') as f:
|
36 |
+
tags = exifread.process_file(f)
|
37 |
+
return tags
|
38 |
+
|
39 |
+
|
40 |
+
def get_image_ifds(image_path):
|
41 |
+
ifds = parse_exif(image_path, verbose=False)
|
42 |
+
return ifds
|
43 |
+
|
44 |
+
|
45 |
+
def get_metadata(image_path):
|
46 |
+
metadata = {}
|
47 |
+
tags = get_image_tags(image_path)
|
48 |
+
ifds = get_image_ifds(image_path)
|
49 |
+
metadata['linearization_table'] = get_linearization_table(tags, ifds)
|
50 |
+
metadata['black_level'] = get_black_level(tags, ifds)
|
51 |
+
metadata['white_level'] = get_white_level(tags, ifds)
|
52 |
+
metadata['cfa_pattern'] = get_cfa_pattern(tags, ifds)
|
53 |
+
metadata['as_shot_neutral'] = get_as_shot_neutral(tags, ifds)
|
54 |
+
color_matrix_1, color_matrix_2 = get_color_matrices(tags, ifds)
|
55 |
+
metadata['color_matrix_1'] = color_matrix_1
|
56 |
+
metadata['color_matrix_2'] = color_matrix_2
|
57 |
+
metadata['orientation'] = get_orientation(tags, ifds)
|
58 |
+
# isn't used
|
59 |
+
metadata['noise_profile'] = get_noise_profile(tags, ifds)
|
60 |
+
# ...
|
61 |
+
# fall back to default values, if necessary
|
62 |
+
if metadata['black_level'] is None:
|
63 |
+
metadata['black_level'] = 0
|
64 |
+
print("Black level is None; using 0.")
|
65 |
+
if metadata['white_level'] is None:
|
66 |
+
metadata['white_level'] = 2**16
|
67 |
+
print("White level is None; using 2 ** 16.")
|
68 |
+
if metadata['cfa_pattern'] is None:
|
69 |
+
metadata['cfa_pattern'] = [0, 1, 1, 2]
|
70 |
+
print("CFAPattern is None; using [0, 1, 1, 2] (RGGB)")
|
71 |
+
if metadata['as_shot_neutral'] is None:
|
72 |
+
metadata['as_shot_neutral'] = [1, 1, 1]
|
73 |
+
print("AsShotNeutral is None; using [1, 1, 1]")
|
74 |
+
if metadata['color_matrix_1'] is None:
|
75 |
+
metadata['color_matrix_1'] = [1] * 9
|
76 |
+
print("ColorMatrix1 is None; using [1, 1, 1, 1, 1, 1, 1, 1, 1]")
|
77 |
+
if metadata['color_matrix_2'] is None:
|
78 |
+
metadata['color_matrix_2'] = [1] * 9
|
79 |
+
print("ColorMatrix2 is None; using [1, 1, 1, 1, 1, 1, 1, 1, 1]")
|
80 |
+
if metadata['orientation'] is None:
|
81 |
+
metadata['orientation'] = 0
|
82 |
+
print("Orientation is None; using 0.")
|
83 |
+
# ...
|
84 |
+
return metadata
|
85 |
+
|
86 |
+
|
87 |
+
def get_linearization_table(tags, ifds):
|
88 |
+
possible_keys = [
|
89 |
+
'Image Tag 0xC618', 'Image Tag 50712', 'LinearizationTable',
|
90 |
+
'Image LinearizationTable'
|
91 |
+
]
|
92 |
+
return get_values(tags, possible_keys)
|
93 |
+
|
94 |
+
|
95 |
+
def get_black_level(tags, ifds):
|
96 |
+
possible_keys = [
|
97 |
+
'Image Tag 0xC61A', 'Image Tag 50714', 'BlackLevel', 'Image BlackLevel'
|
98 |
+
]
|
99 |
+
vals = get_values(tags, possible_keys)
|
100 |
+
if vals is None:
|
101 |
+
# print("Black level not found in exifread tags. Searching IFDs.")
|
102 |
+
vals = get_tag_values_from_ifds(50714, ifds)
|
103 |
+
return vals
|
104 |
+
|
105 |
+
|
106 |
+
def get_white_level(tags, ifds):
|
107 |
+
possible_keys = [
|
108 |
+
'Image Tag 0xC61D', 'Image Tag 50717', 'WhiteLevel', 'Image WhiteLevel'
|
109 |
+
]
|
110 |
+
vals = get_values(tags, possible_keys)
|
111 |
+
if vals is None:
|
112 |
+
# print("White level not found in exifread tags. Searching IFDs.")
|
113 |
+
vals = get_tag_values_from_ifds(50717, ifds)
|
114 |
+
return vals
|
115 |
+
|
116 |
+
|
117 |
+
def get_cfa_pattern(tags, ifds):
|
118 |
+
possible_keys = ['CFAPattern', 'Image CFAPattern']
|
119 |
+
vals = get_values(tags, possible_keys)
|
120 |
+
if vals is None:
|
121 |
+
# print("CFAPattern not found in exifread tags. Searching IFDs.")
|
122 |
+
vals = get_tag_values_from_ifds(33422, ifds)
|
123 |
+
return vals
|
124 |
+
|
125 |
+
|
126 |
+
def get_as_shot_neutral(tags, ifds):
|
127 |
+
possible_keys = [
|
128 |
+
'Image Tag 0xC628', 'Image Tag 50728', 'AsShotNeutral',
|
129 |
+
'Image AsShotNeutral'
|
130 |
+
]
|
131 |
+
return get_values(tags, possible_keys)
|
132 |
+
|
133 |
+
|
134 |
+
def get_color_matrices(tags, ifds):
|
135 |
+
possible_keys_1 = [
|
136 |
+
'Image Tag 0xC621', 'Image Tag 50721', 'ColorMatrix1',
|
137 |
+
'Image ColorMatrix1'
|
138 |
+
]
|
139 |
+
color_matrix_1 = get_values(tags, possible_keys_1)
|
140 |
+
possible_keys_2 = [
|
141 |
+
'Image Tag 0xC622', 'Image Tag 50722', 'ColorMatrix2',
|
142 |
+
'Image ColorMatrix2'
|
143 |
+
]
|
144 |
+
color_matrix_2 = get_values(tags, possible_keys_2)
|
145 |
+
#print(f'Color matrix 1:{color_matrix_1}')
|
146 |
+
#print(f'Color matrix 2:{color_matrix_2}')
|
147 |
+
#print(np.sum(np.abs(np.array(color_matrix_1) - np.array(color_matrix_2))))
|
148 |
+
return color_matrix_1, color_matrix_2
|
149 |
+
|
150 |
+
|
151 |
+
def get_orientation(tags, ifds):
|
152 |
+
possible_tags = ['Orientation', 'Image Orientation']
|
153 |
+
return get_values(tags, possible_tags)
|
154 |
+
|
155 |
+
|
156 |
+
def get_noise_profile(tags, ifds):
|
157 |
+
possible_keys = [
|
158 |
+
'Image Tag 0xC761', 'Image Tag 51041', 'NoiseProfile',
|
159 |
+
'Image NoiseProfile'
|
160 |
+
]
|
161 |
+
vals = get_values(tags, possible_keys)
|
162 |
+
if vals is None:
|
163 |
+
# print("Noise profile not found in exifread tags. Searching IFDs.")
|
164 |
+
vals = get_tag_values_from_ifds(51041, ifds)
|
165 |
+
return vals
|
166 |
+
|
167 |
+
|
168 |
+
def get_values(tags, possible_keys):
|
169 |
+
values = None
|
170 |
+
for key in possible_keys:
|
171 |
+
if key in tags.keys():
|
172 |
+
values = tags[key].values
|
173 |
+
return values
|
174 |
+
|
175 |
+
|
176 |
+
def normalize(raw_image, black_level, white_level):
|
177 |
+
if type(black_level) is list and len(black_level) == 1:
|
178 |
+
black_level = float(black_level[0])
|
179 |
+
if type(white_level) is list and len(white_level) == 1:
|
180 |
+
white_level = float(white_level[0])
|
181 |
+
black_level_mask = black_level
|
182 |
+
if type(black_level) is list and len(black_level) == 4:
|
183 |
+
if type(black_level[0]) is Ratio:
|
184 |
+
black_level = ratios2floats(black_level)
|
185 |
+
if type(black_level[0]) is Fraction:
|
186 |
+
black_level = fractions2floats(black_level)
|
187 |
+
black_level_mask = np.zeros(raw_image.shape)
|
188 |
+
idx2by2 = [[0, 0], [0, 1], [1, 0], [1, 1]]
|
189 |
+
step2 = 2
|
190 |
+
for i, idx in enumerate(idx2by2):
|
191 |
+
black_level_mask[idx[0]::step2, idx[1]::step2] = black_level[i]
|
192 |
+
normalized_image = raw_image.astype(np.float32) - black_level_mask
|
193 |
+
# if some values were smaller than black level
|
194 |
+
normalized_image[normalized_image < 0] = 0
|
195 |
+
normalized_image = normalized_image / (white_level - black_level_mask)
|
196 |
+
return normalized_image
|
197 |
+
|
198 |
+
|
199 |
+
def ratios2floats(ratios):
|
200 |
+
floats = []
|
201 |
+
for ratio in ratios:
|
202 |
+
floats.append(float(ratio.num) / ratio.den)
|
203 |
+
return floats
|
204 |
+
|
205 |
+
|
206 |
+
def fractions2floats(fractions):
|
207 |
+
floats = []
|
208 |
+
for fraction in fractions:
|
209 |
+
floats.append(float(fraction.numerator) / fraction.denominator)
|
210 |
+
return floats
|
211 |
+
|
212 |
+
|
213 |
+
def illumination_parameters_estimation(current_image,
|
214 |
+
illumination_estimation_option):
|
215 |
+
ie_method = illumination_estimation_option.lower()
|
216 |
+
if ie_method == "gw":
|
217 |
+
mask = np.any(current_image < 1e-2, axis=-1) | np.any(
|
218 |
+
current_image > 1 - 5e-2, axis=-1)
|
219 |
+
if np.sum(~mask) == 0:
|
220 |
+
return np.array([1, 1, 1])
|
221 |
+
valid_pixels = current_image[~mask]
|
222 |
+
ie = np.mean(valid_pixels, axis=0)
|
223 |
+
ie /= ie[1]
|
224 |
+
ie[ie < 1e-1] = 1
|
225 |
+
return ie
|
226 |
+
elif ie_method == "sog":
|
227 |
+
sog_p = 4.
|
228 |
+
ie = np.mean(current_image**sog_p, axis=(0, 1))**(1 / sog_p)
|
229 |
+
ie /= ie[1]
|
230 |
+
return ie
|
231 |
+
elif ie_method == "wp":
|
232 |
+
ie = np.max(current_image, axis=(0, 1))
|
233 |
+
ie /= ie[1]
|
234 |
+
return ie
|
235 |
+
elif ie_method == "iwp":
|
236 |
+
samples_count = 20
|
237 |
+
sample_size = 20
|
238 |
+
rows, cols = current_image.shape[:2]
|
239 |
+
data = np.reshape(current_image, (rows * cols, 3))
|
240 |
+
maxima = np.zeros((samples_count, 3))
|
241 |
+
for i in range(samples_count):
|
242 |
+
maxima[i, :] = np.max(data[
|
243 |
+
np.random.randint(low=0, high=rows *
|
244 |
+
cols, size=(sample_size)), :],
|
245 |
+
axis=0)
|
246 |
+
ie = np.mean(maxima, axis=0)
|
247 |
+
ie /= ie[1]
|
248 |
+
return ie
|
249 |
+
else:
|
250 |
+
raise ValueError(
|
251 |
+
'Bad illumination_estimation_option value! Use the following options: "gw", "wp", "sog", "iwp"'
|
252 |
+
)
|
253 |
+
|
254 |
+
|
255 |
+
def sample_acceptable_white_point(lower=0.35, upper=0.5, samples_count=10):
|
256 |
+
x = np.linspace(lower, upper, samples_count)
|
257 |
+
a, b, c = 2.14325171, 7.12239676, 0.10934688
|
258 |
+
y = a * np.exp(-b * x) + c
|
259 |
+
ret = np.stack([x, y], axis=1)
|
260 |
+
return ret
|
261 |
+
|
262 |
+
|
263 |
+
def patch_based_white_balance(img: np.ndarray, split_ratio=2) -> np.ndarray:
|
264 |
+
h, w = img.shape[:2]
|
265 |
+
patch_size = (h // split_ratio, w // split_ratio)
|
266 |
+
patches = [
|
267 |
+
img[i:i + patch_size[0], j:j + patch_size[1], :]
|
268 |
+
for i in range(0, h, patch_size[0])
|
269 |
+
for j in range(0, w, patch_size[1])
|
270 |
+
]
|
271 |
+
white_points = []
|
272 |
+
for patch in patches:
|
273 |
+
white_point = illumination_parameters_estimation(patch, "gw")
|
274 |
+
white_points.append(white_point)
|
275 |
+
white_points.append(illumination_parameters_estimation(img, "gw"))
|
276 |
+
white_points = np.array(white_points)
|
277 |
+
|
278 |
+
white_point = white_point_regularization(white_points)
|
279 |
+
return white_point
|
280 |
+
|
281 |
+
|
282 |
+
def white_point_regularization(white_points: np.ndarray,
|
283 |
+
radius=0.125) -> np.ndarray:
|
284 |
+
"""
|
285 |
+
Regularize the white point vector to avoid numerical instability.
|
286 |
+
"""
|
287 |
+
centers = np.array([[0.339, 1, 0.361], [0.367, 1,
|
288 |
+
0.289], [0.398, 1, 0.237],
|
289 |
+
[0.464, 1, 0.198], [0.39, 1, 0.29], [0.480, 1, 0.187],
|
290 |
+
[0.535, 1, 0.165], [0.582, 1, 0.135]])
|
291 |
+
center_weights = np.zeros(len(centers), dtype=np.float32)
|
292 |
+
mini_dist = float("inf")
|
293 |
+
global_cloest_center = None
|
294 |
+
for i, wp in enumerate(white_points):
|
295 |
+
distances = np.linalg.norm(np.abs(centers - wp), axis=1)
|
296 |
+
for i, distance in enumerate(distances):
|
297 |
+
if distance <= radius:
|
298 |
+
center_weights[i] += 1. / (distance + 1e-6)
|
299 |
+
if distance < mini_dist:
|
300 |
+
mini_dist = distance
|
301 |
+
global_cloest_center = centers[i]
|
302 |
+
if center_weights.sum() == 0:
|
303 |
+
white_point = global_cloest_center
|
304 |
+
return white_point
|
305 |
+
center_weights /= center_weights.sum()
|
306 |
+
weighted_white_points = centers * center_weights[:, np.newaxis]
|
307 |
+
white_point = weighted_white_points.sum(axis=0)
|
308 |
+
return white_point
|
309 |
+
|
310 |
+
|
311 |
+
def white_balance(demosaic_img, as_shot_neutral):
|
312 |
+
if type(as_shot_neutral[0]) is Ratio:
|
313 |
+
as_shot_neutral = ratios2floats(as_shot_neutral)
|
314 |
+
|
315 |
+
as_shot_neutral = np.asarray(as_shot_neutral)
|
316 |
+
# transform vector into matrix
|
317 |
+
if as_shot_neutral.shape == (3, ):
|
318 |
+
as_shot_neutral = np.diag(1. / as_shot_neutral)
|
319 |
+
|
320 |
+
assert as_shot_neutral.shape == (3, 3)
|
321 |
+
|
322 |
+
white_balanced_image = np.dot(demosaic_img, as_shot_neutral.T)
|
323 |
+
white_balanced_image = np.clip(white_balanced_image, 0.0, 1.0)
|
324 |
+
|
325 |
+
return white_balanced_image
|
326 |
+
|
327 |
+
|
328 |
+
def simple_demosaic(img, cfa_pattern):
|
329 |
+
raw_colors = np.asarray(cfa_pattern).reshape((2, 2))
|
330 |
+
demosaiced_image = np.zeros((img.shape[0] // 2, img.shape[1] // 2, 3))
|
331 |
+
for i in range(2):
|
332 |
+
for j in range(2):
|
333 |
+
ch = raw_colors[i, j]
|
334 |
+
if ch == 1:
|
335 |
+
demosaiced_image[:, :, ch] += img[i::2, j::2] / 2
|
336 |
+
else:
|
337 |
+
demosaiced_image[:, :, ch] = img[i::2, j::2]
|
338 |
+
return demosaiced_image
|
339 |
+
|
340 |
+
|
341 |
+
def denoise_image(demosaiced_image):
|
342 |
+
multichannel = False
|
343 |
+
if len(demosaiced_image.shape) == 3:
|
344 |
+
multichannel = True
|
345 |
+
current_image = denoise_bilateral(demosaiced_image,
|
346 |
+
sigma_color=None,
|
347 |
+
sigma_spatial=1.,
|
348 |
+
channel_axis=-1,
|
349 |
+
mode='reflect',
|
350 |
+
multichannel=multichannel)
|
351 |
+
return current_image
|
352 |
+
|
353 |
+
|
354 |
+
def apply_color_space_transform(demosaiced_image, color_matrix_1,
|
355 |
+
color_matrix_2):
|
356 |
+
if isinstance(color_matrix_1[0], Fraction):
|
357 |
+
color_matrix_1 = fractions2floats(color_matrix_1)
|
358 |
+
if isinstance(color_matrix_2[0], Fraction):
|
359 |
+
color_matrix_2 = fractions2floats(color_matrix_2)
|
360 |
+
xyz2cam1 = np.reshape(np.asarray(color_matrix_1), (3, 3))
|
361 |
+
xyz2cam2 = np.reshape(np.asarray(color_matrix_2), (3, 3))
|
362 |
+
# normalize rows (needed?)
|
363 |
+
xyz2cam1 = xyz2cam1 / np.sum(xyz2cam1, axis=1, keepdims=True)
|
364 |
+
xyz2cam2 = xyz2cam2 / np.sum(xyz2cam1, axis=1, keepdims=True)
|
365 |
+
# inverse
|
366 |
+
cam2xyz1 = np.linalg.inv(xyz2cam1)
|
367 |
+
cam2xyz2 = np.linalg.inv(xyz2cam2)
|
368 |
+
# for now, use one matrix # TODO: interpolate btween both
|
369 |
+
# simplified matrix multiplication
|
370 |
+
xyz_image = cam2xyz1[np.newaxis,
|
371 |
+
np.newaxis, :, :] * demosaiced_image[:, :,
|
372 |
+
np.newaxis, :]
|
373 |
+
xyz_image = np.sum(xyz_image, axis=-1)
|
374 |
+
xyz_image = np.clip(xyz_image, 0.0, 1.0)
|
375 |
+
return xyz_image
|
376 |
+
|
377 |
+
|
378 |
+
def transform_xyz_to_srgb(xyz_image):
|
379 |
+
# srgb2xyz = np.array([[0.4124564, 0.3575761, 0.1804375],
|
380 |
+
# [0.2126729, 0.7151522, 0.0721750],
|
381 |
+
# [0.0193339, 0.1191920, 0.9503041]])
|
382 |
+
|
383 |
+
# xyz2srgb = np.linalg.inv(srgb2xyz)
|
384 |
+
|
385 |
+
xyz2srgb = np.array([[3.2404542, -1.5371385, -0.4985314],
|
386 |
+
[-0.9692660, 1.8760108, 0.0415560],
|
387 |
+
[0.0556434, -0.2040259, 1.0572252]])
|
388 |
+
|
389 |
+
# normalize rows (needed?)
|
390 |
+
xyz2srgb = xyz2srgb / np.sum(xyz2srgb, axis=-1, keepdims=True)
|
391 |
+
|
392 |
+
srgb_image = xyz2srgb[np.newaxis,
|
393 |
+
np.newaxis, :, :] * xyz_image[:, :, np.newaxis, :]
|
394 |
+
srgb_image = np.sum(srgb_image, axis=-1)
|
395 |
+
srgb_image = np.clip(srgb_image, 0.0, 1.0)
|
396 |
+
return srgb_image
|
397 |
+
|
398 |
+
|
399 |
+
def reverse_orientation(image, orientation):
|
400 |
+
# 1 = Horizontal(normal)
|
401 |
+
# 2 = Mirror horizontal
|
402 |
+
# 3 = Rotate 180
|
403 |
+
# 4 = Mirror vertical
|
404 |
+
# 5 = Mirror horizontal and rotate 270 CW
|
405 |
+
# 6 = Rotate 90 CW
|
406 |
+
# 7 = Mirror horizontal and rotate 90 CW
|
407 |
+
# 8 = Rotate 270 CW
|
408 |
+
rev_orientations = np.array([1, 2, 3, 4, 5, 8, 7, 6])
|
409 |
+
return fix_orientation(image, rev_orientations[orientation - 1])
|
410 |
+
|
411 |
+
|
412 |
+
def apply_gamma(x, gamma=1.5):
|
413 |
+
return x**(1.0 / gamma)
|
414 |
+
# x = x.copy()
|
415 |
+
# idx = x <= 0.0031308
|
416 |
+
# x[idx] *= 12.92
|
417 |
+
# x[idx == False] = (x[idx == False] ** (1.0 / 2.4)) * 1.055 - 0.055
|
418 |
+
# return x
|
419 |
+
|
420 |
+
|
421 |
+
def apply_tone_map(x, tone_mapping='Base'):
|
422 |
+
if tone_mapping == 'Flash':
|
423 |
+
return perform_flash(x, perform_gamma_correction=0) / 255.
|
424 |
+
elif tone_mapping == 'Storm':
|
425 |
+
return perform_storm(x, perform_gamma_correction=0) / 255.
|
426 |
+
elif tone_mapping == 'Drago':
|
427 |
+
tonemap = cv2.createTonemapDrago()
|
428 |
+
return tonemap.process(x.astype(np.float32))
|
429 |
+
elif tone_mapping == 'Mantiuk':
|
430 |
+
tonemap = cv2.createTonemapMantiuk()
|
431 |
+
return tonemap.process(x.astype(np.float32))
|
432 |
+
elif tone_mapping == 'Reinhard':
|
433 |
+
tonemap = cv2.createTonemapReinhard()
|
434 |
+
return tonemap.process(x.astype(np.float32))
|
435 |
+
elif tone_mapping == 'Linear':
|
436 |
+
return np.clip(x / np.sort(x.flatten())[-50000], 0, 1)
|
437 |
+
elif tone_mapping == 'Base':
|
438 |
+
# return 3 * x ** 2 - 2 * x ** 3
|
439 |
+
# tone_curve = loadmat('tone_curve.mat')
|
440 |
+
tone_curve = loadmat(
|
441 |
+
os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
442 |
+
'tone_curve.mat'))
|
443 |
+
tone_curve = tone_curve['tc']
|
444 |
+
x = np.round(x * (len(tone_curve) - 1)).astype(int)
|
445 |
+
tone_mapped_image = np.squeeze(tone_curve[x])
|
446 |
+
return tone_mapped_image
|
447 |
+
else:
|
448 |
+
raise ValueError(
|
449 |
+
'Bad tone_mapping option value! Use the following options: "Base", "Flash", "Storm", "Linear", "Drago", "Mantiuk", "Reinhard"'
|
450 |
+
)
|
451 |
+
|
452 |
+
|
453 |
+
def autocontrast(output_image, cutoff_prcnt=2, preserve_tone=False):
|
454 |
+
if preserve_tone:
|
455 |
+
min_val, max_val = np.percentile(output_image,
|
456 |
+
[cutoff_prcnt, 100 - cutoff_prcnt])
|
457 |
+
output_image = (output_image - min_val) / (max_val - min_val)
|
458 |
+
else:
|
459 |
+
channels = [None] * 3
|
460 |
+
for ch in range(3):
|
461 |
+
min_val, max_val = np.percentile(
|
462 |
+
output_image[..., ch], [cutoff_prcnt, 100 - cutoff_prcnt])
|
463 |
+
channels[ch] = (output_image[..., ch] - min_val) / (max_val -
|
464 |
+
min_val)
|
465 |
+
output_image = np.dstack(channels)
|
466 |
+
output_image = np.clip(output_image, 0, 1)
|
467 |
+
return output_image
|
468 |
+
|
469 |
+
|
470 |
+
def autocontrast_using_pil(img, cutoff=(2, 0.2)):
|
471 |
+
img_uint8 = np.clip(255 * img, 0, 255).astype(np.uint8)
|
472 |
+
img_pil = Image.fromarray(img_uint8)
|
473 |
+
img_pil = ImageOps.autocontrast(img_pil, cutoff=cutoff, preserve_tone=True)
|
474 |
+
output_image = np.array(img_pil).astype(np.float32) / 255
|
475 |
+
return output_image
|
476 |
+
|
477 |
+
|
478 |
+
def raw_rgb_to_cct(rawRgb, xyz2cam1, xyz2cam2):
|
479 |
+
"""Convert raw-RGB triplet to corresponding correlated color temperature (CCT)"""
|
480 |
+
pass
|
481 |
+
# pxyz = [.5, 1, .5]
|
482 |
+
# loss = 1e10
|
483 |
+
# k = 1
|
484 |
+
# while loss > 1e-4:
|
485 |
+
# cct = XyzToCct(pxyz)
|
486 |
+
# xyz = RawRgbToXyz(rawRgb, cct, xyz2cam1, xyz2cam2)
|
487 |
+
# loss = norm(xyz - pxyz)
|
488 |
+
# pxyz = xyz
|
489 |
+
# fprintf('k = %d, loss = %f\n', [k, loss])
|
490 |
+
# k = k + 1
|
491 |
+
# end
|
492 |
+
# temp = cct
|
493 |
+
|
494 |
+
|
495 |
+
def resize_using_skimage(img, width=1296, height=864):
|
496 |
+
out_shape = (height, width) + img.shape[2:]
|
497 |
+
if img.shape == out_shape:
|
498 |
+
return img
|
499 |
+
out_img = skimage_resize(img,
|
500 |
+
out_shape,
|
501 |
+
preserve_range=True,
|
502 |
+
anti_aliasing=True)
|
503 |
+
out_img = out_img.astype(np.uint8)
|
504 |
+
return out_img
|
505 |
+
|
506 |
+
|
507 |
+
def resize_using_pil(img, width=1296, height=864):
|
508 |
+
img_pil = Image.fromarray(img)
|
509 |
+
out_size = (width, height)
|
510 |
+
if img_pil.size == out_size:
|
511 |
+
return img
|
512 |
+
out_img = img_pil.resize(out_size, Image.ANTIALIAS)
|
513 |
+
out_img = np.array(out_img)
|
514 |
+
return out_img
|
515 |
+
|
516 |
+
|
517 |
+
def fix_orientation(image, orientation):
|
518 |
+
# 1 = Horizontal(normal)
|
519 |
+
# 2 = Mirror horizontal
|
520 |
+
# 3 = Rotate 180
|
521 |
+
# 4 = Mirror vertical
|
522 |
+
# 5 = Mirror horizontal and rotate 270 CW
|
523 |
+
# 6 = Rotate 90 CW
|
524 |
+
# 7 = Mirror horizontal and rotate 90 CW
|
525 |
+
# 8 = Rotate 270 CW
|
526 |
+
orientation_dict = [
|
527 |
+
"Horizontal (normal)", "Mirror horizontal", "Rotate 180",
|
528 |
+
"Mirror vertical", "Mirror horizontal and rotate 270 CW",
|
529 |
+
"Rotate 90 CW", "Mirror horizontal and rotate 90 CW", "Rotate 270 CW"
|
530 |
+
]
|
531 |
+
orientation_dict = {v: k for k, v in enumerate(orientation_dict)}
|
532 |
+
orientation = orientation_dict[orientation] + 1
|
533 |
+
|
534 |
+
if orientation == 1:
|
535 |
+
pass
|
536 |
+
elif orientation == 2:
|
537 |
+
image = cv2.flip(image, 0)
|
538 |
+
elif orientation == 3:
|
539 |
+
image = cv2.rotate(image, cv2.ROTATE_180)
|
540 |
+
elif orientation == 4:
|
541 |
+
image = cv2.flip(image, 1)
|
542 |
+
elif orientation == 5:
|
543 |
+
image = cv2.flip(image, 0)
|
544 |
+
image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
545 |
+
elif orientation == 6:
|
546 |
+
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
|
547 |
+
elif orientation == 7:
|
548 |
+
image = cv2.flip(image, 0)
|
549 |
+
image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
|
550 |
+
elif orientation == 8:
|
551 |
+
image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
552 |
+
|
553 |
+
return image
|
554 |
+
|
555 |
+
|
556 |
+
def compute_lsc_gain(rggb):
|
557 |
+
gains = []
|
558 |
+
for ch in range(4):
|
559 |
+
channel_max = rggb[:, :, ch].max()
|
560 |
+
gain = rggb[:, :, ch] / (channel_max * 0.8)
|
561 |
+
gains.append(gain.clip(0., 1.0))
|
562 |
+
return gains
|
563 |
+
|
564 |
+
|
565 |
+
def resize_rggb(rggb_raw: torch.Tensor, target_height=768, target_width=1024):
|
566 |
+
height, width = rggb_raw.shape[-2:]
|
567 |
+
target_size = (target_width,
|
568 |
+
target_height) if height > width else (target_width, 1024)
|
569 |
+
resized_raw = TF.resize(rggb_raw,
|
570 |
+
size=(target_size),
|
571 |
+
interpolation=TF.InterpolationMode.BICUBIC,
|
572 |
+
antialias=True)
|
573 |
+
resized_raw = torch.clamp(resized_raw, 0, 1)
|
574 |
+
|
575 |
+
return resized_raw
|
PolyuColor/raw_prc_pipeline/sharpening.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import numpy as np
|
3 |
+
|
4 |
+
|
5 |
+
def sharpen_image_with_unsharp_masking(image, sigma=1.0, alpha=1.0):
|
6 |
+
"""
|
7 |
+
sharp operation
|
8 |
+
"""
|
9 |
+
image = (image * 65535).astype(np.uint16)
|
10 |
+
blurred = cv2.GaussianBlur(image, (0, 0), sigmaX=sigma, sigmaY=sigma)
|
11 |
+
sharpened = cv2.addWeighted(image, 1 + alpha, blurred, -alpha, 0.15)
|
12 |
+
sharpened_image = np.clip(sharpened / 65535.0, 0, 1)
|
13 |
+
|
14 |
+
return sharpened_image
|
PolyuColor/raw_prc_pipeline/tone_curve.mat
ADDED
Binary file (6.57 kB). View file
|
|
PolyuColor/raw_prc_pipeline/tone_mapping.py
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import cv2
|
3 |
+
from utils import *
|
4 |
+
|
5 |
+
__all__ = ['local_tone_mapping', 'lmhe_global_tone_mapping']
|
6 |
+
|
7 |
+
|
8 |
+
def _filterGaussianWindow(img: np.ndarray, window):
|
9 |
+
w = max(np.round(window), 3)
|
10 |
+
if w % 2 == 0:
|
11 |
+
w += 1
|
12 |
+
img_blur = cv2.GaussianBlur(img, (w, w),
|
13 |
+
0,
|
14 |
+
borderType=cv2.BORDER_REPLICATE)
|
15 |
+
return img_blur
|
16 |
+
|
17 |
+
|
18 |
+
def _remove_specials(img: np.ndarray, replace_value=1) -> np.ndarray:
|
19 |
+
mask = np.isinf(img) | np.isnan(img)
|
20 |
+
img[mask] = replace_value
|
21 |
+
return img
|
22 |
+
|
23 |
+
|
24 |
+
def _ashikhmin_filtering(image: np.ndarray, Ashikhmin_sMax=5) -> tuple:
|
25 |
+
r, c = image.shape
|
26 |
+
threshold = 0.5
|
27 |
+
|
28 |
+
Lfiltered = np.zeros((r, c, Ashikhmin_sMax), dtype=image.dtype)
|
29 |
+
LC = np.zeros((r, c, Ashikhmin_sMax), dtype=image.dtype)
|
30 |
+
for i in range(Ashikhmin_sMax):
|
31 |
+
Lfiltered[:, :, i] = _filterGaussianWindow(image, i + 1)
|
32 |
+
LC[:, :, i] = _remove_specials(
|
33 |
+
np.abs(Lfiltered[:, :, i] -
|
34 |
+
_filterGaussianWindow(image, (i + 1) * 2)) /
|
35 |
+
Lfiltered[:, :, i])
|
36 |
+
|
37 |
+
L_adapt = -np.ones_like(image)
|
38 |
+
for i in range(Ashikhmin_sMax):
|
39 |
+
LC_i = LC[:, :, i]
|
40 |
+
mask = LC_i < threshold
|
41 |
+
L_adapt[mask] = Lfiltered[:, :, i][mask]
|
42 |
+
|
43 |
+
mask = L_adapt < 0
|
44 |
+
L_adapt[mask] = Lfiltered[:, :, -1][mask]
|
45 |
+
L_detail = _remove_specials(image / L_adapt)
|
46 |
+
L_detail = np.clip(L_detail, 0, None)
|
47 |
+
|
48 |
+
return L_adapt, L_detail
|
49 |
+
|
50 |
+
|
51 |
+
def _tvi_ashikhmin(img: np.ndarray) -> np.ndarray:
|
52 |
+
Lout = np.zeros_like(img, dtype=img.dtype)
|
53 |
+
mask = img < 0.0034
|
54 |
+
Lout[mask] = img[mask] / 0.0014
|
55 |
+
|
56 |
+
mask = (img >= 0.0034) & (img < 1)
|
57 |
+
Lout[mask] = 2.4483 + np.log(img[mask] / 0.0034) / 0.4027
|
58 |
+
|
59 |
+
mask = (img >= 1) & (img < 7.2444)
|
60 |
+
Lout[mask] = 16.5630 + (img[mask] - 1) / 0.4027
|
61 |
+
|
62 |
+
mask = img >= 7.2444
|
63 |
+
Lout[mask] = 32.0693 + np.log(img[mask] / 7.2444) / 0.0556
|
64 |
+
|
65 |
+
return Lout
|
66 |
+
|
67 |
+
|
68 |
+
def local_tone_mapping(image: np.ndarray,
|
69 |
+
scale_ratio: float = 50,
|
70 |
+
s=0.7,
|
71 |
+
mode=1) -> np.ndarray:
|
72 |
+
"""
|
73 |
+
Local tone mapping function.
|
74 |
+
|
75 |
+
Parameters:
|
76 |
+
image: np.ndarray
|
77 |
+
Input image with shape (h, w, 3), range [0, 1], color space sRGB.
|
78 |
+
scale_ratio: float
|
79 |
+
Scale ratio of the input image to the output image. luminance 1 equals to 10,000 cd/m^2.
|
80 |
+
s: float
|
81 |
+
s factor of the local tone mapping function, control the staturation of the image.
|
82 |
+
mode: int
|
83 |
+
Gain map appling mode.
|
84 |
+
|
85 |
+
Returns:
|
86 |
+
img_out: np.ndarray
|
87 |
+
Output image with shape (h, w, 3)
|
88 |
+
"""
|
89 |
+
image = image / scale_ratio
|
90 |
+
|
91 |
+
r = image[:, :, 0]
|
92 |
+
g = image[:, :, 1]
|
93 |
+
b = image[:, :, 2]
|
94 |
+
|
95 |
+
lumin = 0.2126729 * r + 0.7151522 * g + 0.0721750 * b
|
96 |
+
ld_max = 100
|
97 |
+
L, Ldetail = _ashikhmin_filtering(lumin)
|
98 |
+
maxL = L.max()
|
99 |
+
minL = L.min()
|
100 |
+
maxL_TVI = _tvi_ashikhmin(maxL)
|
101 |
+
minL_TVI = _tvi_ashikhmin(minL)
|
102 |
+
Ld = (ld_max / 100) * (_tvi_ashikhmin(L) - minL_TVI) / (maxL_TVI -
|
103 |
+
minL_TVI)
|
104 |
+
new_lumin = Ld * Ldetail
|
105 |
+
lumin[lumin <= 0] = 1
|
106 |
+
if mode == 1:
|
107 |
+
img_out = (new_lumin[:, :, np.newaxis] *
|
108 |
+
((image / lumin[:, :, np.newaxis])**s))
|
109 |
+
else:
|
110 |
+
img_out = new_lumin[:, :, np.newaxis] * (
|
111 |
+
(image / lumin[:, :, np.newaxis] - 1) * s + 1)
|
112 |
+
img_out = _remove_specials(img_out)
|
113 |
+
img_out = np.clip(img_out, 0, 1)
|
114 |
+
return img_out
|
115 |
+
|
116 |
+
|
117 |
+
def compute_y(img: np.ndarray) -> np.ndarray:
|
118 |
+
y = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2]
|
119 |
+
return y
|
120 |
+
|
121 |
+
|
122 |
+
def lmhe_global_tone_mapping(img: np.ndarray,
|
123 |
+
mu: float = 7,
|
124 |
+
bit_depth=10,
|
125 |
+
s=0.7) -> np.ndarray:
|
126 |
+
"""
|
127 |
+
Log based modified histogram equalization
|
128 |
+
Args:
|
129 |
+
img: input image, range [0, 1]
|
130 |
+
mu: parameter for lmhe
|
131 |
+
bit_depth: bit depth of tone mapping curve
|
132 |
+
protect_ratio: protect ratio of the image, the protected area will not be tone mapped
|
133 |
+
Returns:
|
134 |
+
tone mapped image
|
135 |
+
"""
|
136 |
+
y = compute_y(img)
|
137 |
+
bit_counts = 2**bit_depth
|
138 |
+
hist, bins = np.histogram(y.ravel(), bit_counts, [0, 1])
|
139 |
+
m = np.log(hist * hist.max() * (10**(-mu)) + 1) / (np.log(hist.max()**2 *
|
140 |
+
(10**(-mu)) + 1))
|
141 |
+
cdf = np.cumsum(m)
|
142 |
+
cdf_m = np.ma.masked_equal(cdf, 0)
|
143 |
+
cdf_m = (cdf_m - cdf_m.min()) / (cdf_m.max() - cdf_m.min())
|
144 |
+
cdf = np.ma.filled(cdf_m, 0)
|
145 |
+
y_new = np.interp(y.ravel(), bins[:-1], cdf)
|
146 |
+
y_new = y_new.reshape(img.shape[0], img.shape[1])
|
147 |
+
y[y == 0] = 1
|
148 |
+
img_out = y_new[:, :, np.newaxis] * (
|
149 |
+
(img / y[:, :, np.newaxis] - 1) * s + 1)
|
150 |
+
return img_out
|
PolyuColor/requirements.txt
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
ExifRead==3.0.0
|
2 |
+
numpy==1.23.4
|
3 |
+
opencv_python==4.8.1.78
|
4 |
+
Pillow==10.2.0
|
5 |
+
rawpy==0.19.0
|
6 |
+
scikit_image==0.19.3
|
7 |
+
scipy==1.12.0
|
8 |
+
tqdm==4.64.1
|
PolyuColor/resources/average_shading.png
ADDED
Git LFS Details
|
PolyuColor/resources/sid_fp32_best.ckpt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:70f38d04429c00b43637137c8210c074518f0c2a8ac33e729b1d31a7f3d8265a
|
3 |
+
size 31057848
|
PolyuColor/run.py
ADDED
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import os
|
3 |
+
import time
|
4 |
+
import warnings
|
5 |
+
from pathlib import Path
|
6 |
+
|
7 |
+
import cv2
|
8 |
+
import numpy as np
|
9 |
+
from tqdm import tqdm
|
10 |
+
|
11 |
+
from raw_prc_pipeline import (expected_img_ext, expected_landscape_img_height,
|
12 |
+
expected_landscape_img_width)
|
13 |
+
from raw_prc_pipeline.pipeline import (PipelineExecutor,
|
14 |
+
RawProcessingPipelineDemo)
|
15 |
+
from utils import fraction_from_json, json_read
|
16 |
+
|
17 |
+
warnings.filterwarnings("ignore")
|
18 |
+
|
19 |
+
|
20 |
+
def parse_args():
|
21 |
+
parser = argparse.ArgumentParser(
|
22 |
+
description=
|
23 |
+
'Demo script for processing PNG images with given metadata files.')
|
24 |
+
parser.add_argument(
|
25 |
+
'-p',
|
26 |
+
'--png_dir',
|
27 |
+
type=str,
|
28 |
+
default='data',
|
29 |
+
help='Path of the directory containing PNG images with metadata files')
|
30 |
+
parser.add_argument(
|
31 |
+
'-o',
|
32 |
+
'--out_dir',
|
33 |
+
type=str,
|
34 |
+
default=None,
|
35 |
+
help=
|
36 |
+
'Path to the directory where processed images will be saved. Images will be saved in JPG format.'
|
37 |
+
)
|
38 |
+
parser.add_argument(
|
39 |
+
'-ie',
|
40 |
+
'--illumination_estimation',
|
41 |
+
type=str,
|
42 |
+
default='gw',
|
43 |
+
help=
|
44 |
+
'Options for illumination estimation algorithms: "gw", "wp", "sog", "iwp".'
|
45 |
+
)
|
46 |
+
parser.add_argument(
|
47 |
+
'-tm',
|
48 |
+
'--tone_mapping',
|
49 |
+
type=str,
|
50 |
+
default='Storm',
|
51 |
+
help=
|
52 |
+
'Options for tone mapping algorithms: "Base", "Flash", "Storm", "Linear", "Drago", "Mantiuk", "Reinhard".'
|
53 |
+
)
|
54 |
+
parser.add_argument(
|
55 |
+
'-n',
|
56 |
+
'--denoising_flg',
|
57 |
+
action='store_false',
|
58 |
+
help=
|
59 |
+
'Denoising flag. By default resulted images will be denoised with some default parameters.'
|
60 |
+
)
|
61 |
+
parser.add_argument('-m',
|
62 |
+
'--camera_matrix',
|
63 |
+
type=float,
|
64 |
+
nargs=9,
|
65 |
+
default=[
|
66 |
+
1.06835938, -0.29882812, -0.14257812, -0.43164062,
|
67 |
+
1.35546875, 0.05078125, -0.1015625, 0.24414062,
|
68 |
+
0.5859375
|
69 |
+
],
|
70 |
+
help='Mean color matrix of Hauwei Mate 40 Pro')
|
71 |
+
args = parser.parse_args()
|
72 |
+
|
73 |
+
if args.out_dir is None:
|
74 |
+
args.out_dir = args.png_dir
|
75 |
+
|
76 |
+
return args
|
77 |
+
|
78 |
+
|
79 |
+
class PNGProcessingDemo:
|
80 |
+
def __init__(self, ie_method, tone_mapping, denoising_flg, camera_matrix,
|
81 |
+
save_dir):
|
82 |
+
self.camera_matrix = camera_matrix
|
83 |
+
self.save_dir = save_dir
|
84 |
+
self.pipeline_demo = RawProcessingPipelineDemo(
|
85 |
+
illumination_estimation=ie_method,
|
86 |
+
denoise_flg=denoising_flg,
|
87 |
+
tone_mapping=tone_mapping,
|
88 |
+
out_landscape_height=expected_landscape_img_height,
|
89 |
+
out_landscape_width=expected_landscape_img_width)
|
90 |
+
self.process_times = []
|
91 |
+
|
92 |
+
def __call__(self, png_path: Path, out_path: Path):
|
93 |
+
# parse raw img
|
94 |
+
raw_image = cv2.imread(str(png_path), cv2.IMREAD_UNCHANGED)
|
95 |
+
# parse metadata
|
96 |
+
metadata = json_read(png_path.with_suffix('.json'),
|
97 |
+
object_hook=fraction_from_json)
|
98 |
+
start_time = time.perf_counter()
|
99 |
+
# executing img pipelinex
|
100 |
+
pipeline_exec = PipelineExecutor(raw_image,
|
101 |
+
metadata,
|
102 |
+
os.path.basename(
|
103 |
+
str(png_path)).split('.')[0],
|
104 |
+
self.pipeline_demo,
|
105 |
+
save_dir=self.save_dir)
|
106 |
+
# process img
|
107 |
+
output_image = pipeline_exec()
|
108 |
+
end_time = time.perf_counter()
|
109 |
+
self.process_times.append(end_time - start_time)
|
110 |
+
|
111 |
+
# save results
|
112 |
+
output_image = cv2.cvtColor(output_image, cv2.COLOR_RGB2BGR)
|
113 |
+
cv2.imwrite(str(out_path), output_image,
|
114 |
+
[cv2.IMWRITE_JPEG_QUALITY, 100])
|
115 |
+
|
116 |
+
|
117 |
+
def main(png_dir, out_dir, illumination_estimation, tone_mapping,
|
118 |
+
denoising_flg, camera_matrix):
|
119 |
+
png_dir = Path(png_dir)
|
120 |
+
out_dir = Path(out_dir)
|
121 |
+
out_dir.mkdir(exist_ok=True)
|
122 |
+
|
123 |
+
png_paths = list(png_dir.glob('*.png'))
|
124 |
+
out_paths = [
|
125 |
+
out_dir / png_path.with_suffix(expected_img_ext).name
|
126 |
+
for png_path in png_paths
|
127 |
+
]
|
128 |
+
|
129 |
+
png_processor = PNGProcessingDemo(illumination_estimation, tone_mapping,
|
130 |
+
denoising_flg, camera_matrix,
|
131 |
+
str(out_dir))
|
132 |
+
|
133 |
+
for png_path, out_path in tqdm(zip(png_paths, out_paths),
|
134 |
+
total=len(png_paths)):
|
135 |
+
png_processor(png_path, out_path)
|
136 |
+
print("Average processing time: {:.2f}s".format(
|
137 |
+
np.mean(png_processor.process_times)))
|
138 |
+
|
139 |
+
|
140 |
+
if __name__ == '__main__':
|
141 |
+
args = parse_args()
|
142 |
+
main(**vars(args))
|
PolyuColor/run.sh
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env bash
|
2 |
+
python run.py -p data
|
PolyuColor/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 |
+
from .image_utils import *
|
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 |
+
|
PolyuColor/utils/image_utils.py
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
+
import numpy as np
|
3 |
+
import torch
|
4 |
+
|
5 |
+
|
6 |
+
def save_img(img, name, gamma=False):
|
7 |
+
if gamma:
|
8 |
+
img = np.power(img, 1/2.2)
|
9 |
+
img = np.clip(img, 0, 1)
|
10 |
+
img = (img * 65535).astype(np.uint16)
|
11 |
+
if img.ndim == 3:
|
12 |
+
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
13 |
+
cv2.imwrite(name, img)
|
14 |
+
|
15 |
+
|
16 |
+
def compute_y(img: np.ndarray) -> np.ndarray:
|
17 |
+
y = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2]
|
18 |
+
return y
|
19 |
+
|
20 |
+
|
21 |
+
def compute_raw_y(img: np.ndarray) -> np.ndarray:
|
22 |
+
g1 = img[..., 1]
|
23 |
+
g2 = img[..., 2]
|
24 |
+
ret = (g1 + g2) / 2
|
25 |
+
return ret
|
26 |
+
|
27 |
+
|
28 |
+
def pack_raw(im):
|
29 |
+
# pack Bayer image to 4 channels
|
30 |
+
if isinstance(im, torch.Tensor):
|
31 |
+
im = torch.unsqueeze(im, dim=-1)
|
32 |
+
img_shape = im.shape
|
33 |
+
H = img_shape[0]
|
34 |
+
W = img_shape[1]
|
35 |
+
|
36 |
+
out = torch.cat((im[0:H:2, 0:W:2, :], im[0:H:2, 1:W:2, :],
|
37 |
+
im[1:H:2, 1:W:2, :], im[1:H:2, 0:W:2, :]),
|
38 |
+
dim=-1)
|
39 |
+
elif isinstance(im, np.ndarray):
|
40 |
+
im = np.expand_dims(im, axis=-1)
|
41 |
+
img_shape = im.shape
|
42 |
+
H = img_shape[0]
|
43 |
+
W = img_shape[1]
|
44 |
+
|
45 |
+
out = np.concatenate((im[0:H:2, 0:W:2, :], im[0:H:2, 1:W:2, :],
|
46 |
+
im[1:H:2, 1:W:2, :], im[1:H:2, 0:W:2, :]),
|
47 |
+
axis=-1)
|
48 |
+
return out
|
49 |
+
|
50 |
+
|
51 |
+
def depack_raw(im):
|
52 |
+
# unpack 4 channels to Bayer image
|
53 |
+
img_shape = im.shape
|
54 |
+
H = img_shape[0]
|
55 |
+
W = img_shape[1]
|
56 |
+
if isinstance(im, torch.Tensor):
|
57 |
+
output = torch.zeros((H * 2, W * 2), dtype=im.dtype)
|
58 |
+
elif isinstance(im, np.ndarray):
|
59 |
+
output = np.zeros((H * 2, W * 2), dtype=im.dtype)
|
60 |
+
img_shape = output.shape
|
61 |
+
H = img_shape[0]
|
62 |
+
W = img_shape[1]
|
63 |
+
|
64 |
+
output[0:H:2, 0:W:2] = im[:, :, 0]
|
65 |
+
output[0:H:2, 1:W:2] = im[:, :, 1]
|
66 |
+
output[1:H:2, 1:W:2] = im[:, :, 2]
|
67 |
+
output[1:H:2, 0:W:2] = im[:, :, 3]
|
68 |
+
|
69 |
+
return output
|
PolyuColor/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')
|