-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathhdready.py
More file actions
213 lines (173 loc) · 7.69 KB
/
hdready.py
File metadata and controls
213 lines (173 loc) · 7.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
from math import exp, floor
import os
import fire
from PIL import Image
from os import listdir
from os.path import isfile, join, dirname
from multiprocessing import Pool
class Images_stack():
"""A class to represent a stack of images to merge"""
def __init__(self, img_list:list, stdDeviation:int, processes:int):
"""
Constructs all the necessary attributes for the stack object
Args:
img_list (list): list containing PIL Image objects
stdDeviation (int): parameter of the Gaussian functions directly
impacting the final dynamic range
processes (int): number of processes used for multiprocessing
"""
self.stack = img_list
self.stdDeviation = stdDeviation
self.height = self.stack[0].height
self.width = self.stack[0].width
self.processes = processes
self.parts = [(x * floor(self.width / self.processes), 0,
(x + 1) * floor(self.width / self.processes), self.height)
for x in range(self.processes - 1)]
self.parts += [((self.processes - 1) *
floor(self.width / self.processes),
0, self.width, self.height)]
def images_crop(self):
"""
This function cuts the images stack into processes ones
Return:
(list): list containing Divided_images_stack objects
"""
if self.processes == 1:
return self
return [Divided_images_stack([img.crop(self.parts[x])
for img in self.stack],
self.stdDeviation, self.parts, self.width,
self.height) for x in range(self.processes)]
class Divided_images_stack():
"""A class of pieces of images stack"""
def __init__(self, img_list:list, stdDeviation:int, parts:list,
final_width:int, final_height:int):
"""
Constructs all the necessary attributes for the divided images
stack object
Args:
img_list (list): list containing PIL Image objects
stdDeviation (int): parameter of the Gaussian functions directly
impacting the final dynamic range
parts (list): list containing tuples of coordinates defining
a piece of a divided image
final_width (int): width of the original and final images
final_height (int): height of the original and final images
"""
self.stack = img_list
self.stdDeviation = stdDeviation
self.parts = parts
self.width = self.stack[0].width
self.height = self.stack[0].height
self.final_width = final_width
self.final_height = final_height
self.coefficients = {value: round((exp( - ((value - 127) ** 2) /
(2 * self.stdDeviation ** 2))), 6)
for value in range(0, 256)}
def exposition_measure(self, channels:tuple):
"""
This function measures the exposition of a given pixel
and apply a Gauss curve to compute and return the weight
Arg:
channels (tuple): tuple that contains red, green and blue channel
Return:
coeff (int): exposure weight of the pixel
"""
weight = 1
for x in range(3):
weight *= self.coefficients[channels[x]]
return weight
def generate_new_image(self):
"""
This function merges the images into a new one
Return:
finalImage (PIL Image object): a part of the final image
"""
finalImage = Image.new(mode="RGB", size=(
self.final_width, self.final_height))
for y in range(self.height):
for x in range(self.width):
newPixel = [0, 0, 0]
# Compute coefficients
pixels = [image.getpixel((x, y)) for image in self.stack]
coefficient = [self.exposition_measure(rgb) for rgb in pixels]
sum_coeff = sum(coefficient)
# Normalization of coefficient values
coefficient = [c / sum_coeff if sum_coeff != 0
else c for c in coefficient]
# Merging
for i in range(len(coefficient)):
for n in range(3):
newPixel[n] += pixels[i][n] * coefficient[i]
# Rounding values
newPixel = tuple([round(a) for a in newPixel])
# Write new rgb values into the final image
finalImage.putpixel((x, y), newPixel)
return finalImage
def reassemble(self):
"""
This function reassembles the parts of the final image
Return:
(Image object): final image
"""
finalImage = Image.new(mode="RGB",
size=(self.final_width, self.final_height))
for x in range(len(self.stack)):
finalImage.paste(self.stack[x], self.parts[x][:2])
return finalImage
def open_images(imagesFolderPath:str):
"""This function open and store the images to merge into a list
Args:
imagesFolderPath (str): path of the folder containing the images
e.g. 'C:/Users/John/myImage/bracketed_images_bridge/'
"""
images = []
imagesPath = [
f for f in listdir(imagesFolderPath)
if isfile(join(imagesFolderPath, f))]
images = [Image.open(os.path.join(imagesFolderPath, name))
for name in imagesPath]
return images
def hdr(imagesToMerge:Divided_images_stack):
"""Function called by pool"""
return imagesToMerge.generate_new_image()
def start(imgFolderPath:str, finalPath:str, stdDeviation:int, processes:int):
"""
Main function that launches the HDR process
Args:
imgFolderPath (str): path to the folder containing the images to merge
finalPath (str): path to the final image file
(like "C:/user/my_hdry_photo/hdr_edited.jpg")
stdDeviation (int): parameter of the Gaussian functions directly
impacting the final dynamic range
processes (int): number of processes used for multiprocessing
"""
if __name__ == '__main__':
# Check if the folder containing the images exists or not
if os.path.isdir(imgFolderPath):
# Opening of the images to merge
images = open_images(imgFolderPath)
else:
raise FileNotFoundError
# Check if the folder contains images:
if len(os.listdir(imgFolderPath)) == 0:
raise Exception("There is no image in the folder")
# Check if the final directory exists and create it if it doesn't
if not os.path.exists(os.path.dirname(finalPath)):
os.makedirs(os.path.dirname(finalPath))
# Creating the images stack
stack = Images_stack(images, stdDeviation, processes)
# Dividing the images stack into processes ones
divided_stack = stack.images_crop()
# HDR merging
with Pool(processes) as p:
results = Divided_images_stack(p.map(hdr, divided_stack),
stack.stdDeviation, stack.parts,
stack.width, stack.height)
# Reassembling the pieces of the final image
final_image = results.reassemble()
final_image.save(finalPath)
print('HDR merging process completed')
# Setting up the CLI application
fire.Fire(start)