Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
test_images/*
src/test_images/*
test_results/*
src/test_results/*
__pycache__
dehazing.zip
conv2to3.py
.vscode
src/debug.*
debug.*
poetry-convert.py
122 changes: 122 additions & 0 deletions AImage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""
Fast Single Image Haze Removal Using Dark Channel Prior
Original by https://github.com/cssartori

@author Philip Kahn
@date 20200501
"""

import numpy as np
from skimage.io import imread, imsave, imshow

#A class to hold an Image
class AImage(object):
"""
Class to hold an Image.
"""
#img_array is the matrix of pixels in img
__img_array__ = None
#filename is the path to the image in img
__filename__ = None

def __init__(self, img=None, filename=None):
self.__filename__ = filename
self.__img_array__ = img

def height(self) -> int:
"""
Get the image height
"""
return self.__img_array__.shape[0]

def width(self) -> int:
"""
Get the image width
"""
return self.__img_array__.shape[1]

def colors(self) -> int:
"""
Get the number of color channels
"""
return self.__img_array__.shape[2]

def filename(self) -> str:
"""
Get the working file name
"""
return self.__filename__

def __getitem__(self, index) -> np.ndarray:
"""
Get a slice of the image
"""
return self.__img_array__[index]

def __setitem__(self, index, value):
"""
Set a slice of the image
"""
self.__img_array__[index] = value

def array(self) -> np.ndarray:
"""
Get the image array
"""
return self.__img_array__

def show(self):
"""
Show the internal image
"""
imshow(self.__img_array__)

@staticmethod
def fromarray(array):
"""
Returns a standardized image
"""
#check if the array is float type
if array.dtype != np.float64:
#cast to float with values from 0 to 1
array = np.divide(np.asfarray(array), 255.0)
sImage = AImage(array)
return sImage

@staticmethod
def open(filename) -> np.ndarray:
"""
Get an image array from an image file
"""
try:
array = imread(filename)
img = AImage.load(array, filename)
except (IOError, PermissionError, FileNotFoundError):
raise IOError(f"Couldn't access file {filename}")
return img

@staticmethod
def load(array:np.ndarray, filename:str= None):
"""
Get a convenient AImage from an ndarray
"""
#check if the array is float type
if array.dtype != np.float64:
#cast to float with values from 0 to 1
array = np.divide(np.asfarray(array), 255).astype(np.float64)
return AImage(array, filename)

@staticmethod
def save(im, filename):
"""
Save an image
"""
if isinstance(im, AImage):
imsave(filename, im.array())
sImage = AImage(im.array(), filename)
elif isinstance(im, np.ndarray):
imsave(filename, im)
sImage = AImage(im, filename)
else:
raise TypeError('im parameter should be either a np.ndarray or AImage.AImage')
return sImage
47 changes: 47 additions & 0 deletions AtmLight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Fast Single Image Haze Removal Using Dark Channel Prior
Original by https://github.com/cssartori

@author Philip Kahn
@date 20200501
"""

import numpy as np
from numba import jit

@jit
def estimate(imageArray:np.ndarray, jDark:np.ndarray, px:float= 1e-3) -> np.ndarray:
"""
Automatic atmospheric light estimation. According to section (4.4) in the reference paper
http://kaiminghe.com/cvpr09/index.html

Parameters
-----------

imageArray: np.ndarray
an H*W RGB hazed image

jDark: np.ndarray
the dark channel of imageArray

px: float (default=1e-3, i.e. 0.1%)
the percentage of brighter pixels to be considered

Return
-----------
The atmosphere light estimated in imageArray, A (a RGB vector).
"""
#reshape both matrix to get it in 1-D array shape
imgAVec = np.resize(imageArray, (imageArray.shape[0]*imageArray.shape[1], imageArray.shape[2]))
jDarkVec = np.reshape(jDark, jDark.size)
#the number of pixels to be considered
numPixels = np.int(jDark.size * px)
#index sort the jDark channel in descending order
isJD = np.argsort(-jDarkVec)
arraySum = np.array([0.0, 0.0, 0.0])
for i in range(0, numPixels):
arraySum[:] += imgAVec[isJD[i],:]
arr:np.ndarray = np.array([0.0, 0.0, 0.0])
arr[:] = arraySum[:]/numPixels
#returns the calculated airlight arr
return arr
49 changes: 49 additions & 0 deletions DarkChannel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""
Fast Single Image Haze Removal Using Dark Channel Prior
Original by https://github.com/cssartori

@author Philip Kahn
@date 20200501
"""

import numpy as np
from numba import jit, njit, prange

@jit
def estimate(imageArray:np.ndarray, ps:int= 15) -> np.ndarray:
"""
Dark Channel estimation. According to equation (5) in the reference paper
http://research.microsoft.com/en-us/um/people/kahe/cvpr09/

Parameters
-----------

imageArray: np.ndarray
an H*W RGB hazed image

ps: int
the patch size (a patch P(x) has size (ps x ps) and is centered at pixel x)

Return
-----------
The dark channel estimated in imageArray, jDark (a matrix H*W).
"""
offset = ps // 2
#Padding of the image to have windows of ps x ps size centered at each image pixel
imPad = np.pad(imageArray, [(offset, offset), (offset, offset), (0, 0)], 'edge')
return getJDark(offset, np.empty(imageArray.shape[:2]), imPad)

@njit(parallel=True, cache= True)
def getJDark(offset:int, jDark:tuple, paddedImage:np.ndarray) -> np.ndarray:
"""
Get the dark channel
"""
#jDark is the Dark channel to be found
for i in prange(offset, (jDark.shape[0]+offset)): #pylint: disable= not-an-iterable
for j in prange(offset, (jDark.shape[1]+offset)): #pylint: disable= not-an-iterable
#creates the patch P(x) of size ps x ps centered at x
patch = paddedImage[i-offset:i+1+offset, j-offset:j+1+offset]
#selects the minimum value in this patch and set as the dark channel of pixel x
jDark[i-offset, j-offset] = patch.min()

return jDark
113 changes: 113 additions & 0 deletions Dehaze.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!python3
"""
Fast Single Image Haze Removal Using Dark Channel Prior
Original by https://github.com/cssartori

Main References
[1] Single Image Haze Removal Using Dark Channel Prior (2009)
http://kaiminghe.com/cvpr09/index.html

[2] Guided Image Filtering (2010)
http://kaiminghe.com/eccv10/index.html

@author Philip Kahn
@date 20200501
"""

import numpy as np
from typing import Optional, Tuple, Union
try:
import DarkChannel
import AtmLight
import Transmission
import Refine
import Radiance
from timeit_local import timeit
except (ModuleNotFoundError, ImportError):
# pylint: disable= relative-beyond-top-level
from . import DarkChannel
from . import AtmLight
from . import Transmission
from . import Refine
from . import Radiance
from .timeit_local import timeit



def dehaze(imageArray:np.ndarray, a:Optional[np.ndarray]= None, t:Optional[np.ndarray]= None, rt:Optional[np.ndarray]= None, tmin:float= 0.1, ps:int= 15, w:float= 0.95, px:float= 1e-3, r:int= 40, eps:float= 1e-3, m:bool= False, returnLight:bool= False) -> Union[np.ndarray, Tuple[np.ndarray, float]]: #pylint: disable= unused-argument
"""
Application of the dehazing algorithm, as described in section (4) of the reference paper
http://kaiminghe.com/cvpr09/index.html

Parameters
-----------

imageArray: np.ndarray
an H*W RGB hazed image

a: np.ndarray (default= None, will be calculated internally)
the atmospheric light RGB array of imageArray

t: np.ndarray (default= None, will be calculated internally)
the transmission matrix H*W of imageArray

rt: np.ndarray (default= None, will be calculated internally)
the raw transmission matrix H*W of imageArray, to be refined

tmin: float (default=0.1)
the minimum value the transmission can take

ps: int (default=15)
the patch size for dark channel estimation

w: float (default=0.95)
the omega weight, amount of haze to be kept

px: float (default=1e-3, i.e. 0.1%)
the percentage of brightest pixels to be considered when estimating atmospheric light

r: int (default=40)
the radius of the guided filter in pixels

eps: float (default=1e-3)
the epsilon parameter for guided filter

m:
print out messages along processing

returnLight: bool (default= False)
Return the light sum along with the array

Return
-----------
The dehazed image version of imageArray, dehazed (a H*W RGB matrix).
"""
def doNothing(*args, **kwargs): #pylint: disable= unused-argument
return
if m:
timeDisp = print
else:
timeDisp = doNothing
with timeit("\tDark channel estimated in", logFn= timeDisp):
jDark = DarkChannel.estimate(imageArray, ps)
#return jDark
#if no atmospheric given
if a is None:
with timeit("\tAtmospheric light estimated in", logFn= timeDisp):
a = AtmLight.estimate(imageArray, jDark)
#if no raw transmission and complete transmission given
if rt is None and t is None:
with timeit("\tTransmission estimated in", logFn= timeDisp):
rt = Transmission.estimate(imageArray, a, w)
#threshold of raw transmission
rt = np.maximum(rt, tmin)
#if no complete transmission given, refine the raw using guided filter
if t is None:
with timeit("\tRefinement filter run in", logFn= timeDisp):
t = Refine.guided_filter(imageArray, rt)
#recover the scene radiance
with timeit("\tRadiance recovery in", logFn= timeDisp):
dehazed = Radiance.recover(imageArray, a, t, tmin)
if returnLight:
return dehazed, np.sum(a)
return dehazed
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 Philip Kahn

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading