霧霾模糊?影像增強教你如何去霧

摘要:詳細介紹影像去霧演算法,經過影像增強後的影像也能應用於目標檢測、影像分類或物聯網檢測等領域,並且效果更好。

本文分享自華為雲社區《影像預處理之影像去霧詳解》,作者: eastmount。

一.影像去霧

隨著社會的發展,環境污染逐漸加劇,越來越多的城市頻繁出現霧霾,這不僅給人們的身體健康帶來危害,還給那些依賴影像資訊的電腦視覺系統造成了不良影響,因為在霧天採集到的影像對比度和飽和度均較低,顏色易發生偏移與失真等。因此,尋找一種簡單有效的影像去霧方法,對電腦視覺的後續研究至關重要。

該部分主要從下列幾篇論文摘取對影像去霧演算法進行普及,引用及參考中文論文:

影像增強(Image Enhancement)是指按照某種特定的需求,突出影像中有用的資訊,去除或者削弱無用的資訊。影像增強的目的是使處理後的影像更適合人眼的視覺特性或易於機器識別。 在醫學成像、遙感成像、人物攝影等領域,影像增強技術都有著廣泛的應用。影像增強同時可以作為目標識別、目標跟蹤、特徵點匹配、影像融合、超解析度重構等影像處理演算法的預處理演算法。

近些年來,出現了眾多的單幅影像去霧演算法,應用比較廣泛的有:

  • 直方圖均衡化去霧演算法
  • Retinex去霧演算法
  • 暗通道先驗去霧演算法
  • 基於卷積神經網路的DehazeNet去霧演算法

其主要可以分為 3 類:基於影像增強的去霧演算法、基於影像復原的去霧演算法和基於 CNN 的去霧演算法。

(1) 基於影像增強的去霧演算法

通過影像增強技術突出影像細節,提升對比度,使之看起來更加清晰,這類演算法的適用性較廣。具體的演算法有:

  • Retinex 演算法
    根據成像原理,消除了反射分量的影響,達到了影像增強去霧的效果
  • 直方圖均衡化演算法
    使影像的像素分布更加均勻,放大了影像的細節
  • 偏微分方程演算法
    將影像視作一個偏微分方程,通過計算梯度場提高對比度
  • 小波變換演算法
    對影像進行分解,放大有用的部分

此外,在這類演算法的基礎上出現了眾多的基於影像增強原理的改進演算法。

(2) 基於影像復原的去霧演算法

主要是基於大氣散射物理學模型,通過對大量有霧影像和無霧影像進行觀察總結,得到其中存在的一些映射關係,然後根據有霧影像的形成過程來進行逆運算,從而恢復清晰影像。其中最經典的要屬何愷明大佬提出的:

  • 暗通道先驗去霧演算法
    通過對大量無霧影像進行特徵分析,找到了無霧影像與大氣散射模型中某些參數的先驗關係。該演算法複雜度低,去霧效果好,因此在其基礎上出現了大量基於暗通道先驗的改進演算法。

(3) 基於CNN的去霧演算法

使用 CNN 建立一個端到端的模型,通過有霧影像恢復出無霧影像,目前使用神經網路進行去霧的演算法主要有兩種思路:

  • 使用 CNN 生成大氣散射模型的某些參數,然後再根據大氣散射模型來恢復無霧影像
  • 使用 CNN (例如 GAN)直接根據模糊影像生成無霧的清晰影像

CNN 因其強大的學習能力在多個領域得到應用,因此也出現了採用 CNN 進行去霧的演算法。2016年CAI等首次提出了一種名為DehazeNet的去霧網路,用於估計有霧影像的透射率。DehazeNet 將有霧的模糊影像作為輸入,輸出其透射率,基於大氣散射模型理論恢復出無霧的清晰影像。

