├── __init__.py
├── images
├── building.jpg
├── streets.jpg
└── streets_gray.jpg
├── plots
├── eq_building.png
├── eq_streets.png
└── eq_streets_gray.png
├── results
├── equalized_streets.jpg
├── equalized_building.jpg
└── equalized_streets_gray.jpg
├── requirements.txt
├── main.py
├── Readme.md
└── dg_clahe.py
/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/images/building.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimimal/dual_gamma_clahe/HEAD/images/building.jpg
--------------------------------------------------------------------------------
/images/streets.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimimal/dual_gamma_clahe/HEAD/images/streets.jpg
--------------------------------------------------------------------------------
/plots/eq_building.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimimal/dual_gamma_clahe/HEAD/plots/eq_building.png
--------------------------------------------------------------------------------
/plots/eq_streets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimimal/dual_gamma_clahe/HEAD/plots/eq_streets.png
--------------------------------------------------------------------------------
/images/streets_gray.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimimal/dual_gamma_clahe/HEAD/images/streets_gray.jpg
--------------------------------------------------------------------------------
/plots/eq_streets_gray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimimal/dual_gamma_clahe/HEAD/plots/eq_streets_gray.png
--------------------------------------------------------------------------------
/results/equalized_streets.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimimal/dual_gamma_clahe/HEAD/results/equalized_streets.jpg
--------------------------------------------------------------------------------
/results/equalized_building.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimimal/dual_gamma_clahe/HEAD/results/equalized_building.jpg
--------------------------------------------------------------------------------
/results/equalized_streets_gray.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dimimal/dual_gamma_clahe/HEAD/results/equalized_streets_gray.jpg
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | anyio==3.5.0
2 | async-generator==1.10
3 | attrs==21.4.0
4 | axis==44
5 | black==22.1.0
6 | block==0.0.5
7 | certifi==2021.10.8
8 | charset-normalizer==2.0.10
9 | click==8.0.3
10 | contextvars==2.4
11 | cycler==0.11.0
12 | dataclasses==0.8
13 | decorator==4.4.2
14 | dual==0.0.8
15 | h11==0.12.0
16 | httpcore==0.14.5
17 | httpx==0.22.0
18 | idna==3.3
19 | imageio==2.14.0
20 | immutables==0.16
21 | importlib-metadata==4.8.3
22 | iniconfig==1.1.1
23 | kiwisolver==1.3.1
24 | matplotlib==3.3.4
25 | mypy-extensions==0.4.3
26 | networkx==2.5.1
27 | numpy==1.19.5
28 | packaging==21.3
29 | pathspec==0.9.0
30 | Pillow==8.4.0
31 | pkg_resources==0.0.0
32 | platformdirs==2.4.0
33 | pluggy==1.0.0
34 | py==1.11.0
35 | pyparsing==3.0.7
36 | pytest==6.2.5
37 | python-dateutil==2.8.2
38 | PyWavelets==1.1.1
39 | rfc3986==1.5.0
40 | scikit-image==0.17.2
41 | scipy==1.5.4
42 | six==1.16.0
43 | sniffio==1.2.0
44 | tifffile==2020.9.3
45 | toml==0.10.2
46 | tomli==1.2.3
47 | typed-ast==1.5.2
48 | typing_extensions==4.0.1
49 | xmltodict==0.12.0
50 | zipp==3.6.0
51 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import os
2 | import argparse
3 | from skimage import io
4 | from dg_clahe import dual_gamma_clahe
5 | import matplotlib.pyplot as plt
6 | from typing import List
7 |
8 |
9 | def parse_kernel(input_list: str) -> List:
10 | if "," not in input_list:
11 | raise ValueError(f"No , seperation for kernel values")
12 | input_list = input_list.split(",")
13 | input_list = [int(i.strip()) for i in input_list]
14 | if len(input_list) > 2:
15 | raise ValueError(
16 | f"kernel should be either int or a sequence of 2 ints but got {input}"
17 | )
18 | if len(input_list) == 1:
19 | return input_list[0], input_list[1]
20 | else:
21 | return input_list
22 |
23 |
24 | def file_exists(file_):
25 | if not os.path.exists(file_):
26 | raise ValueError(f"Image file not found {file_}")
27 |
28 |
29 | def check_folder(folder):
30 | if os.path.isfile(folder):
31 | raise ValueError(
32 | f"Output directory is an existing file {folder}, please set a folder here or leave it default."
33 | )
34 | elif not os.path.isdir(folder):
35 | os.makedirs(folder)
36 | return folder
37 |
38 |
39 | def parse_arguments():
40 | parser = argparse.ArgumentParser()
41 | parser.add_argument("image", help="The image path", type=str)
42 | parser.add_argument(
43 | "--kernel",
44 | help="Size of the kernel, if int then its (height, width)",
45 | type=parse_kernel,
46 | default=[32, 32],
47 | )
48 | parser.add_argument(
49 | "--alpha", help="Alpha parameter of the algorithm", type=float, default=40
50 | )
51 | parser.add_argument(
52 | "--delta", help="The Delta threshold of the algorithm", type=int, default=50
53 | )
54 | parser.add_argument(
55 | "--p",
56 | help="The factor for the computation of clip limits",
57 | type=float,
58 | default=1.5,
59 | )
60 | parser.add_argument(
61 | "--show",
62 | help="Display the 2 figures with matplotlib, before and after equalization",
63 | action="store_true",
64 | )
65 | parser.add_argument(
66 | "--out",
67 | help="Output directory of the equalized image. Default folder is the ./images folder",
68 | default="./out_dir/",
69 | type=check_folder,
70 | )
71 | return parser.parse_args()
72 |
73 |
74 | def main(args):
75 | image_name = "equalized_" + args.image.split(os.sep)[-1]
76 | image = io.imread(args.image)
77 | equalized_image = dual_gamma_clahe(
78 | image.copy(),
79 | block_size=args.kernel,
80 | alpha=args.alpha,
81 | delta=args.delta,
82 | pi=args.p,
83 | bins=256,
84 | )
85 |
86 | if args.show:
87 | if image.ndim == 2:
88 | cmap = "gray"
89 | else:
90 | cmap = None
91 | fig, ax = plt.subplots(1, 2)
92 | ax[0].imshow(image, cmap=cmap)
93 | ax[1].imshow(equalized_image, cmap=cmap)
94 | ax[0].set_title("Input Image")
95 | ax[1].set_title("Equalized Image ")
96 | plt.show()
97 |
98 | # Store image
99 | io.imsave(os.path.join(args.out, image_name), equalized_image)
100 |
101 |
102 | if __name__ == "__main__":
103 | args = parse_arguments()
104 | main(args)
105 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | ## Automatic Contrast-Limited Adaptive Histogram Equalization With Dual Gamma Correction
2 |
3 | This is the implementation in python of the algorithm of the paper `Automatic Contrast-Limited Adaptive Histogram
4 | Equalization With Dual Gamma Correction` [link here](https://ieeexplore.ieee.org/document/8269243). This is not an official repository. In the algorithm we use the HSV color space
5 | and specifically the V channel to equalize our images. The algorithm works with grayscale images as well. The proper selection of
6 | parameters is important in order to get a good result in RGB and Gray images. Before we process the image, we pad the image with the
7 | reflection. Reflection, reflects the columns and rows at the sides of the image. The rationale behind this approach, is that the blocks
8 | might not fit in the image properly. Thus, either we had to crop the image or pad and then return the original image size. The first approach could
9 | have affect our results. Vectorization methods were applied where possible. The algorithm spans across 2 double for loops, in the first one we process
10 | the image in blocks, where we calculate each block's histograms and all the necessary parameters. This loop instead of running it per pixel taking O(WxH) where
11 | W and H are the width and height of image respectively; it takes O(kxl) where kxl is the amount of fitted blocks per image dimension. The second step is the bilinear interpolation
12 | of pixels. Because we process the image per block, some pixels which rely on the boundaries of the blocks may have quite different values that create artifacts. To address this problem
13 | we apply bilinear interpolation where the value of each pixel is interpolated among the values of 4 blocks.
14 |
15 | ## Prerequisites
16 | Create your environment and install the dependencies required for this project.
17 |
18 | ```commandline
19 | python3 -m venv env
20 | source env/bin/activate
21 | pip install pip --upgrade
22 | pip install -r requirements.txt
23 | ```
24 |
25 | To execute the algorithm run the `main.py` script. Running the main function with the -h flag
26 | you can get the list of arguments that you can pass to the main. You pass all the necessary arguments
27 | noted here in order to run the algorithm.
28 |
29 | ```
30 | usage: main.py [-h] [--kernel KERNEL] [--alpha ALPHA] [--delta DELTA] [--p P]
31 | [--show] [--out OUT]
32 | image
33 |
34 | positional arguments:
35 | image The image path
36 |
37 | optional arguments:
38 | -h, --help show this help message and exit
39 | --kernel KERNEL Size of the kernel, if int then its (height, width)
40 | --alpha ALPHA Alpha parameter of the algorithm
41 | --delta DELTA The Delta threshold of the algorithm
42 | --p P The factor for the computation of clip limits
43 | --show Display the 2 figures with matplotlib, before and after
44 | equalization
45 | --out OUT Output directory of the equalized image. Default folder is
46 | the ./images folder
47 |
48 | ```
49 |
50 | ## Running
51 |
52 | `python main.py ./images/streets_gray.jpg --kernel 32,32 --alpha 40 --delta 50 --p 1.5 --show`
53 |
54 |
55 | `python main.py ./images/streets.jpg --kernel 32,32 --alpha 40 --delta 50 --p 1.5 --show`
56 |
57 | `python main.py ./images/building.jpg --kernel 32,32 --alpha 40 --delta 50 --p 1.5 --show`
58 |
59 | ## Results
60 |
61 | > RGB Example
62 |
63 |
64 | > Grayscale Example
65 |
66 |
--------------------------------------------------------------------------------
/dg_clahe.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from skimage import io
3 | from skimage.color import rgb2hsv, hsv2rgb
4 | import matplotlib.pyplot as plt
5 | from typing import List, Union
6 |
7 |
8 | def compute_clip_limit(
9 | block: np.ndarray, alpha: float = 40, pi: float = 1.5, R: int = 255
10 | ) -> int:
11 | """This function computes the beta clip limits of each image block
12 |
13 | Args:
14 | block (np.ndarray): The block of each image
15 | alpha (float): The alpha variance enhance parameter
16 | pi (float): The P factor enhanement
17 | R (int): The color range of the image
18 |
19 | Returns:
20 | (int): The beta clip limit for the histogram blocks
21 | """
22 | avg = block.mean()
23 | l_max = block.max()
24 | n = l_max - block.min()
25 | sigma = block.std()
26 | m = block.size
27 | return int(
28 | (m / n) * (1 + (pi * l_max / R) + (alpha * sigma / 100.0) / (avg + 0.001))
29 | )
30 |
31 |
32 | def clip_and_redistribute_hist(hist: np.ndarray, beta: int) -> np.ndarray:
33 | """Clips and redistribute the excess values of the histograms among the bins using the block clip value beta
34 |
35 | Args:
36 | hist (np.ndarray): The block's histogram
37 | beta (int): The clipping value
38 |
39 | Returns:
40 | hist (np.ndarray): The redistributed histogram of shape (n_height_blocks, n_width_blocks, bins)
41 | """
42 | # compute the exceeded pixels mask
43 | mask = hist > beta
44 | exceed_values = hist[mask]
45 | bin_value = (exceed_values.sum() - exceed_values.size * beta) // hist.size
46 | hist[mask] = beta
47 | hist += bin_value
48 | return hist
49 |
50 |
51 | def compute_gamma(l_max: int, d_range: int, weighted_cdf: np.ndarray) -> np.ndarray:
52 | """Computes the Gamma(l) function from paper in a vectorized way across the range of the color space:
53 | https://www.researcher-app.com/paper/708253
54 |
55 | Args:
56 | l_max (int): The global max value of the image
57 | d_range (int): The range of the colors in the image
58 | weighted_cdf (np.ndarray): The normalized CDF_w()
59 |
60 | Returns:
61 | (np.ndarray): The gamma vector of size nbins
62 | """
63 | return l_max * np.power(np.arange(d_range + 1) / l_max, (1 + weighted_cdf) / 2.0)
64 |
65 |
66 | def compute_w_en(l_alpha: int, l_max: int, cdf: np.ndarray) -> np.ndarray:
67 | """Computes the W_en function according to the paper in a vectorized way, for the entire cdf:
68 | https://www.researcher-app.com/paper/708253I
69 |
70 | Args:
71 | l_alpha (float): The global l_a variable of the 75th percentile according to the CDF
72 | l_max (int): the global max intensity value
73 | cdf (np.ndarray): The normalized cdf
74 |
75 | Returns:
76 | (np.ndarray): The W_en factors of range of the bin size
77 | """
78 | return np.power(l_max / l_alpha, 1 - (np.log(np.e + cdf) / 8))
79 |
80 |
81 | def compute_block_coords(
82 | center_block: int, block_index: int, index: int, n_blocks: int
83 | ) -> tuple:
84 | """This function computes the neighbour block coordinates to interpolate from the histogram. This computes the coords
85 | of the interpolating blocks from both axis (x, y)
86 |
87 | Args:
88 | center_block (int): The index of the center block along the pixel coords
89 | block_index (int): The index of the block in histogram coords
90 | index (int): The current index of pixel along axis
91 | n_blocks (int): The amount of blocks along axis
92 |
93 | Returns:
94 | (int, int): The indexes of the surrounding blocks for each axis
95 | """
96 | if index < center_block:
97 | block_min = block_index - 1
98 | if block_min < 0:
99 | block_min = 0
100 | block_max = block_index
101 | else:
102 | block_min = block_index
103 | block_max = block_index + 1
104 | if block_max >= n_blocks:
105 | block_max = block_index
106 | return block_min, block_max
107 |
108 |
109 | def compute_mn_factors(coords: tuple, index: int) -> float:
110 | """Function which computes the interpolation factors of m, n according to the paper. Depending on the axis, it returns
111 | the proper values
112 | https://www.researcher-app.com/paper/708253
113 |
114 | Args:
115 | coords (tuple): The block coordinates (x, y)
116 | index (int): The pixel index
117 |
118 | Returns:
119 | (float): The m or n interpolation factors
120 | """
121 | if coords[1] - coords[0] == 0:
122 | return 0
123 | else:
124 | return (coords[1] - index) / (coords[1] - coords[0])
125 |
126 |
127 | def dual_gamma_clahe(
128 | image: np.ndarray,
129 | block_size: Union[int, List] = [32, 32],
130 | alpha: float = 20,
131 | pi: float = 1.5,
132 | delta: float = 50,
133 | bins: int = 256,
134 | ):
135 | """The dual gamma clahe algorithm taking as inputs the image in numpy format along with the parameters. The algorithm transforms
136 | the image in HSV if its RGB and applies the equalization on the V channel. If grayscale image applied, then its using it as is.
137 |
138 | Args:
139 | image (np.ndarray): The image in np format, either grayscale or RGB
140 | block_size (int, int): The size of the selected block window
141 | alpha (float): The alpha variance enhance parameter
142 | pi (float): The P factor enhanement
143 | delta (float): The threshold value to aply T1 or Gamma depending on the threshold
144 | bins (int): The number of bins
145 |
146 | Returns:
147 | (np.ndarray): The enhanced image using dual gamma
148 | """
149 | ndim = image.ndim
150 | R = bins - 1
151 | if R != 255:
152 | raise ValueError(
153 | f"The range should be 256. This algorithm hasn't been tested on a different value, but given {bins}"
154 | )
155 | if ndim == 3 and image.shape[2] == 3:
156 | hsv_image = rgb2hsv(image)
157 | gray_image = hsv_image[:, :, 2]
158 | gray_image = np.clip(gray_image * 255, 0, 255)
159 | elif ndim == 2:
160 | gray_image = image.copy().astype(np.float)
161 | else:
162 | raise ValueError(
163 | f"Wrong number of shape or dimensions. Either single or 3 channel but image has shape {image.shape}"
164 | )
165 |
166 | if isinstance(block_size, int):
167 | width_block = block_size
168 | height_block = block_size
169 | block_size = (block_size, block_size)
170 | elif isinstance(block_size, List):
171 | assert (
172 | len(block_size) == 2
173 | ), f"block_size dimension is not int or (tuple, tuple) but {len(block_size)}"
174 | height_block, width_block = block_size
175 |
176 | # Compute global histogram and global values
177 | glob_l_max = gray_image.max()
178 | glob_hist = np.histogram(gray_image, bins=bins)[0]
179 | glob_cdf = np.cumsum(glob_hist)
180 | glob_cdf = glob_cdf / glob_cdf[-1]
181 | glob_l_a = np.argwhere(glob_cdf > 0.75)[0]
182 |
183 | # Compute the padding values to pad the image
184 | pad_start = [height_block // 2, width_block // 2]
185 | pad_end = [
186 | (k - im % k) % k + int(np.ceil(k / 2.))
187 | for k, im in zip(block_size, gray_image.shape)
188 | ]
189 |
190 | unpad_indices = tuple(
191 | [
192 | slice(pad_h, im - pad_w)
193 | for pad_h, pad_w, im in zip(pad_start, pad_end, gray_image.shape)
194 | ]
195 | )
196 |
197 | # Pad the image with reflect mode for color invariance
198 | gray_image = np.pad(
199 | gray_image,
200 | [[pad_h, pad_w] for pad_h, pad_w in zip(pad_start, pad_end)],
201 | mode="reflect",
202 | )
203 |
204 | pad_height, pad_width = gray_image.shape[:2]
205 | n_height_blocks = int(pad_height / block_size[0])
206 | n_width_blocks = int(pad_width / block_size[1])
207 | hists = np.zeros((n_height_blocks, n_width_blocks, bins))
208 | beta_thresholds = np.zeros((n_height_blocks, n_width_blocks))
209 | result = np.zeros_like(gray_image)
210 | for ii in range(n_height_blocks):
211 | for jj in range(n_width_blocks):
212 |
213 | # Compute the block range and max block value
214 | max_val_block = gray_image[
215 | ii : ii + block_size[0], jj : jj + block_size[1]
216 | ].max()
217 | r_block = (
218 | max_val_block
219 | - gray_image[ii : ii + block_size[0], jj : jj + block_size[1]].min()
220 | )
221 |
222 | hists[ii, jj] = np.histogram(
223 | gray_image[ii : ii + block_size[0], jj : jj + block_size[1]], bins=bins
224 | )[0]
225 | beta_thresholds[ii, jj] = compute_clip_limit(
226 | gray_image[ii : ii + block_size[0], jj : jj + block_size[1]],
227 | alpha=alpha,
228 | pi=pi,
229 | R=R,
230 | )
231 | hists[ii, jj] = clip_and_redistribute_hist(
232 | hists[ii, jj], beta_thresholds[ii, jj]
233 | )
234 |
235 | pdf_min = hists[ii, jj].min()
236 | pdf_max = hists[ii, jj].max()
237 |
238 | weighted_hist = pdf_max * (hists[ii, jj] - pdf_min) / (pdf_max - pdf_min)
239 | weighted_cum_hist = np.cumsum(weighted_hist)
240 | pdf_sum = weighted_cum_hist[-1]
241 | weighted_cum_hist /= pdf_sum
242 |
243 | # Equalize histogram here!!!
244 | hists[ii, jj] = np.cumsum(hists[ii, jj])
245 | norm_cdf = hists[ii, jj] / hists[ii, jj, -1]
246 |
247 | # Compute Wen T1 and Gamma
248 | w_en = compute_w_en(l_max=glob_l_max, l_alpha=glob_l_a, cdf=norm_cdf)
249 | tau_1 = max_val_block * w_en * norm_cdf
250 | gamma = compute_gamma(
251 | l_max=glob_l_max, d_range=R, weighted_cdf=weighted_cum_hist
252 | )
253 |
254 | if r_block > delta:
255 | hists[ii, jj] = np.maximum(tau_1, gamma)
256 | else:
257 | hists[ii, jj] = gamma
258 |
259 | # Bilinear interpolation
260 | for i in range(pad_height):
261 | for j in range(pad_width):
262 | p_i = int(gray_image[i][j])
263 |
264 | # Get current block index
265 | block_x = i // block_size[0]
266 | block_y = j // block_size[1]
267 |
268 | # Get the central indexes of the running block
269 | center_block_x = block_x * block_size[0] + n_height_blocks // 2
270 | center_block_y = block_y * block_size[1] + n_width_blocks // 2
271 |
272 | # Compute the block coordinates to interpolate from
273 | block_y_a, block_y_c = compute_block_coords(
274 | center_block_x, block_x, i, n_height_blocks
275 | )
276 | block_x_a, block_x_b = compute_block_coords(
277 | center_block_y, block_y, j, n_width_blocks
278 | )
279 |
280 | # Block image coordinates
281 | y_a = block_y_c * block_size[0] + block_size[0] // 2
282 | y_c = block_y_a * block_size[0] + block_size[0] // 2
283 | x_a = block_x_a * block_size[1] + block_size[1] // 2
284 | x_b = block_x_b * block_size[1] + block_size[1] // 2
285 |
286 | m = compute_mn_factors((y_a, y_c), i)
287 | n = compute_mn_factors((x_a, x_b), j)
288 |
289 | Ta = hists[block_y_c, block_x_a, p_i]
290 | Tb = hists[block_y_c, block_x_b, p_i]
291 | Tc = hists[block_y_a, block_x_a, p_i]
292 | Td = hists[block_y_a, block_x_b, p_i]
293 |
294 | result[i, j] = int(
295 | m * (n * Ta + (1 - n) * Tb) + (1 - m) * (n * Tc + (1 - n) * Td)
296 | )
297 |
298 |
299 | result = result[unpad_indices]
300 | result = np.clip(result, 0, R)
301 | if ndim == 3:
302 | result = result / 255.0
303 | hsv_image[:, :, 2] = result
304 | return (255 * hsv2rgb(hsv_image)).astype(np.uint8)
305 | else:
306 | return result.astype(np.uint8)
307 |
308 |
309 | if __name__ == "__main__":
310 | path = "./images/streets.jpg"
311 | image = io.imread(path)
312 | equalized_image = dual_gamma_clahe(
313 | image.copy(), block_size=32, alpha=100.0, delta=50.0, pi=1.5, bins=256
314 | )
315 | # equalized_image = dual_gamma_clahe(image.copy(), block_size=32, alpha=10., delta=70., pi=1.5, bins=256)
316 | # equalized_image = dual_gamma_clahe(image.copy(), block_size=64, alpha=80., delta=40., pi=3.5, bins=256)
317 | fig, ax = plt.subplots(1, 2)
318 | if image.ndim == 2:
319 | cmap = "gray"
320 | else:
321 | cmap = None
322 | ax[0].imshow(image, cmap=cmap)
323 | ax[1].imshow(equalized_image, cmap=cmap)
324 | ax[0].set_title("Input Image")
325 | ax[1].set_title("Equalized Image ")
326 | ax[0].axis("off")
327 | ax[1].axis("off")
328 | plt.show()
329 |
--------------------------------------------------------------------------------