描述

上一篇文章,对何恺明的论文进行了逐行分析,但总体内容还是有些繁琐,不利于学习总结。
本篇将重点思想按照工程顺序给出,便于理解去雾的核心思想。我还列出了python版本的实现,是我按照论文的公式去复现的。

算法核心思想

1. 雾的模型

有雾的图像,与正常图像有什么区别呢?作者结合参考文献,给出了这样的公式

意思是说:我们拿到的有雾图像素值,是正确图像经过一个t的衰减,增加一个大气光引起的增益后的像素值。

这是论文的基础公式

2. 暗通道先验

作者对5000张没有雾的图片进行了统计,发现了这样一件事情:

除了天空等一些特殊情况,图片中每个像素总有一个颜色通道的值会比较低

同时,有雾的图像是不满足这个先验的。也就是说,如果提取每个像素的最低颜色通道值,无雾图会比较黑,有雾图像会比较白。

暗通道先验是论文的核心思想

3. 明确去雾问题

我们去雾的目标,就是已知有雾图像I,想办法求出大气光A和透射t,最终得到去雾图像J

根据论文里的计算,这个关系的公式就是:

t_0取值为0.1,x为每个像素点。如公式所示,如果我们知道了大气光A和透射t,就可以还原无雾图像了。

4. 确定大气光A

根据统计或者可以说是分析,我们发现天空的颜色总是与大气光接近,天空的暗通道会很亮同时透射率很接近0。

因此何恺明这样来确定大气光:求出原始有雾图像的暗通道图像,在暗通道图像中找到像素值最高的、占总像素数目前0.1%的像素点位置,这些位置中原始图像像素值最高的那个像素点,就是大气光A

这样做,我们就有可能排除图中蓝色框中的错误选择,显然像素值最高的白车和白建筑并不是A。黄色框是暗通道最亮的0.1%,这些位置中在原始图像最亮的像素点才最可能是大气光

5. 根据大气光计算出透射率

有了大气光A之后,还要计算的就是透射率。

每个像素点,都有一个对应的透射值,公式如下:

\omega取值为0.95,Ω(x)为像素的15×15邻域,c代表颜色rgb三个通道

6. 透射率需要优化

何恺明提到,因为假设的15×15邻域透射率一致并不一定成立,所以计算出的透射率t并不是最优的。何恺明在去雾文章使用了Soft Matting的方式优化,看图会发现暗通道的边缘信息被很好的保留了下来。但这种方法速度过慢,实现起来也稍微复杂些。


而后在2010年何恺明又提出了导向滤波的方式,计算速度更快效果更好。

工程实现步骤

为了复现这篇有意思的文章,我按照文章的意思编写了python程序来复现。具体的工程步骤如下:

  1. 求出原始图像的暗通道图像
  2. 暗通道图像中的像素值进行升序,取像素值大小前0.1%,再从原始图中确定大气光A
  3. 利用大气光A,求出每个像素点的透射率
  4. 透射率t优化(使用导向滤波)
  5. 根据大气光A和透视率t,计算出了无雾图像

值得说明的一点就是:由于Soft Matting复现复杂且速度过慢(还由于我比较懒,Soft matting得写很多行代码的。。。),透射率t的优化部分,我直接借鉴了一个开源的导向滤波函数。透射率优化这步,虽然对结果有重要影响,但不影响理解何恺明的思想。为我的偷懒说一句抱歉。

论文复现(python版本)

import cv2
import numpy as np
import sys
import copy
# 计算大气光A
def estimating_atmospheric_light(image, dark_channel):
    i = dark_channel.shape
    num_sum = dark_channel.shape[0] * dark_channel.shape[1]
    num = int(num_sum * 0.001)
    print("image has " + str(num_sum) + " points. 0.1% has " + str(num) + " points")

    pixels = []  # 储存全部的像素值
    for i in range(dark_channel.shape[0]):
        for j in range(dark_channel.shape[1]):
            pixels.append(dark_channel[i][j])

    pixels_sorted_id = sorted(range(len(pixels)), key=lambda k: pixels[k])  # 像素值排序,储存升序后的id

    value_threshold = pixels[pixels_sorted_id[len(pixels)-num]]
    print("dark channel value >= " + str(value_threshold) + " belong 0.1%")

    max_value = 0
    row = 0
    col = 0
    for i in range(dark_channel.shape[0]):
        for j in range(dark_channel.shape[1]):
            if dark_channel[i][j] > value_threshold:
                value = int(image[i][j][0]) + int(image[i][j][1]) + int(image[i][j][2])
                if value > max_value:
                    max_value = value
                    row = i
                    col = j

    A = image[row][col]
    print("A at: [" + str(row) + ", " + str(col) + "], value: " + str(A))
    # cv2.circle(image, (col, row), 10, (0, 0, 255), 1)
    return A

# 计算图像的暗通道
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 guidedFilter(p, i, r, e):
    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))

    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

    mean_a = cv2.boxFilter(a, cv2.CV_64F, (r, r))
    mean_b = cv2.boxFilter(b, cv2.CV_64F, (r, r))

    q = mean_a * i + mean_b
    return q

# 去雾步骤
def dehaze(image):
    dark_prior = dark_channel(image)  # 原图像的暗通道
    cv2.imshow("dark channel of origin", dark_prior)

    A = estimating_atmospheric_light(image, dark_prior)  # 确定大气光

    t = image / A
    t_min = dark_channel(t)

    # 导向滤波优化image/A的暗通道
    img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY).astype('float64') / 255
    t_min = guidedFilter(t_min, img_gray, 225, 0.0001)
    cv2.imshow("transmission", t_min)

    # 计算透射率
    t_x = [[0 for i in range(t_min.shape[1])] for i in range(t_min.shape[0])]
    for i in range(t_min.shape[0]):
        for j in range(t_min.shape[1]):
            t_x[i][j] = 1 - 0.95 * t_min[i][j]

    # 计算去雾图
    image_noHaze = copy.copy(image)
    for i in range(image_noHaze.shape[0]):
        for j in range(image_noHaze.shape[1]):
            image_noHaze[i][j][0] = max(min((int(image[i][j][0]) - int(A[0])) / max(0.1, t_x[i][j]) + int(A[0]), 255), 0)
            image_noHaze[i][j][1] = max(min((int(image[i][j][1]) - int(A[1])) / max(0.1, t_x[i][j]) + int(A[1]), 255), 0)
            image_noHaze[i][j][2] = max(min((int(image[i][j][2]) - int(A[2])) / max(0.1, t_x[i][j]) + int(A[2]), 255), 0)

    image_noHaze = np.uint8(image_noHaze)

    return image_noHaze


if __name__ == '__main__':
    image_origin = cv2.imread('2.jpg')
    print("origin image shape: " + str(image_origin.shape))
    result = dehaze(image_origin)

    cv2.imshow("origin", image_origin)
    cv2.imshow("result", result)
    cv2.imwrite('defog.jpg', result)

    while True:
        k = cv2.waitKey(1)
        if k == 27:  # esc键
            break
    sys.exit(0)

代码结果

原图

去雾后

总结

这篇文章针对何恺明去雾论文,给出了python版本的复现。代码可以直接运行。

写的不容易,欢迎各位朋友点赞并加关注,谢谢!