下圖是分別對直方圖均衡化、暗通道先驗去霧、DehazeNet和AOD-Net去霧演算法進行測試,實驗結果如圖所示。由圖可知,基於影像增強的直方圖均衡化演算法的去霧影像對比度明顯增強,由於不考慮降質原因,在增加對比度的同時也對雜訊進行了放大,出現細節丟失與色彩偏差現象。基於物理模型的暗通道去霧演算法、基於神經網路的 DehazeNet 和 AOD-Net 演算法的去霧效果較直方圖均衡化演算法更佳。

其他去霧演算法對比結果如下圖所示,比如城市和道路有無影像去霧效果對比。

最後,正如總結王道累老師總結的一樣,目前針對有霧影像去霧的演算法主要是從基於影像增強、影像復原和 CNN 3 個方向進行的。

  • 基於影像增強的方法不考慮有霧影像的形成過程,而是直接通過突出影像的細節,提高對比度等方式,從而使有霧影像看上去更加清晰。
  • 基於影像復原的方法則是追尋影像降質的物理過程,通過物理模型還原出清晰的影像。
  • 基於 CNN 的方法則是利用神經網路強大的學習能力,尋找有霧影像與影像復原物理模型中某些係數的映射關係或者使用 GAN,根據有霧影像還原出無霧的清晰影像。

上述 3 類去霧演算法對於霧天影像都有著明顯的去霧效果,儘管其在實際生活中已經得到了廣泛的應用,但下述幾點仍有可能是今後影像去霧領域的研究重點和難點:

  • 更加真實的霧天影像數據集
    採用神經網路進行去霧的演算法在效果上好於影像增強和復原的方法,但是由於在自然界中很難拍攝到一組背景相同的有霧影像和無霧影像,因此目前訓練神經網路所採用的數據集均是通過合成得到的,雖然能夠在一定程度上擬合自然環境,但是仍然存在著一些差距。所以目前急需一種由在真實環境中獲取到的具有相同背景的有霧影像和無霧影像構建的數據集,來提高神經網路去霧演算法的魯棒性和穩定性。
  • 更加簡便的去霧演算法
    目前各類演算法能夠有效去除單幅影像上的霧霾,但相對較好的演算法都存在著時間複雜度高的問題,很難應用到影片去霧或者需求較多的複雜任務中去。
  • 魯棒性更強的去霧演算法
    上述演算法都只對影像上存在的均勻的薄霧有較好的去霧效果,對於濃霧或者分布不均的團霧則效果較差,因此找到一種適用範圍更廣的去霧方法將會是一個極具挑戰性的課題。

二.ACE去霧演算法

1.演算法原理

該部分主要介紹參考作者書籍以及相關論文進行敘述,簡單介紹ACE演算法的原理知識。如果讀者想詳細了解其原理,推薦閱讀英文原文,詳見下面的參考文獻,都是大佬。
引用及參考中文論文:

英文原文:

影像對比度增強的演算法在很多場合都有用處,特別是在醫學影像中,這是因為在眾多疾病的診斷中,醫學影像的視覺檢查時很有必要的。Retinex演算法是代表性的影像增強演算法,它根據人的視網膜和大腦皮層模擬對物體顏色的波長光線反射能力而形成,對複雜環境下的一維條碼具有一定範圍內的動態壓縮,對影像邊緣有著一定自適應的增強。

自動色彩均衡(Automatic Color Enhancement,ACE) 演算法是Rizzi大神在Retinex演算法的理論上提出的,它通過計算影像目標像素點和周圍像素點的明暗程度及其關係來對最終的像素值進行校正,實現影像的對比度調整,產生類似人體視網膜的色彩恆常性和亮度恆常性的均衡,具有很好的影像增強效果。

ACE演算法包括兩個步驟:

  • 一是對影像進行色彩和空域調整,完成影像的色差校正,得到空域重構影像。
    模仿視覺系統的側抑制性和區域自適應性,進行色彩的空域調整。側抑制性是一個生理學概念,指在某個神經元受到刺激而產生興奮時,再刺激相近的神經元,後者所發生的興奮對前者產生的抑制作用。
  • 二是對校正後的影像進行動態擴展。
    對影像的動態範圍進行全局調整,並使影像滿足灰度世界理論和白斑點假設。演算法針對單通道,再延伸應用到RGB彩色空間的3通道影像,即對3個通道分別處理再進行整合完成。

