OpenCV計算機視覺學習(9)——圖像直方圖 & 直方圖均衡化

 

人工智能學習離不開實踐的驗證,推薦大家可以多在FlyAI-AI競賽服務平臺多參加訓練和競賽,以此來提升自己的能力。FlyAI是爲AI開發者提供數據競賽並支持GPU離線訓練的一站式服務平臺。每週免費提供項目開源算法樣例,支持算法能力變現以及快速的迭代算法模型。

如果需要處理的原圖及代碼,請移步小編的GitHub地址

  傳送門:請點擊我

  如果點擊有誤:https://github.com/LeBron-Jian/ComputerVisionPractice

1,如何提高圖像像素

  對曝光過度或者逆光拍攝的圖片可以通過直方圖均衡化的方法用來增強局部或者整體的對比度。

  對於相機採集的原始圖像經常會出現一種現象,即圖像所有像素的灰度值分佈不均勻,而是集中在某一特定的小區域,導致圖像中的所有信息的灰度值都很接近,即對比度差,很難從圖像中分辨出某一特徵的信息。而質量較高的圖像中,像素的強度應該均衡的分佈。

  爲了提高圖像處理的效果,經常會在圖像處理之前進行直方圖均衡化,即將圖像的直方圖灰度級別由集中在某一小部分灰度級分散成在所有灰度級別都有一定的覆蓋,所以通過直方圖均衡化的方法用來增強局部或整體的對比度。

  具體的思路就是通過找出圖像中最亮和最暗的像素值將之映射到純黑和純白之後再將其他的像素值按照某種算法映射到純黑和純白之間的值。另外一種方法就是尋找圖像中像素的平均值作爲中間灰度值,然後擴展範圍以達到儘量充滿可顯示的值。

  一個好的圖像會有來自圖像的所有區域的像素。所以你需要把這個直方圖拉伸到兩端(如上圖所給出的),這就是直方圖均衡的作用(用簡單的話說)。這通常會改善圖像的對比度。

2,直方圖均衡化的原理

2.1  直方圖的介紹

  具體直方圖實現的原理是什麼呢?請看下圖:

   左圖是一個圖像的像素組合,我們拿到的是一個12*20 大小的圖像像素;右圖就是他的直方圖展示,橫軸表示在0~255之間的區間塊,我們將其分爲16個bin,統計圖像中每個像素的個數,右圖反映的時圖像中每個像素出現的頻率,橫軸是像素區間,縱座標是像素出現的頻率。

  看到上面兩個圖,大概直方圖的解釋應該很明顯了。

2.2  用實驗數據展示什麼是直方圖?

  我們可以把直方圖看做一個圖,它給我們一個關於圖像的強度分佈的總體思路。它是一個帶有像素值的圖(從0到255, 不總是)在X軸上,在y軸上的圖像對應的像素個數。

  這只是理解圖像的另一種方式,通過觀察圖像的直方圖,我們可以直觀的瞭解圖像的對比度,亮度,亮度分佈等。(下圖來至於Cambridge in Color website的圖片,建議去訪問這個網站,瞭解更多細節。)

  你可以看到圖像和它的直方圖。(這個直方圖是用灰度圖像繪製的,而不是彩色圖像)。在直方圖中,橫座標表示圖像中各個像素點的灰度級,縱座標表示具有該灰度級的像素個數。直方圖的左邊部分顯示了圖像中較暗像素的數量,右邊區域顯示了更明亮的像素。從直方圖中可以看到,深色區域的像素數量比亮色區域更多,而中間色調的數量(中值大約在127左右)則少得多。

2.3 直方圖均衡化的原理

  有時圖像的視覺上的缺陷並不在強度值集中在很窄的範圍內。而是某些強度值的使用頻率很大。在完美均衡的直方圖中,每個柱的值都應該相等。即50%的像素值應該小於128,25%的像素值應該小於64.總結出的經驗可定義爲:在標準的直方圖中 p% 的像素擁有的強度值一定小於或等於 255*p%,將該規律用於均衡直方圖中:強度 i 的灰度值應該在對應的像素強度低於 i 的百分比的強度中。因此,所需要的查詢表可由下面的式子建立:

1

