原文鏈接:http://www.juzicode.com/opencv-python-histogram-calchist-draw-hist
圖像的直方圖反映的是圖像像素值的統計特征,比如一個CV_8U類型的圖像,表示的是其在0~255的256種數值的分布情況。我們可以將統計“顆粒度”划分在每一個像素值上,當然統計區間也可以不必在每一個像素值上划分,也可以將0-255平分成更寬的區間,比如0-7,8-15…..248-255每8個像素值作為一個區間來統計。在直方圖中經常會遇到“bin”的概念,比如一個CV_8U的圖像如果bin的尺寸設置為256,這樣bin的寬度就為1,對應前面例子中在每一個像素值上統計,如果bin的尺寸設置為32,這樣bin的寬度就為256/32=8,這樣就會在每8個像素值上統計。
OpenCV里用calcHist()計算得到的直方圖是一個矩陣(數組),雖然也是是一個二維圖像,但是並不能直接用imshow()顯示,需要經過轉換配合繪制直線等方法將直方圖表示成一幅直觀的圖像,另外也可以借助numpy和matplotlib繪制直方圖。后者接口更簡潔,稍后我們先來看看此方法。
1、matplotlib hist()繪制直方圖
matplotlib中可以使用hist()方法繪制直方圖,其接口形式:
hist(x, bins=None, range=None,......)
- 參數含義:
- x:輸入序列,如果是二維圖像需要展開為一維數組;
- bins:柱子的多少,如果是CV_8U類型的圖像設置為256,表示每個像素值為1個區間;
- range:像素值的閾值范圍,如果不設置會自動計算;
實際上matplotlib里的hist()方法入參有十幾個,這里我們只需要使用上述幾個參數就可以完成繪圖。
下面的例子讀入lena圖,然后分別對其BGR通道進行直方圖的繪制,繪制直方圖時入參x要求為一維數組,所以使用ravel()方法將圖像展開:
import numpy as np
import matplotlib.pyplot as plt
import cv2
plt.rc('font',family='Youyuan',size='9')
plt.rc('axes',unicode_minus='False')
print('VX公眾號: 桔子code / juzicode.com')
img_src = cv2.imread('..\\lena.jpg')
b,g,r = cv2.split(img_src)
#顯示圖像
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('b hist')
ax[0,0].hist(b.ravel(),bins=256)
ax[0,1].set_title('g hist')
ax[0,1].hist(g.ravel(),bins=256)
ax[1,0].set_title('r hist')
ax[1,0].hist(r.ravel(),bins=256)
ax[1,1].set_title('src')
ax[1,1].imshow(cv2.cvtColor(img_src,cv2.COLOR_BGR2RGB))
#ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');
ax[1,1].axis('off')#關閉坐標軸顯示
plt.show()
運行結果:
在hist()方法中,設置bins=256,所以直方圖的x方向的坐標長度為256,這時會統計每種像素值的像素個數。
hist()繪制的直方圖,可以看做是bar()繪制的柱狀圖的一種特例,在直方圖中柱子之間的間隔為0,x方向的坐標用數字代替了,可參考數據可視化~matplotlib餅圖、柱狀圖。
2、計算直方圖calcHist
calcHist()可以用來統計圖像的直方圖,接口形式:
cv2.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]]) ->hist
- 參數含義:
- images:輸入圖像,是一個圖像集合,可以是包含多通道彩色圖像的list或tuple,也可以是多個灰度圖組成的list或者tuple;list或tuple形式的輸入;
- channels:根據images確定,指明要用images里的哪個通道號,根據images的形式確定;list或tuple形式的輸入;
- mask:掩碼;
- histSize:直方圖的尺寸,實際就是元素取值划分的等分;list或tuple形式的輸入;
- ranges:圖像元素取值的范圍;list或tuple形式的輸入;
- accumulate:如果為True表示多個圖像時累積計算像素值個數;
- hist:返回的直方圖數據,是一個二維數組,數組形狀為(histSize決定的行數,1);
下面這個例子對lena圖的BGR通道分別計算直方圖:
import numpy as np
import cv2
print('VX公眾號: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
img_src = cv2.imread('..\\lena.jpg')
b,g,r = cv2.split(img_src)
histSize = 256
histRange = (0, histSize) #統計的范圍和histSize保持一致時可覆蓋所有取值
b_hist = cv2.calcHist([b], [0], None, [histSize], histRange)
g_hist = cv2.calcHist([g], [0], None, [histSize], histRange)
r_hist = cv2.calcHist([r], [0], None, [histSize], histRange)
print('b_hist.shape:',b_hist.shape)
min_max = cv2.minMaxLoc(b_hist)
print('b_hist.minMaxLoc:',min_max)
print('b_hist.非0數:',cv2.countNonZero(b_hist))
for i,v in enumerate(b_hist):
print(v,end=' ')
if (i+1)%16==0:print()
運行結果:
VX公眾號: 桔子code / juzicode.com
cv2.__version__: 4.5.3
b_hist.shape: (256, 1)
b_hist.minMaxLoc: (0.0, 3260.0, (0, 0), (0, 95))
b_hist.非0數: 191
[0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.]
[0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [1.] [0.] [0.] [0.] [1.]
[1.] [0.] [3.] [2.] [6.] [8.] [2.] [12.] [16.] [34.] [42.] [33.] [64.] [74.] [123.] [148.]
[229.] [279.] [333.] [452.] [573.] [740.] [938.] [1137.] [1294.] [1616.] [1779.] [2091.] [2260.] [2464.] [2684.] [2690.]
[2732.] [2793.] [2807.] [2763.] [2782.] [2741.] [2610.] [2649.] [2710.] [2839.] [2981.] [2908.] [3101.] [3091.] [3102.] [3148.]
[3026.] [2967.] [3032.] [2851.] [2872.] [2776.] [2783.] [2818.] [2831.] [2970.] [2929.] [2959.] [3217.] [3209.] [3132.] [3260.]
[3253.] [3117.] [2999.] [2868.] [2785.] [2655.] [2628.] [2558.] [2620.] [2613.] [2614.] [2746.] [2775.] [2751.] [2661.] [2641.]
[2617.] [2591.] [2563.] [2571.] [2601.] [2792.] [2829.] [2862.] [3042.] [3190.] [3250.] [3225.] [3190.] [2933.] [2740.] [2422.]
[2197.] [1949.] [1754.] [1489.] [1302.] [1116.] [1045.] [968.] [848.] [863.] [863.] [883.] [878.] [837.] [848.] [862.]
[846.] [786.] [798.] [801.] [888.] [892.] [868.] [906.] [835.] [858.] [964.] [1018.] [976.] [1019.] [972.] [956.]
[885.] [965.] [948.] [929.] [919.] [821.] [856.] [838.] [777.] [755.] [779.] [741.] [719.] [698.] [618.] [581.]
[619.] [585.] [580.] [583.] [569.] [617.] [584.] [621.] [620.] [625.] [569.] [548.] [460.] [401.] [380.] [359.]
[324.] [267.] [200.] [201.] [134.] [138.] [130.] [125.] [118.] [99.] [115.] [82.] [57.] [58.] [51.] [45.]
[34.] [27.] [27.] [21.] [11.] [6.] [5.] [2.] [6.] [2.] [1.] [0.] [2.] [1.] [1.] [0.]
[0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.]
[0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.] [0.]
從b_hist.shape屬性可以看到,b_hist是一個256行x1列的二維numpy數組,行數等於histSize=256。通過修改histSize的大小,可以看到b_hist.shape的屬性隨着histSize發生變化:
histSize = 156
b_hist = cv2.calcHist([b], [0], None, [histSize], histRange)
print('b_hist.shape:',b_hist.shape)
-----運行結果:
b_hist.shape: (156, 1)
除了前面的例子中,images傳入單個單通道圖像組成的list、channels固定傳入[0]的方式,images還可以使用單個多通道的圖像,channels入參對應其通道號傳入:
img_src = cv2.imread('..\\lena.jpg')
#b,g,r = cv2.split(img_src)
histSize = 256
histRange = (0, histSize) #統計的范圍和histSize保持一致時可覆蓋所有取值
b_hist = cv2.calcHist([img_src], [0], None, [histSize], histRange)
g_hist = cv2.calcHist([img_src], [1], None, [histSize], histRange)
r_hist = cv2.calcHist([img_src], [2], None, [histSize], histRange)
b,g,r = cv2.split(img_src)
b_hist2 = cv2.calcHist([b], [0], None, [histSize], histRange)
g_hist2 = cv2.calcHist([g], [0], None, [histSize], histRange)
r_hist2 = cv2.calcHist([r], [0], None, [histSize], histRange)
print('b_hist差異:',cv2.countNonZero(cv2.absdiff(b_hist,b_hist2)))
print('g_hist差異:',cv2.countNonZero(cv2.absdiff(g_hist,g_hist2)))
print('r_hist差異:',cv2.countNonZero(cv2.absdiff(r_hist,r_hist2)))
運行結果:
b_hist差異: 0
g_hist差異: 0
r_hist差異: 0
從運行結果看,2種方式計算得到的直方圖沒有差異。在這個例子中入參“[img_src], [0], ”對應的是img_src的第0通道,對應了img_src的b通道。
images入參除了包含單個多通道彩色圖像,還可以包含多個多通道彩色圖像,這時channels的入參就會更復雜些,后面圖像的通道號需要根據前面圖像的通道號來疊加考慮,比如傳入一個3通道的img_src1和一個3通道的img_src2:images=[img_src1,img_src2],這時計算img_src1的channels仍然分別取值為[0]、[1]、[2],img_src2的channels就需要在前一個圖像通道的取值基礎上疊加,分別取值為[3]、[4]、[5]。下面的例子來做一個驗證,同時傳入2個相同的3通道圖像,這時其3,4,5通道的直方圖應該要等於0,1,2通道的直方圖:
import numpy as np
import cv2
print('VX公眾號: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
img_src = cv2.imread('..\\lena.jpg')
histSize = 256
histRange = (0, histSize) #統計的范圍和histSize保持一致時可覆蓋所有取值
b_hist = cv2.calcHist([img_src,img_src], [0], None, [histSize], histRange)
g_hist = cv2.calcHist([img_src,img_src], [1], None, (histSize,), histRange)
r_hist = cv2.calcHist((img_src,img_src), [2], None, [histSize], histRange)
#接下來的3,4,5通道號對應第2個輸入圖片的直方圖
b_hist2 = cv2.calcHist((img_src,img_src), [3], None, [histSize], histRange)
g_hist2 = cv2.calcHist((img_src,img_src), [4], None, [histSize], histRange)
r_hist2 = cv2.calcHist((img_src,img_src), [5], None, [histSize], histRange)
print('b_hist差異:',cv2.countNonZero(cv2.absdiff(b_hist,b_hist2)))
print('g_hist差異:',cv2.countNonZero(cv2.absdiff(g_hist,g_hist2)))
print('r_hist差異:',cv2.countNonZero(cv2.absdiff(r_hist,r_hist2)))
運行結果:
b_hist差異: 0
g_hist差異: 0
r_hist差異: 0
以此類推,還可以有多種其他傳入方法:
b,g,r = cv2.split(img_src)
histSize = 256
histRange = (0, histSize) #統計的范圍和histSize保持一致時可覆蓋所有取值
b_hist = cv2.calcHist([img_src,b,g,r], [0], None, [histSize], histRange)
g_hist = cv2.calcHist([img_src,b,g,r], [1], None, (histSize,), histRange)
r_hist = cv2.calcHist((img_src,b,g,r), [2], None, [histSize], histRange)
#接下來的3,4,5通道號對應第2個輸入圖片的直方圖
b_hist2 = cv2.calcHist((img_src,b,g,r), [3], None, [histSize], histRange)
g_hist2 = cv2.calcHist((img_src,b,g,r), [4], None, [histSize], histRange)
r_hist2 = cv2.calcHist((img_src,b,g,r), [5], None, [histSize], histRange)
3、calcHist()計算 matplotlib plot()顯示
前面介紹了matplotlib hist()方法直接顯示直方圖,這里利用calHist()計算出直方圖,得到的是一個數組,該數組的下標表示像素值代表x軸,數組元素的值表示該下標對應的像素值個數代表y軸,所以也可以利用matplotlib的plot()方法繪制直方圖:
import numpy as np
import matplotlib.pyplot as plt
import cv2
print('VX公眾號: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
plt.rc('axes',unicode_minus='False')
img_src = cv2.imread('..\\lena.jpg')
b,g,r = cv2.split(img_src)
histSize = 256
histRange = (0, histSize) 統計的范圍和histSize保持一致時可覆蓋所有取值
b_hist = cv2.calcHist([b], [0], None, [histSize], histRange)
g_hist = cv2.calcHist([g], [0], None, [histSize], histRange)
r_hist = cv2.calcHist([r], [0], None, [histSize], histRange)
#顯示圖像
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('b hist')
ax[0,0].plot(b_hist)
ax[0,1].set_title('g hist')
ax[0,1].plot(g_hist)
ax[1,0].set_title('r hist')
ax[1,0].plot(r_hist)
ax[1,1].set_title('src')
ax[1,1].imshow(cv2.cvtColor(img_src,cv2.COLOR_BGR2RGB))
#ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');
ax[1,1].axis('off')#關閉坐標軸顯示
plt.show()
運行結果:
該方法繪制的直方圖和matplotlib的hist()方法繪制曲線的分布和走勢是一樣的。
4、OpenCV繪圖顯示直方圖
calcHist()計算得到的直方圖名義上是“圖”,但是並不能直接用OpenCV的imshow()顯示,需要做轉換才能顯示,直方圖是一個histSize行x1列的二維數組,其第2維是只包含一個元素的numpy數組,比如取b_hist的第55個元素的值:
print('b_hist[55]:',b_hist[55])
print('int(b_hist[55]):',int(b_hist[55]))
-----運行結果:
b_hist[55]: [122.07056]
int(b_hist[55]): 122
這里用int()方式取整后,直接將numpy數組轉換成了int型。這樣數組下標55就代表了其x軸的取值,取整后的122就代表了y軸的取值。
下面的例子繪制lena圖BGR通道的直方圖,用calcHist()計算完BGR通道的直方圖后,創建一個hist_img_w,hist_img_h = 512,350大小的numpy數組用來保存可視化的直方圖圖像img_hist。BGR通道直方圖數據值歸一化到img_hist的高度,這樣做以免繪圖時超出了圖像邊界。然后以histSize寬度為循環邊界,每次用line()方法繪制hist_img_w/histSize個寬度的直線:
import numpy as np
import cv2
print('VX公眾號: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
img_src = cv2.imread('..\\lena.jpg')
histSize = 256
histRange = (0, histSize)
b_hist = cv2.calcHist([img_src], [0], None, [histSize], histRange)
g_hist = cv2.calcHist([img_src], [1], None, [histSize], histRange)
r_hist = cv2.calcHist([img_src], [2], None, [histSize], histRange)
#創建直方圖空圖像
hist_img_w,hist_img_h = 512,350
img_hist = np.zeros((hist_img_h, hist_img_w, 3), dtype=np.uint8)
#歸一化到0和直方圖顯示的高度
cv2.normalize(b_hist, b_hist, alpha=0, beta=hist_img_h, norm_type=cv2.NORM_MINMAX)
cv2.normalize(g_hist, g_hist, alpha=0, beta=hist_img_h, norm_type=cv2.NORM_MINMAX)
cv2.normalize(r_hist, r_hist, alpha=0, beta=hist_img_h, norm_type=cv2.NORM_MINMAX)
#繪圖,以histSize寬度為循環邊界,每次繪制bin_w個寬度
bin_w = int(round( hist_img_w/histSize ))
print('bin_w',bin_w)
for i in range(1, histSize):
cv2.line(img_hist,
( bin_w*(i-1), hist_img_h - int(b_hist[i-1]) ),#起始點位置
( bin_w*(i) , hist_img_h - int(b_hist[i]) ), #結束點位置
( 255, 0, 0), thickness=2)
cv2.line(img_hist,
( bin_w*(i-1), hist_img_h - int(g_hist[i-1]) ),
( bin_w*(i) , hist_img_h - int(g_hist[i]) ),
( 0, 255, 0), thickness=2)
cv2.line(img_hist,
( bin_w*(i-1), hist_img_h - int(r_hist[i-1]) ),
( bin_w*(i) , hist_img_h - int(r_hist[i]) ),
( 0, 0, 255), thickness=2)
cv2.imshow('img_src', img_src)
cv2.imshow('img_hist', img_hist)
cv2.waitKey()
畫出來的直方圖是這樣的:
5、2D直方圖
2D直方圖仍然使用calcHist()計算,入參形式和一維直方圖類似但稍有差異。
從前面介紹一維直方圖的例子來看,calcHist()使用時channels入參都只有1個元素表明輸入圖像的某一個通道,而計算2D直方圖則需要指明2個通道,並且images參數所表示的圖像必須是多個通道的。同時histSize參數增加到2個,histSize[0]對應channels[0]通道的直方圖尺寸,histSize[1]對應channels[1]通道的直方圖尺寸。histRange參數增加到4個,histRange[0]和[1]對應channels[0]通道的取值范圍,histRange[2]和[3]對應channels[1]通道的取值范圍。下面是一個計算lena圖像HSV色彩空間中H和S分量2D直方圖的例子:
import numpy as np
import cv2
print('VX公眾號: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
img_src = cv2.imread('..\\lena.jpg')
img_hsv = cv2.cvtColor(img_src,cv2.COLOR_BGR2HSV)
img_hist = cv2.calcHist( [img_hsv], [0, 1], None, [180, 256], [0, 180, 0, 256] )
print('img_hist.shape:',img_hist.shape)
#歸一化到255
minmax=cv2.minMaxLoc(img_hist)
img_hist2 = (255*img_hist/minmax[1]).astype(np.uint8)
#顯示
cv2.imshow('img_hist', img_hist)
cv2.imshow('img_hist2', img_hist2)
cv2.waitKey()
在這個例子中channels=[0, 1],取其中的H、S分量計算直方圖;histSize=[180, 256],表示H分量histSize為180,S分量的histSize為256;histRange=[0, 180, 0, 256],H分量的histRange為0~180,S分量的histRange為0~256。
運行結果:
小結:在一維直方圖里x方向表示像素值大小的取值,y方向表示的是該像素值有多少的取值(包含像素值的量的多少),在x軸的左側表示更暗的像素的多少,x軸的右側表示更亮的像素的多少。除了通常意義上表示的亮度(灰度級),如果將圖像轉換為HSV色彩空間,也可以用來表示飽和度、色度的直方圖。二維直方圖的調用形式在入參上和一維直方圖類似,二維直方圖可以直接使用imshow()方法顯示,