(1) 區域自適應濾波

輸入影像I(灰度圖為例),該步是對單通道影像I中所有點p的區域自適應濾波,得到完成色差校正,空域重構後的中間結果影像,計算公式如下:

式中:Ic§-Ic(j)為p、j兩個像素點間灰度差值,表達擬生物學上的側抑制性;d(p,j)表示距離度量函數,使用兩點間的歐氏距離,作用上控制點j對p的影響權重,映射出濾波的區域適應性;Sa(x)是亮度表現函數(奇函數),本文演算法選擇經典Saturation函數。

不同亮度函數和參數的選擇控制了對比度增強的程度,經典的Saturation函數在飽和前取越大的斜率,結果的對比度增強越明顯,如圖2所示,極限情況是sign函數形式,而Sign函數由於無差別過度增強放大,導致雜訊同樣得到放大效果不佳,最終選擇Saturation函數作為相對亮度表現函數。公式如下:

(2) 色調重整拉伸,對影像動態擴展

將式(1)中得到的中間量拉伸映射到 [0, 255] 中,佔滿動態範圍 [0, 255](8位灰度影像),計算公式如下,式中:[minR,maxR]是中間量L(x)的全部定義域,該項使影像達到全局白平衡。

下圖是條形碼影像進行ACE影像增強後的效果圖,通過影像增強後的圖(b)對比度更強,改善了原影像的明暗程度,增強的同時保持了影像的真實性。

ACE演算法英文介紹如下:

實驗對比效果如下圖所示,大家在寫該主題論文的時候,注意和傳統方法對比。

2.程式碼實現

由於OpenCV中暫時沒有ACE演算法包,下面的程式碼是借鑒「zmshy2128」老師的文章,修改實現的彩色直方圖均衡化處理。後面有機會作者詳細分析其程式碼實現過程。

  • 自動色彩均衡(ACE)快速演算法 – zmshy2128老師
# -*- coding: utf-8 -*-
# By:Eastmount CSDN 2021-03-12
# 慘zmshy2128老師文章並修改成Python3程式碼
import cv2
import numpy as np
import math
import matplotlib.pyplot as plt

#線性拉伸處理
#去掉最大最小0.5%的像素值 線性拉伸至[0,1]
def stretchImage(data, s=0.005, bins = 2000):   
    ht = np.histogram(data, bins);
    d = np.cumsum(ht[0])/float(data.size)
    lmin = 0; lmax=bins-1
    while lmin<bins:
        if d[lmin]>=s:
            break
        lmin+=1
    while lmax>=0:
        if d[lmax]<=1-s:
            break
        lmax-=1
    return np.clip((data-ht[1][lmin])/(ht[1][lmax]-ht[1][lmin]), 0,1)

#根據半徑計算權重參數矩陣
g_para = {}
def getPara(radius = 5):                        
    global g_para
    m = g_para.get(radius, None)
    if m is not None:
        return m
    size = radius*2+1
    m = np.zeros((size, size))
    for h in range(-radius, radius+1):
        for w in range(-radius, radius+1):
            if h==0 and w==0:
                continue
            m[radius+h, radius+w] = 1.0/math.sqrt(h**2+w**2)
    m /= m.sum()
    g_para[radius] = m
    return m

#常規的ACE實現
def zmIce(I, ratio=4, radius=300):                     
    para = getPara(radius)
    height,width = I.shape
    zh = []
    zw = []
    n = 0
    while n < radius:
        zh.append(0)
        zw.append(0)
        n += 1
    for n in range(height):
        zh.append(n)
    for n in range(width):
        zw.append(n)
    n = 0
    while n < radius:
        zh.append(height-1)
        zw.append(width-1)
        n += 1
    #print(zh)
    #print(zw)
    
    Z = I[np.ix_(zh, zw)]
    res = np.zeros(I.shape)
    for h in range(radius*2+1):
        for w in range(radius*2+1):
            if para[h][w] == 0:
                continue
            res += (para[h][w] * np.clip((I-Z[h:h+height, w:w+width])*ratio, -1, 1))
    return res

