Single Image Haze Removal Using Dark Channel Prior 论文阅读与代码实现

He 提出暗通道去雾方法进行了详细的描述,该方法以大气散射模型为基础,利用暗通道先验原理求出全球大气光成分A和透射率t。先使用了软抠图对透射率图进行优化,但是运算时间过长。后来使用引导滤波精细化透射率图,缩短了一部分运算时间。

暗通道先验:

在绝大多数非天空的局部区域里,某一些像素总会有至少一个RGB颜色通道具有很低的值。换言之,该区域光强度的最小值是个很小的数,值接近于0。

实际生活中造成暗原色中低通道值主要有三个因素:

a)汽车、建筑物和城市中玻璃窗户的阴影,或者是树叶、树与岩石等自然景观的投影;

b)色彩鲜艳的物体或表面,在RGB的三个通道中有些通道的值很低(比如绿色的草地/树/植物,红色或黄色的花朵/叶子,或者蓝色的水面);

c)颜色较暗的物体或者表面,例如灰暗色的树干和石头。总之,自然景物中到处都是阴影或者彩色,这些景物的图像的暗原色总是很灰暗的。

我们给暗通道一个数学定义,对于任意的输入图像J,其暗通道可以用下式表达:

c表示图像R,G,B中的每个通道。

Jc表示彩色图像的某一通道 。

Ω(x)表示以像素X为中心的一个窗口。

 式(5)的意义用代码表达也很简单,求解过程如下:

(1)求出每个像素RGB分量中的最小值,存入一副和原始图像大小相同的灰度图中

(2)对这幅灰度图以15x15的窗口进行最小值滤波,即以每个窗口的最小值代替这个像素点的最小值。滤波的半径由窗口大小决           定(论文采用7),一般有WindowSize = 2 * Radius + 1; 

def darkChannel(src, r=15):
    
    temp = np.min(src,2)
    
    s = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (r,r))

    dst = cv2.erode(temp, s)
    
    return dst

举个栗子:

由上述几幅图像,可以明显的看到暗通道先验理论的普遍性。在作者的论文中,对5000多副无雾图像手动裁剪掉天空区域,重调整图片大小,使得图片长宽不大于500,Ω(x)的大小选取15*15,发现75%的暗通道的像素值都为0,90%的暗通道的像素值都低于25,很好验证暗通道理论的普适性,因此,我们可以认为其实一条定理。

 

大气散射模型(求解A与t(x)):

在计算机视觉和计算机图形中,下述方程所描述的雾图形成模型被广泛使用:

I(X)就是我们现在已经有的图像(待去雾的图像)。

J(x)是我们要恢复的无雾的图像。

A是全球大气光成分, t(x)为透射率。

将式(1)稍作处理,变形为下式(C表示R/G/B三个通道的意思):

首先假设在每一个窗口内透射率t(x)为常数,也就是假设在同一窗口的上的透射率是相同的,定义他为,且A值已经给定,然后对式(7)两边求两次最小值运算得到下式:

上式中,J是待求的无雾的图像,根据前述的暗原色先验理论有:

可推导出:

把式(10)带入式(8)中,得到:

这里使用guideFilter引导滤波,优化已经求得的通道图片,所以上式变为t=1-V/A,这里V为透射率图。

引导图应该与原图尽可能相似,取np.min(src,2)

也就是:V = guideFilter(np.min(m,2) , dc, r, eps),这里应该在[0,1]范围内进行,也就是引导图和预估的投射图都必须从[0,255]->[0,1]进行计算。导向滤波的r值应当不小于进行最小值滤波半径r的4倍

即使是晴天白云,空气中也存在着一些颗粒,因此,看远处的物体还是能感觉到雾的影响,另外,雾的存在让人类感到景深的存在,因此,有必要在去雾的时候保留一定程度的雾,这可以通过在式(11)中引入一个在[0,1] 之间的因子,则式(11)修正为:

注:文中所有的测试结果依赖于:  ω=0.95

上述推论中都是假设全球达气光A值时已知的,在实际中,我们可以借助于暗通道图来从有雾图像中获取该值。具体步骤如下:

(1) 从暗通道图中按照亮度的大小取前0.1%的像素。(经过导向滤波这应为投射率图)

(2)在这些位置中,在原始有雾图像I中寻找对应的具有最高亮度的点的值,作为A值。

使用累计灰度直方图来实现求A值

bins = 256  
hist = np.histogram(V, bins)                  #灰度直方图

这里V是待统计数据数组,bins为等分数。返回值有两个:

hist[0]:hist:array,返回数组V中的数据在每个等分区间的个数。

hist[1]:bin_degs,长度为len(hist)+1,为分组的边界。(bin_degs[0], bin_edgs[1])=hist[0]

normHist = hist[0]/float(V.size)              #归一化灰度直方图

概率直方图,灰度值k的像素点个数占的图比例

