OpenCV—Python 暗通道图像去雾算法

一、前言

何恺明的暗通道先验(dark channel prior)去雾算法是CV界去雾领域很有名的算法,关于该算法的论文"Single Image Haze Removal Using Dark Channel Prior"一举获得2009年CVPR最佳论文。作者统计了大量的无雾图像,发现一条规律:每一幅图像的RGB三个颜色通道中,总有一个通道的灰度值很低,几乎趋向于0。基于这个几乎可以视作是定理的先验知识,作者提出暗通道先验的去雾算法。

其暗通道的数学表达式为:
J d a r k ( x ) = min ⁡ y ∈ Ω ( x ) [ min ⁡ y ∈ Ω ( x ) J c ( y ) ] J^{dark}(x) = \min_{y \in \Omega (x)}\left [ \min_{y \in \Omega (x)}J^c(y) \right ] Jdark(x)=yΩ(x)min[yΩ(x)minJc(y)]

式中: J c J^c Jc 图像每个通道, Ω ( x ) \Omega (x) Ω(x) 表示以像素x为中心的窗口。
原理:先取图像中每一个像素的三通道中的灰度值的最小值,得到一幅灰度图像,再在这幅灰度图像中,以每一个像素为中心取一定大小的矩形窗口,取矩形窗口中灰度值最小值代替中心像素灰度值,从而得到输入图像的暗通道图像。
滤波窗口大小:WindowSize = 2*Radius + 1。
暗通道先验理论: J d a r k → 0 J^{dark} \to 0 Jdark0

二、暗通道去雾原理

去雾模型 I ( x ) = J ( x ) t ( x ) + A [ 1 − t ( x ) ] I(x) = J(x)t(x) + A[1-t(x)] I(x)=J(x)t(x)+A[1t(x)]
其中: I ( x ) I(x) I(x)为原图,待去雾图像。 J ( x ) J(x) J(x)是要恢复的无雾图像。A是大气光成分, t ( x ) t(x) t(x) 透光率。
对于成像模型,将其归一化,即两边同时除以每个通道的大气光值:
I c ( x ) A c = t ( x ) J c ( x ) A c + 1 − t ( x ) \frac{I^c(x)}{A^c} = t(x)\frac{J^c(x)}{A^c}+1- t(x) AcIc(x)=t(x)AcJc(x)+1t(x)

假设大气光A为已知量, t ( x ) t(x) t(x) 透光率为常数,将其定义为 t ~ ( x ) \widetilde{t}(x) t (x) 对上式两边两次最小化运算:
min ⁡ y ∈ Ω ( x ) [ min ⁡ c I c ( y ) A c ] = t ~ ( x ) min ⁡ y ∈ Ω ( x ) [ min ⁡ c J c ( y ) A c ] + 1 − t ~ ( x ) \min_{y \in \Omega (x)}\left [ \min_c \frac{I^c(y)}{A^c} \right ] = \widetilde{t}(x) \min_{y \in \Omega (x)}\left [ \min_c \frac{J^c(y)}{A^c} \right ] +1-\widetilde{t}(x) yΩ(x)min[cminAcIc(y)]=t (x)yΩ(x)min[cminAcJc(y)]+1t (x)
根据 暗通道先验理论: J d a r k → 0 J^{dark} \to 0 Jdark0 有:
J d a r k ( x ) = min ⁡ y ∈ Ω ( x ) [ min ⁡ c J c ( y ) ] = 0 J^{dark}(x) = \min_{y \in \Omega (x)}[ \min_cJ^c(y)] = 0 Jdark(x)=yΩ(x)min[cminJc(y)]=0

推出:
min ⁡ y ∈ Ω ( x ) [ min ⁡ c I c ( y ) A c ] = 0 \min_{y \in \Omega (x)}\left [ \min_c \frac{I^c(y)}{A^c} \right ] =0 yΩ(x)min[cminAcIc(y)]=0

代回原式,得到:
t ~ ( x ) = 1 − min ⁡ y ∈ Ω ( x ) [ min ⁡ c I c ( y ) A c ] \widetilde{t}(x) = 1- \min_{y \in \Omega (x)}\left [ \min_c \frac{I^c(y)}{A^c} \right ] t (x)=1yΩ(x)min[cminAcIc(y)]

为了防止去雾太过彻底,恢复出的景物不自然,应引入参数 ω = 0.95 \omega = 0.95 ω=0.95,重新定义传输函数为:
t ~ ( x ) = 1 − ω min ⁡ y ∈ Ω ( x ) [ min ⁡ c I c ( y ) A c ] \widetilde{t}(x) = 1- \omega \min_{y \in \Omega (x)}\left [ \min_c \frac{I^c(y)}{A^c} \right ] t (x)=1ωyΩ(x)min[cminAcIc(y)]