#單通道ACE快速增強實現
def zmIceFast(I, ratio, radius):
    print(I)
    height, width = I.shape[:2]
    if min(height, width) <=2:
        return np.zeros(I.shape)+0.5
    Rs = cv2.resize(I, (int((width+1)/2), int((height+1)/2)))
    Rf = zmIceFast(Rs, ratio, radius)             #遞歸調用
    Rf = cv2.resize(Rf, (width, height))
    Rs = cv2.resize(Rs, (width, height))
 
    return Rf+zmIce(I,ratio, radius)-zmIce(Rs,ratio,radius)   

#rgb三通道分別增強 ratio是對比度增強因子 radius是卷積模板半徑          
def zmIceColor(I, ratio=4, radius=3):               
    res = np.zeros(I.shape)
    for k in range(3):
        res[:,:,k] = stretchImage(zmIceFast(I[:,:,k], ratio, radius))
    return res

#主函數
if __name__ == '__main__':
    img = cv2.imread('car.png')
    res = zmIceColor(img/255.0)*255
    cv2.imwrite('car-Ice.jpg', res)

運行結果如圖所示,ACE演算法能有效進行影像去霧處理,實現影像的細節增強。

最後是目標檢測去霧和女神去霧的效果,哈哈,繼續加油!

三.暗通道先驗去霧演算法

該演算法是電腦視覺領域何愷明大佬於2009年提出的影像去霧經典演算法,並獲取當年CVPR最佳論文。論文題目為《Single Image Haze Removal Using Dark Channel Prior》。下圖是大佬的百科簡介,是真的厲害,值得我們大家學習。

  • 2003年5月,何愷明拿到保送清華的資格,是當年執信中學唯一保送上清華大學的學生;高考結果出爐以後,何愷明獲得滿分900分的成績,成為當年廣東省9位滿分狀元之一。
  • 2009年,何愷明成為首獲電腦視覺領域三大國際會議之一CVPR「最佳論文獎」的中國學者。
  • 在2015年的ImageNet影像識別大賽中,何愷明和他的團隊用「影像識別深度差殘學習」系統,擊敗Google、英特爾、高通等業界團隊,榮獲第一。
  • 何愷明作為第一作者獲得了CVPR 2009,CVPR 2016和ICCV 2017(Marr Prize)的最佳論文獎,並獲得了ICCV 2017最佳學生論文獎。
  • 2018年,第31屆電腦視覺和模式識別大會(Conference on Computer Vision and Pattern Recognition, CVPR)在美國鹽湖城召開,何愷明獲得本屆大會的PAMI年輕學者獎。

1.演算法原理

言歸正傳,如果是影像處理或研究影像去霧領域的作者,建議大家認真閱讀這篇英文原文,能在2009年提出該演算法真的很驚艷。

引用及參考中文論文:

英文原文:

 

暗通道先驗(Dark Channel Prior, DCP)去霧演算法 依賴大氣散射模型進行去霧處理,通過對大量有霧影像和無霧影像進行觀察總結,得到其中存在的一些映射關係,然後根據有霧影像的形成過程來進行逆運算,從而恢復清晰影像。

演算法實現過程及原理如下,參考何愷明老師和何濤老師的論文。

(1) 大氣散射模型

在電腦視覺和電腦圖形學中,方程所描述的大氣散射模型被廣泛使用。參數解釋如下:

  • x是影像的空間坐標
  • I(x)代表有霧影像(待去霧影像)
  • J(x)代表無霧影像(待恢復影像)
  • A代表全球大氣光值
  • t(x)代表透射率

方程右邊第一項為場景直接衰減項,第二項為環境光項。

(2) 暗通道定義