accumulativeHist = np.cumsum(normHist)    #累计直方图

代表图像组成成分在灰度级的累计概率分布情况,每一个概率值代表小于等于此灰度值的概率。

for k in range(bins-1, 0, -1):
        
        if accumulativeHist[k]<=0.999:             #取前0.1%的像素,并获得该位置k
            break 
        
A  = np.mean(m,2)[V>=hist[1][k]].max()        #在原始有雾的图像I中寻找对应的具有最高亮度的点的值

这里返回的k值就是边界值k。

到这一步,我们就可以进行无雾图像的恢复了。由式(1)可知:  J = ( I - A)/t + A  

现在I,A,t都已经求得了,因此,完全可以进行J的计算。

当投射图t 的值很小时,会导致J的值偏大,从而使淂图像整体向白场过度,因此一般可设置一阈值T0,当t值小于T0时,令t=T0,本文中所有效果图均以T0=0.1为标准计算。

因此,最终的恢复公式如下:

Python代码实现:

# -*- coding: utf-8 -*-
"""
Created on Thu Nov 15 16:49:29 2018

@author: x
"""
import cv2  
import numpy as np 
import os.path
import glob

# time start
t1 = cv2.getTickCount() 
   
def darkChannel(src, r=15):
    temp = np.min(src,2)
    s = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (r,r))
    dst = cv2.erode(temp, s) 
    return dst

def guideFilter(I, p, r, eps):
    #I的均值平滑
    mean_I = cv2.blur(I, (r, r))
    #p的均值平滑
    mean_p = cv2.blur(p, (r, r))
    #I*I和I*p的均值平滑
    mean_II = cv2.blur(I*I, (r, r))
    mean_Ip = cv2.blur(I*p, (r, r))
    #方差
    var_I = mean_II - mean_I * mean_I #方差公式
    #协方差
    cov_Ip = mean_Ip - mean_I * mean_p
    a = cov_Ip / (var_I + eps)
    b = mean_p - a*mean_I
    #对a、b进行均值平滑
    mean_a = cv2.blur(a, (r, r))
    mean_b = cv2.blur(b, (r, r))
    q = mean_a*I + mean_b
    return q

def getParameter(m, r, eps, w, t0):  #输入rgb图像,值范围[0,1]  
    dc = darkChannel(m)                           #得到暗通道图像
    V = guideFilter(np.min(m,2) , dc, r, eps)     #使用引导滤波优化,获得透射率图V
    bins = 256  
    hist = np.histogram(V, bins)                  #灰度直方图
    normHist = hist[0]/float(V.size)              #归一化灰度直方图,即概率直方图,灰度值k的像素点个数占的图比例
    accumulativeHist = np.cumsum(normHist)         #累计直方图,代表图像组成成分在灰度级的累计概率分布情况,
                                                   #每一个概率值代表小于等于此灰度值的概率
    for k in range(bins-1, 0, -1):
        
        if accumulativeHist[k]<=0.999:             #取前0.1%的像素,并获得该位置k
            break 
        
    A  = np.mean(m,2)[V>=hist[1][k]].max()        #在原始有雾的图像I中寻找对应的具有最高亮度的点的值
    #if A > 220/255.0:                               #全球大气光成分最大值取220
     #   A = 220/255.0      
    V = V*w                                       #修正因子
    t = 1 - V/A
    t = np.maximum(t, t0)   
    return t,A
       
def hazeRemoval(m, r=75, eps=0.001, w=0.95, t0=0.10, bGamma=False):  
    
    J = np.zeros(m.shape)  
    t, A = getParameter(m, r, eps, w, t0)               #得到遮罩图像和大气光照      
    for k in range(3):  
        J[:,:,k] = (m[:,:,k]-A)/t + A                  #三通道去雾还原图片
        
    J =  np.clip(J, 0, 1)                             #将值限制在[0,1]之间,因为图片已经归一化了    
    if bGamma:  
        J = J**(np.log(0.5)/np.log(J.mean()))       #gamma校正,默认不进行该操作  
    return J
        
def batchProcess(jpgfile,outdir):
    
    img = cv2.imread(jpgfile, cv2.IMREAD_ANYCOLOR)    
    try:
        new_img= hazeRemoval(img/255.0)*255 
        cv2.imwrite(os.path.join(outdir,os.path.basename(jpgfile)), new_img)
        
    except Exception as e:
        print(e)
        
if __name__ == '__main__':
    for jpgfile in glob.glob(r'C:\Users\x\Desktop\kk\*.jpg'):
        batchProcess(jpgfile,r'C:\Users\x\Desktop\t')
    
# time end
t2 = cv2.getTickCount()
    
t = (t2-t1)/cv2.getTickFrequency()
print ("耗时为:")
print (t)

实验结果对比:

猜你喜欢

转载自blog.csdn.net/qq_40755643/article/details/83347135