lut[i] = int(255.0 *p[i]) #p[i]是是強度值小於或等於i的像素的數目。

  p[i] 即直方圖累計值,這是包含小於給點強度值的像素的直方圖,以代替包含指定強度值像素的數目。比如第一幅圖像的累計直方圖如下圖中的藍線:

   而完美均衡的直方圖,其累積直方圖應爲一條斜線,如上圖中均衡化之後的紅線。

  更專業一點,這種累計直方圖應該稱爲累計分佈(cumulative  distribition)。在Numpy中有一個專門的函數來計算。這個在後面說。

  所以直方圖均衡化就是對圖像使用一種特殊的查詢表。通常來說,直方圖均衡化大大增加了圖像的表象。但是根據圖像可視內容的不同,不同圖像的直方圖均衡化產生的效果不盡相同。下面我們具體學習一下。

  比如下圖小狗,我們畫出原圖,並展示出其像素直方圖分佈範圍:

   我們對直方圖進行均衡化,均衡化的圖,如下:

   最終我們得到了小狗直方圖均衡化後的圖像。那其計算原理如下圖:

   簡單解釋一下,上面兩張圖是我們取了圖片中一點像素,是我們直方圖均衡化前後的兩張表的對比。那麼如何進行直方圖均衡化的計算,也就是將左圖的像素點轉換爲右圖呢,我們就需要下圖的計算過程了。圖很明顯,我就不再贅述了。

  下面首先對直方圖的計算進行學習,然後學習直方圖均衡化。

3,直方圖的繪製

3.1  使用OpenCV統計繪製直方圖

  OpenCV提供了cv.calcHist()函數來獲取直方圖,與C++中一樣,都是cv.calcHist()。讓我們熟悉一下這個函數及其參數:

1

2

3

4

5

6

7

def calcHist(images, channels, mask, histSize, ranges, hist=None, accumulate=None):

    # real signature unknown; restored from __doc__

    """

    calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]]) -> hist

    .   @overload

    """

    pass

  • images:它是uint8類型或float32的源圖像。當傳入函數時,它要用方括號括起來,也就是」[img]」
  • channels:它也用方括號括起來。它是我們計算直方圖的信道的索引。例如,如果輸入是灰度圖像,它的值是0。對於顏色圖像,您可以通過0、1或2來分別計算藍色、綠色或紅色通道的直方圖,即BGR通道
  • mask:遮罩圖。爲了找到完整圖像的直方圖,它被指定爲「None」。但如果你想找到圖像的特定區域的直方圖,你必須爲它創建一個遮罩圖,並將其作爲遮罩。
  • histSize:這代表了我們的BINS數。需要用方括號來表示。在整個範圍內,我們通過了256。
  • ranges:強度值範圍,通常是 [ 0,256 ]

  讓我們從一個樣本圖像開始,只需要在灰度模式下加載圖像並找出其完整的直方圖:

1

2

3

4

5

6

7

8

#_*_coding:utf-8_*_

import cv2  # opencv讀取的格式是BGR

import numpy as np

import matplotlib.pyplot as plt  # Matplotlib讀取的格式是RGB

 

img = cv2.imread('cat.jpg', 0)   #0 表示灰度圖

hist = cv2.calcHist([img], [0], None, [256], [0, 256])

print(hist.shape)  #  (256, 1)

  hist是一個256x1陣列,每個值對應於該圖像中的像素值機器對應的像素值。

3.2  使用Numpy計算直方圖

  Numpy中提供了np.histogram()方法,用於對一位數組進行直方圖統計,其參數列表入下:

1

Histogram(a,bins=10,range=None,normed=False,weights=None)

  • a:是保存待統計的數組
  • bins:指定統計的區間個數,即對統計範圍的等分數
  • range:是一個長度爲2的元組,表示統計範圍的最大值和最小值,默認值爲None,表示範圍由數據的範圍決定,即(a.min(), a.max))。
  • normed:當normed參數爲False時,函數返回數組a中的數據在每個區間的個數,否則對個數進行正規化處理,使它等於每個區間的概率密度。
  • weights:weights參數和 bincount()的類似

返回值(有兩個)

  • hist : hist和之前計算的一樣,每個區間的統計結果。
  • bins : 數組,存儲每個統計區間的起點。range爲[0,256]時,bins有257個元素,因爲Numpy計算bins是以0-0.99,1-1.99等,所以最後一個是255-255.99。爲了表示這一點,他們還在bins的末端添加了256。但我們不需要256。到255就足夠了。

  讓我們從一個樣本圖像開始。只需在灰度模式下加載圖像並找到其完整的直方圖

1

hist, bins = np.histogram(img.ravel(), 255, [0,256])

  Numpy還有另一個函數,np.bincount(),比np.histograme()要快得多(大約10X)。對於一維直方圖,你可以試一下。不要忘記在np.bincount中設置minlength=256。例如,hist=np.bincount(img.ravel(),minlength=256)

  OpenCV函數比np.histogram()快(大約40X)。所以考慮效率的時候堅持用OpenCV函數。

3.3   使用matplotlib繪製直方圖

  Matplotlib中有一個繪製直方圖的函數:

1

matplotlib.pyplot.hist()

  參數:數據源必須是一維數組,通常要通過函數 ravel() 拉直圖像,像素一般是256,表示[0, 256]。

  函數ravel() 將多維數組降爲一維,格式爲:一維數組 =  多維數組.ravel()

  hist()直接找到直方圖繪製。您不需要使用calcHist()或np.histogram()函數來找到直方圖。看下面的代碼:

1

2

3

4

5