在絕大多數非天空的局部區域中,某些像素總會至少有一個顏色通道的值很低。對於一幅影像J(x),其暗通道的數學定義表示如下:

其中,Ω(x)表示以x為中心的局部區域,上標c表示RGB三個通道。該公式的意義用程式碼表達也很簡單,首先求出每個像素RGB分量中的最小值,存入一副和原始影像大小相同的灰度圖中,然後再對這幅灰度圖進行最小值濾波,濾波的半徑由窗口大小決定。

(3) 暗通道先驗理論

暗通道先驗理論指出:對於非天空區域的無霧影像J(x)的暗通道趨於0,即:

實際生活中造成暗原色中低通道值主要有三個因素:

  • a) 汽車、建築物和城市中玻璃窗戶的陰影,或者是樹葉、樹與岩石等自然景觀的投影;
  • b) 色彩鮮艷的物體或表面,在RGB的三個通道中有些通道的值很低(比如綠色的草地/樹/植物,紅色或黃色的花朵/葉子,或者藍色的水面);
  • c) 顏色較暗的物體或者表面,例如灰暗色的樹榦和石頭。

總之,自然景物中到處都是陰影或者彩色,這些景物的影像的暗原色總是很灰暗的,而有霧的影像較亮。因此,可以明顯的看到暗通道先驗理論的普遍性。

(4) 公式變形

根據大氣散射模型,將第一個公式稍作處理,變形為下式:

假設每一個窗口的透射率t(x)為常數,記為t』(x),並且A值已給定,對式兩邊同時進行兩次最小值運算,可得:

其中,J(x)是要求的無霧影像,根據前述的暗通道先驗理論可知:

因此可推導出:

(5) 透射率計算

將上式帶入可得到透射率t』(x)的預估值,如下所示:

現實生活中,即便晴空萬里,空氣中也會存在一些顆粒,在眺望遠處的景物時,人們還是能感覺到霧的存在。另外,霧的存在讓人們感受到景深,因此在去霧的同時有必要保留一定程度的霧。可以通過引入一個0到1之 間 的 因 子 w(一 般取0.95)對預估透射率進行修正,如式所示:

以上的推導過程均假設大氣光值A是已知的,在實際中,可以藉助暗通道圖從原始霧圖中求取。具體步驟如下:

  • 先求取暗通道圖,在暗通道圖中按照亮度的大小提取最亮的前0.1%的像素
  • 在原始霧圖I(x)中找對應位置上具有最高亮度的點的值,作為大氣光值A

此外,由於透射率t偏小時,會造成J偏大,恢復的無霧影像整體向白場過度,因此有必要對透射率設置一個下限值t0(一般取值為0.1),當t值小於t0 時,取t=t0。將以上求得的透射率和大氣光值代入公式,最終整理得到影像的恢復公式如下:

這就是暗通道先驗去霧演算法的原理過程,下面簡單補充論文中的處理效果圖。

再次膜拜偶像,極力推薦大家閱讀論文。

2.演算法實現

實現程式碼引用木老師的,感覺比我寫得好,參考如下:

# -*- coding: utf-8 -*-
"""
Created on Sat Sep 11 00:16:07 2021
@author: xiuzhang

參考資料:
https://blog.csdn.net/leviopku/article/details/83898619
"""

import sys
import cv2
import math
import numpy as np
 
def DarkChannel(im,sz):
    b,g,r = cv2.split(im)
    dc = cv2.min(cv2.min(r,g),b)
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(sz,sz))
    dark = cv2.erode(dc,kernel)
    return dark
 
def AtmLight(im,dark):
    [h,w] = im.shape[:2]
    imsz = h*w
    numpx = int(max(math.floor(imsz/1000),1))
    darkvec = dark.reshape(imsz,1)
    imvec = im.reshape(imsz,3)
 
    indices = darkvec.argsort()
    indices = indices[imsz-numpx::]
 
    atmsum = np.zeros([1,3])
    for ind in range(1,numpx):
       atmsum = atmsum + imvec[indices[ind]]
 
    A = atmsum / numpx;
    return A
 
