暗通道去霧算法原理及實現
1. 算法原理。
基本原理來源於何凱明大神的CVPR09的論文Single Image Haze Removal Using Dark Channel Prior
- 暗通道。
所謂暗通道是一個基本假設,這個假設認為,在絕大多數的非天空的局部區域中,某一些像素總會有至少一個顏色通道具有很低的值。這個其實很容易理解,實際生活中造成這個假設的原因有很多,比如汽車,建築物或者城市中的陰影,或者說色彩鮮艷的物體或表面(比如綠色的樹葉,各種鮮艷的花,或者藍色綠色的睡眠),顏色較暗的物體或者表面,這些景物的暗通道總是變現為比較暗的狀態。
所以暗通道是什么呢?其實比較簡單,作者認為暗通道是:
暗通道先驗理論指出:
暗通道實際上是在rgb三個通道中取最小值組成灰度圖,然后再進行一個最小值濾波得到的。我們來看一下有霧圖像和無霧圖像暗通道的區別:
可以發現,有霧的時候會呈現一定的灰色,而無霧的時候咋會呈現大量的黑色(像素為接近0),作者統計了5000多副圖像的特征,基本都符合這樣一條先驗定理。
- 霧圖形成模型
計算機視覺中,下面這個霧圖形成模型是被廣泛使用的:
其中I(x)是現有的圖像(待去霧),J(x)是要恢復的原無霧圖像,A是全球大氣光成分,t(x)是透射率,現在的條件就是已知I(x),來求J(x),顯然不加任何限制的話是有無窮多個解的。
但是現實生活中,即使是晴天白雲,空氣中也會存在一些顆粒,看遠方的物體還是能夠感覺到霧的影響,另外,霧的存在可以讓人們感覺到景深的存在,所以我們保留一部分的霧,上式修正為:其中w是[0-1]之間的一個值,一般取0.95差不多。
上面的推導都是假設全球大氣光是已知的,實際中,我們可以借助暗通道圖來從有霧圖像中來獲取該值:
- 從暗通道圖中按照亮度大小取前0.1%的像素。
- 在這些位置中,在原始圖像中尋找對應具有最高亮度點的值,作為A值。
到這里,我們就可以進行無霧圖像的恢復了:
當投射圖t很小時,會導致J的值偏大,會導致圖片某些地方過爆,所以一般可以設置一個閾值來限制,我們設置一個閾值:一般設置較小,0.1即可。
利用這個理論的去霧效果就不錯了,下面是我在網上找的例子:
但是這個去霧效果還是挺粗糙的,主要原因是由於透射率圖過於粗糙了,何凱明在文章中提出了soft matting方法,然后其缺點是速度特別慢,不適用在實時場合,2011年,又提出可以使用導向濾波的方式來獲得更細膩的結果,這個方法的運算主要集中在方框濾波(均值濾波),而這種操作在opencv或者其他的圖像庫中都有快速算法。可以考慮使用。
2.代碼實現。
我很快在網上找到一個python版本的算法:
1 # -*- coding: utf-8 -*- 2 """ 3 Created on Sat Jun 9 11:28:14 2018 4 5 @author: zhxing 6 """ 7 8 import cv2 9 import numpy as np 10 11 def zmMinFilterGray(src, r=7): 12 '''''最小值濾波,r是濾波器半徑''' 13 return cv2.erode(src,np.ones((2*r-1,2*r-1))) 14 # ============================================================================= 15 # if r <= 0: 16 # return src 17 # h, w = src.shape[:2] 18 # I = src 19 # res = np.minimum(I , I[[0]+range(h-1) , :]) 20 # res = np.minimum(res, I[range(1,h)+[h-1], :]) 21 # I = res 22 # res = np.minimum(I , I[:, [0]+range(w-1)]) 23 # res = np.minimum(res, I[:, range(1,w)+[w-1]]) 24 # ============================================================================= 25 # return zmMinFilterGray(res, r-1) 26 27 def guidedfilter(I, p, r, eps): 28 '''''引導濾波,直接參考網上的matlab代碼''' 29 height, width = I.shape 30 m_I = cv2.boxFilter(I, -1, (r,r)) 31 m_p = cv2.boxFilter(p, -1, (r,r)) 32 m_Ip = cv2.boxFilter(I*p, -1, (r,r)) 33 cov_Ip = m_Ip-m_I*m_p 34 35 m_II = cv2.boxFilter(I*I, -1, (r,r)) 36 var_I = m_II-m_I*m_I 37 38 a = cov_Ip/(var_I+eps) 39 b = m_p-a*m_I 40 41 m_a = cv2.boxFilter(a, -1, (r,r)) 42 m_b = cv2.boxFilter(b, -1, (r,r)) 43 return m_a*I+m_b 44 45 def getV1(m, r, eps, w, maxV1): #輸入rgb圖像,值范圍[0,1] 46 '''''計算大氣遮罩圖像V1和光照值A, V1 = 1-t/A''' 47 V1 = np.min(m,2) #得到暗通道圖像 48 V1 = guidedfilter(V1, zmMinFilterGray(V1,7), r, eps) #使用引導濾波優化 49 bins = 2000 50 ht = np.histogram(V1, bins) #計算大氣光照A 51 d = np.cumsum(ht[0])/float(V1.size) 52 for lmax in range(bins-1, 0, -1): 53 if d[lmax]<=0.999: 54 break 55 A = np.mean(m,2)[V1>=ht[1][lmax]].max() 56 57 V1 = np.minimum(V1*w, maxV1) #對值范圍進行限制 58 59 return V1,A 60 61 def deHaze(m, r=81, eps=0.001, w=0.95, maxV1=0.80, bGamma=False): 62 Y = np.zeros(m.shape) 63 V1,A = getV1(m, r, eps, w, maxV1) #得到遮罩圖像和大氣光照 64 for k in range(3): 65 Y[:,:,k] = (m[:,:,k]-V1)/(1-V1/A) #顏色校正 66 Y = np.clip(Y, 0, 1) 67 if bGamma: 68 Y = Y**(np.log(0.5)/np.log(Y.mean())) #gamma校正,默認不進行該操作 69 return Y 70 71 if __name__ == '__main__': 72 m = deHaze(cv2.imread('test.jpg')/255.0)*255 73 cv2.imwrite('defog.jpg', m)
最小值濾波我給用腐蝕來替代了,其實腐蝕就是最小值濾波,最大值濾波是膨脹。這個測試效果還不錯。
這份python代碼中使用的是暗通道和RGB圖像的最小值圖像(實際上是一種灰度圖)來進行導向濾波,我試着用灰度圖和暗通道來做,也是可以的,效果區別不大。
這個python版本跑的還是挺慢的,600-500的圖像需要花費近0.1s的時間,我照着這個寫了一個c++版本的,速度立馬提高一倍,代碼比python要長一些,就不在這里貼了,相同的圖像速度可以提高一倍以上,如果加上GPU加速的話應該可以實現實時處理。
c++ code,這個工程里還包含了視頻去抖,圖像灰度對比對拉伸,以及去燥(這個效果還不好)的代碼。
3. 各參數的影響。
- 暗通道最小值濾波半徑r。
這個半徑對於去霧效果是有影響的。一定情況下,半徑越大去霧的效果越不明顯,建議的范圍一般是5-25之間,一般選擇5,7,9等就會取得不錯的效果。 - w的影響自然也是很大的。
這個值是我們設置的保留霧的程度(c++代碼中w是去除霧的程度,一般設置為0.95就可以了)。這個基本不用修改。 - 導向濾波中均值濾波半徑。
這個半徑建議取值不小於求暗通道時最小值濾波半徑的4倍。因為前面最小值后暗通道時一塊一塊的,為了使得透射率圖更加精細,這個r不能過小(很容易理解,如果這個r和和最小值濾波的一樣的話,那么在進行濾波的時候包含的塊信息就很少,還是容易出現一塊一塊的形狀)。 - eps,這個值只是保證除號下面不是0,一般取較小,0.001是一個常用的值。
4. notes。
- 這個去霧算法只針對彩色圖像,而且對於低對比度的天空或者水面背景的去霧效果會產生塊效應,去霧效果不好,而且這種效應並不能通過調參來避免。
- 暗通道去霧使得圖像整體的亮度有所降低,所以在最后可以自適應的提高亮度來減輕這種現象。
- 導向濾波在matlab中有現成函數,在opencv contrib里也有函數可以調用,另外為了加速運算可以下采樣之后進行濾波然后再上采樣恢復。