6

7

import numpy as np

import cv2 as cv

from matplotlib import pyplot as plt

 

img = cv.imread('cat.jpg', 0)

plt.hist(img.ravel(), 256, [0,256])

plt.show()

  效果如下:

  或者我們可以用常規的matplotlib的plot函數繪製直方圖,適合繪製BGR圖像直方圖。爲此,我們需要首先找到直方圖的數據,代碼如下:

1

2

3

4

5

6

7

8

9

10

11

import numpy as np

import cv2 as cv

from matplotlib import pyplot as plt

 

img = cv.imread('cat.jpg')

color = ('b''g''r')

for i, col in enumerate(color):

    histr = cv.calcHist([img], [i], None, [256], [0,256])

    plt.plot(histr, color=col)

    plt.xlim([0,256])

plt.show()

  效果如下:

4   彩色圖像不同通道的直方圖

  下面來看下彩色圖像的直方圖處理,首先讀取並分離各通道,接着計算每個通道的直方圖,這裏將其封裝成函數,接着調用,代碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

import cv2

import numpy as np

 

 

def calcAndDrawHist(image, color):

    hist = cv2.calcHist([image], [0], None, [256], [0.0, 255.0])

    minVal, maxVal, minLoc, maxLoc = cv2.minMaxLoc(hist)

    histImg = np.zeros([256, 256, 3], np.uint8)

    hpt = int(0.9*256)

 

    for in range(256):

        intensity = int(hist[h] * hpt / maxVal)

        cv2.line(histImg, (h, 256), (h, 256-intensity), color)

 

    return histImg

 

def show_histphoto(photo_path):

    image = cv2.imread(photo_path)

    b, g, r = cv2.split(image)

 

    histImgB = calcAndDrawHist(b, [255, 0, 0])

    histImgG = calcAndDrawHist(b, [0, 255, 0])

    histImgR = calcAndDrawHist(b, [0, 0, 255])

 

    cv2.imshow('histImgB', histImgB)

    cv2.imshow('histImgG', histImgG)

    cv2.imshow('histImgR', histImgR)

    cv2.imshow('Img', image)

    cv2.waitKey(0)

    cv2.destroyAllWindows()

 

 

if __name__ == '__main__':

    photo_path = 'cat.jpg'

    show_histphoto(photo_path)

  三個通道的直方圖如下:

  圖如下:

4.1  更進一步

  這樣做有些繁瑣,參考(https://plus.google.com/118298613334549762938)的做法,無需分離通道,用折現來描繪直方圖的邊界可在一副圖中同時繪製三個通道的直方圖。方法如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

def Line_chart(photo_path):

    image = cv2.imread(photo_path)

    # 創建用於繪製直方圖的全0 圖像

    h = np.zeros((256, 256, 3))

    # 直方圖中各bin的頂點位置

    bins = np.arange(256).reshape(256, 1)

    # BGR 三種顏色

    color = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]

    for ch, col in enumerate(color):

        originHist = cv2.calcHist([image], [ch], None, [256], [0, 256])

        cv2.normalize(originHist, originHist, 0, 255*0.9, cv2.NORM_MINMAX)

        hist = np.int32(np.around(originHist))

        pts = np.column_stack((bins, hist))

        cv2.polylines(h, [pts], False, col)

 

    h = np.flipud(h)

    cv2.imshow('colorhist', h)

    cv2.waitKey(0)

 

if __name__ == '__main__':

    photo_path = 'cat.jpg'

    # show_histphoto(photo_path)

    Line_chart(photo_path)

  結果如下:

代碼說明:

  這裏的for循環是對三個通道遍歷一次,每次繪製相應通道的直方圖的折線。for循環的第一行是計算對應通道的直方圖,經過上面的介紹,應該很容易就能明白。

  這裏所不同的是沒有手動的計算直方圖的最大值再乘以一個係數,而是直接調用了OpenCV的歸一化函數。該函數將直方圖的範圍限定在0-255×0.9之間,與之前的一樣。下面的hist= np.int32(np.around(originHist))先將生成的原始直方圖中的每個元素四捨六入五湊偶取整(cv2.calcHist函數得到的是float32類型的數組),接着將整數部分轉成np.int32類型。即61.123先轉成61.0,再轉成61。注意,這裏必須使用np.int32(...)進行轉換,numpy的轉換函數可以對數組中的每個元素都進行轉換,而Python的int(...)只能轉換一個元素,如果使用int(...),將導致only length-1 arrays can be converted to Python scalars錯誤。

  下面的pts = np.column_stack((bins,hist))是將直方圖中每個bin的值轉成相應的座標。比如hist[0] =3,...,hist[126] = 178,...,hist[255] = 5;而bins的值爲[[0],[1],[2]...,[255]]。使用np.column_stack將其組合成[0, 3]、[126, 178]、[255, 5]這樣的座標作爲元素組成的數組。

  最後使用cv2.polylines函數根據這些點繪製出折線,第三個False參數指出這個折線不需要閉合。第四個參數指定了折線的顏色。

  當所有完成後,別忘了用h = np.flipud(h)反轉繪製好的直方圖,因爲繪製時,[0,0]在圖像的左上角。這在直方圖可視化一節中有說明。

 