上述推论假设大气 A A A为已知量。实际中,可借助于暗通道图从有雾图中获取该值。具体步骤大致为:从暗通道图中按照亮度大小提取最亮的额前0.1%像素。然后在原始尤物图像 I I I 中寻找对应位置最高两点的值,作为 A A A值。至此,可以进行无雾图像恢复了。
考虑到当透射图 t t t 值很小时,会导致J值偏大,使整张图向白场过度,故设置一个阈值 t 0 t_0 t0,当 t t t < t 0 t_0 t0时,令 t t t = t 0 t_0 t0(示例程序 t 0 = 0.1 t_0 = 0.1 t0=0.1
最终公式:
J ( x ) = I ( x ) − A max ⁡ [ t ( x ) , t 0 ] + A J(x) = \frac{I(x) - A }{\max [ t(x),t_0]} + A J(x)=max[t(x),t0]I(x)A+A

导向滤波方法
​ 对于求解得到的传输函数,应用滤波器对其进行细化,文章中介绍的方法是软抠图的方法,此方法过程复杂,速度缓慢,因此采用导向滤波对传输函数进行滤波。导向滤波的原理此处不再赘述,详情:https://blog.csdn.net/wsp_1138886114/article/details/84228939

import cv2
import numpy as np




def zmMinFilterGray(src, r=7):
    '''最小值滤波,r是滤波器半径'''
    return cv2.erode(src, np.ones((2 * r + 1, 2 * r + 1)))


def guidedfilter(I, p, r, eps):
    height, width = I.shape
    m_I = cv2.boxFilter(I, -1, (r, r))
    m_p = cv2.boxFilter(p, -1, (r, r))
    m_Ip = cv2.boxFilter(I * p, -1, (r, r))
    cov_Ip = m_Ip - m_I * m_p

    m_II = cv2.boxFilter(I * I, -1, (r, r))
    var_I = m_II - m_I * m_I

    a = cov_Ip / (var_I + eps)
    b = m_p - a * m_I

    m_a = cv2.boxFilter(a, -1, (r, r))
    m_b = cv2.boxFilter(b, -1, (r, r))
    return m_a * I + m_b


def Defog(m, r, eps, w, maxV1):                 # 输入rgb图像,值范围[0,1]
    '''计算大气遮罩图像V1和光照值A, V1 = 1-t/A'''
    V1 = np.min(m, 2)                           # 得到暗通道图像
    Dark_Channel = zmMinFilterGray(V1, 7)
    cv2.imshow('20190708_Dark',Dark_Channel)    # 查看暗通道
    cv2.waitKey(0)
    cv2.destroyAllWindows()

    V1 = guidedfilter(V1, Dark_Channel, r, eps)  # 使用引导滤波优化
    bins = 2000
    ht = np.histogram(V1, bins)                  # 计算大气光照A
    d = np.cumsum(ht[0]) / float(V1.size)
    for lmax in range(bins - 1, 0, -1):
        if d[lmax] <= 0.999:
            break
    A = np.mean(m, 2)[V1 >= ht[1][lmax]].max()
    V1 = np.minimum(V1 * w, maxV1)               # 对值范围进行限制
    return V1, A


def deHaze(m, r=81, eps=0.001, w=0.95, maxV1=0.80, bGamma=False):
    Y = np.zeros(m.shape)
    Mask_img, A = Defog(m, r, eps, w, maxV1)             # 得到遮罩图像和大气光照

    for k in range(3):
        Y[:,:,k] = (m[:,:,k] - Mask_img)/(1-Mask_img/A)  # 颜色校正
    Y = np.clip(Y, 0, 1)
    if bGamma:
        Y = Y ** (np.log(0.5) / np.log(Y.mean()))       # gamma校正,默认不进行该操作
    return Y


if __name__ == '__main__':
    m = deHaze(cv2.imread('20190708.png') / 255.0) * 255
    cv2.imwrite('20190708_02.png', m)

在这里插入图片描述

代码优化参数
  • 以上代码优化,可以使用快速导向滤波减少时间复杂度,从而减少运行时间
  • 暗通道最小值滤波半径r。
    这个半径对于去雾效果是有影响的。一定情况下,半径越大去雾的效果越不明显,建议的范围一般是5-25之间,一般选择5,7,9等就会取得不错的效果。
  • w的影响自然也是很大的。
    这个值是我们设置的保留雾的程度(c++代码中w是去除雾的程度,一般设置为0.95就可以了)。这个基本不用修改。
  • 导向滤波中均值滤波半径。
    这个半径建议取值不小于求暗通道时最小值滤波半径的4倍。因为前面最小值后暗通道时一块一块的,为了使得透射率图更加精细,这个r不能过小(很容易理解,如果这个r和和最小值滤波的一样的话,那么在进行滤波的时候包含的块信息就很少,还是容易出现一块一块的形状)。

关于去雾的其他方法参考:https://blog.csdn.net/wsp_1138886114/article/details/83793331

鸣谢
https://blog.csdn.net/baimafujinji/article/details/27206237
https://blog.csdn.net/baimafujinji/article/details/30060161
https://www.cnblogs.com/rust/p/10457915.html

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 1024 设计师:上身试试 返回首页