Artyom commited on
Commit
00c3521
1 Parent(s): f8d6c27

polyucolor

Browse files
.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

  • SHA256: b0e613647b65bb8ed66634743230df38bf1731a3025a5dfcca695438df854ab0
  • Pointer size: 133 Bytes
  • Size of remote file: 81.3 MB
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')