def TransmissionEstimate(im,A,sz):
    omega = 0.95
    im3 = np.empty(im.shape,im.dtype)
 
    for ind in range(0,3):
        im3[:,:,ind] = im[:,:,ind]/A[0,ind]

    transmission = 1 - omega*DarkChannel(im3,sz)
    return transmission
 
def Guidedfilter(im,p,r,eps):
    mean_I = cv2.boxFilter(im,cv2.CV_64F,(r,r))
    mean_p = cv2.boxFilter(p, cv2.CV_64F,(r,r))
    mean_Ip = cv2.boxFilter(im*p,cv2.CV_64F,(r,r))
    cov_Ip = mean_Ip - mean_I*mean_p
 
    mean_II = cv2.boxFilter(im*im,cv2.CV_64F,(r,r))
    var_I   = mean_II - mean_I*mean_I
 
    a = cov_Ip/(var_I + eps)
    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*im + mean_b
    return q
 
def TransmissionRefine(im,et):
    gray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
    gray = np.float64(gray)/255
    r = 60
    eps = 0.0001
    t = Guidedfilter(gray,et,r,eps)
 
    return t
 
def Recover(im,t,A,tx = 0.1):
    res = np.empty(im.shape,im.dtype)
    t = cv2.max(t,tx)
 
    for ind in range(0,3):
        res[:,:,ind] = (im[:,:,ind]-A[0,ind])/t + A[0,ind]
 
    return res
 
if __name__ == '__main__':
    
    fn = 'car-02.png'
    src = cv2.imread(fn)
    I = src.astype('float64')/255
    
    dark = DarkChannel(I,15)
    A = AtmLight(I,dark)
    te = TransmissionEstimate(I,A,15)
    t = TransmissionRefine(src,te)
    J = Recover(I,t,A,0.1)
    
    arr = np.hstack((I, J))
    cv2.imshow("contrast", arr)
    cv2.imwrite("car-02-dehaze.png", J*255 )
    cv2.imwrite("car-02-contrast.png", arr*255)
    cv2.waitKey();

實現效果如下圖所示:

如果想和後續目標汽車檢測結合,同樣可以先去霧再進行檢測,如下圖所示:

四.影像雜訊和霧生成

影像處理總少不了雜訊添加或生成,下面補充兩個簡單的椒鹽雜訊和霧氣模擬生成的程式碼。這與本文的實驗緊密相關,能為我們提供更多的GAN生成樣本。後面人工智慧系列文章,GAN我們看看能不能學習真實霧化場景的影像,值得期待,哈哈!

1.加鹽雜訊

原圖是一張風景影像:

程式碼如下:

# -*- coding:utf-8 -*-
import cv2
import numpy as np

#讀取圖片
img = cv2.imread("fj.png", cv2.IMREAD_UNCHANGED)
rows, cols, chn = img.shape

#加雜訊
for i in range(50000):    
    x = np.random.randint(0, rows) 
    y = np.random.randint(0, cols)    
    img[x,y,:] = 210

cv2.imshow("noise", img)
           
#等待顯示
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite('fj-res.png',img)

輸出結果如下圖所示:

2.霧的模擬生成

程式碼如下:

import numpy as np
import cv2 as cv
import os
import random
 
file = ['fj.png']
output = 'fj-wu.png'

for file_img in file:
    #打開影像
    img = cv.imread(file_img)
    mask_img = cv.imread(file_img)
    
    #霧的顏色
    mask_img[:, :] = (166, 178, 180) 
    
    #裡面參數可調,主要調整霧的濃度
    image = cv.addWeighted(img,
                           round(random.uniform(0.03, 0.28), 2),
                           mask_img, 1, 0) 

    #保存的文件夾
    cv.imwrite(output, image)

輸出結果如下圖所示,效果還不錯。

 

點擊關注,第一時間了解華為雲新鮮技術~