├── images
├── 1_4.png
├── 32_1.png
├── 0_Calib_Chapel_CRF0.jpg
└── 0_Calib_Chapel_local_CRF0.jpg
├── load_images.py
├── compute_irradiance.py
├── gsolve.py
├── README.md
├── hdr_debevec.py
├── run_hdr_image.py
├── tonemap.py
└── localtonemap
├── tonemap.py
├── util.py
└── clahe.py
/images/1_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepankarc/hdr-imaging/HEAD/images/1_4.png
--------------------------------------------------------------------------------
/images/32_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepankarc/hdr-imaging/HEAD/images/32_1.png
--------------------------------------------------------------------------------
/images/0_Calib_Chapel_CRF0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepankarc/hdr-imaging/HEAD/images/0_Calib_Chapel_CRF0.jpg
--------------------------------------------------------------------------------
/images/0_Calib_Chapel_local_CRF0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/deepankarc/hdr-imaging/HEAD/images/0_Calib_Chapel_local_CRF0.jpg
--------------------------------------------------------------------------------
/load_images.py:
--------------------------------------------------------------------------------
1 | import glob, os
2 | import numpy as np
3 | import cv2
4 |
5 | def load_images(image_dir, image_ext, root_dir):
6 | iter_items = glob.iglob(root_dir+image_dir+image_ext)
7 |
8 | images = []
9 | exposure_times = []
10 | for item in iter_items:
11 | images.append(cv2.cvtColor(cv2.imread(item), code=cv2.COLOR_BGR2RGB))
12 | fname = os.path.basename(item)
13 | num_ = int(fname[:-4].split('_')[0])
14 | den_ = int(fname[:-4].split('_')[-1])
15 | exposure_times.append(num_ / den_)
16 |
17 | images = [img for _,img in sorted(zip(exposure_times, images), key=lambda pair: pair[0], reverse=True)]
18 | B = np.log(sorted(exposure_times, reverse=True))
19 |
20 | return [images, B]
21 |
--------------------------------------------------------------------------------
/compute_irradiance.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | def compute_irradiance(crf_channel, w, images, B):
4 | H,W,C = images[0].shape
5 | num_images = len(images)
6 |
7 | # irradiance map for each color channel
8 | irradiance_map = np.empty((H*W, C))
9 | for ch in range(C):
10 | crf = crf_channel[ch]
11 | num_ = np.empty((num_images, H*W))
12 | den_ = np.empty((num_images, H*W))
13 | for j in range(num_images):
14 | flat_image = (images[j][:,:,ch].flatten()).astype('int32')
15 | num_[j, :] = np.multiply(w[flat_image], crf[flat_image] - B[j])
16 | den_[j, :] = w[flat_image]
17 |
18 | irradiance_map[:, ch] = np.sum(num_, axis=0) / (np.sum(den_, axis=0) + 1e-6)
19 |
20 | irradiance_map = np.reshape(np.exp(irradiance_map), (H,W,C))
21 |
22 | return irradiance_map
23 |
--------------------------------------------------------------------------------
/gsolve.py:
--------------------------------------------------------------------------------
1 | """Solve for imaging system response function.
2 |
3 | Given a set of pixel values observed for several pixels in several
4 | images with different exposure times, this function returns the
5 | imaging system’s response function g as well as the log film irradiance
6 | values for the observed pixels.
7 |
8 | Assumes:
9 |
10 | Zmin = 0
11 | Zmax = 255
12 |
13 | Arguments:
14 |
15 | Z(i,j) is the pixel values of pixel location number i in image j
16 | B(j) is the log delta t, or log shutter speed, for image j
17 | l is lamdba, the constant that determines the amount of smoothness
18 | w(z) is the weighting function value for pixel value z
19 |
20 | Returns:
21 |
22 | g(z) is the log exposure corresponding to pixel value z
23 | lE(i) is the log film irradiance at pixel location i
24 | """
25 | import numpy as np
26 |
27 | def gsolve(Z, B, lambda_, w, Zmin, Zmax):
28 |
29 | n = Zmax + 1
30 | num_px, num_im = Z.shape
31 | A = np.zeros((num_px * num_im + n, n + num_px))
32 | b = np.zeros((A.shape[0]))
33 |
34 | # include the data fitting equations
35 | k = 0
36 | for i in range(num_px):
37 | for j in range(num_im):
38 | wij = w[Z[i,j]]
39 | A[k, Z[i,j]] = wij
40 | A[k, n+i] = -wij
41 | b[k] = wij * B[j]
42 | k += 1
43 |
44 | # fix the curve by setting its middle value to 0
45 | A[k, n//2] = 1
46 | k += 1
47 |
48 | # include the smoothness equations
49 | for i in range(n-2):
50 | A[k, i]= lambda_ * w[i+1]
51 | A[k, i+1] = -2 * lambda_ * w[i+1]
52 | A[k, i+2] = lambda_ * w[i+1]
53 | k += 1
54 |
55 | # solve the system using LLS
56 | output = np.linalg.lstsq(A, b)
57 | x = output[0]
58 | g = x[:n]
59 | lE = x[n:]
60 |
61 | return [g, lE]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### HDR Imaging
2 |
3 | This library performs High Dynamic Range post-processing for a given set of images. The technique implemented is discussed in [1]. Broadly the steps involved in the process are:
4 | 1. Estimation of the Camera Response Function (CRF)
5 | 2. Computation of the irradiance map
6 | 3. Tone Mapping
7 |
8 | Tone mapping is implemented in two ways - local and global. Global tone mapping has been implemented as discussed in [2].
9 |
10 | #### Parameters
11 |
12 | ROOT_DIR (String) = root folder which contains the folders for the source images.
13 | IMAGE_DIR (String) = name of the directory containing images.
14 | IMAGE_EXT (String) = image extension (eg. .jpg)
15 | COMPUTE_CRF (bool) = flag to compute CRF using supplied image.
16 |
17 | ### Usage
18 |
19 | `python run_hdr_image.py ROOT_DIR IMAGE_DIR IMAGE_EXT COMPUTE_CRF`
20 |
21 | ### Results
22 |
23 |
24 |
25 | Fig.1 - Original Images (Left - Low Exposure Image, Right - High Exposure Image)26 | 27 |
28 |
29 | 30 | Fig.2 - HDR Images (Left - Global Tonemapping, Right - Local Tonemapping)31 | 32 | ### References 33 | 34 | [1] [Paul E. Debevec Jitendra Malik - Recovering High Dynamic Range Radiance Maps from Photographs](http://www.pauldebevec.com/Research/HDR/debevec-siggraph97.pdf) 35 | 36 | [2] [Reinhard, Erik and Stark, Michael and Shirley, Peter and Ferwerda, James - Photographic Tone Reproduction for Digital Images](http://www.cmap.polytechnique.fr/~peyre/cours/x2005signal/hdr_photographic.pdf) 37 | -------------------------------------------------------------------------------- /hdr_debevec.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as mp_plt 3 | from gsolve import gsolve 4 | 5 | def plot_crf(crf_channel, C, Zmax): 6 | mp_plt.figure(figsize=(24,8)) 7 | channel_names = ['red', 'green', 'blue'] 8 | for ch in range(C): 9 | mp_plt.subplot(1,3,ch+1) 10 | mp_plt.plot(crf_channel[ch], np.arange(Zmax+1), color=channel_names[ch], linewidth=2) 11 | mp_plt.xlabel('log(X)') 12 | mp_plt.ylabel('Pixel intensity') 13 | mp_plt.title('CRF for {} channel'.format(channel_names[ch])) 14 | 15 | mp_plt.figure(figsize=(8,8)) 16 | for ch in range(C): 17 | mp_plt.plot(crf_channel[ch], np.arange(Zmax+1), color=channel_names[ch], linewidth=2, label=channel_names[ch]+' channel') 18 | mp_plt.xlabel('log(X)') 19 | mp_plt.ylabel('Pixel intensity') 20 | mp_plt.title('Camera Response Function'.format(channel_names[ch])) 21 | 22 | mp_plt.legend() 23 | 24 | def hdr_debevec(images, B, lambda_ = 50, num_px = 150): 25 | num_images = len(images) 26 | Zmin = 0 27 | Zmax = 255 28 | 29 | # image parameters 30 | H,W,C = images[0].shape 31 | 32 | # optmization parameters 33 | px_idx = np.random.choice(H*W, (num_px,), replace=False) 34 | 35 | # define pixel intensity weighting function w 36 | w = np.concatenate((np.arange(128) - Zmin, Zmax - np.arange(128,256))) 37 | 38 | # compute Z matrix 39 | Z = np.empty((num_px, num_images)) 40 | crf_channel = [] 41 | log_irrad_channel = [] 42 | for ch in range(C): 43 | for j, image in enumerate(images): 44 | flat_image = image[:,:,ch].flatten() 45 | Z[:, j] = flat_image[px_idx] 46 | 47 | # get crf and irradiance for each color channel 48 | [crf, log_irrad] = gsolve(Z.astype('int32'), B, lambda_, w, Zmin, Zmax) 49 | crf_channel.append(crf) 50 | log_irrad_channel.append(log_irrad) 51 | 52 | plot_crf(crf_channel, C, Zmax) 53 | return [crf_channel, log_irrad_channel, w] 54 | -------------------------------------------------------------------------------- /run_hdr_image.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import os, glob, sys 3 | import cv2 4 | import matplotlib.pyplot as mp_plt 5 | from load_images import load_images 6 | from hdr_debevec import hdr_debevec 7 | from compute_irradiance import compute_irradiance 8 | from tonemap import reinhard_tonemap, plot_and_save, local_tonemap 9 | 10 | 11 | def run_hdr(image_dir, image_ext, root_dir, COMPUTE_CRF, kwargs): 12 | if(len(kwargs) > 0): 13 | lambda_ = kwargs['lambda_'] 14 | num_px = kwargs['num_px'] 15 | gamma = kwargs['gamma'] 16 | alpha = kwargs['alpha'] 17 | gamma_local = kwargs['gamma_local'] 18 | saturation_local = kwargs['saturation_local'] 19 | 20 | [images, B] = load_images(image_dir, image_ext, root_dir) 21 | 22 | plot_idx = np.random.choice(len(images), (2,), replace=False) 23 | mp_plt.figure(figsize=(16,16)) 24 | mp_plt.subplot(1,2,1) 25 | mp_plt.imshow(images[plot_idx[0]]) 26 | mp_plt.title("Exposure time: {} secs".format(np.exp(B[plot_idx[0]]))) 27 | mp_plt.subplot(1,2,2) 28 | mp_plt.imshow(images[plot_idx[1]]) 29 | mp_plt.title("Exposure time: {} secs".format(np.exp(B[plot_idx[1]]))) 30 | 31 | if(COMPUTE_CRF): 32 | [crf_channel, log_irrad_channel, w] = hdr_debevec(images, B, lambda_=lambda_, num_px=num_px) 33 | np.save(root_dir+"crf.npy", [crf_channel, log_irrad_channel, w]) 34 | else: 35 | hdr_loc = kwargs['hdr_loc'] 36 | [crf_channel, log_irrad_channel, w] = np.load(hdr_loc) 37 | irradiance_map = compute_irradiance(crf_channel, w, images, B) 38 | tonemapped_img = reinhard_tonemap(irradiance_map, gamma=gamma, alpha=alpha) 39 | plot_and_save(tonemapped_img, root_dir+image_dir[:-1], "Globally Tonemapped Image") 40 | local_tonemap(irradiance_map, root_dir+image_dir[:-1]+"_local", saturation=saturation_local, gamma=gamma_local) 41 | return [tonemapped_img, irradiance_map] 42 | 43 | if __name__ == "__main__": 44 | ROOT_DIR = sys.argv[1] 45 | IMAGE_DIR = sys.argv[2] 46 | IMAGE_EXT = sys.argv[3] 47 | COMPUTE_CRF = sys.argv[4] 48 | kwargs = {'lambda_': 50, 'num_px': 150, 'gamma': 1 / 2.2, 'alpha': 0.35, 'hdr_loc':ROOT_DIR+"crf.npy", 49 | 'gamma_local':1.5, 'saturation_local':2.0} 50 | hdr_image, irmap = run_hdr(IMAGE_DIR, IMAGE_EXT, ROOT_DIR, COMPUTE_CRF, kwargs) 51 | -------------------------------------------------------------------------------- /tonemap.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | import matplotlib.pyplot as mp_plt 4 | from localtonemap.tonemap import tonemap 5 | 6 | def plot_and_save(img, img_name, img_title): 7 | params = [cv2.IMWRITE_JPEG_QUALITY, 80] 8 | mp_plt.figure(figsize=(8,8)) 9 | mp_plt.imshow(img) 10 | mp_plt.title(img_title) 11 | image_save = img * 255 12 | cv2.imwrite(img_name+".jpg", image_save[:,:,[2,1,0]], params) 13 | 14 | def reinhard_tonemap(irradiance_map, gamma=1/2.2, alpha=0.25): 15 | C = 3 # num of channels 16 | 17 | # tone mapping parameters 18 | E_map = np.empty_like(irradiance_map) 19 | 20 | # normalize irradiance map 21 | for ch in range(C): 22 | map_channel = irradiance_map[:,:,ch] 23 | E_min = map_channel.min() 24 | E_max = map_channel.max() 25 | E_map[:,:,ch] = (map_channel - E_min) / (E_max - E_min) 26 | 27 | # gamma correction 28 | E_map = E_map**gamma 29 | 30 | # convert to grayscale and apply Reinhart Tone Mapping 31 | L = cv2.cvtColor(E_map.astype('float32'), cv2.COLOR_RGB2GRAY) 32 | L_avg = np.exp(np.mean(np.log(L))) # average normalized grayscale irradiance 33 | T = alpha / L_avg * L 34 | L_tone = T * (1 + (T / (T.max())**2)) / (1 + T) 35 | M = L_tone / L 36 | 37 | # apply scaling to each channel 38 | tonemapped_img = np.empty_like(E_map) 39 | for ch in range(C): 40 | tonemapped_img[:,:,ch] = E_map[:,:,ch] * M 41 | 42 | return np.clip(tonemapped_img, 0.0, 1.0) 43 | 44 | def local_tonemap(irradiance_map, img_name, saturation=1., gamma=1/2.2): 45 | """# tonemap using Opencv's Durand Tonemap algorithm 46 | tonemap_obj = cv2.createTonemapDurand(gamma=4, sigma_color = 5.0) 47 | hdr_local = tonemap_obj.process(irmap.astype('float32')) 48 | 49 | mp_plt.figure(figsize=(16,16)) 50 | mp_plt.imshow(hdr_local)""" 51 | 52 | params = [cv2.IMWRITE_JPEG_QUALITY, 80] # jpeg quality params 53 | 54 | # compute tonemapped image 55 | local_tonemap = tonemap(irradiance_map, saturation=saturation, gamma_=gamma, numtiles=(8,8)) 56 | 57 | # plot and save locally tonemapped image 58 | mp_plt.figure(figsize=(8,8)) 59 | mp_plt.imshow(local_tonemap) 60 | mp_plt.title("Locally Tonemapped Image") 61 | cv2.imwrite(img_name+".jpg", local_tonemap[:,:,[2,1,0]], params) 62 | 63 | return local_tonemap 64 | -------------------------------------------------------------------------------- /localtonemap/tonemap.py: -------------------------------------------------------------------------------- 1 | """ 2 | Migrate from MATLAB's tonemap function 3 | """ 4 | import numpy as np 5 | import localtonemap.util as util 6 | import localtonemap.clahe as clahe 7 | 8 | import matplotlib.pyplot as plt 9 | 10 | 11 | def tonemap(E, l_remap=(0, 1), saturation=1., gamma_=1.5, numtiles=(4, 4)): 12 | """ 13 | render HDR for viewing 14 | exposure estimate -> log2 -> CLAHE -> remap to l_remap -> gamma correction -> HDR 15 | @param E: exposure (N x M x 3) 16 | @param l_remap: remap intensity to l_remap in the image adjust step 17 | @param saturation: saturation of the color. 18 | @param numtiles: number of contextual tiles in the CLAHE step 19 | return contrast reduced image 20 | """ 21 | if E.shape[0] % numtiles[0] != 0 or E.shape[1] % numtiles[1] != 0: 22 | E = util.crop_image(E, (E.shape[0] // numtiles[0] * numtiles[0], E.shape[1] // numtiles[1] * numtiles[1])) 23 | l2E, has_nonzero = lognormal(E) 24 | if has_nonzero: 25 | I = tone_operator(l2E, l_remap, saturation, gamma_, numtiles) 26 | else: 27 | I = l2E 28 | # clip 29 | I[I < 0] = 0 30 | I[1 < I] = 1 31 | return np.uint8(I * 255.) 32 | 33 | 34 | def lognormal(E): 35 | """ 36 | log2(E). remove 0s. 37 | return log2E, has_nonzero 38 | """ 39 | mask = (E != 0) 40 | 41 | if np.any(mask): 42 | min_nonzero = np.min(E[mask]) 43 | E[np.logical_not(mask)] = min_nonzero 44 | l2E = util.rescale(np.log2(E)) 45 | has_nonzero = True 46 | 47 | else:# all elements are zero 48 | l2E = np.zeros_like(E) 49 | has_nonzero = False 50 | 51 | return l2E, has_nonzero 52 | 53 | def tone_operator(l2E, l_remap, saturation, gamma_, numtiles): 54 | """ 55 | The main algorithm is CLAHE: contrast limited adaptive histogram equalization 56 | preprocessing: convert RGB to XYZ to Lab 57 | postprocessing: back to RGB 58 | """ 59 | lab = util.srgb2lab(l2E) 60 | lab[:,:,0] = util.rescale(lab[:,:,0]) 61 | # lab[:, :, 0] /= 100 62 | lab[:, :, 0] = clahe.hist_equalize(lab[:, :, 0], numtiles) 63 | lab[:, :, 0] = imadjust(lab[:, :, 0], range_in=l_remap, range_out=(0, 1), gamma=gamma_) * 100 64 | lab[:, :, 1:] = lab[:, :, 1:] * saturation 65 | I = util.lab2srgb(lab) 66 | return I 67 | 68 | 69 | def imadjust(I, range_in=None, range_out=(0, 1), gamma=1): 70 | """ 71 | remap I from range_in to range_out 72 | @param I: image 73 | @param range_in: range of the input image. will be assigned minmax(I) if none 74 | @param range_out: range of the output image 75 | @param gamma: factor of the gamma correction 76 | """ 77 | if range_in is None: 78 | range_in = (np.min(I), np.max(I)) 79 | out = (I - range_in[0]) / (range_in[1] - range_in[0]) 80 | out = out**gamma 81 | out = out * (range_out[1] - range_out[0]) + range_out[0] 82 | return out 83 | -------------------------------------------------------------------------------- /localtonemap/util.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | E = 216. / 24389 4 | K = 24389. / 27 5 | 6 | M = np.array([[0.412424, 0.212656, 0.0193324], 7 | [0.357579, 0.715158, 0.119193], 8 | [0.180464, 0.0721856, 0.950444]]) 9 | 10 | 11 | def hist_count(I): 12 | """ 13 | count [0,255] 14 | @param I: image 15 | """ 16 | freq = np.zeros(256, dtype=int) 17 | for x in I.ravel(): 18 | freq[x] += 1 19 | return freq 20 | 21 | 22 | def rescale(I, window=(0, 1)): 23 | """ 24 | rescale the intensity of the image [Imin, Imax] -> window 25 | @param window: tuple 1x2 26 | """ 27 | a = np.min(I) 28 | b = np.max(I) 29 | 30 | J = I 31 | if a == b: 32 | if a == 0: 33 | J = I 34 | else: 35 | J = I / a 36 | else: 37 | J = (I - a) / (b - a) * (window[1] - window[0]) + window[0] 38 | return J 39 | 40 | 41 | # image channel conversion 42 | def srgb2lab(rgb): 43 | dims = rgb.shape 44 | rgb = np.reshape(np.transpose(rgb, (2, 0, 1)), [3, dims[0] * dims[1]]) 45 | lab_1d = xyz2lab(srgb2xyz(rgb)) 46 | lab = np.transpose(np.reshape(lab_1d, [3, dims[0], dims[1]]), (1, 2, 0)) 47 | return lab 48 | 49 | 50 | def srgb2xyz(rgb): 51 | """ 52 | http://brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html 53 | """ 54 | mask = rgb > 0.04045 55 | rgb[mask] = ((rgb[mask] + 0.055) / 1.055) ** 2.4 56 | rgb[np.logical_not(mask)] = rgb[np.logical_not(mask)] / 12.92 57 | 58 | return np.dot(M.T, rgb) 59 | 60 | 61 | def xyz2lab(xyz): 62 | """ 63 | http://brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html 64 | """ 65 | 66 | xyz[0, :] = xyz[0, :] / 0.95047 67 | xyz[2, :] = xyz[2, :] / 1.08883 68 | 69 | mask = xyz > E 70 | 71 | xyz[mask] = xyz[mask] ** (1. / 3) 72 | xyz[np.logical_not(mask)] = (K * xyz[np.logical_not(mask)] + 16) / 116. 73 | 74 | lab = np.zeros_like(xyz) 75 | lab[0, :] = 116. * xyz[1, :] - 16. 76 | lab[1, :] = 500. * (xyz[0, :] - xyz[1, :]) 77 | lab[2, :] = 200. * (xyz[1, :] - xyz[2, :]) 78 | return lab 79 | 80 | 81 | def lab2srgb(lab): 82 | dims = lab.shape 83 | lab = np.reshape(np.transpose(lab, (2, 0, 1)), [3, dims[0] * dims[1]]) 84 | rgb = xyz2srgb(lab2xyz(lab)) 85 | rgb = np.transpose(np.reshape(rgb, [3, dims[0], dims[1]]), (1, 2, 0)) 86 | return rgb 87 | 88 | 89 | def lab2xyz(lab): 90 | """ 91 | http://brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html 92 | """ 93 | f = np.copy(lab) 94 | xyz = np.copy(f) 95 | 96 | mask = lab[0, :] > K * E 97 | mask_not = np.logical_not(mask) 98 | xyz[1, mask] = ((lab[0, mask] + 16.0) / 116.) ** 3 99 | xyz[1, mask_not] = lab[0, mask_not] / K 100 | 101 | mask = xyz[1, :] > E 102 | mask_not = np.logical_not(mask) 103 | f[1, mask] = (lab[0, mask] + 16.0) / 116. 104 | f[1, mask_not] = (K * xyz[1, mask_not] + 16.) / 116. 105 | 106 | f[0, :] = lab[1, :] / 500. + f[1, :] 107 | f[2, :] = f[1, :] - lab[2, :] / 200. 108 | 109 | tmp = f[0, :] ** 3 110 | mask = tmp > E 111 | mask_not = np.logical_not(mask) 112 | xyz[0, mask] = tmp[mask] 113 | xyz[0, mask_not] = (116. * f[0, mask_not] - 16.) / K 114 | 115 | tmp = f[2, :] ** 3 116 | mask = tmp > E 117 | mask_not = np.logical_not(mask) 118 | xyz[2, mask] = tmp[mask] 119 | xyz[2, mask_not] = (116. * f[2, mask_not] - 16.) / K 120 | 121 | xyz[0, :] = xyz[0, :] * 0.95047 122 | xyz[2, :] = xyz[2, :] * 1.08883 123 | return xyz 124 | 125 | 126 | def xyz2srgb(xyz): 127 | """ 128 | http://brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html 129 | """ 130 | invMT = np.linalg.inv(M.T) 131 | 132 | rgb = np.dot(invMT, xyz) 133 | mask = rgb > 0.0031308 134 | mask_not = np.logical_not(mask) 135 | rgb[mask] = ((1.055 * rgb[mask]) ** (1 / 2.4)) - 0.055 136 | rgb[mask_not] = 12.92 * rgb[mask_not] 137 | return rgb 138 | 139 | 140 | def crop_image(I, crop_size): 141 | return I[max((I.shape[0] - crop_size[0]) // 2, 0):min((I.shape[0] + crop_size[0]) // 2, I.shape[0]), max((I.shape[1] - crop_size[1]) // 2, 0):min((I.shape[1] + crop_size[1]) // 2, I.shape[1])] 142 | -------------------------------------------------------------------------------- /localtonemap/clahe.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import localtonemap.util as util 3 | import matplotlib.pyplot as plt 4 | 5 | from matplotlib import rc 6 | rc('font', size=30) 7 | 8 | def hist_equalize(I, numtiles=(8, 8)): 9 | assert I.shape[0] % numtiles[0] == 0 and I.shape[1] % numtiles[1] == 0 10 | img_range = np.array([0, 1]) 11 | tile_size = (I.shape[0] // numtiles[0], I.shape[1] // numtiles[1]) 12 | tile_mappings = maketile_mapping(I, numtiles, tile_size, img_range, img_range) 13 | out = make_clahe_image(I, tile_mappings, numtiles, tile_size, img_range) 14 | return out 15 | 16 | 17 | def maketile_mapping(I, numtiles, tile_size, selected_range, full_range, num_bins=256, norm_clip_limit=0.01): 18 | 19 | num_pixel_in_tile = np.prod(tile_size) 20 | min_clip_limit = np.ceil(np.float(num_pixel_in_tile) / num_bins) 21 | clip_limit = min_clip_limit + np.round(norm_clip_limit * (num_pixel_in_tile - min_clip_limit)) 22 | 23 | tile_mappings = [] 24 | # image_col = 0 25 | image_row = 0 26 | print('make tile mappings') 27 | 28 | for tile_row in range(numtiles[0]): 29 | tile_mappings.append([]) 30 | image_col = 0 31 | # image_row = 0 32 | for tile_col in range(numtiles[1]): 33 | # print('tile ({}, {}):'.format(tile_row, tile_col), end=',') 34 | tile = I[image_row:(image_row + tile_size[0]), image_col:(image_col + tile_size[1])] 35 | # print('\timhist', end=',') 36 | tile_hist = imhist(tile, num_bins, full_range[1]) 37 | 38 | # print('\tclip hist', end=',') 39 | tile_hist = clip_histogram(tile_hist, clip_limit, num_bins) 40 | 41 | """ plot histogram 42 | fig = plt.figure(figsize=(20, 12)) 43 | plt.bar(np.arange(256) / 256., tile_hist, width=0.005, edgecolor='b'); 44 | plt.xlim(0, 1); 45 | plt.xlabel('intensity'); 46 | plt.ylabel('count'); 47 | plt.tight_layout() 48 | plt.savefig('../result/intermediate/histogram/hist{}{}.pdf'.format(tile_row, tile_col)); 49 | """ 50 | 51 | # print('\tmake mapping') 52 | tile_mapping = make_mapping(tile_hist, selected_range, num_pixel_in_tile) 53 | tile_mappings[-1].append(tile_mapping) 54 | 55 | """ plot mapping 56 | fig = plt.figure(figsize=(20, 12)) 57 | plt.plot(np.arange(256) / 256., tile_mapping, lw=2); 58 | plt.xlim(0, 1); 59 | plt.xlabel('x'); 60 | plt.ylabel('f(x)'); 61 | plt.tight_layout() 62 | plt.savefig('../result/intermediate/histogram/mapping{}{}.pdf'.format(tile_row, tile_col)); 63 | """ 64 | 65 | image_col += tile_size[1] 66 | image_row += tile_size[0] 67 | return tile_mappings 68 | 69 | 70 | def imhist(tile, num_bins, top): 71 | """ 72 | image histogram 73 | @param tile: a rectangular tile cropped from the image 74 | @param num_bins: number of bins 75 | @param top: scale the rightmost bin to the top 76 | """ 77 | s = (num_bins - 1.) / top # scale factor 78 | tile_scaled = np.floor(tile * s + .5) 79 | hist = np.zeros(num_bins, dtype=np.int32) 80 | for i in range(num_bins): 81 | hist[i] = np.sum(tile_scaled == i) 82 | return hist 83 | 84 | 85 | def clip_histogram(img_hist, clip_limit, num_bins): 86 | """ 87 | clip the histogram according to the clipLimit and redistributes clipped pixels across bins below the clipLimit 88 | @param img_hist: histogram of the image 89 | @param clip_limit: the clipping limit 90 | @param num_bins: number of bins 91 | """ 92 | total_excess = np.sum(np.maximum(img_hist - clip_limit, 0)) 93 | 94 | avg_bin_incr = np.floor(total_excess / num_bins) 95 | upper_limit = clip_limit - avg_bin_incr 96 | 97 | for k in range(num_bins): 98 | if img_hist[k] > clip_limit: 99 | img_hist[k] = clip_limit 100 | else: 101 | if img_hist[k] > upper_limit: 102 | total_excess -= clip_limit - img_hist[k] 103 | img_hist[k] = clip_limit 104 | else: 105 | total_excess -= avg_bin_incr 106 | img_hist[k] += avg_bin_incr 107 | 108 | # redistributes the remaining pixels, one pixel at a time 109 | k = 0 110 | # print('total excess={}'.format(total_excess), end=';') 111 | while total_excess != 0: 112 | step_size = max(int(np.floor(num_bins / total_excess)), 1) 113 | for m in range(k, num_bins, step_size): 114 | if img_hist[m] < clip_limit: 115 | img_hist[m] += 1 116 | total_excess -= 1 117 | if total_excess == 0: 118 | break 119 | 120 | k += 1 121 | if k == num_bins: 122 | k = 0 123 | return img_hist 124 | 125 | 126 | def make_mapping(img_hist, selected_range, num_pixel_in_tile): 127 | """ 128 | using uniform distribution 129 | """ 130 | high_sum = np.cumsum(img_hist) 131 | val_spread = selected_range[1] - selected_range[0] 132 | 133 | scale = val_spread / num_pixel_in_tile 134 | mapping = np.minimum(selected_range[0] + high_sum * scale, selected_range[1]) 135 | return mapping 136 | 137 | 138 | def make_clahe_image(I, tile_mappings, numtiles, tile_size, selected_range, num_bins=256): 139 | """ 140 | interpolates between neighboring tile mappings to produce a new mapping in order to remove artificially induced tile borders 141 | """ 142 | assert num_bins > 1 143 | # print('make clahe image') 144 | Ic = np.zeros_like(I) 145 | 146 | bin_step = 1. / (num_bins - 1) 147 | start = np.ceil(selected_range[0] / bin_step) 148 | stop = np.floor(selected_range[1] / bin_step) 149 | 150 | aLut = np.arange(0, 1 + 1e-10, 1.0 / (stop - start)) 151 | 152 | """ plot discontinuous 153 | imgtile_row = 0 154 | for tile_row in range(numtiles[0]): 155 | imgtile_col = 0 156 | for tile_col in range(numtiles[1]): 157 | mapping = tile_mappings[tile_row][tile_col] 158 | tile = I[imgtile_row:imgtile_row+tile_size[0], imgtile_col: imgtile_col+tile_size[1]]; 159 | Ic[imgtile_row:imgtile_row+tile_size[0], imgtile_col: imgtile_col+tile_size[1]] = grayxform(tile, mapping); 160 | imgtile_col += tile_size[1] 161 | imgtile_row += tile_size[0] 162 | fig = plt.figure(figsize=(20, 12)) 163 | plt.imshow(Ic, cmap='gray') 164 | plt.tight_layout() 165 | plt.axis('off') 166 | plt.show() 167 | """ 168 | 169 | imgtile_row = 0 170 | for k in range(numtiles[0] + 1): 171 | if k == 0: # edge case: top row 172 | imgtile_num_rows = tile_size[0] // 2 173 | maptile_rows = (0, 0) 174 | elif k == numtiles[0]: 175 | imgtile_num_rows = tile_size[0] // 2 176 | maptile_rows = (numtiles[0] - 1, numtiles[0] - 1) 177 | else: 178 | imgtile_num_rows = tile_size[0] 179 | maptile_rows = (k - 1, k) 180 | 181 | imgtile_col = 0 182 | for l in range(numtiles[1] + 1): 183 | # print('tile ({}, {})'.format(k, l)) 184 | if l == 0: 185 | imgtile_num_cols = tile_size[1] // 2 186 | maptile_cols = (0, 0) 187 | elif l == numtiles[1]: 188 | imgtile_num_cols = tile_size[1] // 2 189 | maptile_cols = (numtiles[1] - 1, numtiles[1] - 1) 190 | else: 191 | imgtile_num_cols = tile_size[1] 192 | maptile_cols = (l - 1, l) 193 | 194 | ul_maptile = tile_mappings[maptile_rows[0]][maptile_cols[0]] 195 | ur_maptile = tile_mappings[maptile_rows[0]][maptile_cols[1]] 196 | bl_maptile = tile_mappings[maptile_rows[1]][maptile_cols[0]] 197 | br_maptile = tile_mappings[maptile_rows[1]][maptile_cols[1]] 198 | 199 | norm_factor = imgtile_num_rows * imgtile_num_cols 200 | 201 | imgpxl_vals = grayxform(I[imgtile_row:(imgtile_row + imgtile_num_rows), imgtile_col:(imgtile_col + imgtile_num_cols)], aLut) 202 | 203 | row_w = np.tile(np.expand_dims(np.arange(imgtile_num_rows), axis=1), [1, imgtile_num_cols]) 204 | col_w = np.tile(np.expand_dims(np.arange(imgtile_num_cols), axis=0), [imgtile_num_rows, 1]) 205 | row_rev_w = np.tile(np.expand_dims(np.arange(imgtile_num_rows, 0, -1), axis=1), [1, imgtile_num_cols]) 206 | col_rev_w = np.tile(np.expand_dims(np.arange(imgtile_num_cols, 0, -1), axis=0), [imgtile_num_rows, 1]) 207 | 208 | Ic[imgtile_row:(imgtile_row + imgtile_num_rows), imgtile_col:(imgtile_col + imgtile_num_cols)] = (row_rev_w * (col_rev_w * grayxform(imgpxl_vals, ul_maptile) + col_w * grayxform(imgpxl_vals, ur_maptile)) + row_w * (col_rev_w * grayxform(imgpxl_vals, bl_maptile) + col_w * grayxform(imgpxl_vals, br_maptile))) / norm_factor 209 | 210 | imgtile_col += imgtile_num_cols 211 | 212 | imgtile_row += imgtile_num_rows 213 | return Ic 214 | 215 | 216 | def grayxform(I, aLut): 217 | """ 218 | map I to aLut 219 | @param I: image 220 | @param aLut: look-up table 221 | """ 222 | max_idx = len(aLut) - 1 223 | val = np.copy(I) 224 | val[val < 0] = 0 225 | val[val > 1] = 1 226 | indexes = np.int32(val * max_idx + 0.5) 227 | return aLut[indexes] 228 | --------------------------------------------------------------------------------