5,直方圖均衡化

5.1   使用OpenCV繪製直方圖均衡化

  我們可以調整直方圖的值和它的bin值,讓它看起來像x,y座標,這樣你就可以用cv.line()或cv.polyline()函數來繪製它,從而生成與上面相同的圖像。這已經是OpenCV-Python2官方的樣本了。查看sampl/python/hist.py的代碼。我們用cv.calcHist()函數來找一張完整的圖片的直方圖。但是我們只要圖片的一部分的直方圖呢?在你想要找到的區域中,創建一個帶有白色的遮罩圖像。然後把它作爲遮罩

  我們可以拿到一幅圖像進行mask操作,並且可以看一下其直方圖分佈:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

#_*_coding:utf-8_*_

import cv2  # opencv讀取的格式是BGR

import numpy as np

import matplotlib.pyplot as plt  # Matplotlib讀取的格式是RGB

 

img = cv2.imread('cat.jpg', 0)

 

# 創建 mask

mask = np.zeros(img.shape[:2], np.uint8)

print(mask.shape)  # (414, 500)

mask[100:300, 100:400] = 255

 

masked_img = cv2.bitwise_and(img, img, mask=mask)  # 與操作

 

hist_full = cv2.calcHist([img], [0], None, [256], [0, 256])

hist_mask = cv2.calcHist([img], [0], mask, [256], [0, 256])

 

plt.subplot(221), plt.imshow(img, 'gray'), plt.title('gray image')

plt.subplot(222), plt.imshow(mask, 'gray'), plt.title('mask image')

plt.subplot(223), plt.imshow(masked_img, 'gray'), plt.title('image bitwise and mask')

plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask), plt.title('hist image')

plt.xlim([0, 256])

plt.show()

  效果如下:

  OpenCV有一個函數可以這樣做,cv.equalizeHist(),它封裝好了計算cdf和cdf重映射以及根據cdf表生成直方圖均衡圖像的過程。它的輸入只是灰度圖像,輸出是我們的直方圖均衡圖像。

1

2

3

4

img = cv.imread('cat,jpg', 0)

equ = cv.equalizeHist(img)

res = np.hstack((img, equ)) # 並排疊加圖片

cv.imwrite('res.png', res)

  所以現在你可以用不同的光條件來拍攝不同的圖像,平衡它,並檢查結果。

  當圖像的直方圖被限制在一個特定的區域時,直方圖均衡是很好的。在那些有很大強度變化的地方,直方圖覆蓋了一個大區域,比如明亮的和暗的像素,這樣的地方就不好用了。

  我們可以看看直方圖均衡化之前和之後的直方圖分佈圖。

  補充代碼如下:

1

2

3

plt.hist(img.ravel(), 256)

plt.hist(equ.ravel(), 256)

plt.show()

   展示在一個直方圖,效果如下:

5.2   使用OpenCV繪製自適應直方圖均衡化

   局部直方圖均衡化,即把圖像分成許多小塊(比如按 8*8 作爲一個小塊),那麼對每個小塊進行均衡化。這種方法主要對圖像直方圖不是那麼單一的(比如存在多峯情況)的圖像比較實用。

  直方圖自適應直方圖均衡化的源碼如下:

1

2

3

4

5

6

7

8

9

10

def createCLAHE(clipLimit=None, tileGridSize=None): # real signature unknown; restored from __doc__

    """

    createCLAHE([, clipLimit[, tileGridSize]]) -> retval

    .   @brief Creates a smart pointer to a cv::CLAHE class and initializes it.

    .  

    .   @param clipLimit Threshold for contrast limiting.

    .   @param tileGridSize Size of grid for histogram equalization. Input image will be divided into

    .   equally sized rectangular tiles. tileGridSize defines the number of tiles in row and column.

    """

    pass

  參數說明:

  • clipLimit:顏色對比度的閾值
  • titleGridSize:進行像素均衡化的網格大小,即在多少網格下進行直方圖的均衡化操作

  代碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

#_*_coding:utf-8_*_

import cv2  # opencv讀取的格式是BGR

import numpy as np

import matplotlib.pyplot as plt  # Matplotlib讀取的格式是RGB

 

img = cv2.imread('cat.jpg', 0)

 

equ = cv2.equalizeHist(img)

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))

 

res_clahe = clahe.apply(img)

 

 

res = np.hstack((img, equ, res_clahe))

cv2.imshow('res', res)

cv2.waitKey(0)

cv2.destroyAllWindows()

   效果如下:

  示例1:單通道的灰階圖的直方圖均衡化

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

