├── .gitignore ├── .idea └── vcs.xml ├── LICENSE ├── Pyramid_ref.pdf ├── README.md ├── exposure_fusion.pdf ├── image.py ├── image_set ├── jpeg │ ├── flower.jpeg │ ├── t_0_1 │ ├── t_1_1 │ ├── t_2_1 │ ├── t_3_1 │ ├── t_4_1 │ ├── t_5_1 │ ├── t_6_1 │ ├── t_7_1 │ ├── t_8_1 │ └── t_9_1 ├── mask │ ├── mask_mean.jpg │ ├── mask_over.jpg │ └── mask_under.jpg └── raw │ ├── t_0_0 │ ├── t_1_0 │ ├── t_2_0 │ ├── t_3_0 │ ├── t_4_0 │ ├── t_5_0 │ ├── t_6_0 │ ├── t_7_0 │ ├── t_8_0 │ └── t_9_0 ├── laplacianfusion.py ├── list_images.txt ├── list_jpeg.txt ├── list_raw.txt ├── main.py ├── naivefusion.py ├── res ├── lap_mask_6.jpg └── mask_naive.jpg ├── result_jpeg_exposure.png ├── result_jpeg_exposure_mask.jpg ├── result_jpeg_naive.png ├── result_jpeg_naive_mask.png └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | .spyderworkspace 88 | 89 | # Rope project settings 90 | .ropeproject 91 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Rachid Riad 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. 22 | -------------------------------------------------------------------------------- /Pyramid_ref.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/Pyramid_ref.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Exposure Fusion 2 | Exposure Fusion Technique 3 | This code implements the Exposure Fusion, a Low-Dynamic-Range technique. It blends multi-exposure sequence of photo into a high-quality image, and is guided by measurement as Contrast, Saturation and Well-exposedness. 4 | 5 | You can find the Research paper where the algorithm comes from [here](https://github.com/Rachine/ExposureFusion/blob/master/exposure_fusion.pdf) [1] 6 | 7 | This code used python 2.7 and the packages Numpy and SciPy. 8 | 9 | Here some multi-exposure sequence of photo used: 10 | 11 | 12 | 13 | Here the result with the Naive implementation: 14 | 15 | 16 | 17 | Here the result with the Exposure Fusion algorithm with the Laplacian Pyramid: 18 | 19 | 20 | 21 | ### Usage 22 | 23 | To test the code with your own images, put them in a new folder under the folder _image\_set_ and edit the text file _list\_images.txt_ by putting the names of your images. 24 | Then run _main.py_ with the desired arguments. You can see all the possible arguments by running: `python main.py -h` 25 | 26 | ### Authors 27 | - Chaïmaa Kadaoui 28 | - [Rachid Riad](https://rachine.github.io/) 29 | 30 | 31 | ### References 32 | [1]: Exposure Fusion: A Simple and Practical Alternative to High Dynamic Range Photography 33 | Mertens, T.; Kautz, J.; Van Reeth, F. 34 | 35 | [2]: P. Burt and T. Adelson. The Laplacian Pyramid as a Com- 36 | pact Image Code. IEEE Transactions on Communication, 37 | COM-31:532–540, 1983. 38 | -------------------------------------------------------------------------------- /exposure_fusion.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/exposure_fusion.pdf -------------------------------------------------------------------------------- /image.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os.path 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | from scipy import ndimage, misc 7 | import pdb 8 | 9 | 10 | def weightedAverage(pixel): 11 | return 0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2] 12 | 13 | 14 | def exponential_euclidean(canal, sigma): 15 | return np.exp(-(canal - 0.5)**2 / (2 * sigma**2)) 16 | 17 | 18 | def show(color_array): 19 | """ Function to show image""" 20 | plt.imshow(color_array) 21 | plt.show() 22 | plt.axis('off') 23 | 24 | 25 | def show_gray(gray_array): 26 | """ Function to show grayscale image""" 27 | fig = plt.figure() 28 | plt.imshow(gray_array, cmap=plt.cm.Greys_r) 29 | plt.show() 30 | plt.axis('off') 31 | 32 | 33 | class Image(object): 34 | """Class for Image""" 35 | 36 | def __init__(self, fmt, path, crop=False, n=0): 37 | self.path = os.path.join("image_set", fmt, str(path)) 38 | self.fmt = fmt 39 | self.array = misc.imread(self.path) 40 | self.array = self.array.astype(np.float32) / 255 41 | if crop: 42 | self.crop_image(n) 43 | self.shape = self.array.shape 44 | 45 | def crop_image(self, n): 46 | resolution = 2**n 47 | (height, width, _) = self.array.shape 48 | (max_height, max_width) = (resolution * (height // resolution), 49 | resolution * (width // resolution)) 50 | (begin_height, begin_width) = ((height - max_height) / 2, 51 | (width - max_width) / 2) 52 | self.array = self.array[begin_height:max_height + begin_height, 53 | begin_width:max_width + begin_width] 54 | 55 | @property 56 | def grayScale(self): 57 | """Grayscale image""" 58 | rgb = self.array 59 | self._grayScale = np.dot(rgb[..., :3], [0.299, 0.587, 0.114]) 60 | return self._grayScale 61 | 62 | def saturation(self): 63 | """Function that returns the Saturation map""" 64 | red_canal = self.array[:, :, 0] 65 | green_canal = self.array[:, :, 1] 66 | blue_canal = self.array[:, :, 2] 67 | mean = (red_canal + green_canal + blue_canal) / 3.0 68 | saturation = np.sqrt(((red_canal - mean)**2 + (green_canal - mean)**2 + 69 | (blue_canal - mean)**2) / 3) 70 | return saturation 71 | 72 | def contrast(self): 73 | """Function that returns the Constrast numpy array""" 74 | grey = self.grayScale 75 | contrast = np.zeros((self.shape[0], self.shape[1])) 76 | grey_extended = np.zeros((self.shape[0] + 2, self.shape[1] + 2)) 77 | grey_extended[1:self.shape[0] + 1, 1:self.shape[1] + 1] = grey 78 | # kernel = np.array([[ -1, -1, -1 ], 79 | # [ -1, 8, -1 ], 80 | # [ -1, -1, -1 ]]) 81 | kernel = np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]]) 82 | for row in range(self.shape[0]): 83 | for col in range(self.shape[1]): 84 | contrast[row][col] = np.abs( 85 | (kernel * 86 | grey_extended[row:(row + 3), col:(col + 3)]).sum()) 87 | contrast = (contrast - np.min(contrast)) 88 | contrast = contrast / np.max(contrast) 89 | return contrast 90 | 91 | def sobel(self): 92 | """Function that returns the Constrast numpy array""" 93 | grey = self.grayScale 94 | sobel_h = np.zeros((self.shape[0], self.shape[1])) 95 | sobel_v = np.zeros((self.shape[0], self.shape[1])) 96 | grey_extended = np.zeros((self.shape[0] + 2, self.shape[1] + 2)) 97 | grey_extended[1:self.shape[0] + 1, 1:self.shape[1] + 1] = grey 98 | kernel1 = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]]) 99 | kernel2 = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, -1]]) 100 | for row in range(self.shape[0]): 101 | for col in range(self.shape[1]): 102 | sobel_h[row][col] = np.abs( 103 | (kernel1 * 104 | grey_extended[row:(row + 3), col:(col + 3)]).sum()) 105 | sobel_v[row][col] = np.abs( 106 | (kernel2 * 107 | grey_extended[row:(row + 3), col:(col + 3)]).sum()) 108 | return sobel_h, sobel_v 109 | 110 | def exposedness(self): 111 | """Function that returns the Well-Exposedness map""" 112 | red_canal = self.array[:, :, 0] 113 | green_canal = self.array[:, :, 1] 114 | blue_canal = self.array[:, :, 2] 115 | sigma = 0.2 116 | red_exp = exponential_euclidean(red_canal, sigma) 117 | green_exp = exponential_euclidean(green_canal, sigma) 118 | blue_exp = exponential_euclidean(blue_canal, sigma) 119 | return red_exp * green_exp * blue_exp 120 | 121 | 122 | if __name__ == "__main__": 123 | im = Image("jpeg", "grandcanal_mean.jpg") 124 | sat = im.contrast() 125 | show_gray(sat) 126 | -------------------------------------------------------------------------------- /image_set/jpeg/flower.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/flower.jpeg -------------------------------------------------------------------------------- /image_set/jpeg/t_0_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/t_0_1 -------------------------------------------------------------------------------- /image_set/jpeg/t_1_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/t_1_1 -------------------------------------------------------------------------------- /image_set/jpeg/t_2_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/t_2_1 -------------------------------------------------------------------------------- /image_set/jpeg/t_3_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/t_3_1 -------------------------------------------------------------------------------- /image_set/jpeg/t_4_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/t_4_1 -------------------------------------------------------------------------------- /image_set/jpeg/t_5_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/t_5_1 -------------------------------------------------------------------------------- /image_set/jpeg/t_6_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/t_6_1 -------------------------------------------------------------------------------- /image_set/jpeg/t_7_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/t_7_1 -------------------------------------------------------------------------------- /image_set/jpeg/t_8_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/t_8_1 -------------------------------------------------------------------------------- /image_set/jpeg/t_9_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/jpeg/t_9_1 -------------------------------------------------------------------------------- /image_set/mask/mask_mean.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/mask/mask_mean.jpg -------------------------------------------------------------------------------- /image_set/mask/mask_over.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/mask/mask_over.jpg -------------------------------------------------------------------------------- /image_set/mask/mask_under.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/mask/mask_under.jpg -------------------------------------------------------------------------------- /image_set/raw/t_0_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/raw/t_0_0 -------------------------------------------------------------------------------- /image_set/raw/t_1_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/raw/t_1_0 -------------------------------------------------------------------------------- /image_set/raw/t_2_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/raw/t_2_0 -------------------------------------------------------------------------------- /image_set/raw/t_3_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/raw/t_3_0 -------------------------------------------------------------------------------- /image_set/raw/t_4_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/raw/t_4_0 -------------------------------------------------------------------------------- /image_set/raw/t_5_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/raw/t_5_0 -------------------------------------------------------------------------------- /image_set/raw/t_6_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/raw/t_6_0 -------------------------------------------------------------------------------- /image_set/raw/t_7_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/raw/t_7_0 -------------------------------------------------------------------------------- /image_set/raw/t_8_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/raw/t_8_0 -------------------------------------------------------------------------------- /image_set/raw/t_9_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/image_set/raw/t_9_0 -------------------------------------------------------------------------------- /laplacianfusion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Sat Dec 10 16:26:15 2016 4 | 5 | @author: Rachid & Chaima 6 | """ 7 | 8 | import numpy as np 9 | import matplotlib.pyplot as plt 10 | from scipy import ndimage, misc 11 | import image 12 | import utils 13 | import pdb 14 | 15 | # 16 | #def div0( a, b ): 17 | # """ ignore / 0, div0( [-1, 0, 1], 0 ) -> [0, 0, 0] """ 18 | # with np.errstate(divide='ignore', invalid='ignore'): 19 | # c = np.true_divide( a, b ) 20 | # c[ ~ np.isfinite( c )] = 0 21 | # return c 22 | 23 | 24 | class LaplacianMap(object): 25 | """Class for weights attribution with Laplacian Fusion""" 26 | 27 | def __init__(self, fmt, names, n=3): 28 | """names is a liste of names, fmt is the format of the images""" 29 | self.images = [] 30 | for name in names: 31 | self.images.append(image.Image(fmt, name, crop=True, n=n)) 32 | self.shape = self.images[0].shape 33 | self.num_images = len(self.images) 34 | self.height_pyr = n 35 | 36 | def get_weights_map(self, w_c, w_s, w_e): 37 | """Return the normalized Weight map""" 38 | self.weights = [] 39 | sums = np.zeros((self.shape[0], self.shape[1])) 40 | for image_name in self.images: 41 | contrast = image_name.contrast() 42 | saturation = image_name.saturation() 43 | exposedness = image_name.exposedness() 44 | weight = (contrast**w_c) * (saturation**w_s) * (exposedness** 45 | w_e) + 1e-12 46 | self.weights.append(weight) 47 | sums = sums + weight 48 | for index in range(self.num_images): 49 | self.weights[index] = self.weights[index] / sums 50 | return self.weights 51 | 52 | def get_gaussian_pyramid(self, image, n): 53 | """Return the Gaussian Pyramid of an image""" 54 | gaussian_pyramid_floors = [image] 55 | for floor in range(1, n): 56 | gaussian_pyramid_floors.append( 57 | utils.Reduce(gaussian_pyramid_floors[-1], 1)) 58 | return gaussian_pyramid_floors 59 | 60 | def get_gaussian_pyramid_weights(self): 61 | """Return the Gaussian Pyramid of the Weight map of all images""" 62 | self.weights_pyramid = [] 63 | for index in range(self.num_images): 64 | self.weights_pyramid.append( 65 | self.get_gaussian_pyramid(self.weights[index], 66 | self.height_pyr)) 67 | return self.weights_pyramid 68 | 69 | def get_laplacian_pyramid(self, image, n): 70 | """Return the Laplacian Pyramid of an image""" 71 | gaussian_pyramid_floors = self.get_gaussian_pyramid(image, n) 72 | laplacian_pyramid_floors = [gaussian_pyramid_floors[-1]] 73 | for floor in range(n - 2, -1, -1): 74 | new_floor = gaussian_pyramid_floors[floor] - utils.Expand( 75 | gaussian_pyramid_floors[floor + 1], 1) 76 | laplacian_pyramid_floors = [new_floor] + laplacian_pyramid_floors 77 | return laplacian_pyramid_floors 78 | 79 | def get_laplacian_pyramid_images(self): 80 | """Return all the Laplacian pyramid for all images""" 81 | self.laplacian_pyramid = [] 82 | for index in range(self.num_images): 83 | self.laplacian_pyramid.append( 84 | self.get_laplacian_pyramid(self.images[index].array, 85 | self.height_pyr)) 86 | return self.laplacian_pyramid 87 | 88 | def result_exposure(self, w_c=1, w_s=1, w_e=1): 89 | "Return the Exposure Fusion image with Laplacian/Gaussian Fusion method" 90 | print "weights" 91 | self.get_weights_map(w_c, w_s, w_e) 92 | print "gaussian pyramid" 93 | self.get_gaussian_pyramid_weights() 94 | print "laplacian pyramid" 95 | self.get_laplacian_pyramid_images() 96 | result_pyramid = [] 97 | for floor in range(self.height_pyr): 98 | print 'floor ', floor 99 | result_floor = np.zeros(self.laplacian_pyramid[0][floor].shape) 100 | for index in range(self.num_images): 101 | print 'image ', index 102 | for canal in range(3): 103 | result_floor[:, :, 104 | canal] += self.laplacian_pyramid[index][floor][:, :, 105 | canal] * self.weights_pyramid[index][floor] 106 | result_pyramid.append(result_floor) 107 | # Get the image from the Laplacian pyramid 108 | self.result_image = result_pyramid[-1] 109 | for floor in range(self.height_pyr - 2, -1, -1): 110 | print 'floor ', floor 111 | self.result_image = result_pyramid[floor] + utils.Expand( 112 | self.result_image, 1) 113 | self.result_image[self.result_image < 0] = 0 114 | self.result_image[self.result_image > 1] = 1 115 | return self.result_image 116 | 117 | 118 | if __name__ == "__main__": 119 | names = [line.rstrip('\n') for line in open('list_images.txt')] 120 | lap = LaplacianMap('arno', names, n=6) 121 | res = lap.result_exposure(1, 1, 1) 122 | image.show(res) 123 | misc.imsave("res/arno_3.jpg", res) 124 | -------------------------------------------------------------------------------- /list_images.txt: -------------------------------------------------------------------------------- 1 | mask_under.jpg 2 | mask_over.jpg 3 | mask_mean.jpg -------------------------------------------------------------------------------- /list_jpeg.txt: -------------------------------------------------------------------------------- 1 | t_0_1 2 | t_1_1 3 | t_2_1 4 | t_3_1 5 | t_4_1 6 | t_5_1 7 | t_6_1 8 | t_7_1 9 | t_8_1 10 | t_9_1 11 | -------------------------------------------------------------------------------- /list_raw.txt: -------------------------------------------------------------------------------- 1 | t_0_0 2 | t_1_0 3 | t_2_0 4 | t_3_0 5 | t_4_0 6 | t_5_0 7 | t_6_0 8 | t_7_0 9 | t_8_0 10 | t_9_0 11 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import argparse 4 | import image 5 | import naivefusion 6 | import laplacianfusion 7 | 8 | # Loading the arguments 9 | 10 | parser = argparse.ArgumentParser( 11 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 12 | parser.add_argument( 13 | '-l', 14 | '--list', 15 | dest='names', 16 | type=str, 17 | default='list_images.txt', 18 | help='The text file which contains the names of the images') 19 | parser.add_argument( 20 | '-f', 21 | '--folder', 22 | dest='folder', 23 | type=str, 24 | required=True, 25 | help='The folder containing the images') 26 | parser.add_argument( 27 | '-hp', 28 | '--heightpyr', 29 | dest='height_pyr', 30 | type=int, 31 | default=6, 32 | help='The height of the Laplacian pyramid') 33 | parser.add_argument( 34 | '-wc', 35 | dest='w_c', 36 | type=float, 37 | default=1.0, 38 | help='Exponent of the contrast') 39 | parser.add_argument( 40 | '-ws', 41 | dest='w_s', 42 | type=float, 43 | default=1.0, 44 | help='Exponent of the saturation') 45 | parser.add_argument( 46 | '-we', 47 | dest='w_e', 48 | type=float, 49 | default=1.0, 50 | help='Exponent of the exposedness') 51 | args = parser.parse_args() 52 | params = vars(args) # convert to ordinary dict 53 | 54 | names = [line.rstrip('\n') for line in open(params['names'])] 55 | folder = params['folder'] 56 | height_pyr = params['height_pyr'] 57 | w_c = params['w_c'] 58 | w_s = params['w_s'] 59 | w_e = params['w_e'] 60 | 61 | # Naive Fusion 62 | 63 | W = naivefusion.WeightsMap(folder, names) 64 | res_naive = W.result_exposure(w_c, w_s, w_e) 65 | image.show(res_naive) 66 | 67 | # Laplacian Fusion 68 | 69 | lap = laplacianfusion.LaplacianMap(folder, names, n=height_pyr) 70 | res_lap = lap.result_exposure(w_c, w_s, w_e) 71 | image.show(res_lap) 72 | -------------------------------------------------------------------------------- /naivefusion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | from scipy import ndimage, misc 6 | import image 7 | 8 | #def div0( a, b ): 9 | # """ ignore / 0, div0( [-1, 0, 1], 0 ) -> [0, 0, 0] """ 10 | # with np.errstate(divide='ignore', invalid='ignore'): 11 | # c = np.true_divide( a, b ) 12 | # c[ ~ np.isfinite( c )] = 0 13 | # return c 14 | 15 | 16 | class WeightsMap(object): 17 | """Class for weights attribution for all images""" 18 | 19 | def __init__(self, fmt, names): 20 | """names is a liste of names, fmt is the format of the images""" 21 | self.images = [] 22 | for name in names: 23 | self.images.append(image.Image(fmt, name)) 24 | self.shape = self.images[0].shape 25 | self.num_images = len(self.images) 26 | 27 | def get_weights_map(self, w_c, w_s, w_e): 28 | """Return the normalized Weight map""" 29 | self.weights = [] 30 | sums = np.zeros((self.shape[0], self.shape[1])) 31 | for image_name in self.images: 32 | contrast = image_name.contrast() 33 | saturation = image_name.saturation() 34 | exposedness = image_name.exposedness() 35 | weight = (contrast**w_c)*(saturation**w_s)*(exposedness**w_e) + 1e-12 36 | # weight = ndimage.gaussian_filter(weight, sigma=(3, 3), order=0) 37 | self.weights.append(weight) 38 | sums = sums + weight 39 | for index in range(self.num_images): 40 | self.weights[index] = self.weights[index]/sums 41 | return self.weights 42 | 43 | def result_exposure(self, w_c=1, w_s=1, w_e=1): 44 | "Return the Exposure Fusion image with Naive method" 45 | self.get_weights_map(w_c, w_s, w_e) 46 | self.result_image = np.zeros(self.shape) 47 | for canal in range(3): 48 | for index in range(self.num_images): 49 | self.result_image[:, :, canal] += self.weights[index] * self.images[index].array[:, :, canal] 50 | self.result_image[self.result_image < 0] = 0 51 | self.result_image[self.result_image > 1] = 1 52 | return self.result_image 53 | 54 | if __name__ == "__main__": 55 | names = [line.rstrip('\n') for line in open('list_jpeg_test.txt')] 56 | W = WeightsMap("mask", names) 57 | im = W.result_exposure(1, 1, 1) 58 | image.show(im) 59 | misc.imsave("res/mask_naive.jpg", im) 60 | -------------------------------------------------------------------------------- /res/lap_mask_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/res/lap_mask_6.jpg -------------------------------------------------------------------------------- /res/mask_naive.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/res/mask_naive.jpg -------------------------------------------------------------------------------- /result_jpeg_exposure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/result_jpeg_exposure.png -------------------------------------------------------------------------------- /result_jpeg_exposure_mask.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/result_jpeg_exposure_mask.jpg -------------------------------------------------------------------------------- /result_jpeg_naive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/result_jpeg_naive.png -------------------------------------------------------------------------------- /result_jpeg_naive_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rachine/ExposureFusion/cc830224331270cfe545c68d27c8abac013762af/result_jpeg_naive_mask.png -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Sat Dec 10 16:32:39 2016 4 | 5 | @author: Rachid & Chaima 6 | """ 7 | import pdb 8 | 9 | import numpy as np 10 | import scipy.signal as sig 11 | 12 | 13 | def kernel_1D(n, a=0.6): 14 | """Kernel function in 1 dimension""" 15 | kernel = [.0625, .25, .375, .25, .0625] 16 | # if n == 0: 17 | # return a 18 | # elif n == -1 or n == 1: 19 | # return 1./4 20 | # elif n == -2 or n == 2: 21 | # return 1./4 - float(a)/2 22 | # else: 23 | # return 0 24 | return kernel[n] 25 | 26 | 27 | def kernel_old(m, n, a=0.6): 28 | """Returns the value of the kernel at position w and n""" 29 | return kernel_1D(m, a)*kernel_1D(n, a) 30 | 31 | 32 | def get_kernel(a=0.6): 33 | kernel = np.zeros((5,5)) 34 | for i in range(5): 35 | for j in range(5): 36 | kernel[i, j] = kernel_1D(i, a)*kernel_1D(j, a) 37 | return kernel 38 | 39 | 40 | def Reduce_old(image, n, a=0.6): 41 | """Reduce function for Pyramids""" 42 | try: 43 | if n == 0: 44 | return image 45 | else: 46 | image = Reduce(image, n-1, a) 47 | [R, C] = [image.shape[0], image.shape[1]] 48 | image_extended = np.zeros((R+4, C+4)) 49 | image_extended[2:R+2, 2:C+2] = image 50 | try: 51 | image_reduced = np.zeros((R/2, C/2)) 52 | except Exception as e: 53 | print "Dimension Error" 54 | print e 55 | 56 | for i in range(R/2): 57 | for j in range(C/2): 58 | for m in range(-2, 3): 59 | for n in range(-2, 3): 60 | image_reduced[i, j] += kernel_old(m, n) * image_extended[2 * i + m + 2, 2 * j + n + 2] 61 | return image_reduced 62 | except Exception as e: 63 | print "Dimension Error" 64 | print e 65 | 66 | 67 | def weighted_sum(image, i, j, a): 68 | weighted_sum = 0 69 | for m in range(-2, 3): 70 | for n in range(-2, 3): 71 | pixel_i = float(i - m) / 2 72 | pixel_j = float(j - n) / 2 73 | if pixel_i.is_integer() and pixel_j.is_integer(): 74 | weighted_sum += kernel_old(m, n, a) * image[pixel_i, pixel_j] 75 | return 4 * weighted_sum 76 | 77 | 78 | def Expand_old(image, n, a=0.6): 79 | """Expand function for Pyramids""" 80 | try: 81 | if n == 0: 82 | return image 83 | else: 84 | image = Expand(image, n-1, a) 85 | [R, C] = image.shape 86 | image_extended = np.zeros((R+4, C+4)) 87 | image_extended[2:R+2, 2:C+2] = image 88 | new_floor = np.zeros((2 * R, 2 * C)) 89 | for i in range(2 * R): 90 | for j in range(2 * C): 91 | new_floor[i, j] = weighted_sum(image_extended, i+2, j+2, a) 92 | new_floor = (new_floor - np.min(new_floor)) 93 | new_floor = new_floor / np.max(new_floor) 94 | return new_floor 95 | except Exception as e: 96 | print "Dimension error" 97 | print e 98 | 99 | 100 | def Reduce1(image, a=0.6): 101 | kernel = get_kernel(a) 102 | shape = image.shape 103 | if len(shape) == 3: 104 | image_reduced = np.zeros((shape[0]/2, shape[1]/2, 3)) 105 | for canal in range(3): 106 | canal_reduced = sig.convolve2d(image[:, :, canal], kernel, 'same') 107 | image_reduced[:, :, canal] = canal_reduced[::2, ::2] 108 | else: 109 | image_reduced = sig.convolve2d(image, kernel, 'same')[::2, ::2] 110 | return image_reduced 111 | 112 | 113 | def Reduce(image, n, a=0.6): 114 | """Reduce function for Pyramids""" 115 | try: 116 | if n == 0: 117 | return image 118 | else: 119 | image = Reduce(image, n-1, a) 120 | return Reduce1(image, a) 121 | except Exception as e: 122 | print "Dimension Error" 123 | print e 124 | 125 | 126 | def Expand1(image, a=0.6): 127 | kernel = get_kernel(a) 128 | shape = image.shape 129 | if len(shape) == 3: 130 | image_to_expand = np.zeros((2*shape[0], 2*shape[1], 3)) 131 | image_expanded = np.zeros(image_to_expand.shape) 132 | for canal in range(3): 133 | image_to_expand[::2, ::2, canal] = image[:, :, canal] 134 | image_expanded[:, :, canal] = sig.convolve2d(image_to_expand[:, :, canal], 4*kernel, 'same') 135 | else: 136 | image_to_expand = np.zeros((2 * shape[0], 2 * shape[1])) 137 | image_to_expand[::2, ::2] = image 138 | image_expanded = sig.convolve2d(image_to_expand[:, :], 4*kernel, 'same') 139 | return image_expanded 140 | 141 | 142 | def Expand(image, n, a=0.6): 143 | """Expand function for Pyramids""" 144 | try: 145 | if n == 0: 146 | return image 147 | else: 148 | image = Expand(image, n-1, a) 149 | return Expand1(image, a) 150 | except Exception as e: 151 | print "Dimension error" 152 | print e 153 | --------------------------------------------------------------------------------