暗通道先验去雾算法是何恺明2009年发表在CVPR上的一篇论文,还获得了当年的CVPR最佳论文
原文链接:IEEE Xplore Full-Text PDF:
本篇论文提出了一种简单但是有效的图像先验条件——暗通道先验法从一幅输入图像中去雾。暗通道先验是一种对于大量户外有雾图像的统计结果,**它最重要的一个观察结果是户外无雾图像的绝大部分区域包含某些像素的亮度值至少在某一个通道上是非常低的。**结合这个先验条件与雾天图像模型,我们可以直接估计雾的厚度并且回复一幅高质量的无雾图像。基于各种各样的有雾图像的实验去雾结果证明了所提出先验条件的有效性。并且,一幅高质量的图像深度图可作为图像去雾的附带产品。
在计算机视觉和计算机图形学中,方程所描述的大气散射模型被广泛使用。参数解释如下:
x是图像的空间坐标
I(x)代表有雾图像(待去雾图像)
J(x)代表无雾图像(待恢复图像)
A代表全球大气光值
t(x)代表透射率
方程右边第一项为场景直接衰减项,第二项为环境光项。
在绝大多数非天空的局部区域中,某些像素总会至少有一个颜色通道的值很低。对于一幅图像J(x),其暗通道的数学定义表示如下:
其中,Ω(x)表示以x为中心的局部区域,上标c表示RGB三个通道。该公式的意义用代码表达也很简单,首先求出每个像素RGB分量中的最小值,存入一副和原始图像大小相同的灰度图中,然后再对这幅灰度图进行最小值滤波,滤波的半径由窗口大小决定。
暗通道先验理论指出:对于非天空区域的无雾图像J(x)的暗通道趋于0,即:
实际生活中造成暗原色中低通道值主要有三个因素:
总之,自然景物中到处都是阴影或者彩色,这些景物的图像的暗原色总是很灰暗的,而有雾的图像较亮。因此,可以明显的看到暗通道先验理论的普遍性。
根据大气散射模型,将公式稍变形为下式:
假设每一个窗口的透射率t(x)为常数,记为t’(x),并且A值已给定,对式两边同时进行两次最小值运算,可得:
其中,J(x)是要求的无雾图像,根据前述的暗通道先验理论可知:
因此可推导出:
将上式带入可得到透射率t’(x)的预估值,如下所示:
现实生活中,即便晴空万里,空气中也会存在一些颗粒,在眺望远处的景物时,人们还是能感觉到雾的存在。另外,雾的存在让人们感受到景深,因此在去雾的同时有必要保留一定程度的雾。可以通过引入一个0到1之 间 的 因 子 w(一 般取0.95)对预估透射率进行修正,如式所示:
以上的推导过程均假设大气光值A是已知的,在实际中,可以借助暗通道图从原始雾图中求取。具体步骤如下:
先求取暗通道图,在暗通道图中按照亮度的大小提取最亮的前0.1%的像素
在原始雾图I(x)中找对应位置上具有最高亮度的点的值,作为大气光值A
此外,由于透射率t偏小时,会造成J偏大,恢复的无雾图像整体向白场过度,因此有必要对透射率设置一个下限值t0(一般取值为0.1),当t值小于t0 时,取t=t0。将以上求得的透射率和大气光值代入公式,最终整理得到图像的恢复公式如下:
import cv2
import numpy as np
import os
#计算雾化图像的暗通道
def dark_channel(img, size = 15):
r, g, b = cv2.split(img)
min_img = cv2.min(r, cv2.min(g, b))#取最暗通道
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (size, size))
dc_img = cv2.erode(min_img,kernel)
return dc_img
#估计全局大气光值
def get_atmo(img, percent = 0.001):
mean_perpix = np.mean(img, axis = 2).reshape(-1)
mean_topper = mean_perpix[:int(img.shape[0] * img.shape[1] * percent)]
return np.mean(mean_topper)
#估算透射率图
def get_trans(img, atom, w = 0.95):
x = img / atom
t = 1 - w * dark_channel(x, 15)
return t
#引导滤波
def guided_filter(p, i, r, e):
"""
:param p: input image
:param i: guidance image
:param r: radius
:param e: regularization
:return: filtering output q
"""
#1
mean_I = cv2.boxFilter(i, cv2.CV_64F, (r, r))
mean_p = cv2.boxFilter(p, cv2.CV_64F, (r, r))
corr_I = cv2.boxFilter(i * i, cv2.CV_64F, (r, r))
corr_Ip = cv2.boxFilter(i * p, cv2.CV_64F, (r, r))
#2
var_I = corr_I - mean_I * mean_I
cov_Ip = corr_Ip - mean_I * mean_p
#3
a = cov_Ip / (var_I + e)
b = mean_p - a * mean_I
#4
mean_a = cv2.boxFilter(a, cv2.CV_64F, (r, r))
mean_b = cv2.boxFilter(b, cv2.CV_64F, (r, r))
#5
q = mean_a * i + mean_b
return q
def dehaze(im):
img = im.astype('float64') / 255
img_gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY).astype('float64') / 255
atom = get_atmo(img)
trans = get_trans(img, atom)
trans_guided = guided_filter(trans, img_gray, 20, 0.0001)
trans_guided = cv2.max(trans_guided, 0.25)
result = np.empty_like(img)
for i in range(3):
result[:, :, i] = (img[:, :, i] - atom) / trans_guided + atom
return result*255
def dehaze_V2(originPath,savePath):
'''originaPath:文件夹的路径,图片上一级
savePath:同理'''
for image_name in os.listdir(originPath):
image_path = os.path.join(originPath,image_name)
im = cv2.imread(image_path)
img = im.astype('float64') / 255
img_gray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY).astype('float64') / 255
atom = get_atmo(img)
trans = get_trans(img, atom)
trans_guided = guided_filter(trans, img_gray, 20, 0.0001)
trans_guided = cv2.max(trans_guided, 0.25)
result = np.empty_like(img)
for i in range(3):
result[:, :, i] = (img[:, :, i] - atom) / trans_guided + atom
oneSave = os.path.join(savePath,image_name)
cv2.imwrite(oneSave, result*255)
# cv2.imshow("source",img)
# cv2.imshow("result", result)
cv2.waitKey(0)
if __name__ == '__main__':
dehaze_V2(r'./input/set1','./output/set1')