import cv2 as cv

import numpy as np

from matplotlib import pyplot as plt

 

 

im = cv.imread("test.png", 0)

cv.imshow("before", im)

 

# Histogram Equalization

im2 = cv.equalizeHist(im)

print(im2)

 

cv.imshow("after", im2)

plt.show()

cv.waitKey(0)

  

  示例2:彩圖的直方圖均衡

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

import cv2 as cv

import numpy as np

from matplotlib import pyplot as plt

 

 

im = cv.imread("test.jpg")

cv.imshow("before", im)

 

# split g,b,r

g = im[:,:,0]

b = im[:,:,1]

r = im[:,:,2]

 

 

# Histogram Equalization

r2 = cv.equalizeHist(r)

g2 = cv.equalizeHist(g)

b2 = cv.equalizeHist(b)

 

im2 = im.copy()

im2[:,:,0] = g2

im2[:,:,1] = b2

im2[:,:,2] = r2

 

print(im2)

 

cv.imshow("after", im2)

plt.show()

cv.waitKey(0)

  

  示例3:帶遮罩的直方圖均衡化

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

import cv2 as cv

import numpy as np

from matplotlib import pyplot as plt

 

im = cv.imread("test.png", 0)

cv.imshow("before", im)

mask = cv.imread("test_mask2.png", 0)

cv.imshow("mask", mask)

 

# calculate histogram with mask

hist_mask = cv.calcHist([im], [0], mask, [256], [0,256])

 

# calculate cdf with mask

cdf = hist_mask.cumsum()

 

# Histogram Equalization

cdf = (cdf-cdf[0])*255/(cdf[-1]-1)

cdf = cdf.astype(np.uint8)# Transform from float64 back to unit8

 

# generate img after Histogram Equalization

im2 = np.zeros((384, 495, 1), dtype =np.uint8)

im2 = cdf[im]

 

# im2 = cv.equalizeHist(im)

print(im2)

 

cv.imshow("after", im2)

plt.show()

cv.waitKey(0)

  

5.2,使用Numpy進行直方圖均衡化

  計算累積和的 cumsun()函數

1

numpy.cumsum(a, axis=None, dtype=None, out=None)

  這個函數的功能是返回給定axis上的累積和

1

2

3

4

>>>import numpy as np 

>>> b=[1,2,3,4,5,6,7] 

>>> np.cumsum(a) 

array([  1,   3,   6,  10,  15,  21,  28,  36,  45,  55,  75, 105])

  直方圖均衡化

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

import numpy as np

import cv2 as cv

from matplotlib import pyplot as plt

 

img = cv.imread('wiki.jpg', 0)

 

hist, bins = np.histogram(img.flatten(), 256, [0,256])

 

cdf = hist.cumsum()

cdf_normalized = cdf*float(hist.max())/cdf.max()

 

plt.plot(cdf_normalized, color = 'b')

plt.hist(img.flatten(),256,[0,256], color = 'r')

plt.xlim([0,256])

plt.legend(('cdf','histogram'), loc = 'upper left')

plt.show()

  你可以看到直方圖位於更亮的區域。我們需要讓它充滿整個頻譜。爲此,我們需要一個轉換函數,它將更亮區域的輸入像素映射到全區域的輸出像素。這就是直方圖均衡所做的。

  現在我們找到了最小的直方圖值(不包括0),並應用了在wiki頁面中給出的直方圖均衡等式。

1

2

cdf = (cdf-cdf[0]) *255/ (cdf[-1]-1)

cdf = cdf.astype(np.uint8)

  現在我們有了一個查找表,它提供了關於每個輸入像素值的輸出像素值的信息。所以我們只要應用變換。

1

img2 = cdf[img]

  另一個重要的特徵是,即使圖像是一個較暗的圖像(而不是我們使用的更亮的圖像),在均衡之後,我們將得到幾乎相同的圖像。因此,它被用作一種「參考工具」,使所有的圖像都具有相同的光照條件。這在很多情況下都很有用。例如,在人臉識別中,在對人臉數據進行訓練之前,人臉的圖像是均勻的,使它們具有相同的光照條件。

 

  示例1:單通道的灰階圖的直方圖均衡化

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

import cv2 as cv

import numpy as np

from matplotlib import pyplot as plt

 

img = cv.imread("test.png", 0)

cv.imshow("before", img)

 

# calculate hist

hist, bins = np.histogram(img, 256)

# calculate cdf

cdf = hist.cumsum()

# plot hist

plt.plot(hist,'r')

 

# remap cdf to [0,255]

cdf = (cdf-cdf[0])*255/(cdf[-1]-1)

cdf = cdf.astype(np.uint8)# Transform from float64 back to unit8

 

# generate img after Histogram Equalization

img2 = np.zeros((384, 495, 1), dtype =np.uint8)

img2 = cdf[img]

 

hist2, bins2 = np.histogram(img2, 256)

cdf2 = hist2.cumsum()

plt.plot(hist2, 'g')

 

cv.imshow("after", img2)

plt.show()

cv.waitKey(0)

  我們可以看到,直方圖均衡化後的圖像對比度增強了。

  示例2:彩圖的直方圖均衡化

  首先是分離通道,對比三個通道分別進行處理,合併三通道顏色到圖片

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

import cv2 as cv

import numpy as np

from matplotlib import pyplot as plt

 

 

img = cv.imread("test.png")

cv.imshow("before", img)

 

# split g,b,r

g = img[:,:,0]

b = img[:,:,1]

r = img[:,:,2]

 

# calculate hist

hist_r, bins_r = np.histogram(r, 256)

hist_g, bins_g = np.histogram(g, 256)

hist_b, bins_b = np.histogram(b, 256)

 

# calculate cdf

cdf_r = hist_r.cumsum()

cdf_g = hist_g.cumsum()

cdf_b = hist_b.cumsum()

 

# remap cdf to [0,255]

cdf_r = (cdf_r-cdf_r[0])*255/(cdf_r[-1]-1)

cdf_r = cdf_r.astype(np.uint8)# Transform from float64 back to unit8

cdf_g = (cdf_g-cdf_g[0])*255/(cdf_g[-1]-1)

cdf_g = cdf_g.astype(np.uint8)# Transform from float64 back to unit8

cdf_b = (cdf_b-cdf_b[0])*255/(cdf_b[-1]-1)

cdf_b = cdf_b.astype(np.uint8)# Transform from float64 back to unit8

 

# get pixel by cdf table

r2 = cdf_r[r]

g2 = cdf_g[g]

b2 = cdf_b[b]

 

# merge g,b,r channel

img2 = img.copy()

img2[:,:,0] = g2

img2[:,:,1] = b2

img2[:,:,2] = r2

 

# show img after histogram equalization

cv.imshow("img2", img2)

 

cv.waitKey(0)

  

  示例三:帶遮罩的直方圖均衡化

  如果想要在做直方圖均衡化的時候不考慮圖像的某一部分,比如我們不想考慮圖片右上角的雲彩,那麼可以使用遮罩在計算hist和cdf時不考慮這一部分像素。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

import cv2 as cv

import numpy as np

from matplotlib import pyplot as plt

 

 

img = cv.imread("test.png", 0)

cv.imshow("src", img)

 

# load mask img

mask = cv.imread("test_mask2.png", 0)

cv.imshow("mask", mask)

 

# apply mask to src

masked_img = np.ma.masked_array(img, mask = mask)

masked_img = np.ma.filled(masked_img,0).astype('uint8')

# print(masked_img)

masked_img = np.ma.masked_equal(masked_img,0)

# print(masked_img)

cv.imshow("masked_img", masked_img)

 

 

# calculate hist

hist, bins = np.histogram(masked_img.compressed(), 256) # img have to be compressed() to let mask work

# calculate cdf

cdf = hist.cumsum()

 

print(cdf)

# plot hist

plt.plot(hist,'r')

 

# remap cdf to [0,255]

cdf = (cdf-cdf[0])*255/(cdf[-1]-1)

cdf = cdf.astype(np.uint8)# Transform from float64 back to unit8

 

# generate img after Histogram Equalization

img2 = np.zeros((384, 495, 1), dtype =np.uint8)

img2 = cdf[img]

 

hist2, bins2 = np.histogram(img2, 256)

cdf2 = hist2.cumsum()

plt.plot(hist2, 'g')

 

cv.imshow("dst", img2)

plt.show()

cv.waitKey(0)

  

6,使用查找表(LUT)來拉伸直方圖

  在圖像處理中,直方圖均衡化一般用來均衡圖像的強度,或增加圖像的對比度。在介紹使用直方圖均衡化來拉伸圖像的直方圖之前,先學習使用查詢表的方法。

  直方圖的繪製代碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

import cv2

import numpy as np

import matplotlib.pyplot as plt

 

 

def show_histphoto(photo_path):

    image = cv2.imread(photo_path, 0)

    print(image.shape)

    hist = cv2.calcHist([image], [0], None, [256], [0.0, 256.0])

    # print(hist.shape)

    plt.plot(hist)

 

def Line_chart(photo_path):

    image = cv2.imread(photo_path)

    # 創建用於繪製直方圖的全0 圖像

    h = np.zeros((256, 256, 3))

    # 直方圖中各bin的頂點位置

    bins = np.arange(256).reshape(256, 1)

    # BGR 三種顏色

    color = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]

    for ch, col in enumerate(color):

        originHist = cv2.calcHist([image], [ch], None, [256], [0, 256])

        cv2.normalize(originHist, originHist, 0, 255*0.9, cv2.NORM_MINMAX)

        hist = np.int32(np.around(originHist))

        pts = np.column_stack((bins, hist))

        cv2.polylines(h, [pts], False, col)

 

    h = np.flipud(h)

    cv2.imshow('colorhist', h)

    cv2.waitKey(0)

 

if __name__ == '__main__':

    photo_path = 'test.jpg'

    # show_histphoto(photo_path)

    Line_chart(photo_path)

  (一種藉助matplotlib,代碼簡單,一種使用opencv,需要轉化)

   觀察上圖中原始圖像的直方圖,很容易發現大部分強度值範圍都沒有用到。因此先檢測圖像非0的最低(imin)強度值和最高(imax)強度值。將最低強度值imin設置爲0,最高值 imax 設爲255。中間的按照255.0*(i - imin)/(imax - imin)+ 0.5)的形式設置。

  實現的任務主要集中在查詢表的創建中,代碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

minBinNo, maxBinNo = 0, 255

 

# 計算從左起第一個不爲0的直方圖位置

for binNo, binValue in enumerate(hist):

    if binValue != 0:

        minBinNo = binNo

        break

 

# 計算從右起第一個不爲0的直方圖位置

for binNo, binValue in enumerate(reversed(hist)):

    if binValue != 0:

        maxBinNo = 255 - binNo

        break

 

# 生成查找表

for i, v in enumerate(lut):

    if i < minBinNo:

        lut[i] = 0

    elif i > maxBinNo:

        lut[i] = 255

    else:

        lut[i] = int(255.0*(i-minBinNo)/(maxBinNo-minBinNo)+0.5)

  查詢表創建完成後,就直接調用相應的OpenCV函數,這裏調用的時 cv2.LUT函數:

1

2

#計算

result = cv2.LUT(image, lut)

  cv2.LUT 函數只有兩個參數,分別爲輸入圖像和查找表,其返回處理的結果。

  完整代碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

import cv2

import numpy as np

 

image = cv2.imread('wiki.jpg', 0)

# 創建空的查找表

lut = np.zeros(256, dtype=image.dtype)

# OpenCV提供了cv.calcHist()函數來獲取直方圖

hist = cv2.calcHist([image],  # 計算圖像的直方圖

                    [0],  # 使用的通道

                    None,  # 沒有使用mask

                    [256],  # it is a 2D histogram

                    [0.0, 255.0])

# print(hist.shape)  # (256, 1)

minBinNo, maxBinNo = 0, 255

 

# 計算從左起第一個不爲0的直方圖位置

for binNo, binValue in enumerate(hist):

    if binValue != 0:

        minBinNo = binNo

        break

 

# 計算從右起第一個不爲0的直方圖位置

for binNo, binValue in enumerate(reversed(hist)):

    if binValue != 0:

        maxBinNo = 255 - binNo

        break

 

# 生成查找表

for i, v in enumerate(lut):

    if i < minBinNo:

        lut[i] = 0

    elif i > maxBinNo:

        lut[i] = 255

    else:

        lut[i] = int(255.0 * (i - minBinNo) / (maxBinNo - minBinNo) + 0.5)

 

# 計算

result = cv2.LUT(image, lut)

print(result.shape)   # (534, 800)

cv2.imshow('result', result)

cv2.waitKey(0)

cv2.destroyAllWindows()

  效果如下:

 

7,直方圖均衡化OpenCV實現和Numpy實現的對比

  圖像爲下圖:

7.1  使用OpenCV函數實現

  用OpenCV函數實現直方圖均衡化很簡單,只需要調用一個函數即可:

1

2

3

4

5

6

def opencv_equalizeHist(image_path):

    img = cv2.imread(image_path, 0)

    equ = cv2.equalizeHist(img)

    cv2.imshow('equ', equ)

    cv2.waitKey(0)

    cv2.destroyAllWindows()

  這樣就圖像均衡化了。效果如下:

7.2,使用Numpy函數實現

  通過前面的介紹,可以明白直方圖均衡化就是用一種特殊的查詢表來實現的,所以這裏用Numpy函數,以查找表的方式手動來實現圖像直方圖均衡化:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

def numpy_equalizeHist(image_path):

    img = cv2.imread(image_path, 0)

    # 創建空的查詢表

    lut = np.zeros(256, dtype=img.dtype)

 

    hist, bins = np.histogram(img.flatten(), 256, [0, 256])

    # 計算累計直方圖

    cdf = hist.cumsum()

    # 除以直方圖中的0值

    cdf_m = np.ma.masked_equal(cdf, 0)

    #等同於前面介紹的lut[i] = int(255.0 *p[i])公式

    cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min())

    #將掩模處理掉的元素補爲0

    cdf = np.ma.filled(cdf_m, 0).astype('uint8')

    # 計算

    result2 = cdf[img]

    # result = cv2.LUT(img, cdf)

 

    # cv2.imshow("OpenCVLUT", result)

    cv2.imshow("NumPyLUT", result2)

    cv2.waitKey(0)

    cv2.destroyAllWindows()

  結果如下:

 

7.3,三種方法的直方圖對比

  這裏對比了使用查找表,使用OpenCV,使用numpy直方圖均衡化生成的直方圖:

  (注意:這裏有將三種方法生成的直方圖均衡化的圖片保存下來,然後對此均衡化的圖片畫直方圖)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

import cv2

import numpy as np

import matplotlib.pyplot as plt

 

 

def LookUpTable(photo_path):

    image = cv2.imread(photo_path, 0)

    # 創建空的查找表

    lut = np.zeros(256, dtype=image.dtype)

    # OpenCV提供了cv.calcHist()函數來獲取直方圖

    hist = cv2.calcHist([image],  # 計算圖像的直方圖

                        [0],  # 使用的通道

                        None,  # 沒有使用mask

                        [256],  # it is a 2D histogram

                        [0.0, 255.0])

    # print(hist.shape)  # (256, 1)

    minBinNo, maxBinNo = 0, 255

 

    # 計算從左起第一個不爲0的直方圖位置

    for binNo, binValue in enumerate(hist):

        if binValue != 0:

            minBinNo = binNo

            break

 

    # 計算從右起第一個不爲0的直方圖位置

    for binNo, binValue in enumerate(reversed(hist)):

        if binValue != 0:

            maxBinNo = 255 - binNo

            break

 

    # 生成查找表

    for i, v in enumerate(lut):

        if i < minBinNo:

            lut[i] = 0

        elif i > maxBinNo:

            lut[i] = 255

        else:

            lut[i] = int(255.0 * (i - minBinNo) / (maxBinNo - minBinNo) + 0.5)

 

    # 計算

    lut = cv2.LUT(image, lut)

    cv2.imwrite('lut.jpg', lut)

    # cv2.imshow('lut', lut)

    # cv2.waitKey(0)

    # cv2.destroyAllWindows()

    return lut

 

 

def opencv_equalizeHist(image_path):

    img = cv2.imread(image_path, 0)

    equ = cv2.equalizeHist(img)

    cv2.imwrite('equ.jpg', equ)

    # cv2.imshow('equ', equ)

    # cv2.waitKey(0)

    # cv2.destroyAllWindows()

    return equ

 

 

def numpy_equalizeHist(image_path):

    img = cv2.imread(image_path, 0)

    hist, bins = np.histogram(img.flatten(), 256, [0, 256])

    # 計算累計直方圖

    cdf = hist.cumsum()

    # 除以直方圖中的0值

    cdf_m = np.ma.masked_equal(cdf, 0)

    # 等同於前面介紹的lut[i] = int(255.0 *p[i])公式

    cdf_m = (cdf_m - cdf_m.min()) * 255 / (cdf_m.max() - cdf_m.min())

    # 將掩模處理掉的元素補爲0

    cdf = np.ma.filled(cdf_m, 0).astype('uint8')

    # 計算

    numpy_lut = cdf[img]

    cv2.imwrite('numpy_lut.jpg', numpy_lut)

    # cv2.imshow("NumPyLUT", numpy_lut)

    # cv2.waitKey(0)

    # cv2.destroyAllWindows()

    return numpy_lut

 

 

def show_allphoto():

    lut = cv2.imread('lut.jpg', 0)

    np_equ = cv2.imread('numpy_lut.jpg', 0)

    opencv_equ = cv2.imread('equ.jpg', 0)

    print(lut.shape,  np_equ.shape, opencv_equ.shape)

 

    lut = cv2.calcHist([lut], [0], None, [256], [0.0, 256.0])

    np_equ = cv2.calcHist([np_equ], [0], None, [256], [0.0, 256.0])

    opencv_equ = cv2.calcHist([opencv_equ], [0], None, [256], [0.0, 256.0])

 

    plt.subplot(311), plt.plot(lut)

    plt.subplot(312), plt.plot(np_equ)

    plt.subplot(313), plt.plot(opencv_equ)

    plt.show()

 

 

 

if __name__ == '__main__':

    photo_path = 'wiki.jpg'

    # lut = LookUpTable(photo_path)

    # np_equ = numpy_equalizeHist(photo_path)

    # opencv_equ = opencv_equalizeHist(photo_path)

    show_allphoto()

  結構如下:

   lut計算出來的和opencv和numpy計算的結果還是不太一樣。但是 opencv和numpy計算的結果相似。具體原因不知道,再學習。

 

 

https://blog.csdn.net/sunny2038/article/details/9403059

https://blog.csdn.net/v_xchen_v/article/details/79913245


 

更多精彩內容請訪問FlyAI-AI競賽服務平臺;爲AI開發者提供數據競賽並支持GPU離線訓練的一站式服務平臺;每週免費提供項目開源算法樣例,支持算法能力變現以及快速的迭代算法模型。

挑戰者,都在FlyAI!!!